diff --git a/Libraries/Core/Generators/RepeatableQuestGenerator.cs b/Libraries/Core/Generators/RepeatableQuestGenerator.cs index 98d75b14..43da0cd1 100644 --- a/Libraries/Core/Generators/RepeatableQuestGenerator.cs +++ b/Libraries/Core/Generators/RepeatableQuestGenerator.cs @@ -497,7 +497,7 @@ public class RepeatableQuestGenerator( (double)(_mathUtil.Interp1(pmcLevel, levelsConfig, roublesConfig) * multi)); roublesBudget = Math.Max(roublesBudget, 5000d); var itemSelection = possibleItemsToRetrievePool.Where( - (x) => _itemHelper.GetItemPrice(x.Key) < roublesBudget).ToList(); + (x) => _itemHelper.GetItemPrice(x.Id) < roublesBudget).ToList(); // We also have the option to use whitelist and/or blacklist which is defined in repeatableQuests.json as // [{"minPlayerLevel": 1, "itemIds": ["id1",...]}, {"minPlayerLevel": 15, "itemIds": ["id3",...]}] @@ -511,8 +511,8 @@ public class RepeatableQuestGenerator( itemSelection = itemSelection.Where((x) => { // Whitelist can contain item tpls and item base type ids return ( - itemIdsWhitelisted.Any((v) => _itemHelper.IsOfBaseclass(x.Key, v)) || - itemIdsWhitelisted.Contains(x.Key) + itemIdsWhitelisted.Any((v) => _itemHelper.IsOfBaseclass(x.Id, v)) || + itemIdsWhitelisted.Contains(x.Id) ); }).ToList(); // check if items are missing @@ -530,8 +530,8 @@ public class RepeatableQuestGenerator( itemSelection = itemSelection.Where((x) => { return ( - itemIdsBlacklisted.All((v) => !_itemHelper.IsOfBaseclass(x.Key, v)) || - !itemIdsBlacklisted.Contains(x.Key) + itemIdsBlacklisted.All((v) => !_itemHelper.IsOfBaseclass(x.Id, v)) || + !itemIdsBlacklisted.Contains(x.Id) ); }).ToList(); } @@ -572,10 +572,10 @@ public class RepeatableQuestGenerator( usedItemIndexes.Add(chosenItemIndex); var itemSelected = itemSelection[chosenItemIndex]; - var itemUnitPrice = _itemHelper.GetItemPrice(itemSelected.Key).Value; + var itemUnitPrice = _itemHelper.GetItemPrice(itemSelected.Id).Value; var minValue = (double)completionConfig.MinimumRequestedAmount.Value; var maxValue = (double)completionConfig.MaximumRequestedAmount.Value; - if (_itemHelper.IsOfBaseclass(itemSelected.Key, BaseClasses.AMMO)) { + if (_itemHelper.IsOfBaseclass(itemSelected.Id, BaseClasses.AMMO)) { // Prevent multiple ammo requirements from being picked if (isAmmo > 0 && isAmmo < _maxRandomNumberAttempts) { isAmmo++; @@ -600,12 +600,12 @@ public class RepeatableQuestGenerator( roublesBudget -= value * itemUnitPrice; // Push a CompletionCondition with the item and the amount of the item - chosenRequirementItemsTpls.Add(itemSelected.Key); - quest.Conditions.AvailableForFinish.Add(GenerateCompletionAvailableForFinish(itemSelected.Key, value)); + chosenRequirementItemsTpls.Add(itemSelected.Id); + quest.Conditions.AvailableForFinish.Add(GenerateCompletionAvailableForFinish(itemSelected.Id, value)); if (roublesBudget > 0) { // Reduce the list possible items to fulfill the new budget constraint - itemSelection = itemSelection.Where((dbItem) => _itemHelper.GetItemPrice(dbItem.Key) < roublesBudget).ToList(); + itemSelection = itemSelection.Where((dbItem) => _itemHelper.GetItemPrice(dbItem.Id) < roublesBudget).ToList(); if (!itemSelection.Any()) { break; } @@ -805,7 +805,7 @@ public class RepeatableQuestGenerator( string side, string sessionId) { - Quest questData = null; + RepeatableQuest questData = null; switch (type) { case "Elimination": @@ -889,6 +889,6 @@ public class RepeatableQuestGenerator( questClone.QuestStatus.Uid = sessionId; // Needs to match user id questClone.QuestStatus.QId = questClone.Id; // Needs to match quest id - return (RepeatableQuest)questClone; + return questClone; } } diff --git a/Libraries/Core/Generators/RepeatableQuestRewardGenerator.cs b/Libraries/Core/Generators/RepeatableQuestRewardGenerator.cs index cd865d11..b3f764e3 100644 --- a/Libraries/Core/Generators/RepeatableQuestRewardGenerator.cs +++ b/Libraries/Core/Generators/RepeatableQuestRewardGenerator.cs @@ -1,6 +1,7 @@ using SptCommon.Annotations; using Core.Helpers; using Core.Models.Eft.Common.Tables; +using Core.Models.Eft.Player; using Core.Models.Enums; using Core.Models.Spt.Config; using Core.Models.Spt.Repeatable; @@ -9,6 +10,7 @@ using Core.Servers; using Core.Services; using Core.Utils; using Core.Utils.Cloners; +using System.Linq; namespace Core.Generators; @@ -69,7 +71,7 @@ public class RepeatableQuestRewardGenerator( // 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) + // Possible improvement -> draw trader-specific items e.g. with _itemHelper.isOfBaseclass(val._id, ItemHelper.BASECLASS.FoodDrink) QuestRewards rewards = new() { Started = [], Success = [], Fail = [] }; // Start reward index to keep track @@ -94,11 +96,11 @@ public class RepeatableQuestRewardGenerator( } // Add money reward - rewards.Success.Add(GetMoneyReward(traderId, rewardParams.RewardRoubles, rewardIndex)); + rewards.Success.Add(GetMoneyReward(traderId, rewardParams.RewardRoubles.Value, rewardIndex)); rewardIndex++; // Add GP coin reward - rewards.Success.Add(GenerateItemReward(Money.GP, rewardParams.GpCoinRewardCount, rewardIndex)); + rewards.Success.Add(GenerateItemReward(Money.GP, rewardParams.GpCoinRewardCount.Value, rewardIndex)); rewardIndex++; // Add preset weapon to reward if checks pass @@ -283,9 +285,38 @@ public class RepeatableQuestRewardGenerator( throw new NotImplementedException(); } - private List ChooseRewardItemsWithinBudget(RepeatableQuestConfig repeatableConfig, double? itemRewardBudget, string traderId) + private List ChooseRewardItemsWithinBudget(RepeatableQuestConfig repeatableConfig, double? roublesBudget, string traderId) { - throw new NotImplementedException(); + // First filter for type and baseclass to avoid lookup in handbook for non-available items + var rewardableItemPool = GetRewardableItems(repeatableConfig, traderId); + var minPrice = Math.Min(25000, 0.5 * roublesBudget.Value); + + var rewardableItemPoolWithinBudget = FilterRewardPoolWithinBudget( + rewardableItemPool, + roublesBudget.Value, + minPrice); + + if (rewardableItemPoolWithinBudget.Count == 0) + { + _logger.Warning(_localisationService.GetText("repeatable-no_reward_item_found_in_price_range", new { + minPrice = minPrice, + roublesBudget = roublesBudget })); + + // In case we don't find any items in the price range + rewardableItemPoolWithinBudget = rewardableItemPool + .Where((x) => _itemHelper.GetItemPrice(x.Id) < roublesBudget) + .ToList(); + } + + return rewardableItemPoolWithinBudget; + } + + private List FilterRewardPoolWithinBudget(List rewardItems, double roublesBudget, double minPrice) + { + return rewardItems.Where((item) => { + var itemPrice = _presetHelper.GetDefaultPresetOrItemPrice(item.Id); + return itemPrice < roublesBudget && itemPrice > minPrice; + }).ToList(); } private KeyValuePair? GetRandomWeaponPresetWithinBudget(double? itemRewardBudget, double rewardIndex) @@ -293,19 +324,109 @@ public class RepeatableQuestRewardGenerator( throw new NotImplementedException(); } - private Reward GenerateItemReward(string d235b4d86f7742e017bc88a, object gpCoinRewardCount, double rewardIndex) + private Reward GenerateItemReward(string tpl, double count, int index, bool foundInRaid = true) { - throw new NotImplementedException(); + var id = _hashUtil.Generate(); + var questRewardItem = new Reward{ + Id = _hashUtil.Generate(), + Unknown = false, + GameMode = [], + AvailableInGameEditions = [], + Index = index, + Target = id, + Value = count, + IsEncoded = false, + FindInRaid = foundInRaid, + Type = RewardType.Item, + Items = [], + }; + + var rootItem = new Item { Id = id, Template = tpl, Upd = new Upd { StackObjectsCount = count, SpawnedInSession = foundInRaid } + }; + questRewardItem.Items = [rootItem]; + + return questRewardItem; } - private Reward GetMoneyReward(string traderId, object rewardRoubles, double rewardIndex) + private Reward GetMoneyReward(string traderId, double rewardRoubles, int rewardIndex) { - throw new NotImplementedException(); + // Determine currency based on trader + // PK and Fence use Euros, everyone else is Roubles + var currency = traderId is Traders.PEACEKEEPER or Traders.FENCE ? Money.EUROS : Money.ROUBLES; + + // Convert reward amount to Euros if necessary + var rewardAmountToGivePlayer = + currency == Money.EUROS ? _handbookHelper.FromRUB(rewardRoubles, Money.EUROS) : rewardRoubles; + + // Get chosen currency + amount and return + return GenerateItemReward(currency, rewardAmountToGivePlayer, rewardIndex, false); } - public Dictionary> GetRewardableItems(RepeatableQuestConfig repeatableConfig, string traderId) + public List GetRewardableItems(RepeatableQuestConfig repeatableQuestConfig, string traderId) { - throw new NotImplementedException(); + // Get an array of seasonal items that should not be shown right now as seasonal event is not active + var seasonalItems = _seasonalEventService.GetInactiveSeasonalEventItems(); + + // Check for specific baseclasses which don't make sense as reward item + // also check if the price is greater than 0; there are some items whose price can not be found + // those are not in the game yet (e.g. AGS grenade launcher) + return _databaseService.GetItems().Values.Where((itemTemplate => { + // Base "Item" item has no parent, ignore it + if (itemTemplate.Parent == "") + { + return false; + } + + if (seasonalItems.Contains(itemTemplate.Id)) + { + return false; + } + + var traderWhitelist = repeatableQuestConfig.TraderWhitelist.FirstOrDefault( + (trader) => trader.TraderId == traderId); + + return IsValidRewardItem(itemTemplate.Id, repeatableQuestConfig, traderWhitelist?.RewardBaseWhitelist); + })).ToList(); + } + + private bool IsValidRewardItem(string tpl, RepeatableQuestConfig repeatableQuestConfig, List? itemBaseWhitelist = null) + { + // Return early if not valid item to give as reward + if (!_itemHelper.isValidItem(tpl)) + { + return false; + } + + // Check item is not blacklisted + if ( + _itemFilterService.IsItemBlacklisted(tpl) || + _itemFilterService.IsItemRewardBlacklisted(tpl) || + repeatableQuestConfig.RewardBlacklist.Contains(tpl) || + _itemFilterService.IsItemBlacklisted(tpl) + ) + { + return false; + } + + // Item has blacklisted base types + if (_itemHelper.IsOfBaseclasses(tpl, repeatableQuestConfig.RewardBaseTypeBlacklist )) + { + return false; + } + + // Skip boss items + if (_itemFilterService.IsBossItem(tpl)) + { + return false; + } + + // Trader has specific item base types they can give as rewards to player + if (itemBaseWhitelist is not null && !_itemHelper.IsOfBaseclasses(tpl, itemBaseWhitelist)) + { + return false; + } + + return true; } } diff --git a/Libraries/Core/Helpers/PresetHelper.cs b/Libraries/Core/Helpers/PresetHelper.cs index d7887379..7ba243c2 100644 --- a/Libraries/Core/Helpers/PresetHelper.cs +++ b/Libraries/Core/Helpers/PresetHelper.cs @@ -1,4 +1,4 @@ -using SptCommon.Annotations; +using SptCommon.Annotations; using Core.Models.Eft.Common; using Core.Models.Enums; using Core.Services; @@ -171,7 +171,7 @@ public class PresetHelper( * @param tpl The item template to get the price of * @returns The price of the given item preset, or base item if no preset exists */ - public decimal GetDefaultPresetOrItemPrice(string tpl) + public double GetDefaultPresetOrItemPrice(string tpl) { // Get default preset if it exists var defaultPreset = GetDefaultPreset(tpl); diff --git a/Libraries/Core/Models/Eft/Common/Tables/RepeatableQuests.cs b/Libraries/Core/Models/Eft/Common/Tables/RepeatableQuests.cs index 0efde926..33b30934 100644 --- a/Libraries/Core/Models/Eft/Common/Tables/RepeatableQuests.cs +++ b/Libraries/Core/Models/Eft/Common/Tables/RepeatableQuests.cs @@ -71,16 +71,16 @@ public record RepeatableQuestStatus public record RepeatableTemplates { [JsonPropertyName("Elimination")] - public Quest? Elimination { get; set; } + public RepeatableQuest? Elimination { get; set; } [JsonPropertyName("Completion")] - public Quest? Completion { get; set; } + public RepeatableQuest? Completion { get; set; } [JsonPropertyName("Exploration")] - public Quest? Exploration { get; set; } + public RepeatableQuest? Exploration { get; set; } [JsonPropertyName("Pickup")] - public Quest? Pickup { get; set; } + public RepeatableQuest? Pickup { get; set; } } public record PmcDataRepeatableQuest diff --git a/Libraries/Core/Services/ItemFilterService.cs b/Libraries/Core/Services/ItemFilterService.cs index 0e44646d..affd997e 100644 --- a/Libraries/Core/Services/ItemFilterService.cs +++ b/Libraries/Core/Services/ItemFilterService.cs @@ -104,6 +104,11 @@ public class ItemFilterService( throw new NotImplementedException(); } + /** + * Check if the provided template id is blacklisted in config/item.json/lootableItemBlacklist + * @param tpl template id + * @returns true if blacklisted + */ public bool IsLootableItemBlacklisted(string itemKey) { if (_lootableItemBlacklistCache is null) @@ -140,4 +145,24 @@ public class ItemFilterService( _itemBlacklistCache.Add(item); } } + + /** + * Check if the provided template id is boss item in config/item.json + * @param tpl template id + * @returns true if boss item + */ + public bool IsBossItem(string tpl) + { + return _itemConfig.BossItems.Contains(tpl); + } + + /** + * Check if item is blacklisted from being a reward for player + * @param tpl item tpl to check is on blacklist + * @returns True when blacklisted + */ + public bool IsItemRewardBlacklisted(string tpl) + { + return _itemConfig.RewardItemBlacklist.Contains(tpl); + } }