040be2feaa
Convert constructors into primary constructors Simplified logic with use of ??, ??= and method groups Cleaned up redundant conditional access qualifiers
580 lines
20 KiB
C#
580 lines
20 KiB
C#
using SPTarkov.Common.Extensions;
|
|
using SPTarkov.DI.Annotations;
|
|
using SPTarkov.Server.Core.Extensions;
|
|
using SPTarkov.Server.Core.Helpers;
|
|
using SPTarkov.Server.Core.Models.Common;
|
|
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
|
|
using SPTarkov.Server.Core.Models.Eft.Hideout;
|
|
using SPTarkov.Server.Core.Models.Enums;
|
|
using SPTarkov.Server.Core.Models.Spt.Config;
|
|
using SPTarkov.Server.Core.Models.Spt.Hideout;
|
|
using SPTarkov.Server.Core.Models.Utils;
|
|
using SPTarkov.Server.Core.Servers;
|
|
using SPTarkov.Server.Core.Services;
|
|
using SPTarkov.Server.Core.Utils;
|
|
using SPTarkov.Server.Core.Utils.Cloners;
|
|
|
|
namespace SPTarkov.Server.Core.Generators;
|
|
|
|
[Injectable]
|
|
public class ScavCaseRewardGenerator(
|
|
ISptLogger<ScavCaseRewardGenerator> logger,
|
|
RandomUtil randomUtil,
|
|
ItemHelper itemHelper,
|
|
PresetHelper presetHelper,
|
|
DatabaseService databaseService,
|
|
RagfairPriceService ragfairPriceService,
|
|
SeasonalEventService seasonalEventService,
|
|
ItemFilterService itemFilterService,
|
|
ServerLocalisationService localisationService,
|
|
ConfigServer configServer,
|
|
ICloner cloner
|
|
)
|
|
{
|
|
protected List<TemplateItem> _dbAmmoItemsCache = [];
|
|
protected List<TemplateItem> _dbItemsCache = [];
|
|
protected readonly ScavCaseConfig _scavCaseConfig = configServer.GetConfig<ScavCaseConfig>();
|
|
|
|
/// <summary>
|
|
/// Create an array of rewards that will be given to the player upon completing their scav case build
|
|
/// </summary>
|
|
/// <param name="recipeId">recipe of the scav case craft</param>
|
|
/// <returns>Product array</returns>
|
|
public List<List<Item>> Generate(MongoId recipeId)
|
|
{
|
|
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 collection, the count range of which is defined in hideout/scavcase.json
|
|
var randomlyPickedCommonRewards = PickRandomRewards(
|
|
commonPricedItems,
|
|
rewardItemCounts.Common,
|
|
RewardRarity.Common
|
|
);
|
|
|
|
var randomlyPickedRareRewards = PickRandomRewards(
|
|
rarePricedItems,
|
|
rewardItemCounts.Rare,
|
|
RewardRarity.Rare
|
|
);
|
|
|
|
var randomlyPickedSuperRareRewards = PickRandomRewards(
|
|
superRarePricedItems,
|
|
rewardItemCounts.Superrare,
|
|
RewardRarity.SuperRare
|
|
);
|
|
|
|
// Add randomised stack sizes to ammo and money rewards
|
|
var commonRewards = RandomiseContainerItemRewards(
|
|
randomlyPickedCommonRewards,
|
|
RewardRarity.Common
|
|
);
|
|
var rareRewards = RandomiseContainerItemRewards(
|
|
randomlyPickedRareRewards,
|
|
RewardRarity.Rare
|
|
);
|
|
var superRareRewards = RandomiseContainerItemRewards(
|
|
randomlyPickedSuperRareRewards,
|
|
RewardRarity.SuperRare
|
|
);
|
|
|
|
var result = new List<List<Item>>();
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get all db items that are not blacklisted in scavcase config or global blacklist
|
|
/// Store in class field
|
|
/// </summary>
|
|
protected void CacheDbItems()
|
|
{
|
|
// 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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pick a number of items to be rewards, the count is defined by the values in `itemFilters` param
|
|
/// </summary>
|
|
/// <param name="items">item pool to pick rewards from</param>
|
|
/// <param name="itemFilters">how the rewards should be filtered down (by item count)</param>
|
|
/// <param name="rarity">Rarity of reward</param>
|
|
/// <returns></returns>
|
|
protected List<TemplateItem> PickRandomRewards(
|
|
List<TemplateItem> items,
|
|
RewardCountAndPriceDetails itemFilters,
|
|
string rarity
|
|
)
|
|
{
|
|
List<TemplateItem> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Choose if money should be a reward based on the moneyRewardChancePercent config chance in scavCaseConfig
|
|
/// </summary>
|
|
/// <returns>true if reward should be money</returns>
|
|
protected bool RewardShouldBeMoney()
|
|
{
|
|
return randomUtil.GetChance100(_scavCaseConfig.MoneyRewards.MoneyRewardChancePercent);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Choose if ammo should be a reward based on the ammoRewardChancePercent config chance in scavCaseConfig
|
|
/// </summary>
|
|
/// <returns>true if reward should be ammo</returns>
|
|
protected bool RewardShouldBeAmmo()
|
|
{
|
|
return randomUtil.GetChance100(_scavCaseConfig.AmmoRewards.AmmoRewardChancePercent);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Choose from rouble/dollar/euro at random
|
|
/// </summary>
|
|
protected TemplateItem GetRandomMoney()
|
|
{
|
|
List<TemplateItem> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get a random ammo from items.json that is not in the ammo blacklist AND inside the price range defined in scavcase.json config
|
|
/// </summary>
|
|
/// <param name="rarity">The rarity desired ammo reward is for</param>
|
|
/// <returns>random ammo item from items.json</returns>
|
|
protected TemplateItem GetRandomAmmo(string rarity)
|
|
{
|
|
var possibleAmmoPool = _dbAmmoItemsCache.Where(ammo =>
|
|
{
|
|
// Is ammo handbook price between desired range
|
|
var handbookPrice = ragfairPriceService.GetStaticPriceForItem(ammo.Id);
|
|
if (
|
|
_scavCaseConfig.AmmoRewards.AmmoRewardValueRangeRub.TryGetValue(
|
|
rarity,
|
|
out var matchingAmmoRewardForRarity
|
|
)
|
|
&& handbookPrice >= matchingAmmoRewardForRarity.Min
|
|
&& handbookPrice <= matchingAmmoRewardForRarity.Max
|
|
)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
if (!possibleAmmoPool.Any())
|
|
{
|
|
// Filtered pool is empty
|
|
logger.Warning(
|
|
localisationService.GetText("scavcase-no_cartridges_found_matching_price")
|
|
);
|
|
}
|
|
|
|
// Get a random ammo and return it
|
|
return randomUtil.GetArrayValue(possibleAmmoPool);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Take all the rewards picked create the Product object array ready to return to calling code.
|
|
/// Also add a stack count to ammo and money
|
|
/// </summary>
|
|
/// <param name="rewardItems">items to convert</param>
|
|
/// <param name="rarity">The rarity desired ammo reward is for</param>
|
|
/// <returns>Product array</returns>
|
|
protected List<List<Item>> RandomiseContainerItemRewards(
|
|
List<TemplateItem> rewardItems,
|
|
string rarity
|
|
)
|
|
{
|
|
// Each array is an item + children
|
|
List<List<Item>> result = [];
|
|
foreach (var rewardItemDb in rewardItems)
|
|
{
|
|
List<Item> resultItem =
|
|
[
|
|
new()
|
|
{
|
|
Id = new MongoId(),
|
|
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
|
|
var presetAndMods = cloner.Clone(preset.Items).ReplaceIDs().ToList();
|
|
presetAndMods.RemapRootItemId();
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// </summary>
|
|
/// <param name="dbItems">all items from the items.json</param>
|
|
/// <param name="itemFilters">controls how the dbItems will be filtered and returned (handbook price)</param>
|
|
/// <returns>filtered dbItems array</returns>
|
|
protected List<TemplateItem> GetFilteredItemsByPrice(
|
|
List<TemplateItem> dbItems,
|
|
RewardCountAndPriceDetails itemFilters
|
|
)
|
|
{
|
|
return dbItems
|
|
.Where(item =>
|
|
{
|
|
var handbookPrice = ragfairPriceService.GetStaticPriceForItem(item.Id);
|
|
if (
|
|
handbookPrice >= itemFilters.MinPriceRub
|
|
&& handbookPrice <= itemFilters.MaxPriceRub
|
|
)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
})
|
|
.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gathers the reward min and max count params for each reward quality level from config and scavcase.json into a single object
|
|
/// </summary>
|
|
/// <param name="scavCaseDetails">production.json/scavRecipes object</param>
|
|
/// <returns>ScavCaseRewardCountsAndPrices object</returns>
|
|
protected ScavCaseRewardCountsAndPrices GetScavCaseRewardCountsAndPrices(
|
|
ScavRecipe scavCaseDetails
|
|
)
|
|
{
|
|
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[RewardRarity.Common].Min,
|
|
MaxPriceRub = _scavCaseConfig.RewardItemValueRangeRub[RewardRarity.Common].Max,
|
|
},
|
|
Rare = new RewardCountAndPriceDetails
|
|
{
|
|
MinCount = scavCaseDetails.EndProducts.Rare.Min,
|
|
MaxCount = scavCaseDetails.EndProducts.Rare.Max,
|
|
MinPriceRub = _scavCaseConfig.RewardItemValueRangeRub[RewardRarity.Rare].Min,
|
|
MaxPriceRub = _scavCaseConfig.RewardItemValueRangeRub[RewardRarity.Rare].Max,
|
|
},
|
|
Superrare = new RewardCountAndPriceDetails
|
|
{
|
|
MinCount = scavCaseDetails.EndProducts.Superrare.Min,
|
|
MaxCount = scavCaseDetails.EndProducts.Superrare.Max,
|
|
MinPriceRub = _scavCaseConfig.RewardItemValueRangeRub[RewardRarity.SuperRare].Min,
|
|
MaxPriceRub = _scavCaseConfig.RewardItemValueRangeRub[RewardRarity.SuperRare].Max,
|
|
},
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Randomises the size of ammo and money stacks
|
|
/// </summary>
|
|
/// <param name="itemToCalculate">ammo or money item</param>
|
|
/// <param name="rarity">rarity (common/rare/superrare)</param>
|
|
/// <returns>value to set stack count to</returns>
|
|
protected int GetRandomAmountRewardForScavCase(TemplateItem itemToCalculate, string rarity)
|
|
{
|
|
var parentId = itemToCalculate.Parent;
|
|
|
|
if (parentId == BaseClasses.AMMO)
|
|
{
|
|
return GetRandomisedAmmoRewardStackSize(itemToCalculate);
|
|
}
|
|
else if (parentId == BaseClasses.MONEY)
|
|
{
|
|
return GetRandomisedMoneyRewardStackSize(itemToCalculate, rarity);
|
|
}
|
|
else
|
|
{
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Randomises the size of ammo stacks
|
|
/// </summary>
|
|
/// <param name="itemToCalculate">ammo or money item</param>
|
|
/// <returns>value to set stack count to</returns>
|
|
protected int GetRandomisedAmmoRewardStackSize(TemplateItem itemToCalculate)
|
|
{
|
|
return randomUtil.GetInt(
|
|
_scavCaseConfig.AmmoRewards.MinStackSize,
|
|
itemToCalculate.Properties.StackMaxSize ?? 0
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Randomises the size of money stacks
|
|
/// </summary>
|
|
/// <param name="itemToCalculate">ammo or money item</param>
|
|
/// <param name="rarity">rarity (common/rare/superrare)</param>
|
|
/// <returns>value to set stack count to</returns>
|
|
protected int GetRandomisedMoneyRewardStackSize(TemplateItem itemToCalculate, string rarity)
|
|
{
|
|
var id = itemToCalculate.Id;
|
|
|
|
if (id == Money.ROUBLES)
|
|
{
|
|
return randomUtil.GetInt(
|
|
_scavCaseConfig.MoneyRewards.RubCount.GetByJsonProp<MinMax<int>>(rarity).Min,
|
|
_scavCaseConfig.MoneyRewards.RubCount.GetByJsonProp<MinMax<int>>(rarity).Max
|
|
);
|
|
}
|
|
else if (id == Money.EUROS)
|
|
{
|
|
return randomUtil.GetInt(
|
|
_scavCaseConfig.MoneyRewards.EurCount.GetByJsonProp<MinMax<int>>(rarity).Min,
|
|
_scavCaseConfig.MoneyRewards.EurCount.GetByJsonProp<MinMax<int>>(rarity).Max
|
|
);
|
|
}
|
|
else if (id == Money.DOLLARS)
|
|
{
|
|
return randomUtil.GetInt(
|
|
_scavCaseConfig.MoneyRewards.UsdCount.GetByJsonProp<MinMax<int>>(rarity).Min,
|
|
_scavCaseConfig.MoneyRewards.UsdCount.GetByJsonProp<MinMax<int>>(rarity).Max
|
|
);
|
|
}
|
|
else if (id == Money.GP)
|
|
{
|
|
return randomUtil.GetInt(
|
|
_scavCaseConfig.MoneyRewards.GpCount.GetByJsonProp<MinMax<int>>(rarity).Min,
|
|
_scavCaseConfig.MoneyRewards.GpCount.GetByJsonProp<MinMax<int>>(rarity).Max
|
|
);
|
|
}
|
|
else
|
|
{
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
public record RewardRarity
|
|
{
|
|
public const string Common = "common";
|
|
public const string Rare = "rare";
|
|
public const string SuperRare = "superrare";
|
|
}
|