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