Refactor oh how bot equipment items are generated.

Now uses blacklist data from bot.json when picking equipment mods
Equipment gen now passes same BotData object as weapon mod gen
Pass botEquipmentRole via request object instead of calculating it every item slot
Fixed `getFilteredDynamicModsForItem()` being hard coded to use first blacklist object  regardless of which one matched the bot level
This commit is contained in:
Dev 2024-10-08 20:14:43 +01:00
parent a3816ad271
commit ed92c6802c
4 changed files with 105 additions and 53 deletions

View File

@ -1432,7 +1432,7 @@
"65392f611406374f82152ba5",
"653931da5db71d30ab1d6296"
],
"mod_nvg": ["5c11046cd174af02a012e42b", "5c110624d174af029e69734c"],
"mod_nvg": ["5c11046cd174af02a012e42b"],
"mod_reciever": ["5d4405aaa4b9361e6a4e6bd3"],
"mod_stock": ["5cde739cd7f00c0010373bd3"],
"mod_rear_sight": ["5a0ed824fcdbcb0176308b0d"],

View File

@ -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,

View File

@ -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<IGetRaidConfigurationRequestData>();
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<string, string[]>,
): Record<string, string[]> {
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;

View File

@ -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;
}