From 3386316c3ad8e5ad25f8a66fb706960ca4cb157e Mon Sep 17 00:00:00 2001 From: CWX Date: Thu, 23 Jan 2025 12:10:46 +0000 Subject: [PATCH] Finish scavcaserewardgen --- .../Generators/ScavCaseRewardGenerator.cs | 325 +++++++++++++++++- .../Hideout/ScavCaseRewardCountsAndPrices.cs | 8 +- 2 files changed, 314 insertions(+), 19 deletions(-) diff --git a/Libraries/Core/Generators/ScavCaseRewardGenerator.cs b/Libraries/Core/Generators/ScavCaseRewardGenerator.cs index ff5a4846..624f1062 100644 --- a/Libraries/Core/Generators/ScavCaseRewardGenerator.cs +++ b/Libraries/Core/Generators/ScavCaseRewardGenerator.cs @@ -1,23 +1,83 @@ -using SptCommon.Annotations; +using Core.Helpers; +using Core.Models.Common; +using SptCommon.Annotations; using Core.Models.Eft.Common.Tables; using Core.Models.Eft.Hideout; using Core.Models.Eft.ItemEvent; +using Core.Models.Enums; +using Core.Models.Spt.Config; using Core.Models.Spt.Hideout; +using Core.Models.Utils; +using Core.Servers; +using Core.Services; +using Core.Utils; +using SptCommon.Extensions; namespace Core.Generators; [Injectable] -public class ScavCaseRewardGenerator() +public class ScavCaseRewardGenerator( + ISptLogger _logger, + RandomUtil _randomUtil, + HashUtil _hashUtil, + ItemHelper _itemHelper, + PresetHelper _presetHelper, + DatabaseService _databaseService, + RagfairPriceService _ragfairPriceService, + SeasonalEventService _seasonalEventService, + ItemFilterService _itemFilterService, + ConfigServer _configServer +) { + protected ScavCaseConfig _scavCaseConfig = _configServer.GetConfig(); + protected List _dbItemsCache = new List(); + protected List _dbAmmoItemsCache = new List(); /// /// Create an array of rewards that will be given to the player upon completing their scav case build /// /// recipe of the scav case craft /// Product array - public List> Generate(string recipeId) + public List> Generate(string recipeId) { - throw new NotImplementedException(); + CacheDbItems(); + + // Get scavcase details from hideout/scavcase.json + var scavCaseDetails = _databaseService + .GetHideout() + .Production.ScavRecipes.FirstOrDefault(r => r.Id == recipeId); + var rewardItemCounts = GetScavCaseRewardCountsAndPrices(scavCaseDetails); + + // Get items that fit the price criteria as set by the scavCase config + var commonPricedItems = GetFilteredItemsByPrice(_dbItemsCache, rewardItemCounts.Common); + var rarePricedItems = GetFilteredItemsByPrice(_dbItemsCache, rewardItemCounts.Rare); + var superRarePricedItems = GetFilteredItemsByPrice(_dbItemsCache, rewardItemCounts.Superrare); + + // Get randomly picked items from each item collction, the count range of which is defined in hideout/scavcase.json + var randomlyPickedCommonRewards = PickRandomRewards( + commonPricedItems, + rewardItemCounts.Common, + "common" + ); + var randomlyPickedRareRewards = PickRandomRewards(rarePricedItems, rewardItemCounts.Rare, "rare"); + var randomlyPickedSuperRareRewards = PickRandomRewards( + superRarePricedItems, + rewardItemCounts.Superrare, + "superrare" + ); + + // Add randomised stack sizes to ammo and money rewards + var commonRewards = RandomiseContainerItemRewards(randomlyPickedCommonRewards, "common"); + var rareRewards = RandomiseContainerItemRewards(randomlyPickedRareRewards, "rare"); + var superRareRewards = RandomiseContainerItemRewards(randomlyPickedSuperRareRewards, "superrare"); + + var result = new List>(); + result = result.Concat(commonRewards).ToList(); + result = result.Concat(rareRewards).ToList(); + result = result.Concat(superRareRewards).ToList(); + // TODO: please make this better, how merge 2d Lists + + return result.ToList(); } /// @@ -26,7 +86,102 @@ public class ScavCaseRewardGenerator() /// protected void CacheDbItems() { - throw new NotImplementedException(); + // TODO: pre-loop and get array of valid items, e.g. non-node/non-blacklisted, then loop over those results for below code + + // Get an array of seasonal items that should not be shown right now as seasonal event is not active + var inactiveSeasonalItems = _seasonalEventService.GetInactiveSeasonalEventItems(); + if (!_dbItemsCache.Any()) { + _dbItemsCache = _databaseService.GetItems().Values.Where(item => { + // Base "Item" item has no parent, ignore it + if (item.Parent == "") { + return false; + } + + if (item.Type == "Node") { + return false; + } + + if (item.Properties.QuestItem ?? false) { + return false; + } + + // Skip item if item id is on blacklist + if ( + item.Type != "Item" || + _scavCaseConfig.RewardItemBlacklist.Contains(item.Id) || + _itemFilterService.IsItemBlacklisted(item.Id) + ) { + return false; + } + + // Globally reward-blacklisted + if (_itemFilterService.IsItemRewardBlacklisted(item.Id)) { + return false; + } + + if (!_scavCaseConfig.AllowBossItemsAsRewards && _itemFilterService.IsBossItem(item.Id)) { + return false; + } + + // Skip item if parent id is blacklisted + if (_itemHelper.IsOfBaseclasses(item.Id, _scavCaseConfig.RewardItemParentBlacklist)) { + return false; + } + + if (inactiveSeasonalItems.Contains(item.Id)) { + return false; + } + + return true; + }).ToList(); + } + + if (!_dbAmmoItemsCache.Any()) { + _dbAmmoItemsCache = _databaseService.GetItems().Values.Where(item => { + // Base "Item" item has no parent, ignore it + if (item.Parent == "") { + return false; + } + + if (item.Type != "Item") { + return false; + } + + // Not ammo, skip + if (!_itemHelper.IsOfBaseclass(item.Id, BaseClasses.AMMO)) { + return false; + } + + // Skip item if item id is on blacklist + if ( + _scavCaseConfig.RewardItemBlacklist.Contains(item.Id) || + _itemFilterService.IsItemBlacklisted(item.Id) + ) { + return false; + } + + // Globally reward-blacklisted + if (_itemFilterService.IsItemRewardBlacklisted(item.Id)) { + return false; + } + + if (!_scavCaseConfig.AllowBossItemsAsRewards && _itemFilterService.IsBossItem(item.Id)) { + return false; + } + + // Skip seasonal items + if (inactiveSeasonalItems.Contains(item.Id)) { + return false; + } + + // Skip ammo that doesn't stack as high as value in config + if (item.Properties.StackMaxSize < _scavCaseConfig.AmmoRewards.MinStackSize) { + return false; + } + + return true; + }).ToList(); + } } /// @@ -40,7 +195,30 @@ public class ScavCaseRewardGenerator() RewardCountAndPriceDetails itemFilters, string rarity) { - throw new NotImplementedException(); + List result = []; + + var rewardWasMoney = false; + var rewardWasAmmo = false; + var randomCount = _randomUtil.GetInt((int)itemFilters.MinCount, (int)itemFilters.MaxCount); + for (var i = 0; i < randomCount; i++) { + if (RewardShouldBeMoney() && !rewardWasMoney) { + // Only allow one reward to be money + result.Add(GetRandomMoney()); + if (!_scavCaseConfig.AllowMultipleMoneyRewardsPerRarity) { + rewardWasMoney = true; + } + } else if (RewardShouldBeAmmo() && !rewardWasAmmo) { + // Only allow one reward to be ammo + result.Add(GetRandomAmmo(rarity)); + if (!_scavCaseConfig.AllowMultipleAmmoRewardsPerRarity) { + rewardWasAmmo = true; + } + } else { + result.Add(_randomUtil.GetArrayValue(items)); + } + } + + return result; } /// @@ -49,7 +227,7 @@ public class ScavCaseRewardGenerator() /// true if reward should be money protected bool RewardShouldBeMoney() { - throw new NotImplementedException(); + return _randomUtil.GetChance100(_scavCaseConfig.MoneyRewards.MoneyRewardChancePercent); } /// @@ -58,7 +236,7 @@ public class ScavCaseRewardGenerator() /// true if reward should be ammo protected bool RewardShouldBeAmmo() { - throw new NotImplementedException(); + return _randomUtil.GetChance100(_scavCaseConfig.AmmoRewards.AmmoRewardChancePercent); } /// @@ -66,7 +244,14 @@ public class ScavCaseRewardGenerator() /// protected TemplateItem GetRandomMoney() { - throw new NotImplementedException(); + List money = []; + var items = _databaseService.GetItems(); + money.Add(items[Money.ROUBLES]); + money.Add(items[Money.EUROS]); + money.Add(items[Money.DOLLARS]); + money.Add(items[Money.GP]); + + return _randomUtil.GetArrayValue(money); } /// @@ -76,7 +261,25 @@ public class ScavCaseRewardGenerator() /// random ammo item from items.json protected TemplateItem GetRandomAmmo(string rarity) { - throw new NotImplementedException(); + var possibleAmmoPool = _dbAmmoItemsCache.Where(ammo => { + // Is ammo handbook price between desired range + var handbookPrice = _ragfairPriceService.GetStaticPriceForItem(ammo.Id); + if ( + handbookPrice >= _scavCaseConfig.AmmoRewards.AmmoRewardValueRangeRub[rarity].Max && + handbookPrice <= _scavCaseConfig.AmmoRewards.AmmoRewardValueRangeRub[rarity].Max + ) { + return true; + } + + return false; + }); + + if (!possibleAmmoPool.Any()) { + _logger.Warning("Unable to get a list of ammo that matches desired criteria for scav case reward"); + } + + // Get a random ammo and return it + return _randomUtil.GetArrayValue(possibleAmmoPool); } /// @@ -85,9 +288,42 @@ public class ScavCaseRewardGenerator() /// /// items to convert /// Product array - protected List> RandomiseContainerItemRewards(List rewardItems, string rarity) + protected List> RandomiseContainerItemRewards(List rewardItems, string rarity) { - throw new NotImplementedException(); + /** Each array is an item + children */ + List> result = []; + foreach (var rewardItemDb in rewardItems) { + List resultItem = [new Item { Id = _hashUtil.Generate(), Template = rewardItemDb.Id, Upd = null }]; + var rootItem = resultItem.FirstOrDefault(); + + if (_itemHelper.IsOfBaseclass(rewardItemDb.Id, BaseClasses.AMMO_BOX)) { + _itemHelper.AddCartridgesToAmmoBox(resultItem, rewardItemDb); + } + // Armor or weapon = use default preset from globals.json + else if ( + _itemHelper.ArmorItemHasRemovableOrSoftInsertSlots(rewardItemDb.Id) || + _itemHelper.IsOfBaseclass(rewardItemDb.Id, BaseClasses.WEAPON) + ) { + var preset = _presetHelper.GetDefaultPreset(rewardItemDb.Id); + if (preset is null) { + _logger.Warning($"No preset for item: {rewardItemDb.Id} {rewardItemDb.Name}, skipping"); + + continue; + } + + // Ensure preset has unique ids and is cloned so we don't alter the preset data stored in memory + List presetAndMods = _itemHelper.ReplaceIDs(preset.Items); + _itemHelper.RemapRootItemId(presetAndMods); + + resultItem = presetAndMods; + } else if (_itemHelper.IsOfBaseclasses(rewardItemDb.Id, [BaseClasses.AMMO, BaseClasses.MONEY])) { + rootItem.Upd = new Upd { StackObjectsCount = GetRandomAmountRewardForScavCase(rewardItemDb, rarity) }; + } + + result.Add(resultItem); + } + + return result; } /// @@ -99,7 +335,13 @@ public class ScavCaseRewardGenerator() List dbItems, RewardCountAndPriceDetails itemFilters) { - throw new NotImplementedException(); + return dbItems.Where(item => { + var handbookPrice = _ragfairPriceService.GetStaticPriceForItem(item.Id); + if (handbookPrice >= itemFilters.MinPriceRub && handbookPrice <= itemFilters.MaxPriceRub) { + return true; + } + return false; + }).ToList(); } /// @@ -109,7 +351,31 @@ public class ScavCaseRewardGenerator() /// ScavCaseRewardCountsAndPrices object protected ScavCaseRewardCountsAndPrices GetScavCaseRewardCountsAndPrices(ScavRecipe scavCaseDetails) { - throw new NotImplementedException(); + return new ScavCaseRewardCountsAndPrices + { + // Create reward min/max counts for each type + Common = new RewardCountAndPriceDetails + { + MinCount = scavCaseDetails.EndProducts.Common.Min, + MaxCount = scavCaseDetails.EndProducts.Common.Max, + MinPriceRub = _scavCaseConfig.RewardItemValueRangeRub["common"].Min, + MaxPriceRub = _scavCaseConfig.RewardItemValueRangeRub["common"].Max + }, + Rare = new RewardCountAndPriceDetails + { + MinCount = scavCaseDetails.EndProducts.Rare.Min, + MaxCount = scavCaseDetails.EndProducts.Rare.Max, + MinPriceRub = _scavCaseConfig.RewardItemValueRangeRub["rare"].Min, + MaxPriceRub = _scavCaseConfig.RewardItemValueRangeRub["rare"].Max + }, + Superrare = new RewardCountAndPriceDetails + { + MinCount = scavCaseDetails.EndProducts.Superrare.Min, + MaxCount = scavCaseDetails.EndProducts.Superrare.Max, + MinPriceRub = _scavCaseConfig.RewardItemValueRangeRub["superrare"].Min, + MaxPriceRub = _scavCaseConfig.RewardItemValueRangeRub["superrare"].Max + } + }; } /// @@ -120,6 +386,35 @@ public class ScavCaseRewardGenerator() /// value to set stack count to protected int GetRandomAmountRewardForScavCase(TemplateItem itemToCalculate, string rarity) { - throw new NotImplementedException(); + var amountToGive = 1; + if (itemToCalculate.Parent == BaseClasses.AMMO) { + amountToGive = _randomUtil.GetInt( + _scavCaseConfig.AmmoRewards.MinStackSize, + itemToCalculate.Properties.StackMaxSize ?? 0 + ); + } else if (itemToCalculate.Parent == BaseClasses.MONEY) + { + amountToGive = itemToCalculate.Id switch + { + Money.ROUBLES => _randomUtil.GetInt( + (int)_scavCaseConfig.MoneyRewards.RubCount.GetByJsonProp(rarity).Min, + (int)_scavCaseConfig.MoneyRewards.RubCount.GetByJsonProp(rarity).Max + ), + Money.EUROS => _randomUtil.GetInt( + (int)_scavCaseConfig.MoneyRewards.EurCount.GetByJsonProp(rarity).Min, + (int)_scavCaseConfig.MoneyRewards.EurCount.GetByJsonProp(rarity).Max + ), + Money.DOLLARS => _randomUtil.GetInt( + (int)_scavCaseConfig.MoneyRewards.UsdCount.GetByJsonProp(rarity).Min, + (int)_scavCaseConfig.MoneyRewards.UsdCount.GetByJsonProp(rarity).Max + ), + Money.GP => _randomUtil.GetInt( + (int)_scavCaseConfig.MoneyRewards.GpCount.GetByJsonProp(rarity).Min, + (int)_scavCaseConfig.MoneyRewards.GpCount.GetByJsonProp(rarity).Max + ), + _ => amountToGive + }; + } + return amountToGive; } } diff --git a/Libraries/Core/Models/Spt/Hideout/ScavCaseRewardCountsAndPrices.cs b/Libraries/Core/Models/Spt/Hideout/ScavCaseRewardCountsAndPrices.cs index 87dbfd48..b28cc4d6 100644 --- a/Libraries/Core/Models/Spt/Hideout/ScavCaseRewardCountsAndPrices.cs +++ b/Libraries/Core/Models/Spt/Hideout/ScavCaseRewardCountsAndPrices.cs @@ -17,14 +17,14 @@ public record ScavCaseRewardCountsAndPrices public record RewardCountAndPriceDetails { [JsonPropertyName("minCount")] - public int? MinCount { get; set; } + public double? MinCount { get; set; } [JsonPropertyName("maxCount")] - public int? MaxCount { get; set; } + public double? MaxCount { get; set; } [JsonPropertyName("minPriceRub")] - public int? MinPriceRub { get; set; } + public double? MinPriceRub { get; set; } [JsonPropertyName("maxPriceRub")] - public int? MaxPriceRub { get; set; } + public double? MaxPriceRub { get; set; } }