diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/quest.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/quest.json index 036e0964..8381ad65 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/quest.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/quest.json @@ -281,7 +281,7 @@ 0.03, 0.03 ], - "rewardSpread": 0.4, + "rewardSpread": 0.25, "skillRewardChance": [ 0, 1, @@ -517,26 +517,86 @@ } ], "questConfig": { - "Exploration": { - "maxExtracts": 4, - "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", @@ -1386,7 +1446,7 @@ 0.06, 0.07 ], - "rewardSpread": 0.5, + "rewardSpread": 0.35, "skillRewardChance": [ 0, 5, @@ -1605,26 +1665,86 @@ } ], "questConfig": { - "Exploration": { - "maxExtracts": 15, - "maxExtractsWithSpecificExit": 4, - "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", @@ -2546,7 +2666,7 @@ 0.04, 0.05 ], - "rewardSpread": 0.5, + "rewardSpread": 0.35, "skillRewardChance": [ 0, 0, @@ -2622,25 +2742,86 @@ } ], "questConfig": { - "Exploration": { - "possibleSkillRewards": [ - "Endurance", - "Strength", - "Vitality" - ], - "maxExtracts": 3, - "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 91e15ea0..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,15 +119,15 @@ 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 ) { - if (pool.Pool?.Exploration?.Locations?.Count is null or 0) + if (pool.Pool.Exploration.Locations?.Count is null or 0) { // there are no more locations left for exploration; delete it as a possible quest type - pool.Types = pool.Types?.Where(t => t != "Exploration").ToList(); + pool.Types = pool.Types.Where(t => t != "Exploration").ToList(); locationInfo = null; return false; } @@ -129,9 +137,9 @@ public class ExplorationQuestGenerator( var locationKey = randomUtil.DrawRandomFromDict(pool.Pool.Exploration.Locations)[0]; // Make the location info object - var locationTarget = pool.Pool!.Exploration!.Locations![locationKey]; + 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,15 +154,21 @@ 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 config, bool requiresSpecificExtract) + protected int GetNumberOfExits(ExplorationConfig explorationConfigConfig, bool requiresSpecificExtract) { - // Different max extract count when specific extract needed - var exitTimesMax = requiresSpecificExtract ? config.MaximumExtractsWithSpecificExit : config.MaximumExtracts + 1; + var exitTimesMin = requiresSpecificExtract + ? explorationConfigConfig.MinimumExtractsWithSpecificExit + : explorationConfigConfig.MinimumExtracts; - return randomUtil.RandInt(1, exitTimesMax); + // Different max extract count when specific extract needed + var exitTimesMax = requiresSpecificExtract + ? explorationConfigConfig.MaximumExtractsWithSpecificExit + 1 + : explorationConfigConfig.MaximumExtracts + 1; + + return randomUtil.RandInt(exitTimesMin, exitTimesMax); } /// @@ -225,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 ) { @@ -247,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/Eft/Common/Tables/Quest.cs b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/Quest.cs index 1d59015d..b6d5353e 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/Quest.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/Tables/Quest.cs @@ -329,7 +329,7 @@ public record QuestConditionCounter public record QuestConditionCounterCondition { [JsonPropertyName("id")] - public string? Id { get; set; } + public MongoId? Id { get; set; } [JsonPropertyName("dynamicLocale")] public bool? DynamicLocale { get; set; } diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/QuestConfig.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/QuestConfig.cs index 24beed00..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,14 +359,32 @@ 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 + /// + [JsonPropertyName("minExtracts")] + public required int MinimumExtracts { get; set; } + /// /// Maximum extract count that a per map extract requirement can be generated with /// [JsonPropertyName("maxExtracts")] public required int MaximumExtracts { get; set; } + /// + /// Minimum extract count that a specific extract can be generated with + /// + [JsonPropertyName("minExtractsWithSpecificExit")] + public required int MinimumExtractsWithSpecificExit { get; set; } + /// /// Maximum extract count that a specific extract can be generated with /// @@ -395,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