import { inject, injectable } from "tsyringe"; import { BotEquipmentModGenerator } from "@spt-aki/generators/BotEquipmentModGenerator"; import { BotLootGenerator } from "@spt-aki/generators/BotLootGenerator"; import { BotWeaponGenerator } from "@spt-aki/generators/BotWeaponGenerator"; import { BotGeneratorHelper } from "@spt-aki/helpers/BotGeneratorHelper"; import { BotHelper } from "@spt-aki/helpers/BotHelper"; import { ItemHelper } from "@spt-aki/helpers/ItemHelper"; import { WeightedRandomHelper } from "@spt-aki/helpers/WeightedRandomHelper"; import { Inventory as PmcInventory } from "@spt-aki/models/eft/common/tables/IBotBase"; import { Chances, Generation, IBotType, Inventory, Mods } from "@spt-aki/models/eft/common/tables/IBotType"; import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; import { EquipmentSlots } from "@spt-aki/models/enums/EquipmentSlots"; import { EquipmentFilterDetails, IBotConfig, RandomisationDetails } from "@spt-aki/models/spt/config/IBotConfig"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; import { ConfigServer } from "@spt-aki/servers/ConfigServer"; import { DatabaseServer } from "@spt-aki/servers/DatabaseServer"; import { BotEquipmentModPoolService } from "@spt-aki/services/BotEquipmentModPoolService"; import { LocalisationService } from "@spt-aki/services/LocalisationService"; import { HashUtil } from "@spt-aki/utils/HashUtil"; import { RandomUtil } from "@spt-aki/utils/RandomUtil"; @injectable() export class BotInventoryGenerator { protected botConfig: IBotConfig; constructor( @inject("WinstonLogger") protected logger: ILogger, @inject("HashUtil") protected hashUtil: HashUtil, @inject("RandomUtil") protected randomUtil: RandomUtil, @inject("DatabaseServer") protected databaseServer: DatabaseServer, @inject("BotWeaponGenerator") protected botWeaponGenerator: BotWeaponGenerator, @inject("BotLootGenerator") protected botLootGenerator: BotLootGenerator, @inject("BotGeneratorHelper") protected botGeneratorHelper: BotGeneratorHelper, @inject("BotHelper") protected botHelper: BotHelper, @inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper, @inject("ItemHelper") protected itemHelper: ItemHelper, @inject("LocalisationService") protected localisationService: LocalisationService, @inject("BotEquipmentModPoolService") protected botEquipmentModPoolService: BotEquipmentModPoolService, @inject("BotEquipmentModGenerator") protected botEquipmentModGenerator: BotEquipmentModGenerator, @inject("ConfigServer") protected configServer: ConfigServer ) { this.botConfig = this.configServer.getConfig(ConfigTypes.BOT); } /** * Add equipment/weapons/loot to bot * @param sessionId Session id * @param botJsonTemplate Base json db file for the bot having its loot generated * @param botRole Role bot has (assault/pmcBot) * @param isPmc Is bot being converted into a pmc * @param botLevel Level of bot being generated * @returns PmcInventory object with equipment/weapons/loot */ public generateInventory(sessionId: string, botJsonTemplate: IBotType, botRole: string, isPmc: boolean, botLevel: number): PmcInventory { const templateInventory = botJsonTemplate.inventory; const equipmentChances = botJsonTemplate.chances; const itemGenerationLimitsMinMax = botJsonTemplate.generation; // Generate base inventory with no items const botInventory = this.generateInventoryBase(); this.generateAndAddEquipmentToBot(templateInventory, equipmentChances, botRole, botInventory, botLevel); // Roll weapon spawns (primary/secondary/holster) and generate a weapon for each roll that passed this.generateAndAddWeaponsToBot(templateInventory, equipmentChances, sessionId, botInventory, botRole, isPmc, itemGenerationLimitsMinMax, botLevel); // Pick loot and add to bots containers (rig/backpack/pockets/secure) this.botLootGenerator.generateLoot(sessionId, botJsonTemplate, isPmc, botRole, botInventory, botLevel); return botInventory; } /** * Create a pmcInventory object with all the base/generic items needed * @returns PmcInventory object */ protected generateInventoryBase(): PmcInventory { const equipmentId = this.hashUtil.generate(); const equipmentTpl = "55d7217a4bdc2d86028b456d"; const stashId = this.hashUtil.generate(); const stashTpl = "566abbc34bdc2d92178b4576"; const questRaidItemsId = this.hashUtil.generate(); const questRaidItemsTpl = "5963866286f7747bf429b572"; const questStashItemsId = this.hashUtil.generate(); const questStashItemsTpl = "5963866b86f7747bfa1c4462"; const sortingTableId = this.hashUtil.generate(); const sortingTableTpl = "602543c13fee350cd564d032"; return { items: [ { "_id": equipmentId, "_tpl": equipmentTpl }, { "_id": stashId, "_tpl": stashTpl }, { "_id": questRaidItemsId, "_tpl": questRaidItemsTpl }, { "_id": questStashItemsId, "_tpl": questStashItemsTpl }, { "_id": sortingTableId, "_tpl": sortingTableTpl } ], equipment: equipmentId, stash: stashId, questRaidItems: questRaidItemsId, questStashItems: questStashItemsId, sortingTable: sortingTableId, hideoutAreaStashes: {}, fastPanel: {} }; } /** * Add equipment to a bot * @param templateInventory bot/x.json data from db * @param equipmentChances Chances items will be added to bot * @param botRole Role bot has (assault/pmcBot) * @param botInventory Inventory to add equipment to * @param botLevel Level of bot */ protected generateAndAddEquipmentToBot(templateInventory: Inventory, equipmentChances: Chances, botRole: string, botInventory: PmcInventory, botLevel: number): void { // These will be handled later const excludedSlots: string[] = [ EquipmentSlots.FIRST_PRIMARY_WEAPON, EquipmentSlots.SECOND_PRIMARY_WEAPON, EquipmentSlots.HOLSTER, EquipmentSlots.ARMOR_VEST, EquipmentSlots.TACTICAL_VEST, EquipmentSlots.FACE_COVER, EquipmentSlots.HEADWEAR, EquipmentSlots.EARPIECE ]; const botEquipConfig = this.botConfig.equipment[this.botGeneratorHelper.getBotEquipmentRole(botRole)]; const randomistionDetails = this.botHelper.getBotRandomizationDetails(botLevel, botEquipConfig); for (const equipmentSlot in templateInventory.equipment) { // Weapons have special generation and will be generated seperately; ArmorVest should be generated after TactivalVest if (excludedSlots.includes(equipmentSlot)) { continue; } this.generateEquipment(equipmentSlot, templateInventory.equipment[equipmentSlot], templateInventory.mods, equipmentChances, botRole, botInventory, randomistionDetails); } // Generate below in specific order this.generateEquipment(EquipmentSlots.FACE_COVER, templateInventory.equipment.FaceCover, templateInventory.mods, equipmentChances, botRole, botInventory, randomistionDetails); this.generateEquipment(EquipmentSlots.HEADWEAR, templateInventory.equipment.Headwear, templateInventory.mods, equipmentChances, botRole, botInventory, randomistionDetails); this.generateEquipment(EquipmentSlots.EARPIECE, templateInventory.equipment.Earpiece, templateInventory.mods, equipmentChances, botRole, botInventory, randomistionDetails); this.generateEquipment(EquipmentSlots.TACTICAL_VEST, templateInventory.equipment.TacticalVest, templateInventory.mods, equipmentChances, botRole, botInventory, randomistionDetails); this.generateEquipment(EquipmentSlots.ARMOR_VEST, templateInventory.equipment.ArmorVest, templateInventory.mods, equipmentChances, botRole, botInventory, randomistionDetails); } /** * Add a piece of equipment with mods to inventory from the provided pools * @param equipmentSlot Slot to select an item for * @param equipmentPool Possible items to choose from * @param modPool Possible mods to apply to item chosen * @param spawnChances Chances items will be chosen to be added * @param botRole Role of bot e.g. assault * @param inventory Inventory to add item into * @param randomisationDetails settings from bot.json to adjust how item is generated */ protected generateEquipment( equipmentSlot: string, equipmentPool: Record, modPool: Mods, spawnChances: Chances, botRole: string, inventory: PmcInventory, randomisationDetails: RandomisationDetails): void { const spawnChance = ([EquipmentSlots.POCKETS, EquipmentSlots.SECURED_CONTAINER] as string[]).includes(equipmentSlot) ? 100 : spawnChances.equipment[equipmentSlot]; if (typeof spawnChance === "undefined") { this.logger.warning(this.localisationService.getText("bot-no_spawn_chance_defined_for_equipment_slot", equipmentSlot)); return; } const shouldSpawn = this.randomUtil.getChance100(spawnChance); if (Object.keys(equipmentPool).length && shouldSpawn) { const id = this.hashUtil.generate(); const equipmentItemTpl = this.weightedRandomHelper.getWeightedValue(equipmentPool); const itemTemplate = this.itemHelper.getItem(equipmentItemTpl); if (!itemTemplate[0]) { this.logger.error(this.localisationService.getText("bot-missing_item_template", equipmentItemTpl)); this.logger.info(`EquipmentSlot -> ${equipmentSlot}`); return; } if (this.botGeneratorHelper.isItemIncompatibleWithCurrentItems(inventory.items, equipmentItemTpl, equipmentSlot).incompatible) { // Bad luck - randomly picked item was not compatible with current gear return; } const item = { "_id": id, "_tpl": equipmentItemTpl, "parentId": inventory.equipment, "slotId": equipmentSlot, ...this.botGeneratorHelper.generateExtraPropertiesForItem(itemTemplate[1], botRole) }; // use dynamic mod pool if enabled in config const botEquipmentRole = this.botGeneratorHelper.getBotEquipmentRole(botRole); if (this.botConfig.equipment[botEquipmentRole] && randomisationDetails?.randomisedArmorSlots?.includes(equipmentSlot)) { modPool[equipmentItemTpl] = this.getFilteredDynamicModsForItem(equipmentItemTpl, this.botConfig.equipment[botEquipmentRole].blacklist); } if (typeof(modPool[equipmentItemTpl]) !== "undefined" || Object.keys(modPool[equipmentItemTpl] || {}).length > 0) { const items = this.botEquipmentModGenerator.generateModsForEquipment([item], modPool, id, itemTemplate[1], spawnChances.mods, botRole); inventory.items.push(...items); } else { inventory.items.push(item); } } } /** * Get all possible mods for item and filter down based on equipment blacklist from bot.json config * @param itemTpl Item mod pool is being retreived and filtered * @param equipmentBlacklist blacklist to filter mod pool with * @returns Filtered pool of mods */ protected getFilteredDynamicModsForItem(itemTpl: string, equipmentBlacklist: EquipmentFilterDetails[]): 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)); if (filteredMods.length > 0) { modPool[modSlot] = filteredMods; } } return modPool; } /** * Work out what weapons bot should have equipped and add them to bot inventory * @param templateInventory bot/x.json data from db * @param equipmentChances Chances bot can have equipment equipped * @param sessionId Session id * @param botInventory Inventory to add weapons to * @param botRole assault/pmcBot/bossTagilla etc * @param isPmc Is the bot being generated as a pmc * @param botLevel level of bot having weapon generated * @param itemGenerationLimitsMinMax Limits for items the bot can have */ protected generateAndAddWeaponsToBot(templateInventory: Inventory, equipmentChances: Chances, sessionId: string, botInventory: PmcInventory, botRole: string, isPmc: boolean, itemGenerationLimitsMinMax: Generation, botLevel: number): void { const weaponSlotsToFill = this.getDesiredWeaponsForBot(equipmentChances); for (const weaponSlot of weaponSlotsToFill) { // Add weapon to bot if true and bot json has something to put into the slot if (weaponSlot.shouldSpawn && Object.keys(templateInventory.equipment[weaponSlot.slot]).length) { this.addWeaponAndMagazinesToInventory(sessionId, weaponSlot, templateInventory, botInventory, equipmentChances, botRole, isPmc, itemGenerationLimitsMinMax, botLevel); } } } /** * Calculate if the bot should have weapons in Primary/Secondary/Holster slots * @param equipmentChances Chances bot has certain equipment * @returns What slots bot should have weapons generated for */ protected getDesiredWeaponsForBot(equipmentChances: Chances): { slot: EquipmentSlots; shouldSpawn: boolean; }[] { const shouldSpawnPrimary = this.randomUtil.getChance100(equipmentChances.equipment.FirstPrimaryWeapon); return [ { slot: EquipmentSlots.FIRST_PRIMARY_WEAPON, shouldSpawn: shouldSpawnPrimary }, { slot: EquipmentSlots.SECOND_PRIMARY_WEAPON, shouldSpawn: shouldSpawnPrimary ? this.randomUtil.getChance100(equipmentChances.equipment.SecondPrimaryWeapon) : false }, { slot: EquipmentSlots.HOLSTER, shouldSpawn: shouldSpawnPrimary ? this.randomUtil.getChance100(equipmentChances.equipment.Holster) // Primary weapon = roll for chance at pistol : true // No primary = force pistol } ]; } /** * Add weapon + spare mags/ammo to bots inventory * @param sessionId Session id * @param weaponSlot Weapon slot being generated * @param templateInventory bot/x.json data from db * @param botInventory Inventory to add weapon+mags/ammo to * @param equipmentChances Chances bot can have equipment equipped * @param botRole assault/pmcBot/bossTagilla etc * @param isPmc Is the bot being generated as a pmc * @param itemGenerationWeights */ protected addWeaponAndMagazinesToInventory( sessionId: string, weaponSlot: { slot: EquipmentSlots; shouldSpawn: boolean; }, templateInventory: Inventory, botInventory: PmcInventory, equipmentChances: Chances, botRole: string, isPmc: boolean, itemGenerationWeights: Generation, botLevel: number): void { const generatedWeapon = this.botWeaponGenerator.generateRandomWeapon( sessionId, weaponSlot.slot, templateInventory, botInventory.equipment, equipmentChances.mods, botRole, isPmc, botLevel); botInventory.items.push(...generatedWeapon.weapon); this.botWeaponGenerator.addExtraMagazinesToInventory(generatedWeapon, itemGenerationWeights.items.magazines, botInventory, botRole); } }