diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/bot.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/bot.json index 909db44f6..d0be49685 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/bot.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/bot.json @@ -1203,6 +1203,8 @@ ], "faceShieldIsActiveChancePercent": 85, "filterPlatesByLevel": true, + "skipBackPlateIfFrontPlateMissing": true, + "limitPlateClassToFrontPlateClass": true, "forceOnlyArmoredRigWhenNoArmor": true, "laserIsActiveChancePercent": 85, "lightIsActiveDayChancePercent": 25, diff --git a/Libraries/SPTarkov.Server.Core/Generators/BotEquipmentModGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/BotEquipmentModGenerator.cs index fc7693964..fdfa2cc98 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/BotEquipmentModGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/BotEquipmentModGenerator.cs @@ -107,9 +107,33 @@ public List GenerateModsForEquipment( logger.Warning($"bot: {settings.BotData.Role} lacks a mod slot pool for item: {parentTemplate.Id} {parentTemplate.Name}"); } + // Order the modpool by front plates, then backplates, then everything else + var orderedCompatibleModsPool = (compatibleModsPool ?? []).OrderBy(pair => + { + if (pair.Key.Equals("front_plate", StringComparison.OrdinalIgnoreCase)) + { + return 0; + } + + if (pair.Key.Equals("back_plate", StringComparison.OrdinalIgnoreCase)) + { + return 1; + } + + return 2; + }) + .ToList(); + + var frontPlateSpawned = false; // Iterate over mod pool and choose mods to add to item - foreach (var (modSlotName, modPool) in compatibleModsPool ?? []) + foreach (var (modSlotName, modPool) in orderedCompatibleModsPool) { + // Skip backplate slot if there's no front plate and bot should skip it via config + if (modSlotName.Equals("back_plate", StringComparison.OrdinalIgnoreCase) && settings.BotEquipmentConfig.SkipBackPlateIfFrontPlateMissing.GetValueOrDefault(false) && !frontPlateSpawned) + { + continue; + } + // Get the templates slot object from db var itemSlotTemplate = GetModItemSlotFromDbTemplate(modSlotName, parentTemplate); if (itemSlotTemplate is null) @@ -166,11 +190,23 @@ public List GenerateModsForEquipment( && itemHelper.IsRemovablePlateSlot(modSlotName.ToLowerInvariant()) ) { + int? frontPlateArmorClass = null; + if (modSlotName.Equals("back_plate", StringComparison.OrdinalIgnoreCase) && settings.BotEquipmentConfig.LimitPlateClassToFrontPlateClass.GetValueOrDefault(false)) + { + var frontPlate = equipment.FirstOrDefault(item => item.SlotId.Equals("front_plate", StringComparison.OrdinalIgnoreCase)); + + if (frontPlate != null) + { + frontPlateArmorClass = itemHelper.GetItem(frontPlate.Template).Value?.Properties?.ArmorClass; + } + } + var plateSlotFilteringOutcome = FilterPlateModsForSlotByLevel( settings, modSlotName.ToLowerInvariant(), compatibleModsPool.GetValueOrDefault(modSlotName), - parentTemplate + parentTemplate, + frontPlateArmorClass ); switch (plateSlotFilteringOutcome.Result) { @@ -238,6 +274,11 @@ public List GenerateModsForEquipment( var modId = new MongoId(); equipment.Add(CreateModItem(modId, modTpl.Value, parentId, modSlotName, modTemplate.Value, settings.BotData.Role)); + if (modSlotName.Equals("front_plate", StringComparison.OrdinalIgnoreCase)) + { + frontPlateSpawned = true; + } + // Does item being added exist in mod pool - has its own mod pool if (settings.ModPool.ContainsKey(modTpl.Value)) // Call self again with mod being added as item to add child mods to @@ -261,7 +302,8 @@ public FilterPlateModsForSlotByLevelResult FilterPlateModsForSlotByLevel( GenerateEquipmentProperties settings, string modSlot, HashSet existingPlateTplPool, - TemplateItem armorItem + TemplateItem armorItem, + int? maxArmorLevel = null ) { var result = new FilterPlateModsForSlotByLevelResult { Result = Result.UNKNOWN_FAILURE, PlateModTemplates = null }; @@ -293,12 +335,22 @@ TemplateItem armorItem // Choose a plate level based on weighting var chosenArmorPlateLevelString = weightedRandomHelper.GetWeightedValue(plateWeights); + // Check if the max plate value was sent over, if it's null then it shouldn't be trying to limit classes + if (maxArmorLevel != null) + { + var chosenLevel = int.Parse(chosenArmorPlateLevelString); + if (chosenLevel > maxArmorLevel.Value) + { + chosenArmorPlateLevelString = maxArmorLevel.Value.ToString(); + } + } + // Convert the array of ids into database items var platesFromDb = existingPlateTplPool.Select(plateTpl => itemHelper.GetItem(plateTpl).Value); // Filter plates to the chosen level based on its armorClass property var platesOfDesiredLevel = platesFromDb.Where(item => - item.Properties.ArmorClass.Value == double.Parse(chosenArmorPlateLevelString, CultureInfo.InvariantCulture) + item.Properties.ArmorClass.Value == int.Parse(chosenArmorPlateLevelString, CultureInfo.InvariantCulture) ); if (platesOfDesiredLevel.Any()) { diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/BotConfig.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/BotConfig.cs index 498b44432..ee25a0f2e 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/BotConfig.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/BotConfig.cs @@ -217,6 +217,19 @@ public record EquipmentFilters [JsonPropertyName("forceOnlyArmoredRigWhenNoArmor")] public bool? ForceOnlyArmoredRigWhenNoArmor { get; set; } + /// + /// Whether the bot should skip chances to have a back plate if the front plate is missing + /// + [JsonPropertyName("skipBackPlateIfFrontPlateMissing")] + public bool? SkipBackPlateIfFrontPlateMissing { get; set; } + + /// + /// Try to match the bot's back plate level to the front plate's level instead of being a better plate + /// Only works if filtering plates by level + /// + [JsonPropertyName("limitPlateClassToFrontPlateClass")] + public bool? LimitPlateClassToFrontPlateClass { get; set; } + /// /// Should plates be filtered by level ///