using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Generators.RepeatableQuestGeneration; using SPTarkov.Server.Core.Helpers; using SPTarkov.Server.Core.Models.Eft.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Spt.Config; using SPTarkov.Server.Core.Models.Spt.Repeatable; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Services; using SPTarkov.Server.Core.Utils; using SPTarkov.Server.Core.Utils.Cloners; using SPTarkov.Server.Core.Utils.Collections; using SPTarkov.Server.Core.Utils.Json; using BodyParts = SPTarkov.Server.Core.Constants.BodyParts; using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel; namespace SPTarkov.Server.Core.Generators; [Obsolete("In the process of being removed, do NOT add any new logic!!")] [Injectable] public class RepeatableQuestGenerator( ISptLogger _logger, RandomUtil _randomUtil, HashUtil _hashUtil, MathUtil _mathUtil, RepeatableQuestHelper _repeatableQuestHelper, ItemHelper _itemHelper, RepeatableQuestRewardGenerator _repeatableQuestRewardGenerator, DatabaseService _databaseService, LocalisationService _localisationService, ConfigServer _configServer, ICloner _cloner, // This is temporary while this is being refactored, eventually these will all live in the RepeatableQuestController. CompletionQuestGenerator _completionQuestGenerator ) { /// /// Body parts to present to the client as opposed to the body part information in quest data. /// private static readonly Dictionary> _bodyPartsToClient = new() { { BodyParts.Arms, [BodyParts.LeftArm, BodyParts.RightArm] }, { BodyParts.Legs, [BodyParts.LeftLeg, BodyParts.RightLeg] }, { BodyParts.Head, [BodyParts.Head] }, { BodyParts.Chest, [BodyParts.Chest, BodyParts.Stomach] }, }; protected int _maxRandomNumberAttempts = 6; protected QuestConfig _questConfig = _configServer.GetConfig(); /// /// This method is called by /GetClientRepeatableQuests/ and creates one element of quest type format (see /// assets/database/templates/repeatableQuests.json). /// It randomly draws a quest type (currently Elimination, Completion or Exploration) as well as a trader who is /// providing the quest /// /// Session id /// Player's level for requested items and reward generation /// Players trader standing/rep levels /// Possible quest types pool /// Repeatable quest config /// RepeatableQuest public RepeatableQuest? GenerateRepeatableQuest( string sessionId, int pmcLevel, Dictionary pmcTraderInfo, QuestTypePool questTypePool, RepeatableQuestConfig repeatableConfig ) { var questType = _randomUtil.DrawRandomFromList(questTypePool.Types).First(); // Get traders from whitelist and filter by quest type availability var traders = repeatableConfig .TraderWhitelist.Where(x => x.QuestTypes.Contains(questType)) .Select(x => x.TraderId) // filter out locked traders .Where(x => pmcTraderInfo[x].Unlocked.GetValueOrDefault(false)) .ToList(); var traderId = _randomUtil.DrawRandomFromList(traders).FirstOrDefault(); if (traderId is null) { // TODO: Localize me! _logger.Error( "Could not draw traderId from whitelist during repeatable quest generation" ); return null; } if (_logger.IsLogEnabled(LogLevel.Debug)) { _logger.Debug($"Generating operation task type: {questType} for {traderId}"); } return questType switch { "Elimination" => GenerateEliminationQuest( sessionId, pmcLevel, traderId, questTypePool, repeatableConfig ), "Completion" => _completionQuestGenerator.Generate( sessionId, pmcLevel, traderId, repeatableConfig ), "Exploration" => GenerateExplorationQuest( sessionId, pmcLevel, traderId, questTypePool, repeatableConfig ), "Pickup" => GeneratePickupQuest( sessionId, pmcLevel, traderId, questTypePool, repeatableConfig ), _ => null, }; } /// /// Generate a randomised Elimination quest /// /// Session id /// Player's level for requested items and reward generation /// Trader from which the quest will be provided /// Pools for quests (used to avoid redundant quests) /// /// The configuration for the repeatably kind (daily, weekly) as configured in QuestConfig /// for the requestd quest /// /// Object of quest type format for "Elimination" (see assets/database/templates/repeatableQuests.json) protected RepeatableQuest GenerateEliminationQuest( string sessionId, int pmcLevel, string traderId, QuestTypePool questTypePool, RepeatableQuestConfig repeatableConfig ) { var rand = new Random(); var eliminationConfig = _repeatableQuestHelper.GetEliminationConfigByPmcLevel( pmcLevel, repeatableConfig ); var locationsConfig = repeatableConfig.Locations; var targetsConfig = new ProbabilityObjectArray( _mathUtil, _cloner, eliminationConfig.Targets ); var bodyPartsConfig = new ProbabilityObjectArray>( _mathUtil, _cloner, eliminationConfig.BodyParts ); var weaponCategoryRequirementConfig = new ProbabilityObjectArray>( _mathUtil, _cloner, eliminationConfig.WeaponCategoryRequirements ); var weaponRequirementConfig = new ProbabilityObjectArray>( _mathUtil, _cloner, eliminationConfig.WeaponRequirements ); // the difficulty of the quest varies in difficulty depending on the condition // possible conditions are // - amount of npcs to kill // - type of npc to kill (scav, boss, pmc) // - with hit to what body part they should be killed // - from what distance they should be killed // a random combination of listed conditions can be required // possible conditions elements and their relative probability can be defined in QuestConfig.js // We use ProbabilityObjectArray to draw by relative probability. e.g. for targets: // "targets": { // "Savage": 7, // "AnyPmc": 2, // "bossBully": 0.5 // } // higher is more likely. We define the difficulty to be the inverse of the relative probability. // We want to generate a reward which is scaled by the difficulty of this mission. To get a upper bound with which we scale // the actual difficulty we calculate the minimum and maximum difficulty (max being the sum of max of each condition type // times the number of kills we have to perform): // The minimum difficulty is the difficulty for the most probable (= easiest target) with no additional conditions var minDifficulty = 1 / targetsConfig.MaxProbability(); // min difficulty is the lowest amount of scavs without any constraints // Target on bodyPart max. difficulty is that of the least probable element var maxTargetDifficulty = 1 / targetsConfig.MinProbability(); var maxBodyPartsDifficulty = eliminationConfig.MinKills / bodyPartsConfig.MinProbability(); // maxDistDifficulty is defined by 2, this could be a tuning parameter if we don't like the reward generation const int maxDistDifficulty = 2; var maxKillDifficulty = eliminationConfig.MaxKills; var targetPool = questTypePool.Pool.Elimination; targetsConfig = targetsConfig.Filter(x => targetPool.Targets.ContainsKey(x.Key)); if (targetsConfig.Count == 0 || targetsConfig.All(x => x.Data?.IsBoss ?? false)) { // There are no more targets left for elimination; delete it as a possible quest type // also if only bosses are left we need to leave otherwise it's a guaranteed boss elimination // -> then it would not be a quest with low probability anymore questTypePool.Types = questTypePool.Types.Where(t => t != "Elimination").ToList(); return null; } var botTypeToEliminate = targetsConfig.Draw()[0]; var targetDifficulty = 1 / targetsConfig.Probability(botTypeToEliminate); targetPool.Targets.TryGetValue(botTypeToEliminate, out var targetLocationPool); var locations = targetLocationPool.Locations; // we use any as location if "any" is in the pool, and we don't hit the specific location random // we use any also if the random condition is not met in case only "any" was in the pool var locationKey = "any"; if ( locations.Contains("any") && ( _randomUtil.GetChance100(eliminationConfig.SpecificLocationChance) || locations.Count <= 1 ) ) { locationKey = "any"; targetPool.Targets.Remove(botTypeToEliminate); } else { // Specific location locations = locations.Where(l => l != "any").ToList(); if (locations.Count > 0) { // Get name of location we want elimination to occur on locationKey = _randomUtil.DrawRandomFromList(locations).FirstOrDefault(); // Get a pool of locations the chosen bot type can be eliminated on if ( !targetPool.Targets.TryGetValue( botTypeToEliminate, out var possibleLocationPool ) ) { _logger.Warning( $"Bot to kill: {botTypeToEliminate} not found in elimination dict" ); } // Filter locations bot can be killed on to just those not chosen by key possibleLocationPool.Locations = possibleLocationPool .Locations.Where(location => location != locationKey) .ToList(); // None left after filtering if (possibleLocationPool.Locations.Count == 0) { // TODO: Why do any of this?! // Remove chosen bot to eliminate from pool targetPool.Targets.Remove(botTypeToEliminate); } } else { // Never should reach this if everything works out _logger.Error( _localisationService.GetText( "quest-repeatable_elimination_generation_failed_please_report" ) ); } } // draw the target body part and calculate the difficulty factor var bodyPartsToClient = new List(); var bodyPartDifficulty = 0d; if (_randomUtil.GetChance100(eliminationConfig.BodyPartChance)) { // if we add a bodyPart condition, we draw randomly one or two parts // each bodyPart of the BODYPARTS ProbabilityObjectArray includes the string(s) which need to be presented to the client in ProbabilityObjectArray.data // e.g. we draw "Arms" from the probability array but must present ["LeftArm", "RightArm"] to the client bodyPartsToClient = []; var bodyParts = bodyPartsConfig.Draw(_randomUtil.RandInt(1, 3), false); double probability = 0; foreach (var bodyPart in bodyParts) { // more than one part lead to an "OR" condition hence more parts reduce the difficulty probability += bodyPartsConfig.Probability(bodyPart).Value; if (_bodyPartsToClient.TryGetValue(bodyPart, out var bodyPartListToClient)) { bodyPartsToClient.AddRange(bodyPartListToClient); } else { bodyPartsToClient.Add(bodyPart); } } bodyPartDifficulty = 1 / probability; } // Draw a distance condition int? distance = null; var distanceDifficulty = 0; var isDistanceRequirementAllowed = !eliminationConfig.DistLocationBlacklist.Contains( locationKey ); if (targetsConfig.Data(botTypeToEliminate)?.IsBoss ?? false) { // Get all boss spawn information var bossSpawns = _databaseService .GetLocations() .GetDictionary() .Select(x => x.Value) .Where(x => x.Base?.Id != null) .Select(x => new { x.Base.Id, BossSpawn = x.Base.BossLocationSpawn }); // filter for the current boss to spawn on map var thisBossSpawns = bossSpawns .Select(x => new { x.Id, BossSpawn = x.BossSpawn.Where(e => e.BossName == botTypeToEliminate), }) .Where(x => x.BossSpawn.Count() > 0); // remove blacklisted locations var allowedSpawns = thisBossSpawns.Where(x => !eliminationConfig.DistLocationBlacklist.Contains(x.Id) ); // if the boss spawns on nom-blacklisted locations and the current location is allowed we can generate a distance kill requirement isDistanceRequirementAllowed = isDistanceRequirementAllowed && allowedSpawns.Any(); } if ( _randomUtil.GetChance100(eliminationConfig.DistanceProbability) && isDistanceRequirementAllowed ) { // Random distance with lower values more likely; simple distribution for starters... distance = (int) Math.Floor( Math.Abs(rand.NextDouble() - rand.NextDouble()) * (1 + eliminationConfig.MaxDistance - eliminationConfig.MinDistance) + eliminationConfig.MinDistance ); distance = (int)Math.Ceiling((decimal)(distance / 5)) * 5; distanceDifficulty = (int)( maxDistDifficulty * distance / eliminationConfig.MaxDistance ); } string? allowedWeaponsCategory = null; if (_randomUtil.GetChance100(eliminationConfig.WeaponCategoryRequirementProbability)) { // Filter out close range weapons from far distance requirement if (distance > 50) { List weaponTypeBlacklist = ["Shotgun", "Pistol"]; // Filter out close range weapons from long distance requirement weaponCategoryRequirementConfig.RemoveAll(category => weaponTypeBlacklist.Contains(category.Key) ); } else if (distance < 20) { List weaponTypeBlacklist = ["MarksmanRifle", "DMR"]; // Filter out far range weapons from close distance requirement weaponCategoryRequirementConfig.RemoveAll(category => weaponTypeBlacklist.Contains(category.Key) ); } // Pick a weighted weapon category var weaponRequirement = weaponCategoryRequirementConfig.Draw(1, false); // Get the hideout id value stored in the .data array allowedWeaponsCategory = weaponCategoryRequirementConfig.Data(weaponRequirement[0])[0]; } // Only allow a specific weapon requirement if a weapon category was not chosen string? allowedWeapon = null; if ( allowedWeaponsCategory is not null && eliminationConfig.WeaponRequirementProbability > rand.NextDouble() ) { var weaponRequirement = weaponRequirementConfig.Draw(1, false); var specificAllowedWeaponCategory = weaponRequirementConfig.Data(weaponRequirement[0]); var allowedWeapons = _itemHelper.GetItemTplsOfBaseType( specificAllowedWeaponCategory[0] ); allowedWeapon = _randomUtil.GetArrayValue(allowedWeapons); } // Draw how many npm kills are required var desiredKillCount = GetEliminationKillCount( botTypeToEliminate, targetsConfig, eliminationConfig ); var killDifficulty = desiredKillCount; // not perfectly happy here; we give difficulty = 1 to the quest reward generation when we have the most difficult mission // e.g. killing reshala 5 times from a distance of 200m with a headshot. var maxDifficulty = DifficultyWeighing(1, 1, 1, 1, 1); var curDifficulty = DifficultyWeighing( targetDifficulty.Value / maxTargetDifficulty, bodyPartDifficulty / maxBodyPartsDifficulty, distanceDifficulty / maxDistDifficulty, killDifficulty / maxKillDifficulty, allowedWeaponsCategory is not null || allowedWeapon is not null ? 1 : 0 ); // Aforementioned issue makes it a bit crazy since now all easier quests give significantly lower rewards than Completion / Exploration // I therefore moved the mapping a bit up (from 0.2...1 to 0.5...2) so that normal difficulty still gives good reward and having the // crazy maximum difficulty will lead to a higher difficulty reward gain factor than 1 var difficulty = _mathUtil.MapToRange(curDifficulty, minDifficulty, maxDifficulty, 0.5, 2); var quest = _repeatableQuestHelper.GenerateRepeatableTemplate( RepeatableQuestType.Elimination, traderId, repeatableConfig.Side, sessionId ); // ASSUMPTION: All fence quests are for scavs if (traderId == Traders.FENCE) { quest.Side = "Scav"; } var availableForFinishCondition = quest.Conditions.AvailableForFinish[0]; availableForFinishCondition.Counter.Id = _hashUtil.Generate(); availableForFinishCondition.Counter.Conditions = []; // Only add specific location condition if specific map selected if (locationKey != "any") { var locationId = Enum.Parse(locationKey); availableForFinishCondition.Counter.Conditions.Add( GenerateEliminationLocation(locationsConfig[locationId]) ); } availableForFinishCondition.Counter.Conditions.Add( GenerateEliminationCondition( botTypeToEliminate, bodyPartsToClient, distance, allowedWeapon, allowedWeaponsCategory ) ); availableForFinishCondition.Value = desiredKillCount; availableForFinishCondition.Id = _hashUtil.Generate(); quest.Location = GetQuestLocationByMapId(locationKey); quest.Rewards = _repeatableQuestRewardGenerator.GenerateReward( pmcLevel, Math.Min(difficulty, 1), traderId, repeatableConfig, eliminationConfig ); return quest; } /// /// Get a number of kills needed to complete elimination quest /// /// Target type desired e.g. anyPmc/bossBully/Savage /// Config of the target /// Config of the elimination /// Number of AI to kill protected int GetEliminationKillCount( string targetKey, ProbabilityObjectArray targetsConfig, EliminationConfig eliminationConfig ) { if (targetsConfig.Data(targetKey)?.IsBoss ?? false) { return _randomUtil.RandInt( eliminationConfig.MinBossKills, eliminationConfig.MaxBossKills + 1 ); } if (targetsConfig.Data(targetKey)?.IsPmc ?? false) { return _randomUtil.RandInt( eliminationConfig.MinPmcKills, eliminationConfig.MaxPmcKills + 1 ); } return _randomUtil.RandInt(eliminationConfig.MinKills, eliminationConfig.MaxKills + 1); } protected double DifficultyWeighing( double target, double bodyPart, int dist, int kill, int weaponRequirement ) { return Math.Sqrt(Math.Sqrt(target) + bodyPart + dist + weaponRequirement) * kill; } /// /// A repeatable quest, besides some more or less static components, exists of reward and condition (see /// assets/database/templates/repeatableQuests.json) /// This is a helper method for GenerateEliminationQuest to create a location condition. /// /// the location on which to fulfill the elimination quest /// Elimination-location-subcondition object protected QuestConditionCounterCondition GenerateEliminationLocation(List location) { return new QuestConditionCounterCondition { Id = _hashUtil.Generate(), DynamicLocale = true, Target = new ListOrT(location, null), ConditionType = "Location", }; } /// /// Create kill condition for an elimination quest /// /// Bot type target of elimination quest e.g. "AnyPmc", "Savage" /// Body parts player must hit /// Distance from which to kill (currently only >= supported) /// What weapon must be used - undefined = any /// What category of weapon must be used - undefined = any /// EliminationCondition object protected QuestConditionCounterCondition GenerateEliminationCondition( string target, List? targetedBodyParts, double? distance, string? allowedWeapon, string? allowedWeaponCategory ) { var killConditionProps = new QuestConditionCounterCondition { Id = _hashUtil.Generate(), DynamicLocale = true, Target = new ListOrT(null, target), // e,g, "AnyPmc" Value = 1, ResetOnSessionEnd = false, EnemyHealthEffects = [], Daytime = new DaytimeCounter { From = 0, To = 0 }, ConditionType = "Kills", }; if (target.StartsWith("boss")) { killConditionProps.Target = new ListOrT(null, "Savage"); killConditionProps.SavageRole = [target]; } // Has specific body part hit condition if (targetedBodyParts is not null) { killConditionProps.BodyPart = targetedBodyParts; } // Don't allow distance + melee requirement if (distance is not null && allowedWeaponCategory != "5b5f7a0886f77409407a7f96") { killConditionProps.Distance = new CounterConditionDistance { CompareMethod = ">=", Value = distance.Value, }; } // Has specific weapon requirement if (allowedWeapon is not null) { killConditionProps.Weapon = [allowedWeapon]; } // Has specific weapon category requirement if (allowedWeaponCategory?.Length > 0) { // TODO - fix - does weaponCategories exist? // killConditionProps.weaponCategories = [allowedWeaponCategory]; } return killConditionProps; } /// /// Generates a valid Exploration quest /// /// session id for the quest /// player's level for reward generation /// trader from which the quest will be provided /// Pools for quests (used to avoid redundant quests) /// /// The configuration for the repeatably kind (daily, weekly) as configured in QuestConfig /// for the requested quest /// /// object of quest type format for "Exploration" (see assets/database/templates/repeatableQuests.json) protected RepeatableQuest? GenerateExplorationQuest( string sessionId, int pmcLevel, string traderId, QuestTypePool questTypePool, RepeatableQuestConfig repeatableConfig ) { var explorationConfig = repeatableConfig.QuestConfig.Exploration; var requiresSpecificExtract = _randomUtil.Random.NextDouble() < repeatableConfig.QuestConfig.Exploration.SpecificExits.Probability; if (questTypePool.Pool.Exploration.Locations.Count == 0) { // there are no more locations left for exploration; delete it as a possible quest type questTypePool.Types = questTypePool.Types.Where(t => t != "Exploration").ToList(); return null; } // If location drawn is factory, it's possible to either get factory4_day and factory4_night or only one // of the both var locationKey = _randomUtil.DrawRandomFromDict(questTypePool.Pool.Exploration.Locations)[ 0 ]; var locationTarget = questTypePool.Pool.Exploration.Locations[locationKey]; // Remove the location from the available pool questTypePool.Pool.Exploration.Locations.Remove(locationKey); // Different max extract count when specific extract needed var exitTimesMax = requiresSpecificExtract ? explorationConfig.MaximumExtractsWithSpecificExit : explorationConfig.MaximumExtracts + 1; var numExtracts = _randomUtil.RandInt(1, exitTimesMax); var quest = _repeatableQuestHelper.GenerateRepeatableTemplate( RepeatableQuestType.Exploration, traderId, repeatableConfig.Side, sessionId ); var exitStatusCondition = new QuestConditionCounterCondition { Id = _hashUtil.Generate(), DynamicLocale = true, Status = ["Survived"], ConditionType = "ExitStatus", }; var locationCondition = new QuestConditionCounterCondition { Id = _hashUtil.Generate(), DynamicLocale = true, Target = new ListOrT(locationTarget, null), ConditionType = "Location", }; quest.Conditions.AvailableForFinish[0].Counter.Id = _hashUtil.Generate(); quest.Conditions.AvailableForFinish[0].Counter.Conditions = [ exitStatusCondition, locationCondition, ]; quest.Conditions.AvailableForFinish[0].Value = numExtracts; quest.Conditions.AvailableForFinish[0].Id = _hashUtil.Generate(); quest.Location = GetQuestLocationByMapId(locationKey.ToString()); if (requiresSpecificExtract) { // Fetch extracts for the requested side var mapExits = GetLocationExitsForSide(locationKey.ToString(), repeatableConfig.Side); // Only get exits that have a greater than 0% chance to spawn var exitPool = mapExits.Where(exit => exit.Chance > 0).ToList(); // Exclude exits with a requirement to leave (e.g. car extracts) var possibleExits = exitPool .Where(exit => exit.PassageRequirement is not null || repeatableConfig.QuestConfig.Exploration.SpecificExits.PassageRequirementWhitelist.Contains( "PassageRequirement" ) ) .ToList(); if (possibleExits.Count == 0) { _logger.Error( $"Unable to choose specific exit on map: {locationKey}, Possible exit pool was empty" ); } else { // Choose one of the exits we filtered above var chosenExit = _randomUtil.DrawRandomFromList(possibleExits)[0]; // Create a quest condition to leave raid via chosen exit var exitCondition = GenerateExplorationExitCondition(chosenExit); quest.Conditions.AvailableForFinish[0].Counter.Conditions.Add(exitCondition); } } // Difficulty for exploration goes from 1 extract to maxExtracts // Difficulty for reward goes from 0.2...1 -> map var difficulty = _mathUtil.MapToRange( numExtracts, 1, explorationConfig.MaximumExtracts, 0.2, 1 ); quest.Rewards = _repeatableQuestRewardGenerator.GenerateReward( pmcLevel, difficulty, traderId, repeatableConfig, explorationConfig ); return quest; } /// /// Filter a maps exits to just those for the desired side /// /// Map id (e.g. factory4_day) /// Pmc/Scav /// List of Exit objects protected List GetLocationExitsForSide(string locationKey, PlayerGroup playerGroup) { var mapExtracts = _databaseService.GetLocation(locationKey.ToLower()).AllExtracts; return mapExtracts.Where(exit => exit.Side == Enum.GetName(playerGroup)).ToList(); } protected RepeatableQuest GeneratePickupQuest( string sessionId, int pmcLevel, string traderId, QuestTypePool questTypePool, RepeatableQuestConfig repeatableConfig ) { var pickupConfig = repeatableConfig.QuestConfig.Pickup; var quest = _repeatableQuestHelper.GenerateRepeatableTemplate( RepeatableQuestType.Pickup, traderId, repeatableConfig.Side, sessionId ); var itemTypeToFetchWithCount = _randomUtil.GetArrayValue( pickupConfig.ItemTypeToFetchWithMaxCount ); var itemCountToFetch = _randomUtil.RandInt( itemTypeToFetchWithCount.MinimumPickupCount.Value, itemTypeToFetchWithCount.MaximumPickupCount + 1 ); // Choose location - doesnt seem to work for anything other than 'any' // var locationKey: string = this.randomUtil.drawRandomFromDict(questTypePool.pool.Pickup.locations)[0]; // var locationTarget = questTypePool.pool.Pickup.locations[locationKey]; var findCondition = quest.Conditions.AvailableForFinish.FirstOrDefault(x => x.ConditionType == "FindItem" ); findCondition.Target = new ListOrT([itemTypeToFetchWithCount.ItemType], null); findCondition.Value = itemCountToFetch; var counterCreatorCondition = quest.Conditions.AvailableForFinish.FirstOrDefault(x => x.ConditionType == "CounterCreator" ); // var locationCondition = counterCreatorCondition._props.counter.conditions.find(x => x._parent === "Location"); // (locationCondition._props as ILocationConditionProps).target = [...locationTarget]; var equipmentCondition = counterCreatorCondition.Counter.Conditions.FirstOrDefault(x => x.ConditionType == "Equipment" ); equipmentCondition.EquipmentInclusive = [ [itemTypeToFetchWithCount.ItemType], ]; // Add rewards quest.Rewards = _repeatableQuestRewardGenerator.GenerateReward( pmcLevel, 1, traderId, repeatableConfig, pickupConfig ); return quest; } /// /// Convert a location into an quest code can read (e.g. factory4_day into 55f2d3fd4bdc2d5f408b4567) /// /// e.g factory4_day /// guid protected string GetQuestLocationByMapId(string locationKey) { return _questConfig.LocationIdMap[locationKey]; } /// /// Exploration repeatable quests can specify a required extraction point. /// This method creates the according object which will be appended to the conditions list /// /// The exit name to generate the condition for /// Exit condition protected QuestConditionCounterCondition GenerateExplorationExitCondition(Exit exit) { return new QuestConditionCounterCondition { Id = _hashUtil.Generate(), DynamicLocale = true, ExitName = exit.Name, ConditionType = "ExitName", }; } }