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

594 lines
21 KiB
TypeScript
Raw Normal View History

2023-03-03 15:23:46 +00:00
import { inject, injectable } from "tsyringe";
import { PMCLootGenerator } from "@spt-aki/generators/PMCLootGenerator";
import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
import { IBotType } from "@spt-aki/models/eft/common/tables/IBotType";
import { ITemplateItem, Props } from "@spt-aki/models/eft/common/tables/ITemplateItem";
import { BaseClasses } from "@spt-aki/models/enums/BaseClasses";
import { IBotLootCache, LootCacheType } from "@spt-aki/models/spt/bots/IBotLootCache";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer";
import { LocalisationService } from "@spt-aki/services/LocalisationService";
import { RagfairPriceService } from "@spt-aki/services/RagfairPriceService";
import { JsonUtil } from "@spt-aki/utils/JsonUtil";
2023-03-03 15:23:46 +00:00
@injectable()
export class BotLootCacheService
{
2023-03-22 14:49:24 +00:00
protected lootCache: Record<string, IBotLootCache>;
2023-03-03 15:23:46 +00:00
constructor(
@inject("WinstonLogger") protected logger: ILogger,
@inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("ItemHelper") protected itemHelper: ItemHelper,
2023-03-03 15:23:46 +00:00
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("PMCLootGenerator") protected pmcLootGenerator: PMCLootGenerator,
@inject("LocalisationService") protected localisationService: LocalisationService,
2023-11-13 11:13:25 -05:00
@inject("RagfairPriceService") protected ragfairPriceService: RagfairPriceService,
2023-03-03 15:23:46 +00:00
)
{
this.clearCache();
}
/**
2023-04-24 11:57:19 +01:00
* Remove cached bot loot data
2023-03-03 15:23:46 +00:00
*/
public clearCache(): void
{
this.lootCache = {};
}
/**
* Get the fully created loot array, ordered by price low to high
* @param botRole bot to get loot for
* @param isPmc is the bot a pmc
* @param lootType what type of loot is needed (backpack/pocket/stim/vest etc)
* @param botJsonTemplate Base json db file for the bot having its loot generated
2023-03-03 15:23:46 +00:00
* @returns ITemplateItem array
*/
2023-11-13 11:13:25 -05:00
public getLootFromCache(
botRole: string,
isPmc: boolean,
lootType: LootCacheType,
botJsonTemplate: IBotType,
): Record<string, number>
2023-03-03 15:23:46 +00:00
{
if (!this.botRoleExistsInCache(botRole))
{
this.initCacheForBotRole(botRole);
this.addLootToCache(botRole, isPmc, botJsonTemplate);
2023-03-03 15:23:46 +00:00
}
let result = undefined;
2023-03-03 15:23:46 +00:00
switch (lootType)
{
case LootCacheType.SPECIAL:
result = this.lootCache[botRole].specialItems;
break;
2023-03-03 15:23:46 +00:00
case LootCacheType.BACKPACK:
result = this.lootCache[botRole].backpackLoot;
break;
2023-03-03 15:23:46 +00:00
case LootCacheType.POCKET:
result = this.lootCache[botRole].pocketLoot;
break;
2023-03-03 15:23:46 +00:00
case LootCacheType.VEST:
result = this.lootCache[botRole].vestLoot;
break;
case LootCacheType.SECURE:
result = this.lootCache[botRole].secureLoot;
break;
2023-03-03 15:23:46 +00:00
case LootCacheType.COMBINED:
result = this.lootCache[botRole].combinedPoolLoot;
break;
2023-03-03 15:23:46 +00:00
case LootCacheType.HEALING_ITEMS:
result = this.lootCache[botRole].healingItems;
break;
2023-03-03 15:23:46 +00:00
case LootCacheType.GRENADE_ITEMS:
result = this.lootCache[botRole].grenadeItems;
break;
2023-03-03 15:23:46 +00:00
case LootCacheType.DRUG_ITEMS:
result = this.lootCache[botRole].drugItems;
break;
case LootCacheType.FOOD_ITEMS:
result = this.lootCache[botRole].foodItems;
break;
case LootCacheType.DRINK_ITEMS:
result = this.lootCache[botRole].drinkItems;
break;
case LootCacheType.CURRENCY_ITEMS:
result = this.lootCache[botRole].currencyItems;
break;
2023-03-03 15:23:46 +00:00
case LootCacheType.STIM_ITEMS:
result = this.lootCache[botRole].stimItems;
break;
2023-03-03 15:23:46 +00:00
default:
2023-11-13 11:13:25 -05:00
this.logger.error(
this.localisationService.getText("bot-loot_type_not_found", {
lootType: lootType,
botRole: botRole,
isPmc: isPmc,
}),
);
2023-03-03 15:23:46 +00:00
break;
}
return this.jsonUtil.clone(result);
2023-03-03 15:23:46 +00:00
}
/**
* Generate loot for a bot and store inside a private class property
* @param botRole bots role (assault / pmcBot etc)
* @param isPmc Is the bot a PMC (alteres what loot is cached)
* @param botJsonTemplate db template for bot having its loot generated
2023-03-03 15:23:46 +00:00
*/
protected addLootToCache(botRole: string, isPmc: boolean, botJsonTemplate: IBotType): void
2023-03-03 15:23:46 +00:00
{
// the full pool of loot we use to create the various sub-categories with
const lootPool = botJsonTemplate.inventory.items;
2023-03-03 15:23:46 +00:00
// Flatten all individual slot loot pools into one big pool, while filtering out potentially missing templates
const specialLootPool: Record<string, number> = {};
const backpackLootPool: Record<string, number> = {};
const pocketLootPool: Record<string, number> = {};
const vestLootPool: Record<string, number> = {};
const secureLootTPool: Record<string, number> = {};
const combinedLootPool: Record<string, number> = {};
2023-03-03 15:23:46 +00:00
if (isPmc)
{
// Replace lootPool from bot json with our own generated list for PMCs
lootPool.Backpack = this.jsonUtil.clone(this.pmcLootGenerator.generatePMCBackpackLootPool(botRole));
lootPool.Pockets = this.jsonUtil.clone(this.pmcLootGenerator.generatePMCPocketLootPool(botRole));
lootPool.TacticalVest = this.jsonUtil.clone(this.pmcLootGenerator.generatePMCVestLootPool(botRole));
2023-03-03 15:23:46 +00:00
}
// Backpack/Pockets etc
2023-03-03 15:23:46 +00:00
for (const [slot, pool] of Object.entries(lootPool))
{
// No items to add, skip
if (Object.keys(pool).length === 0)
2023-03-03 15:23:46 +00:00
{
continue;
}
// Sort loot pool into separate buckets
2023-03-03 15:23:46 +00:00
switch (slot.toLowerCase())
{
case "specialloot":
this.addItemsToPool(specialLootPool, pool);
2023-03-03 15:23:46 +00:00
break;
case "pockets":
this.addItemsToPool(pocketLootPool, pool);
2023-03-03 15:23:46 +00:00
break;
case "tacticalvest":
this.addItemsToPool(vestLootPool, pool);
2023-03-03 15:23:46 +00:00
break;
case "securedcontainer":
this.addItemsToPool(secureLootTPool, pool);
break;
case "backpack":
this.addItemsToPool(backpackLootPool, pool);
2023-03-03 15:23:46 +00:00
break;
default:
this.logger.warning(`How did you get here ${slot}`);
2023-03-03 15:23:46 +00:00
}
2023-11-13 11:13:25 -05:00
// Add all items (if any) to combined pool (excluding secure)
if (Object.keys(pool).length > 0 && slot.toLowerCase() !== "securedcontainer")
{
this.addItemsToPool(combinedLootPool, pool);
}
}
// Assign whitelisted special items to bot if any exist
const specialLootItems: Record<string, number> =
(Object.keys(botJsonTemplate.generation.items.specialItems.whitelist)?.length > 0)
? botJsonTemplate.generation.items.specialItems.whitelist
: {};
// no whitelist, find and assign from combined item pool
if (Object.keys(specialLootItems).length === 0)
{
for (const [tpl, weight] of Object.entries(specialLootPool))
{
const itemTemplate = this.itemHelper.getItem(tpl)[1];
if (!(this.isBulletOrGrenade(itemTemplate._props) || this.isMagazine(itemTemplate._props)))
{
specialLootItems[tpl] = weight;
}
}
}
// Assign whitelisted healing items to bot if any exist
const healingItems: Record<string, number> =
(Object.keys(botJsonTemplate.generation.items.healing.whitelist)?.length > 0)
? botJsonTemplate.generation.items.healing.whitelist
: {};
// No whitelist, find and assign from combined item pool
if (Object.keys(healingItems).length === 0)
{
for (const [tpl, weight] of Object.entries(combinedLootPool))
{
const itemTemplate = this.itemHelper.getItem(tpl)[1];
if (
this.isMedicalItem(itemTemplate._props)
&& itemTemplate._parent !== BaseClasses.STIMULATOR
&& itemTemplate._parent !== BaseClasses.DRUGS
)
{
healingItems[tpl] = weight;
}
}
}
// Assign whitelisted drugs to bot if any exist
const drugItems: Record<string, number> =
(Object.keys(botJsonTemplate.generation.items.drugs.whitelist)?.length > 0)
? botJsonTemplate.generation.items.drugs.whitelist
: {};
// no drugs whitelist, find and assign from combined item pool
if (Object.keys(drugItems).length === 0)
{
for (const [tpl, weight] of Object.entries(combinedLootPool))
2023-03-03 15:23:46 +00:00
{
const itemTemplate = this.itemHelper.getItem(tpl)[1];
if (this.isMedicalItem(itemTemplate._props) && itemTemplate._parent === BaseClasses.DRUGS)
{
drugItems[tpl] = weight;
}
2023-03-03 15:23:46 +00:00
}
}
// Assign whitelisted food to bot if any exist
const foodItems: Record<string, number> =
(Object.keys(botJsonTemplate.generation.items.food.whitelist)?.length > 0)
? botJsonTemplate.generation.items.food.whitelist
: {};
// No food whitelist, find and assign from combined item pool
if (Object.keys(foodItems).length === 0)
{
for (const [tpl, weight] of Object.entries(combinedLootPool))
{
const itemTemplate = this.itemHelper.getItem(tpl)[1];
if (this.itemHelper.isOfBaseclass(itemTemplate._id, BaseClasses.FOOD))
{
foodItems[tpl] = weight;
}
}
}
// Assign whitelisted drink to bot if any exist
const drinkItems: Record<string, number> =
(Object.keys(botJsonTemplate.generation.items.food.whitelist)?.length > 0)
? botJsonTemplate.generation.items.food.whitelist
: {};
// No drink whitelist, find and assign from combined item pool
if (Object.keys(drinkItems).length === 0)
{
for (const [tpl, weight] of Object.entries(combinedLootPool))
{
const itemTemplate = this.itemHelper.getItem(tpl)[1];
if (this.itemHelper.isOfBaseclass(itemTemplate._id, BaseClasses.DRINK))
{
drinkItems[tpl] = weight;
}
}
}
// Assign whitelisted currency to bot if any exist
const currencyItems: Record<string, number> =
(Object.keys(botJsonTemplate.generation.items.currency.whitelist)?.length > 0)
? botJsonTemplate.generation.items.currency.whitelist
: {};
// No currency whitelist, find and assign from combined item pool
if (Object.keys(currencyItems).length === 0)
{
for (const [tpl, weight] of Object.entries(combinedLootPool))
{
const itemTemplate = this.itemHelper.getItem(tpl)[1];
if (this.itemHelper.isOfBaseclass(itemTemplate._id, BaseClasses.MONEY))
{
currencyItems[tpl] = weight;
}
}
}
// Assign whitelisted stims to bot if any exist
const stimItems: Record<string, number> =
(Object.keys(botJsonTemplate.generation.items.stims.whitelist)?.length > 0)
? botJsonTemplate.generation.items.stims.whitelist
: {};
// No whitelist, find and assign from combined item pool
if (Object.keys(stimItems).length === 0)
{
for (const [tpl, weight] of Object.entries(combinedLootPool))
{
const itemTemplate = this.itemHelper.getItem(tpl)[1];
if (this.isMedicalItem(itemTemplate._props) && itemTemplate._parent === BaseClasses.STIMULATOR)
{
stimItems[tpl] = weight;
}
}
}
// Assign whitelisted grenades to bot if any exist
const grenadeItems: Record<string, number> =
(Object.keys(botJsonTemplate.generation.items.grenades.whitelist)?.length > 0)
? botJsonTemplate.generation.items.grenades.whitelist
: {};
// no whitelist, find and assign from combined item pool
if (Object.keys(grenadeItems).length === 0)
{
for (const [tpl, weight] of Object.entries(combinedLootPool))
{
const itemTemplate = this.itemHelper.getItem(tpl)[1];
if (this.isGrenade(itemTemplate._props))
{
grenadeItems[tpl] = weight;
}
}
}
// Get backpack loot (excluding magazines, bullets, grenades, drink, food and healing/stim items)
const filteredBackpackItems = {};
for (const itemKey of Object.keys(backpackLootPool))
{
const itemResult = this.itemHelper.getItem(itemKey);
if (!itemResult[0])
{
continue;
}
const itemTemplate = itemResult[1];
if (
this.isBulletOrGrenade(itemTemplate._props)
|| this.isMagazine(itemTemplate._props)
|| this.isMedicalItem(itemTemplate._props)
|| this.isGrenade(itemTemplate._props)
|| this.isFood(itemTemplate._id)
|| this.isDrink(itemTemplate._id)
|| this.isCurrency(itemTemplate._id)
)
{
// Is type we dont want as backpack loot, skip
continue;
}
filteredBackpackItems[itemKey] = backpackLootPool[itemKey];
}
// Get pocket loot (excluding magazines, bullets, grenades, drink, food medical and healing/stim items)
const filteredPocketItems = {};
for (const itemKey of Object.keys(pocketLootPool))
{
const itemResult = this.itemHelper.getItem(itemKey);
if (!itemResult[0])
{
continue;
}
const itemTemplate = itemResult[1];
if (
this.isBulletOrGrenade(itemTemplate._props)
|| this.isMagazine(itemTemplate._props)
|| this.isMedicalItem(itemTemplate._props)
|| this.isGrenade(itemTemplate._props)
|| this.isFood(itemTemplate._id)
|| this.isDrink(itemTemplate._id)
|| this.isCurrency(itemTemplate._id)
|| !("Height" in itemTemplate._props) // lacks height
|| !("Width" in itemTemplate._props) // lacks width
)
{
continue;
}
filteredPocketItems[itemKey] = pocketLootPool[itemKey];
}
// Get vest loot (excluding magazines, bullets, grenades, medical and healing/stim items)
const filteredVestItems = {};
for (const itemKey of Object.keys(vestLootPool))
{
const itemResult = this.itemHelper.getItem(itemKey);
if (!itemResult[0])
{
continue;
}
const itemTemplate = itemResult[1];
if (
this.isBulletOrGrenade(itemTemplate._props)
|| this.isMagazine(itemTemplate._props)
|| this.isMedicalItem(itemTemplate._props)
|| this.isGrenade(itemTemplate._props)
|| this.isFood(itemTemplate._id)
|| this.isDrink(itemTemplate._id)
|| this.isCurrency(itemTemplate._id)
)
{
continue;
}
filteredVestItems[itemKey] = vestLootPool[itemKey];
}
2023-03-03 15:23:46 +00:00
this.lootCache[botRole].healingItems = healingItems;
this.lootCache[botRole].drugItems = drugItems;
this.lootCache[botRole].foodItems = foodItems;
this.lootCache[botRole].drinkItems = drinkItems;
this.lootCache[botRole].currencyItems = currencyItems;
2023-03-03 15:23:46 +00:00
this.lootCache[botRole].stimItems = stimItems;
this.lootCache[botRole].grenadeItems = grenadeItems;
this.lootCache[botRole].specialItems = specialLootItems;
this.lootCache[botRole].backpackLoot = filteredBackpackItems;
this.lootCache[botRole].pocketLoot = filteredPocketItems;
this.lootCache[botRole].vestLoot = filteredVestItems;
this.lootCache[botRole].secureLoot = secureLootTPool;
2023-03-03 15:23:46 +00:00
}
/**
* Add unique items into combined pool
* @param poolToAddTo Pool of items to add to
2023-03-03 15:23:46 +00:00
* @param itemsToAdd items to add to combined pool if unique
*/
protected addUniqueItemsToPool(poolToAddTo: ITemplateItem[], itemsToAdd: ITemplateItem[]): void
2023-03-03 15:23:46 +00:00
{
if (poolToAddTo.length === 0)
2023-03-03 15:23:46 +00:00
{
poolToAddTo.push(...itemsToAdd);
2023-03-03 15:23:46 +00:00
return;
}
const mergedItemPools = [...poolToAddTo, ...itemsToAdd];
2023-03-03 15:23:46 +00:00
// Save only unique array values
2023-11-13 11:13:25 -05:00
const uniqueResults = [...new Set([].concat(...mergedItemPools))];
poolToAddTo.splice(0, poolToAddTo.length);
poolToAddTo.push(...uniqueResults);
}
protected addItemsToPool(poolToAddTo: Record<string, number>, poolOfItemsToAdd: Record<string, number>): void
{
for (const tpl in poolOfItemsToAdd)
{
// Skip adding items that already exist
if (poolToAddTo[tpl])
{
continue;
}
poolToAddTo[tpl] = poolOfItemsToAdd[tpl];
}
2023-03-03 15:23:46 +00:00
}
/**
* Ammo/grenades have this property
2023-11-13 11:13:25 -05:00
* @param props
* @returns
2023-03-03 15:23:46 +00:00
*/
protected isBulletOrGrenade(props: Props): boolean
{
return ("ammoType" in props);
}
/**
* Internal and external magazine have this property
2023-11-13 11:13:25 -05:00
* @param props
* @returns
2023-03-03 15:23:46 +00:00
*/
protected isMagazine(props: Props): boolean
{
return ("ReloadMagType" in props);
}
/**
* Medical use items (e.g. morphine/lip balm/grizzly)
2023-11-13 11:13:25 -05:00
* @param props
* @returns
2023-03-03 15:23:46 +00:00
*/
protected isMedicalItem(props: Props): boolean
{
return ("medUseTime" in props);
}
/**
* Grenades have this property (e.g. smoke/frag/flash grenades)
2023-11-13 11:13:25 -05:00
* @param props
* @returns
2023-03-03 15:23:46 +00:00
*/
protected isGrenade(props: Props): boolean
{
return ("ThrowType" in props);
}
protected isFood(tpl: string): boolean
{
return this.itemHelper.isOfBaseclass(tpl, BaseClasses.FOOD);
}
protected isDrink(tpl: string): boolean
{
return this.itemHelper.isOfBaseclass(tpl, BaseClasses.DRINK);
}
protected isCurrency(tpl: string): boolean
{
return this.itemHelper.isOfBaseclass(tpl, BaseClasses.MONEY);
}
2023-03-03 15:23:46 +00:00
/**
* Check if a bot type exists inside the loot cache
* @param botRole role to check for
* @returns true if they exist
*/
protected botRoleExistsInCache(botRole: string): boolean
{
return !!this.lootCache[botRole];
}
/**
* If lootcache is null, init with empty property arrays
* @param botRole Bot role to hydrate
*/
protected initCacheForBotRole(botRole: string): void
{
this.lootCache[botRole] = {
backpackLoot: {},
pocketLoot: {},
vestLoot: {},
secureLoot: {},
combinedPoolLoot: {},
specialItems: {},
grenadeItems: {},
drugItems: {},
foodItems: {},
drinkItems: {},
currencyItems: {},
healingItems: {},
stimItems: {},
2023-03-03 15:23:46 +00:00
};
}
/**
* Compares two item prices by their flea (or handbook if that doesnt exist) price
* -1 when a < b
* 0 when a === b
* 1 when a > b
2023-11-13 11:13:25 -05:00
* @param itemAPrice
* @param itemBPrice
* @returns
2023-03-03 15:23:46 +00:00
*/
protected compareByValue(itemAPrice: number, itemBPrice: number): number
{
// If item A has no price, it should be moved to the back when sorting
if (!itemAPrice)
{
return 1;
}
if (!itemBPrice)
{
return -1;
}
if (itemAPrice < itemBPrice)
{
return -1;
}
if (itemAPrice > itemBPrice)
{
return 1;
}
return 0;
}
2023-11-13 11:13:25 -05:00
}