Improved weapon mod selection when a default mod is desired but is incompatible with weapon

Introduced a `Set` into request object that holds conflicting items instead of constantly recalculating them when needed
This commit is contained in:
Dev 2024-08-16 23:19:07 +01:00
parent 931c567fc2
commit 52d9fbaeb6
4 changed files with 98 additions and 70 deletions

View File

@ -1,3 +1,4 @@
import { request } from "node:http";
import { BotGeneratorHelper } from "@spt/helpers/BotGeneratorHelper";
import { BotHelper } from "@spt/helpers/BotHelper";
import { BotWeaponGeneratorHelper } from "@spt/helpers/BotWeaponGeneratorHelper";
@ -384,6 +385,8 @@ export class BotEquipmentModGenerator {
parentTemplate: request.parentTemplate,
modSpawnResult: modSpawnResult,
weaponStats: request.weaponStats,
conflictingItemTpls: request.conflictingItemTpls,
botData: request.botData,
};
const modToAdd = this.chooseModToPutIntoSlot(modToSpawnRequest);
@ -488,6 +491,11 @@ export class BotEquipmentModGenerator {
),
);
// Update conflicting item list now item has been chosen
for (const conflictingItem of modToAddTemplate._props.ConflictingItems) {
request.conflictingItemTpls.add(conflictingItem);
}
// I first thought we could use the recursive generateModsForItems as previously for cylinder magazines.
// However, the recursion doesn't go over the slots of the parent mod but over the modPool which is given by the bot config
// where we decided to keep cartridges instead of camoras. And since a CylinderMagazine only has one cartridge entry and
@ -523,6 +531,7 @@ export class BotEquipmentModGenerator {
},
modLimits: request.modLimits,
weaponStats: request.weaponStats,
conflictingItemTpls: request.conflictingItemTpls,
};
// Call self recursively to add mods to this mod
this.generateModsForWeapon(sessionId, recursiveRequestData);
@ -758,19 +767,11 @@ export class BotEquipmentModGenerator {
}
// Ensure there's a pool of mods to pick from
let modPool = this.getModPoolForSlot(
request.itemModPool,
request.modSpawnResult,
request.parentTemplate,
weaponTemplate,
request.modSlot,
request.botEquipBlacklist,
request.isRandomisableSlot,
);
if (!(modPool || parentSlot?._required)) {
let modPool = this.getModPoolForSlot(request, weaponTemplate);
if (!modPool && !parentSlot?._required) {
// Nothing in mod pool + item not required
this.logger.debug(
`Mod pool for slot: ${request.modSlot} on item: ${request.parentTemplate._name} was empty, skipping mod`,
`Mod pool for optional slot: ${request.modSlot} on item: ${request.parentTemplate._name} was empty, skipping mod`,
);
return undefined;
}
@ -805,6 +806,7 @@ export class BotEquipmentModGenerator {
// Pick random mod that's compatible
const chosenModResult = this.getCompatibleWeaponModTplForSlotFromPool(
request,
modPool,
parentSlot,
request.modSpawnResult,
@ -856,6 +858,7 @@ export class BotEquipmentModGenerator {
* @returns Chosen weapon details
*/
protected getCompatibleWeaponModTplForSlotFromPool(
request: IModToSpawnRequest,
modPool: string[],
parentSlot: Slot,
choiceTypeEnum: ModSpawn,
@ -863,7 +866,7 @@ export class BotEquipmentModGenerator {
modSlotName: string,
): IChooseRandomCompatibleModResult {
// Filter out incompatible mods from pool
let preFilteredModPool = this.getFilteredModPool(modPool, weapon);
let preFilteredModPool = this.getFilteredModPool(modPool, request.conflictingItemTpls);
if (preFilteredModPool.length === 0) {
return {
incompatible: true,
@ -970,15 +973,12 @@ export class BotEquipmentModGenerator {
/**
* Get a list of mod tpls that are compatible with the current weapon
* @param initialModPool
* @param weapon
* @param modPool
* @param tplBlacklist Tpls that are incompatible and should not be used
* @returns string array of compatible mod tpls with weapon
*/
protected getFilteredModPool(initialModPool: string[], weapon: Item[]): string[] {
const equippedItemsDb = weapon.map((item) => this.itemHelper.getItem(item._tpl)[1]);
const conflicingItemsList = equippedItemsDb.flatMap((item) => item._props.ConflictingItems);
return initialModPool.filter((tpl) => !conflicingItemsList.includes(tpl));
protected getFilteredModPool(modPool: string[], tplBlacklist: Set<string>): string[] {
return modPool.filter((tpl) => !tplBlacklist.has(tpl));
}
/**
@ -986,60 +986,31 @@ export class BotEquipmentModGenerator {
* Is slot flagged as randomisable
* Is slot required
* Is slot flagged as default mod only
* @param itemModPool Existing pool of mods to choose
* @param itemSpawnCategory How should slot be handled
* @param parentTemplate Mods parent
* @param request
* @param weaponTemplate Mods root parent (weapon/equipment)
* @param modSlot name of mod slot to choose for
* @param botEquipBlacklist A blacklist of items not allowed to be picked
* @param isRandomisableSlot Slot is flagged as a randomisable slot
* @returns Array of mod tpls
*/
protected getModPoolForSlot(
itemModPool: Record<string, string[]>,
itemSpawnCategory: ModSpawn,
parentTemplate: ITemplateItem,
weaponTemplate: ITemplateItem,
modSlot: string,
botEquipBlacklist: EquipmentFilterDetails,
isRandomisableSlot: boolean,
): string[] {
protected getModPoolForSlot(request: IModToSpawnRequest, weaponTemplate: ITemplateItem): string[] {
// Mod is flagged as being default only, try and find it in globals
if (itemSpawnCategory === ModSpawn.DEFAULT_MOD) {
const matchingPreset = this.getMatchingPreset(weaponTemplate, parentTemplate._id);
const matchingModFromPreset = matchingPreset?._items.find(
(item) => item?.slotId?.toLowerCase() === modSlot.toLowerCase(),
);
if (request.modSpawnResult === ModSpawn.DEFAULT_MOD) {
return this.getModPoolForDefaultSlot(request, weaponTemplate);
}
// Only filter mods down to single default item if it already exists in existing itemModPool, OR the default item has no children
// Filtering mod pool to item that wasnt already there can have problems;
// You'd have a mod being picked without any sub-mods in its chain, possibly resulting in missing required mods not being added
if (matchingModFromPreset) {
// Mod is in existing mod pool
if (itemModPool[modSlot].includes(matchingModFromPreset._tpl)) {
// Found mod on preset + it already exists in mod pool
return [matchingModFromPreset._tpl];
}
if (request.isRandomisableSlot) {
return this.getDynamicModPool(request.parentTemplate._id, request.modSlot, request.botEquipBlacklist);
}
// Get an array of items that are allowed in slot from parent item
// Check the filter of the slot to ensure a chosen mod fits
const parentSlotCompatibleItems = parentTemplate._props.Slots?.find(
(slot) => slot._name.toLowerCase() === modSlot.toLowerCase(),
)?._props.filters[0].Filter;
// Mod isnt in existing pool, only add if it has no children and matches parent filter
if (
parentSlotCompatibleItems?.includes(matchingModFromPreset._tpl) &&
this.itemHelper.getItem(matchingModFromPreset._tpl)[1]._props.Slots?.length === 0
) {
// Mod has no children and matches parent filters, can be used
return [matchingModFromPreset._tpl];
}
}
// Required mod is not default or randomisable, use existing pool
return request.itemModPool[request.modSlot];
}
protected getModPoolForDefaultSlot(request: IModToSpawnRequest, weaponTemplate: ITemplateItem): string[] {
const { itemModPool, modSlot, parentTemplate, botData, conflictingItemTpls } = request;
const matchingModFromPreset = this.getMatchingModFromPreset(request, weaponTemplate);
if (!matchingModFromPreset) {
if (itemModPool[modSlot]?.length > 1) {
this.logger.debug(
`No default: ${modSlot} mod found on template: ${weaponTemplate._name} and multiple items found in existing pool`,
`${botData.role} No default: ${modSlot} mod found for: ${weaponTemplate._name}, using existing pool`,
);
}
@ -1047,14 +1018,63 @@ export class BotEquipmentModGenerator {
return itemModPool[modSlot];
}
if (isRandomisableSlot) {
return this.getDynamicModPool(parentTemplate._id, modSlot, botEquipBlacklist);
// Only filter mods down to single default item if it already exists in existing itemModPool, OR the default item has no children
// Filtering mod pool to item that wasnt already there can have problems;
// You'd have a mod being picked without any sub-mods in its chain, possibly resulting in missing required mods not being added
// Mod is in existing mod pool
if (itemModPool[modSlot].includes(matchingModFromPreset._tpl)) {
// Found mod on preset + it already exists in mod pool
return [matchingModFromPreset._tpl];
}
// Required mod is not default or randomisable, use existing pool
// Get an array of items that are allowed in slot from parent item
// Check the filter of the slot to ensure a chosen mod fits
const parentSlotCompatibleItems = parentTemplate._props.Slots?.find(
(slot) => slot._name.toLowerCase() === modSlot.toLowerCase(),
)?._props.filters[0].Filter;
// Mod isnt in existing pool, only add if it has no children and exists inside parent filter
if (
parentSlotCompatibleItems?.includes(matchingModFromPreset._tpl) &&
this.itemHelper.getItem(matchingModFromPreset._tpl)[1]._props.Slots?.length === 0
) {
// Chosen mod has no conflicts + no children + is in parent compat list
if (!conflictingItemTpls.has(matchingModFromPreset._tpl)) {
return [matchingModFromPreset._tpl];
}
// Above chosen mod had conflicts with existing weapon mods
this.logger.debug(
`${botData.role} Chosen default: ${modSlot} mod found for: ${weaponTemplate._name} weapon conflicts with item on weapon, cannot use default`,
);
const existingModPool = itemModPool[modSlot];
if (existingModPool.length === 1) {
// The only item in pool isn't compatible
this.logger.debug(
`${botData.role} ${modSlot} Mod pool for: ${weaponTemplate._name} weapon has only incompatible items, using parent list instead`,
);
// Last ditch, use full pool of items minus conflicts
const newListOfModsForSlot = parentSlotCompatibleItems.filter((tpl) => !conflictingItemTpls.has(tpl));
if (newListOfModsForSlot.length > 0) {
return newListOfModsForSlot;
}
}
// Return full mod pool
return itemModPool[modSlot];
}
// Tried everything, return mod pool
return itemModPool[modSlot];
}
protected getMatchingModFromPreset(request: IModToSpawnRequest, weaponTemplate: ITemplateItem) {
const matchingPreset = this.getMatchingPreset(weaponTemplate, request.parentTemplate._id);
return matchingPreset?._items.find((item) => item?.slotId?.toLowerCase() === request.modSlot.toLowerCase());
}
/**
* Get default preset for weapon OR get specific weapon presets for edge cases (mp5/silenced dvl)
* @param weaponTemplate Weapons db template
@ -1387,8 +1407,10 @@ export class BotEquipmentModGenerator {
protected mergeCamoraPools(camorasWithShells: Record<string, string[]>): string[] {
const uniqueShells = new Set<string>();
for (const shells of Object.values(camorasWithShells)) {
// Add all shells to the set.
shells.forEach((shell) => uniqueShells.add(shell));
// Add all shells to the set
for (const shell of shells) {
uniqueShells.add(shell);
}
}
return Array.from(uniqueShells);

View File

@ -176,6 +176,7 @@ export class BotWeaponGenerator {
botData: { role: botRole, level: botLevel, equipmentRole: botEquipmentRole },
modLimits: modLimits,
weaponStats: {},
conflictingItemTpls: new Set(),
};
weaponWithModsArray = this.botEquipmentModGenerator.generateModsForWeapon(
sessionId,

View File

@ -22,6 +22,8 @@ export interface IGenerateWeaponRequest {
modLimits: BotModLimits;
/** Info related to the weapon being generated */
weaponStats: IWeaponStats;
/** Array of item tpls the weapon does not support */
conflictingItemTpls: Set<string>;
}
export interface IBotData {

View File

@ -1,7 +1,7 @@
import { Item } from "@spt/models/eft/common/tables/IItem";
import { ITemplateItem } from "@spt/models/eft/common/tables/ITemplateItem";
import { ModSpawn } from "@spt/models/enums/ModSpawn";
import { IWeaponStats } from "@spt/models/spt/bots/IGenerateWeaponRequest";
import { IBotData, IWeaponStats } from "@spt/models/spt/bots/IGenerateWeaponRequest";
import { EquipmentFilterDetails } from "@spt/models/spt/config/IBotConfig";
export interface IModToSpawnRequest {
@ -25,4 +25,7 @@ export interface IModToSpawnRequest {
modSpawnResult: ModSpawn;
/** Weapon stats for weapon being generated */
weaponStats: IWeaponStats;
/** Array of item tpls the weapon does not support */
conflictingItemTpls: Set<string>;
botData: IBotData;
}