diff --git a/project/assets/configs/bot.json b/project/assets/configs/bot.json index 49249372..3794a9d1 100644 --- a/project/assets/configs/bot.json +++ b/project/assets/configs/bot.json @@ -1432,7 +1432,7 @@ "65392f611406374f82152ba5", "653931da5db71d30ab1d6296" ], - "mod_nvg": ["5c11046cd174af02a012e42b", "5c110624d174af029e69734c"], + "mod_nvg": ["5c11046cd174af02a012e42b"], "mod_reciever": ["5d4405aaa4b9361e6a4e6bd3"], "mod_stock": ["5cde739cd7f00c0010373bd3"], "mod_rear_sight": ["5a0ed824fcdbcb0176308b0d"], diff --git a/project/src/generators/BotEquipmentModGenerator.ts b/project/src/generators/BotEquipmentModGenerator.ts index 2b8d14f2..a551b642 100644 --- a/project/src/generators/BotEquipmentModGenerator.ts +++ b/project/src/generators/BotEquipmentModGenerator.ts @@ -70,7 +70,8 @@ export class BotEquipmentModGenerator { * @param equipment Equipment item to add mods to * @param modPool Mod list to choose frm * @param parentId parentid of item to add mod to - * @param parentTemplate template objet of item to add mods to + * @param parentTemplate Template object of item to add mods to + * @param specificBlacklist The relevant blacklist from bot.json equipment dictionary * @param forceSpawn should this mod be forced to spawn * @returns Item + compatible mods as an array */ @@ -79,19 +80,22 @@ export class BotEquipmentModGenerator { parentId: string, parentTemplate: ITemplateItem, settings: IGenerateEquipmentProperties, + specificBlacklist: EquipmentFilterDetails, shouldForceSpawn = false, ): IItem[] { let forceSpawn = shouldForceSpawn; + // Get mod pool for the desired item const compatibleModsPool = settings.modPool[parentTemplate._id]; if (!compatibleModsPool) { this.logger.warning( - `bot: ${settings.botRole} lacks a mod slot pool for item: ${parentTemplate._id} ${parentTemplate._name}`, + `bot: ${settings.botData.role} lacks a mod slot pool for item: ${parentTemplate._id} ${parentTemplate._name}`, ); } // Iterate over mod pool and choose mods to add to item for (const modSlotName in compatibleModsPool) { + // Get the templates slot object from db const itemSlotTemplate = this.getModItemSlotFromDb(modSlotName, parentTemplate); if (!itemSlotTemplate) { this.logger.error( @@ -99,7 +103,7 @@ export class BotEquipmentModGenerator { modSlot: modSlotName, parentId: parentTemplate._id, parentName: parentTemplate._name, - botRole: settings.botRole, + botRole: settings.botData.role, }), ); @@ -126,6 +130,13 @@ export class BotEquipmentModGenerator { // Get pool of items we can add for this slot let modPoolToChooseFrom = compatibleModsPool[modSlotName]; + // Filter the pool of items in blacklist + const filteredModPool = this.filterModsByBlacklist(modPoolToChooseFrom, specificBlacklist, modSlotName); + if (filteredModPool.length > 0) { + // use filtered pool as it has items in it + modPoolToChooseFrom = filteredModPool; + } + // Slot can hold armor plates + we are filtering possible items by bot level, handle if ( settings.botEquipmentConfig.filterPlatesByLevel && @@ -186,18 +197,35 @@ export class BotEquipmentModGenerator { // Get chosen mods db template and check it fits into slot const modTemplate = this.itemHelper.getItem(modTpl); - if (!this.isModValidForSlot(modTemplate, itemSlotTemplate, modSlotName, parentTemplate, settings.botRole)) { + if ( + !this.isModValidForSlot( + modTemplate, + itemSlotTemplate, + modSlotName, + parentTemplate, + settings.botData.role, + ) + ) { continue; } // Generate new id to ensure all items are unique on bot const modId = this.hashUtil.generate(); - equipment.push(this.createModItem(modId, modTpl, parentId, modSlotName, modTemplate[1], settings.botRole)); + equipment.push( + this.createModItem(modId, modTpl, parentId, modSlotName, modTemplate[1], settings.botData.role), + ); // Does item being added exist in mod pool - has its own mod pool if (Object.keys(settings.modPool).includes(modTpl)) { // Call self again with mod being added as item to add child mods to - this.generateModsForEquipment(equipment, modId, modTemplate[1], settings, forceSpawn); + this.generateModsForEquipment( + equipment, + modId, + modTemplate[1], + settings, + specificBlacklist, + forceSpawn, + ); } } @@ -234,7 +262,8 @@ export class BotEquipmentModGenerator { // Get the front/back/side weights based on bots level const plateSlotWeights = settings.botEquipmentConfig?.armorPlateWeighting?.find( (armorWeight) => - settings.botLevel >= armorWeight.levelRange.min && settings.botLevel <= armorWeight.levelRange.max, + settings.botData.level >= armorWeight.levelRange.min && + settings.botData.level <= armorWeight.levelRange.max, ); if (!plateSlotWeights) { // No weights, return original array of plate tpls @@ -726,25 +755,26 @@ export class BotEquipmentModGenerator { /** * Randomly choose if a mod should be spawned, 100% for required mods OR mod is ammo slot - * @param itemSlot slot the item sits in - * @param modSlot slot the mod sits in + * @param itemSlot slot the item sits in from db + * @param modSlotName Name of slot the mod sits in * @param modSpawnChances Chances for various mod spawns * @param botEquipConfig Various config settings for generating this type of bot * @returns ModSpawn.SPAWN when mod should be spawned, ModSpawn.DEFAULT_MOD when default mod should spawn, ModSpawn.SKIP when mod is skipped */ protected shouldModBeSpawned( itemSlot: ISlot, - modSlot: string, + modSlotName: string, modSpawnChances: IModsChances, botEquipConfig: EquipmentFilters, ): ModSpawn { const slotRequired = itemSlot._required; - if (this.getAmmoContainers().includes(modSlot)) { + if (this.getAmmoContainers().includes(modSlotName)) { + // Always force mags/cartridges in weapon to spawn return ModSpawn.SPAWN; } - const spawnMod = this.probabilityHelper.rollChance(modSpawnChances[modSlot]); - if (!spawnMod && (slotRequired || botEquipConfig.weaponSlotIdsToMakeRequired?.includes(modSlot))) { - // Mod is required but spawn chance roll failed, choose default mod spawn for slot + const spawnMod = this.probabilityHelper.rollChance(modSpawnChances[modSlotName]); + if (!spawnMod && (slotRequired || botEquipConfig.weaponSlotIdsToMakeRequired?.includes(modSlotName))) { + // Edge case: Mod is required but spawn chance roll failed, choose default mod spawn for slot return ModSpawn.DEFAULT_MOD; } @@ -1249,11 +1279,7 @@ export class BotEquipmentModGenerator { const supportedSubMods = desiredSlotObject._props.filters[0].Filter; if (supportedSubMods) { // Filter mods - let filteredMods = this.filterWeaponModsByBlacklist( - supportedSubMods, - botEquipBlacklist, - desiredSlotName, - ); + let filteredMods = this.filterModsByBlacklist(supportedSubMods, botEquipBlacklist, desiredSlotName); if (filteredMods.length === 0) { this.logger.warning( this.localisationService.getText("bot-unable_to_filter_mods_all_blacklisted", { @@ -1289,7 +1315,7 @@ export class BotEquipmentModGenerator { this.botEquipmentModPoolService.getCompatibleModsForWeaponSlot(parentItemId, modSlot), ); - const filteredMods = this.filterWeaponModsByBlacklist(modsFromDynamicPool, botEquipBlacklist, modSlot); + const filteredMods = this.filterModsByBlacklist(modsFromDynamicPool, botEquipBlacklist, modSlot); if (filteredMods.length === 0) { this.logger.warning( this.localisationService.getText("bot-unable_to_filter_mod_slot_all_blacklisted", modSlot), @@ -1307,7 +1333,7 @@ export class BotEquipmentModGenerator { * @param modSlot Slot mods belong to * @returns Filtered array of mod tpls */ - protected filterWeaponModsByBlacklist( + protected filterModsByBlacklist( allowedMods: string[], botEquipBlacklist: EquipmentFilterDetails, modSlot: string, diff --git a/project/src/generators/BotInventoryGenerator.ts b/project/src/generators/BotInventoryGenerator.ts index 2b631033..3cc8d6c5 100644 --- a/project/src/generators/BotInventoryGenerator.ts +++ b/project/src/generators/BotInventoryGenerator.ts @@ -6,6 +6,7 @@ import { BotWeaponGenerator } from "@spt/generators/BotWeaponGenerator"; import { BotGeneratorHelper } from "@spt/helpers/BotGeneratorHelper"; import { BotHelper } from "@spt/helpers/BotHelper"; import { ItemHelper } from "@spt/helpers/ItemHelper"; +import { ProfileHelper } from "@spt/helpers/ProfileHelper"; import { WeatherHelper } from "@spt/helpers/WeatherHelper"; import { WeightedRandomHelper } from "@spt/helpers/WeightedRandomHelper"; import { IInventory as PmcInventory } from "@spt/models/eft/common/tables/IBotBase"; @@ -20,6 +21,7 @@ import { IGenerateEquipmentProperties } from "@spt/models/spt/bots/IGenerateEqui import { EquipmentFilterDetails, IBotConfig } from "@spt/models/spt/config/IBotConfig"; import { ILogger } from "@spt/models/spt/utils/ILogger"; import { ConfigServer } from "@spt/servers/ConfigServer"; +import { BotEquipmentFilterService } from "@spt/services/BotEquipmentFilterService"; import { BotEquipmentModPoolService } from "@spt/services/BotEquipmentModPoolService"; import { DatabaseService } from "@spt/services/DatabaseService"; import { LocalisationService } from "@spt/services/LocalisationService"; @@ -40,11 +42,13 @@ export class BotInventoryGenerator { @inject("BotWeaponGenerator") protected botWeaponGenerator: BotWeaponGenerator, @inject("BotLootGenerator") protected botLootGenerator: BotLootGenerator, @inject("BotGeneratorHelper") protected botGeneratorHelper: BotGeneratorHelper, + @inject("ProfileHelper") protected profileHelper: ProfileHelper, @inject("BotHelper") protected botHelper: BotHelper, @inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper, @inject("ItemHelper") protected itemHelper: ItemHelper, @inject("WeatherHelper") protected weatherHelper: WeatherHelper, @inject("LocalisationService") protected localisationService: LocalisationService, + @inject("BotEquipmentFilterService") protected botEquipmentFilterService: BotEquipmentFilterService, @inject("BotEquipmentModPoolService") protected botEquipmentModPoolService: BotEquipmentModPoolService, @inject("BotEquipmentModGenerator") protected botEquipmentModGenerator: BotEquipmentModGenerator, @inject("ConfigServer") protected configServer: ConfigServer, @@ -83,6 +87,7 @@ export class BotInventoryGenerator { ?.getValue(); this.generateAndAddEquipmentToBot( + sessionId, templateInventory, wornItemChances, botRole, @@ -142,6 +147,7 @@ export class BotInventoryGenerator { /** * Add equipment to a bot + * @param sessionId Session id * @param templateInventory bot/x.json data from db * @param wornItemChances Chances items will be added to bot * @param botRole Role bot has (assault/pmcBot) @@ -150,6 +156,7 @@ export class BotInventoryGenerator { * @param chosenGameVersion Game version for bot, only really applies for PMCs */ protected generateAndAddEquipmentToBot( + sessionId: string, templateInventory: IInventory, wornItemChances: IChances, botRole: string, @@ -193,8 +200,16 @@ export class BotInventoryGenerator { } } + // Get profile of player generating bots, we use their level later on + const pmcProfile = this.profileHelper.getPmcProfile(sessionId); + const botEquipmentRole = this.botGeneratorHelper.getBotEquipmentRole(botRole); + + // Iterate over all equipment slots of bot, do it in specifc order to reduce conflicts + // e.g. ArmorVest should be generated after TactivalVest + // or FACE_COVER before HEADWEAR for (const equipmentSlot in templateInventory.equipment) { - // Weapons have special generation and will be generated separately; ArmorVest should be generated after TactivalVest + // Skip some slots as they need to be done in a specific order + with specific parameter values + // e.g. Weapons if (excludedSlots.includes(equipmentSlot)) { continue; } @@ -204,73 +219,74 @@ export class BotInventoryGenerator { rootEquipmentPool: templateInventory.equipment[equipmentSlot], modPool: templateInventory.mods, spawnChances: wornItemChances, - botRole: botRole, - botLevel: botLevel, + botData: { role: botRole, level: botLevel, equipmentRole: botEquipmentRole }, inventory: botInventory, botEquipmentConfig: botEquipConfig, randomisationDetails: randomistionDetails, + generatingPlayerLevel: pmcProfile.Info.Level, }); } // Generate below in specific order this.generateEquipment({ rootEquipmentSlot: EquipmentSlots.POCKETS, + // Unheard profiles have unique sized pockets, TODO - handle this somewhere else in a better way rootEquipmentPool: chosenGameVersion === GameEditions.UNHEARD ? { [ItemTpl.POCKETS_1X4_TUE]: 1 } : templateInventory.equipment.Pockets, modPool: templateInventory.mods, spawnChances: wornItemChances, - botRole: botRole, - botLevel: botLevel, + botData: { role: botRole, level: botLevel, equipmentRole: botEquipmentRole }, inventory: botInventory, botEquipmentConfig: botEquipConfig, randomisationDetails: randomistionDetails, generateModsBlacklist: [ItemTpl.POCKETS_1X4_TUE], + generatingPlayerLevel: pmcProfile.Info.Level, }); this.generateEquipment({ rootEquipmentSlot: EquipmentSlots.FACE_COVER, rootEquipmentPool: templateInventory.equipment.FaceCover, modPool: templateInventory.mods, spawnChances: wornItemChances, - botRole: botRole, - botLevel: botLevel, + botData: { role: botRole, level: botLevel, equipmentRole: botEquipmentRole }, inventory: botInventory, botEquipmentConfig: botEquipConfig, randomisationDetails: randomistionDetails, + generatingPlayerLevel: pmcProfile.Info.Level, }); this.generateEquipment({ rootEquipmentSlot: EquipmentSlots.HEADWEAR, rootEquipmentPool: templateInventory.equipment.Headwear, modPool: templateInventory.mods, spawnChances: wornItemChances, - botRole: botRole, - botLevel: botLevel, + botData: { role: botRole, level: botLevel, equipmentRole: botEquipmentRole }, inventory: botInventory, botEquipmentConfig: botEquipConfig, randomisationDetails: randomistionDetails, + generatingPlayerLevel: pmcProfile.Info.Level, }); this.generateEquipment({ rootEquipmentSlot: EquipmentSlots.EARPIECE, rootEquipmentPool: templateInventory.equipment.Earpiece, modPool: templateInventory.mods, spawnChances: wornItemChances, - botRole: botRole, - botLevel: botLevel, + botData: { role: botRole, level: botLevel, equipmentRole: botEquipmentRole }, inventory: botInventory, botEquipmentConfig: botEquipConfig, randomisationDetails: randomistionDetails, + generatingPlayerLevel: pmcProfile.Info.Level, }); const hasArmorVest = this.generateEquipment({ rootEquipmentSlot: EquipmentSlots.ARMOR_VEST, rootEquipmentPool: templateInventory.equipment.ArmorVest, modPool: templateInventory.mods, spawnChances: wornItemChances, - botRole: botRole, - botLevel: botLevel, + botData: { role: botRole, level: botLevel, equipmentRole: botEquipmentRole }, inventory: botInventory, botEquipmentConfig: botEquipConfig, randomisationDetails: randomistionDetails, + generatingPlayerLevel: pmcProfile.Info.Level, }); // Bot has no armor vest and flagged to be forceed to wear armored rig in this event @@ -295,11 +311,11 @@ export class BotInventoryGenerator { rootEquipmentPool: templateInventory.equipment.TacticalVest, modPool: templateInventory.mods, spawnChances: wornItemChances, - botRole: botRole, - botLevel: botLevel, + botData: { role: botRole, level: botLevel, equipmentRole: botEquipmentRole }, inventory: botInventory, botEquipmentConfig: botEquipConfig, randomisationDetails: randomistionDetails, + generatingPlayerLevel: pmcProfile.Info.Level, }); } @@ -360,6 +376,7 @@ export class BotInventoryGenerator { /** * Add a piece of equipment with mods to inventory from the provided pools + * @param sessionId Session id * @param settings Values to adjust how item is chosen and added to bot * @returns true when item added */ @@ -381,11 +398,13 @@ export class BotInventoryGenerator { return false; } + // Roll dice on equipment item const shouldSpawn = this.randomUtil.getChance100(spawnChance); if (shouldSpawn && Object.keys(settings.rootEquipmentPool).length) { let pickedItemDb: ITemplateItem; let found = false; + // Limit attempts to find a compatible item as its expensive to check them all const maxAttempts = Math.round(Object.keys(settings.rootEquipmentPool).length * 0.75); // Roughly 75% of pool size let attempts = 0; while (!found) { @@ -398,7 +417,7 @@ export class BotInventoryGenerator { if (!dbResult[0]) { this.logger.error(this.localisationService.getText("bot-missing_item_template", chosenItemTpl)); - this.logger.info(`EquipmentSlot -> ${settings.rootEquipmentSlot}`); + this.logger.debug(`EquipmentSlot -> ${settings.rootEquipmentSlot}`); // remove picked item delete settings.rootEquipmentPool[chosenItemTpl]; @@ -408,6 +427,7 @@ export class BotInventoryGenerator { continue; } + // Is the chosen item compatible with other items equipped const compatibilityResult = this.botGeneratorHelper.isItemIncompatibleWithCurrentItems( settings.inventory.items, chosenItemTpl, @@ -438,32 +458,38 @@ export class BotInventoryGenerator { _tpl: pickedItemDb._id, parentId: settings.inventory.equipment, slotId: settings.rootEquipmentSlot, - ...this.botGeneratorHelper.generateExtraPropertiesForItem(pickedItemDb, settings.botRole), + ...this.botGeneratorHelper.generateExtraPropertiesForItem(pickedItemDb, settings.botData.role), }; - // Use dynamic mod pool if enabled in config for this bot - const botEquipmentRole = this.botGeneratorHelper.getBotEquipmentRole(settings.botRole); + const botEquipBlacklist = this.botEquipmentFilterService.getBotEquipmentBlacklist( + settings.botData.equipmentRole, + settings.generatingPlayerLevel, + ); + + // Edge case: Filter the armor items mod pool if bot exists in config dict + config has armor slot if ( - this.botConfig.equipment[botEquipmentRole] && + this.botConfig.equipment[settings.botData.equipmentRole] && settings.randomisationDetails?.randomisedArmorSlots?.includes(settings.rootEquipmentSlot) ) { + // Filter out mods from relevant blacklist settings.modPool[pickedItemDb._id] = this.getFilteredDynamicModsForItem( pickedItemDb._id, - this.botConfig.equipment[botEquipmentRole].blacklist, + botEquipBlacklist.equipment, ); } - // Item has slots, fill them + // Does item have slots for sub-mods to be inserted into if (pickedItemDb._props.Slots?.length > 0 && !settings.generateModsBlacklist?.includes(pickedItemDb._id)) { const childItemsToAdd = this.botEquipmentModGenerator.generateModsForEquipment( [item], id, pickedItemDb, settings, + botEquipBlacklist, ); settings.inventory.items.push(...childItemsToAdd); } else { - // No slots, push root item only + // No slots, add root item only settings.inventory.items.push(item); } @@ -476,17 +502,17 @@ export class BotInventoryGenerator { /** * Get all possible mods for item and filter down based on equipment blacklist from bot.json config * @param itemTpl Item mod pool is being retrieved and filtered - * @param equipmentBlacklist blacklist to filter mod pool with + * @param equipmentBlacklist Blacklist to filter mod pool with * @returns Filtered pool of mods */ protected getFilteredDynamicModsForItem( itemTpl: string, - equipmentBlacklist: EquipmentFilterDetails[], + equipmentBlacklist: Record, ): Record { const modPool = this.botEquipmentModPoolService.getModsForGearSlot(itemTpl); for (const modSlot of Object.keys(modPool ?? [])) { - const blacklistedMods = equipmentBlacklist[0]?.equipment[modSlot] || []; - const filteredMods = modPool[modSlot].filter((x) => !blacklistedMods.includes(x)); + const blacklistedMods = equipmentBlacklist[modSlot] ?? []; + const filteredMods = modPool[modSlot].filter((slotName) => !blacklistedMods.includes(slotName)); if (filteredMods.length > 0) { modPool[modSlot] = filteredMods; diff --git a/project/src/models/spt/bots/IGenerateEquipmentProperties.ts b/project/src/models/spt/bots/IGenerateEquipmentProperties.ts index a2645a6c..f45c65f4 100644 --- a/project/src/models/spt/bots/IGenerateEquipmentProperties.ts +++ b/project/src/models/spt/bots/IGenerateEquipmentProperties.ts @@ -1,6 +1,7 @@ import { IInventory as PmcInventory } from "@spt/models/eft/common/tables/IBotBase"; import { IChances, IMods } from "@spt/models/eft/common/tables/IBotType"; import { EquipmentFilters, RandomisationDetails } from "@spt/models/spt/config/IBotConfig"; +import { IBotData } from "./IGenerateWeaponRequest"; export interface IGenerateEquipmentProperties { /** Root Slot being generated */ @@ -10,14 +11,13 @@ export interface IGenerateEquipmentProperties { modPool: IMods; /** Dictionary of mod items and their chance to spawn for this bot type */ spawnChances: IChances; - /** Role being generated for */ - botRole: string; - /** Level of bot being generated */ - botLevel: number; + /** Bot-specific properties */ + botData: IBotData; inventory: PmcInventory; botEquipmentConfig: EquipmentFilters; /** Settings from bot.json to adjust how item is generated */ randomisationDetails: RandomisationDetails; /** OPTIONAL - Do not generate mods for tpls in this array */ generateModsBlacklist?: string[]; + generatingPlayerLevel: number; }