Implement level ranged Exploration objective generation

This commit is contained in:
Cj
2025-10-01 02:58:20 -04:00
parent 1cf33ad402
commit c95446bb20
6 changed files with 300 additions and 93 deletions
@@ -517,28 +517,86 @@
}
],
"questConfig": {
"Exploration": {
"minExtracts": 1,
"maxExtracts": 4,
"minExtractsWithSpecificExit": 1,
"maxExtractsWithSpecificExit": 2,
"possibleSkillRewards": [
"Endurance",
"Strength",
"Vitality"
],
"specificExits": {
"chance": 20,
"passageRequirementWhitelist": [
"None",
"TransferItem",
"WorldEvent",
"Train",
"Reference",
"Empty"
]
"Exploration": [
{
"levelRange": {
"min": 1,
"max": 15
},
"minExtracts": 1,
"maxExtracts": 3,
"minExtractsWithSpecificExit": 1,
"maxExtractsWithSpecificExit": 2,
"possibleSkillRewards": [
"Endurance",
"Strength",
"Vitality"
],
"specificExits": {
"chance": 10,
"passageRequirementWhitelist": [
"None",
"TransferItem",
"WorldEvent",
"Train",
"Reference",
"Empty"
]
}
},
{
"levelRange": {
"min": 16,
"max": 40
},
"minExtracts": 2,
"maxExtracts": 7,
"minExtractsWithSpecificExit": 1,
"maxExtractsWithSpecificExit": 3,
"possibleSkillRewards": [
"Endurance",
"Strength",
"Vitality"
],
"specificExits": {
"chance": 15,
"passageRequirementWhitelist": [
"None",
"TransferItem",
"WorldEvent",
"Train",
"Reference",
"Empty"
]
}
},
{
"levelRange": {
"min": 41,
"max": 100
},
"minExtracts": 3,
"maxExtracts": 15,
"minExtractsWithSpecificExit": 2,
"maxExtractsWithSpecificExit": 4,
"possibleSkillRewards": [
"Endurance",
"Strength",
"Vitality"
],
"specificExits": {
"chance": 20,
"passageRequirementWhitelist": [
"None",
"TransferItem",
"WorldEvent",
"Train",
"Reference",
"Empty"
]
}
}
},
],
"Completion": {
"possibleSkillRewards": [
"Endurance",
@@ -1607,28 +1665,86 @@
}
],
"questConfig": {
"Exploration": {
"minExtracts": 5,
"maxExtracts": 15,
"minExtractsWithSpecificExit": 3,
"maxExtractsWithSpecificExit": 7,
"possibleSkillRewards": [
"Endurance",
"Strength",
"Vitality"
],
"specificExits": {
"chance": 10,
"passageRequirementWhitelist": [
"None",
"TransferItem",
"WorldEvent",
"Train",
"Reference",
"Empty"
]
"Exploration": [
{
"levelRange": {
"min": 1,
"max": 15
},
"minExtracts": 3,
"maxExtracts": 5,
"minExtractsWithSpecificExit": 1,
"maxExtractsWithSpecificExit": 4,
"possibleSkillRewards": [
"Endurance",
"Strength",
"Vitality"
],
"specificExits": {
"chance": 10,
"passageRequirementWhitelist": [
"None",
"TransferItem",
"WorldEvent",
"Train",
"Reference",
"Empty"
]
}
},
{
"levelRange": {
"min": 16,
"max": 40
},
"minExtracts": 4,
"maxExtracts": 8,
"minExtractsWithSpecificExit": 2,
"maxExtractsWithSpecificExit": 5,
"possibleSkillRewards": [
"Endurance",
"Strength",
"Vitality"
],
"specificExits": {
"chance": 15,
"passageRequirementWhitelist": [
"None",
"TransferItem",
"WorldEvent",
"Train",
"Reference",
"Empty"
]
}
},
{
"levelRange": {
"min": 41,
"max": 100
},
"minExtracts": 5,
"maxExtracts": 15,
"minExtractsWithSpecificExit": 3,
"maxExtractsWithSpecificExit": 6,
"possibleSkillRewards": [
"Endurance",
"Strength",
"Vitality"
],
"specificExits": {
"chance": 20,
"passageRequirementWhitelist": [
"None",
"TransferItem",
"WorldEvent",
"Train",
"Reference",
"Empty"
]
}
}
},
],
"Completion": {
"possibleSkillRewards": [
"Endurance",
@@ -2626,27 +2742,86 @@
}
],
"questConfig": {
"Exploration": {
"possibleSkillRewards": [
"Endurance",
"Strength",
"Vitality"
],
"minExtracts": 1,
"maxExtracts": 3,
"minExtractsWithSpecificExit": 1,
"maxExtractsWithSpecificExit": 1,
"specificExits": {
"chance": 20,
"passageRequirementWhitelist": [
"None",
"WorldEvent",
"Train",
"Reference",
"Empty"
]
"Exploration": [
{
"levelRange": {
"min": 1,
"max": 15
},
"minExtracts": 1,
"maxExtracts": 3,
"minExtractsWithSpecificExit": 1,
"maxExtractsWithSpecificExit": 2,
"possibleSkillRewards": [
"Endurance",
"Strength",
"Vitality"
],
"specificExits": {
"chance": 10,
"passageRequirementWhitelist": [
"None",
"TransferItem",
"WorldEvent",
"Train",
"Reference",
"Empty"
]
}
},
{
"levelRange": {
"min": 16,
"max": 40
},
"minExtracts": 2,
"maxExtracts": 7,
"minExtractsWithSpecificExit": 1,
"maxExtractsWithSpecificExit": 3,
"possibleSkillRewards": [
"Endurance",
"Strength",
"Vitality"
],
"specificExits": {
"chance": 15,
"passageRequirementWhitelist": [
"None",
"TransferItem",
"WorldEvent",
"Train",
"Reference",
"Empty"
]
}
},
{
"levelRange": {
"min": 41,
"max": 100
},
"minExtracts": 3,
"maxExtracts": 15,
"minExtractsWithSpecificExit": 2,
"maxExtractsWithSpecificExit": 4,
"possibleSkillRewards": [
"Endurance",
"Strength",
"Vitality"
],
"specificExits": {
"chance": 20,
"passageRequirementWhitelist": [
"None",
"TransferItem",
"WorldEvent",
"Train",
"Reference",
"Empty"
]
}
}
},
],
"Pickup": {
"possibleSkillRewards": [
"Endurance",
@@ -665,6 +665,7 @@
"repeatable-accepted_repeatable_quest_not_found_in_active_quests": "Accepted a repeatable quest: %s which could not be found in the activeQuests array. Please report this bug",
"repeatable-completion_quest_whitelist_too_small_or_blacklist_too_restrictive": "Generate Completion Quest: No items remain. Either Whitelist is too small or Blacklist too restrictive",
"repeatable-difficulty_was_nan": "Repeatable Reward Generation: Difficulty was NaN. Setting to 1.",
"repeatable-exploration_config_no_template": "Unable to find exploration config for pmc level: {{pmcLevel}}",
"repeatable-no_reward_item_found_in_price_range": "Repeatable Reward Generation: No item found in price range {{minPrice}} to {{roublesBudget}}",
"repeatable-quest_handover_failed_condition_already_satisfied": "Quest handover error: condition is already satisfied? qid: {{questId}}, condition: {{conditionId}}, profileCounter:{{profileCounter}}, value:{{value}}",
"repeatable-quest_handover_failed_condition_invalid": "Quest handover error: condition not found or incorrect value. qid: {{body.qid}}, condition: {{body.conditionId}}",
@@ -47,7 +47,7 @@ public class CompletionQuestGenerator(
RepeatableQuestConfig repeatableConfig
)
{
var completionConfig = repeatableConfig.QuestConfig.Completion;
var completionConfig = repeatableConfig.QuestConfig.CompletionConfig;
var levelsConfig = repeatableConfig.RewardScaling.Levels;
var roublesConfig = repeatableConfig.RewardScaling.Roubles;
@@ -73,12 +73,12 @@ public class CompletionQuestGenerator(
// 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",...]}]
if (repeatableConfig.QuestConfig.Completion.UseWhitelist)
if (repeatableConfig.QuestConfig.CompletionConfig.UseWhitelist)
{
itemsToRetrievePool = GetWhitelistedItemSelection(itemsToRetrievePool, pmcLevel);
}
if (repeatableConfig.QuestConfig.Completion.UseBlacklist)
if (repeatableConfig.QuestConfig.CompletionConfig.UseBlacklist)
{
itemsToRetrievePool = GetBlacklistedItemSelection(itemsToRetrievePool, pmcLevel);
}
@@ -108,10 +108,10 @@ public class CompletionQuestGenerator(
/// <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="completionConfigConfig">Completion quest type config</param>
/// <param name="itemTplBlacklist">Item tpls to not add to pool</param>
/// <returns>Set of item tpls</returns>
protected HashSet<MongoId> GetItemsToRetrievePool(Completion completionConfig, HashSet<MongoId> itemTplBlacklist)
protected HashSet<MongoId> GetItemsToRetrievePool(CompletionConfig completionConfigConfig, HashSet<MongoId> itemTplBlacklist)
{
// Get seasonal items that should not be added to pool as seasonal event is not active
var seasonalItems = seasonalEventService.GetInactiveSeasonalEventItems();
@@ -137,7 +137,7 @@ public class CompletionQuestGenerator(
return repeatableQuestRewardGenerator.IsValidRewardItem(
itemTemplate.Id,
itemTplBlacklist,
completionConfig.RequiredItemTypeBlacklist
completionConfigConfig.RequiredItemTypeBlacklist
);
})
.Select(item => item.Id)
@@ -239,21 +239,21 @@ public class CompletionQuestGenerator(
/// Generate the available for finish conditions for this quest
/// </summary>
/// <param name="quest">Quest to add the conditions to</param>
/// <param name="completionConfig">Completion config</param>
/// <param name="completionConfigConfig">Completion config</param>
/// <param name="repeatableConfig">Repeatable config</param>
/// <param name="itemSelection">Filtered item selection</param>
/// <param name="roublesBudget">Budget in roubles</param>
/// <returns>Chosen item template Ids</returns>
protected List<MongoId> GenerateAvailableForFinish(
RepeatableQuest quest,
Completion completionConfig,
CompletionConfig completionConfigConfig,
RepeatableQuestConfig repeatableConfig,
List<MongoId> itemSelection,
double roublesBudget
)
{
// Store the indexes of items we are asking player to supply
var distinctItemsToRetrieveCount = randomUtil.GetInt(1, completionConfig.UniqueItemCount);
var distinctItemsToRetrieveCount = randomUtil.GetInt(1, completionConfigConfig.UniqueItemCount);
var chosenRequirementItemsTpls = new List<MongoId>();
var usedItemIndexes = new HashSet<int>();
@@ -289,8 +289,8 @@ public class CompletionQuestGenerator(
var tplChosen = itemSelection[chosenItemIndex];
var itemPrice = itemHelper.GetItemPrice(tplChosen).Value;
var minValue = completionConfig.MinimumRequestedAmount;
var maxValue = completionConfig.MaximumRequestedAmount;
var minValue = completionConfigConfig.MinimumRequestedAmount;
var maxValue = completionConfigConfig.MaximumRequestedAmount;
var value = minValue;
@@ -308,7 +308,7 @@ public class CompletionQuestGenerator(
// Push a CompletionCondition with the item and the amount of the item into quest
chosenRequirementItemsTpls.Add(tplChosen);
quest.Conditions.AvailableForFinish.Add(GenerateCondition(tplChosen, value, repeatableConfig.QuestConfig.Completion));
quest.Conditions.AvailableForFinish.Add(GenerateCondition(tplChosen, value, repeatableConfig.QuestConfig.CompletionConfig));
// Is there budget left for more items
if (roublesBudget > 0)
@@ -339,14 +339,14 @@ public class CompletionQuestGenerator(
/// </summary>
/// <param name="itemTpl">Id of the item to request</param>
/// <param name="value">Amount of items of this specific type to request</param>
/// <param name="completionConfig">Completion config from quest.json</param>
/// <param name="completionConfigConfig">Completion config from quest.json</param>
/// <returns>object of "Completion"-condition</returns>
protected QuestCondition GenerateCondition(MongoId itemTpl, double value, Completion completionConfig)
protected QuestCondition GenerateCondition(MongoId itemTpl, double value, CompletionConfig completionConfigConfig)
{
var onlyFoundInRaid = completionConfig.RequiredItemsAreFiR;
var onlyFoundInRaid = completionConfigConfig.RequiredItemsAreFiR;
var minDurability = itemHelper.IsOfBaseclasses(itemTpl, [BaseClasses.WEAPON, BaseClasses.ARMOR])
? randomUtil.GetArrayValue(
[completionConfig.RequiredItemMinDurabilityMinMax.Min, completionConfig.RequiredItemMinDurabilityMinMax.Max]
[completionConfigConfig.RequiredItemMinDurabilityMinMax.Min, completionConfigConfig.RequiredItemMinDurabilityMinMax.Max]
)
: 0;
@@ -51,7 +51,12 @@ public class ExplorationQuestGenerator(
RepeatableQuestConfig repeatableConfig
)
{
var explorationConfig = repeatableConfig.QuestConfig.Exploration;
var explorationConfig = repeatableQuestHelper.GetExplorationConfigByPmcLevel(pmcLevel, repeatableConfig);
if (explorationConfig is null)
{
logger.Warning(localisationService.GetText("repeatable-exploration_config_no_template", new { pmcLevel }));
return null;
}
// Try and get a location to generate for
if (!TryGetLocationInfo(repeatableConfig, explorationConfig, questTypePool, out var locationInfo) || locationInfo is null)
@@ -85,7 +90,10 @@ public class ExplorationQuestGenerator(
}
// If we require a specific extract requirement, generate it
if (locationInfo.RequiresSpecificExtract && !TryGenerateSpecificExtractRequirement(quest, repeatableConfig, locationInfo))
if (
locationInfo.RequiresSpecificExtract
&& !TryGenerateSpecificExtractRequirement(quest, repeatableConfig, explorationConfig, locationInfo)
)
{
logger.Error(
localisationService.GetText("repeatable-specific_extract_condition_failed_to_generate", locationInfo.LocationName)
@@ -111,7 +119,7 @@ public class ExplorationQuestGenerator(
/// <returns>True if location selected, false if no locations remain</returns>
protected bool TryGetLocationInfo(
RepeatableQuestConfig repeatableConfig,
Exploration explorationConfig,
ExplorationConfig explorationConfig,
QuestTypePool pool,
out LocationInfo? locationInfo
)
@@ -131,7 +139,7 @@ public class ExplorationQuestGenerator(
// Make the location info object
var locationTarget = pool.Pool.Exploration.Locations![locationKey];
var requiresSpecificExtract = randomUtil.GetChance100(repeatableConfig.QuestConfig.Exploration.SpecificExits.Chance);
var requiresSpecificExtract = randomUtil.GetChance100(explorationConfig.SpecificExits.Chance);
var numExtracts = GetNumberOfExits(explorationConfig, requiresSpecificExtract);
@@ -146,17 +154,19 @@ public class ExplorationQuestGenerator(
/// <summary>
/// Get the number of times the player needs to exit
/// </summary>
/// <param name="explorationConfig">Exploration config</param>
/// <param name="explorationConfigConfig">Exploration config</param>
/// <param name="requiresSpecificExtract">Is this a specific extract</param>
/// <returns>Number of exit requirements</returns>
protected int GetNumberOfExits(Exploration explorationConfig, bool requiresSpecificExtract)
protected int GetNumberOfExits(ExplorationConfig explorationConfigConfig, bool requiresSpecificExtract)
{
var exitTimesMin = requiresSpecificExtract ? explorationConfig.MinimumExtractsWithSpecificExit : explorationConfig.MinimumExtracts;
var exitTimesMin = requiresSpecificExtract
? explorationConfigConfig.MinimumExtractsWithSpecificExit
: explorationConfigConfig.MinimumExtracts;
// Different max extract count when specific extract needed
var exitTimesMax = requiresSpecificExtract
? explorationConfig.MaximumExtractsWithSpecificExit + 1
: explorationConfig.MaximumExtracts + 1;
? explorationConfigConfig.MaximumExtractsWithSpecificExit + 1
: explorationConfigConfig.MaximumExtracts + 1;
return randomUtil.RandInt(exitTimesMin, exitTimesMax);
}
@@ -229,11 +239,13 @@ public class ExplorationQuestGenerator(
/// </summary>
/// <param name="quest">quest to add it to</param>
/// <param name="repeatableConfig">repeatable config</param>
/// <param name="explorationConfig">exploration config</param>
/// <param name="locationInfo">LocationInfo object with the generated data</param>
/// <returns>True if generated, false if not</returns>
protected bool TryGenerateSpecificExtractRequirement(
RepeatableQuest quest,
RepeatableQuestConfig repeatableConfig,
ExplorationConfig explorationConfig,
LocationInfo locationInfo
)
{
@@ -251,7 +263,7 @@ public class ExplorationQuestGenerator(
// Exclude exits with a requirement to leave (e.g. car extracts)
var possibleExits = exitPool.Where(exit =>
repeatableConfig.QuestConfig.Exploration.SpecificExits.PassageRequirementWhitelist.Contains(exit.PassageRequirement.ToString())
explorationConfig.SpecificExits.PassageRequirementWhitelist.Contains(exit.PassageRequirement.ToString())
);
if (!possibleExits.Any())
@@ -32,6 +32,19 @@ public class RepeatableQuestHelper(
return repeatableConfig.QuestConfig.Elimination.FirstOrDefault(x => pmcLevel >= x.LevelRange.Min && pmcLevel <= x.LevelRange.Max);
}
/// <summary>
/// Get the relevant exploration config based on the current players PMC level
/// </summary>
/// <param name="pmcLevel">Level of PMC character</param>
/// <param name="repeatableConfig">Main repeatable config</param>
/// <returns>ExplorationConfig</returns>
public ExplorationConfig? GetExplorationConfigByPmcLevel(int pmcLevel, RepeatableQuestConfig repeatableConfig)
{
return repeatableConfig.QuestConfig.ExplorationConfig.FirstOrDefault(x =>
pmcLevel >= x.LevelRange.Min && pmcLevel <= x.LevelRange.Max
);
}
/// <summary>
/// Gets a cloned repeatable quest template for the provided type with a unique id
/// </summary>
@@ -338,16 +338,16 @@ public record RepeatableQuestTypesConfig
/// Defines exploration repeatable task generation parameters
/// </summary>
[JsonPropertyName("Exploration")]
public required Exploration Exploration { get; set; }
public required List<ExplorationConfig> ExplorationConfig { get; set; }
/// <summary>
/// Defines completion repeatable task generation parameters
/// </summary>
[JsonPropertyName("Completion")]
public required Completion Completion { get; set; }
public required CompletionConfig CompletionConfig { get; set; }
/// <summary>
/// Defines pickup repeatable task generation parameters - TODO: Not implemented/No Data
/// Defines pickup repeatable task generation parameters - TODO: Not implemented/No Data - NOTE: Does not work with dynamicLocale
/// </summary>
[JsonPropertyName("Pickup")]
public Pickup? Pickup { get; set; }
@@ -359,8 +359,14 @@ public record RepeatableQuestTypesConfig
public required List<EliminationConfig> Elimination { get; set; }
}
public record Exploration : BaseQuestConfig
public record ExplorationConfig : BaseQuestConfig
{
/// <summary>
/// Level range at which elimination tasks should be generated from this config
/// </summary>
[JsonPropertyName("levelRange")]
public required MinMax<int> LevelRange { get; set; }
/// <summary>
/// Minimum extract count that a per map extract requirement can be generated with
/// </summary>
@@ -407,7 +413,7 @@ public record SpecificExits
public required HashSet<string> PassageRequirementWhitelist { get; set; }
}
public record Completion : BaseQuestConfig
public record CompletionConfig : BaseQuestConfig
{
/// <summary>
/// Minimum item count that can be requested