diff --git a/Core/Controllers/RepeatableQuestController.cs b/Core/Controllers/RepeatableQuestController.cs index 592c9e4e..a2b7eab0 100644 --- a/Core/Controllers/RepeatableQuestController.cs +++ b/Core/Controllers/RepeatableQuestController.cs @@ -14,6 +14,7 @@ using Core.Servers; using Core.Services; using Core.Utils; using Core.Utils.Cloners; +using Core.Utils.Extensions; namespace Core.Controllers; @@ -315,28 +316,32 @@ public class RepeatableQuestController questPool.Pool.Pickup.Locations[ELocationName.any] = ["any"]; var eliminationConfig = _repeatableQuestHelper.GetEliminationConfigByPmcLevel(pmcLevel.Value, repeatableConfig); - var targetsConfig = _repeatableQuestHelper.ProbabilityObjectArray(eliminationConfig.Targets); + var targetsConfig = _repeatableQuestHelper.ProbabilityObjectArray(eliminationConfig.Targets); // Populate Elimination quest targets and their locations foreach (var target in targetsConfig) { // Target is boss - //if (target.isBoss) - //{ - // questPool.Pool.Elimination.Targets[targetKey] = new { locations: ["any"] }; - //} - //else - //{ - // // Non-boss targets - // var possibleLocations = locations; + if (target.Data.IsBoss) + { + var targets = questPool.Pool.Elimination.Targets.Get(target.Key); + targets.Locations.Clear(); + targets.Locations.Add("any"); + } + else + { + // Non-boss targets + var possibleLocations = locations; - // var allowedLocations = - // targetKey == "Savage" - // ? possibleLocations.filter((location) => location != "laboratory") // Exclude labs for Savage targets. - // : possibleLocations; + var targets = questPool.Pool.Elimination.Targets.Get(target.Key); + var allowedLocations = + target.Key == "Savage" + ? targets.Locations.Where((location) => location != "laboratory") // Exclude labs for Savage targets. + : possibleLocations; - // questPool.Pool.Elimination.Targets[targetKey] = new { Locations = allowedLocations }; - //} - _logger.Error("NOT IMPLEMENTED - GenerateQuestPool"); + + targets.Locations.Clear(); + targets.Locations.AddRange(allowedLocations); + } } return questPool; diff --git a/Core/Generators/RepeatableQuestGenerator.cs b/Core/Generators/RepeatableQuestGenerator.cs index c690ad82..abdd0c09 100644 --- a/Core/Generators/RepeatableQuestGenerator.cs +++ b/Core/Generators/RepeatableQuestGenerator.cs @@ -1,16 +1,13 @@ -using Core.Annotations; +using Core.Annotations; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; -using Core.Models.Eft.Health; -using Core.Models.Eft.Hideout; using Core.Models.Enums; using Core.Models.Spt.Config; using Core.Models.Spt.Repeatable; using Core.Models.Utils; using Core.Utils; -using static System.Runtime.InteropServices.JavaScript.JSType; -using System.Collections.Generic; using Core.Helpers; +using Core.Servers; using Core.Services; using Core.Utils.Collections; using Core.Utils.Extensions; @@ -23,12 +20,14 @@ public class RepeatableQuestGenerator { protected ISptLogger _logger; protected RandomUtil _randomUtil; - private readonly HashUtil _hashUtil; - private readonly MathUtil _mathUtil; - private readonly RepeatableQuestHelper _repeatableQuestHelper; - private readonly ItemHelper _itemHelper; - private readonly RepeatableQuestRewardGenerator _repeatableQuestRewardGenerator; - private readonly DatabaseService _databaseService; + protected HashUtil _hashUtil; + protected MathUtil _mathUtil; + protected RepeatableQuestHelper _repeatableQuestHelper; + protected ItemHelper _itemHelper; + protected RepeatableQuestRewardGenerator _repeatableQuestRewardGenerator; + protected DatabaseService _databaseService; + protected ConfigServer _configServer; + protected QuestConfig _questConfig; public RepeatableQuestGenerator( ISptLogger logger, @@ -38,7 +37,8 @@ public class RepeatableQuestGenerator RepeatableQuestHelper repeatableQuestHelper, ItemHelper itemHelper, RepeatableQuestRewardGenerator repeatableQuestRewardGenerator, - DatabaseService databaseService + DatabaseService databaseService, + ConfigServer configServer ) { _logger = logger; @@ -49,6 +49,9 @@ public class RepeatableQuestGenerator _itemHelper = itemHelper; _repeatableQuestRewardGenerator = repeatableQuestRewardGenerator; _databaseService = databaseService; + _configServer = configServer; + + _questConfig = _configServer.GetConfig(); } /// @@ -57,7 +60,7 @@ public class RepeatableQuestGenerator /// /// Session id /// Player's level for requested items and reward generation - /// Players traper standing/rep levels + /// Players trader standing/rep levels /// Possible quest types pool /// Repeatable quest config /// RepeatableQuest @@ -73,10 +76,10 @@ public class RepeatableQuestGenerator // Get traders from whitelist and filter by quest type availability var traders = repeatableConfig.TraderWhitelist - .Where((x) => x.QuestTypes.Contains(questType)) - .Select((x) => x.TraderId).ToList(); + .Where(x => x.QuestTypes.Contains(questType)) + .Select(x => x.TraderId).ToList(); // filter out locked traders - traders = traders.Where((x) => pmcTraderInfo[x].Unlocked.GetValueOrDefault(false)).ToList(); + traders = traders.Where(x => pmcTraderInfo[x].Unlocked.GetValueOrDefault(false)).ToList(); var traderId = _randomUtil.DrawRandomFromList(traders).FirstOrDefault(); return questType switch @@ -92,13 +95,14 @@ public class RepeatableQuestGenerator /// /// 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, + string sessionId, int pmcLevel, string traderId, QuestTypePool questTypePool, @@ -110,7 +114,7 @@ public class RepeatableQuestGenerator var eliminationConfig = _repeatableQuestHelper.GetEliminationConfigByPmcLevel(pmcLevel, repeatableConfig); var locationsConfig = repeatableConfig.Locations; var targetsConfig = _repeatableQuestHelper.ProbabilityObjectArray(eliminationConfig.Targets); - var bodypartsConfig = _repeatableQuestHelper.ProbabilityObjectArray>(eliminationConfig.BodyParts); + var bodyPartsConfig = _repeatableQuestHelper.ProbabilityObjectArray>(eliminationConfig.BodyParts); var weaponCategoryRequirementConfig = _repeatableQuestHelper.ProbabilityObjectArray>(eliminationConfig.WeaponCategoryRequirements); var weaponRequirementConfig = _repeatableQuestHelper.ProbabilityObjectArray>(eliminationConfig.WeaponRequirements); @@ -139,7 +143,7 @@ public class RepeatableQuestGenerator // Target on bodyPart max. difficulty is that of the least probable element var maxTargetDifficulty = 1 / targetsConfig.MinProbability(); - var maxBodyPartsDifficulty = eliminationConfig.MinKills / bodypartsConfig.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 var maxDistDifficulty = 2; @@ -159,13 +163,13 @@ public class RepeatableQuestGenerator var targetKey = targetsConfig.Draw()[0]; var targetDifficulty = 1 / targetsConfig.Probability(targetKey); - var locations = questTypePool.Pool.Elimination.Targets[targetKey].locations; + var locations = questTypePool.Pool.Elimination.Targets.Get(targetKey).Locations; // we use any as location if "any" is in the pool and we do not 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.includes("any") && - (eliminationConfig.SpecificLocationProbability < rand.Next() || locations.length <= 1) + if (locations.Contains("any") && + (eliminationConfig.SpecificLocationProbability < rand.Next() || locations.Count <= 1) ) { locationKey = "any"; @@ -173,13 +177,13 @@ public class RepeatableQuestGenerator } else { - locations = locations.Where((l) => l != "any"); - if (locations.length > 0) + locations = locations.Where(l => l != "any").ToList(); + if (locations.Count > 0) { - locationKey = _randomUtil.DrawRandomFromList(locations)[0]; - questTypePool.Pool.Elimination.Targets[targetKey].locations = locations.Where( - (l) => l != locationKey); - if (questTypePool.Pool.Elimination.Targets[targetKey].locations.length == 0) + locationKey = _randomUtil.DrawRandomFromList(locations).FirstOrDefault(); + questTypePool.Pool.Elimination.Targets.Get(targetKey).Locations = locations.Where( + (l) => l != locationKey).ToList(); + if (questTypePool.Pool.Elimination.Targets.Get(targetKey).Locations.Count == 0) { questTypePool.Pool.Elimination.Targets.Remove(targetKey); } @@ -193,20 +197,20 @@ public class RepeatableQuestGenerator // draw the target body part and calculate the difficulty factor var bodyPartsToClient = new List(); - var bodyPartDifficulty = 0; + var bodyPartDifficulty = 0d; if (eliminationConfig.BodyPartProbability > rand.Next()) { // 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); + var bodyParts = bodyPartsConfig.Draw(_randomUtil.RandInt(1, 3), false); double probability = 0; foreach (var bi in bodyParts) { // more than one part lead to an "OR" condition hence more parts reduce the difficulty - probability += bodypartsConfig.Probability(bi); - foreach (var biClient in bodypartsConfig.Data(bi)) + probability += bodyPartsConfig.Probability(bi).Value; + foreach (var biClient in bodyPartsConfig.Data(bi)) { bodyPartsToClient.Add(biClient); } @@ -215,11 +219,11 @@ public class RepeatableQuestGenerator } // Draw a distance condition - int distance; + int? distance = -1; var distanceDifficulty = 0; var isDistanceRequirementAllowed = !eliminationConfig.DistLocationBlacklist.Contains(locationKey); - if (targetsConfig.Data(targetKey).IsBoss) + if (targetsConfig.Data(targetKey).IsBoss.GetValueOrDefault(false)) { // Get all boss spawn information var bossSpawns = _databaseService.GetLocations().GetDictionary() @@ -243,27 +247,31 @@ public class RepeatableQuestGenerator if (eliminationConfig.DistanceProbability > rand.Next() && isDistanceRequirementAllowed) { // Random distance with lower values more likely; simple distribution for starters... - distance = Math.Floor( - Math.Abs(Math.Random() - rand.Next()) * (1 + eliminationConfig.MaxDistance - eliminationConfig.MinDistance) + - eliminationConfig.MinDistance); - distance = Math.Ceiling(distance / 5) * 5; - distanceDifficulty = (maxDistDifficulty * distance) / eliminationConfig.MaxDistance; + distance = (int)Math.Floor( + (decimal)(Math.Abs(rand.Next(0, 1) - rand.Next(0, 1)) * (1 + eliminationConfig.MaxDistance - eliminationConfig.MinDistance) + + eliminationConfig.MinDistance)); + distance = (int)Math.Ceiling((decimal)(distance / 5)) * 5; + distanceDifficulty = (int)(maxDistDifficulty * distance / eliminationConfig.MaxDistance); } - string allowedWeaponsCategory; + string? allowedWeaponsCategory = null; if (eliminationConfig.WeaponCategoryRequirementProbability > rand.Next()) { // Filter out close range weapons from far distance requirement if (distance > 50) { - weaponCategoryRequirementConfig = weaponCategoryRequirementConfig.Where((category) => - ["Shotgun", "Pistol"].Contains(category.Key)); + List weaponTypes = ["Shotgun", "Pistol"]; + weaponCategoryRequirementConfig = (ProbabilityObjectArray>)weaponCategoryRequirementConfig + .Where((category) => weaponTypes + .Contains(category.Key)); } else if (distance < 20) { + List weaponTypes = ["MarksmanRifle", "DMR"]; // Filter out far range weapons from close distance requirement - weaponCategoryRequirementConfig = weaponCategoryRequirementConfig.Where((category) => - ["MarksmanRifle", "DMR"].Contains(category.Key)); + weaponCategoryRequirementConfig = (ProbabilityObjectArray>)weaponCategoryRequirementConfig + .Where((category) => weaponTypes + .Contains(category.Key)); } // Pick a weighted weapon category @@ -274,12 +282,12 @@ public class RepeatableQuestGenerator } // Only allow a specific weapon requirement if a weapon category was not chosen - string allowedWeapon; - if (!allowedWeaponsCategory && eliminationConfig.WeaponRequirementProb > rand.Next()) + string? allowedWeapon = null; + if (allowedWeaponsCategory is not null && eliminationConfig.WeaponRequirementProbability > rand.Next()) { var weaponRequirement = weaponRequirementConfig.Draw(1, false); - var allowedWeaponsCategory = weaponRequirementConfig.Data(weaponRequirement[0])[0]; - var allowedWeapons = _itemHelper.GetItemTplsOfBaseType(allowedWeaponsCategory); + var specificAllowedWeaponCategory = weaponRequirementConfig.Data(weaponRequirement[0])[0]; + var allowedWeapons = _itemHelper.GetItemTplsOfBaseType(specificAllowedWeaponCategory); allowedWeapon = _randomUtil.GetArrayValue(allowedWeapons); } @@ -291,18 +299,18 @@ public class RepeatableQuestGenerator // 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 / maxTargetDifficulty, - bodyPartDifficulty / maxBodyPartsDifficulty, + targetDifficulty.Value / maxTargetDifficulty, + bodyPartDifficulty / maxBodyPartsDifficulty.Value, distanceDifficulty / maxDistDifficulty, - killDifficulty / maxKillDifficulty, - allowedWeaponsCategory || allowedWeapon ? 1 : 0); + killDifficulty / maxKillDifficulty.Value, + 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 = GenerateRepeatableTemplate("Elimination", traderId, repeatableConfig.Side, sessionid); + var quest = GenerateRepeatableTemplate("Elimination", traderId, repeatableConfig.Side, sessionId); // ASSUMPTION: All fence quests are for scavs if (traderId == Traders.FENCE) @@ -317,13 +325,14 @@ public class RepeatableQuestGenerator // Only add specific location condition if specific map selected if (locationKey != "any") { - availableForFinishCondition.Counter.Conditions.Add(GenerateEliminationLocation(locationsConfig[locationKey])); + Enum.TryParse(typeof (ELocationName), locationKey, true, out var locationId); + availableForFinishCondition.Counter.Conditions.Add(GenerateEliminationLocation(locationsConfig[(ELocationName)locationId])); } availableForFinishCondition.Counter.Conditions.Add( GenerateEliminationCondition( targetKey, bodyPartsToClient, - distance, + distance.Value, allowedWeapon, allowedWeaponsCategory) ); @@ -350,32 +359,31 @@ public class RepeatableQuestGenerator */ protected int GetEliminationKillCount( string targetKey, - object targetsConfig, //ProbabilityObjectArray + ProbabilityObjectArray targetsConfig, EliminationConfig eliminationConfig) { - if (targetsConfig.Data(targetKey).isBoss) + if (targetsConfig.Data(targetKey).IsBoss.GetValueOrDefault(false)) { - return _randomUtil.RandInt(eliminationConfig.MinBossKills, eliminationConfig.MaxBossKills + 1); + return _randomUtil.RandInt(eliminationConfig.MinBossKills.Value, eliminationConfig.MaxBossKills + 1); } - if (targetsConfig.Data(targetKey).isPmc) + if (targetsConfig.Data(targetKey).IsPmc.GetValueOrDefault(false)) { - return _randomUtil.RandInt(eliminationConfig.MinPmcKills, eliminationConfig.MaxPmcKills + 1); + return _randomUtil.RandInt(eliminationConfig.MinPmcKills.Value, eliminationConfig.MaxPmcKills + 1); } - return _randomUtil.RandInt(eliminationConfig.MinKills, eliminationConfig.MaxKills + 1); + return _randomUtil.RandInt(eliminationConfig.MinKills.Value, eliminationConfig.MaxKills + 1); } protected double DifficultyWeighing( - int target, - int bodyPart, + double target, + double bodyPart, int dist, int kill, int weaponRequirement) { return Math.Sqrt(Math.Sqrt(target) + bodyPart + dist + weaponRequirement) * kill; } -} /// /// Get a number of kills needed to complete elimination quest @@ -415,13 +423,54 @@ public class RepeatableQuestGenerator /// EliminationCondition object protected QuestConditionCounterCondition GenerateEliminationCondition( string target, - List targetedBodyParts, - double distance, - string allowedWeapon, - string allowedWeaponCategory + List? targetedBodyParts, + double? distance, + string? allowedWeapon, + string? allowedWeaponCategory ) { - throw new NotImplementedException(); + var killConditionProps = new QuestConditionCounterCondition{ + Id = _hashUtil.Generate(), + DynamicLocale = true, + Target = target, // e,g, "AnyPmc" + Value = 1, + ResetOnSessionEnd = false, + EnemyHealthEffects = [], + Daytime = new DaytimeCounter(){ From = 0, To = 0 }, + ConditionType = "Kills"}; + + if (target.StartsWith("boss")) + { + killConditionProps.Target = "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; } /// @@ -500,7 +549,7 @@ public class RepeatableQuestGenerator /// guid protected string GetQuestLocationByMapId(string locationKey) { - throw new NotImplementedException(); + return _questConfig.LocationIdMap[locationKey]; } /// diff --git a/Core/Models/Eft/Common/Tables/Quest.cs b/Core/Models/Eft/Common/Tables/Quest.cs index d0abe9da..ce3196e4 100644 --- a/Core/Models/Eft/Common/Tables/Quest.cs +++ b/Core/Models/Eft/Common/Tables/Quest.cs @@ -435,7 +435,7 @@ public record ValueCompare public record CounterConditionDistance { [JsonPropertyName("value")] - public int? Value { get; set; } + public double? Value { get; set; } [JsonPropertyName("compareMethod")] public string? CompareMethod { get; set; } diff --git a/Core/Utils/Extensions/ObjectExtensions.cs b/Core/Utils/Extensions/ObjectExtensions.cs index b3b1d961..5c5644a7 100644 --- a/Core/Utils/Extensions/ObjectExtensions.cs +++ b/Core/Utils/Extensions/ObjectExtensions.cs @@ -1,3 +1,5 @@ +using Core.Models.Spt.Repeatable; + namespace Core.Utils.Extensions { public static class ObjectExtensions @@ -6,5 +8,16 @@ namespace Core.Utils.Extensions { return obj.GetType().GetProperties().Any(x => x.Name == key.ToString()); } + + public static T? Get(this object obj, string toLower) + { + return (T?)obj.GetType().GetProperties().SingleOrDefault(p => p.GetJsonName() == toLower)?.GetValue(obj); + } + + + public static void Remove(this EliminationTargetPool pool, T key) + { + + } } }