diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/quest.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/quest.json index 7a41179f..8381ad65 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/quest.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/quest.json @@ -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", diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/database/locales/server/en.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/database/locales/server/en.json index e37aac2e..efca89e5 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/database/locales/server/en.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/database/locales/server/en.json @@ -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}}", diff --git a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/CompletionQuestGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/CompletionQuestGenerator.cs index db653116..6ec7c9ca 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/CompletionQuestGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/CompletionQuestGenerator.cs @@ -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( /// /// Generate a pool of item tpls the player should reasonably be able to retrieve /// - /// Completion quest type config + /// Completion quest type config /// Item tpls to not add to pool /// Set of item tpls - protected HashSet GetItemsToRetrievePool(Completion completionConfig, HashSet itemTplBlacklist) + protected HashSet GetItemsToRetrievePool(CompletionConfig completionConfigConfig, HashSet 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 /// /// Quest to add the conditions to - /// Completion config + /// Completion config /// Repeatable config /// Filtered item selection /// Budget in roubles /// Chosen item template Ids protected List GenerateAvailableForFinish( RepeatableQuest quest, - Completion completionConfig, + CompletionConfig completionConfigConfig, RepeatableQuestConfig repeatableConfig, List 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(); var usedItemIndexes = new HashSet(); @@ -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( /// /// Id of the item to request /// Amount of items of this specific type to request - /// Completion config from quest.json + /// Completion config from quest.json /// object of "Completion"-condition - 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; diff --git a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/ExplorationQuestGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/ExplorationQuestGenerator.cs index c7730d71..ae1f87c8 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/ExplorationQuestGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/RepeatableQuestGeneration/ExplorationQuestGenerator.cs @@ -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( /// True if location selected, false if no locations remain 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( /// /// Get the number of times the player needs to exit /// - /// Exploration config + /// Exploration config /// Is this a specific extract /// Number of exit requirements - 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( /// /// quest to add it to /// repeatable config + /// exploration config /// LocationInfo object with the generated data /// True if generated, false if not 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()) diff --git a/Libraries/SPTarkov.Server.Core/Helpers/RepeatableQuestHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/RepeatableQuestHelper.cs index 155c3cac..0904e6a8 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/RepeatableQuestHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/RepeatableQuestHelper.cs @@ -32,6 +32,19 @@ public class RepeatableQuestHelper( return repeatableConfig.QuestConfig.Elimination.FirstOrDefault(x => pmcLevel >= x.LevelRange.Min && pmcLevel <= x.LevelRange.Max); } + /// + /// Get the relevant exploration config based on the current players PMC level + /// + /// Level of PMC character + /// Main repeatable config + /// ExplorationConfig + public ExplorationConfig? GetExplorationConfigByPmcLevel(int pmcLevel, RepeatableQuestConfig repeatableConfig) + { + return repeatableConfig.QuestConfig.ExplorationConfig.FirstOrDefault(x => + pmcLevel >= x.LevelRange.Min && pmcLevel <= x.LevelRange.Max + ); + } + /// /// Gets a cloned repeatable quest template for the provided type with a unique id /// diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/QuestConfig.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/QuestConfig.cs index 9bb6c15b..134d3d0d 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/QuestConfig.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/QuestConfig.cs @@ -338,16 +338,16 @@ public record RepeatableQuestTypesConfig /// Defines exploration repeatable task generation parameters /// [JsonPropertyName("Exploration")] - public required Exploration Exploration { get; set; } + public required List ExplorationConfig { get; set; } /// /// Defines completion repeatable task generation parameters /// [JsonPropertyName("Completion")] - public required Completion Completion { get; set; } + public required CompletionConfig CompletionConfig { get; set; } /// - /// 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 /// [JsonPropertyName("Pickup")] public Pickup? Pickup { get; set; } @@ -359,8 +359,14 @@ public record RepeatableQuestTypesConfig public required List Elimination { get; set; } } -public record Exploration : BaseQuestConfig +public record ExplorationConfig : BaseQuestConfig { + /// + /// Level range at which elimination tasks should be generated from this config + /// + [JsonPropertyName("levelRange")] + public required MinMax LevelRange { get; set; } + /// /// Minimum extract count that a per map extract requirement can be generated with /// @@ -407,7 +413,7 @@ public record SpecificExits public required HashSet PassageRequirementWhitelist { get; set; } } -public record Completion : BaseQuestConfig +public record CompletionConfig : BaseQuestConfig { /// /// Minimum item count that can be requested