0
0
mirror of https://github.com/sp-tarkov/server.git synced 2025-02-13 09:50:43 -05:00
server/project/src/generators/BotEquipmentModGenerator.ts

1519 lines
64 KiB
TypeScript
Raw Normal View History

import { request } from "node:http";
import { BotGeneratorHelper } from "@spt/helpers/BotGeneratorHelper";
import { BotHelper } from "@spt/helpers/BotHelper";
import { BotWeaponGeneratorHelper } from "@spt/helpers/BotWeaponGeneratorHelper";
import { ItemHelper } from "@spt/helpers/ItemHelper";
import { PresetHelper } from "@spt/helpers/PresetHelper";
import { ProbabilityHelper } from "@spt/helpers/ProbabilityHelper";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { WeightedRandomHelper } from "@spt/helpers/WeightedRandomHelper";
import { IPreset } from "@spt/models/eft/common/IGlobals";
import { IMods, IModsChances } from "@spt/models/eft/common/tables/IBotType";
import { IItem } from "@spt/models/eft/common/tables/IItem";
import { ISlot, ITemplateItem } from "@spt/models/eft/common/tables/ITemplateItem";
import { BaseClasses } from "@spt/models/enums/BaseClasses";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
import { ModSpawn } from "@spt/models/enums/ModSpawn";
import { IChooseRandomCompatibleModResult } from "@spt/models/spt/bots/IChooseRandomCompatibleModResult";
import {
IFilterPlateModsForSlotByLevelResult,
Result,
} from "@spt/models/spt/bots/IFilterPlateModsForSlotByLevelResult";
import { IGenerateEquipmentProperties } from "@spt/models/spt/bots/IGenerateEquipmentProperties";
import { IGenerateWeaponRequest } from "@spt/models/spt/bots/IGenerateWeaponRequest";
import { IModToSpawnRequest } from "@spt/models/spt/bots/IModToSpawnRequest";
import { EquipmentFilterDetails, EquipmentFilters, IBotConfig } from "@spt/models/spt/config/IBotConfig";
import { ExhaustableArray } from "@spt/models/spt/server/ExhaustableArray";
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 { BotWeaponModLimitService } from "@spt/services/BotWeaponModLimitService";
import { DatabaseService } from "@spt/services/DatabaseService";
import { ItemFilterService } from "@spt/services/ItemFilterService";
import { LocalisationService } from "@spt/services/LocalisationService";
import { HashUtil } from "@spt/utils/HashUtil";
import { RandomUtil } from "@spt/utils/RandomUtil";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectable } from "tsyringe";
2023-03-03 15:23:46 +00:00
@injectable()
export class BotEquipmentModGenerator {
2023-03-03 15:23:46 +00:00
protected botConfig: IBotConfig;
constructor(
@inject("PrimaryLogger") protected logger: ILogger,
2023-03-03 15:23:46 +00:00
@inject("HashUtil") protected hashUtil: HashUtil,
@inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("ProbabilityHelper") protected probabilityHelper: ProbabilityHelper,
@inject("DatabaseService") protected databaseService: DatabaseService,
2023-03-03 15:23:46 +00:00
@inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("BotEquipmentFilterService") protected botEquipmentFilterService: BotEquipmentFilterService,
@inject("ItemFilterService") protected itemFilterService: ItemFilterService,
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("BotWeaponModLimitService") protected botWeaponModLimitService: BotWeaponModLimitService,
@inject("BotHelper") protected botHelper: BotHelper,
@inject("BotGeneratorHelper") protected botGeneratorHelper: BotGeneratorHelper,
@inject("BotWeaponGeneratorHelper") protected botWeaponGeneratorHelper: BotWeaponGeneratorHelper,
@inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper,
@inject("PresetHelper") protected presetHelper: PresetHelper,
2023-03-03 15:23:46 +00:00
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("BotEquipmentModPoolService") protected botEquipmentModPoolService: BotEquipmentModPoolService,
2023-11-13 11:05:05 -05:00
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("PrimaryCloner") protected cloner: ICloner,
) {
2023-03-03 15:23:46 +00:00
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
}
2023-11-13 11:05:05 -05:00
2023-03-03 15:23:46 +00:00
/**
* Check mods are compatible and add to array
* @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 object of item to add mods to
* @param specificBlacklist The relevant blacklist from bot.json equipment dictionary
2023-03-03 15:23:46 +00:00
* @param forceSpawn should this mod be forced to spawn
* @returns Item + compatible mods as an array
*/
2023-11-13 11:05:05 -05:00
public generateModsForEquipment(
equipment: IItem[],
2023-11-13 11:05:05 -05:00
parentId: string,
parentTemplate: ITemplateItem,
2024-01-07 14:46:25 +00:00
settings: IGenerateEquipmentProperties,
specificBlacklist: EquipmentFilterDetails,
shouldForceSpawn = false,
): IItem[] {
let forceSpawn = shouldForceSpawn;
// Get mod pool for the desired item
2024-01-07 14:46:25 +00:00
const compatibleModsPool = settings.modPool[parentTemplate._id];
if (!compatibleModsPool) {
this.logger.warning(
`bot: ${settings.botData.role} lacks a mod slot pool for item: ${parentTemplate._id} ${parentTemplate._name}`,
);
}
2023-03-03 15:23:46 +00:00
// 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) {
2023-11-13 11:05:05 -05:00
this.logger.error(
this.localisationService.getText("bot-mod_slot_missing_from_item", {
modSlot: modSlotName,
2023-11-13 11:05:05 -05:00
parentId: parentTemplate._id,
parentName: parentTemplate._name,
botRole: settings.botData.role,
2023-11-13 11:05:05 -05:00
}),
);
2023-03-03 15:23:46 +00:00
continue;
}
const modSpawnResult = this.shouldModBeSpawned(
itemSlotTemplate,
modSlotName.toLowerCase(),
settings.spawnChances.equipmentMods,
settings.botEquipmentConfig,
);
// Rolled to skip mod and it shouldnt be force-spawned
if (modSpawnResult === ModSpawn.SKIP && !forceSpawn) {
2023-03-03 15:23:46 +00:00
continue;
}
// Ensure submods for nvgs all spawn together
if (modSlotName === "mod_nvg") {
forceSpawn = true;
}
2023-03-03 15:23:46 +00:00
// 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 &&
this.itemHelper.isRemovablePlateSlot(modSlotName.toLowerCase())
) {
const plateSlotFilteringOutcome = this.filterPlateModsForSlotByLevel(
settings,
modSlotName.toLowerCase(),
compatibleModsPool[modSlotName],
parentTemplate,
);
if ([Result.UNKNOWN_FAILURE, Result.NO_DEFAULT_FILTER].includes(plateSlotFilteringOutcome.result)) {
this.logger.debug(
`Plate slot: ${modSlotName} selection for armor: ${parentTemplate._id} failed: ${
Result[plateSlotFilteringOutcome.result]
}, skipping`,
);
2024-01-07 14:46:25 +00:00
continue;
}
if ([Result.LACKS_PLATE_WEIGHTS].includes(plateSlotFilteringOutcome.result)) {
this.logger.warning(
`Plate slot: ${modSlotName} lacks weights for armor: ${parentTemplate._id}, unable to adjust plate choice, using existing data`,
);
}
// Replace mod pool with pool of chosen plate items
modPoolToChooseFrom = plateSlotFilteringOutcome.plateModTpls;
}
2023-11-13 11:05:05 -05:00
// Choose random mod from pool and check its compatibility
let modTpl: string | undefined;
let found = false;
const exhaustableModPool = this.createExhaustableArray(modPoolToChooseFrom);
while (exhaustableModPool.hasValues()) {
2023-03-03 15:23:46 +00:00
modTpl = exhaustableModPool.getRandomValue();
if (
modTpl &&
!this.botGeneratorHelper.isItemIncompatibleWithCurrentItems(equipment, modTpl, modSlotName)
.incompatible
) {
2023-03-03 15:23:46 +00:00
found = true;
break;
}
}
2023-11-13 11:05:05 -05:00
// Compatible item not found but slot REQUIRES item, get random item from db
if (!found && itemSlotTemplate._required) {
modTpl = this.getRandomModTplFromItemDb(modTpl, itemSlotTemplate, modSlotName, equipment);
2023-03-03 15:23:46 +00:00
found = !!modTpl;
}
// Compatible item not found + not required - skip
if (!(found || itemSlotTemplate._required)) {
2023-03-03 15:23:46 +00:00
continue;
}
// Get chosen mods db template and check it fits into slot
2023-03-03 15:23:46 +00:00
const modTemplate = this.itemHelper.getItem(modTpl);
if (
!this.isModValidForSlot(
modTemplate,
itemSlotTemplate,
modSlotName,
parentTemplate,
settings.botData.role,
)
) {
2023-03-03 15:23:46 +00:00
continue;
}
// Generate new id to ensure all items are unique on bot
2023-03-03 15:23:46 +00:00
const modId = this.hashUtil.generate();
equipment.push(
this.createModItem(modId, modTpl, parentId, modSlotName, modTemplate[1], settings.botData.role),
);
2023-03-03 15:23:46 +00:00
// 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,
specificBlacklist,
forceSpawn,
);
2023-03-03 15:23:46 +00:00
}
}
return equipment;
}
/**
* Filter a bots plate pool based on its current level
* @param settings Bot equipment generation settings
* @param modSlot Armor slot being filtered
* @param existingPlateTplPool Plates tpls to choose from
* @param armorItem The armor items db template
* @returns Array of plate tpls to choose from
*/
protected filterPlateModsForSlotByLevel(
settings: IGenerateEquipmentProperties,
modSlot: string,
existingPlateTplPool: string[],
armorItem: ITemplateItem,
): IFilterPlateModsForSlotByLevelResult {
const result: IFilterPlateModsForSlotByLevelResult = {
result: Result.UNKNOWN_FAILURE,
plateModTpls: undefined,
};
2024-01-07 14:46:25 +00:00
// Not pmc or not a plate slot, return original mod pool array
if (!this.itemHelper.isRemovablePlateSlot(modSlot)) {
result.result = Result.NOT_PLATE_HOLDING_SLOT;
result.plateModTpls = existingPlateTplPool;
return result;
}
// Get the front/back/side weights based on bots level
const plateSlotWeights = settings.botEquipmentConfig?.armorPlateWeighting?.find(
(armorWeight) =>
settings.botData.level >= armorWeight.levelRange.min &&
settings.botData.level <= armorWeight.levelRange.max,
);
if (!plateSlotWeights) {
// No weights, return original array of plate tpls
result.result = Result.LACKS_PLATE_WEIGHTS;
result.plateModTpls = existingPlateTplPool;
return result;
}
// Get the specific plate slot weights (front/back/side)
const plateWeights: Record<string, number> = plateSlotWeights[modSlot];
if (!plateWeights) {
// No weights, return original array of plate tpls
result.result = Result.LACKS_PLATE_WEIGHTS;
result.plateModTpls = existingPlateTplPool;
return result;
2024-01-07 14:46:25 +00:00
}
// Choose a plate level based on weighting
const chosenArmorPlateLevel = this.weightedRandomHelper.getWeightedValue<string>(plateWeights);
// Convert the array of ids into database items
const platesFromDb = existingPlateTplPool.map((plateTpl) => this.itemHelper.getItem(plateTpl)[1]);
// Filter plates to the chosen level based on its armorClass property
const platesOfDesiredLevel = platesFromDb.filter((item) => item._props.armorClass === chosenArmorPlateLevel);
if (platesOfDesiredLevel.length === 0) {
this.logger.debug(
`Plate filter was too restrictive for armor: ${armorItem._name} ${armorItem._id}, unable to find plates of level: ${chosenArmorPlateLevel}. Using mod items default plate`,
);
const relatedItemDbModSlot = armorItem._props.Slots.find((slot) => slot._name.toLowerCase() === modSlot);
const defaultPlate = relatedItemDbModSlot._props.filters[0].Plate;
if (!defaultPlate) {
// No relevant plate found after filtering AND no default plate
// Last attempt, get default preset and see if it has a plate default
const defaultPreset = this.presetHelper.getDefaultPreset(armorItem._id);
if (defaultPreset) {
const relatedPresetSlot = defaultPreset._items.find(
(item) => item.slotId?.toLowerCase() === modSlot,
);
if (relatedPresetSlot) {
result.result = Result.SUCCESS;
result.plateModTpls = [relatedPresetSlot._tpl];
return result;
}
}
result.result = Result.NO_DEFAULT_FILTER;
return result;
}
result.result = Result.SUCCESS;
result.plateModTpls = [defaultPlate];
return result;
}
2024-01-07 14:46:25 +00:00
// Only return the items ids
result.result = Result.SUCCESS;
result.plateModTpls = platesOfDesiredLevel.map((item) => item._id);
2024-01-07 14:46:25 +00:00
return result;
2024-01-07 14:46:25 +00:00
}
2023-03-03 15:23:46 +00:00
/**
* Add mods to a weapon using the provided mod pool
* @param sessionId Session id
* @param request Data used to generate the weapon
2023-03-03 15:23:46 +00:00
* @returns Weapon + mods array
*/
public generateModsForWeapon(sessionId: string, request: IGenerateWeaponRequest): IItem[] {
2023-03-03 15:23:46 +00:00
const pmcProfile = this.profileHelper.getPmcProfile(sessionId);
// Get pool of mods that fit weapon
const compatibleModsPool = request.modPool[request.parentTemplate._id];
2023-03-03 15:23:46 +00:00
2023-11-13 11:05:05 -05:00
if (
!(
request.parentTemplate._props.Slots.length ||
request.parentTemplate._props.Cartridges?.length ||
request.parentTemplate._props.Chambers?.length
)
) {
2023-11-13 11:05:05 -05:00
this.logger.error(
this.localisationService.getText("bot-unable_to_add_mods_to_weapon_missing_ammo_slot", {
weaponName: request.parentTemplate._name,
weaponId: request.parentTemplate._id,
botRole: request.botData.role,
2023-11-13 11:05:05 -05:00
}),
);
2023-03-03 15:23:46 +00:00
return request.weapon;
2023-03-03 15:23:46 +00:00
}
const botEquipConfig = this.botConfig.equipment[request.botData.equipmentRole];
2023-11-13 11:05:05 -05:00
const botEquipBlacklist = this.botEquipmentFilterService.getBotEquipmentBlacklist(
request.botData.equipmentRole,
2023-11-13 11:05:05 -05:00
pmcProfile.Info.Level,
);
const botWeaponSightWhitelist = this.botEquipmentFilterService.getBotWeaponSightWhitelist(
request.botData.equipmentRole,
);
const randomisationSettings = this.botHelper.getBotRandomizationDetails(request.botData.level, botEquipConfig);
2023-03-03 15:23:46 +00:00
// Iterate over mod pool and choose mods to attach
const sortedModKeys = this.sortModKeys(Object.keys(compatibleModsPool));
for (const modSlot of sortedModKeys) {
2023-03-03 15:23:46 +00:00
// Check weapon has slot for mod to fit in
const modsParentSlot = this.getModItemSlotFromDb(modSlot, request.parentTemplate);
if (!modsParentSlot) {
2023-11-13 11:05:05 -05:00
this.logger.error(
this.localisationService.getText("bot-weapon_missing_mod_slot", {
modSlot: modSlot,
weaponId: request.parentTemplate._id,
weaponName: request.parentTemplate._name,
botRole: request.botData.role,
2023-11-13 11:05:05 -05:00
}),
);
2023-03-03 15:23:46 +00:00
continue;
}
// Check spawn chance of mod
const modSpawnResult = this.shouldModBeSpawned(
modsParentSlot,
modSlot,
request.modSpawnChances,
botEquipConfig,
);
if (modSpawnResult === ModSpawn.SKIP) {
2023-03-03 15:23:46 +00:00
continue;
}
const isRandomisableSlot = randomisationSettings?.randomisedWeaponModSlots?.includes(modSlot) ?? false;
const modToSpawnRequest: IModToSpawnRequest = {
modSlot: modSlot,
isRandomisableSlot: isRandomisableSlot,
botWeaponSightWhitelist: botWeaponSightWhitelist,
botEquipBlacklist: botEquipBlacklist,
itemModPool: compatibleModsPool,
weapon: request.weapon,
ammoTpl: request.ammoTpl,
parentTemplate: request.parentTemplate,
modSpawnResult: modSpawnResult,
weaponStats: request.weaponStats,
conflictingItemTpls: request.conflictingItemTpls,
botData: request.botData,
};
const modToAdd = this.chooseModToPutIntoSlot(modToSpawnRequest);
2023-03-03 15:23:46 +00:00
// Compatible mod not found
if (!modToAdd || typeof modToAdd === "undefined") {
2023-03-03 15:23:46 +00:00
continue;
}
if (
!this.isModValidForSlot(modToAdd, modsParentSlot, modSlot, request.parentTemplate, request.botData.role)
) {
2023-03-03 15:23:46 +00:00
continue;
}
const modToAddTemplate = modToAdd[1];
2023-03-03 15:23:46 +00:00
// Skip adding mod to weapon if type limit reached
2023-11-13 11:05:05 -05:00
if (
this.botWeaponModLimitService.weaponModHasReachedLimit(
request.botData.equipmentRole,
2023-11-13 11:05:05 -05:00
modToAddTemplate,
request.modLimits,
request.parentTemplate,
request.weapon,
2023-11-13 11:05:05 -05:00
)
) {
2023-03-03 15:23:46 +00:00
continue;
}
// If item is a mount for scopes, set scope chance to 100%, this helps fix empty mounts appearing on weapons
if (this.modSlotCanHoldScope(modSlot, modToAddTemplate._parent)) {
2023-03-03 15:23:46 +00:00
// mod_mount was picked to be added to weapon, force scope chance to ensure its filled
const scopeSlots = ["mod_scope", "mod_scope_000", "mod_scope_001", "mod_scope_002", "mod_scope_003"];
this.adjustSlotSpawnChances(request.modSpawnChances, scopeSlots, 100);
2023-03-03 15:23:46 +00:00
// Hydrate pool of mods that fit into mount as its a randomisable slot
if (isRandomisableSlot) {
2023-03-03 15:23:46 +00:00
// Add scope mods to modPool dictionary to ensure the mount has a scope in the pool to pick
this.addCompatibleModsForProvidedMod(
"mod_scope",
modToAddTemplate,
request.modPool,
botEquipBlacklist,
);
2023-03-03 15:23:46 +00:00
}
}
// If picked item is muzzle adapter that can hold a child, adjust spawn chance
if (this.modSlotCanHoldMuzzleDevices(modSlot, modToAddTemplate._parent)) {
const muzzleSlots = ["mod_muzzle", "mod_muzzle_000", "mod_muzzle_001"];
// Make chance of muzzle devices 95%, nearly certain but not guaranteed
this.adjustSlotSpawnChances(request.modSpawnChances, muzzleSlots, 95);
}
2023-03-03 15:23:46 +00:00
// If front/rear sight are to be added, set opposite to 100% chance
if (this.modIsFrontOrRearSight(modSlot, modToAddTemplate._id)) {
request.modSpawnChances.mod_sight_front = 100;
request.modSpawnChances.mod_sight_rear = 100;
2023-03-03 15:23:46 +00:00
}
// Handguard mod can take a sub handguard mod + weapon has no UBGL (takes same slot)
// Force spawn chance to be 100% to ensure it gets added
2023-11-13 11:05:05 -05:00
if (
modSlot === "mod_handguard" &&
modToAddTemplate._props.Slots.some((slot) => slot._name === "mod_handguard") &&
!request.weapon.some((item) => item.slotId === "mod_launcher")
) {
// Needed for handguards with lower
request.modSpawnChances.mod_handguard = 100;
}
// If stock mod can take a sub stock mod, force spawn chance to be 100% to ensure sub-stock gets added
// Or if bot has stock force enabled
if (this.shouldForceSubStockSlots(modSlot, botEquipConfig, modToAddTemplate)) {
2023-03-03 15:23:46 +00:00
// Stock mod can take additional stocks, could be a locking device, force 100% chance
const subStockSlots = ["mod_stock", "mod_stock_000", "mod_stock_001", "mod_stock_akms"];
this.adjustSlotSpawnChances(request.modSpawnChances, subStockSlots, 100);
}
// Gather stats on mods being added to weapon
if (this.itemHelper.isOfBaseclass(modToAddTemplate._id, BaseClasses.IRON_SIGHT)) {
if (modSlot === "mod_sight_front") {
request.weaponStats.hasFrontIronSight = true;
} else if (modSlot === "mod_sight_rear") {
request.weaponStats.hasRearIronSight = true;
}
} else if (
!request.weaponStats.hasOptic &&
this.itemHelper.isOfBaseclass(modToAddTemplate._id, BaseClasses.SIGHTS)
) {
request.weaponStats.hasOptic = true;
2023-03-03 15:23:46 +00:00
}
const modId = this.hashUtil.generate();
request.weapon.push(
this.createModItem(
modId,
modToAddTemplate._id,
request.weaponId,
modSlot,
modToAddTemplate,
request.botData.role,
),
);
2023-11-13 11:05:05 -05:00
// Update conflicting item list now item has been chosen
for (const conflictingItem of modToAddTemplate._props.ConflictingItems) {
request.conflictingItemTpls.add(conflictingItem);
}
2023-03-03 15:23:46 +00:00
// I first thought we could use the recursive generateModsForItems as previously for cylinder magazines.
2023-11-13 11:05:05 -05:00
// However, the recursion doesn't go over the slots of the parent mod but over the modPool which is given by the bot config
2023-03-03 15:23:46 +00:00
// where we decided to keep cartridges instead of camoras. And since a CylinderMagazine only has one cartridge entry and
// this entry is not to be filled, we need a special handling for the CylinderMagazine
const modParentItem = this.itemHelper.getItem(modToAddTemplate._parent)[1];
if (this.botWeaponGeneratorHelper.magazineIsCylinderRelated(modParentItem._name)) {
2023-03-03 15:23:46 +00:00
// We don't have child mods, we need to create the camoras for the magazines instead
this.fillCamora(request.weapon, request.modPool, modId, modToAddTemplate);
} else {
let containsModInPool = Object.keys(request.modPool).includes(modToAddTemplate._id);
2023-03-03 15:23:46 +00:00
// Sometimes randomised slots are missing sub-mods, if so, get values from mod pool service
// Check for a randomisable slot + without data in modPool + item being added as additional slots
if (isRandomisableSlot && !containsModInPool && modToAddTemplate._props.Slots.length > 0) {
2023-03-03 15:23:46 +00:00
const modFromService = this.botEquipmentModPoolService.getModsForWeaponSlot(modToAddTemplate._id);
if (Object.keys(modFromService ?? {}).length > 0) {
request.modPool[modToAddTemplate._id] = modFromService;
2023-03-03 15:23:46 +00:00
containsModInPool = true;
}
}
if (containsModInPool) {
const recursiveRequestData: IGenerateWeaponRequest = {
weapon: request.weapon,
modPool: request.modPool,
weaponId: modId,
parentTemplate: modToAddTemplate,
modSpawnChances: request.modSpawnChances,
ammoTpl: request.ammoTpl,
botData: {
role: request.botData.role,
level: request.botData.level,
equipmentRole: request.botData.equipmentRole,
},
modLimits: request.modLimits,
weaponStats: request.weaponStats,
conflictingItemTpls: request.conflictingItemTpls,
};
2023-11-13 11:05:05 -05:00
// Call self recursively to add mods to this mod
this.generateModsForWeapon(sessionId, recursiveRequestData);
2023-03-03 15:23:46 +00:00
}
}
}
return request.weapon;
2023-03-03 15:23:46 +00:00
}
/**
* Should the provided bot have its stock chance values altered to 100%
* @param modSlot Slot to check
* @param botEquipConfig Bots equipment config/chance values
* @param modToAddTemplate Mod being added to bots weapon
* @returns True if it should
*/
protected shouldForceSubStockSlots(
modSlot: string,
botEquipConfig: EquipmentFilters,
modToAddTemplate: ITemplateItem,
): boolean {
// Slots a weapon can store its stock in
const stockSlots = ["mod_stock", "mod_stock_000", "mod_stock_001", "mod_stock_akms"];
// Can the stock hold child items
const hasSubSlots = modToAddTemplate._props.Slots?.length > 0;
return (stockSlots.includes(modSlot) && hasSubSlots) || botEquipConfig.forceStock;
}
2023-03-03 15:23:46 +00:00
/**
* Is this modslot a front or rear sight
* @param modSlot Slot to check
* @returns true if it's a front/rear sight
*/
protected modIsFrontOrRearSight(modSlot: string, tpl: string): boolean {
// Gas block /w front sight is special case, deem it a 'front sight' too
if (modSlot === "mod_gas_block" && tpl === "5ae30e795acfc408fb139a0b") {
// M4A1 front sight with gas block
return true;
}
2023-03-03 15:23:46 +00:00
return ["mod_sight_front", "mod_sight_rear"].includes(modSlot);
}
/**
* Does the provided mod details show the mod can hold a scope
* @param modSlot e.g. mod_scope, mod_mount
* @param modsParentId Parent id of mod item
* @returns true if it can hold a scope
*/
protected modSlotCanHoldScope(modSlot: string, modsParentId: string): boolean {
return (
[
"mod_scope",
"mod_mount",
"mod_mount_000",
"mod_scope_000",
"mod_scope_001",
"mod_scope_002",
"mod_scope_003",
].includes(modSlot.toLowerCase()) && modsParentId === BaseClasses.MOUNT
);
2023-03-03 15:23:46 +00:00
}
/**
* Set mod spawn chances to defined amount
* @param modSpawnChances Chance dictionary to update
2023-03-03 15:23:46 +00:00
*/
2023-11-13 11:05:05 -05:00
protected adjustSlotSpawnChances(
modSpawnChances: IModsChances,
2023-11-13 11:05:05 -05:00
modSlotsToAdjust: string[],
newChancePercent: number,
): void {
if (!modSpawnChances) {
this.logger.warning("Unable to adjust scope spawn chances as spawn chance object is empty");
return;
}
if (!modSlotsToAdjust) {
return;
}
for (const modName of modSlotsToAdjust) {
modSpawnChances[modName] = newChancePercent;
}
2023-03-03 15:23:46 +00:00
}
/**
* Does the provided modSlot allow muzzle-related items
* @param modSlot Slot id to check
* @param modsParentId OPTIONAL: parent id of modslot being checked
* @returns True if modSlot can have muzzle-related items
*/
protected modSlotCanHoldMuzzleDevices(modSlot: string, modsParentId?: string): boolean {
return ["mod_muzzle", "mod_muzzle_000", "mod_muzzle_001"].includes(modSlot.toLowerCase());
}
/**
* Sort mod slots into an ordering that maximises chance of a successful weapon generation
* @param unsortedSlotKeys Array of mod slot strings to sort
* @returns Sorted array
*/
protected sortModKeys(unsortedSlotKeys: string[]): string[] {
// No need to sort with only 1 item in array
if (unsortedSlotKeys.length <= 1) {
return unsortedSlotKeys;
2023-03-03 15:23:46 +00:00
}
const sortedKeys: string[] = [];
const modRecieverKey = "mod_reciever";
const modMount001Key = "mod_mount_001";
const modGasBlockKey = "mod_gas_block";
2023-03-03 15:23:46 +00:00
const modPistolGrip = "mod_pistol_grip";
const modStockKey = "mod_stock";
const modBarrelKey = "mod_barrel";
const modHandguardKey = "mod_handguard";
2023-03-03 15:23:46 +00:00
const modMountKey = "mod_mount";
const modScopeKey = "mod_scope";
if (unsortedSlotKeys.includes(modHandguardKey)) {
sortedKeys.push(modHandguardKey);
unsortedSlotKeys.splice(unsortedSlotKeys.indexOf(modHandguardKey), 1);
}
if (unsortedSlotKeys.includes(modBarrelKey)) {
2023-03-03 15:23:46 +00:00
sortedKeys.push(modBarrelKey);
unsortedSlotKeys.splice(unsortedSlotKeys.indexOf(modBarrelKey), 1);
2023-03-03 15:23:46 +00:00
}
if (unsortedSlotKeys.includes(modMount001Key)) {
2023-03-03 15:23:46 +00:00
sortedKeys.push(modMount001Key);
unsortedSlotKeys.splice(unsortedSlotKeys.indexOf(modMount001Key), 1);
2023-03-03 15:23:46 +00:00
}
if (unsortedSlotKeys.includes(modRecieverKey)) {
2023-03-03 15:23:46 +00:00
sortedKeys.push(modRecieverKey);
unsortedSlotKeys.splice(unsortedSlotKeys.indexOf(modRecieverKey), 1);
2023-03-03 15:23:46 +00:00
}
2023-11-13 11:05:05 -05:00
if (unsortedSlotKeys.includes(modPistolGrip)) {
2023-03-03 15:23:46 +00:00
sortedKeys.push(modPistolGrip);
unsortedSlotKeys.splice(unsortedSlotKeys.indexOf(modPistolGrip), 1);
2023-03-03 15:23:46 +00:00
}
if (unsortedSlotKeys.includes(modGasBlockKey)) {
sortedKeys.push(modGasBlockKey);
unsortedSlotKeys.splice(unsortedSlotKeys.indexOf(modGasBlockKey), 1);
2023-03-03 15:23:46 +00:00
}
if (unsortedSlotKeys.includes(modStockKey)) {
2023-03-03 15:23:46 +00:00
sortedKeys.push(modStockKey);
unsortedSlotKeys.splice(unsortedSlotKeys.indexOf(modStockKey), 1);
2023-03-03 15:23:46 +00:00
}
if (unsortedSlotKeys.includes(modMountKey)) {
2023-03-03 15:23:46 +00:00
sortedKeys.push(modMountKey);
unsortedSlotKeys.splice(unsortedSlotKeys.indexOf(modMountKey), 1);
2023-03-03 15:23:46 +00:00
}
if (unsortedSlotKeys.includes(modScopeKey)) {
2023-03-03 15:23:46 +00:00
sortedKeys.push(modScopeKey);
unsortedSlotKeys.splice(unsortedSlotKeys.indexOf(modScopeKey), 1);
2023-03-03 15:23:46 +00:00
}
sortedKeys.push(...unsortedSlotKeys);
2023-03-03 15:23:46 +00:00
return sortedKeys;
}
/**
* Get a Slot property for an item (chamber/cartridge/slot)
* @param modSlot e.g patron_in_weapon
* @param parentTemplate item template
* @returns Slot item
*/
protected getModItemSlotFromDb(modSlot: string, parentTemplate: ITemplateItem): ISlot {
const modSlotLower = modSlot.toLowerCase();
switch (modSlotLower) {
2023-03-03 15:23:46 +00:00
case "patron_in_weapon":
case "patron_in_weapon_000":
case "patron_in_weapon_001":
return parentTemplate._props.Chambers.find((chamber) => chamber._name.includes(modSlotLower));
2023-03-03 15:23:46 +00:00
case "cartridges":
return parentTemplate._props.Cartridges.find((c) => c._name.toLowerCase() === modSlotLower);
2023-03-03 15:23:46 +00:00
default:
return parentTemplate._props.Slots.find((s) => s._name.toLowerCase() === modSlotLower);
2023-03-03 15:23:46 +00:00
}
}
/**
* Randomly choose if a mod should be spawned, 100% for required mods OR mod is ammo slot
* @param itemSlot slot the item sits in from db
* @param modSlotName Name of slot the mod sits in
2023-03-03 15:23:46 +00:00
* @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
2023-03-03 15:23:46 +00:00
*/
protected shouldModBeSpawned(
itemSlot: ISlot,
modSlotName: string,
modSpawnChances: IModsChances,
botEquipConfig: EquipmentFilters,
): ModSpawn {
const slotRequired = itemSlot._required;
if (this.getAmmoContainers().includes(modSlotName)) {
// Always force mags/cartridges in weapon to spawn
return ModSpawn.SPAWN;
}
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;
2023-03-03 15:23:46 +00:00
}
return spawnMod ? ModSpawn.SPAWN : ModSpawn.SKIP;
2023-03-03 15:23:46 +00:00
}
/**
* Choose a mod to fit into the desired slot
* @param request Data used to choose an appropriate mod with
* @returns itemHelper.getItem() result
2023-03-03 15:23:46 +00:00
*/
protected chooseModToPutIntoSlot(request: IModToSpawnRequest): [boolean, ITemplateItem] | undefined {
/** Slot mod will fill */
const parentSlot = request.parentTemplate._props.Slots?.find((i) => i._name === request.modSlot);
const weaponTemplate = this.itemHelper.getItem(request.weapon[0]._tpl)[1];
2023-11-13 11:05:05 -05:00
2023-03-03 15:23:46 +00:00
// It's ammo, use predefined ammo parameter
if (this.getAmmoContainers().includes(request.modSlot) && request.modSlot !== "mod_magazine") {
return this.itemHelper.getItem(request.ammoTpl);
2023-03-03 15:23:46 +00:00
}
2024-01-27 18:12:13 +00:00
// Ensure there's a pool of mods to pick from
let modPool = this.getModPoolForSlot(request, weaponTemplate);
if (!modPool && !parentSlot?._required) {
2024-01-27 18:12:13 +00:00
// Nothing in mod pool + item not required
this.logger.debug(
`Mod pool for optional slot: ${request.modSlot} on item: ${request.parentTemplate._name} was empty, skipping mod`,
);
return undefined;
2024-01-27 18:12:13 +00:00
}
2023-03-03 15:23:46 +00:00
2024-01-27 18:12:13 +00:00
// Filter out non-whitelisted scopes, use full modpool if filtered pool would have no elements
if (request.modSlot.includes("mod_scope") && request.botWeaponSightWhitelist) {
2024-01-27 18:12:13 +00:00
// scope pool has more than one scope
if (modPool.length > 1) {
modPool = this.filterSightsByWeaponType(request.weapon[0], modPool, request.botWeaponSightWhitelist);
}
}
2024-07-23 17:30:20 +01:00
if (request.modSlot === "mod_gas_block") {
if (request.weaponStats.hasOptic && modPool.length > 1) {
// Attempt to limit modpool to low profile gas blocks when weapon has an optic
const onlyLowProfileGasBlocks = modPool.filter((tpl) =>
this.botConfig.lowProfileGasBlockTpls.includes(tpl),
);
if (onlyLowProfileGasBlocks.length > 0) {
modPool = onlyLowProfileGasBlocks;
}
} else if (request.weaponStats.hasRearIronSight && modPool.length > 1) {
// Attempt to limit modpool to high profile gas blocks when weapon has rear iron sight + no front iron sight
const onlyHighProfileGasBlocks = modPool.filter(
(tpl) => !this.botConfig.lowProfileGasBlockTpls.includes(tpl),
);
if (onlyHighProfileGasBlocks.length > 0) {
modPool = onlyHighProfileGasBlocks;
}
2023-03-03 15:23:46 +00:00
}
2024-01-27 18:12:13 +00:00
}
2024-01-27 18:12:13 +00:00
// Pick random mod that's compatible
const chosenModResult = this.getCompatibleWeaponModTplForSlotFromPool(
request,
modPool,
2024-07-23 17:30:20 +01:00
parentSlot,
request.modSpawnResult,
request.weapon,
request.modSlot,
);
2024-07-23 17:30:20 +01:00
if (chosenModResult.slotBlocked && !parentSlot._required) {
2024-01-27 18:12:13 +00:00
// Don't bother trying to fit mod, slot is completely blocked
return undefined;
2024-01-27 18:12:13 +00:00
}
// Log if mod chosen was incompatible
2024-07-23 17:30:20 +01:00
if (chosenModResult.incompatible && parentSlot._required) {
2024-01-27 18:12:13 +00:00
this.logger.debug(chosenModResult.reason);
2023-03-03 15:23:46 +00:00
}
// Get random mod to attach from items db for required slots if none found above
if (!chosenModResult.found && parentSlot !== undefined && parentSlot._required) {
chosenModResult.chosenTpl = this.getRandomModTplFromItemDb("", parentSlot, request.modSlot, request.weapon);
2024-01-27 18:12:13 +00:00
chosenModResult.found = true;
2023-03-03 15:23:46 +00:00
}
// Compatible item not found + not required
if (!chosenModResult.found && parentSlot !== undefined && !parentSlot._required) {
return undefined;
2023-03-03 15:23:46 +00:00
}
if (!chosenModResult.found && parentSlot !== undefined) {
if (parentSlot._required) {
2023-11-13 11:05:05 -05:00
this.logger.warning(
`Required slot unable to be filled, ${request.modSlot} on ${request.parentTemplate._name} ${request.parentTemplate._id} for weapon: ${request.weapon[0]._tpl}`,
2023-11-13 11:05:05 -05:00
);
2023-03-03 15:23:46 +00:00
}
return undefined;
2023-03-03 15:23:46 +00:00
}
2024-07-23 17:30:20 +01:00
return this.itemHelper.getItem(chosenModResult.chosenTpl);
2024-01-27 18:12:13 +00:00
}
/**
* Choose a weapon mod tpl for a given slot from a pool of choices
* Checks chosen tpl is compatible with all existing weapon items
* @param modPool Pool of mods that can be picked from
* @param parentSlot Slot the picked mod will have as a parent
* @param choiceTypeEnum How should chosen tpl be treated: DEFAULT_MOD/SPAWN/SKIP
* @param weapon Array of weapon items chosen item will be added to
* @param modSlotName Name of slot picked mod will be placed into
* @returns Chosen weapon details
*/
protected getCompatibleWeaponModTplForSlotFromPool(
request: IModToSpawnRequest,
2024-01-27 18:12:13 +00:00
modPool: string[],
parentSlot: ISlot,
choiceTypeEnum: ModSpawn,
weapon: IItem[],
modSlotName: string,
): IChooseRandomCompatibleModResult {
// Filter out incompatible mods from pool
let preFilteredModPool = this.getFilteredModPool(modPool, request.conflictingItemTpls);
if (preFilteredModPool.length === 0) {
return {
incompatible: true,
found: false,
reason: `Unable to add mod to ${ModSpawn[choiceTypeEnum]} slot: ${modSlotName}. All: ${modPool.length} had conflicts`,
};
}
// Filter mod pool to only items that appear in parents allowed list
preFilteredModPool = preFilteredModPool.filter((tpl) => parentSlot._props.filters[0].Filter.includes(tpl));
if (preFilteredModPool.length === 0) {
return { incompatible: true, found: false, reason: "No mods found in parents allowed list" };
}
return this.getCompatibleModFromPool(preFilteredModPool, choiceTypeEnum, weapon);
}
2024-01-27 18:12:13 +00:00
/**
*
* @param modPool Pool of item Tpls to choose from
* @param modSpawnType How should the slot choice be handled - forced/normal etc
* @param weapon Weapon mods at current time
* @param modSlotName Name of mod slot being filled
* @returns IChooseRandomCompatibleModResult
*/
protected getCompatibleModFromPool(
modPool: string[],
modSpawnType: ModSpawn,
weapon: IItem[],
): IChooseRandomCompatibleModResult {
// Create exhaustable pool to pick mod item from
const exhaustableModPool = this.createExhaustableArray(modPool);
// Create default response if no compatible item is found below
const chosenModResult: IChooseRandomCompatibleModResult = {
incompatible: true,
found: false,
reason: "unknown",
};
// Limit how many attempts to find a compatible mod can occur before giving up
const maxBlockedAttempts = Math.round(modPool.length * 0.75); // 75% of pool size
let blockedAttemptCount = 0;
let chosenTpl: string;
while (exhaustableModPool.hasValues()) {
2024-07-23 17:30:20 +01:00
chosenTpl = exhaustableModPool.getRandomValue();
const pickedItemDetails = this.itemHelper.getItem(chosenTpl);
if (!pickedItemDetails[0]) {
// Not valid item, try again
continue;
}
if (!pickedItemDetails[1]._props) {
// no props data, try again
continue;
}
// Success - Default wanted + only 1 item in pool
if (modSpawnType === ModSpawn.DEFAULT_MOD && modPool.length === 1) {
2024-01-27 18:12:13 +00:00
chosenModResult.found = true;
chosenModResult.incompatible = false;
2024-01-27 18:12:13 +00:00
chosenModResult.chosenTpl = chosenTpl;
break;
}
// Check if existing weapon mods are incompatible with chosen item
const existingItemBlockingChoice = weapon.find((item) =>
pickedItemDetails[1]._props.ConflictingItems?.includes(item._tpl),
2024-01-27 18:12:13 +00:00
);
if (existingItemBlockingChoice) {
// Give max of x attempts of picking a mod if blocked by another
if (blockedAttemptCount > maxBlockedAttempts) {
blockedAttemptCount = 0; // reset
break;
}
blockedAttemptCount++;
// Not compatible - Try again
continue;
2024-01-27 18:12:13 +00:00
}
// Edge case- Some mod combos will never work, make sure this isnt the case
if (this.weaponModComboIsIncompatible(weapon, chosenTpl)) {
chosenModResult.reason = `Chosen weapon mod: ${chosenTpl} can never be compatible with existing weapon mods`;
2024-01-27 18:12:13 +00:00
break;
}
// Success
chosenModResult.found = true;
chosenModResult.incompatible = false;
chosenModResult.chosenTpl = chosenTpl;
break;
2024-01-27 18:12:13 +00:00
}
return chosenModResult;
2023-03-03 15:23:46 +00:00
}
protected createExhaustableArray<T>(itemsToAddToArray: T[]) {
return new ExhaustableArray<T>(itemsToAddToArray, this.randomUtil, this.cloner);
}
/**
* Get a list of mod tpls that are compatible with the current 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(modPool: string[], tplBlacklist: Set<string>): string[] {
return modPool.filter((tpl) => !tplBlacklist.has(tpl));
}
/**
* Filter mod pool down based on various criteria:
* Is slot flagged as randomisable
* Is slot required
* Is slot flagged as default mod only
* @param request
* @param weaponTemplate Mods root parent (weapon/equipment)
* @returns Array of mod tpls
*/
protected getModPoolForSlot(request: IModToSpawnRequest, weaponTemplate: ITemplateItem): string[] {
// Mod is flagged as being default only, try and find it in globals
if (request.modSpawnResult === ModSpawn.DEFAULT_MOD) {
return this.getModPoolForDefaultSlot(request, weaponTemplate);
}
if (request.isRandomisableSlot) {
return this.getDynamicModPool(request.parentTemplate._id, request.modSlot, request.botEquipBlacklist);
}
// 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(
`${botData.role} No default: ${modSlot} mod found for: ${weaponTemplate._name}, using existing pool`,
);
}
// Couldnt find default in globals, use existing mod pool data
return itemModPool[modSlot];
}
// 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];
}
// 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
* @param parentItemTpl Tpl of the parent item
* @returns Default preset found
*/
protected getMatchingPreset(weaponTemplate: ITemplateItem, parentItemTpl: string): IPreset | undefined {
// Edge case - using mp5sd reciever means default mp5 handguard doesnt fit
const isMp5sd = parentItemTpl === "5926f2e086f7745aae644231";
if (isMp5sd) {
return this.presetHelper.getPreset("59411abb86f77478f702b5d2");
}
// Edge case - dvl 500mm is the silenced barrel and has specific muzzle mods
const isDvl500mmSilencedBarrel = parentItemTpl === "5888945a2459774bf43ba385";
if (isDvl500mmSilencedBarrel) {
return this.presetHelper.getPreset("59e8d2b386f77445830dd299");
}
return this.presetHelper.getDefaultPreset(weaponTemplate._id);
}
/**
* Temp fix to prevent certain combinations of weapons with mods that are known to be incompatible
* @param weapon Array of items that make up a weapon
* @param modTpl Mod to check compatibility with weapon
* @returns True if incompatible
*/
protected weaponModComboIsIncompatible(weapon: IItem[], modTpl: string): boolean {
// STM-9 + AR-15 Lone Star Ion Lite handguard
if (weapon[0]._tpl === "60339954d62c9b14ed777c06" && modTpl === "5d4405f0a4b9361e6a4e6bd9") {
return true;
}
return false;
}
2023-03-03 15:23:46 +00:00
/**
* Create a mod item with provided parameters as properties + add upd property
2023-03-03 15:23:46 +00:00
* @param modId _id
* @param modTpl _tpl
* @param parentId parentId
* @param modSlot slotId
2023-11-13 11:05:05 -05:00
* @param modTemplate Used to add additional properties in the upd object
* @param botRole The bots role mod is being created for
2023-03-03 15:23:46 +00:00
* @returns Item object
*/
2023-11-13 11:05:05 -05:00
protected createModItem(
modId: string,
modTpl: string,
parentId: string,
modSlot: string,
modTemplate: ITemplateItem,
botRole: string,
): IItem {
2023-03-03 15:23:46 +00:00
return {
2023-10-31 22:52:09 +00:00
_id: modId,
_tpl: modTpl,
parentId: parentId,
slotId: modSlot,
2023-11-13 11:05:05 -05:00
...this.botGeneratorHelper.generateExtraPropertiesForItem(modTemplate, botRole),
2023-03-03 15:23:46 +00:00
};
}
/**
* Get a list of containers that hold ammo
* e.g. mod_magazine / patron_in_weapon_000
* @returns string array
*/
protected getAmmoContainers(): string[] {
2023-03-03 15:23:46 +00:00
return ["mod_magazine", "patron_in_weapon", "patron_in_weapon_000", "patron_in_weapon_001", "cartridges"];
}
/**
* Get a random mod from an items compatible mods Filter array
* @param fallbackModTpl Default value to return if parentSlot Filter is empty
* @param parentSlot Item mod will go into, used to get compatible items
2023-03-03 15:23:46 +00:00
* @param modSlot Slot to get mod to fill
* @param items Items to ensure picked mod is compatible with
* @returns Item tpl
2023-03-03 15:23:46 +00:00
*/
protected getRandomModTplFromItemDb(
fallbackModTpl: string,
parentSlot: ISlot,
modSlot: string,
items: IItem[],
): string | undefined {
2023-11-13 11:05:05 -05:00
// Find compatible mods and make an array of them
2023-03-03 15:23:46 +00:00
const allowedItems = parentSlot._props.filters[0].Filter;
// Find mod item that fits slot from sorted mod array
const exhaustableModPool = this.createExhaustableArray(allowedItems);
let tmpModTpl = fallbackModTpl;
while (exhaustableModPool.hasValues()) {
2024-07-23 17:30:20 +01:00
tmpModTpl = exhaustableModPool.getRandomValue();
if (!this.botGeneratorHelper.isItemIncompatibleWithCurrentItems(items, tmpModTpl, modSlot).incompatible) {
2023-03-03 15:23:46 +00:00
return tmpModTpl;
}
}
// No mod found
return undefined;
2023-03-03 15:23:46 +00:00
}
/**
* Check if mod exists in db + is for a required slot
* @param modToAdd Db template of mod to check
* @param slotAddedToTemplate Slot object the item will be placed as child into
* @param modSlot Slot the mod will fill
* @param parentTemplate Db template of the mods being added
* @param botRole Bots wildspawntype (assault/pmcBot/exUsec etc)
* @returns True if valid for slot
2023-03-03 15:23:46 +00:00
*/
2023-11-13 11:05:05 -05:00
protected isModValidForSlot(
modToAdd: [boolean, ITemplateItem],
slotAddedToTemplate: ISlot,
2023-11-13 11:05:05 -05:00
modSlot: string,
parentTemplate: ITemplateItem,
botRole: string,
): boolean {
const modBeingAddedDbTemplate = modToAdd[1];
2024-01-27 18:12:13 +00:00
// Mod lacks db template object
if (!modBeingAddedDbTemplate) {
2023-11-13 11:05:05 -05:00
this.logger.error(
this.localisationService.getText("bot-no_item_template_found_when_adding_mod", {
modId: modBeingAddedDbTemplate?._id ?? "UNKNOWN",
2023-11-13 11:05:05 -05:00
modSlot: modSlot,
}),
);
this.logger.debug(`Item -> ${parentTemplate?._id}; Slot -> ${modSlot}`);
2023-03-03 15:23:46 +00:00
return false;
}
// Mod has invalid db item
if (!modToAdd[0]) {
// Parent slot must be filled but db object is invalid, show warning and return false
if (slotAddedToTemplate._required) {
2023-11-13 11:05:05 -05:00
this.logger.warning(
this.localisationService.getText("bot-unable_to_add_mod_item_invalid", {
itemName: modBeingAddedDbTemplate?._name ?? "UNKNOWN",
2023-11-13 11:05:05 -05:00
modSlot: modSlot,
parentItemName: parentTemplate._name,
botRole: botRole,
2023-11-13 11:05:05 -05:00
}),
);
2023-03-03 15:23:46 +00:00
}
return false;
}
// Mod was found in db
2023-03-03 15:23:46 +00:00
return true;
}
/**
* Find mod tpls of a provided type and add to modPool
* @param desiredSlotName Slot to look up and add we are adding tpls for (e.g mod_scope)
2023-03-03 15:23:46 +00:00
* @param modTemplate db object for modItem we get compatible mods from
* @param modPool Pool of mods we are adding to
* @param botEquipBlacklist A blacklist of items that cannot be picked
2023-03-03 15:23:46 +00:00
*/
2023-11-13 11:05:05 -05:00
protected addCompatibleModsForProvidedMod(
desiredSlotName: string,
modTemplate: ITemplateItem,
modPool: IMods,
2023-11-13 11:05:05 -05:00
botEquipBlacklist: EquipmentFilterDetails,
): void {
const desiredSlotObject = modTemplate._props.Slots?.find((slot) => slot._name.includes(desiredSlotName));
if (desiredSlotObject) {
2023-03-03 15:23:46 +00:00
const supportedSubMods = desiredSlotObject._props.filters[0].Filter;
if (supportedSubMods) {
2023-03-03 15:23:46 +00:00
// Filter mods
let filteredMods = this.filterModsByBlacklist(supportedSubMods, botEquipBlacklist, desiredSlotName);
if (filteredMods.length === 0) {
2023-11-13 11:05:05 -05:00
this.logger.warning(
this.localisationService.getText("bot-unable_to_filter_mods_all_blacklisted", {
slotName: desiredSlotObject._name,
itemName: modTemplate._name,
}),
);
2023-03-03 15:23:46 +00:00
filteredMods = supportedSubMods;
}
if (!modPool[modTemplate._id]) {
2023-03-03 15:23:46 +00:00
modPool[modTemplate._id] = {};
}
modPool[modTemplate._id][desiredSlotObject._name] = supportedSubMods;
}
}
}
/**
* Get the possible items that fit a slot
* @param parentItemId item tpl to get compatible items for
* @param modSlot Slot item should fit in
* @param botEquipBlacklist Equipment that should not be picked
* @returns Array of compatible items for that slot
2023-03-03 15:23:46 +00:00
*/
2023-11-13 11:05:05 -05:00
protected getDynamicModPool(
parentItemId: string,
modSlot: string,
botEquipBlacklist: EquipmentFilterDetails,
): string[] {
const modsFromDynamicPool = this.cloner.clone(
2023-11-13 11:05:05 -05:00
this.botEquipmentModPoolService.getCompatibleModsForWeaponSlot(parentItemId, modSlot),
);
2023-03-03 15:23:46 +00:00
const filteredMods = this.filterModsByBlacklist(modsFromDynamicPool, botEquipBlacklist, modSlot);
if (filteredMods.length === 0) {
2023-11-13 11:05:05 -05:00
this.logger.warning(
this.localisationService.getText("bot-unable_to_filter_mod_slot_all_blacklisted", modSlot),
);
2023-03-03 15:23:46 +00:00
return modsFromDynamicPool;
}
return filteredMods;
}
/**
* Take a list of tpls and filter out blacklisted values using itemFilterService + botEquipmentBlacklist
* @param allowedMods Base mods to filter
* @param botEquipBlacklist Equipment blacklist
* @param modSlot Slot mods belong to
2023-03-03 15:23:46 +00:00
* @returns Filtered array of mod tpls
*/
protected filterModsByBlacklist(
2023-11-13 11:05:05 -05:00
allowedMods: string[],
botEquipBlacklist: EquipmentFilterDetails,
modSlot: string,
): string[] {
// No blacklist, nothing to filter out
if (!botEquipBlacklist) {
2023-03-03 15:23:46 +00:00
return allowedMods;
}
2023-11-13 11:05:05 -05:00
2023-03-03 15:23:46 +00:00
let result: string[] = [];
2023-11-13 11:05:05 -05:00
// Get item blacklist and mod equipment blacklist as one array
const blacklist = this.itemFilterService
.getBlacklistedItems()
.concat(botEquipBlacklist.equipment[modSlot] || []);
result = allowedMods.filter((tpl) => !blacklist.includes(tpl));
2023-03-03 15:23:46 +00:00
return result;
}
/**
* With the shotgun revolver (60db29ce99594040e04c4a27) 12.12 introduced CylinderMagazines.
* Those magazines (e.g. 60dc519adf4c47305f6d410d) have a "Cartridges" entry with a _max_count=0.
* Ammo is not put into the magazine directly but assigned to the magazine's slots: The "camora_xxx" slots.
* This function is a helper called by generateModsForItem for mods with parent type "CylinderMagazine"
* @param items The items where the CylinderMagazine's camora are appended to
* @param modPool ModPool which should include available cartridges
* @param cylinderMagParentId The CylinderMagazine's UID
* @param cylinderMagTemplate The CylinderMagazine's template
2023-03-03 15:23:46 +00:00
*/
protected fillCamora(
items: IItem[],
modPool: IMods,
cylinderMagParentId: string,
cylinderMagTemplate: ITemplateItem,
): void {
let itemModPool = modPool[cylinderMagTemplate._id];
if (!itemModPool) {
2023-11-13 11:05:05 -05:00
this.logger.warning(
this.localisationService.getText("bot-unable_to_fill_camora_slot_mod_pool_empty", {
weaponId: cylinderMagTemplate._id,
weaponName: cylinderMagTemplate._name,
2023-11-13 11:05:05 -05:00
}),
);
const camoraSlots = cylinderMagTemplate._props.Slots.filter((slot) => slot._name.startsWith("camora"));
// Attempt to generate camora slots for item
modPool[cylinderMagTemplate._id] = {};
for (const camora of camoraSlots) {
modPool[cylinderMagTemplate._id][camora._name] = camora._props.filters[0].Filter;
}
itemModPool = modPool[cylinderMagTemplate._id];
2023-03-03 15:23:46 +00:00
}
let exhaustableModPool = undefined;
2023-03-03 15:23:46 +00:00
let modSlot = "cartridges";
const camoraFirstSlot = "camora_000";
if (modSlot in itemModPool) {
exhaustableModPool = this.createExhaustableArray(itemModPool[modSlot]);
} else if (camoraFirstSlot in itemModPool) {
2023-03-03 15:23:46 +00:00
modSlot = camoraFirstSlot;
exhaustableModPool = this.createExhaustableArray(this.mergeCamoraPools(itemModPool));
} else {
this.logger.error(this.localisationService.getText("bot-missing_cartridge_slot", cylinderMagTemplate._id));
2023-03-03 15:23:46 +00:00
return;
}
let modTpl: string;
let found = false;
while (exhaustableModPool.hasValues()) {
2023-03-03 15:23:46 +00:00
modTpl = exhaustableModPool.getRandomValue();
if (!this.botGeneratorHelper.isItemIncompatibleWithCurrentItems(items, modTpl, modSlot).incompatible) {
2023-03-03 15:23:46 +00:00
found = true;
break;
}
}
if (!found) {
2023-03-03 15:23:46 +00:00
this.logger.error(this.localisationService.getText("bot-no_compatible_camora_ammo_found", modSlot));
return;
}
for (const slot of cylinderMagTemplate._props.Slots) {
2023-03-03 15:23:46 +00:00
const modSlotId = slot._name;
const modId = this.hashUtil.generate();
2024-06-19 10:40:59 +01:00
items.push({ _id: modId, _tpl: modTpl, parentId: cylinderMagParentId, slotId: modSlotId });
2023-03-03 15:23:46 +00:00
}
}
/**
2023-11-13 11:05:05 -05:00
* Take a record of camoras and merge the compatible shells into one array
2024-06-19 10:40:59 +01:00
* @param camorasWithShells Dictionary of camoras we want to merge into one array
* @returns String array of shells for multiple camora sources
2023-03-03 15:23:46 +00:00
*/
protected mergeCamoraPools(camorasWithShells: Record<string, string[]>): string[] {
2024-06-19 10:40:59 +01:00
const uniqueShells = new Set<string>();
for (const shells of Object.values(camorasWithShells)) {
// Add all shells to the set
for (const shell of shells) {
uniqueShells.add(shell);
}
2023-03-03 15:23:46 +00:00
}
2024-06-19 10:40:59 +01:00
return Array.from(uniqueShells);
2023-03-03 15:23:46 +00:00
}
/**
* Filter out non-whitelisted weapon scopes
* Controlled by bot.json weaponSightWhitelist
* e.g. filter out rifle scopes from SMGs
2023-03-03 15:23:46 +00:00
* @param weapon Weapon scopes will be added to
* @param scopes Full scope pool
2023-11-13 11:05:05 -05:00
* @param botWeaponSightWhitelist Whitelist of scope types by weapon base type
* @returns Array of scope tpls that have been filtered to just ones allowed for that weapon type
2023-03-03 15:23:46 +00:00
*/
2023-11-13 11:05:05 -05:00
protected filterSightsByWeaponType(
weapon: IItem,
2023-11-13 11:05:05 -05:00
scopes: string[],
botWeaponSightWhitelist: Record<string, string[]>,
): string[] {
2023-03-03 15:23:46 +00:00
const weaponDetails = this.itemHelper.getItem(weapon._tpl);
// Return original scopes array if whitelist not found
const whitelistedSightTypes = botWeaponSightWhitelist[weaponDetails[1]._parent];
if (!whitelistedSightTypes) {
2023-11-13 11:05:05 -05:00
this.logger.debug(
`Unable to find whitelist for weapon type: ${weaponDetails[1]._parent} ${weaponDetails[1]._name}, skipping sight filtering`,
2023-11-13 11:05:05 -05:00
);
2023-03-03 15:23:46 +00:00
return scopes;
}
// Filter items that are not directly scopes OR mounts that do not hold the type of scope we allow for this weapon type
const filteredScopesAndMods: string[] = [];
for (const item of scopes) {
2023-03-03 15:23:46 +00:00
// Mods is a scope, check base class is allowed
if (this.itemHelper.isOfBaseclasses(item, whitelistedSightTypes)) {
// Add mod to allowed list
2023-03-03 15:23:46 +00:00
filteredScopesAndMods.push(item);
continue;
}
// Edge case, what if item is a mount for a scope and not directly a scope?
// Check item is mount + has child items
2023-03-03 15:23:46 +00:00
const itemDetails = this.itemHelper.getItem(item)[1];
if (this.itemHelper.isOfBaseclass(item, BaseClasses.MOUNT) && itemDetails._props.Slots.length > 0) {
// Check to see if mount has a scope slot (only include primary slot, ignore the rest like the backup sight slots)
// Should only find 1 as there's currently no items with a mod_scope AND a mod_scope_000
const scopeSlot = itemDetails._props.Slots.filter((slot) =>
["mod_scope", "mod_scope_000"].includes(slot._name),
2023-11-13 11:05:05 -05:00
);
// Mods scope slot found must allow ALL whitelisted scope types OR be a mount
2023-11-13 11:05:05 -05:00
if (
scopeSlot?.every((slot) =>
slot._props.filters[0].Filter.every(
(tpl) =>
this.itemHelper.isOfBaseclasses(tpl, whitelistedSightTypes) ||
this.itemHelper.isOfBaseclass(tpl, BaseClasses.MOUNT),
),
2023-11-13 11:05:05 -05:00
)
) {
// Add mod to allowed list
2023-03-03 15:23:46 +00:00
filteredScopesAndMods.push(item);
}
}
}
2023-11-13 11:05:05 -05:00
// No mods added to return list after filtering has occurred, send back the original mod list
if (!filteredScopesAndMods || filteredScopesAndMods.length === 0) {
2023-11-13 11:05:05 -05:00
this.logger.debug(
`Scope whitelist too restrictive for: ${weapon._tpl} ${weaponDetails[1]._name}, skipping filter`,
);
2023-03-03 15:23:46 +00:00
return scopes;
}
return filteredScopesAndMods;
}
2023-11-13 11:05:05 -05:00
}