Created GetItemsToRetrievePool to handle logic instead of using GetRewardableItems, now returns tpls instead of entire objects #401

Decoupled `IsValidRewardItem` from repeatable config
Made `IsValidRewardItem` public
Added `RequiredItemTypeBlacklist` to Completion config, Blacklisted ammo
More nullguards
This commit is contained in:
Chomp
2025-06-17 09:50:32 +01:00
parent 125b81e3e5
commit 51fc2c4a0b
4 changed files with 95 additions and 52 deletions
@@ -330,7 +330,6 @@
"5485a8684bdc2da71d8b4567",
"55818b224bdc2dde698b456f",
"57864a66245977548f04a81f",
"55818af64bdc2d5b648b4570",
"550aa4dd4bdc2dc9348b4569",
"55818a594bdc2db9688b456a",
"55818a104bdc2db9688b4569"
@@ -1216,7 +1215,10 @@
"requiredItemMinDurabilityMinMax": {
"min": 60,
"max": 80
}
},
"requiredItemTypeBlacklist": [
"5485a8684bdc2da71d8b4567"
]
},
"Elimination": [
{
@@ -28,6 +28,7 @@ public class RepeatableQuestGenerator(
DatabaseService _databaseService,
LocalisationService _localisationService,
ConfigServer _configServer,
SeasonalEventService _seasonalEventService,
ICloner _cloner
)
{
@@ -557,25 +558,27 @@ public class RepeatableQuestGenerator(
RepeatableQuestConfig repeatableConfig
)
{
var completionConfig = repeatableConfig.QuestConfig.Completion;
var completionConfig = repeatableConfig?.QuestConfig?.Completion;
if (completionConfig is null)
{
_logger.Error("Unable to generate Completion quest, no Completion config found");
return null;
}
var levelsConfig = repeatableConfig.RewardScaling.Levels;
var roublesConfig = repeatableConfig.RewardScaling.Roubles;
var quest = GenerateRepeatableTemplate("Completion", traderId, repeatableConfig.Side, sessionId);
// Filter the items.json items to items the player must retrieve to complete quest: shouldn't be a quest item or "non-existant"
var possibleItemsToRetrievePool = _repeatableQuestRewardGenerator.GetRewardableItems(
repeatableConfig,
traderId
);
// Filter the items.json items to items the player must retrieve to complete quest: shouldn't be a quest item or "non-existent"
var itemsToRetrievePool = GetItemsToRetrievePool(completionConfig, repeatableConfig.RewardBlacklist);
// Be fair, don't var the items be more expensive than the reward
var multi = _randomUtil.GetDouble(0.5, 1);
// Be fair, don't value the items be more expensive than the reward
var multiplier = _randomUtil.GetDouble(0.5, 1);
var roublesBudget = Math.Floor(
(double) (_mathUtil.Interp1(pmcLevel, levelsConfig, roublesConfig) * multi)
(double) (_mathUtil.Interp1(pmcLevel, levelsConfig, roublesConfig) * multiplier)
);
roublesBudget = Math.Max(roublesBudget, 5000d);
var itemSelection = possibleItemsToRetrievePool.Where(x => _itemHelper.GetItemPrice(x.Id) < roublesBudget
var itemSelection = itemsToRetrievePool.Where(itemTpl => _itemHelper.GetItemPrice(itemTpl) < roublesBudget
)
.ToList();
@@ -585,7 +588,7 @@ public class RepeatableQuestGenerator(
{
var itemWhitelist = _databaseService.GetTemplates().RepeatableQuests.Data.Completion.ItemsWhitelist;
// Filter and concatenate the arrays according to current player level
// Filter and concatenate items according to current player level
var itemIdsWhitelisted = itemWhitelist
.Where(p => p.MinPlayerLevel <= pmcLevel)
.SelectMany(x => x.ItemIds)
@@ -593,8 +596,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.Id, v)) ||
itemIdsWhitelisted.Contains(x.Id);
return itemIdsWhitelisted.Any(v => _itemHelper.IsOfBaseclass(x, v)) ||
itemIdsWhitelisted.Contains(x);
}
)
.ToList();
@@ -607,7 +610,7 @@ public class RepeatableQuestGenerator(
{
var itemBlacklist = _databaseService.GetTemplates().RepeatableQuests.Data.Completion.ItemsBlacklist;
// we filter and concatenate the arrays according to current player level
// Filter and concatenate the arrays according to current player level
var itemIdsBlacklisted = itemBlacklist
.Where(p => p.MinPlayerLevel <= pmcLevel)
.SelectMany(x => x.ItemIds)
@@ -615,13 +618,14 @@ public class RepeatableQuestGenerator(
itemSelection = itemSelection.Where(x =>
{
return itemIdsBlacklisted.All(v => !_itemHelper.IsOfBaseclass(x.Id, v)) ||
!itemIdsBlacklisted.Contains(x.Id);
return itemIdsBlacklisted.All(v => !_itemHelper.IsOfBaseclass(x, v)) ||
!itemIdsBlacklisted.Contains(x);
}
)
.ToList();
}
// Filtering too harsh
if (!itemSelection.Any())
{
_logger.Error(
@@ -633,10 +637,7 @@ public class RepeatableQuestGenerator(
return null;
}
// Draw items to ask player to retrieve
var isAmmo = 0;
// Store the indexes of items we are asking player to provide
// Store the indexes of items we are asking player to supply
var distinctItemsToRetrieveCount = _randomUtil.GetInt(1, completionConfig.UniqueItemCount.Value);
var chosenRequirementItemsTpls = new List<string>();
var usedItemIndexes = new HashSet<int>();
@@ -674,32 +675,18 @@ public class RepeatableQuestGenerator(
return null;
}
// Store index of item we've already chosen for later checking
usedItemIndexes.Add(chosenItemIndex);
var itemSelected = itemSelection[chosenItemIndex];
var itemUnitPrice = _itemHelper.GetItemPrice(itemSelected.Id).Value;
var tplChosen = itemSelection[chosenItemIndex];
var itemPrice = _itemHelper.GetItemPrice(tplChosen).Value;
var minValue = completionConfig.MinimumRequestedAmount.Value;
var maxValue = completionConfig.MaximumRequestedAmount.Value;
if (_itemHelper.IsOfBaseclass(itemSelected.Id, BaseClasses.AMMO))
{
// Prevent multiple ammo requirements from being picked
if (isAmmo > 0 && isAmmo < _maxRandomNumberAttempts)
{
isAmmo++;
i--;
continue;
}
isAmmo++;
minValue = completionConfig.MinimumRequestedBulletAmount.Value;
maxValue = completionConfig.MaximumRequestedBulletAmount.Value;
}
var value = minValue;
// Get the value range within budget
var x = (int) Math.Floor(roublesBudget / itemUnitPrice);
var x = (int) Math.Floor(roublesBudget / itemPrice);
maxValue = Math.Min(maxValue, x);
if (maxValue > minValue)
// If it doesn't blow the budget we have for the request, draw a random amount of the selected
@@ -708,19 +695,21 @@ public class RepeatableQuestGenerator(
value = _randomUtil.RandInt(minValue, maxValue + 1);
}
roublesBudget -= value * itemUnitPrice;
roublesBudget -= value * itemPrice;
// Push a CompletionCondition with the item and the amount of the item
chosenRequirementItemsTpls.Add(itemSelected.Id);
quest.Conditions.AvailableForFinish.Add(GenerateCompletionAvailableForFinish(itemSelected.Id, value, repeatableConfig.QuestConfig.Completion));
// Push a CompletionCondition with the item and the amount of the item into quest
chosenRequirementItemsTpls.Add(tplChosen);
quest.Conditions.AvailableForFinish.Add(GenerateCompletionAvailableForFinish(tplChosen, value, repeatableConfig.QuestConfig.Completion));
// Is there budget left for more items
if (roublesBudget > 0)
{
// Reduce the list possible items to fulfill the new budget constraint
itemSelection = itemSelection.Where(dbItem => _itemHelper.GetItemPrice(dbItem.Id) < roublesBudget)
// Reduce item pool to fit budget
itemSelection = itemSelection.Where(tpl => _itemHelper.GetItemPrice(tpl) < roublesBudget)
.ToList();
if (!itemSelection.Any())
{
// Nothing fits new budget, exit
break;
}
}
@@ -742,6 +731,44 @@ public class RepeatableQuestGenerator(
return quest;
}
/// <summary>
/// Generate a pool of item tpls the player should reasonably be able to retrieve
/// </summary>
/// <param name="completionConfig">Completion quest type config</param>
/// <param name="itemTplBlacklist">Item tpls to not add to pool</param>
/// <returns>Set of item tpls</returns>
protected HashSet<string> GetItemsToRetrievePool(Completion completionConfig, HashSet<string> itemTplBlacklist)
{
// Get seasonal items that should not be added to pool as seasonal event is not active
var seasonalItems = _seasonalEventService.GetInactiveSeasonalEventItems();
// Check for specific base classes 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
return _databaseService.GetItems()
.Values.Where(itemTemplate =>
{
// Base "Item" item has no parent, ignore it
if (itemTemplate.Parent == string.Empty)
{
return false;
}
if (seasonalItems.Contains(itemTemplate.Id))
{
return false;
}
// Valid reward items share same logic as items to retrieve
return _repeatableQuestRewardGenerator.IsValidRewardItem(
itemTemplate.Id,
itemTplBlacklist,
completionConfig.RequiredItemTypeBlacklist
);
}
).Select(item => item.Id)
.ToHashSet();
}
/// <summary>
/// A repeatable quest, besides some more or less static components, exists of reward and condition (see
/// assets/database/templates/repeatableQuests.json)
@@ -693,7 +693,7 @@ public class RepeatableQuestRewardGenerator(
// 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
// Check for specific base classes 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()
@@ -715,7 +715,8 @@ public class RepeatableQuestRewardGenerator(
return IsValidRewardItem(
itemTemplate.Id,
repeatableQuestConfig,
repeatableQuestConfig.RewardBlacklist,
repeatableQuestConfig.RewardBaseTypeBlacklist,
traderWhitelist?.RewardBaseWhitelist
);
}
@@ -728,10 +729,13 @@ public class RepeatableQuestRewardGenerator(
/// or content of bot loot. Items that are tested as valid may be in a player backpack or stash.
/// </summary>
/// <param name="tpl"> Template id of item to check</param>
/// <param name="repeatableQuestConfig"> Config </param>
/// <param name="itemTplBlacklist"> Specific item tpls to ignore </param>
/// <param name="itemTypeBlacklist"> Specific item base types to ignore </param>
/// <param name="itemBaseWhitelist"> Default null, specific trader item base classes</param>
/// <returns> True if item is valid reward </returns>
protected bool IsValidRewardItem(string tpl, RepeatableQuestConfig repeatableQuestConfig,
public bool IsValidRewardItem(string tpl,
HashSet<string> itemTplBlacklist,
HashSet<string> itemTypeBlacklist,
List<string>? itemBaseWhitelist = null)
{
// Return early if not valid item to give as reward
@@ -744,7 +748,7 @@ public class RepeatableQuestRewardGenerator(
if (
_itemFilterService.IsItemBlacklisted(tpl) ||
_itemFilterService.IsItemRewardBlacklisted(tpl) ||
repeatableQuestConfig.RewardBlacklist.Contains(tpl) ||
itemTplBlacklist.Contains(tpl) ||
_itemFilterService.IsItemBlacklisted(tpl)
)
{
@@ -752,7 +756,7 @@ public class RepeatableQuestRewardGenerator(
}
// Item has blacklisted base types
if (_itemHelper.IsOfBaseclasses(tpl, repeatableQuestConfig.RewardBaseTypeBlacklist))
if (_itemHelper.IsOfBaseclasses(tpl, itemTypeBlacklist))
{
return false;
}
@@ -600,6 +600,16 @@ public record Completion : BaseQuestConfig
get;
set;
}
/// <summary>
/// Blacklisted item types to not collect
/// </summary>
[JsonPropertyName("requiredItemTypeBlacklist")]
public HashSet<string>? RequiredItemTypeBlacklist
{
get;
set;
}
}
public record Pickup : BaseQuestConfig