Server/project/src/generators/BotLootGenerator.ts

585 lines
23 KiB
TypeScript
Raw Normal View History

2023-03-03 16:23:46 +01:00
import { inject, injectable } from "tsyringe";
import { BotGeneratorHelper } from "../helpers/BotGeneratorHelper";
import { BotWeaponGeneratorHelper } from "../helpers/BotWeaponGeneratorHelper";
import { HandbookHelper } from "../helpers/HandbookHelper";
import { ItemHelper } from "../helpers/ItemHelper";
import { WeightedRandomHelper } from "../helpers/WeightedRandomHelper";
2023-03-03 16:23:46 +01:00
import { Inventory as PmcInventory } from "../models/eft/common/tables/IBotBase";
import { IBotType, Inventory, ModsChances } from "../models/eft/common/tables/IBotType";
2023-03-03 16:23:46 +01:00
import { Item } from "../models/eft/common/tables/IItem";
import { ITemplateItem } from "../models/eft/common/tables/ITemplateItem";
import { BaseClasses } from "../models/enums/BaseClasses";
import { ConfigTypes } from "../models/enums/ConfigTypes";
import { EquipmentSlots } from "../models/enums/EquipmentSlots";
import { ItemAddedResult } from "../models/enums/ItemAddedResult";
2023-03-22 15:49:24 +01:00
import { LootCacheType } from "../models/spt/bots/IBotLootCache";
2023-03-03 16:23:46 +01:00
import { IBotConfig } from "../models/spt/config/IBotConfig";
import { IPmcConfig } from "../models/spt/config/IPmcConfig";
2023-03-03 16:23:46 +01:00
import { ILogger } from "../models/spt/utils/ILogger";
import { ConfigServer } from "../servers/ConfigServer";
import { DatabaseServer } from "../servers/DatabaseServer";
import { BotLootCacheService } from "../services/BotLootCacheService";
import { LocalisationService } from "../services/LocalisationService";
import { HashUtil } from "../utils/HashUtil";
import { RandomUtil } from "../utils/RandomUtil";
import { BotWeaponGenerator } from "./BotWeaponGenerator";
@injectable()
export class BotLootGenerator
{
protected botConfig: IBotConfig;
protected pmcConfig: IPmcConfig;
2023-03-03 16:23:46 +01:00
constructor(
@inject("WinstonLogger") protected logger: ILogger,
@inject("HashUtil") protected hashUtil: HashUtil,
@inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("HandbookHelper") protected handbookHelper: HandbookHelper,
@inject("BotGeneratorHelper") protected botGeneratorHelper: BotGeneratorHelper,
@inject("BotWeaponGenerator") protected botWeaponGenerator: BotWeaponGenerator,
@inject("BotWeaponGeneratorHelper") protected botWeaponGeneratorHelper: BotWeaponGeneratorHelper,
@inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper,
2023-03-03 16:23:46 +01:00
@inject("BotLootCacheService") protected botLootCacheService: BotLootCacheService,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ConfigServer") protected configServer: ConfigServer
)
{
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC);
2023-03-03 16:23:46 +01:00
}
/**
* Add loot to bots containers
* @param sessionId Session id
* @param botJsonTemplate Base json db file for the bot having its loot generated
2023-03-03 16:23:46 +01:00
* @param isPmc Will bot be a pmc
* @param botRole Role of bot, e.g. asssult
* @param botInventory Inventory to add loot to
* @param botLevel Level of bot
*/
public generateLoot(sessionId: string, botJsonTemplate: IBotType, isPmc: boolean, botRole: string, botInventory: PmcInventory, botLevel: number): void
2023-03-03 16:23:46 +01:00
{
// Limits on item types to be added as loot
const itemCounts = botJsonTemplate.generation.items;
const backpackLootCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.backpackLoot.weights);
const pocketLootCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.pocketLoot.weights);
const vestLootCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.vestLoot.weights);
const specialLootItemCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.specialItems.weights);
const healingItemCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.healing.weights);
const drugItemCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.drugs.weights);
const stimItemCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.stims.weights);
const grenadeCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.grenades.weights);
2023-03-03 16:23:46 +01:00
// Forced pmc healing loot
if (isPmc && this.pmcConfig.forceHealingItemsIntoSecure)
{
this.addForcedMedicalItemsToPmcSecure(botInventory, botRole);
}
const containersBotHasAvailable = this.getAvailableContainersBotCanStoreItemsIn(botInventory);
2023-03-03 16:23:46 +01:00
// Special items
this.addLootFromPool(
this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.SPECIAL, botJsonTemplate),
containersBotHasAvailable,
2023-03-03 16:23:46 +01:00
specialLootItemCount,
botInventory,
botRole);
// Meds
this.addLootFromPool(
this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.HEALING_ITEMS, botJsonTemplate),
containersBotHasAvailable,
2023-03-03 16:23:46 +01:00
healingItemCount,
botInventory,
botRole,
false,
0,
isPmc);
// Drugs
this.addLootFromPool(
this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.DRUG_ITEMS, botJsonTemplate),
containersBotHasAvailable,
2023-03-03 16:23:46 +01:00
drugItemCount,
botInventory,
botRole,
false,
0,
isPmc);
// Stims
this.addLootFromPool(
this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.STIM_ITEMS, botJsonTemplate),
containersBotHasAvailable,
2023-03-03 16:23:46 +01:00
stimItemCount,
botInventory,
botRole,
true,
0,
isPmc);
// Grenades
this.addLootFromPool(
this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.GRENADE_ITEMS, botJsonTemplate),
[EquipmentSlots.POCKETS, EquipmentSlots.TACTICAL_VEST], // Can't use containersBotHasEquipped as we dont want grenades added to backpack
2023-03-03 16:23:46 +01:00
grenadeCount,
botInventory,
botRole,
false,
0,
isPmc);
// Backpack - generate loot if they have one
if (containersBotHasAvailable.includes(EquipmentSlots.BACKPACK))
{
// Add randomly generated weapon to PMC backpacks
if (isPmc && this.randomUtil.getChance100(this.pmcConfig.looseWeaponInBackpackChancePercent))
{
this.addLooseWeaponsToInventorySlot(sessionId, botInventory, EquipmentSlots.BACKPACK, botJsonTemplate.inventory, botJsonTemplate.chances.mods, botRole, isPmc, botLevel);
}
2023-03-03 16:23:46 +01:00
this.addLootFromPool(
this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.BACKPACK, botJsonTemplate),
[EquipmentSlots.BACKPACK],
backpackLootCount,
botInventory,
botRole,
true,
this.pmcConfig.maxBackpackLootTotalRub,
isPmc);
}
// TacticalVest - generate loot if they have one
if (containersBotHasAvailable.includes(EquipmentSlots.TACTICAL_VEST))
{
// Vest
this.addLootFromPool(
this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.VEST, botJsonTemplate),
[EquipmentSlots.TACTICAL_VEST],
vestLootCount,
botInventory,
botRole,
true,
this.pmcConfig.maxVestLootTotalRub,
isPmc);
}
2023-03-03 16:23:46 +01:00
// Pockets
this.addLootFromPool(
this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.POCKET, botJsonTemplate),
2023-03-03 16:23:46 +01:00
[EquipmentSlots.POCKETS],
pocketLootCount,
botInventory,
botRole,
true,
this.pmcConfig.maxPocketLootTotalRub,
2023-03-03 16:23:46 +01:00
isPmc);
}
/**
* Get an array of the containers a bot has on them (pockets/backpack/vest)
* @param botInventory Bot to check
* @returns Array of available slots
*/
protected getAvailableContainersBotCanStoreItemsIn(botInventory: PmcInventory): EquipmentSlots[]
{
const result = [EquipmentSlots.POCKETS];
if (botInventory.items.find(x => x.slotId === EquipmentSlots.TACTICAL_VEST))
{
result.push(EquipmentSlots.TACTICAL_VEST);
}
if (botInventory.items.find(x => x.slotId === EquipmentSlots.BACKPACK))
{
result.push(EquipmentSlots.BACKPACK);
}
return result;
}
/**
* Force healing items onto bot to ensure they can heal in-raid
* @param botInventory Inventory to add items to
* @param botRole Role of bot (sptBear/sptUsec)
*/
protected addForcedMedicalItemsToPmcSecure(botInventory: PmcInventory, botRole: string): void
{
const grizzly = this.itemHelper.getItem("590c657e86f77412b013051d")[1];
this.addLootFromPool(
[grizzly],
[EquipmentSlots.SECURED_CONTAINER],
2,
botInventory,
botRole,
false,
0,
true);
const surv12 = this.itemHelper.getItem("5d02797c86f774203f38e30a")[1];
this.addLootFromPool(
[surv12],
[EquipmentSlots.SECURED_CONTAINER],
1,
botInventory,
botRole,
false,
0,
true);
const morphine = this.itemHelper.getItem("544fb3f34bdc2d03748b456a")[1];
this.addLootFromPool(
[morphine],
[EquipmentSlots.SECURED_CONTAINER],
3,
botInventory,
botRole,
false,
0,
true);
2023-07-27 16:01:08 +02:00
const afak = this.itemHelper.getItem("60098ad7c2240c0fe85c570a")[1];
this.addLootFromPool(
[afak],
[EquipmentSlots.SECURED_CONTAINER],
2,
botInventory,
botRole,
false,
0,
true);
}
/**
* Get a biased random number
* @param min Smallest size
* @param max Biggest size
* @param nValue Value to bias choice
* @returns Chosen number
*/
2023-03-03 16:23:46 +01:00
protected getRandomisedCount(min: number, max: number, nValue: number): number
{
const range = max - min;
return this.randomUtil.getBiasedRandomNumber(min, max, range, nValue);
}
/**
* Take random items from a pool and add to an inventory until totalItemCount or totalValueLimit is reached
* @param pool Pool of items to pick from
* @param equipmentSlots What equipment slot will the loot items be added to
2023-03-03 16:23:46 +01:00
* @param totalItemCount Max count of items to add
* @param inventoryToAddItemsTo Bot inventory loot will be added to
* @param botRole Role of the bot loot is being generated for (assault/pmcbot)
* @param useLimits Should item limit counts be used as defined in config/bot.json
* @param totalValueLimitRub Total value of loot allowed in roubles
* @param isPmc Is bot being generated for a pmc
2023-03-03 16:23:46 +01:00
*/
protected addLootFromPool(
pool: ITemplateItem[],
equipmentSlots: string[],
totalItemCount: number,
inventoryToAddItemsTo: PmcInventory,
botRole: string,
useLimits = false,
totalValueLimitRub = 0,
isPmc = false): void
{
// Loot pool has items
if (pool.length)
{
let currentTotalRub = 0;
const itemLimits: Record<string, number> = {};
const itemSpawnLimits: Record<string,Record<string, number>> = {};
let fitItemIntoContainerAttempts = 0;
2023-03-03 16:23:46 +01:00
for (let i = 0; i < totalItemCount; i++)
{
const itemToAddTemplate = this.getRandomItemFromPoolByRole(pool, botRole);
2023-03-03 16:23:46 +01:00
const id = this.hashUtil.generate();
const itemsToAdd: Item[] = [{
_id: id,
_tpl: itemToAddTemplate._id,
...this.botGeneratorHelper.generateExtraPropertiesForItem(itemToAddTemplate, botRole)
2023-03-03 16:23:46 +01:00
}];
if (useLimits)
{
if (Object.keys(itemLimits).length === 0)
{
this.initItemLimitArray(isPmc, botRole, itemLimits);
}
if (!itemSpawnLimits[botRole])
{
itemSpawnLimits[botRole] = this.getItemSpawnLimitsForBotType(isPmc, botRole);
}
if (this.itemHasReachedSpawnLimit(itemToAddTemplate, botRole, isPmc, itemLimits, itemSpawnLimits[botRole]))
{
i--;
continue;
}
}
// Fill ammo box
if (this.itemHelper.isOfBaseclass(itemToAddTemplate._id, BaseClasses.AMMO_BOX))
{
this.itemHelper.addCartridgesToAmmoBox(itemsToAdd, itemToAddTemplate);
}
// make money a stack
else if (this.itemHelper.isOfBaseclass(itemToAddTemplate._id, BaseClasses.MONEY))
{
this.randomiseMoneyStackSize(isPmc, itemToAddTemplate, itemsToAdd[0]);
}
// Make ammo a stack
else if (this.itemHelper.isOfBaseclass(itemToAddTemplate._id, BaseClasses.AMMO))
{
this.randomiseAmmoStackSize(isPmc, itemToAddTemplate, itemsToAdd[0]);
}
// Attempt to add item to container(s)
const itemAddedResult = this.botWeaponGeneratorHelper.addItemWithChildrenToEquipmentSlot(equipmentSlots, id, itemToAddTemplate._id, itemsToAdd, inventoryToAddItemsTo);
if (itemAddedResult === ItemAddedResult.NO_SPACE)
{
fitItemIntoContainerAttempts++;
if (fitItemIntoContainerAttempts >= 4)
{
this.logger.debug(`Failed to place item ${i} of ${totalItemCount} item into ${botRole} container: ${equipmentSlots}, ${fitItemIntoContainerAttempts} times, skipping`);
break;
}
}
else
{
fitItemIntoContainerAttempts = 0;
}
2023-03-03 16:23:46 +01:00
// Stop adding items to bots pool if rolling total is over total limit
if (totalValueLimitRub > 0 && itemAddedResult === ItemAddedResult.SUCCESS)
2023-03-03 16:23:46 +01:00
{
currentTotalRub += this.handbookHelper.getTemplatePrice(itemToAddTemplate._id);
if (currentTotalRub > totalValueLimitRub)
{
break;
}
}
}
}
}
/**
* Add generated weapons to inventory as loot
* @param botInventory inventory to add preset to
* @param equipmentSlot slot to place the preset in (backpack)
* @param templateInventory bots template, assault.json
* @param modChances chances for mods to spawn on weapon
* @param botRole bots role, .e.g. pmcBot
* @param isPmc are we generating for a pmc
*/
protected addLooseWeaponsToInventorySlot(sessionId: string, botInventory: PmcInventory, equipmentSlot: string, templateInventory: Inventory, modChances: ModsChances, botRole: string, isPmc: boolean, botLevel: number): void
{
const chosenWeaponType = this.randomUtil.getArrayValue([EquipmentSlots.FIRST_PRIMARY_WEAPON, EquipmentSlots.FIRST_PRIMARY_WEAPON, EquipmentSlots.FIRST_PRIMARY_WEAPON, EquipmentSlots.HOLSTER]);
const randomisedWeaponCount = this.randomUtil.getInt(this.pmcConfig.looseWeaponInBackpackLootMinMax.min, this.pmcConfig.looseWeaponInBackpackLootMinMax.max);
2023-03-03 16:23:46 +01:00
if (randomisedWeaponCount > 0)
{
for (let i = 0; i < randomisedWeaponCount; i++)
{
const generatedWeapon = this.botWeaponGenerator.generateRandomWeapon(sessionId, chosenWeaponType, templateInventory, botInventory.equipment, modChances, botRole, isPmc, botLevel);
this.botWeaponGeneratorHelper.addItemWithChildrenToEquipmentSlot([equipmentSlot], generatedWeapon.weapon[0]._id, generatedWeapon.weapon[0]._tpl, [...generatedWeapon.weapon], botInventory);
}
}
}
/**
* Get a random item from the pool parameter using the biasedRandomNumber system
* @param pool Pool of items to pick an item from
* @param isPmc Is the bot being created a pmc
* @returns ITemplateItem object
*/
protected getRandomItemFromPoolByRole(pool: ITemplateItem[], botRole: string): ITemplateItem
{
const itemIndex = this.randomUtil.getBiasedRandomNumber(0, pool.length - 1, pool.length - 1, this.getBotLootNValueByRole(botRole));
return pool[itemIndex];
}
/**
* Get the loot nvalue from botconfig
* @param botRole Role of bot e.g. assault/bosstagilla/sptBear
* @returns nvalue as number
*/
protected getBotLootNValueByRole(botRole: string): number
{
const result = this.botConfig.lootNValue[botRole];
if (!result)
{
2023-07-19 14:16:45 +02:00
this.logger.warning(this.localisationService.getText("bot-unable_to_find_loot_n_value_for_bot", botRole));
return this.botConfig.lootNValue["scav"];
}
return result;
}
2023-03-03 16:23:46 +01:00
/**
* Hydrate item limit array to contain items that have a limit for a specific bot type
2023-03-03 16:23:46 +01:00
* All values are set to 0
* @param isPmc Is the bot a pmc
* @param botRole Role the bot has
2023-03-03 16:23:46 +01:00
* @param limitCount
*/
protected initItemLimitArray(isPmc: boolean, botRole: string, limitCount: Record<string, number>): void
{
// Init current count of items we want to limit
const spawnLimits = this.getItemSpawnLimitsForBotType(isPmc, botRole);
for (const limit in spawnLimits)
{
limitCount[limit] = 0;
}
}
/**
* Check if an item has reached its bot-specific spawn limit
* @param itemTemplate Item we check to see if its reached spawn limit
* @param botRole Bot type
* @param isPmc Is bot we're working with a pmc
* @param limitCount Spawn limits for items on bot
* @param itemSpawnLimits The limits this bot is allowed to have
2023-03-03 16:23:46 +01:00
* @returns true if item has reached spawn limit
*/
protected itemHasReachedSpawnLimit(itemTemplate: ITemplateItem, botRole: string, isPmc: boolean, limitCount: Record<string, number>, itemSpawnLimits: Record<string, number>): boolean
{
// PMCs and scavs have different sections of bot config for spawn limits
if (!!itemSpawnLimits && itemSpawnLimits.length === 0)
{
// No items found in spawn limit, drop out
return false;
}
// No spawn limits, skipping
if (!itemSpawnLimits)
{
return false;
}
const idToCheckFor = this.getMatchingIdFromSpawnLimits(itemTemplate, itemSpawnLimits);
if (!idToCheckFor)
{
// ParentId or tplid not found in spawnLimits, not a spawn limited item, skip
return false;
}
// Increment item count with this bot type
limitCount[idToCheckFor]++;
// return true, we are over limit
if (limitCount[idToCheckFor] > itemSpawnLimits[idToCheckFor])
{
// Prevent edge-case of small loot pools + code trying to add limited item over and over infinitely
2023-03-03 16:23:46 +01:00
if (limitCount[idToCheckFor] > itemSpawnLimits[idToCheckFor] * 10)
{
this.logger.debug(this.localisationService.getText("bot-item_spawn_limit_reached_skipping_item", {botRole: botRole, itemName: itemTemplate._name, attempts: limitCount[idToCheckFor]}));
2023-03-03 16:23:46 +01:00
return false;
}
return true;
}
return false;
}
/**
* Randomise the stack size of a money object, uses different values for pmc or scavs
* @param isPmc is this a PMC
* @param itemTemplate item details
* @param moneyItem Money stack to randomise
*/
protected randomiseMoneyStackSize(isPmc: boolean, itemTemplate: ITemplateItem, moneyItem: Item): void
{
// Only add if no upd or stack objects exist - preserves existing stack count
if (!moneyItem.upd?.StackObjectsCount)
{
// PMCs have a different stack max size
const minStackSize = itemTemplate._props.StackMinRandom;
const maxStackSize = (isPmc)
? this.pmcConfig.dynamicLoot.moneyStackLimits[itemTemplate._id]
2023-03-03 16:23:46 +01:00
: itemTemplate._props.StackMaxRandom;
moneyItem.upd = { "StackObjectsCount": this.randomUtil.getInt(minStackSize, maxStackSize) };
}
}
/**
* Randomise the size of an ammo stack
* @param isPmc is this a PMC
* @param itemTemplate item details
* @param ammoItem Ammo stack to randomise
*/
protected randomiseAmmoStackSize(isPmc: boolean, itemTemplate: ITemplateItem, ammoItem: Item): void
{
// only add if no upd or stack objects exist - preserves existing stack count
if (!ammoItem.upd?.StackObjectsCount)
{
const minStackSize = itemTemplate._props.StackMinRandom;
const maxStackSize = itemTemplate._props.StackMaxSize;
ammoItem.upd = { "StackObjectsCount": this.randomUtil.getInt(minStackSize, maxStackSize) };
}
}
/**
* Get spawn limits for a specific bot type from bot.json config
* If no limit found for a non pmc bot, fall back to defaults
* @param isPmc is the bot we want limits for a pmc
* @param botRole what role does the bot have
* @returns Dictionary of tplIds and limit
*/
protected getItemSpawnLimitsForBotType(isPmc: boolean, botRole: string): Record<string, number>
{
if (isPmc)
{
return this.botConfig.itemSpawnLimits["pmc"];
}
if (this.botConfig.itemSpawnLimits[botRole.toLowerCase()])
{
return this.botConfig.itemSpawnLimits[botRole.toLowerCase()];
}
this.logger.warning(this.localisationService.getText("bot-unable_to_find_spawn_limits_fallback_to_defaults", botRole));
return this.botConfig.itemSpawnLimits["default"];
}
/**
* Get the parentId or tplId of item inside spawnLimits object if it exists
* @param itemTemplate item we want to look for in spawn limits
* @param spawnLimits Limits to check for item
* @returns id as string, otherwise undefined
*/
protected getMatchingIdFromSpawnLimits(itemTemplate: ITemplateItem, spawnLimits: Record<string, number>): string
{
if (itemTemplate._id in spawnLimits)
{
return itemTemplate._id;
}
// tplId not found in spawnLimits, check if parentId is
if (itemTemplate._parent in spawnLimits)
{
return itemTemplate._parent;
}
// parentId and tplid not found
return undefined;
}
}