Server/project/src/helpers/BotGeneratorHelper.ts

576 lines
22 KiB
TypeScript
Raw Normal View History

2023-03-03 16:23:46 +01:00
import { inject, injectable } from "tsyringe";
import { ApplicationContext } from "@spt-aki/context/ApplicationContext";
import { ContextVariableType } from "@spt-aki/context/ContextVariableType";
import { DurabilityLimitsHelper } from "@spt-aki/helpers/DurabilityLimitsHelper";
import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
import { Item, Repairable, Upd } from "@spt-aki/models/eft/common/tables/IItem";
import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem";
import { IGetRaidConfigurationRequestData } from "@spt-aki/models/eft/match/IGetRaidConfigurationRequestData";
import { BaseClasses } from "@spt-aki/models/enums/BaseClasses";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
2024-01-27 19:12:13 +01:00
import { IChooseRandomCompatibleModResult } from "@spt-aki/models/spt/bots/IChooseRandomCompatibleModResult";
import { EquipmentFilters, IBotConfig, IRandomisedResourceValues } from "@spt-aki/models/spt/config/IBotConfig";
import { IPmcConfig } from "@spt-aki/models/spt/config/IPmcConfig";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { ConfigServer } from "@spt-aki/servers/ConfigServer";
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer";
import { LocalisationService } from "@spt-aki/services/LocalisationService";
import { JsonUtil } from "@spt-aki/utils/JsonUtil";
import { RandomUtil } from "@spt-aki/utils/RandomUtil";
2023-03-03 16:23:46 +01:00
@injectable()
2023-11-13 17:07:59 +01:00
export class BotGeneratorHelper
2023-03-03 16:23:46 +01:00
{
protected botConfig: IBotConfig;
protected pmcConfig: IPmcConfig;
2023-03-03 16:23:46 +01:00
constructor(
@inject("WinstonLogger") protected logger: ILogger,
@inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("DurabilityLimitsHelper") protected durabilityLimitsHelper: DurabilityLimitsHelper,
@inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("ApplicationContext") protected applicationContext: ApplicationContext,
2023-03-03 16:23:46 +01:00
@inject("LocalisationService") protected localisationService: LocalisationService,
2023-11-13 17:07:59 +01:00
@inject("ConfigServer") protected configServer: ConfigServer,
)
2023-03-03 16:23:46 +01:00
{
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC);
2023-03-03 16:23:46 +01:00
}
/**
* Adds properties to an item
* e.g. Repairable / HasHinge / Foldable / MaxDurability
* @param itemTemplate Item extra properties are being generated for
* @param botRole Used by weapons to randomize the durability values. Null for non-equipped items
* @returns Item Upd object with extra properties
*/
public generateExtraPropertiesForItem(itemTemplate: ITemplateItem, botRole?: string): { upd?: Upd; }
2023-03-03 16:23:46 +01:00
{
// Get raid settings, if no raid, default to day
2023-11-13 17:07:59 +01:00
const raidSettings = this.applicationContext.getLatestValue(ContextVariableType.RAID_CONFIGURATION)?.getValue<
IGetRaidConfigurationRequestData
>();
const raidIsNight = raidSettings?.timeVariant === "PAST";
const itemProperties: Upd = {};
2023-03-03 16:23:46 +01:00
2023-11-13 17:07:59 +01:00
if (itemTemplate._props.MaxDurability)
2023-03-03 16:23:46 +01:00
{
2023-11-13 17:07:59 +01:00
if (itemTemplate._props.weapClass)
{ // Is weapon
itemProperties.Repairable = this.generateWeaponRepairableProperties(itemTemplate, botRole);
2023-03-03 16:23:46 +01:00
}
2023-11-13 17:07:59 +01:00
else if (itemTemplate._props.armorClass)
{ // Is armor
itemProperties.Repairable = this.generateArmorRepairableProperties(itemTemplate, botRole);
2023-03-03 16:23:46 +01:00
}
}
2023-11-13 17:07:59 +01:00
if (itemTemplate._props.HasHinge)
2023-03-03 16:23:46 +01:00
{
itemProperties.Togglable = { On: true };
2023-03-03 16:23:46 +01:00
}
2023-11-13 17:07:59 +01:00
if (itemTemplate._props.Foldable)
2023-03-03 16:23:46 +01:00
{
itemProperties.Foldable = { Folded: false };
2023-03-03 16:23:46 +01:00
}
2023-11-13 17:07:59 +01:00
if (itemTemplate._props.weapFireType?.length)
2023-03-03 16:23:46 +01:00
{
2023-11-13 17:07:59 +01:00
if (itemTemplate._props.weapFireType.includes("fullauto"))
2023-03-03 16:23:46 +01:00
{
itemProperties.FireMode = { FireMode: "fullauto" };
2023-03-03 16:23:46 +01:00
}
2023-11-13 17:07:59 +01:00
else
2023-03-03 16:23:46 +01:00
{
itemProperties.FireMode = { FireMode: this.randomUtil.getArrayValue(itemTemplate._props.weapFireType) };
2023-03-03 16:23:46 +01:00
}
}
2023-11-13 17:07:59 +01:00
if (itemTemplate._props.MaxHpResource)
2023-03-03 16:23:46 +01:00
{
2023-11-13 17:07:59 +01:00
itemProperties.MedKit = {
HpResource: this.getRandomizedResourceValue(
itemTemplate._props.MaxHpResource,
this.botConfig.lootItemResourceRandomization[botRole]?.meds,
),
};
2023-03-03 16:23:46 +01:00
}
2023-11-13 17:07:59 +01:00
if (itemTemplate._props.MaxResource && itemTemplate._props.foodUseTime)
2023-03-03 16:23:46 +01:00
{
2023-11-13 17:07:59 +01:00
itemProperties.FoodDrink = {
HpPercent: this.getRandomizedResourceValue(
itemTemplate._props.MaxResource,
this.botConfig.lootItemResourceRandomization[botRole]?.food,
),
};
2023-03-03 16:23:46 +01:00
}
if (itemTemplate._parent === BaseClasses.FLASHLIGHT)
{
// Get chance from botconfig for bot type
const lightLaserActiveChance = raidIsNight
? this.getBotEquipmentSettingFromConfig(botRole, "lightIsActiveNightChancePercent", 50)
: this.getBotEquipmentSettingFromConfig(botRole, "lightIsActiveDayChancePercent", 25);
itemProperties.Light = {
IsActive: (this.randomUtil.getChance100(lightLaserActiveChance)),
SelectedMode: 0,
};
}
else if (itemTemplate._parent === BaseClasses.TACTICAL_COMBO)
2023-03-03 16:23:46 +01:00
{
// Get chance from botconfig for bot type, use 50% if no value found
2023-11-13 17:07:59 +01:00
const lightLaserActiveChance = this.getBotEquipmentSettingFromConfig(
botRole,
"laserIsActiveChancePercent",
50,
);
itemProperties.Light = {
IsActive: (this.randomUtil.getChance100(lightLaserActiveChance)),
SelectedMode: 0,
};
2023-03-03 16:23:46 +01:00
}
2023-11-13 17:07:59 +01:00
if (itemTemplate._parent === BaseClasses.NIGHTVISION)
2023-03-03 16:23:46 +01:00
{
// Get chance from botconfig for bot type
const nvgActiveChance = raidIsNight
? this.getBotEquipmentSettingFromConfig(botRole, "nvgIsActiveChanceNightPercent", 90)
: this.getBotEquipmentSettingFromConfig(botRole, "nvgIsActiveChanceDayPercent", 15);
itemProperties.Togglable = { On: (this.randomUtil.getChance100(nvgActiveChance)) };
2023-03-03 16:23:46 +01:00
}
// Togglable face shield
2023-11-13 17:07:59 +01:00
if (itemTemplate._props.HasHinge && itemTemplate._props.FaceShieldComponent)
2023-03-03 16:23:46 +01:00
{
// Get chance from botconfig for bot type, use 75% if no value found
2023-11-13 17:07:59 +01:00
const faceShieldActiveChance = this.getBotEquipmentSettingFromConfig(
botRole,
"faceShieldIsActiveChancePercent",
75,
);
itemProperties.Togglable = { On: (this.randomUtil.getChance100(faceShieldActiveChance)) };
2023-03-03 16:23:46 +01:00
}
return Object.keys(itemProperties).length ? { upd: itemProperties } : {};
2023-03-03 16:23:46 +01:00
}
/**
* Randomize the HpResource for bots e.g (245/400 resources)
* @param maxResource Max resource value of medical items
* @param randomizationValues Value provided from config
* @returns Randomized value from maxHpResource
*/
protected getRandomizedResourceValue(maxResource: number, randomizationValues: IRandomisedResourceValues): number
{
if (!randomizationValues)
{
return maxResource;
}
if (this.randomUtil.getChance100(randomizationValues.chanceMaxResourcePercent))
{
return maxResource;
}
2023-11-13 17:07:59 +01:00
return this.randomUtil.getInt(
this.randomUtil.getPercentOfValue(randomizationValues.resourcePercent, maxResource, 0),
maxResource,
);
}
2023-03-03 16:23:46 +01:00
/**
* Get the chance for the weapon attachment or helmet equipment to be set as activated
* @param botRole role of bot with weapon/helmet
* @param setting the setting of the weapon attachment/helmet equipment to be activated
* @param defaultValue default value for the chance of activation if the botrole or bot equipment role is null
* @returns Percent chance to be active
*/
2023-11-13 17:07:59 +01:00
protected getBotEquipmentSettingFromConfig(
botRole: string,
setting: keyof EquipmentFilters,
defaultValue: number,
): number
2023-03-03 16:23:46 +01:00
{
2023-11-13 17:07:59 +01:00
if (!botRole)
2023-03-03 16:23:46 +01:00
{
return defaultValue;
}
const botEquipmentSettings = this.botConfig.equipment[this.getBotEquipmentRole(botRole)];
if (!botEquipmentSettings)
{
2023-11-13 17:07:59 +01:00
this.logger.warning(
this.localisationService.getText("bot-missing_equipment_settings", {
botRole: botRole,
setting: setting,
defaultValue: defaultValue,
}),
);
2023-03-03 16:23:46 +01:00
return defaultValue;
}
2023-11-13 17:07:59 +01:00
if (botEquipmentSettings[setting] === undefined || typeof botEquipmentSettings[setting] !== "number")
2023-03-03 16:23:46 +01:00
{
2023-11-13 17:07:59 +01:00
this.logger.warning(
this.localisationService.getText("bot-missing_equipment_settings_property", {
botRole: botRole,
setting: setting,
defaultValue: defaultValue,
}),
);
2023-03-03 16:23:46 +01:00
return defaultValue;
}
return <number>botEquipmentSettings[setting];
}
/**
* Create a repairable object for a weapon that containers durability + max durability properties
* @param itemTemplate weapon object being generated for
* @param botRole type of bot being generated for
* @returns Repairable object
*/
2023-11-13 17:07:59 +01:00
protected generateWeaponRepairableProperties(itemTemplate: ITemplateItem, botRole: string): Repairable
2023-03-03 16:23:46 +01:00
{
const maxDurability = this.durabilityLimitsHelper.getRandomizedMaxWeaponDurability(itemTemplate, botRole);
2023-11-13 17:07:59 +01:00
const currentDurability = this.durabilityLimitsHelper.getRandomizedWeaponDurability(
itemTemplate,
botRole,
maxDurability,
);
2023-03-03 16:23:46 +01:00
return { Durability: currentDurability, MaxDurability: maxDurability };
2023-03-03 16:23:46 +01:00
}
/**
* Create a repairable object for an armor that containers durability + max durability properties
* @param itemTemplate weapon object being generated for
* @param botRole type of bot being generated for
* @returns Repairable object
*/
2023-11-13 17:07:59 +01:00
protected generateArmorRepairableProperties(itemTemplate: ITemplateItem, botRole: string): Repairable
2023-03-03 16:23:46 +01:00
{
let maxDurability: number;
let currentDurability: number;
if (parseInt(`${itemTemplate._props.armorClass}`) === 0)
{
maxDurability = itemTemplate._props.MaxDurability;
currentDurability = itemTemplate._props.MaxDurability;
}
2023-11-13 17:07:59 +01:00
else
2023-03-03 16:23:46 +01:00
{
maxDurability = this.durabilityLimitsHelper.getRandomizedMaxArmorDurability(itemTemplate, botRole);
2023-11-13 17:07:59 +01:00
currentDurability = this.durabilityLimitsHelper.getRandomizedArmorDurability(
itemTemplate,
botRole,
maxDurability,
);
2023-03-03 16:23:46 +01:00
}
return { Durability: currentDurability, MaxDurability: maxDurability };
2023-03-03 16:23:46 +01:00
}
public isWeaponModIncompatibleWithCurrentMods(
itemsEquipped: Item[],
tplToCheck: string,
modSlot: string,
): IChooseRandomCompatibleModResult
{
// TODO: Can probably be optimized to cache itemTemplates as items are added to inventory
const equippedItemsDb = itemsEquipped.map((item) => this.databaseServer.getTables().templates.items[item._tpl]);
const itemToEquipDb = this.itemHelper.getItem(tplToCheck);
const itemToEquip = itemToEquipDb[1];
if (!itemToEquipDb[0])
{
this.logger.warning(
this.localisationService.getText("bot-invalid_item_compatibility_check", {
itemTpl: tplToCheck,
slot: modSlot,
}),
);
return {
incompatible: true,
found: false,
reason: `item: ${tplToCheck} does not exist in the database`
};
}
// No props property
if (!itemToEquip._props)
{
this.logger.warning(
this.localisationService.getText("bot-compatibility_check_missing_props", {
id: itemToEquip._id,
name: itemToEquip._name,
slot: modSlot,
}),
);
return {
incompatible: true,
found: false,
reason: `item: ${tplToCheck} does not have a _props field`
};
}
// Check if any of the current weapon mod templates have the incoming item defined as incompatible
const blockingItem = equippedItemsDb.find((x) => x._props.ConflictingItems?.includes(tplToCheck));
if (blockingItem)
{
return {
incompatible: true,
found: false,
reason:
`Cannot add: ${tplToCheck} ${itemToEquip._name} to slot: ${modSlot}. Blocked by: ${blockingItem._id} ${blockingItem._name}`,
slotBlocked: true,
};
}
// Check inverse to above, if the incoming item has any existing mods in its conflicting items array
const blockingModItem = itemsEquipped.find((item) => itemToEquip._props.ConflictingItems?.includes(item._tpl));
if (blockingModItem)
{
return {
incompatible: true,
found: false,
reason:
` Cannot add: ${tplToCheck} to slot: ${modSlot}. Would block existing item: ${blockingModItem._tpl} in slot: ${blockingModItem.slotId}`,
};
}
return {
incompatible: false,
reason: ""
};
}
2023-03-03 16:23:46 +01:00
/**
* Can item be added to another item without conflict
* @param itemsEquipped Items to check compatibilities with
2023-03-03 16:23:46 +01:00
* @param tplToCheck Tpl of the item to check for incompatibilities
* @param equipmentSlot Slot the item will be placed into
* @returns false if no incompatibilities, also has incompatibility reason
*/
2023-11-13 17:07:59 +01:00
public isItemIncompatibleWithCurrentItems(
itemsEquipped: Item[],
2023-11-13 17:07:59 +01:00
tplToCheck: string,
equipmentSlot: string,
2024-01-27 19:12:13 +01:00
): IChooseRandomCompatibleModResult
2023-03-03 16:23:46 +01:00
{
// Skip slots that have no incompatibilities
2023-11-13 17:07:59 +01:00
if (["Scabbard", "Backpack", "SecureContainer", "Holster", "ArmBand"].includes(equipmentSlot))
2023-03-03 16:23:46 +01:00
{
2024-01-27 19:12:13 +01:00
return { incompatible: false, found: false, reason: "" };
2023-03-03 16:23:46 +01:00
}
// TODO: Can probably be optimized to cache itemTemplates as items are added to inventory
const equippedItemsDb = itemsEquipped.map((i) => this.databaseServer.getTables().templates.items[i._tpl]);
const itemToEquipDb = this.itemHelper.getItem(tplToCheck);
const itemToEquip = itemToEquipDb[1];
2023-03-03 16:23:46 +01:00
if (!itemToEquipDb[0])
2023-03-03 16:23:46 +01:00
{
2023-11-13 17:07:59 +01:00
this.logger.warning(
this.localisationService.getText("bot-invalid_item_compatibility_check", {
itemTpl: tplToCheck,
slot: equipmentSlot,
}),
);
2024-01-27 19:12:13 +01:00
return { incompatible: true, found: false, reason: `item: ${tplToCheck} does not exist in the database` };
2023-03-03 16:23:46 +01:00
}
if (!itemToEquip._props)
2023-03-03 16:23:46 +01:00
{
2023-11-13 17:07:59 +01:00
this.logger.warning(
this.localisationService.getText("bot-compatibility_check_missing_props", {
id: itemToEquip._id,
name: itemToEquip._name,
2023-11-13 17:07:59 +01:00
slot: equipmentSlot,
}),
);
2024-01-27 19:12:13 +01:00
return {
incompatible: true,
found: false,
reason: `item: ${tplToCheck} does not have a _props field`
};
2023-03-03 16:23:46 +01:00
}
// Does an equipped item have a property that blocks the desired item - check for prop "BlocksX" .e.g BlocksEarpiece / BlocksFaceCover
let blockingItem = equippedItemsDb.find((x) => x._props[`Blocks${equipmentSlot}`]);
2023-11-13 17:07:59 +01:00
if (blockingItem)
2023-03-03 16:23:46 +01:00
{
2023-11-13 17:07:59 +01:00
// this.logger.warning(`1 incompatibility found between - ${itemToEquip[1]._name} and ${blockingItem._name} - ${equipmentSlot}`);
return {
incompatible: true,
2024-01-27 19:12:13 +01:00
found: false,
2023-11-13 17:07:59 +01:00
reason:
`${tplToCheck} ${itemToEquip._name} in slot: ${equipmentSlot} blocked by: ${blockingItem._id} ${blockingItem._name}`,
slotBlocked: true,
2023-11-13 17:07:59 +01:00
};
2023-03-03 16:23:46 +01:00
}
// Check if any of the current inventory templates have the incoming item defined as incompatible
blockingItem = equippedItemsDb.find((x) => x._props.ConflictingItems?.includes(tplToCheck));
2023-11-13 17:07:59 +01:00
if (blockingItem)
2023-03-03 16:23:46 +01:00
{
2023-11-13 17:07:59 +01:00
// this.logger.warning(`2 incompatibility found between - ${itemToEquip[1]._name} and ${blockingItem._props.Name} - ${equipmentSlot}`);
return {
incompatible: true,
2024-01-27 19:12:13 +01:00
found: false,
2023-11-13 17:07:59 +01:00
reason:
`${tplToCheck} ${itemToEquip._name} in slot: ${equipmentSlot} blocked by: ${blockingItem._id} ${blockingItem._name}`,
slotBlocked: true,
2023-11-13 17:07:59 +01:00
};
2023-03-03 16:23:46 +01:00
}
// Does item being checked get blocked/block existing item
if (itemToEquip._props.BlocksHeadwear)
{
const existingHeadwear = itemsEquipped.find(x => x.slotId === "Headwear");
if (existingHeadwear)
{
return {
incompatible: true,
2024-01-27 19:12:13 +01:00
found: false,
reason:
`${tplToCheck} ${itemToEquip._name} is blocked by: ${existingHeadwear._tpl} in slot: ${existingHeadwear.slotId}`,
slotBlocked: true,
};
}
}
// Does item being checked get blocked/block existing item
if (itemToEquip._props.BlocksFaceCover)
{
const existingFaceCover = itemsEquipped.find(item => item.slotId === "FaceCover");
if (existingFaceCover)
{
return {
incompatible: true,
2024-01-27 19:12:13 +01:00
found: false,
reason:
`${tplToCheck} ${itemToEquip._name} is blocked by: ${existingFaceCover._tpl} in slot: ${existingFaceCover.slotId}`,
slotBlocked: true,
};
}
}
// Does item being checked get blocked/block existing item
if (itemToEquip._props.BlocksEarpiece)
{
const existingEarpiece = itemsEquipped.find(item => item.slotId === "Earpiece");
if (existingEarpiece)
{
return {
incompatible: true,
2024-01-27 19:12:13 +01:00
found: false,
reason:
`${tplToCheck} ${itemToEquip._name} is blocked by: ${existingEarpiece._tpl} in slot: ${existingEarpiece.slotId}`,
slotBlocked: true,
};
}
}
// Does item being checked get blocked/block existing item
if (itemToEquip._props.BlocksArmorVest)
{
const existingArmorVest = itemsEquipped.find(item => item.slotId === "ArmorVest");
if (existingArmorVest)
{
return {
incompatible: true,
2024-01-27 19:12:13 +01:00
found: false,
reason:
`${tplToCheck} ${itemToEquip._name} is blocked by: ${existingArmorVest._tpl} in slot: ${existingArmorVest.slotId}`,
slotBlocked: true,
};
}
}
2023-03-03 16:23:46 +01:00
// Check if the incoming item has any inventory items defined as incompatible
const blockingInventoryItem = itemsEquipped.find((x) => itemToEquip._props.ConflictingItems?.includes(x._tpl));
2023-11-13 17:07:59 +01:00
if (blockingInventoryItem)
2023-03-03 16:23:46 +01:00
{
2023-11-13 17:07:59 +01:00
// this.logger.warning(`3 incompatibility found between - ${itemToEquip[1]._name} and ${blockingInventoryItem._tpl} - ${equipmentSlot}`)
return {
incompatible: true,
2024-01-27 19:12:13 +01:00
found: false,
2023-11-13 17:07:59 +01:00
reason:
`${tplToCheck} blocks existing item ${blockingInventoryItem._tpl} in slot ${blockingInventoryItem.slotId}`,
};
2023-03-03 16:23:46 +01:00
}
return { incompatible: false, reason: "" };
2023-03-03 16:23:46 +01:00
}
/**
* Convert a bots role to the equipment role used in config/bot.json
* @param botRole Role to convert
* @returns Equipment role (e.g. pmc / assault / bossTagilla)
*/
2023-11-13 17:07:59 +01:00
public getBotEquipmentRole(botRole: string): string
2023-03-03 16:23:46 +01:00
{
2023-11-13 17:07:59 +01:00
return ([this.pmcConfig.usecType.toLowerCase(), this.pmcConfig.bearType.toLowerCase()].includes(
botRole.toLowerCase(),
))
? "pmc"
: botRole;
2023-03-03 16:23:46 +01:00
}
}
/** TODO - move into own class */
export class ExhaustableArray<T>
{
private pool: T[];
constructor(private itemPool: T[], private randomUtil: RandomUtil, private jsonUtil: JsonUtil)
2023-03-03 16:23:46 +01:00
{
this.pool = this.jsonUtil.clone(itemPool);
}
2023-11-13 17:07:59 +01:00
public getRandomValue(): T
2023-03-03 16:23:46 +01:00
{
2023-11-13 17:07:59 +01:00
if (!this.pool?.length)
2023-03-03 16:23:46 +01:00
{
return null;
}
const index = this.randomUtil.getInt(0, this.pool.length - 1);
const toReturn = this.jsonUtil.clone(this.pool[index]);
this.pool.splice(index, 1);
return toReturn;
}
2023-11-13 17:07:59 +01:00
public getFirstValue(): T
2023-03-03 16:23:46 +01:00
{
2023-11-13 17:07:59 +01:00
if (!this.pool?.length)
2023-03-03 16:23:46 +01:00
{
return null;
}
const toReturn = this.jsonUtil.clone(this.pool[0]);
this.pool.splice(0, 1);
return toReturn;
}
2023-11-13 17:07:59 +01:00
public hasValues(): boolean
2023-03-03 16:23:46 +01:00
{
2023-11-13 17:07:59 +01:00
if (this.pool?.length)
2023-03-03 16:23:46 +01:00
{
return true;
}
return false;
}
2023-11-13 17:07:59 +01:00
}