2023-03-03 16:23:46 +01:00
import { inject , injectable } from "tsyringe" ;
2023-10-19 19:21:17 +02:00
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 16:23:46 +01:00
@injectable ( )
export class BotLootCacheService
{
2023-03-22 15:49:24 +01:00
protected lootCache : Record < string , IBotLootCache > ;
2023-03-03 16:23:46 +01:00
constructor (
@inject ( "WinstonLogger" ) protected logger : ILogger ,
@inject ( "JsonUtil" ) protected jsonUtil : JsonUtil ,
2023-03-17 19:20:16 +01:00
@inject ( "ItemHelper" ) protected itemHelper : ItemHelper ,
2023-03-03 16:23:46 +01:00
@inject ( "DatabaseServer" ) protected databaseServer : DatabaseServer ,
@inject ( "PMCLootGenerator" ) protected pmcLootGenerator : PMCLootGenerator ,
@inject ( "LocalisationService" ) protected localisationService : LocalisationService ,
2023-11-13 17:13:25 +01:00
@inject ( "RagfairPriceService" ) protected ragfairPriceService : RagfairPriceService ,
2023-03-03 16:23:46 +01:00
)
{
this . clearCache ( ) ;
}
/ * *
2023-04-24 12:57:19 +02:00
* Remove cached bot loot data
2023-03-03 16:23:46 +01: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 )
2023-03-17 19:20:16 +01:00
* @param botJsonTemplate Base json db file for the bot having its loot generated
2023-03-03 16:23:46 +01:00
* @returns ITemplateItem array
* /
2023-11-13 17:13:25 +01:00
public getLootFromCache (
botRole : string ,
isPmc : boolean ,
lootType : LootCacheType ,
botJsonTemplate : IBotType ,
) : ITemplateItem [ ]
2023-03-03 16:23:46 +01:00
{
if ( ! this . botRoleExistsInCache ( botRole ) )
{
this . initCacheForBotRole ( botRole ) ;
2023-03-17 19:20:16 +01:00
this . addLootToCache ( botRole , isPmc , botJsonTemplate ) ;
2023-03-03 16:23:46 +01:00
}
switch ( lootType )
{
case LootCacheType . SPECIAL :
return this . lootCache [ botRole ] . specialItems ;
case LootCacheType . BACKPACK :
return this . lootCache [ botRole ] . backpackLoot ;
case LootCacheType . POCKET :
return this . lootCache [ botRole ] . pocketLoot ;
case LootCacheType . VEST :
return this . lootCache [ botRole ] . vestLoot ;
case LootCacheType . COMBINED :
return this . lootCache [ botRole ] . combinedPoolLoot ;
case LootCacheType . HEALING_ITEMS :
return this . lootCache [ botRole ] . healingItems ;
case LootCacheType . GRENADE_ITEMS :
return this . lootCache [ botRole ] . grenadeItems ;
case LootCacheType . DRUG_ITEMS :
return this . lootCache [ botRole ] . drugItems ;
case LootCacheType . STIM_ITEMS :
return this . lootCache [ botRole ] . stimItems ;
default :
2023-11-13 17:13:25 +01:00
this . logger . error (
this . localisationService . getText ( "bot-loot_type_not_found" , {
lootType : lootType ,
botRole : botRole ,
isPmc : isPmc ,
} ) ,
) ;
2023-03-03 16:23:46 +01:00
break ;
}
}
/ * *
* 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 )
2023-03-17 19:20:16 +01:00
* @param botJsonTemplate db template for bot having its loot generated
2023-03-03 16:23:46 +01:00
* /
2023-03-17 19:20:16 +01:00
protected addLootToCache ( botRole : string , isPmc : boolean , botJsonTemplate : IBotType ) : void
2023-03-03 16:23:46 +01:00
{
2023-03-17 19:20:16 +01:00
// the full pool of loot we use to create the various sub-categories with
const lootPool = botJsonTemplate . inventory . items ;
2023-03-03 16:23:46 +01:00
// Flatten all individual slot loot pools into one big pool, while filtering out potentially missing templates
const specialLootTemplates : ITemplateItem [ ] = [ ] ;
const backpackLootTemplates : ITemplateItem [ ] = [ ] ;
const pocketLootTemplates : ITemplateItem [ ] = [ ] ;
const vestLootTemplates : ITemplateItem [ ] = [ ] ;
const combinedPoolTemplates : ITemplateItem [ ] = [ ] ;
if ( isPmc )
{
// Replace lootPool passed in with our own generated list if bot is a pmc
lootPool . Backpack = this . jsonUtil . clone ( this . pmcLootGenerator . generatePMCBackpackLootPool ( ) ) ;
lootPool . Pockets = this . jsonUtil . clone ( this . pmcLootGenerator . generatePMCPocketLootPool ( ) ) ;
lootPool . TacticalVest = this . jsonUtil . clone ( this . pmcLootGenerator . generatePMCVestLootPool ( ) ) ;
}
for ( const [ slot , pool ] of Object . entries ( lootPool ) )
{
2023-03-17 19:20:16 +01:00
// No items to add, skip
2023-03-03 16:23:46 +01:00
if ( ! pool ? . length )
{
continue ;
}
2023-03-17 19:20:16 +01:00
// Sort loot pool into separate buckets
2023-03-03 16:23:46 +01:00
let itemsToAdd : ITemplateItem [ ] = [ ] ;
const items = this . databaseServer . getTables ( ) . templates . items ;
switch ( slot . toLowerCase ( ) )
{
case "specialloot" :
2023-03-17 19:20:16 +01:00
itemsToAdd = pool . map ( ( lootTpl : string ) = > items [ lootTpl ] ) ;
2023-03-03 16:23:46 +01:00
this . addUniqueItemsToPool ( specialLootTemplates , itemsToAdd ) ;
break ;
case "pockets" :
2023-03-17 19:20:16 +01:00
itemsToAdd = pool . map ( ( lootTpl : string ) = > items [ lootTpl ] ) ;
2023-03-03 16:23:46 +01:00
this . addUniqueItemsToPool ( pocketLootTemplates , itemsToAdd ) ;
break ;
case "tacticalvest" :
2023-03-17 19:20:16 +01:00
itemsToAdd = pool . map ( ( lootTpl : string ) = > items [ lootTpl ] ) ;
2023-03-03 16:23:46 +01:00
this . addUniqueItemsToPool ( vestLootTemplates , itemsToAdd ) ;
break ;
case "securedcontainer" :
// Don't add these items to loot pool
break ;
default :
2023-03-17 19:20:16 +01:00
itemsToAdd = pool . map ( ( lootTpl : string ) = > items [ lootTpl ] ) ;
2023-03-03 16:23:46 +01:00
this . addUniqueItemsToPool ( backpackLootTemplates , itemsToAdd ) ;
}
2023-11-13 17:13:25 +01:00
2023-03-03 16:23:46 +01:00
// Add items to combined pool if any exist
if ( Object . keys ( itemsToAdd ) . length > 0 )
{
this . addUniqueItemsToPool ( combinedPoolTemplates , itemsToAdd ) ;
}
}
// Sort all items by their worth
this . sortPoolByRagfairPrice ( specialLootTemplates ) ;
this . sortPoolByRagfairPrice ( backpackLootTemplates ) ;
this . sortPoolByRagfairPrice ( pocketLootTemplates ) ;
this . sortPoolByRagfairPrice ( vestLootTemplates ) ;
this . sortPoolByRagfairPrice ( combinedPoolTemplates ) ;
2023-03-17 19:20:16 +01:00
// use whitelist if array has values, otherwise process above sorted pools
2023-11-13 18:29:16 +01:00
const specialLootItems = ( botJsonTemplate . generation . items . specialItems . whitelist ? . length > 0 )
? botJsonTemplate . generation . items . specialItems . whitelist . map ( ( x ) = > this . itemHelper . getItem ( x ) [ 1 ] )
: specialLootTemplates . filter ( ( template ) = >
2023-11-13 18:31:52 +01:00
! ( this . isBulletOrGrenade ( template . _props ) || this . isMagazine ( template . _props ) )
2023-11-13 17:13:25 +01:00
) ;
2023-11-13 18:29:16 +01:00
const healingItems = ( botJsonTemplate . generation . items . healing . whitelist ? . length > 0 )
? botJsonTemplate . generation . items . healing . whitelist . map ( ( x ) = > this . itemHelper . getItem ( x ) [ 1 ] )
: combinedPoolTemplates . filter ( ( template ) = >
this . isMedicalItem ( template . _props )
&& template . _parent !== BaseClasses . STIMULATOR
&& template . _parent !== BaseClasses . DRUGS
2023-11-13 17:13:25 +01:00
) ;
2023-11-13 18:29:16 +01:00
const drugItems = ( botJsonTemplate . generation . items . drugs . whitelist ? . length > 0 )
? botJsonTemplate . generation . items . drugs . whitelist . map ( ( x ) = > this . itemHelper . getItem ( x ) [ 1 ] )
: combinedPoolTemplates . filter ( ( template ) = >
2023-11-13 18:31:52 +01:00
this . isMedicalItem ( template . _props ) && template . _parent === BaseClasses . DRUGS
2023-11-13 17:13:25 +01:00
) ;
2023-11-13 18:29:16 +01:00
const stimItems = ( botJsonTemplate . generation . items . stims . whitelist ? . length > 0 )
? botJsonTemplate . generation . items . stims . whitelist . map ( ( x ) = > this . itemHelper . getItem ( x ) [ 1 ] )
: combinedPoolTemplates . filter ( ( template ) = >
2023-11-13 18:31:52 +01:00
this . isMedicalItem ( template . _props ) && template . _parent === BaseClasses . STIMULATOR
2023-11-13 17:13:25 +01:00
) ;
2023-11-13 18:29:16 +01:00
const grenadeItems = ( botJsonTemplate . generation . items . grenades . whitelist ? . length > 0 )
? botJsonTemplate . generation . items . grenades . whitelist . map ( ( x ) = > this . itemHelper . getItem ( x ) [ 1 ] )
: combinedPoolTemplates . filter ( ( template ) = > this . isGrenade ( template . _props ) ) ;
2023-03-03 16:23:46 +01:00
// Get loot items (excluding magazines, bullets, grenades and healing items)
2023-11-13 17:13:25 +01:00
const backpackLootItems = backpackLootTemplates . filter ( ( template ) = >
2023-11-01 00:57:28 +01:00
// biome-ignore lint/complexity/useSimplifiedLogicExpression: <explanation>
2023-11-13 18:31:52 +01:00
! this . isBulletOrGrenade ( template . _props ) && ! this . isMagazine ( template . _props ) // && !this.isMedicalItem(template._props) // Disabled for now as followSanitar has a lot of med items as loot
2023-11-13 18:29:16 +01:00
&& ! this . isGrenade ( template . _props )
2023-11-13 17:13:25 +01:00
) ;
2023-03-03 16:23:46 +01:00
// Get pocket loot
2023-11-13 17:13:25 +01:00
const pocketLootItems = pocketLootTemplates . filter ( ( template ) = >
2023-11-01 00:57:28 +01:00
// biome-ignore lint/complexity/useSimplifiedLogicExpression: <explanation>
2023-11-13 18:29:16 +01:00
! this . isBulletOrGrenade ( template . _props )
&& ! this . isMagazine ( template . _props )
&& ! this . isMedicalItem ( template . _props )
&& ! this . isGrenade ( template . _props )
&& ( "Height" in template . _props )
&& ( "Width" in template . _props )
2023-11-13 17:13:25 +01:00
) ;
2023-03-03 16:23:46 +01:00
// Get vest loot items
2023-11-13 17:13:25 +01:00
const vestLootItems = vestLootTemplates . filter ( ( template ) = >
2023-11-01 00:57:28 +01:00
// biome-ignore lint/complexity/useSimplifiedLogicExpression: <explanation>
2023-11-13 18:29:16 +01:00
! this . isBulletOrGrenade ( template . _props )
&& ! this . isMagazine ( template . _props )
&& ! this . isMedicalItem ( template . _props )
&& ! this . isGrenade ( template . _props )
2023-11-13 17:13:25 +01:00
) ;
2023-03-03 16:23:46 +01:00
this . lootCache [ botRole ] . healingItems = healingItems ;
this . lootCache [ botRole ] . drugItems = drugItems ;
this . lootCache [ botRole ] . stimItems = stimItems ;
this . lootCache [ botRole ] . grenadeItems = grenadeItems ;
this . lootCache [ botRole ] . specialItems = specialLootItems ;
this . lootCache [ botRole ] . backpackLoot = backpackLootItems ;
this . lootCache [ botRole ] . pocketLoot = pocketLootItems ;
this . lootCache [ botRole ] . vestLoot = vestLootItems ;
}
/ * *
* Sort a pool of item objects by its flea price
* @param poolToSort pool of items to sort
* /
protected sortPoolByRagfairPrice ( poolToSort : ITemplateItem [ ] ) : void
{
2023-11-13 17:13:25 +01:00
poolToSort . sort ( ( a , b ) = >
this . compareByValue (
this . ragfairPriceService . getFleaPriceForItem ( a . _id ) ,
this . ragfairPriceService . getFleaPriceForItem ( b . _id ) ,
)
) ;
2023-03-03 16:23:46 +01:00
}
/ * *
* Add unique items into combined pool
* @param combinedItemPool Pool of items to add to
* @param itemsToAdd items to add to combined pool if unique
* /
protected addUniqueItemsToPool ( combinedItemPool : ITemplateItem [ ] , itemsToAdd : ITemplateItem [ ] ) : void
{
if ( combinedItemPool . length === 0 )
{
combinedItemPool . push ( . . . itemsToAdd ) ;
return ;
}
const mergedItemPools = [ . . . combinedItemPool , . . . itemsToAdd ] ;
// Save only unique array values
2023-11-13 17:13:25 +01:00
const uniqueResults = [ . . . new Set ( [ ] . concat ( . . . mergedItemPools ) ) ] ;
2023-03-03 16:23:46 +01:00
combinedItemPool . splice ( 0 , combinedItemPool . length ) ;
combinedItemPool . push ( . . . uniqueResults ) ;
}
/ * *
* Ammo / grenades have this property
2023-11-13 17:13:25 +01:00
* @param props
* @returns
2023-03-03 16:23:46 +01:00
* /
protected isBulletOrGrenade ( props : Props ) : boolean
{
return ( "ammoType" in props ) ;
}
/ * *
* Internal and external magazine have this property
2023-11-13 17:13:25 +01:00
* @param props
* @returns
2023-03-03 16:23:46 +01:00
* /
protected isMagazine ( props : Props ) : boolean
{
return ( "ReloadMagType" in props ) ;
}
/ * *
* Medical use items ( e . g . morphine / lip balm / grizzly )
2023-11-13 17:13:25 +01:00
* @param props
* @returns
2023-03-03 16:23:46 +01:00
* /
protected isMedicalItem ( props : Props ) : boolean
{
return ( "medUseTime" in props ) ;
}
/ * *
* Grenades have this property ( e . g . smoke / frag / flash grenades )
2023-11-13 17:13:25 +01:00
* @param props
* @returns
2023-03-03 16:23:46 +01:00
* /
protected isGrenade ( props : Props ) : boolean
{
return ( "ThrowType" in props ) ;
}
/ * *
* 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 : [ ] ,
combinedPoolLoot : [ ] ,
specialItems : [ ] ,
grenadeItems : [ ] ,
drugItems : [ ] ,
healingItems : [ ] ,
2023-11-13 17:13:25 +01:00
stimItems : [ ] ,
2023-03-03 16:23:46 +01: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 17:13:25 +01:00
* @param itemAPrice
* @param itemBPrice
* @returns
2023-03-03 16:23:46 +01: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 17:13:25 +01:00
}