From 2b82a1065fd8cf22e6af5f70822298b3b0b56d9c Mon Sep 17 00:00:00 2001 From: CWX Date: Fri, 17 Jan 2025 23:02:12 +0000 Subject: [PATCH] partial implement --- .../RepeatableQuestRewardGenerator.cs | 314 +++++++++++++++++- 1 file changed, 312 insertions(+), 2 deletions(-) diff --git a/Core/Generators/RepeatableQuestRewardGenerator.cs b/Core/Generators/RepeatableQuestRewardGenerator.cs index f0e70051..1f951742 100644 --- a/Core/Generators/RepeatableQuestRewardGenerator.cs +++ b/Core/Generators/RepeatableQuestRewardGenerator.cs @@ -1,18 +1,328 @@ using Core.Annotations; +using Core.Helpers; using Core.Models.Eft.Common.Tables; +using Core.Models.Enums; using Core.Models.Spt.Config; +using Core.Models.Spt.Repeatable; +using Core.Models.Utils; +using Core.Servers; +using Core.Services; +using Core.Utils; +using Core.Utils.Cloners; namespace Core.Generators { - [Injectable] public class RepeatableQuestRewardGenerator { - public QuestRewards GenerateReward(int pmcLevel, double min, string traderId, RepeatableQuestConfig repeatableConfig, EliminationConfig eliminationConfig) + #region Ctor & Params + + private readonly ISptLogger _logger; + private readonly RandomUtil _randomUtil; + private readonly HashUtil _hashUtil; + private readonly MathUtil _mathUtil; + private readonly DatabaseService _databaseService; + private readonly ItemHelper _itemHelper; + private readonly PresetHelper _presetHelper; + private readonly HandbookHelper _handbookHelper; + private readonly LocalisationService _localisationService; + private readonly ItemFilterService _itemFilterService; + private readonly SeasonalEventService _seasonalEventService; + private readonly ConfigServer _configServer; + private readonly ICloner _cloner; + private readonly QuestConfig _questConfig; + + public RepeatableQuestRewardGenerator + ( + ISptLogger logger, + RandomUtil randomUtil, + HashUtil hashUtil, + MathUtil mathUtil, + DatabaseService databaseService, + ItemHelper itemHelper, + PresetHelper presetHelper, + HandbookHelper handbookHelper, + LocalisationService localisationService, + ItemFilterService itemFilterService, + SeasonalEventService seasonalEventService, + ConfigServer configServer, + ICloner cloner + ) + { + _logger = logger; + _randomUtil = randomUtil; + _hashUtil = hashUtil; + _mathUtil = mathUtil; + _databaseService = databaseService; + _itemHelper = itemHelper; + _presetHelper = presetHelper; + _handbookHelper = handbookHelper; + _localisationService = localisationService; + _itemFilterService = itemFilterService; + _seasonalEventService = seasonalEventService; + _configServer = configServer; + _cloner = cloner; + + _questConfig = _configServer.GetConfig(); + } + + #endregion + + /** + * Generate the reward for a mission. A reward can consist of: + * - Experience + * - Money + * - GP coins + * - Weapon preset + * - Items + * - Trader Reputation + * - Skill level experience + * + * The reward is dependent on the player level as given by the wiki. The exact mapping of pmcLevel to + * experience / money / items / trader reputation can be defined in QuestConfig.js + * + * There's also a random variation of the reward the spread of which can be also defined in the config + * + * Additionally, a scaling factor w.r.t. quest difficulty going from 0.2...1 can be used + * @param pmcLevel Level of player reward is being generated for + * @param difficulty Reward scaling factor from 0.2 to 1 + * @param traderId Trader reward will be given by + * @param repeatableConfig Config for quest type (daily, weekly) + * @param questConfig + * @param rewardTplBlacklist OPTIONAL: list of tpls to NOT use when picking a reward + * @returns IQuestRewards + */ + public QuestRewards GenerateReward(int pmcLevel, double difficulty, string traderId, RepeatableQuestConfig repeatableConfig, + EliminationConfig eliminationConfig, BaseQuestConfig baseQuestConfig, List? rewardTplBlacklist = null) + { + // Get vars to configure rewards with + var rewardParams = GetQuestRewardValues(repeatableConfig.RewardScaling, difficulty, pmcLevel); + + // Get budget to spend on item rewards (copy of raw roubles given) + var itemRewardBudget = rewardParams.RewardRoubles; + + // Possible improvement -> draw trader-specific items e.g. with this.itemHelper.isOfBaseclass(val._id, ItemHelper.BASECLASS.FoodDrink) + QuestRewards rewards = new() { Started = [], Success = [], Fail = [] }; + + // Start reward index to keep track + var rewardIndex = -1; + + // Add xp reward + if (rewardParams.RewardXP > 0) + { + rewards.Success.Add( + new() + { + Id = _hashUtil.Generate(), + Unknown = false, + GameMode = [], + AvailableInGameEditions = [], + Index = rewardIndex, + Value = rewardParams.RewardXP, + Type = RewardType.Experience + } + ); + rewardIndex++; + } + + // Add money reward + rewards.Success.Add(GetMoneyReward(traderId, rewardParams.RewardRoubles, rewardIndex)); + rewardIndex++; + + // Add GP coin reward + rewards.Success.Add(GenerateItemReward(Money.GP, rewardParams.GpCoinRewardCount, rewardIndex)); + rewardIndex++; + + // Add preset weapon to reward if checks pass + var traderWhitelistDetails = repeatableConfig.TraderWhitelist.FirstOrDefault( + (traderWhitelist) => traderWhitelist.TraderId == traderId + ); + if (traderWhitelistDetails?.RewardCanBeWeapon ?? false && _randomUtil.GetChance100(traderWhitelistDetails.WeaponRewardChancePercent ?? 0) + ) + { + var chosenWeapon = GetRandomWeaponPresetWithinBudget(itemRewardBudget, rewardIndex); + if (chosenWeapon is not null) + { + rewards.Success.Add(chosenWeapon.Value.Key); + + // Subtract price of preset from item budget so we dont give player too much stuff + itemRewardBudget -= (int)chosenWeapon.Value.Value; + rewardIndex++; + } + } + + var inBudgetRewardItemPool = ChooseRewardItemsWithinBudget(repeatableConfig, itemRewardBudget, traderId); + if (rewardTplBlacklist is not null) + { + // Filter reward pool of items from blacklist, only use if there's at least 1 item remaining + var filteredRewardItemPool = inBudgetRewardItemPool.Where( + (item) => !rewardTplBlacklist.Contains(item.Id) + ); + if (filteredRewardItemPool.Count() > 0) + { + inBudgetRewardItemPool = filteredRewardItemPool.ToList(); + } + } + + + _logger.Debug( + $"Generating: {repeatableConfig.Name} quest for: {traderId} with budget: {itemRewardBudget} totalling: {rewardParams.RewardNumItems} items" + ); + if (inBudgetRewardItemPool.Count > 0) + { + var itemsToReward = GetRewardableItemsFromPoolWithinBudget( + inBudgetRewardItemPool, + rewardParams.RewardNumItems, + itemRewardBudget, + repeatableConfig + ); + + // Add item rewards + foreach (var itemReward in itemsToReward) + { + rewards.Success.Add(GenerateItemReward(itemReward.Key.Id, itemReward.Value, rewardIndex)); + rewardIndex++; + } + } + + // Add rep reward to rewards array + if (rewardParams.RewardReputation > 0) + { + Reward reward = new() + { + Id = _hashUtil.Generate(), + Unknown = false, + GameMode = [], + AvailableInGameEditions = [], + Target = traderId, + Value = rewardParams.RewardReputation, + Type = RewardType.TraderStanding, + Index = rewardIndex + }; + rewards.Success.Add(reward); + rewardIndex++; + + _logger.Debug($"Adding: {rewardParams.RewardReputation} {traderId} trader reputation reward"); + } + + // Chance of adding skill reward + if (_randomUtil.GetChance100((double)rewardParams.SkillRewardChance * 100)) + { + var targetSkill = _randomUtil.GetArrayValue(baseQuestConfig.PossibleSkillRewards); + Reward reward = new() + { + Id = _hashUtil.Generate(), + Unknown = false, + GameMode = [], + AvailableInGameEditions = [], + Target = targetSkill, + Value = rewardParams.SkillPointReward, + Type = RewardType.Skill, + Index = rewardIndex + }; + rewards.Success.Add(reward); + + _logger.Debug($"Adding {rewardParams.SkillPointReward} skill points to {targetSkill}"); + } + + return rewards; + } + + private QuestRewardValues GetQuestRewardValues(RewardScaling? rewardScaling, double? difficulty, int pmcLevel) + { + // difficulty could go from 0.2 ... -> for lowest difficulty receive 0.2*nominal reward + var levelsConfig = rewardScaling.Levels; + var roublesConfig = rewardScaling.Roubles; + var gpCoinConfig = rewardScaling.GpCoins; + var xpConfig = rewardScaling.Experience; + var itemsConfig = rewardScaling.Items; + var rewardSpreadConfig = rewardScaling.RewardSpread; + var skillRewardChanceConfig = rewardScaling.SkillRewardChance; + var skillPointRewardConfig = rewardScaling.SkillPointReward; + var reputationConfig = rewardScaling.Reputation; + + var effectiveDifficulty = difficulty is null ? 1 : difficulty; + if (difficulty is null) + { + _logger.Warning(_localisationService.GetText("repeatable-difficulty_was_nan")); + } + + return new () { + SkillPointReward = _mathUtil.Interp1(pmcLevel, levelsConfig, skillPointRewardConfig), + SkillRewardChance = _mathUtil.Interp1(pmcLevel, levelsConfig, skillRewardChanceConfig), + RewardReputation = GetRewardRep(effectiveDifficulty, pmcLevel, levelsConfig, reputationConfig, rewardSpreadConfig), + RewardNumItems = GetRewardNumItems(pmcLevel, levelsConfig, itemsConfig), + RewardRoubles = GetRewardRoubles(effectiveDifficulty, pmcLevel, levelsConfig, roublesConfig, rewardSpreadConfig), + GpCoinRewardCount = GetGpCoinRewardCount(effectiveDifficulty, pmcLevel, levelsConfig, gpCoinConfig, rewardSpreadConfig), + RewardXP = GetRewardXp(effectiveDifficulty, pmcLevel, levelsConfig, xpConfig, rewardSpreadConfig), + }; + } + + private double GetRewardXp(double? effectiveDifficulty, int pmcLevel, List? levelsConfig, List? xpConfig, double? rewardSpreadConfig) + { + return Math.Floor((effectiveDifficulty + * _mathUtil.Interp1(pmcLevel, levelsConfig, xpConfig) + * _randomUtil.GetFloat((float)(1 - rewardSpreadConfig), (float)(1 + rewardSpreadConfig))) + ?? 0); + } + + private double GetGpCoinRewardCount(double? effectiveDifficulty, int pmcLevel, List? levelsConfig, List? gpCoinConfig, + double? rewardSpreadConfig) + { + return Math.Ceiling((effectiveDifficulty + * _mathUtil.Interp1(pmcLevel, levelsConfig, gpCoinConfig) + * _randomUtil.GetFloat((float)(1 - rewardSpreadConfig), (float)(1 + rewardSpreadConfig))) + ?? 0); + } + + private double GetRewardRep(double? effectiveDifficulty, int pmcLevel, List? levelsConfig, List? reputationConfig, double? rewardSpreadConfig) + { + return Math.Round(100 * effectiveDifficulty + * _mathUtil.Interp1(pmcLevel, levelsConfig, reputationConfig) + * _randomUtil.GetFloat((float)(1 - rewardSpreadConfig), (float)(1 + rewardSpreadConfig)) + ?? 0) / 100; + } + + private double GetRewardNumItems(int pmcLevel, List? levelsConfig, List? itemsConfig) + { + return _randomUtil.RandInt(1, (int)Math.Round(_mathUtil.Interp1(pmcLevel, levelsConfig, itemsConfig) ?? 0) + 1); + } + + private double GetRewardRoubles(double? effectiveDifficulty, int pmcLevel, List? levelsConfig, List? roublesConfig, double? rewardSpreadConfig) + { + return Math.Floor((effectiveDifficulty + * _mathUtil.Interp1(pmcLevel, levelsConfig, roublesConfig) + * _randomUtil.GetFloat((float)(1 - rewardSpreadConfig), (float)(1 + rewardSpreadConfig))) + ?? 0); + } + + private List> GetRewardableItemsFromPoolWithinBudget(List inBudgetRewardItemPool, + object rewardNumItems, double? itemRewardBudget, RepeatableQuestConfig repeatableConfig) { throw new NotImplementedException(); } + private List ChooseRewardItemsWithinBudget(RepeatableQuestConfig repeatableConfig, double? itemRewardBudget, string traderId) + { + throw new NotImplementedException(); + } + + private KeyValuePair? GetRandomWeaponPresetWithinBudget(double? itemRewardBudget, double rewardIndex) + { + throw new NotImplementedException(); + } + + private Reward GenerateItemReward(string d235b4d86f7742e017bc88a, object gpCoinRewardCount, double rewardIndex) + { + throw new NotImplementedException(); + } + + private Reward GetMoneyReward(string traderId, object rewardRoubles, double rewardIndex) + { + throw new NotImplementedException(); + } + + public Dictionary> GetRewardableItems(RepeatableQuestConfig repeatableConfig, string traderId) { throw new NotImplementedException();