Formatting for generator classes.

This commit is contained in:
Refringe 2023-11-13 11:05:05 -05:00
parent 320c8b7d48
commit d3e5418fc8
No known key found for this signature in database
GPG Key ID: 64E03E5F892C6F9E
22 changed files with 2049 additions and 884 deletions

View File

@ -46,7 +46,7 @@ export class BotEquipmentModGenerator
@inject("BotWeaponGeneratorHelper") protected botWeaponGeneratorHelper: BotWeaponGeneratorHelper, @inject("BotWeaponGeneratorHelper") protected botWeaponGeneratorHelper: BotWeaponGeneratorHelper,
@inject("LocalisationService") protected localisationService: LocalisationService, @inject("LocalisationService") protected localisationService: LocalisationService,
@inject("BotEquipmentModPoolService") protected botEquipmentModPoolService: BotEquipmentModPoolService, @inject("BotEquipmentModPoolService") protected botEquipmentModPoolService: BotEquipmentModPoolService,
@inject("ConfigServer") protected configServer: ConfigServer @inject("ConfigServer") protected configServer: ConfigServer,
) )
{ {
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT); this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
@ -63,7 +63,15 @@ export class BotEquipmentModGenerator
* @param forceSpawn should this mod be forced to spawn * @param forceSpawn should this mod be forced to spawn
* @returns Item + compatible mods as an array * @returns Item + compatible mods as an array
*/ */
public generateModsForEquipment(equipment: Item[], modPool: Mods, parentId: string, parentTemplate: ITemplateItem, modSpawnChances: ModsChances, botRole: string, forceSpawn = false): Item[] public generateModsForEquipment(
equipment: Item[],
modPool: Mods,
parentId: string,
parentTemplate: ITemplateItem,
modSpawnChances: ModsChances,
botRole: string,
forceSpawn = false,
): Item[]
{ {
const compatibleModsPool = modPool[parentTemplate._id]; const compatibleModsPool = modPool[parentTemplate._id];
@ -73,7 +81,13 @@ export class BotEquipmentModGenerator
const itemSlot = this.getModItemSlot(modSlot, parentTemplate); const itemSlot = this.getModItemSlot(modSlot, parentTemplate);
if (!itemSlot) if (!itemSlot)
{ {
this.logger.error(this.localisationService.getText("bot-mod_slot_missing_from_item", {modSlot: modSlot, parentId: parentTemplate._id, parentName: parentTemplate._name})); this.logger.error(
this.localisationService.getText("bot-mod_slot_missing_from_item", {
modSlot: modSlot,
parentId: parentTemplate._id,
parentName: parentTemplate._name,
}),
);
continue; continue;
} }
@ -83,27 +97,33 @@ export class BotEquipmentModGenerator
} }
// Ensure submods for nvgs all spawn together // Ensure submods for nvgs all spawn together
forceSpawn = (modSlot === "mod_nvg") forceSpawn = (modSlot === "mod_nvg") ?
? true true :
: false; false;
let modTpl: string; let modTpl: string;
let found = false; let found = false;
// Find random mod and check its compatible // Find random mod and check its compatible
const exhaustableModPool = new ExhaustableArray(compatibleModsPool[modSlot], this.randomUtil, this.jsonUtil); const exhaustableModPool = new ExhaustableArray(
compatibleModsPool[modSlot],
this.randomUtil,
this.jsonUtil,
);
while (exhaustableModPool.hasValues()) while (exhaustableModPool.hasValues())
{ {
modTpl = exhaustableModPool.getRandomValue(); modTpl = exhaustableModPool.getRandomValue();
if (!this.botGeneratorHelper.isItemIncompatibleWithCurrentItems(equipment, modTpl, modSlot).incompatible) if (
!this.botGeneratorHelper.isItemIncompatibleWithCurrentItems(equipment, modTpl, modSlot).incompatible
)
{ {
found = true; found = true;
break; break;
} }
} }
// Combatible item not found but slot REQUIRES item, get random item from db // Compatible item not found but slot REQUIRES item, get random item from db
const parentSlot = parentTemplate._props.Slots.find(i => i._name === modSlot); const parentSlot = parentTemplate._props.Slots.find((i) => i._name === modSlot);
if (!found && parentSlot !== undefined && parentSlot._required) if (!found && parentSlot !== undefined && parentSlot._required)
{ {
modTpl = this.getModTplFromItemDb(modTpl, parentSlot, modSlot, equipment); modTpl = this.getModTplFromItemDb(modTpl, parentSlot, modSlot, equipment);
@ -113,7 +133,7 @@ export class BotEquipmentModGenerator
// Compatible item not found + not required // Compatible item not found + not required
if (!found && parentSlot !== undefined && !parentSlot._required) if (!found && parentSlot !== undefined && !parentSlot._required)
{ {
// Dont add item // Don't add item
continue; continue;
} }
@ -128,8 +148,16 @@ export class BotEquipmentModGenerator
if (Object.keys(modPool).includes(modTpl)) if (Object.keys(modPool).includes(modTpl))
{ {
// Call self recursivly // Call self recursively
this.generateModsForEquipment(equipment, modPool, modId, modTemplate[1], modSpawnChances, botRole, forceSpawn); this.generateModsForEquipment(
equipment,
modPool,
modId,
modTemplate[1],
modSpawnChances,
botRole,
forceSpawn,
);
} }
} }
@ -146,8 +174,8 @@ export class BotEquipmentModGenerator
* @param modSpawnChances Mod spawn chances * @param modSpawnChances Mod spawn chances
* @param ammoTpl Ammo tpl to use when generating magazines/cartridges * @param ammoTpl Ammo tpl to use when generating magazines/cartridges
* @param botRole Role of bot weapon is generated for * @param botRole Role of bot weapon is generated for
* @param botLevel lvel of the bot weapon is being generated for * @param botLevel Level of the bot weapon is being generated for
* @param modLimits limits placed on certian mod types per gun * @param modLimits limits placed on certain mod types per gun
* @param botEquipmentRole role of bot when accessing bot.json equipment config settings * @param botEquipmentRole role of bot when accessing bot.json equipment config settings
* @returns Weapon + mods array * @returns Weapon + mods array
*/ */
@ -162,7 +190,8 @@ export class BotEquipmentModGenerator
botRole: string, botRole: string,
botLevel: number, botLevel: number,
modLimits: BotModLimits, modLimits: BotModLimits,
botEquipmentRole: string): Item[] botEquipmentRole: string,
): Item[]
{ {
const pmcProfile = this.profileHelper.getPmcProfile(sessionId); const pmcProfile = this.profileHelper.getPmcProfile(sessionId);
@ -171,17 +200,27 @@ export class BotEquipmentModGenerator
// Null guard against bad input weapon // Null guard against bad input weapon
// biome-ignore lint/complexity/useSimplifiedLogicExpression: <explanation> // biome-ignore lint/complexity/useSimplifiedLogicExpression: <explanation>
if (!parentTemplate._props.Slots.length if (
&& !parentTemplate._props.Cartridges?.length !parentTemplate._props.Slots.length &&
&& !parentTemplate._props.Chambers?.length) !parentTemplate._props.Cartridges?.length &&
!parentTemplate._props.Chambers?.length
)
{ {
this.logger.error(this.localisationService.getText("bot-unable_to_add_mods_to_weapon_missing_ammo_slot", {weaponName: parentTemplate._name, weaponId: parentTemplate._id})); this.logger.error(
this.localisationService.getText("bot-unable_to_add_mods_to_weapon_missing_ammo_slot", {
weaponName: parentTemplate._name,
weaponId: parentTemplate._id,
}),
);
return weapon; return weapon;
} }
const botEquipConfig = this.botConfig.equipment[botEquipmentRole]; const botEquipConfig = this.botConfig.equipment[botEquipmentRole];
const botEquipBlacklist = this.botEquipmentFilterService.getBotEquipmentBlacklist(botEquipmentRole, pmcProfile.Info.Level); const botEquipBlacklist = this.botEquipmentFilterService.getBotEquipmentBlacklist(
botEquipmentRole,
pmcProfile.Info.Level,
);
const botWeaponSightWhitelist = this.botEquipmentFilterService.getBotWeaponSightWhitelist(botEquipmentRole); const botWeaponSightWhitelist = this.botEquipmentFilterService.getBotWeaponSightWhitelist(botEquipmentRole);
const randomisationSettings = this.botHelper.getBotRandomizationDetails(botLevel, botEquipConfig); const randomisationSettings = this.botHelper.getBotRandomizationDetails(botLevel, botEquipConfig);
@ -193,7 +232,14 @@ export class BotEquipmentModGenerator
const modsParentSlot = this.getModItemSlot(modSlot, parentTemplate); const modsParentSlot = this.getModItemSlot(modSlot, parentTemplate);
if (!modsParentSlot) if (!modsParentSlot)
{ {
this.logger.error(this.localisationService.getText("bot-weapon_missing_mod_slot", {modSlot: modSlot, weaponId: parentTemplate._id, weaponName: parentTemplate._name, botRole: botRole})); this.logger.error(
this.localisationService.getText("bot-weapon_missing_mod_slot", {
modSlot: modSlot,
weaponId: parentTemplate._id,
weaponName: parentTemplate._name,
botRole: botRole,
}),
);
continue; continue;
} }
@ -205,10 +251,19 @@ export class BotEquipmentModGenerator
} }
const isRandomisableSlot = randomisationSettings?.randomisedWeaponModSlots?.includes(modSlot) ?? false; const isRandomisableSlot = randomisationSettings?.randomisedWeaponModSlots?.includes(modSlot) ?? false;
const modToAdd = this.chooseModToPutIntoSlot(modSlot, isRandomisableSlot, botWeaponSightWhitelist, botEquipBlacklist, compatibleModsPool, weapon, ammoTpl, parentTemplate); const modToAdd = this.chooseModToPutIntoSlot(
modSlot,
isRandomisableSlot,
botWeaponSightWhitelist,
botEquipBlacklist,
compatibleModsPool,
weapon,
ammoTpl,
parentTemplate,
);
// Compatible mod not found // Compatible mod not found
if (!modToAdd || typeof (modToAdd) === "undefined") if (!modToAdd || typeof modToAdd === "undefined")
{ {
continue; continue;
} }
@ -220,7 +275,15 @@ export class BotEquipmentModGenerator
const modToAddTemplate = modToAdd[1]; const modToAddTemplate = modToAdd[1];
// Skip adding mod to weapon if type limit reached // Skip adding mod to weapon if type limit reached
if (this.botWeaponModLimitService.weaponModHasReachedLimit(botEquipmentRole, modToAddTemplate, modLimits, parentTemplate, weapon)) if (
this.botWeaponModLimitService.weaponModHasReachedLimit(
botEquipmentRole,
modToAddTemplate,
modLimits,
parentTemplate,
weapon,
)
)
{ {
continue; continue;
} }
@ -234,7 +297,7 @@ export class BotEquipmentModGenerator
"mod_scope_000", "mod_scope_000",
"mod_scope_001", "mod_scope_001",
"mod_scope_002", "mod_scope_002",
"mod_scope_003" "mod_scope_003",
]; ];
this.adjustSlotSpawnChances(modSpawnChances, scopeSlots, 100); this.adjustSlotSpawnChances(modSpawnChances, scopeSlots, 100);
@ -252,7 +315,7 @@ export class BotEquipmentModGenerator
const muzzleSlots = [ const muzzleSlots = [
"mod_muzzle", "mod_muzzle",
"mod_muzzle_000", "mod_muzzle_000",
"mod_muzzle_001" "mod_muzzle_001",
]; ];
// Make chance of muzzle devices 95%, nearly certain but not guaranteed // Make chance of muzzle devices 95%, nearly certain but not guaranteed
this.adjustSlotSpawnChances(modSpawnChances, muzzleSlots, 95); this.adjustSlotSpawnChances(modSpawnChances, muzzleSlots, 95);
@ -267,7 +330,10 @@ export class BotEquipmentModGenerator
// Handguard mod can take a sub handguard mod + weapon has no UBGL (takes same slot) // 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 // Force spawn chance to be 100% to ensure it gets added
if (modSlot === "mod_handguard" && modToAddTemplate._props.Slots.find(x => x._name === "mod_handguard") && !weapon.find(x => x.slotId === "mod_launcher")) if (
modSlot === "mod_handguard" && modToAddTemplate._props.Slots.find((x) => x._name === "mod_handguard") &&
!weapon.find((x) => x.slotId === "mod_launcher")
)
{ {
// Needed for handguards with lower // Needed for handguards with lower
modSpawnChances.mod_handguard = 100; modSpawnChances.mod_handguard = 100;
@ -275,7 +341,10 @@ export class BotEquipmentModGenerator
// If stock mod can take a sub stock mod, force spawn chance to be 100% to ensure sub-stock gets added // If stock mod can take a sub stock mod, force spawn chance to be 100% to ensure sub-stock gets added
// Or if mod_stock is configured to be forced on // Or if mod_stock is configured to be forced on
if (modSlot === "mod_stock" && (modToAddTemplate._props.Slots.find(x => x._name.includes("mod_stock") || botEquipConfig.forceStock))) if (
modSlot === "mod_stock" &&
(modToAddTemplate._props.Slots.find((x) => x._name.includes("mod_stock") || botEquipConfig.forceStock))
)
{ {
// Stock mod can take additional stocks, could be a locking device, force 100% chance // Stock mod can take additional stocks, could be a locking device, force 100% chance
const stockSlots = ["mod_stock", "mod_stock_000", "mod_stock_akms"]; const stockSlots = ["mod_stock", "mod_stock_000", "mod_stock_akms"];
@ -283,10 +352,12 @@ export class BotEquipmentModGenerator
} }
const modId = this.hashUtil.generate(); const modId = this.hashUtil.generate();
weapon.push(this.createModItem(modId, modToAddTemplate._id, weaponParentId, modSlot, modToAddTemplate, botRole)); weapon.push(
this.createModItem(modId, modToAddTemplate._id, weaponParentId, modSlot, modToAddTemplate, botRole),
);
// I first thought we could use the recursive generateModsForItems as previously for cylinder magazines. // I first thought we could use the recursive generateModsForItems as previously for cylinder magazines.
// However, the recursion doesnt go over the slots of the parent mod but over the modPool which is given by the bot config // However, the recursion doesn't go over the slots of the parent mod but over the modPool which is given by the bot config
// where we decided to keep cartridges instead of camoras. And since a CylinderMagazine only has one cartridge entry and // 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 // this entry is not to be filled, we need a special handling for the CylinderMagazine
const modParentItem = this.databaseServer.getTables().templates.items[modToAddTemplate._parent]; const modParentItem = this.databaseServer.getTables().templates.items[modToAddTemplate._parent];
@ -312,8 +383,20 @@ export class BotEquipmentModGenerator
} }
if (containsModInPool) if (containsModInPool)
{ {
// Call self recursivly to add mods to this mod // Call self recursively to add mods to this mod
this.generateModsForWeapon(sessionId, weapon, modPool, modId, modToAddTemplate, modSpawnChances, ammoTpl, botRole, botLevel, modLimits, botEquipmentRole); this.generateModsForWeapon(
sessionId,
weapon,
modPool,
modId,
modToAddTemplate,
modSpawnChances,
ammoTpl,
botRole,
botLevel,
modLimits,
botEquipmentRole,
);
} }
} }
} }
@ -328,8 +411,8 @@ export class BotEquipmentModGenerator
*/ */
protected modIsFrontOrRearSight(modSlot: string, tpl: string): boolean protected modIsFrontOrRearSight(modSlot: string, tpl: string): boolean
{ {
if (modSlot === "mod_gas_block" && tpl === "5ae30e795acfc408fb139a0b") // M4A1 front sight with gas block if (modSlot === "mod_gas_block" && tpl === "5ae30e795acfc408fb139a0b")
{ { // M4A1 front sight with gas block
return true; return true;
} }
@ -344,15 +427,27 @@ export class BotEquipmentModGenerator
*/ */
protected modSlotCanHoldScope(modSlot: string, modsParentId: string): boolean 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()) return [
&& modsParentId === BaseClasses.MOUNT; "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;
} }
/** /**
* Set mod spawn chances to defined amount * Set mod spawn chances to defined amount
* @param modSpawnChances Chance dictionary to update * @param modSpawnChances Chance dictionary to update
*/ */
protected adjustSlotSpawnChances(modSpawnChances: ModsChances, modSlotsToAdjust: string[], newChancePercent: number): void protected adjustSlotSpawnChances(
modSpawnChances: ModsChances,
modSlotsToAdjust: string[],
newChancePercent: number,
): void
{ {
if (!modSpawnChances) if (!modSpawnChances)
{ {
@ -467,11 +562,11 @@ export class BotEquipmentModGenerator
case "patron_in_weapon": case "patron_in_weapon":
case "patron_in_weapon_000": case "patron_in_weapon_000":
case "patron_in_weapon_001": case "patron_in_weapon_001":
return parentTemplate._props.Chambers.find(c => c._name.includes(modSlot)); return parentTemplate._props.Chambers.find((c) => c._name.includes(modSlot));
case "cartridges": case "cartridges":
return parentTemplate._props.Cartridges.find(c => c._name === modSlot); return parentTemplate._props.Cartridges.find((c) => c._name === modSlot);
default: default:
return parentTemplate._props.Slots.find(s => s._name === modSlot); return parentTemplate._props.Slots.find((s) => s._name === modSlot);
} }
} }
@ -486,8 +581,9 @@ export class BotEquipmentModGenerator
protected shouldModBeSpawned(itemSlot: Slot, modSlot: string, modSpawnChances: ModsChances): boolean protected shouldModBeSpawned(itemSlot: Slot, modSlot: string, modSpawnChances: ModsChances): boolean
{ {
const modSpawnChance = itemSlot._required || this.getAmmoContainers().includes(modSlot) // Required OR it is ammo const modSpawnChance = itemSlot._required || this.getAmmoContainers().includes(modSlot) // Required OR it is ammo
? 100 ?
: modSpawnChances[modSlot]; 100 :
modSpawnChances[modSlot];
if (modSpawnChance === 100) if (modSpawnChance === 100)
{ {
@ -498,7 +594,6 @@ export class BotEquipmentModGenerator
} }
/** /**
*
* @param modSlot Slot mod will fit into * @param modSlot Slot mod will fit into
* @param isRandomisableSlot Will generate a randomised mod pool if true * @param isRandomisableSlot Will generate a randomised mod pool if true
* @param modsParent Parent slot the item will be a part of * @param modsParent Parent slot the item will be a part of
@ -517,11 +612,12 @@ export class BotEquipmentModGenerator
itemModPool: Record<string, string[]>, itemModPool: Record<string, string[]>,
weapon: Item[], weapon: Item[],
ammoTpl: string, ammoTpl: string,
parentTemplate: ITemplateItem): [boolean, ITemplateItem] parentTemplate: ITemplateItem,
): [boolean, ITemplateItem]
{ {
let modTpl: string; let modTpl: string;
let found = false; let found = false;
const parentSlot = parentTemplate._props.Slots.find(i => i._name === modSlot); const parentSlot = parentTemplate._props.Slots.find((i) => i._name === modSlot);
// It's ammo, use predefined ammo parameter // It's ammo, use predefined ammo parameter
if (this.getAmmoContainers().includes(modSlot) && modSlot !== "mod_magazine") if (this.getAmmoContainers().includes(modSlot) && modSlot !== "mod_magazine")
@ -539,7 +635,9 @@ export class BotEquipmentModGenerator
// Ensure there's a pool of mods to pick from // Ensure there's a pool of mods to pick from
if (!(itemModPool[modSlot] || parentSlot._required)) if (!(itemModPool[modSlot] || parentSlot._required))
{ {
this.logger.debug(`Mod pool for slot: ${modSlot} on item: ${parentTemplate._name} was empty, skipping mod`); this.logger.debug(
`Mod pool for slot: ${modSlot} on item: ${parentTemplate._name} was empty, skipping mod`,
);
return null; return null;
} }
@ -549,17 +647,25 @@ export class BotEquipmentModGenerator
// scope pool has more than one scope // scope pool has more than one scope
if (itemModPool[modSlot].length > 1) if (itemModPool[modSlot].length > 1)
{ {
itemModPool[modSlot] = this.filterSightsByWeaponType(weapon[0], itemModPool[modSlot], botWeaponSightWhitelist); itemModPool[modSlot] = this.filterSightsByWeaponType(
weapon[0],
itemModPool[modSlot],
botWeaponSightWhitelist,
);
} }
} }
// Pick random mod and check it's compatible // Pick random mod and check it's compatible
const exhaustableModPool = new ExhaustableArray(itemModPool[modSlot], this.randomUtil, this.jsonUtil); const exhaustableModPool = new ExhaustableArray(itemModPool[modSlot], this.randomUtil, this.jsonUtil);
let modCompatibilityResult: {incompatible: boolean, reason: string} = {incompatible: false, reason: ""}; let modCompatibilityResult: {incompatible: boolean; reason: string;} = {incompatible: false, reason: ""};
while (exhaustableModPool.hasValues()) while (exhaustableModPool.hasValues())
{ {
modTpl = exhaustableModPool.getRandomValue(); modTpl = exhaustableModPool.getRandomValue();
modCompatibilityResult = this.botGeneratorHelper.isItemIncompatibleWithCurrentItems(weapon, modTpl, modSlot); modCompatibilityResult = this.botGeneratorHelper.isItemIncompatibleWithCurrentItems(
weapon,
modTpl,
modSlot,
);
if (!modCompatibilityResult.incompatible) if (!modCompatibilityResult.incompatible)
{ {
found = true; found = true;
@ -592,7 +698,11 @@ export class BotEquipmentModGenerator
{ {
if (parentSlot._required) if (parentSlot._required)
{ {
this.logger.warning(`Required slot unable to be filled, ${modSlot} on ${parentTemplate._name} ${parentTemplate._id} for weapon: ${weapon[0]._tpl}`); this.logger.warning(
`Required slot unable to be filled, ${modSlot} on ${parentTemplate._name} ${parentTemplate._id} for weapon: ${
weapon[0]._tpl
}`,
);
} }
return null; return null;
@ -607,21 +717,27 @@ export class BotEquipmentModGenerator
* @param modTpl _tpl * @param modTpl _tpl
* @param parentId parentId * @param parentId parentId
* @param modSlot slotId * @param modSlot slotId
* @param modTemplate Used to add additional properites in the upd object * @param modTemplate Used to add additional properties in the upd object
* @returns Item object * @returns Item object
*/ */
protected createModItem(modId: string, modTpl: string, parentId: string, modSlot: string, modTemplate: ITemplateItem, botRole: string): Item protected createModItem(
modId: string,
modTpl: string,
parentId: string,
modSlot: string,
modTemplate: ITemplateItem,
botRole: string,
): Item
{ {
return { return {
_id: modId, _id: modId,
_tpl: modTpl, _tpl: modTpl,
parentId: parentId, parentId: parentId,
slotId: modSlot, slotId: modSlot,
...this.botGeneratorHelper.generateExtraPropertiesForItem(modTemplate, botRole) ...this.botGeneratorHelper.generateExtraPropertiesForItem(modTemplate, botRole),
}; };
} }
/** /**
* Get a list of containers that hold ammo * Get a list of containers that hold ammo
* e.g. mod_magazine / patron_in_weapon_000 * e.g. mod_magazine / patron_in_weapon_000
@ -635,14 +751,14 @@ export class BotEquipmentModGenerator
/** /**
* Get a random mod from an items compatible mods Filter array * Get a random mod from an items compatible mods Filter array
* @param modTpl ???? default value to return if nothing found * @param modTpl ???? default value to return if nothing found
* @param parentSlot item mod will go into, used to get combatible items * @param parentSlot item mod will go into, used to get compatible items
* @param modSlot Slot to get mod to fill * @param modSlot Slot to get mod to fill
* @param items items to ensure picked mod is compatible with * @param items items to ensure picked mod is compatible with
* @returns item tpl * @returns item tpl
*/ */
protected getModTplFromItemDb(modTpl: string, parentSlot: Slot, modSlot: string, items: Item[]): string protected getModTplFromItemDb(modTpl: string, parentSlot: Slot, modSlot: string, items: Item[]): string
{ {
// Find combatible mods and make an array of them // Find compatible mods and make an array of them
const allowedItems = parentSlot._props.filters[0].Filter; const allowedItems = parentSlot._props.filters[0].Filter;
// Find mod item that fits slot from sorted mod array // Find mod item that fits slot from sorted mod array
@ -669,12 +785,22 @@ export class BotEquipmentModGenerator
* @param parentTemplate template of the mods parent item * @param parentTemplate template of the mods parent item
* @returns true if valid * @returns true if valid
*/ */
protected isModValidForSlot(modToAdd: [boolean, ITemplateItem], itemSlot: Slot, modSlot: string, parentTemplate: ITemplateItem): boolean protected isModValidForSlot(
modToAdd: [boolean, ITemplateItem],
itemSlot: Slot,
modSlot: string,
parentTemplate: ITemplateItem,
): boolean
{ {
// Mod lacks template item // Mod lacks template item
if (!modToAdd[1]) if (!modToAdd[1])
{ {
this.logger.error(this.localisationService.getText("bot-no_item_template_found_when_adding_mod", {modId: modToAdd[1]._id, modSlot: modSlot})); this.logger.error(
this.localisationService.getText("bot-no_item_template_found_when_adding_mod", {
modId: modToAdd[1]._id,
modSlot: modSlot,
}),
);
this.logger.debug(`Item -> ${parentTemplate._id}; Slot -> ${modSlot}`); this.logger.debug(`Item -> ${parentTemplate._id}; Slot -> ${modSlot}`);
return false; return false;
@ -686,16 +812,31 @@ export class BotEquipmentModGenerator
// Slot must be filled, show warning // Slot must be filled, show warning
if (itemSlot._required) if (itemSlot._required)
{ {
this.logger.warning(this.localisationService.getText("bot-unable_to_add_mod_item_invalid", {itemName: modToAdd[1]._name, modSlot: modSlot, parentItemName: parentTemplate._name})); this.logger.warning(
this.localisationService.getText("bot-unable_to_add_mod_item_invalid", {
itemName: modToAdd[1]._name,
modSlot: modSlot,
parentItemName: parentTemplate._name,
}),
);
} }
return false; return false;
} }
// If mod id doesnt exist in slots filter list and mod id doesnt have any of the slots filters as a base class, mod isn't valid for the slot // If mod id doesn't exist in slots filter list and mod id doesn't have any of the slots filters as a base class, mod isn't valid for the slot
if (!(itemSlot._props.filters[0].Filter.includes(modToAdd[1]._id) || this.itemHelper.isOfBaseclasses(modToAdd[1]._id, itemSlot._props.filters[0].Filter))) if (
!(itemSlot._props.filters[0].Filter.includes(modToAdd[1]._id) ||
this.itemHelper.isOfBaseclasses(modToAdd[1]._id, itemSlot._props.filters[0].Filter))
)
{ {
this.logger.warning(this.localisationService.getText("bot-mod_not_in_slot_filter_list", {modId: modToAdd[1]._id, modSlot: modSlot, parentName: parentTemplate._name})); this.logger.warning(
this.localisationService.getText("bot-mod_not_in_slot_filter_list", {
modId: modToAdd[1]._id,
modSlot: modSlot,
parentName: parentTemplate._name,
}),
);
return false; return false;
} }
@ -703,26 +844,39 @@ export class BotEquipmentModGenerator
return true; return true;
} }
/** /**
* Find mod tpls of a provided type and add to modPool * 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) * @param desiredSlotName slot to look up and add we are adding tpls for (e.g mod_scope)
* @param modTemplate db object for modItem we get compatible mods from * @param modTemplate db object for modItem we get compatible mods from
* @param modPool Pool of mods we are adding to * @param modPool Pool of mods we are adding to
*/ */
protected addCompatibleModsForProvidedMod(desiredSlotName: string, modTemplate: ITemplateItem, modPool: Mods, botEquipBlacklist: EquipmentFilterDetails): void protected addCompatibleModsForProvidedMod(
desiredSlotName: string,
modTemplate: ITemplateItem,
modPool: Mods,
botEquipBlacklist: EquipmentFilterDetails,
): void
{ {
const desiredSlotObject = modTemplate._props.Slots.find(x => x._name.includes(desiredSlotName)); const desiredSlotObject = modTemplate._props.Slots.find((x) => x._name.includes(desiredSlotName));
if (desiredSlotObject) if (desiredSlotObject)
{ {
const supportedSubMods = desiredSlotObject._props.filters[0].Filter; const supportedSubMods = desiredSlotObject._props.filters[0].Filter;
if (supportedSubMods) if (supportedSubMods)
{ {
// Filter mods // Filter mods
let filteredMods = this.filterWeaponModsByBlacklist(supportedSubMods, botEquipBlacklist, desiredSlotName); let filteredMods = this.filterWeaponModsByBlacklist(
supportedSubMods,
botEquipBlacklist,
desiredSlotName,
);
if (filteredMods.length === 0) if (filteredMods.length === 0)
{ {
this.logger.warning(this.localisationService.getText("bot-unable_to_filter_mods_all_blacklisted", {slotName: desiredSlotObject._name, itemName: modTemplate._name})); this.logger.warning(
this.localisationService.getText("bot-unable_to_filter_mods_all_blacklisted", {
slotName: desiredSlotObject._name,
itemName: modTemplate._name,
}),
);
filteredMods = supportedSubMods; filteredMods = supportedSubMods;
} }
@ -736,7 +890,6 @@ export class BotEquipmentModGenerator
} }
} }
/** /**
* Get the possible items that fit a slot * Get the possible items that fit a slot
* @param parentItemId item tpl to get compatible items for * @param parentItemId item tpl to get compatible items for
@ -744,14 +897,22 @@ export class BotEquipmentModGenerator
* @param botEquipBlacklist equipment that should not be picked * @param botEquipBlacklist equipment that should not be picked
* @returns array of compatible items for that slot * @returns array of compatible items for that slot
*/ */
protected getDynamicModPool(parentItemId: string, modSlot: string, botEquipBlacklist: EquipmentFilterDetails): string[] protected getDynamicModPool(
parentItemId: string,
modSlot: string,
botEquipBlacklist: EquipmentFilterDetails,
): string[]
{ {
const modsFromDynamicPool = this.jsonUtil.clone(this.botEquipmentModPoolService.getCompatibleModsForWeaponSlot(parentItemId, modSlot)); const modsFromDynamicPool = this.jsonUtil.clone(
this.botEquipmentModPoolService.getCompatibleModsForWeaponSlot(parentItemId, modSlot),
);
const filteredMods = this.filterWeaponModsByBlacklist(modsFromDynamicPool, botEquipBlacklist, modSlot); const filteredMods = this.filterWeaponModsByBlacklist(modsFromDynamicPool, botEquipBlacklist, modSlot);
if (filteredMods.length === 0) if (filteredMods.length === 0)
{ {
this.logger.warning(this.localisationService.getText("bot-unable_to_filter_mod_slot_all_blacklisted", modSlot)); this.logger.warning(
this.localisationService.getText("bot-unable_to_filter_mod_slot_all_blacklisted", modSlot),
);
return modsFromDynamicPool; return modsFromDynamicPool;
} }
@ -765,7 +926,11 @@ export class BotEquipmentModGenerator
* @param modSlot slot mods belong to * @param modSlot slot mods belong to
* @returns Filtered array of mod tpls * @returns Filtered array of mod tpls
*/ */
protected filterWeaponModsByBlacklist(allowedMods: string[], botEquipBlacklist: EquipmentFilterDetails, modSlot: string): string[] protected filterWeaponModsByBlacklist(
allowedMods: string[],
botEquipBlacklist: EquipmentFilterDetails,
modSlot: string,
): string[]
{ {
if (!botEquipBlacklist) if (!botEquipBlacklist)
{ {
@ -774,9 +939,11 @@ export class BotEquipmentModGenerator
let result: string[] = []; let result: string[] = [];
// Get item blacklist and mod equipmet blackist as one array // Get item blacklist and mod equipment blacklist as one array
const blacklist = this.itemFilterService.getBlacklistedItems().concat(botEquipBlacklist.equipment[modSlot] || []); const blacklist = this.itemFilterService.getBlacklistedItems().concat(
result = allowedMods.filter(x => !blacklist.includes(x)); botEquipBlacklist.equipment[modSlot] || [],
);
result = allowedMods.filter((x) => !blacklist.includes(x));
return result; return result;
} }
@ -796,8 +963,13 @@ export class BotEquipmentModGenerator
let itemModPool = modPool[parentTemplate._id]; let itemModPool = modPool[parentTemplate._id];
if (!itemModPool) if (!itemModPool)
{ {
this.logger.warning(this.localisationService.getText("bot-unable_to_fill_camora_slot_mod_pool_empty", {weaponId: parentTemplate._id, weaponName: parentTemplate._name})); this.logger.warning(
const camoraSlots = parentTemplate._props.Slots.filter(x => x._name.startsWith("camora")); this.localisationService.getText("bot-unable_to_fill_camora_slot_mod_pool_empty", {
weaponId: parentTemplate._id,
weaponName: parentTemplate._name,
}),
);
const camoraSlots = parentTemplate._props.Slots.filter((x) => x._name.startsWith("camora"));
// Attempt to generate camora slots for item // Attempt to generate camora slots for item
modPool[parentTemplate._id] = {}; modPool[parentTemplate._id] = {};
@ -818,7 +990,11 @@ export class BotEquipmentModGenerator
else if (camoraFirstSlot in itemModPool) else if (camoraFirstSlot in itemModPool)
{ {
modSlot = camoraFirstSlot; modSlot = camoraFirstSlot;
exhaustableModPool = new ExhaustableArray(this.mergeCamoraPoolsTogether(itemModPool), this.randomUtil, this.jsonUtil); exhaustableModPool = new ExhaustableArray(
this.mergeCamoraPoolsTogether(itemModPool),
this.randomUtil,
this.jsonUtil,
);
} }
else else
{ {
@ -854,17 +1030,17 @@ export class BotEquipmentModGenerator
_id: modId, _id: modId,
_tpl: modTpl, _tpl: modTpl,
parentId: parentId, parentId: parentId,
slotId: modSlotId slotId: modSlotId,
}); });
} }
} }
/** /**
* Take a record of camoras and merge the compatable shells into one array * Take a record of camoras and merge the compatible shells into one array
* @param camorasWithShells camoras we want to merge into one array * @param camorasWithShells camoras we want to merge into one array
* @returns string array of shells fro luitple camora sources * @returns string array of shells for multiple camora sources
*/ */
protected mergeCamoraPoolsTogether(camorasWithShells: Record<string, string[]> ): string[] protected mergeCamoraPoolsTogether(camorasWithShells: Record<string, string[]>): string[]
{ {
const poolResult: string[] = []; const poolResult: string[] = [];
for (const camoraKey in camorasWithShells) for (const camoraKey in camorasWithShells)
@ -892,7 +1068,11 @@ export class BotEquipmentModGenerator
* @param botWeaponSightWhitelist Whitelist of scope types by weapon base type * @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 * @returns Array of scope tpls that have been filtered to just ones allowed for that weapon type
*/ */
protected filterSightsByWeaponType(weapon: Item, scopes: string[], botWeaponSightWhitelist: Record<string, string[]>): string[] protected filterSightsByWeaponType(
weapon: Item,
scopes: string[],
botWeaponSightWhitelist: Record<string, string[]>,
): string[]
{ {
const weaponDetails = this.itemHelper.getItem(weapon._tpl); const weaponDetails = this.itemHelper.getItem(weapon._tpl);
@ -900,7 +1080,11 @@ export class BotEquipmentModGenerator
const whitelistedSightTypes = botWeaponSightWhitelist[weaponDetails[1]._parent]; const whitelistedSightTypes = botWeaponSightWhitelist[weaponDetails[1]._parent];
if (!whitelistedSightTypes) if (!whitelistedSightTypes)
{ {
this.logger.debug(`Unable to find whitelist for weapon type: ${weaponDetails[1]._parent} ${weaponDetails[1]._name}, skipping sight filtering`); this.logger.debug(
`Unable to find whitelist for weapon type: ${weaponDetails[1]._parent} ${
weaponDetails[1]._name
}, skipping sight filtering`,
);
return scopes; return scopes;
} }
@ -920,14 +1104,23 @@ export class BotEquipmentModGenerator
// Edge case, what if item is a mount for a scope and not directly a scope? // Edge case, what if item is a mount for a scope and not directly a scope?
// Check item is mount + has child items // Check item is mount + has child items
const itemDetails = this.itemHelper.getItem(item)[1]; const itemDetails = this.itemHelper.getItem(item)[1];
if (this.itemHelper.isOfBaseclass(item, BaseClasses.MOUNT) && itemDetails._props.Slots.length > 0 ) 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) // 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 // 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(x => ["mod_scope", "mod_scope_000"].includes(x._name)); const scopeSlot = itemDetails._props.Slots.filter((x) =>
["mod_scope", "mod_scope_000"].includes(x._name)
);
// Mods scope slot found must allow ALL whitelisted scope types OR be a mount // Mods scope slot found must allow ALL whitelisted scope types OR be a mount
if (scopeSlot?.every(x => x._props.filters[0].Filter.every(x => this.itemHelper.isOfBaseclasses(x, whitelistedSightTypes) || this.itemHelper.isOfBaseclass(x, BaseClasses.MOUNT)))) if (
scopeSlot?.every((x) =>
x._props.filters[0].Filter.every((x) =>
this.itemHelper.isOfBaseclasses(x, whitelistedSightTypes) ||
this.itemHelper.isOfBaseclass(x, BaseClasses.MOUNT)
)
)
)
{ {
// Add mod to allowed list // Add mod to allowed list
filteredScopesAndMods.push(item); filteredScopesAndMods.push(item);
@ -935,10 +1128,12 @@ export class BotEquipmentModGenerator
} }
} }
// No mods added to return list after filtering has occured, send back the original mod list // No mods added to return list after filtering has occurred, send back the original mod list
if (!filteredScopesAndMods || filteredScopesAndMods.length === 0) if (!filteredScopesAndMods || filteredScopesAndMods.length === 0)
{ {
this.logger.debug(`Scope whitelist too restrictive for: ${weapon._tpl} ${weaponDetails[1]._name}, skipping filter`); this.logger.debug(
`Scope whitelist too restrictive for: ${weapon._tpl} ${weaponDetails[1]._name}, skipping filter`,
);
return scopes; return scopes;
} }

View File

@ -8,9 +8,12 @@ import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper";
import { WeightedRandomHelper } from "@spt-aki/helpers/WeightedRandomHelper"; import { WeightedRandomHelper } from "@spt-aki/helpers/WeightedRandomHelper";
import { import {
Common, Common,
IBaseJsonSkills, IBaseSkill, IBotBase, Info,
Health as PmcHealth, Health as PmcHealth,
Skills as botSkills IBaseJsonSkills,
IBaseSkill,
IBotBase,
Info,
Skills as botSkills,
} from "@spt-aki/models/eft/common/tables/IBotBase"; } from "@spt-aki/models/eft/common/tables/IBotBase";
import { Appearance, Health, IBotType } from "@spt-aki/models/eft/common/tables/IBotType"; import { Appearance, Health, IBotType } from "@spt-aki/models/eft/common/tables/IBotType";
import { Item, Upd } from "@spt-aki/models/eft/common/tables/IItem"; import { Item, Upd } from "@spt-aki/models/eft/common/tables/IItem";
@ -53,7 +56,7 @@ export class BotGenerator
@inject("BotDifficultyHelper") protected botDifficultyHelper: BotDifficultyHelper, @inject("BotDifficultyHelper") protected botDifficultyHelper: BotDifficultyHelper,
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService, @inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService,
@inject("LocalisationService") protected localisationService: LocalisationService, @inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ConfigServer") protected configServer: ConfigServer @inject("ConfigServer") protected configServer: ConfigServer,
) )
{ {
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT); this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
@ -82,7 +85,7 @@ export class BotGenerator
botRelativeLevelDeltaMax: 0, botRelativeLevelDeltaMax: 0,
botCountToGenerate: 1, botCountToGenerate: 1,
botDifficulty: difficulty, botDifficulty: difficulty,
isPlayerScav: true isPlayerScav: true,
}; };
bot = this.generateBot(sessionId, bot, botTemplate, botGenDetails); bot = this.generateBot(sessionId, bot, botTemplate, botGenDetails);
@ -98,7 +101,8 @@ export class BotGenerator
*/ */
public prepareAndGenerateBots( public prepareAndGenerateBots(
sessionId: string, sessionId: string,
botGenerationDetails: BotGenerationDetails): IBotBase[] botGenerationDetails: BotGenerationDetails,
): IBotBase[]
{ {
const output: IBotBase[] = []; const output: IBotBase[] = [];
for (let i = 0; i < botGenerationDetails.botCountToGenerate; i++) for (let i = 0; i < botGenerationDetails.botCountToGenerate; i++)
@ -111,16 +115,21 @@ export class BotGenerator
// Get raw json data for bot (Cloned) // Get raw json data for bot (Cloned)
const botJsonTemplate = this.jsonUtil.clone(this.botHelper.getBotTemplate( const botJsonTemplate = this.jsonUtil.clone(this.botHelper.getBotTemplate(
(botGenerationDetails.isPmc) (botGenerationDetails.isPmc) ?
? bot.Info.Side bot.Info.Side :
: botGenerationDetails.role)); botGenerationDetails.role,
));
bot = this.generateBot(sessionId, bot, botJsonTemplate, botGenerationDetails); bot = this.generateBot(sessionId, bot, botJsonTemplate, botGenerationDetails);
output.push(bot); output.push(bot);
} }
this.logger.debug(`Generated ${botGenerationDetails.botCountToGenerate} ${output[0].Info.Settings.Role} (${botGenerationDetails.eventRole}) bots`); this.logger.debug(
`Generated ${botGenerationDetails.botCountToGenerate} ${
output[0].Info.Settings.Role
} (${botGenerationDetails.eventRole}) bots`,
);
return output; return output;
} }
@ -142,21 +151,43 @@ export class BotGenerator
* @param botGenerationDetails details on how to generate the bot * @param botGenerationDetails details on how to generate the bot
* @returns IBotBase object * @returns IBotBase object
*/ */
protected generateBot(sessionId: string, bot: IBotBase, botJsonTemplate: IBotType, botGenerationDetails: BotGenerationDetails): IBotBase protected generateBot(
sessionId: string,
bot: IBotBase,
botJsonTemplate: IBotType,
botGenerationDetails: BotGenerationDetails,
): IBotBase
{ {
const botRole = botGenerationDetails.role.toLowerCase(); const botRole = botGenerationDetails.role.toLowerCase();
const botLevel = this.botLevelGenerator.generateBotLevel(botJsonTemplate.experience.level, botGenerationDetails, bot); const botLevel = this.botLevelGenerator.generateBotLevel(
botJsonTemplate.experience.level,
botGenerationDetails,
bot,
);
if (!botGenerationDetails.isPlayerScav) if (!botGenerationDetails.isPlayerScav)
{ {
this.botEquipmentFilterService.filterBotEquipment(sessionId, botJsonTemplate, botLevel.level, botGenerationDetails); this.botEquipmentFilterService.filterBotEquipment(
sessionId,
botJsonTemplate,
botLevel.level,
botGenerationDetails,
);
} }
bot.Info.Nickname = this.generateBotNickname(botJsonTemplate, botGenerationDetails.isPlayerScav, botRole, sessionId); bot.Info.Nickname = this.generateBotNickname(
botJsonTemplate,
botGenerationDetails.isPlayerScav,
botRole,
sessionId,
);
if (!this.seasonalEventService.christmasEventEnabled()) if (!this.seasonalEventService.christmasEventEnabled())
{ {
this.seasonalEventService.removeChristmasItemsFromBotInventory(botJsonTemplate.inventory, botGenerationDetails.role); this.seasonalEventService.removeChristmasItemsFromBotInventory(
botJsonTemplate.inventory,
botGenerationDetails.role,
);
} }
// Remove hideout data if bot is not a PMC or pscav // Remove hideout data if bot is not a PMC or pscav
@ -167,7 +198,10 @@ export class BotGenerator
bot.Info.Experience = botLevel.exp; bot.Info.Experience = botLevel.exp;
bot.Info.Level = botLevel.level; bot.Info.Level = botLevel.level;
bot.Info.Settings.Experience = this.randomUtil.getInt(botJsonTemplate.experience.reward.min, botJsonTemplate.experience.reward.max); bot.Info.Settings.Experience = this.randomUtil.getInt(
botJsonTemplate.experience.reward.min,
botJsonTemplate.experience.reward.max,
);
bot.Info.Settings.StandingForKill = botJsonTemplate.experience.standingForKill; bot.Info.Settings.StandingForKill = botJsonTemplate.experience.standingForKill;
bot.Info.Voice = this.randomUtil.getArrayValue(botJsonTemplate.appearance.voice); bot.Info.Voice = this.randomUtil.getArrayValue(botJsonTemplate.appearance.voice);
bot.Health = this.generateHealth(botJsonTemplate.health, bot.Info.Side === "Savage"); bot.Health = this.generateHealth(botJsonTemplate.health, bot.Info.Side === "Savage");
@ -175,7 +209,13 @@ export class BotGenerator
this.setBotAppearance(bot, botJsonTemplate.appearance, botGenerationDetails); this.setBotAppearance(bot, botJsonTemplate.appearance, botGenerationDetails);
bot.Inventory = this.botInventoryGenerator.generateInventory(sessionId, botJsonTemplate, botRole, botGenerationDetails.isPmc, botLevel.level); bot.Inventory = this.botInventoryGenerator.generateInventory(
sessionId,
botJsonTemplate,
botRole,
botGenerationDetails.isPmc,
botLevel.level,
);
if (this.botHelper.isBotPmc(botRole)) if (this.botHelper.isBotPmc(botRole))
{ {
@ -205,7 +245,6 @@ export class BotGenerator
* @param appearance Appearance settings to choose from * @param appearance Appearance settings to choose from
* @param botGenerationDetails Generation details * @param botGenerationDetails Generation details
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected setBotAppearance(bot: IBotBase, appearance: Appearance, botGenerationDetails: BotGenerationDetails): void protected setBotAppearance(bot: IBotBase, appearance: Appearance, botGenerationDetails: BotGenerationDetails): void
{ {
bot.Customization.Head = this.randomUtil.getArrayValue(appearance.head); bot.Customization.Head = this.randomUtil.getArrayValue(appearance.head);
@ -221,9 +260,16 @@ export class BotGenerator
* @param botRole role of bot e.g. assault * @param botRole role of bot e.g. assault
* @returns Nickname for bot * @returns Nickname for bot
*/ */
protected generateBotNickname(botJsonTemplate: IBotType, isPlayerScav: boolean, botRole: string, sessionId: string): string protected generateBotNickname(
botJsonTemplate: IBotType,
isPlayerScav: boolean,
botRole: string,
sessionId: string,
): string
{ {
let name = `${this.randomUtil.getArrayValue(botJsonTemplate.firstName)} ${this.randomUtil.getArrayValue(botJsonTemplate.lastName) || ""}`; let name = `${this.randomUtil.getArrayValue(botJsonTemplate.firstName)} ${
this.randomUtil.getArrayValue(botJsonTemplate.lastName) || ""
}`;
name = name.trim(); name = name.trim();
const playerProfile = this.profileHelper.getPmcProfile(sessionId); const playerProfile = this.profileHelper.getPmcProfile(sessionId);
@ -237,7 +283,7 @@ export class BotGenerator
const pmcNames = [ const pmcNames = [
...this.databaseServer.getTables().bots.types["usec"].firstName, ...this.databaseServer.getTables().bots.types["usec"].firstName,
...this.databaseServer.getTables().bots.types["bear"].firstName ...this.databaseServer.getTables().bots.types["bear"].firstName,
]; ];
return `${name} (${this.randomUtil.getArrayValue(pmcNames)})`; return `${name} (${this.randomUtil.getArrayValue(pmcNames)})`;
@ -253,7 +299,6 @@ export class BotGenerator
{ {
if (this.randomUtil.getChance100(this.pmcConfig.addPrefixToSameNamePMCAsPlayerChance)) if (this.randomUtil.getChance100(this.pmcConfig.addPrefixToSameNamePMCAsPlayerChance))
{ {
const prefix = this.localisationService.getRandomTextThatMatchesPartialKey("pmc-name_prefix_"); const prefix = this.localisationService.getRandomTextThatMatchesPartialKey("pmc-name_prefix_");
name = `${prefix} ${name}`; name = `${prefix} ${name}`;
} }
@ -268,7 +313,10 @@ export class BotGenerator
*/ */
protected logPmcGeneratedCount(output: IBotBase[]): void protected logPmcGeneratedCount(output: IBotBase[]): void
{ {
const pmcCount = output.reduce((acc, cur) => cur.Info.Side === "Bear" || cur.Info.Side === "Usec" ? ++acc : acc, 0); const pmcCount = output.reduce(
(acc, cur) => cur.Info.Side === "Bear" || cur.Info.Side === "Usec" ? ++acc : acc,
0,
);
this.logger.debug(`Generated ${output.length} total bots. Replaced ${pmcCount} with PMCs`); this.logger.debug(`Generated ${output.length} total bots. Replaced ${pmcCount} with PMCs`);
} }
@ -280,68 +328,68 @@ export class BotGenerator
*/ */
protected generateHealth(healthObj: Health, playerScav = false): PmcHealth protected generateHealth(healthObj: Health, playerScav = false): PmcHealth
{ {
const bodyParts = (playerScav) const bodyParts = playerScav ?
? healthObj.BodyParts[0] healthObj.BodyParts[0] :
: this.randomUtil.getArrayValue(healthObj.BodyParts); this.randomUtil.getArrayValue(healthObj.BodyParts);
const newHealth: PmcHealth = { const newHealth: PmcHealth = {
Hydration: { Hydration: {
Current: this.randomUtil.getInt(healthObj.Hydration.min, healthObj.Hydration.max), Current: this.randomUtil.getInt(healthObj.Hydration.min, healthObj.Hydration.max),
Maximum: healthObj.Hydration.max Maximum: healthObj.Hydration.max,
}, },
Energy: { Energy: {
Current: this.randomUtil.getInt(healthObj.Energy.min, healthObj.Energy.max), Current: this.randomUtil.getInt(healthObj.Energy.min, healthObj.Energy.max),
Maximum: healthObj.Energy.max Maximum: healthObj.Energy.max,
}, },
Temperature: { Temperature: {
Current: this.randomUtil.getInt(healthObj.Temperature.min, healthObj.Temperature.max), Current: this.randomUtil.getInt(healthObj.Temperature.min, healthObj.Temperature.max),
Maximum: healthObj.Temperature.max Maximum: healthObj.Temperature.max,
}, },
BodyParts: { BodyParts: {
Head: { Head: {
Health: { Health: {
Current: this.randomUtil.getInt(bodyParts.Head.min, bodyParts.Head.max), Current: this.randomUtil.getInt(bodyParts.Head.min, bodyParts.Head.max),
Maximum: Math.round(bodyParts.Head.max) Maximum: Math.round(bodyParts.Head.max),
} },
}, },
Chest: { Chest: {
Health: { Health: {
Current: this.randomUtil.getInt(bodyParts.Chest.min, bodyParts.Chest.max), Current: this.randomUtil.getInt(bodyParts.Chest.min, bodyParts.Chest.max),
Maximum: Math.round(bodyParts.Chest.max) Maximum: Math.round(bodyParts.Chest.max),
} },
}, },
Stomach: { Stomach: {
Health: { Health: {
Current: this.randomUtil.getInt(bodyParts.Stomach.min, bodyParts.Stomach.max), Current: this.randomUtil.getInt(bodyParts.Stomach.min, bodyParts.Stomach.max),
Maximum: Math.round(bodyParts.Stomach.max) Maximum: Math.round(bodyParts.Stomach.max),
} },
}, },
LeftArm: { LeftArm: {
Health: { Health: {
Current: this.randomUtil.getInt(bodyParts.LeftArm.min, bodyParts.LeftArm.max), Current: this.randomUtil.getInt(bodyParts.LeftArm.min, bodyParts.LeftArm.max),
Maximum: Math.round(bodyParts.LeftArm.max) Maximum: Math.round(bodyParts.LeftArm.max),
} },
}, },
RightArm: { RightArm: {
Health: { Health: {
Current: this.randomUtil.getInt(bodyParts.RightArm.min, bodyParts.RightArm.max), Current: this.randomUtil.getInt(bodyParts.RightArm.min, bodyParts.RightArm.max),
Maximum: Math.round(bodyParts.RightArm.max) Maximum: Math.round(bodyParts.RightArm.max),
} },
}, },
LeftLeg: { LeftLeg: {
Health: { Health: {
Current: this.randomUtil.getInt(bodyParts.LeftLeg.min, bodyParts.LeftLeg.max), Current: this.randomUtil.getInt(bodyParts.LeftLeg.min, bodyParts.LeftLeg.max),
Maximum: Math.round(bodyParts.LeftLeg.max) Maximum: Math.round(bodyParts.LeftLeg.max),
} },
}, },
RightLeg: { RightLeg: {
Health: { Health: {
Current: this.randomUtil.getInt(bodyParts.RightLeg.min, bodyParts.RightLeg.max), Current: this.randomUtil.getInt(bodyParts.RightLeg.min, bodyParts.RightLeg.max),
Maximum: Math.round(bodyParts.RightLeg.max) Maximum: Math.round(bodyParts.RightLeg.max),
}
}
}, },
UpdateTime: this.timeUtil.getTimestamp() },
},
UpdateTime: this.timeUtil.getTimestamp(),
}; };
return newHealth; return newHealth;
@ -357,7 +405,7 @@ export class BotGenerator
const skillsToReturn: botSkills = { const skillsToReturn: botSkills = {
Common: this.getSkillsWithRandomisedProgressValue(botSkills.Common, true), Common: this.getSkillsWithRandomisedProgressValue(botSkills.Common, true),
Mastering: this.getSkillsWithRandomisedProgressValue(botSkills.Mastering, false), Mastering: this.getSkillsWithRandomisedProgressValue(botSkills.Mastering, false),
Points: 0 Points: 0,
}; };
return skillsToReturn; return skillsToReturn;
@ -369,7 +417,10 @@ export class BotGenerator
* @param isCommonSkills Are the skills 'common' skills * @param isCommonSkills Are the skills 'common' skills
* @returns Skills with randomised progress values as an array * @returns Skills with randomised progress values as an array
*/ */
protected getSkillsWithRandomisedProgressValue(skills: Record<string, IBaseSkill>, isCommonSkills: boolean): IBaseSkill[] protected getSkillsWithRandomisedProgressValue(
skills: Record<string, IBaseSkill>,
isCommonSkills: boolean,
): IBaseSkill[]
{ {
if (Object.keys(skills ?? []).length === 0) if (Object.keys(skills ?? []).length === 0)
{ {
@ -388,7 +439,7 @@ export class BotGenerator
// All skills have id and progress props // All skills have id and progress props
const skillToAdd: IBaseSkill = { const skillToAdd: IBaseSkill = {
Id: skillKey, Id: skillKey,
Progress: this.randomUtil.getInt(skill.min, skill.max) Progress: this.randomUtil.getInt(skill.min, skill.max),
}; };
// Common skills have additional props // Common skills have additional props
@ -399,7 +450,7 @@ export class BotGenerator
} }
return skillToAdd; return skillToAdd;
}).filter(x => x !== null); }).filter((x) => x !== null);
} }
/** /**
@ -482,7 +533,9 @@ export class BotGenerator
} }
botInfo.GameVersion = this.weightedRandomHelper.getWeightedValue(this.pmcConfig.gameVersionWeight); botInfo.GameVersion = this.weightedRandomHelper.getWeightedValue(this.pmcConfig.gameVersionWeight);
botInfo.MemberCategory = Number.parseInt(this.weightedRandomHelper.getWeightedValue(this.pmcConfig.accountTypeWeight)); botInfo.MemberCategory = Number.parseInt(
this.weightedRandomHelper.getWeightedValue(this.pmcConfig.accountTypeWeight),
);
} }
/** /**
@ -505,8 +558,8 @@ export class BotGenerator
KillerAccountId: "Unknown", KillerAccountId: "Unknown",
KillerProfileId: "Unknown", KillerProfileId: "Unknown",
KillerName: "Unknown", KillerName: "Unknown",
WeaponName: "Unknown" WeaponName: "Unknown",
} },
}; };
const inventoryItem: Item = { const inventoryItem: Item = {
@ -515,7 +568,7 @@ export class BotGenerator
parentId: bot.Inventory.equipment, parentId: bot.Inventory.equipment,
slotId: "Dogtag", slotId: "Dogtag",
location: undefined, location: undefined,
upd: upd upd: upd,
}; };
bot.Inventory.items.push(inventoryItem); bot.Inventory.items.push(inventoryItem);

View File

@ -39,7 +39,7 @@ export class BotInventoryGenerator
@inject("LocalisationService") protected localisationService: LocalisationService, @inject("LocalisationService") protected localisationService: LocalisationService,
@inject("BotEquipmentModPoolService") protected botEquipmentModPoolService: BotEquipmentModPoolService, @inject("BotEquipmentModPoolService") protected botEquipmentModPoolService: BotEquipmentModPoolService,
@inject("BotEquipmentModGenerator") protected botEquipmentModGenerator: BotEquipmentModGenerator, @inject("BotEquipmentModGenerator") protected botEquipmentModGenerator: BotEquipmentModGenerator,
@inject("ConfigServer") protected configServer: ConfigServer @inject("ConfigServer") protected configServer: ConfigServer,
) )
{ {
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT); this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
@ -54,7 +54,13 @@ export class BotInventoryGenerator
* @param botLevel Level of bot being generated * @param botLevel Level of bot being generated
* @returns PmcInventory object with equipment/weapons/loot * @returns PmcInventory object with equipment/weapons/loot
*/ */
public generateInventory(sessionId: string, botJsonTemplate: IBotType, botRole: string, isPmc: boolean, botLevel: number): PmcInventory public generateInventory(
sessionId: string,
botJsonTemplate: IBotType,
botRole: string,
isPmc: boolean,
botLevel: number,
): PmcInventory
{ {
const templateInventory = botJsonTemplate.inventory; const templateInventory = botJsonTemplate.inventory;
const equipmentChances = botJsonTemplate.chances; const equipmentChances = botJsonTemplate.chances;
@ -66,7 +72,16 @@ export class BotInventoryGenerator
this.generateAndAddEquipmentToBot(templateInventory, equipmentChances, botRole, botInventory, botLevel); this.generateAndAddEquipmentToBot(templateInventory, equipmentChances, botRole, botInventory, botLevel);
// Roll weapon spawns (primary/secondary/holster) and generate a weapon for each roll that passed // Roll weapon spawns (primary/secondary/holster) and generate a weapon for each roll that passed
this.generateAndAddWeaponsToBot(templateInventory, equipmentChances, sessionId, botInventory, botRole, isPmc, itemGenerationLimitsMinMax, botLevel); this.generateAndAddWeaponsToBot(
templateInventory,
equipmentChances,
sessionId,
botInventory,
botRole,
isPmc,
itemGenerationLimitsMinMax,
botLevel,
);
// Pick loot and add to bots containers (rig/backpack/pockets/secure) // Pick loot and add to bots containers (rig/backpack/pockets/secure)
this.botLootGenerator.generateLoot(sessionId, botJsonTemplate, isPmc, botRole, botInventory, botLevel); this.botLootGenerator.generateLoot(sessionId, botJsonTemplate, isPmc, botRole, botInventory, botLevel);
@ -99,24 +114,24 @@ export class BotInventoryGenerator
items: [ items: [
{ {
_id: equipmentId, _id: equipmentId,
_tpl: equipmentTpl _tpl: equipmentTpl,
}, },
{ {
_id: stashId, _id: stashId,
_tpl: stashTpl _tpl: stashTpl,
}, },
{ {
_id: questRaidItemsId, _id: questRaidItemsId,
_tpl: questRaidItemsTpl _tpl: questRaidItemsTpl,
}, },
{ {
_id: questStashItemsId, _id: questStashItemsId,
_tpl: questStashItemsTpl _tpl: questStashItemsTpl,
}, },
{ {
_id: sortingTableId, _id: sortingTableId,
_tpl: sortingTableTpl _tpl: sortingTableTpl,
} },
], ],
equipment: equipmentId, equipment: equipmentId,
stash: stashId, stash: stashId,
@ -124,7 +139,7 @@ export class BotInventoryGenerator
questStashItems: questStashItemsId, questStashItems: questStashItemsId,
sortingTable: sortingTableId, sortingTable: sortingTableId,
hideoutAreaStashes: {}, hideoutAreaStashes: {},
fastPanel: {} fastPanel: {},
}; };
} }
@ -136,7 +151,13 @@ export class BotInventoryGenerator
* @param botInventory Inventory to add equipment to * @param botInventory Inventory to add equipment to
* @param botLevel Level of bot * @param botLevel Level of bot
*/ */
protected generateAndAddEquipmentToBot(templateInventory: Inventory, equipmentChances: Chances, botRole: string, botInventory: PmcInventory, botLevel: number): void protected generateAndAddEquipmentToBot(
templateInventory: Inventory,
equipmentChances: Chances,
botRole: string,
botInventory: PmcInventory,
botLevel: number,
): void
{ {
// These will be handled later // These will be handled later
const excludedSlots: string[] = [ const excludedSlots: string[] = [
@ -147,7 +168,7 @@ export class BotInventoryGenerator
EquipmentSlots.TACTICAL_VEST, EquipmentSlots.TACTICAL_VEST,
EquipmentSlots.FACE_COVER, EquipmentSlots.FACE_COVER,
EquipmentSlots.HEADWEAR, EquipmentSlots.HEADWEAR,
EquipmentSlots.EARPIECE EquipmentSlots.EARPIECE,
]; ];
const botEquipConfig = this.botConfig.equipment[this.botGeneratorHelper.getBotEquipmentRole(botRole)]; const botEquipConfig = this.botConfig.equipment[this.botGeneratorHelper.getBotEquipmentRole(botRole)];
@ -155,21 +176,69 @@ export class BotInventoryGenerator
for (const equipmentSlot in templateInventory.equipment) for (const equipmentSlot in templateInventory.equipment)
{ {
// Weapons have special generation and will be generated seperately; ArmorVest should be generated after TactivalVest // Weapons have special generation and will be generated separately; ArmorVest should be generated after TactivalVest
if (excludedSlots.includes(equipmentSlot)) if (excludedSlots.includes(equipmentSlot))
{ {
continue; continue;
} }
this.generateEquipment(equipmentSlot, templateInventory.equipment[equipmentSlot], templateInventory.mods, equipmentChances, botRole, botInventory, randomistionDetails); this.generateEquipment(
equipmentSlot,
templateInventory.equipment[equipmentSlot],
templateInventory.mods,
equipmentChances,
botRole,
botInventory,
randomistionDetails,
);
} }
// Generate below in specific order // Generate below in specific order
this.generateEquipment(EquipmentSlots.FACE_COVER, templateInventory.equipment.FaceCover, templateInventory.mods, equipmentChances, botRole, botInventory, randomistionDetails); this.generateEquipment(
this.generateEquipment(EquipmentSlots.HEADWEAR, templateInventory.equipment.Headwear, templateInventory.mods, equipmentChances, botRole, botInventory, randomistionDetails); EquipmentSlots.FACE_COVER,
this.generateEquipment(EquipmentSlots.EARPIECE, templateInventory.equipment.Earpiece, templateInventory.mods, equipmentChances, botRole, botInventory, randomistionDetails); templateInventory.equipment.FaceCover,
this.generateEquipment(EquipmentSlots.TACTICAL_VEST, templateInventory.equipment.TacticalVest, templateInventory.mods, equipmentChances, botRole, botInventory, randomistionDetails); templateInventory.mods,
this.generateEquipment(EquipmentSlots.ARMOR_VEST, templateInventory.equipment.ArmorVest, templateInventory.mods, equipmentChances, botRole, botInventory, randomistionDetails); equipmentChances,
botRole,
botInventory,
randomistionDetails,
);
this.generateEquipment(
EquipmentSlots.HEADWEAR,
templateInventory.equipment.Headwear,
templateInventory.mods,
equipmentChances,
botRole,
botInventory,
randomistionDetails,
);
this.generateEquipment(
EquipmentSlots.EARPIECE,
templateInventory.equipment.Earpiece,
templateInventory.mods,
equipmentChances,
botRole,
botInventory,
randomistionDetails,
);
this.generateEquipment(
EquipmentSlots.TACTICAL_VEST,
templateInventory.equipment.TacticalVest,
templateInventory.mods,
equipmentChances,
botRole,
botInventory,
randomistionDetails,
);
this.generateEquipment(
EquipmentSlots.ARMOR_VEST,
templateInventory.equipment.ArmorVest,
templateInventory.mods,
equipmentChances,
botRole,
botInventory,
randomistionDetails,
);
} }
/** /**
@ -189,14 +258,18 @@ export class BotInventoryGenerator
spawnChances: Chances, spawnChances: Chances,
botRole: string, botRole: string,
inventory: PmcInventory, inventory: PmcInventory,
randomisationDetails: RandomisationDetails): void randomisationDetails: RandomisationDetails,
): void
{ {
const spawnChance = ([EquipmentSlots.POCKETS, EquipmentSlots.SECURED_CONTAINER] as string[]).includes(equipmentSlot) const spawnChance =
? 100 ([EquipmentSlots.POCKETS, EquipmentSlots.SECURED_CONTAINER] as string[]).includes(equipmentSlot) ?
: spawnChances.equipment[equipmentSlot]; 100 :
spawnChances.equipment[equipmentSlot];
if (typeof spawnChance === "undefined") if (typeof spawnChance === "undefined")
{ {
this.logger.warning(this.localisationService.getText("bot-no_spawn_chance_defined_for_equipment_slot", equipmentSlot)); this.logger.warning(
this.localisationService.getText("bot-no_spawn_chance_defined_for_equipment_slot", equipmentSlot),
);
return; return;
} }
@ -216,7 +289,13 @@ export class BotInventoryGenerator
return; return;
} }
if (this.botGeneratorHelper.isItemIncompatibleWithCurrentItems(inventory.items, equipmentItemTpl, equipmentSlot).incompatible) if (
this.botGeneratorHelper.isItemIncompatibleWithCurrentItems(
inventory.items,
equipmentItemTpl,
equipmentSlot,
).incompatible
)
{ {
// Bad luck - randomly picked item was not compatible with current gear // Bad luck - randomly picked item was not compatible with current gear
return; return;
@ -227,19 +306,35 @@ export class BotInventoryGenerator
_tpl: equipmentItemTpl, _tpl: equipmentItemTpl,
parentId: inventory.equipment, parentId: inventory.equipment,
slotId: equipmentSlot, slotId: equipmentSlot,
...this.botGeneratorHelper.generateExtraPropertiesForItem(itemTemplate[1], botRole) ...this.botGeneratorHelper.generateExtraPropertiesForItem(itemTemplate[1], botRole),
}; };
// use dynamic mod pool if enabled in config // use dynamic mod pool if enabled in config
const botEquipmentRole = this.botGeneratorHelper.getBotEquipmentRole(botRole); const botEquipmentRole = this.botGeneratorHelper.getBotEquipmentRole(botRole);
if (this.botConfig.equipment[botEquipmentRole] && randomisationDetails?.randomisedArmorSlots?.includes(equipmentSlot)) if (
this.botConfig.equipment[botEquipmentRole] &&
randomisationDetails?.randomisedArmorSlots?.includes(equipmentSlot)
)
{ {
modPool[equipmentItemTpl] = this.getFilteredDynamicModsForItem(equipmentItemTpl, this.botConfig.equipment[botEquipmentRole].blacklist); modPool[equipmentItemTpl] = this.getFilteredDynamicModsForItem(
equipmentItemTpl,
this.botConfig.equipment[botEquipmentRole].blacklist,
);
} }
if (typeof(modPool[equipmentItemTpl]) !== "undefined" || Object.keys(modPool[equipmentItemTpl] || {}).length > 0) if (
typeof (modPool[equipmentItemTpl]) !== "undefined" ||
Object.keys(modPool[equipmentItemTpl] || {}).length > 0
)
{ {
const items = this.botEquipmentModGenerator.generateModsForEquipment([item], modPool, id, itemTemplate[1], spawnChances.mods, botRole); const items = this.botEquipmentModGenerator.generateModsForEquipment(
[item],
modPool,
id,
itemTemplate[1],
spawnChances.mods,
botRole,
);
inventory.items.push(...items); inventory.items.push(...items);
} }
else else
@ -251,17 +346,20 @@ export class BotInventoryGenerator
/** /**
* Get all possible mods for item and filter down based on equipment blacklist from bot.json config * Get all possible mods for item and filter down based on equipment blacklist from bot.json config
* @param itemTpl Item mod pool is being retreived and filtered * @param itemTpl Item mod pool is being retrieved and filtered
* @param equipmentBlacklist blacklist to filter mod pool with * @param equipmentBlacklist blacklist to filter mod pool with
* @returns Filtered pool of mods * @returns Filtered pool of mods
*/ */
protected getFilteredDynamicModsForItem(itemTpl: string, equipmentBlacklist: EquipmentFilterDetails[]): Record<string, string[]> protected getFilteredDynamicModsForItem(
itemTpl: string,
equipmentBlacklist: EquipmentFilterDetails[],
): Record<string, string[]>
{ {
const modPool = this.botEquipmentModPoolService.getModsForGearSlot(itemTpl); const modPool = this.botEquipmentModPoolService.getModsForGearSlot(itemTpl);
for (const modSlot of Object.keys(modPool ?? [])) for (const modSlot of Object.keys(modPool ?? []))
{ {
const blacklistedMods = equipmentBlacklist[0]?.equipment[modSlot] || []; const blacklistedMods = equipmentBlacklist[0]?.equipment[modSlot] || [];
const filteredMods = modPool[modSlot].filter(x => !blacklistedMods.includes(x)); const filteredMods = modPool[modSlot].filter((x) => !blacklistedMods.includes(x));
if (filteredMods.length > 0) if (filteredMods.length > 0)
{ {
@ -283,7 +381,16 @@ export class BotInventoryGenerator
* @param botLevel level of bot having weapon generated * @param botLevel level of bot having weapon generated
* @param itemGenerationLimitsMinMax Limits for items the bot can have * @param itemGenerationLimitsMinMax Limits for items the bot can have
*/ */
protected generateAndAddWeaponsToBot(templateInventory: Inventory, equipmentChances: Chances, sessionId: string, botInventory: PmcInventory, botRole: string, isPmc: boolean, itemGenerationLimitsMinMax: Generation, botLevel: number): void protected generateAndAddWeaponsToBot(
templateInventory: Inventory,
equipmentChances: Chances,
sessionId: string,
botInventory: PmcInventory,
botRole: string,
isPmc: boolean,
itemGenerationLimitsMinMax: Generation,
botLevel: number,
): void
{ {
const weaponSlotsToFill = this.getDesiredWeaponsForBot(equipmentChances); const weaponSlotsToFill = this.getDesiredWeaponsForBot(equipmentChances);
for (const weaponSlot of weaponSlotsToFill) for (const weaponSlot of weaponSlotsToFill)
@ -291,7 +398,17 @@ export class BotInventoryGenerator
// Add weapon to bot if true and bot json has something to put into the slot // Add weapon to bot if true and bot json has something to put into the slot
if (weaponSlot.shouldSpawn && Object.keys(templateInventory.equipment[weaponSlot.slot]).length) if (weaponSlot.shouldSpawn && Object.keys(templateInventory.equipment[weaponSlot.slot]).length)
{ {
this.addWeaponAndMagazinesToInventory(sessionId, weaponSlot, templateInventory, botInventory, equipmentChances, botRole, isPmc, itemGenerationLimitsMinMax, botLevel); this.addWeaponAndMagazinesToInventory(
sessionId,
weaponSlot,
templateInventory,
botInventory,
equipmentChances,
botRole,
isPmc,
itemGenerationLimitsMinMax,
botLevel,
);
} }
} }
} }
@ -301,26 +418,27 @@ export class BotInventoryGenerator
* @param equipmentChances Chances bot has certain equipment * @param equipmentChances Chances bot has certain equipment
* @returns What slots bot should have weapons generated for * @returns What slots bot should have weapons generated for
*/ */
protected getDesiredWeaponsForBot(equipmentChances: Chances): { slot: EquipmentSlots; shouldSpawn: boolean; }[] protected getDesiredWeaponsForBot(equipmentChances: Chances): {slot: EquipmentSlots; shouldSpawn: boolean;}[]
{ {
const shouldSpawnPrimary = this.randomUtil.getChance100(equipmentChances.equipment.FirstPrimaryWeapon); const shouldSpawnPrimary = this.randomUtil.getChance100(equipmentChances.equipment.FirstPrimaryWeapon);
return [ return [
{ {
slot: EquipmentSlots.FIRST_PRIMARY_WEAPON, slot: EquipmentSlots.FIRST_PRIMARY_WEAPON,
shouldSpawn: shouldSpawnPrimary shouldSpawn: shouldSpawnPrimary,
}, },
{ {
slot: EquipmentSlots.SECOND_PRIMARY_WEAPON, slot: EquipmentSlots.SECOND_PRIMARY_WEAPON,
shouldSpawn: shouldSpawnPrimary shouldSpawn: shouldSpawnPrimary ?
? this.randomUtil.getChance100(equipmentChances.equipment.SecondPrimaryWeapon) this.randomUtil.getChance100(equipmentChances.equipment.SecondPrimaryWeapon) :
: false false,
}, },
{ {
slot: EquipmentSlots.HOLSTER, slot: EquipmentSlots.HOLSTER,
shouldSpawn: shouldSpawnPrimary shouldSpawn: shouldSpawnPrimary ?
? this.randomUtil.getChance100(equipmentChances.equipment.Holster) // Primary weapon = roll for chance at pistol this.randomUtil.getChance100(equipmentChances.equipment.Holster) // Primary weapon = roll for chance at pistol
: true // No primary = force pistol :
} true, // No primary = force pistol
},
]; ];
} }
@ -337,14 +455,15 @@ export class BotInventoryGenerator
*/ */
protected addWeaponAndMagazinesToInventory( protected addWeaponAndMagazinesToInventory(
sessionId: string, sessionId: string,
weaponSlot: { slot: EquipmentSlots; shouldSpawn: boolean; }, weaponSlot: {slot: EquipmentSlots; shouldSpawn: boolean;},
templateInventory: Inventory, templateInventory: Inventory,
botInventory: PmcInventory, botInventory: PmcInventory,
equipmentChances: Chances, equipmentChances: Chances,
botRole: string, botRole: string,
isPmc: boolean, isPmc: boolean,
itemGenerationWeights: Generation, itemGenerationWeights: Generation,
botLevel: number): void botLevel: number,
): void
{ {
const generatedWeapon = this.botWeaponGenerator.generateRandomWeapon( const generatedWeapon = this.botWeaponGenerator.generateRandomWeapon(
sessionId, sessionId,
@ -354,10 +473,16 @@ export class BotInventoryGenerator
equipmentChances.mods, equipmentChances.mods,
botRole, botRole,
isPmc, isPmc,
botLevel); botLevel,
);
botInventory.items.push(...generatedWeapon.weapon); botInventory.items.push(...generatedWeapon.weapon);
this.botWeaponGenerator.addExtraMagazinesToInventory(generatedWeapon, itemGenerationWeights.items.magazines, botInventory, botRole); this.botWeaponGenerator.addExtraMagazinesToInventory(
generatedWeapon,
itemGenerationWeights.items.magazines,
botInventory,
botRole,
);
} }
} }

View File

@ -15,9 +15,9 @@ export class BotLevelGenerator
constructor( constructor(
@inject("WinstonLogger") protected logger: ILogger, @inject("WinstonLogger") protected logger: ILogger,
@inject("RandomUtil") protected randomUtil: RandomUtil, @inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("DatabaseServer") protected databaseServer: DatabaseServer @inject("DatabaseServer") protected databaseServer: DatabaseServer,
) )
{ } {}
/** /**
* Return a randomised bot level and exp value * Return a randomised bot level and exp value
@ -26,11 +26,19 @@ export class BotLevelGenerator
* @param bot being level is being generated for * @param bot being level is being generated for
* @returns IRandomisedBotLevelResult object * @returns IRandomisedBotLevelResult object
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars public generateBotLevel(
public generateBotLevel(levelDetails: MinMax, botGenerationDetails: BotGenerationDetails, bot: IBotBase): IRandomisedBotLevelResult levelDetails: MinMax,
botGenerationDetails: BotGenerationDetails,
bot: IBotBase,
): IRandomisedBotLevelResult
{ {
const expTable = this.databaseServer.getTables().globals.config.exp.level.exp_table; const expTable = this.databaseServer.getTables().globals.config.exp.level.exp_table;
const highestLevel = this.getHighestRelativeBotLevel(botGenerationDetails.playerLevel, botGenerationDetails.botRelativeLevelDeltaMax, levelDetails, expTable); const highestLevel = this.getHighestRelativeBotLevel(
botGenerationDetails.playerLevel,
botGenerationDetails.botRelativeLevelDeltaMax,
levelDetails,
expTable,
);
// Get random level based on the exp table. // Get random level based on the exp table.
let exp = 0; let exp = 0;
@ -47,16 +55,21 @@ export class BotLevelGenerator
exp += this.randomUtil.getInt(0, expTable[level].exp - 1); exp += this.randomUtil.getInt(0, expTable[level].exp - 1);
} }
return { level, exp }; return {level, exp};
} }
/** /**
* Get the highest level a bot can be relative to the players level, but no futher than the max size from globals.exp_table * Get the highest level a bot can be relative to the players level, but no further than the max size from globals.exp_table
* @param playerLevel Players current level * @param playerLevel Players current level
* @param relativeDeltaMax max delta above player level to go * @param relativeDeltaMax max delta above player level to go
* @returns highest level possible for bot * @returns highest level possible for bot
*/ */
protected getHighestRelativeBotLevel(playerLevel: number, relativeDeltaMax: number, levelDetails: MinMax, expTable: IExpTable[]): number protected getHighestRelativeBotLevel(
playerLevel: number,
relativeDeltaMax: number,
levelDetails: MinMax,
expTable: IExpTable[],
): number
{ {
const maxPossibleLevel = Math.min(levelDetails.max, expTable.length); const maxPossibleLevel = Math.min(levelDetails.max, expTable.length);

View File

@ -44,7 +44,7 @@ export class BotLootGenerator
@inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper, @inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper,
@inject("BotLootCacheService") protected botLootCacheService: BotLootCacheService, @inject("BotLootCacheService") protected botLootCacheService: BotLootCacheService,
@inject("LocalisationService") protected localisationService: LocalisationService, @inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ConfigServer") protected configServer: ConfigServer @inject("ConfigServer") protected configServer: ConfigServer,
) )
{ {
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT); this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
@ -60,7 +60,14 @@ export class BotLootGenerator
* @param botInventory Inventory to add loot to * @param botInventory Inventory to add loot to
* @param botLevel Level of bot * @param botLevel Level of bot
*/ */
public generateLoot(sessionId: string, botJsonTemplate: IBotType, isPmc: boolean, botRole: string, botInventory: PmcInventory, botLevel: number): void public generateLoot(
sessionId: string,
botJsonTemplate: IBotType,
isPmc: boolean,
botRole: string,
botInventory: PmcInventory,
botLevel: number,
): void
{ {
// Limits on item types to be added as loot // Limits on item types to be added as loot
const itemCounts = botJsonTemplate.generation.items; const itemCounts = botJsonTemplate.generation.items;
@ -68,7 +75,9 @@ export class BotLootGenerator
const backpackLootCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.backpackLoot.weights); const backpackLootCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.backpackLoot.weights);
const pocketLootCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.pocketLoot.weights); const pocketLootCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.pocketLoot.weights);
const vestLootCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.vestLoot.weights); const vestLootCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.vestLoot.weights);
const specialLootItemCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.specialItems.weights); const specialLootItemCount = this.weightedRandomHelper.getWeightedValue<number>(
itemCounts.specialItems.weights,
);
const healingItemCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.healing.weights); const healingItemCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.healing.weights);
const drugItemCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.drugs.weights); const drugItemCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.drugs.weights);
const stimItemCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.stims.weights); const stimItemCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.stims.weights);
@ -88,7 +97,8 @@ export class BotLootGenerator
containersBotHasAvailable, containersBotHasAvailable,
specialLootItemCount, specialLootItemCount,
botInventory, botInventory,
botRole); botRole,
);
// Healing items / Meds // Healing items / Meds
this.addLootFromPool( this.addLootFromPool(
@ -99,7 +109,8 @@ export class BotLootGenerator
botRole, botRole,
false, false,
0, 0,
isPmc); isPmc,
);
// Drugs // Drugs
this.addLootFromPool( this.addLootFromPool(
@ -110,7 +121,8 @@ export class BotLootGenerator
botRole, botRole,
false, false,
0, 0,
isPmc); isPmc,
);
// Stims // Stims
this.addLootFromPool( this.addLootFromPool(
@ -121,7 +133,8 @@ export class BotLootGenerator
botRole, botRole,
true, true,
0, 0,
isPmc); isPmc,
);
// Grenades // Grenades
this.addLootFromPool( this.addLootFromPool(
@ -132,8 +145,8 @@ export class BotLootGenerator
botRole, botRole,
false, false,
0, 0,
isPmc); isPmc,
);
// Backpack - generate loot if they have one // Backpack - generate loot if they have one
if (containersBotHasAvailable.includes(EquipmentSlots.BACKPACK)) if (containersBotHasAvailable.includes(EquipmentSlots.BACKPACK))
@ -141,7 +154,16 @@ export class BotLootGenerator
// Add randomly generated weapon to PMC backpacks // Add randomly generated weapon to PMC backpacks
if (isPmc && this.randomUtil.getChance100(this.pmcConfig.looseWeaponInBackpackChancePercent)) if (isPmc && this.randomUtil.getChance100(this.pmcConfig.looseWeaponInBackpackChancePercent))
{ {
this.addLooseWeaponsToInventorySlot(sessionId, botInventory, EquipmentSlots.BACKPACK, botJsonTemplate.inventory, botJsonTemplate.chances.mods, botRole, isPmc, botLevel); this.addLooseWeaponsToInventorySlot(
sessionId,
botInventory,
EquipmentSlots.BACKPACK,
botJsonTemplate.inventory,
botJsonTemplate.chances.mods,
botRole,
isPmc,
botLevel,
);
} }
this.addLootFromPool( this.addLootFromPool(
@ -152,7 +174,8 @@ export class BotLootGenerator
botRole, botRole,
true, true,
this.pmcConfig.maxBackpackLootTotalRub, this.pmcConfig.maxBackpackLootTotalRub,
isPmc); isPmc,
);
} }
// TacticalVest - generate loot if they have one // TacticalVest - generate loot if they have one
@ -167,10 +190,10 @@ export class BotLootGenerator
botRole, botRole,
true, true,
this.pmcConfig.maxVestLootTotalRub, this.pmcConfig.maxVestLootTotalRub,
isPmc); isPmc,
);
} }
// Pockets // Pockets
this.addLootFromPool( this.addLootFromPool(
this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.POCKET, botJsonTemplate), this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.POCKET, botJsonTemplate),
@ -180,7 +203,8 @@ export class BotLootGenerator
botRole, botRole,
true, true,
this.pmcConfig.maxPocketLootTotalRub, this.pmcConfig.maxPocketLootTotalRub,
isPmc); isPmc,
);
} }
/** /**
@ -192,13 +216,12 @@ export class BotLootGenerator
{ {
const result = [EquipmentSlots.POCKETS]; const result = [EquipmentSlots.POCKETS];
if (botInventory.items.find(x => x.slotId === EquipmentSlots.TACTICAL_VEST)) if (botInventory.items.find((x) => x.slotId === EquipmentSlots.TACTICAL_VEST))
{ {
result.push(EquipmentSlots.TACTICAL_VEST); result.push(EquipmentSlots.TACTICAL_VEST);
} }
if (botInventory.items.find(x => x.slotId === EquipmentSlots.BACKPACK)) if (botInventory.items.find((x) => x.slotId === EquipmentSlots.BACKPACK))
{ {
result.push(EquipmentSlots.BACKPACK); result.push(EquipmentSlots.BACKPACK);
} }
@ -222,7 +245,8 @@ export class BotLootGenerator
botRole, botRole,
false, false,
0, 0,
true); true,
);
const surv12 = this.itemHelper.getItem("5d02797c86f774203f38e30a")[1]; const surv12 = this.itemHelper.getItem("5d02797c86f774203f38e30a")[1];
this.addLootFromPool( this.addLootFromPool(
@ -233,7 +257,8 @@ export class BotLootGenerator
botRole, botRole,
false, false,
0, 0,
true); true,
);
const morphine = this.itemHelper.getItem("544fb3f34bdc2d03748b456a")[1]; const morphine = this.itemHelper.getItem("544fb3f34bdc2d03748b456a")[1];
this.addLootFromPool( this.addLootFromPool(
@ -244,7 +269,8 @@ export class BotLootGenerator
botRole, botRole,
false, false,
0, 0,
true); true,
);
const afak = this.itemHelper.getItem("60098ad7c2240c0fe85c570a")[1]; const afak = this.itemHelper.getItem("60098ad7c2240c0fe85c570a")[1];
this.addLootFromPool( this.addLootFromPool(
@ -255,7 +281,8 @@ export class BotLootGenerator
botRole, botRole,
false, false,
0, 0,
true); true,
);
} }
/** /**
@ -290,14 +317,15 @@ export class BotLootGenerator
botRole: string, botRole: string,
useLimits = false, useLimits = false,
totalValueLimitRub = 0, totalValueLimitRub = 0,
isPmc = false): void isPmc = false,
): void
{ {
// Loot pool has items // Loot pool has items
if (pool.length) if (pool.length)
{ {
let currentTotalRub = 0; let currentTotalRub = 0;
const itemLimits: Record<string, number> = {}; const itemLimits: Record<string, number> = {};
const itemSpawnLimits: Record<string,Record<string, number>> = {}; const itemSpawnLimits: Record<string, Record<string, number>> = {};
let fitItemIntoContainerAttempts = 0; let fitItemIntoContainerAttempts = 0;
for (let i = 0; i < totalItemCount; i++) for (let i = 0; i < totalItemCount; i++)
{ {
@ -306,7 +334,7 @@ export class BotLootGenerator
const itemsToAdd: Item[] = [{ const itemsToAdd: Item[] = [{
_id: id, _id: id,
_tpl: itemToAddTemplate._id, _tpl: itemToAddTemplate._id,
...this.botGeneratorHelper.generateExtraPropertiesForItem(itemToAddTemplate, botRole) ...this.botGeneratorHelper.generateExtraPropertiesForItem(itemToAddTemplate, botRole),
}]; }];
if (useLimits) if (useLimits)
@ -321,7 +349,15 @@ export class BotLootGenerator
itemSpawnLimits[botRole] = this.getItemSpawnLimitsForBotType(isPmc, botRole); itemSpawnLimits[botRole] = this.getItemSpawnLimitsForBotType(isPmc, botRole);
} }
if (this.itemHasReachedSpawnLimit(itemToAddTemplate, botRole, isPmc, itemLimits, itemSpawnLimits[botRole])) if (
this.itemHasReachedSpawnLimit(
itemToAddTemplate,
botRole,
isPmc,
itemLimits,
itemSpawnLimits[botRole],
)
)
{ {
i--; i--;
continue; continue;
@ -345,13 +381,21 @@ export class BotLootGenerator
} }
// Attempt to add item to container(s) // Attempt to add item to container(s)
const itemAddedResult = this.botWeaponGeneratorHelper.addItemWithChildrenToEquipmentSlot(equipmentSlots, id, itemToAddTemplate._id, itemsToAdd, inventoryToAddItemsTo); const itemAddedResult = this.botWeaponGeneratorHelper.addItemWithChildrenToEquipmentSlot(
equipmentSlots,
id,
itemToAddTemplate._id,
itemsToAdd,
inventoryToAddItemsTo,
);
if (itemAddedResult === ItemAddedResult.NO_SPACE) if (itemAddedResult === ItemAddedResult.NO_SPACE)
{ {
fitItemIntoContainerAttempts++; fitItemIntoContainerAttempts++;
if (fitItemIntoContainerAttempts >= 4) if (fitItemIntoContainerAttempts >= 4)
{ {
this.logger.debug(`Failed to place item ${i} of ${totalItemCount} item into ${botRole} container: ${equipmentSlots}, ${fitItemIntoContainerAttempts} times, skipping`); this.logger.debug(
`Failed to place item ${i} of ${totalItemCount} item into ${botRole} container: ${equipmentSlots}, ${fitItemIntoContainerAttempts} times, skipping`,
);
break; break;
} }
@ -383,16 +427,48 @@ export class BotLootGenerator
* @param botRole bots role .e.g. pmcBot * @param botRole bots role .e.g. pmcBot
* @param isPmc are we generating for a pmc * @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 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 chosenWeaponType = this.randomUtil.getArrayValue([
const randomisedWeaponCount = this.randomUtil.getInt(this.pmcConfig.looseWeaponInBackpackLootMinMax.min, this.pmcConfig.looseWeaponInBackpackLootMinMax.max); 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,
);
if (randomisedWeaponCount > 0) if (randomisedWeaponCount > 0)
{ {
for (let i = 0; i < randomisedWeaponCount; i++) for (let i = 0; i < randomisedWeaponCount; i++)
{ {
const generatedWeapon = this.botWeaponGenerator.generateRandomWeapon(sessionId, chosenWeaponType, templateInventory, botInventory.equipment, modChances, botRole, isPmc, botLevel); const generatedWeapon = this.botWeaponGenerator.generateRandomWeapon(
this.botWeaponGeneratorHelper.addItemWithChildrenToEquipmentSlot([equipmentSlot], generatedWeapon.weapon[0]._id, generatedWeapon.weapon[0]._tpl, [...generatedWeapon.weapon], botInventory); sessionId,
chosenWeaponType,
templateInventory,
botInventory.equipment,
modChances,
botRole,
isPmc,
botLevel,
);
this.botWeaponGeneratorHelper.addItemWithChildrenToEquipmentSlot(
[equipmentSlot],
generatedWeapon.weapon[0]._id,
generatedWeapon.weapon[0]._tpl,
[...generatedWeapon.weapon],
botInventory,
);
} }
} }
} }
@ -405,7 +481,12 @@ export class BotLootGenerator
*/ */
protected getRandomItemFromPoolByRole(pool: ITemplateItem[], botRole: string): ITemplateItem protected getRandomItemFromPoolByRole(pool: ITemplateItem[], botRole: string): ITemplateItem
{ {
const itemIndex = this.randomUtil.getBiasedRandomNumber(0, pool.length - 1, pool.length - 1, this.getBotLootNValueByRole(botRole)); const itemIndex = this.randomUtil.getBiasedRandomNumber(
0,
pool.length - 1,
pool.length - 1,
this.getBotLootNValueByRole(botRole),
);
return pool[itemIndex]; return pool[itemIndex];
} }
@ -453,7 +534,13 @@ export class BotLootGenerator
* @param itemSpawnLimits The limits this bot is allowed to have * @param itemSpawnLimits The limits this bot is allowed to have
* @returns true if item has reached spawn limit * @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 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 // PMCs and scavs have different sections of bot config for spawn limits
if (!!itemSpawnLimits && itemSpawnLimits.length === 0) if (!!itemSpawnLimits && itemSpawnLimits.length === 0)
@ -484,7 +571,13 @@ export class BotLootGenerator
// Prevent edge-case of small loot pools + code trying to add limited item over and over infinitely // Prevent edge-case of small loot pools + code trying to add limited item over and over infinitely
if (limitCount[idToCheckFor] > itemSpawnLimits[idToCheckFor] * 10) 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]})); this.logger.debug(
this.localisationService.getText("bot-item_spawn_limit_reached_skipping_item", {
botRole: botRole,
itemName: itemTemplate._name,
attempts: limitCount[idToCheckFor],
}),
);
return false; return false;
} }
@ -505,9 +598,9 @@ export class BotLootGenerator
{ {
// PMCs have a different stack max size // PMCs have a different stack max size
const minStackSize = itemTemplate._props.StackMinRandom; const minStackSize = itemTemplate._props.StackMinRandom;
const maxStackSize = (isPmc) const maxStackSize = isPmc ?
? this.pmcConfig.dynamicLoot.moneyStackLimits[itemTemplate._id] this.pmcConfig.dynamicLoot.moneyStackLimits[itemTemplate._id] :
: itemTemplate._props.StackMaxRandom; itemTemplate._props.StackMaxRandom;
const randomSize = this.randomUtil.getInt(minStackSize, maxStackSize); const randomSize = this.randomUtil.getInt(minStackSize, maxStackSize);
if (!moneyItem.upd) if (!moneyItem.upd)
@ -526,16 +619,16 @@ export class BotLootGenerator
*/ */
protected randomiseAmmoStackSize(isPmc: boolean, itemTemplate: ITemplateItem, ammoItem: Item): void protected randomiseAmmoStackSize(isPmc: boolean, itemTemplate: ITemplateItem, ammoItem: Item): void
{ {
const randomSize = itemTemplate._props.StackMaxSize === 1 const randomSize = itemTemplate._props.StackMaxSize === 1 ?
? 1 1 :
: this.randomUtil.getInt(itemTemplate._props.StackMinRandom, itemTemplate._props.StackMaxRandom); this.randomUtil.getInt(itemTemplate._props.StackMinRandom, itemTemplate._props.StackMaxRandom);
if (!ammoItem.upd) if (!ammoItem.upd)
{ {
ammoItem.upd = {}; ammoItem.upd = {};
} }
ammoItem.upd.StackObjectsCount = randomSize ; ammoItem.upd.StackObjectsCount = randomSize;
} }
/** /**
@ -557,7 +650,9 @@ export class BotLootGenerator
return 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)); this.logger.warning(
this.localisationService.getText("bot-unable_to_find_spawn_limits_fallback_to_defaults", botRole),
);
return this.botConfig.itemSpawnLimits["default"]; return this.botConfig.itemSpawnLimits["default"];
} }
@ -570,7 +665,6 @@ export class BotLootGenerator
*/ */
protected getMatchingIdFromSpawnLimits(itemTemplate: ITemplateItem, spawnLimits: Record<string, number>): string protected getMatchingIdFromSpawnLimits(itemTemplate: ITemplateItem, spawnLimits: Record<string, number>): string
{ {
if (itemTemplate._id in spawnLimits) if (itemTemplate._id in spawnLimits)
{ {
return itemTemplate._id; return itemTemplate._id;

View File

@ -51,7 +51,7 @@ export class BotWeaponGenerator
@inject("BotEquipmentModGenerator") protected botEquipmentModGenerator: BotEquipmentModGenerator, @inject("BotEquipmentModGenerator") protected botEquipmentModGenerator: BotEquipmentModGenerator,
@inject("LocalisationService") protected localisationService: LocalisationService, @inject("LocalisationService") protected localisationService: LocalisationService,
@inject("RepairService") protected repairService: RepairService, @inject("RepairService") protected repairService: RepairService,
@injectAll("InventoryMagGen") protected inventoryMagGenComponents: IInventoryMagGen[] @injectAll("InventoryMagGen") protected inventoryMagGenComponents: IInventoryMagGen[],
) )
{ {
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT); this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
@ -70,10 +70,29 @@ export class BotWeaponGenerator
* @param isPmc Is weapon generated for a pmc * @param isPmc Is weapon generated for a pmc
* @returns GenerateWeaponResult object * @returns GenerateWeaponResult object
*/ */
public generateRandomWeapon(sessionId: string, equipmentSlot: string, botTemplateInventory: Inventory, weaponParentId: string, modChances: ModsChances, botRole: string, isPmc: boolean, botLevel: number): GenerateWeaponResult public generateRandomWeapon(
sessionId: string,
equipmentSlot: string,
botTemplateInventory: Inventory,
weaponParentId: string,
modChances: ModsChances,
botRole: string,
isPmc: boolean,
botLevel: number,
): GenerateWeaponResult
{ {
const weaponTpl = this.pickWeightedWeaponTplFromPool(equipmentSlot, botTemplateInventory); const weaponTpl = this.pickWeightedWeaponTplFromPool(equipmentSlot, botTemplateInventory);
return this.generateWeaponByTpl(sessionId, weaponTpl, equipmentSlot, botTemplateInventory, weaponParentId, modChances, botRole, isPmc, botLevel); return this.generateWeaponByTpl(
sessionId,
weaponTpl,
equipmentSlot,
botTemplateInventory,
weaponParentId,
modChances,
botRole,
isPmc,
botLevel,
);
} }
/** /**
@ -99,8 +118,17 @@ export class BotWeaponGenerator
* @param isPmc Is weapon being generated for a pmc * @param isPmc Is weapon being generated for a pmc
* @returns GenerateWeaponResult object * @returns GenerateWeaponResult object
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars public generateWeaponByTpl(
public generateWeaponByTpl(sessionId: string, weaponTpl: string, equipmentSlot: string, botTemplateInventory: Inventory, weaponParentId: string, modChances: ModsChances, botRole: string, isPmc: boolean, botLevel: number): GenerateWeaponResult sessionId: string,
weaponTpl: string,
equipmentSlot: string,
botTemplateInventory: Inventory,
weaponParentId: string,
modChances: ModsChances,
botRole: string,
isPmc: boolean,
botLevel: number,
): GenerateWeaponResult
{ {
const modPool = botTemplateInventory.mods; const modPool = botTemplateInventory.mods;
const weaponItemTemplate = this.itemHelper.getItem(weaponTpl)[1]; const weaponItemTemplate = this.itemHelper.getItem(weaponTpl)[1];
@ -123,7 +151,13 @@ export class BotWeaponGenerator
const ammoTpl = this.getWeightedCompatibleAmmo(botTemplateInventory.Ammo, weaponItemTemplate); const ammoTpl = this.getWeightedCompatibleAmmo(botTemplateInventory.Ammo, weaponItemTemplate);
// Create with just base weapon item // Create with just base weapon item
let weaponWithModsArray = this.constructWeaponBaseArray(weaponTpl, weaponParentId, equipmentSlot, weaponItemTemplate, botRole); let weaponWithModsArray = this.constructWeaponBaseArray(
weaponTpl,
weaponParentId,
equipmentSlot,
weaponItemTemplate,
botRole,
);
// Chance to add randomised weapon enhancement // Chance to add randomised weapon enhancement
if (isPmc && this.randomUtil.getChance100(this.pmcConfig.weaponHasEnhancementChancePercent)) if (isPmc && this.randomUtil.getChance100(this.pmcConfig.weaponHasEnhancementChancePercent))
@ -137,32 +171,52 @@ export class BotWeaponGenerator
{ {
const botEquipmentRole = this.botGeneratorHelper.getBotEquipmentRole(botRole); const botEquipmentRole = this.botGeneratorHelper.getBotEquipmentRole(botRole);
const modLimits = this.botWeaponModLimitService.getWeaponModLimits(botEquipmentRole); const modLimits = this.botWeaponModLimitService.getWeaponModLimits(botEquipmentRole);
weaponWithModsArray = this.botEquipmentModGenerator.generateModsForWeapon(sessionId, weaponWithModsArray, modPool, weaponWithModsArray[0]._id, weaponItemTemplate, modChances, ammoTpl, botRole, botLevel, modLimits, botEquipmentRole); weaponWithModsArray = this.botEquipmentModGenerator.generateModsForWeapon(
sessionId,
weaponWithModsArray,
modPool,
weaponWithModsArray[0]._id,
weaponItemTemplate,
modChances,
ammoTpl,
botRole,
botLevel,
modLimits,
botEquipmentRole,
);
} }
// Use weapon preset from globals.json if weapon isnt valid // Use weapon preset from globals.json if weapon isnt valid
if (!this.isWeaponValid(weaponWithModsArray, botRole)) if (!this.isWeaponValid(weaponWithModsArray, botRole))
{ {
// Weapon is bad, fall back to weapons preset // Weapon is bad, fall back to weapons preset
weaponWithModsArray = this.getPresetWeaponMods(weaponTpl, equipmentSlot, weaponParentId, weaponItemTemplate, botRole); weaponWithModsArray = this.getPresetWeaponMods(
weaponTpl,
equipmentSlot,
weaponParentId,
weaponItemTemplate,
botRole,
);
} }
// Fill existing magazines to full and sync ammo type // Fill existing magazines to full and sync ammo type
for (const magazine of weaponWithModsArray.filter(x => x.slotId === this.modMagazineSlotId)) for (const magazine of weaponWithModsArray.filter((x) => x.slotId === this.modMagazineSlotId))
{ {
this.fillExistingMagazines(weaponWithModsArray, magazine, ammoTpl); this.fillExistingMagazines(weaponWithModsArray, magazine, ammoTpl);
} }
// Add cartridge to gun chamber if weapon has slot for it // Add cartridge to gun chamber if weapon has slot for it
if (weaponItemTemplate._props.Chambers?.length === 1 if (
&& weaponItemTemplate._props.Chambers[0]?._name === "patron_in_weapon" weaponItemTemplate._props.Chambers?.length === 1 &&
&& weaponItemTemplate._props.Chambers[0]?._props?.filters[0]?.Filter?.includes(ammoTpl)) weaponItemTemplate._props.Chambers[0]?._name === "patron_in_weapon" &&
weaponItemTemplate._props.Chambers[0]?._props?.filters[0]?.Filter?.includes(ammoTpl)
)
{ {
this.addCartridgeToChamber(weaponWithModsArray, ammoTpl, "patron_in_weapon"); this.addCartridgeToChamber(weaponWithModsArray, ammoTpl, "patron_in_weapon");
} }
// Fill UBGL if found // Fill UBGL if found
const ubglMod = weaponWithModsArray.find(x => x.slotId === "mod_launcher"); const ubglMod = weaponWithModsArray.find((x) => x.slotId === "mod_launcher");
let ubglAmmoTpl: string = undefined; let ubglAmmoTpl: string = undefined;
if (ubglMod) if (ubglMod)
{ {
@ -176,7 +230,7 @@ export class BotWeaponGenerator
chosenAmmoTpl: ammoTpl, chosenAmmoTpl: ammoTpl,
chosenUbglAmmoTpl: ubglAmmoTpl, chosenUbglAmmoTpl: ubglAmmoTpl,
weaponMods: modPool, weaponMods: modPool,
weaponTemplate: weaponItemTemplate weaponTemplate: weaponItemTemplate,
}; };
} }
@ -189,7 +243,7 @@ export class BotWeaponGenerator
protected addCartridgeToChamber(weaponWithModsArray: Item[], ammoTpl: string, desiredSlotId: string): void protected addCartridgeToChamber(weaponWithModsArray: Item[], ammoTpl: string, desiredSlotId: string): void
{ {
// Check for slot first // Check for slot first
const existingItemWithSlot = weaponWithModsArray.find(x => x.slotId === desiredSlotId); const existingItemWithSlot = weaponWithModsArray.find((x) => x.slotId === desiredSlotId);
if (!existingItemWithSlot) if (!existingItemWithSlot)
{ {
// Not found, add fresh // Not found, add fresh
@ -198,14 +252,14 @@ export class BotWeaponGenerator
_tpl: ammoTpl, _tpl: ammoTpl,
parentId: weaponWithModsArray[0]._id, parentId: weaponWithModsArray[0]._id,
slotId: desiredSlotId, slotId: desiredSlotId,
upd: {StackObjectsCount: 1} upd: {StackObjectsCount: 1},
}); });
} }
else else
{ {
// Already exists, update values // Already exists, update values
existingItemWithSlot.upd = { existingItemWithSlot.upd = {
StackObjectsCount: 1 StackObjectsCount: 1,
}; };
existingItemWithSlot._tpl = ammoTpl; existingItemWithSlot._tpl = ammoTpl;
} }
@ -221,14 +275,20 @@ export class BotWeaponGenerator
* @param botRole for durability values * @param botRole for durability values
* @returns Base weapon item in array * @returns Base weapon item in array
*/ */
protected constructWeaponBaseArray(weaponTpl: string, weaponParentId: string, equipmentSlot: string, weaponItemTemplate: ITemplateItem, botRole: string): Item[] protected constructWeaponBaseArray(
weaponTpl: string,
weaponParentId: string,
equipmentSlot: string,
weaponItemTemplate: ITemplateItem,
botRole: string,
): Item[]
{ {
return [{ return [{
_id: this.hashUtil.generate(), _id: this.hashUtil.generate(),
_tpl: weaponTpl, _tpl: weaponTpl,
parentId: weaponParentId, parentId: weaponParentId,
slotId: equipmentSlot, slotId: equipmentSlot,
...this.botGeneratorHelper.generateExtraPropertiesForItem(weaponItemTemplate, botRole) ...this.botGeneratorHelper.generateExtraPropertiesForItem(weaponItemTemplate, botRole),
}]; }];
} }
@ -239,10 +299,18 @@ export class BotWeaponGenerator
* @param weaponParentId Value used for the parentid * @param weaponParentId Value used for the parentid
* @returns array of weapon mods * @returns array of weapon mods
*/ */
protected getPresetWeaponMods(weaponTpl: string, equipmentSlot: string, weaponParentId: string, itemTemplate: ITemplateItem, botRole: string): Item[] protected getPresetWeaponMods(
weaponTpl: string,
equipmentSlot: string,
weaponParentId: string,
itemTemplate: ITemplateItem,
botRole: string,
): Item[]
{ {
// Invalid weapon generated, fallback to preset // Invalid weapon generated, fallback to preset
this.logger.warning(this.localisationService.getText("bot-weapon_generated_incorrect_using_default", weaponTpl)); this.logger.warning(
this.localisationService.getText("bot-weapon_generated_incorrect_using_default", weaponTpl),
);
const weaponMods = []; const weaponMods = [];
// TODO: Right now, preset weapons trigger a lot of warnings regarding missing ammo in magazines & such // TODO: Right now, preset weapons trigger a lot of warnings regarding missing ammo in magazines & such
@ -260,11 +328,12 @@ export class BotWeaponGenerator
{ {
const parentItem = preset._items[0]; const parentItem = preset._items[0];
preset._items[0] = { preset._items[0] = {
...parentItem, ...{ ...parentItem,
...{
parentId: weaponParentId, parentId: weaponParentId,
slotId: equipmentSlot, slotId: equipmentSlot,
...this.botGeneratorHelper.generateExtraPropertiesForItem(itemTemplate, botRole) ...this.botGeneratorHelper.generateExtraPropertiesForItem(itemTemplate, botRole),
} },
}; };
weaponMods.push(...preset._items); weaponMods.push(...preset._items);
} }
@ -304,17 +373,30 @@ export class BotWeaponGenerator
const allowedTpls = modSlot._props.filters[0].Filter; const allowedTpls = modSlot._props.filters[0].Filter;
const slotName = modSlot._name; const slotName = modSlot._name;
const weaponSlotItem = weaponItemArray.find(x => x.parentId === mod._id && x.slotId === slotName); const weaponSlotItem = weaponItemArray.find((x) => x.parentId === mod._id && x.slotId === slotName);
if (!weaponSlotItem) if (!weaponSlotItem)
{ {
this.logger.warning(this.localisationService.getText("bot-weapons_required_slot_missing_item", {modSlot: modSlot._name, modName: modDbTemplate._name, slotId: mod.slotId, botRole: botRole})); this.logger.warning(
this.localisationService.getText("bot-weapons_required_slot_missing_item", {
modSlot: modSlot._name,
modName: modDbTemplate._name,
slotId: mod.slotId,
botRole: botRole,
}),
);
return false; return false;
} }
if (!allowedTpls.includes(weaponSlotItem._tpl)) if (!allowedTpls.includes(weaponSlotItem._tpl))
{ {
this.logger.warning(this.localisationService.getText("bot-weapon_contains_invalid_item", {modSlot: modSlot._name, modName: modDbTemplate._name, weaponTpl: weaponSlotItem._tpl})); this.logger.warning(
this.localisationService.getText("bot-weapon_contains_invalid_item", {
modSlot: modSlot._name,
modName: modDbTemplate._name,
weaponTpl: weaponSlotItem._tpl,
}),
);
return false; return false;
} }
@ -332,7 +414,12 @@ export class BotWeaponGenerator
* @param inventory Inventory to add magazines to * @param inventory Inventory to add magazines to
* @param botRole The bot type we're getting generating extra mags for * @param botRole The bot type we're getting generating extra mags for
*/ */
public addExtraMagazinesToInventory(generatedWeaponResult: GenerateWeaponResult, magWeights: GenerationData, inventory: PmcInventory, botRole: string): void public addExtraMagazinesToInventory(
generatedWeaponResult: GenerateWeaponResult,
magWeights: GenerationData,
inventory: PmcInventory,
botRole: string,
): void
{ {
const weaponAndMods = generatedWeaponResult.weapon; const weaponAndMods = generatedWeaponResult.weapon;
const weaponTemplate = generatedWeaponResult.weaponTemplate; const weaponTemplate = generatedWeaponResult.weaponTemplate;
@ -349,7 +436,9 @@ export class BotWeaponGenerator
const ammoTemplate = this.itemHelper.getItem(generatedWeaponResult.chosenAmmoTpl)[1]; const ammoTemplate = this.itemHelper.getItem(generatedWeaponResult.chosenAmmoTpl)[1];
if (!ammoTemplate) if (!ammoTemplate)
{ {
this.logger.error(this.localisationService.getText("bot-unable_to_find_ammo_item", generatedWeaponResult.chosenAmmoTpl)); this.logger.error(
this.localisationService.getText("bot-unable_to_find_ammo_item", generatedWeaponResult.chosenAmmoTpl),
);
return; return;
} }
@ -360,11 +449,24 @@ export class BotWeaponGenerator
this.addUbglGrenadesToBotInventory(weaponAndMods, generatedWeaponResult, inventory); this.addUbglGrenadesToBotInventory(weaponAndMods, generatedWeaponResult, inventory);
} }
const inventoryMagGenModel = new InventoryMagGen(magWeights, magTemplate, weaponTemplate, ammoTemplate, inventory); const inventoryMagGenModel = new InventoryMagGen(
this.inventoryMagGenComponents.find(v => v.canHandleInventoryMagGen(inventoryMagGenModel)).process(inventoryMagGenModel); magWeights,
magTemplate,
weaponTemplate,
ammoTemplate,
inventory,
);
this.inventoryMagGenComponents.find((v) => v.canHandleInventoryMagGen(inventoryMagGenModel)).process(
inventoryMagGenModel,
);
// Add x stacks of bullets to SecuredContainer (bots use a magic mag packing skill to reload instantly) // Add x stacks of bullets to SecuredContainer (bots use a magic mag packing skill to reload instantly)
this.addAmmoToSecureContainer(this.botConfig.secureContainerAmmoStackCount, generatedWeaponResult.chosenAmmoTpl, ammoTemplate._props.StackMaxSize, inventory); this.addAmmoToSecureContainer(
this.botConfig.secureContainerAmmoStackCount,
generatedWeaponResult.chosenAmmoTpl,
ammoTemplate._props.StackMaxSize,
inventory,
);
} }
/** /**
@ -373,25 +475,37 @@ export class BotWeaponGenerator
* @param generatedWeaponResult result of weapon generation * @param generatedWeaponResult result of weapon generation
* @param inventory bot inventory to add grenades to * @param inventory bot inventory to add grenades to
*/ */
protected addUbglGrenadesToBotInventory(weaponMods: Item[], generatedWeaponResult: GenerateWeaponResult, inventory: PmcInventory): void protected addUbglGrenadesToBotInventory(
weaponMods: Item[],
generatedWeaponResult: GenerateWeaponResult,
inventory: PmcInventory,
): void
{ {
// Find ubgl mod item + get details of it from db // Find ubgl mod item + get details of it from db
const ubglMod = weaponMods.find(x => x.slotId === "mod_launcher"); const ubglMod = weaponMods.find((x) => x.slotId === "mod_launcher");
const ubglDbTemplate = this.itemHelper.getItem(ubglMod._tpl)[1]; const ubglDbTemplate = this.itemHelper.getItem(ubglMod._tpl)[1];
// Define min/max of how many grenades bot will have // Define min/max of how many grenades bot will have
const ubglMinMax:GenerationData = { const ubglMinMax: GenerationData = {
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
weights: {"1": 1, "2": 1}, weights: {"1": 1, "2": 1},
whitelist: [] whitelist: [],
}; };
// get ammo template from db // get ammo template from db
const ubglAmmoDbTemplate = this.itemHelper.getItem(generatedWeaponResult.chosenUbglAmmoTpl)[1]; const ubglAmmoDbTemplate = this.itemHelper.getItem(generatedWeaponResult.chosenUbglAmmoTpl)[1];
// Add greandes to bot inventory // Add greandes to bot inventory
const ubglAmmoGenModel = new InventoryMagGen(ubglMinMax, ubglDbTemplate, ubglDbTemplate, ubglAmmoDbTemplate, inventory); const ubglAmmoGenModel = new InventoryMagGen(
this.inventoryMagGenComponents.find(v => v.canHandleInventoryMagGen(ubglAmmoGenModel)).process(ubglAmmoGenModel); ubglMinMax,
ubglDbTemplate,
ubglDbTemplate,
ubglAmmoDbTemplate,
inventory,
);
this.inventoryMagGenComponents.find((v) => v.canHandleInventoryMagGen(ubglAmmoGenModel)).process(
ubglAmmoGenModel,
);
// Store extra grenades in secure container // Store extra grenades in secure container
this.addAmmoToSecureContainer(5, generatedWeaponResult.chosenUbglAmmoTpl, 20, inventory); this.addAmmoToSecureContainer(5, generatedWeaponResult.chosenUbglAmmoTpl, 20, inventory);
@ -404,17 +518,27 @@ export class BotWeaponGenerator
* @param stackSize Size of the ammo stack to add * @param stackSize Size of the ammo stack to add
* @param inventory Player inventory * @param inventory Player inventory
*/ */
protected addAmmoToSecureContainer(stackCount: number, ammoTpl: string, stackSize: number, inventory: PmcInventory): void protected addAmmoToSecureContainer(
stackCount: number,
ammoTpl: string,
stackSize: number,
inventory: PmcInventory,
): void
{ {
for (let i = 0; i < stackCount; i++) for (let i = 0; i < stackCount; i++)
{ {
const id = this.hashUtil.generate(); const id = this.hashUtil.generate();
this.botWeaponGeneratorHelper.addItemWithChildrenToEquipmentSlot([EquipmentSlots.SECURED_CONTAINER], id, ammoTpl, [{ this.botWeaponGeneratorHelper.addItemWithChildrenToEquipmentSlot(
[EquipmentSlots.SECURED_CONTAINER],
id,
ammoTpl,
[{
_id: id, _id: id,
_tpl: ammoTpl, _tpl: ammoTpl,
upd: { StackObjectsCount: stackSize } upd: {StackObjectsCount: stackSize},
}], }],
inventory); inventory,
);
} }
} }
@ -425,9 +549,13 @@ export class BotWeaponGenerator
* @param botRole the bot type we are getting the magazine for * @param botRole the bot type we are getting the magazine for
* @returns magazine tpl string * @returns magazine tpl string
*/ */
protected getMagazineTplFromWeaponTemplate(weaponMods: Item[], weaponTemplate: ITemplateItem, botRole: string): string protected getMagazineTplFromWeaponTemplate(
weaponMods: Item[],
weaponTemplate: ITemplateItem,
botRole: string,
): string
{ {
const magazine = weaponMods.find(m => m.slotId === this.modMagazineSlotId); const magazine = weaponMods.find((m) => m.slotId === this.modMagazineSlotId);
if (!magazine) if (!magazine)
{ {
// Edge case - magazineless chamber loaded weapons dont have magazines, e.g. mp18 // Edge case - magazineless chamber loaded weapons dont have magazines, e.g. mp18
@ -441,11 +569,15 @@ export class BotWeaponGenerator
if (!weaponTemplate._props.isChamberLoad) if (!weaponTemplate._props.isChamberLoad)
{ {
// Shouldn't happen // Shouldn't happen
this.logger.warning(this.localisationService.getText("bot-weapon_missing_magazine_or_chamber", weaponTemplate._id)); this.logger.warning(
this.localisationService.getText("bot-weapon_missing_magazine_or_chamber", weaponTemplate._id),
);
} }
const defaultMagTplId = this.botWeaponGeneratorHelper.getWeaponsDefaultMagazineTpl(weaponTemplate); const defaultMagTplId = this.botWeaponGeneratorHelper.getWeaponsDefaultMagazineTpl(weaponTemplate);
this.logger.debug(`[${botRole}] Unable to find magazine for weapon ${weaponTemplate._id} ${weaponTemplate._name}, using mag template default ${defaultMagTplId}.`); this.logger.debug(
`[${botRole}] Unable to find magazine for weapon ${weaponTemplate._id} ${weaponTemplate._name}, using mag template default ${defaultMagTplId}.`,
);
return defaultMagTplId; return defaultMagTplId;
} }
@ -459,23 +591,42 @@ export class BotWeaponGenerator
* @param weaponTemplate the weapon we want to pick ammo for * @param weaponTemplate the weapon we want to pick ammo for
* @returns an ammo tpl that works with the desired gun * @returns an ammo tpl that works with the desired gun
*/ */
protected getWeightedCompatibleAmmo(ammo: Record<string, Record<string, number>>, weaponTemplate: ITemplateItem): string protected getWeightedCompatibleAmmo(
ammo: Record<string, Record<string, number>>,
weaponTemplate: ITemplateItem,
): string
{ {
const desiredCaliber = this.getWeaponCaliber(weaponTemplate); const desiredCaliber = this.getWeaponCaliber(weaponTemplate);
const compatibleCartridges = ammo[desiredCaliber]; const compatibleCartridges = ammo[desiredCaliber];
if (!compatibleCartridges || compatibleCartridges?.length === 0) if (!compatibleCartridges || compatibleCartridges?.length === 0)
{ {
this.logger.debug(this.localisationService.getText("bot-no_caliber_data_for_weapon_falling_back_to_default", {weaponId: weaponTemplate._id, weaponName: weaponTemplate._name, defaultAmmo: weaponTemplate._props.defAmmo})); this.logger.debug(
this.localisationService.getText("bot-no_caliber_data_for_weapon_falling_back_to_default", {
weaponId: weaponTemplate._id,
weaponName: weaponTemplate._name,
defaultAmmo: weaponTemplate._props.defAmmo,
}),
);
// Immediately returns, as default ammo is guaranteed to be compatible // Immediately returns, as default ammo is guaranteed to be compatible
return weaponTemplate._props.defAmmo; return weaponTemplate._props.defAmmo;
} }
const chosenAmmoTpl = this.weightedRandomHelper.getWeightedValue<string>(compatibleCartridges); const chosenAmmoTpl = this.weightedRandomHelper.getWeightedValue<string>(compatibleCartridges);
if (weaponTemplate._props.Chambers[0] && !weaponTemplate._props.Chambers[0]._props.filters[0].Filter.includes(chosenAmmoTpl)) if (
weaponTemplate._props.Chambers[0] &&
!weaponTemplate._props.Chambers[0]._props.filters[0].Filter.includes(chosenAmmoTpl)
)
{ {
this.logger.debug(this.localisationService.getText("bot-incompatible_ammo_for_weapon_falling_back_to_default", {chosenAmmo: chosenAmmoTpl, weaponId: weaponTemplate._id, weaponName: weaponTemplate._name, defaultAmmo: weaponTemplate._props.defAmmo})); this.logger.debug(
this.localisationService.getText("bot-incompatible_ammo_for_weapon_falling_back_to_default", {
chosenAmmo: chosenAmmoTpl,
weaponId: weaponTemplate._id,
weaponName: weaponTemplate._name,
defaultAmmo: weaponTemplate._props.defAmmo,
}),
);
// Incompatible ammo found, return default (can happen with .366 and 7.62x39 weapons) // Incompatible ammo found, return default (can happen with .366 and 7.62x39 weapons)
return weaponTemplate._props.defAmmo; return weaponTemplate._props.defAmmo;
@ -503,7 +654,9 @@ export class BotWeaponGenerator
if (weaponTemplate._props.LinkedWeapon) if (weaponTemplate._props.LinkedWeapon)
{ {
const ammoInChamber = this.itemHelper.getItem(weaponTemplate._props.Chambers[0]._props.filters[0].Filter[0]); const ammoInChamber = this.itemHelper.getItem(
weaponTemplate._props.Chambers[0]._props.filters[0].Filter[0],
);
if (!ammoInChamber[0]) if (!ammoInChamber[0])
{ {
return; return;
@ -559,9 +712,9 @@ export class BotWeaponGenerator
parentId: ubglMod._id, parentId: ubglMod._id,
slotId: "patron_in_weapon", slotId: "patron_in_weapon",
upd: { upd: {
StackObjectsCount: 1 StackObjectsCount: 1,
} },
} },
); );
} }
@ -573,9 +726,16 @@ export class BotWeaponGenerator
* @param newStackSize how many cartridges should go into the magazine * @param newStackSize how many cartridges should go into the magazine
* @param magazineTemplate magazines db template * @param magazineTemplate magazines db template
*/ */
protected addOrUpdateMagazinesChildWithAmmo(weaponWithMods: Item[], magazine: Item, chosenAmmoTpl: string, magazineTemplate: ITemplateItem): void protected addOrUpdateMagazinesChildWithAmmo(
weaponWithMods: Item[],
magazine: Item,
chosenAmmoTpl: string,
magazineTemplate: ITemplateItem,
): void
{ {
const magazineCartridgeChildItem = weaponWithMods.find(m => m.parentId === magazine._id && m.slotId === "cartridges"); const magazineCartridgeChildItem = weaponWithMods.find((m) =>
m.parentId === magazine._id && m.slotId === "cartridges"
);
if (magazineCartridgeChildItem) if (magazineCartridgeChildItem)
{ {
// Delete the existing cartridge object and create fresh below // Delete the existing cartridge object and create fresh below
@ -603,7 +763,7 @@ export class BotWeaponGenerator
// for CylinderMagazine we exchange the ammo in the "camoras". // for CylinderMagazine we exchange the ammo in the "camoras".
// This might not be necessary since we already filled the camoras with a random whitelisted and compatible ammo type, // This might not be necessary since we already filled the camoras with a random whitelisted and compatible ammo type,
// but I'm not sure whether this is also used elsewhere // but I'm not sure whether this is also used elsewhere
const camoras = weaponMods.filter(x => x.parentId === magazineId && x.slotId.startsWith("camora")); const camoras = weaponMods.filter((x) => x.parentId === magazineId && x.slotId.startsWith("camora"));
for (const camora of camoras) for (const camora of camoras)
{ {
camora._tpl = ammoTpl; camora._tpl = ammoTpl;
@ -613,7 +773,7 @@ export class BotWeaponGenerator
} }
else else
{ {
camora.upd = { StackObjectsCount: 1 }; camora.upd = {StackObjectsCount: 1};
} }
} }
} }

View File

@ -27,7 +27,7 @@ export class FenceBaseAssortGenerator
@inject("ItemHelper") protected itemHelper: ItemHelper, @inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("ItemFilterService") protected itemFilterService: ItemFilterService, @inject("ItemFilterService") protected itemFilterService: ItemFilterService,
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService, @inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService,
@inject("ConfigServer") protected configServer: ConfigServer @inject("ConfigServer") protected configServer: ConfigServer,
) )
{ {
this.traderConfig = this.configServer.getConfig(ConfigTypes.TRADER); this.traderConfig = this.configServer.getConfig(ConfigTypes.TRADER);
@ -43,7 +43,7 @@ export class FenceBaseAssortGenerator
const baseFenceAssort = this.databaseServer.getTables().traders[Traders.FENCE].assort; const baseFenceAssort = this.databaseServer.getTables().traders[Traders.FENCE].assort;
const dbItems = Object.values(this.databaseServer.getTables().templates.items); const dbItems = Object.values(this.databaseServer.getTables().templates.items);
for (const item of dbItems.filter(x => this.isValidFenceItem(x))) for (const item of dbItems.filter((x) => this.isValidFenceItem(x)))
{ {
// Skip blacklisted items // Skip blacklisted items
if (this.itemFilterService.isItemBlacklisted(item._id)) if (this.itemFilterService.isItemBlacklisted(item._id))
@ -65,8 +65,10 @@ export class FenceBaseAssortGenerator
// Skip items on fence ignore list // Skip items on fence ignore list
if (this.traderConfig.fence.blacklist.length > 0) if (this.traderConfig.fence.blacklist.length > 0)
{ {
if (this.traderConfig.fence.blacklist.includes(item._id) if (
|| this.itemHelper.isOfBaseclasses(item._id, this.traderConfig.fence.blacklist)) this.traderConfig.fence.blacklist.includes(item._id) ||
this.itemHelper.isOfBaseclasses(item._id, this.traderConfig.fence.blacklist)
)
{ {
continue; continue;
} }
@ -80,8 +82,10 @@ export class FenceBaseAssortGenerator
// Create barter scheme object // Create barter scheme object
const barterSchemeToAdd: IBarterScheme = { const barterSchemeToAdd: IBarterScheme = {
count: Math.round(this.handbookHelper.getTemplatePrice(item._id) * this.traderConfig.fence.itemPriceMult), count: Math.round(
_tpl: Money.ROUBLES this.handbookHelper.getTemplatePrice(item._id) * this.traderConfig.fence.itemPriceMult,
),
_tpl: Money.ROUBLES,
}; };
// Add barter data to base // Add barter data to base
@ -95,8 +99,8 @@ export class FenceBaseAssortGenerator
slotId: "hideout", slotId: "hideout",
upd: { upd: {
StackObjectsCount: 9999999, StackObjectsCount: 9999999,
UnlimitedCount: true UnlimitedCount: true,
} },
}; };
// Add item to base // Add item to base

View File

@ -6,9 +6,14 @@ import { PresetHelper } from "@spt-aki/helpers/PresetHelper";
import { RagfairServerHelper } from "@spt-aki/helpers/RagfairServerHelper"; import { RagfairServerHelper } from "@spt-aki/helpers/RagfairServerHelper";
import { IContainerMinMax, IStaticContainer } from "@spt-aki/models/eft/common/ILocation"; import { IContainerMinMax, IStaticContainer } from "@spt-aki/models/eft/common/ILocation";
import { ILocationBase } from "@spt-aki/models/eft/common/ILocationBase"; import { ILocationBase } from "@spt-aki/models/eft/common/ILocationBase";
import { ILooseLoot, Spawnpoint, SpawnpointTemplate, SpawnpointsForced } from "@spt-aki/models/eft/common/ILooseLoot"; import { ILooseLoot, Spawnpoint, SpawnpointsForced, SpawnpointTemplate } from "@spt-aki/models/eft/common/ILooseLoot";
import { Item } from "@spt-aki/models/eft/common/tables/IItem"; import { Item } from "@spt-aki/models/eft/common/tables/IItem";
import { IStaticAmmoDetails, IStaticContainerData, IStaticForcedProps, IStaticLootDetails } from "@spt-aki/models/eft/common/tables/ILootBase"; import {
IStaticAmmoDetails,
IStaticContainerData,
IStaticForcedProps,
IStaticLootDetails,
} from "@spt-aki/models/eft/common/tables/ILootBase";
import { BaseClasses } from "@spt-aki/models/enums/BaseClasses"; import { BaseClasses } from "@spt-aki/models/enums/BaseClasses";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { Money } from "@spt-aki/models/enums/Money"; import { Money } from "@spt-aki/models/enums/Money";
@ -25,17 +30,17 @@ import { ProbabilityObject, ProbabilityObjectArray, RandomUtil } from "@spt-aki/
export interface IContainerItem export interface IContainerItem
{ {
items: Item[] items: Item[];
width: number width: number;
height: number height: number;
} }
export interface IContainerGroupCount export interface IContainerGroupCount
{ {
/** Containers this group has + probabilty to spawn */ /** Containers this group has + probabilty to spawn */
containerIdsWithProbability: Record<string, number> containerIdsWithProbability: Record<string, number>;
/** How many containers the map should spawn with this group id */ /** How many containers the map should spawn with this group id */
chosenCount: number chosenCount: number;
} }
@injectable() @injectable()
@ -56,7 +61,7 @@ export class LocationGenerator
@inject("ContainerHelper") protected containerHelper: ContainerHelper, @inject("ContainerHelper") protected containerHelper: ContainerHelper,
@inject("PresetHelper") protected presetHelper: PresetHelper, @inject("PresetHelper") protected presetHelper: PresetHelper,
@inject("LocalisationService") protected localisationService: LocalisationService, @inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ConfigServer") protected configServer: ConfigServer @inject("ConfigServer") protected configServer: ConfigServer,
) )
{ {
this.locationConfig = this.configServer.getConfig(ConfigTypes.LOCATION); this.locationConfig = this.configServer.getConfig(ConfigTypes.LOCATION);
@ -68,7 +73,10 @@ export class LocationGenerator
* @param staticAmmoDist Static ammo distribution - database.loot.staticAmmo * @param staticAmmoDist Static ammo distribution - database.loot.staticAmmo
* @returns Array of container objects * @returns Array of container objects
*/ */
public generateStaticContainers(locationBase: ILocationBase, staticAmmoDist: Record<string, IStaticAmmoDetails[]>): SpawnpointTemplate[] public generateStaticContainers(
locationBase: ILocationBase,
staticAmmoDist: Record<string, IStaticAmmoDetails[]>,
): SpawnpointTemplate[]
{ {
const result: SpawnpointTemplate[] = []; const result: SpawnpointTemplate[] = [];
const locationId = locationBase.Id.toLowerCase(); const locationId = locationBase.Id.toLowerCase();
@ -84,7 +92,9 @@ export class LocationGenerator
// Add mounted weapons to output loot // Add mounted weapons to output loot
result.push(...staticWeaponsOnMap ?? []); result.push(...staticWeaponsOnMap ?? []);
const allStaticContainersOnMap = this.jsonUtil.clone(db.loot.staticContainers[locationBase.Name]?.staticContainers); const allStaticContainersOnMap = this.jsonUtil.clone(
db.loot.staticContainers[locationBase.Name]?.staticContainers,
);
if (!allStaticContainersOnMap) if (!allStaticContainersOnMap)
{ {
this.logger.error(`Unable to find static container data for map: ${locationBase.Name}`); this.logger.error(`Unable to find static container data for map: ${locationBase.Name}`);
@ -109,19 +119,36 @@ export class LocationGenerator
// Add loot to guaranteed containers and add to result // Add loot to guaranteed containers and add to result
for (const container of guaranteedContainers) for (const container of guaranteedContainers)
{ {
const containerWithLoot = this.addLootToContainer(container, staticForcedOnMap, staticLootDist, staticAmmoDist, locationId); const containerWithLoot = this.addLootToContainer(
container,
staticForcedOnMap,
staticLootDist,
staticAmmoDist,
locationId,
);
result.push(containerWithLoot.template); result.push(containerWithLoot.template);
} }
this.logger.success(`Added ${guaranteedContainers.length} guaranteed containers`); this.logger.success(`Added ${guaranteedContainers.length} guaranteed containers`);
// randomisation is turned off globally or just turned off for this map // randomisation is turned off globally or just turned off for this map
if (!this.locationConfig.containerRandomisationSettings.enabled || !this.locationConfig.containerRandomisationSettings.maps[locationId]) if (
!this.locationConfig.containerRandomisationSettings.enabled ||
!this.locationConfig.containerRandomisationSettings.maps[locationId]
)
{ {
this.logger.debug(`Container randomisation disabled, Adding ${staticRandomisableContainersOnMap.length} containers to ${locationBase.Name}`); this.logger.debug(
`Container randomisation disabled, Adding ${staticRandomisableContainersOnMap.length} containers to ${locationBase.Name}`,
);
for (const container of staticRandomisableContainersOnMap) for (const container of staticRandomisableContainersOnMap)
{ {
const containerWithLoot = this.addLootToContainer(container, staticForcedOnMap, staticLootDist, staticAmmoDist, locationId); const containerWithLoot = this.addLootToContainer(
container,
staticForcedOnMap,
staticLootDist,
staticAmmoDist,
locationId,
);
result.push(containerWithLoot.template); result.push(containerWithLoot.template);
} }
@ -145,7 +172,9 @@ export class LocationGenerator
if (Object.keys(data.containerIdsWithProbability).length === 0) if (Object.keys(data.containerIdsWithProbability).length === 0)
{ {
this.logger.debug(`Group: ${groupId} has no containers with < 100% spawn chance to choose from, skipping`); this.logger.debug(
`Group: ${groupId} has no containers with < 100% spawn chance to choose from, skipping`,
);
continue; continue;
} }
@ -178,21 +207,35 @@ export class LocationGenerator
for (const chosenContainerId of chosenContainerIds) for (const chosenContainerId of chosenContainerIds)
{ {
// Look up container object from full list of containers on map // Look up container object from full list of containers on map
const containerObject = staticRandomisableContainersOnMap.find(x => x.template.Id === chosenContainerId); const containerObject = staticRandomisableContainersOnMap.find((x) =>
x.template.Id === chosenContainerId
);
if (!containerObject) if (!containerObject)
{ {
this.logger.debug(`Container: ${chosenContainerIds[chosenContainerId]} not found in staticRandomisableContainersOnMap, this is bad`); this.logger.debug(
`Container: ${
chosenContainerIds[chosenContainerId]
} not found in staticRandomisableContainersOnMap, this is bad`,
);
continue; continue;
} }
// Add loot to container and push into result object // Add loot to container and push into result object
const containerWithLoot = this.addLootToContainer(containerObject, staticForcedOnMap, staticLootDist, staticAmmoDist, locationId); const containerWithLoot = this.addLootToContainer(
containerObject,
staticForcedOnMap,
staticLootDist,
staticAmmoDist,
locationId,
);
result.push(containerWithLoot.template); result.push(containerWithLoot.template);
staticContainerCount++; staticContainerCount++;
} }
} }
this.logger.success(this.localisationService.getText("location-containers_generated_success", staticContainerCount)); this.logger.success(
this.localisationService.getText("location-containers_generated_success", staticContainerCount),
);
return result; return result;
} }
@ -205,7 +248,12 @@ export class LocationGenerator
protected getRandomisableContainersOnMap(staticContainers: IStaticContainerData[]): IStaticContainerData[] protected getRandomisableContainersOnMap(staticContainers: IStaticContainerData[]): IStaticContainerData[]
{ {
return staticContainers return staticContainers
.filter(x => x.probability !== 1 && !x.template.IsAlwaysSpawn && !this.locationConfig.containerRandomisationSettings.containerTypesToNotRandomise.includes(x.template.Items[0]._tpl)); .filter((x) =>
x.probability !== 1 && !x.template.IsAlwaysSpawn &&
!this.locationConfig.containerRandomisationSettings.containerTypesToNotRandomise.includes(
x.template.Items[0]._tpl,
)
);
} }
/** /**
@ -215,11 +263,16 @@ export class LocationGenerator
*/ */
protected getGuaranteedContainers(staticContainersOnMap: IStaticContainerData[]): IStaticContainerData[] protected getGuaranteedContainers(staticContainersOnMap: IStaticContainerData[]): IStaticContainerData[]
{ {
return staticContainersOnMap.filter(x => x.probability === 1 || x.template.IsAlwaysSpawn || this.locationConfig.containerRandomisationSettings.containerTypesToNotRandomise.includes(x.template.Items[0]._tpl)); return staticContainersOnMap.filter((x) =>
x.probability === 1 || x.template.IsAlwaysSpawn ||
this.locationConfig.containerRandomisationSettings.containerTypesToNotRandomise.includes(
x.template.Items[0]._tpl,
)
);
} }
/** /**
* Choose a number of containers based on their probabilty value to fulfil the desired count in containerData.chosenCount * Choose a number of containers based on their probability value to fulfil the desired count in containerData.chosenCount
* @param groupId Name of the group the containers are being collected for * @param groupId Name of the group the containers are being collected for
* @param containerData Containers and probability values for a groupId * @param containerData Containers and probability values for a groupId
* @returns List of chosen container Ids * @returns List of chosen container Ids
@ -231,15 +284,19 @@ export class LocationGenerator
const containerIds = Object.keys(containerData.containerIdsWithProbability); const containerIds = Object.keys(containerData.containerIdsWithProbability);
if (containerData.chosenCount > containerIds.length) if (containerData.chosenCount > containerIds.length)
{ {
this.logger.debug(`Group: ${groupId} wants ${containerData.chosenCount} containers but pool only has ${containerIds.length}, add what's available`); this.logger.debug(
`Group: ${groupId} wants ${containerData.chosenCount} containers but pool only has ${containerIds.length}, add what's available`,
);
return containerIds; return containerIds;
} }
// Create probability array with all possible container ids in this group and their relataive probability of spawning // Create probability array with all possible container ids in this group and their relative probability of spawning
const containerDistribution = new ProbabilityObjectArray<string>(this.mathUtil, this.jsonUtil); const containerDistribution = new ProbabilityObjectArray<string>(this.mathUtil, this.jsonUtil);
for (const containerId of containerIds) for (const containerId of containerIds)
{ {
containerDistribution.push(new ProbabilityObject(containerId, containerData.containerIdsWithProbability[containerId])); containerDistribution.push(
new ProbabilityObject(containerId, containerData.containerIdsWithProbability[containerId]),
);
} }
chosenContainerIds.push(...containerDistribution.draw(containerData.chosenCount)); chosenContainerIds.push(...containerDistribution.draw(containerData.chosenCount));
@ -254,7 +311,8 @@ export class LocationGenerator
*/ */
protected getGroupIdToContainerMappings( protected getGroupIdToContainerMappings(
staticContainerGroupData: IStaticContainer | Record<string, IContainerMinMax>, staticContainerGroupData: IStaticContainer | Record<string, IContainerMinMax>,
staticContainersOnMap: IStaticContainerData[]): Record<string, IContainerGroupCount> staticContainersOnMap: IStaticContainerData[],
): Record<string, IContainerGroupCount>
{ {
// Create dictionary of all group ids and choose a count of containers the map will spawn of that group // Create dictionary of all group ids and choose a count of containers the map will spawn of that group
const mapping: Record<string, IContainerGroupCount> = {}; const mapping: Record<string, IContainerGroupCount> = {};
@ -266,9 +324,15 @@ export class LocationGenerator
mapping[groupId] = { mapping[groupId] = {
containerIdsWithProbability: {}, containerIdsWithProbability: {},
chosenCount: this.randomUtil.getInt( chosenCount: this.randomUtil.getInt(
Math.round(groupData.minContainers * this.locationConfig.containerRandomisationSettings.containerGroupMinSizeMultiplier), Math.round(
Math.round(groupData.maxContainers * this.locationConfig.containerRandomisationSettings.containerGroupMaxSizeMultiplier) groupData.minContainers *
) this.locationConfig.containerRandomisationSettings.containerGroupMinSizeMultiplier,
),
Math.round(
groupData.maxContainers *
this.locationConfig.containerRandomisationSettings.containerGroupMaxSizeMultiplier,
),
),
}; };
} }
} }
@ -290,7 +354,9 @@ export class LocationGenerator
if (container.probability === 1) if (container.probability === 1)
{ {
this.logger.debug(`Container ${container.template.Id} with group ${groupData.groupId} had 100% chance to spawn was picked as random container, skipping`); this.logger.debug(
`Container ${container.template.Id} with group ${groupData.groupId} had 100% chance to spawn was picked as random container, skipping`,
);
continue; continue;
} }
mapping[groupData.groupId].containerIdsWithProbability[container.template.Id] = container.probability; mapping[groupData.groupId].containerIdsWithProbability[container.template.Id] = container.probability;
@ -314,7 +380,8 @@ export class LocationGenerator
staticForced: IStaticForcedProps[], staticForced: IStaticForcedProps[],
staticLootDist: Record<string, IStaticLootDetails>, staticLootDist: Record<string, IStaticLootDetails>,
staticAmmoDist: Record<string, IStaticAmmoDetails[]>, staticAmmoDist: Record<string, IStaticAmmoDetails[]>,
locationName: string): IStaticContainerData locationName: string,
): IStaticContainerData
{ {
const container = this.jsonUtil.clone(staticContainer); const container = this.jsonUtil.clone(staticContainer);
const containerTpl = container.template.Items[0]._tpl; const containerTpl = container.template.Items[0]._tpl;
@ -333,7 +400,7 @@ export class LocationGenerator
const containerLootPool = this.getPossibleLootItemsForContainer(containerTpl, staticLootDist); const containerLootPool = this.getPossibleLootItemsForContainer(containerTpl, staticLootDist);
// Some containers need to have items forced into it (quest keys etc) // Some containers need to have items forced into it (quest keys etc)
const tplsForced = staticForced.filter(x => x.containerId === container.template.Id).map(x => x.itemTpl); const tplsForced = staticForced.filter((x) => x.containerId === container.template.Id).map((x) => x.itemTpl);
// Draw random loot // Draw random loot
// Money spawn more than once in container // Money spawn more than once in container
@ -341,7 +408,11 @@ export class LocationGenerator
const locklist = [Money.ROUBLES, Money.DOLLARS, Money.EUROS]; const locklist = [Money.ROUBLES, Money.DOLLARS, Money.EUROS];
// Choose items to add to container, factor in weighting + lock money down // Choose items to add to container, factor in weighting + lock money down
const chosenTpls = containerLootPool.draw(itemCountToAdd, this.locationConfig.allowDuplicateItemsInStaticContainers, locklist); const chosenTpls = containerLootPool.draw(
itemCountToAdd,
this.locationConfig.allowDuplicateItemsInStaticContainers,
locklist,
);
// Add forced loot to chosen item pool // Add forced loot to chosen item pool
const tplsToAddToContainer = tplsForced.concat(chosenTpls); const tplsToAddToContainer = tplsForced.concat(chosenTpls);
@ -368,11 +439,18 @@ export class LocationGenerator
continue; continue;
} }
containerMap = this.containerHelper.fillContainerMapWithItem(containerMap, result.x, result.y, width, height, result.rotation); containerMap = this.containerHelper.fillContainerMapWithItem(
containerMap,
result.x,
result.y,
width,
height,
result.rotation,
);
const rotation = result.rotation ? 1 : 0; const rotation = result.rotation ? 1 : 0;
items[0].slotId = "main"; items[0].slotId = "main";
items[0].location = { x: result.x, y: result.y, r: rotation }; items[0].location = {x: result.x, y: result.y, r: rotation};
// Add loot to container before returning // Add loot to container before returning
for (const item of items) for (const item of items)
@ -409,15 +487,19 @@ export class LocationGenerator
* @param locationName Map name (to get per-map multiplier for from config) * @param locationName Map name (to get per-map multiplier for from config)
* @returns item count * @returns item count
*/ */
protected getWeightedCountOfContainerItems(containerTypeId: string, staticLootDist: Record<string, IStaticLootDetails>, locationName: string): number protected getWeightedCountOfContainerItems(
containerTypeId: string,
staticLootDist: Record<string, IStaticLootDetails>,
locationName: string,
): number
{ {
// Create probability array to calcualte the total count of lootable items inside container // Create probability array to calculate the total count of lootable items inside container
const itemCountArray = new ProbabilityObjectArray<number>(this.mathUtil, this.jsonUtil); const itemCountArray = new ProbabilityObjectArray<number>(this.mathUtil, this.jsonUtil);
for (const itemCountDistribution of staticLootDist[containerTypeId].itemcountDistribution) for (const itemCountDistribution of staticLootDist[containerTypeId].itemcountDistribution)
{ {
// Add each count of items into array // Add each count of items into array
itemCountArray.push( itemCountArray.push(
new ProbabilityObject(itemCountDistribution.count, itemCountDistribution.relativeProbability) new ProbabilityObject(itemCountDistribution.count, itemCountDistribution.relativeProbability),
); );
} }
@ -427,11 +509,14 @@ export class LocationGenerator
/** /**
* Get all possible loot items that can be placed into a container * Get all possible loot items that can be placed into a container
* Do not add seasonal items if found + current date is inside seasonal event * Do not add seasonal items if found + current date is inside seasonal event
* @param containerTypeId Contianer to get possible loot for * @param containerTypeId Container to get possible loot for
* @param staticLootDist staticLoot.json * @param staticLootDist staticLoot.json
* @returns ProbabilityObjectArray of item tpls + probabilty * @returns ProbabilityObjectArray of item tpls + probability
*/ */
protected getPossibleLootItemsForContainer(containerTypeId: string, staticLootDist: Record<string, IStaticLootDetails>): ProbabilityObjectArray<string, number> protected getPossibleLootItemsForContainer(
containerTypeId: string,
staticLootDist: Record<string, IStaticLootDetails>,
): ProbabilityObjectArray<string, number>
{ {
const seasonalEventActive = this.seasonalEventService.seasonalEventEnabled(); const seasonalEventActive = this.seasonalEventService.seasonalEventEnabled();
const seasonalItemTplBlacklist = this.seasonalEventService.getInactiveSeasonalEventItems(); const seasonalItemTplBlacklist = this.seasonalEventService.getInactiveSeasonalEventItems();
@ -446,7 +531,7 @@ export class LocationGenerator
} }
itemDistribution.push( itemDistribution.push(
new ProbabilityObject(icd.tpl, icd.relativeProbability) new ProbabilityObject(icd.tpl, icd.relativeProbability),
); );
} }
@ -470,7 +555,11 @@ export class LocationGenerator
* @param locationName Location to generate loot for * @param locationName Location to generate loot for
* @returns Array of spawn points with loot in them * @returns Array of spawn points with loot in them
*/ */
public generateDynamicLoot(dynamicLootDist: ILooseLoot, staticAmmoDist: Record<string, IStaticAmmoDetails[]>, locationName: string): SpawnpointTemplate[] public generateDynamicLoot(
dynamicLootDist: ILooseLoot,
staticAmmoDist: Record<string, IStaticAmmoDetails[]>,
locationName: string,
): SpawnpointTemplate[]
{ {
const loot: SpawnpointTemplate[] = []; const loot: SpawnpointTemplate[] = [];
@ -479,13 +568,13 @@ export class LocationGenerator
const allDynamicSpawnpoints = dynamicLootDist.spawnpoints; const allDynamicSpawnpoints = dynamicLootDist.spawnpoints;
//Draw from random distribution // Draw from random distribution
const desiredSpawnpointCount = Math.round( const desiredSpawnpointCount = Math.round(
this.getLooseLootMultiplerForLocation(locationName) * this.getLooseLootMultiplerForLocation(locationName) *
this.randomUtil.randn( this.randomUtil.randn(
dynamicLootDist.spawnpointCount.mean, dynamicLootDist.spawnpointCount.mean,
dynamicLootDist.spawnpointCount.std dynamicLootDist.spawnpointCount.std,
) ),
); );
// Positions not in forced but have 100% chance to spawn // Positions not in forced but have 100% chance to spawn
@ -496,7 +585,7 @@ export class LocationGenerator
for (const spawnpoint of allDynamicSpawnpoints) for (const spawnpoint of allDynamicSpawnpoints)
{ {
// Point is blacklsited, skip // Point is blacklisted, skip
if (blacklistedSpawnpoints?.includes(spawnpoint.template.Id)) if (blacklistedSpawnpoints?.includes(spawnpoint.template.Id))
{ {
this.logger.debug(`Ignoring loose loot location: ${spawnpoint.template.Id}`); this.logger.debug(`Ignoring loose loot location: ${spawnpoint.template.Id}`);
@ -510,7 +599,7 @@ export class LocationGenerator
} }
spawnpointArray.push( spawnpointArray.push(
new ProbabilityObject(spawnpoint.template.Id, spawnpoint.probability, spawnpoint) new ProbabilityObject(spawnpoint.template.Id, spawnpoint.probability, spawnpoint),
); );
} }
@ -525,13 +614,19 @@ export class LocationGenerator
} }
// Filter out duplicate locationIds // Filter out duplicate locationIds
chosenSpawnpoints = [...new Map(chosenSpawnpoints.map(x => [x.locationId, x])).values()]; chosenSpawnpoints = [...new Map(chosenSpawnpoints.map((x) => [x.locationId, x])).values()];
// Do we have enough items in pool to fulfill requirement // Do we have enough items in pool to fulfill requirement
const tooManySpawnPointsRequested = (desiredSpawnpointCount - chosenSpawnpoints.length) > 0; const tooManySpawnPointsRequested = (desiredSpawnpointCount - chosenSpawnpoints.length) > 0;
if (tooManySpawnPointsRequested) if (tooManySpawnPointsRequested)
{ {
this.logger.debug(this.localisationService.getText("location-spawn_point_count_requested_vs_found", {requested: desiredSpawnpointCount+guaranteedLoosePoints.length, found: chosenSpawnpoints.length, mapName: locationName})); this.logger.debug(
this.localisationService.getText("location-spawn_point_count_requested_vs_found", {
requested: desiredSpawnpointCount + guaranteedLoosePoints.length,
found: chosenSpawnpoints.length,
mapName: locationName,
}),
);
} }
// Iterate over spawnpoints // Iterate over spawnpoints
@ -541,14 +636,18 @@ export class LocationGenerator
{ {
if (!spawnPoint.template) if (!spawnPoint.template)
{ {
this.logger.warning(this.localisationService.getText("location-missing_dynamic_template", spawnPoint.locationId)); this.logger.warning(
this.localisationService.getText("location-missing_dynamic_template", spawnPoint.locationId),
);
continue; continue;
} }
if (!spawnPoint.template.Items || spawnPoint.template.Items.length === 0) if (!spawnPoint.template.Items || spawnPoint.template.Items.length === 0)
{ {
this.logger.error(this.localisationService.getText("location-spawnpoint_missing_items", spawnPoint.template.Id)); this.logger.error(
this.localisationService.getText("location-spawnpoint_missing_items", spawnPoint.template.Id),
);
continue; continue;
} }
@ -556,14 +655,18 @@ export class LocationGenerator
const itemArray = new ProbabilityObjectArray<string>(this.mathUtil, this.jsonUtil); const itemArray = new ProbabilityObjectArray<string>(this.mathUtil, this.jsonUtil);
for (const itemDist of spawnPoint.itemDistribution) for (const itemDist of spawnPoint.itemDistribution)
{ {
if (!seasonalEventActive && seasonalItemTplBlacklist.includes(spawnPoint.template.Items.find(x => x._id === itemDist.composedKey.key)._tpl)) if (
!seasonalEventActive && seasonalItemTplBlacklist.includes(
spawnPoint.template.Items.find((x) => x._id === itemDist.composedKey.key)._tpl,
)
)
{ {
// Skip seasonal event items if they're not enabled // Skip seasonal event items if they're not enabled
continue; continue;
} }
itemArray.push( itemArray.push(
new ProbabilityObject(itemDist.composedKey.key, itemDist.relativeProbability) new ProbabilityObject(itemDist.composedKey.key, itemDist.relativeProbability),
); );
} }
@ -587,7 +690,11 @@ export class LocationGenerator
* @param forcedSpawnPoints forced loot to add * @param forcedSpawnPoints forced loot to add
* @param name of map currently generating forced loot for * @param name of map currently generating forced loot for
*/ */
protected addForcedLoot(loot: SpawnpointTemplate[], forcedSpawnPoints: SpawnpointsForced[], locationName: string): void protected addForcedLoot(
loot: SpawnpointTemplate[],
forcedSpawnPoints: SpawnpointsForced[],
locationName: string,
): void
{ {
const lootToForceSingleAmountOnMap = this.locationConfig.forcedLootSingleSpawnById[locationName]; const lootToForceSingleAmountOnMap = this.locationConfig.forcedLootSingleSpawnById[locationName];
if (lootToForceSingleAmountOnMap) if (lootToForceSingleAmountOnMap)
@ -596,27 +703,32 @@ export class LocationGenerator
for (const itemTpl of lootToForceSingleAmountOnMap) for (const itemTpl of lootToForceSingleAmountOnMap)
{ {
// Get all spawn positions for item tpl in forced loot array // Get all spawn positions for item tpl in forced loot array
const items = forcedSpawnPoints.filter(x => x.template.Items[0]._tpl === itemTpl); const items = forcedSpawnPoints.filter((x) => x.template.Items[0]._tpl === itemTpl);
if (!items || items.length === 0) if (!items || items.length === 0)
{ {
this.logger.debug(`Unable to adjust loot item ${itemTpl} as it does not exist inside ${locationName} forced loot.`); this.logger.debug(
`Unable to adjust loot item ${itemTpl} as it does not exist inside ${locationName} forced loot.`,
);
continue; continue;
} }
// Create probability array of all spawn positions for this spawn id // Create probability array of all spawn positions for this spawn id
const spawnpointArray = new ProbabilityObjectArray<string, SpawnpointsForced>(this.mathUtil, this.jsonUtil); const spawnpointArray = new ProbabilityObjectArray<string, SpawnpointsForced>(
this.mathUtil,
this.jsonUtil,
);
for (const si of items) for (const si of items)
{ {
// use locationId as template.Id is the same across all items // use locationId as template.Id is the same across all items
spawnpointArray.push( spawnpointArray.push(
new ProbabilityObject(si.locationId, si.probability, si) new ProbabilityObject(si.locationId, si.probability, si),
); );
} }
// Choose 1 out of all found spawn positions for spawn id and add to loot array // Choose 1 out of all found spawn positions for spawn id and add to loot array
for (const spawnPointLocationId of spawnpointArray.draw(1, false)) for (const spawnPointLocationId of spawnpointArray.draw(1, false))
{ {
const itemToAdd = items.find(x => x.locationId === spawnPointLocationId); const itemToAdd = items.find((x) => x.locationId === spawnPointLocationId);
const lootItem = itemToAdd.template; const lootItem = itemToAdd.template;
lootItem.Root = this.objectId.generate(); lootItem.Root = this.objectId.generate();
lootItem.Items[0]._id = lootItem.Root; lootItem.Items[0]._id = lootItem.Root;
@ -656,29 +768,36 @@ export class LocationGenerator
* @param staticAmmoDist ammo distributions * @param staticAmmoDist ammo distributions
* @returns IContainerItem * @returns IContainerItem
*/ */
protected createDynamicLootItem(chosenComposedKey: string, spawnPoint: Spawnpoint, staticAmmoDist: Record<string, IStaticAmmoDetails[]>): IContainerItem protected createDynamicLootItem(
chosenComposedKey: string,
spawnPoint: Spawnpoint,
staticAmmoDist: Record<string, IStaticAmmoDetails[]>,
): IContainerItem
{ {
const chosenItem = spawnPoint.template.Items.find(x => x._id === chosenComposedKey); const chosenItem = spawnPoint.template.Items.find((x) => x._id === chosenComposedKey);
const chosenTpl = chosenItem._tpl; const chosenTpl = chosenItem._tpl;
// Item array to return // Item array to return
const itemWithMods: Item[] = []; const itemWithMods: Item[] = [];
// Money/Ammo - don't rely on items in spawnPoint.template.Items so we can randomise it ourselves // Money/Ammo - don't rely on items in spawnPoint.template.Items so we can randomise it ourselves
if (this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.MONEY) || this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.AMMO)) if (
this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.MONEY) ||
this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.AMMO)
)
{ {
const itemTemplate = this.itemHelper.getItem(chosenTpl)[1]; const itemTemplate = this.itemHelper.getItem(chosenTpl)[1];
const stackCount = itemTemplate._props.StackMaxSize === 1 const stackCount = itemTemplate._props.StackMaxSize === 1 ?
? 1 1 :
: this.randomUtil.getInt(itemTemplate._props.StackMinRandom, itemTemplate._props.StackMaxRandom); this.randomUtil.getInt(itemTemplate._props.StackMinRandom, itemTemplate._props.StackMaxRandom);
itemWithMods.push( itemWithMods.push(
{ {
_id: this.objectId.generate(), _id: this.objectId.generate(),
_tpl: chosenTpl, _tpl: chosenTpl,
upd: { StackObjectsCount: stackCount } upd: {StackObjectsCount: stackCount},
} },
); );
} }
else if (this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.AMMO_BOX)) else if (this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.AMMO_BOX))
@ -687,7 +806,7 @@ export class LocationGenerator
const ammoBoxTemplate = this.itemHelper.getItem(chosenTpl)[1]; const ammoBoxTemplate = this.itemHelper.getItem(chosenTpl)[1];
const ammoBoxItem: Item[] = [{ const ammoBoxItem: Item[] = [{
_id: this.objectId.generate(), _id: this.objectId.generate(),
_tpl: chosenTpl _tpl: chosenTpl,
}]; }];
this.itemHelper.addCartridgesToAmmoBox(ammoBoxItem, ammoBoxTemplate); this.itemHelper.addCartridgesToAmmoBox(ammoBoxItem, ammoBoxTemplate);
itemWithMods.push(...ammoBoxItem); itemWithMods.push(...ammoBoxItem);
@ -698,15 +817,24 @@ export class LocationGenerator
const magazineTemplate = this.itemHelper.getItem(chosenTpl)[1]; const magazineTemplate = this.itemHelper.getItem(chosenTpl)[1];
const magazineItem: Item[] = [{ const magazineItem: Item[] = [{
_id: this.objectId.generate(), _id: this.objectId.generate(),
_tpl: chosenTpl _tpl: chosenTpl,
}]; }];
this.itemHelper.fillMagazineWithRandomCartridge(magazineItem, magazineTemplate, staticAmmoDist, null, this.locationConfig.minFillLooseMagazinePercent / 100); this.itemHelper.fillMagazineWithRandomCartridge(
magazineItem,
magazineTemplate,
staticAmmoDist,
null,
this.locationConfig.minFillLooseMagazinePercent / 100,
);
itemWithMods.push(...magazineItem); itemWithMods.push(...magazineItem);
} }
else else
{ {
// Get item + children and add into array we return // Get item + children and add into array we return
const itemWithChildren = this.itemHelper.findAndReturnChildrenAsItems(spawnPoint.template.Items, chosenItem._id); const itemWithChildren = this.itemHelper.findAndReturnChildrenAsItems(
spawnPoint.template.Items,
chosenItem._id,
);
// We need to reparent to ensure ids are unique // We need to reparent to ensure ids are unique
this.reparentItemAndChildren(itemWithChildren); this.reparentItemAndChildren(itemWithChildren);
@ -720,7 +848,7 @@ export class LocationGenerator
return { return {
items: itemWithMods, items: itemWithMods,
width: size.width, width: size.width,
height: size.height height: size.height,
}; };
} }
@ -757,14 +885,18 @@ export class LocationGenerator
{ {
if (this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.WEAPON)) if (this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.WEAPON))
{ {
return items.find(v => v._tpl === chosenTpl && v.parentId === undefined); return items.find((v) => v._tpl === chosenTpl && v.parentId === undefined);
} }
return items.find(x => x._tpl === chosenTpl); return items.find((x) => x._tpl === chosenTpl);
} }
// TODO: rewrite, BIG yikes // TODO: rewrite, BIG yikes
protected createStaticLootItem(tpl: string, staticAmmoDist: Record<string, IStaticAmmoDetails[]>, parentId: string = undefined): IContainerItem protected createStaticLootItem(
tpl: string,
staticAmmoDist: Record<string, IStaticAmmoDetails[]>,
parentId: string = undefined,
): IContainerItem
{ {
const itemTemplate = this.itemHelper.getItem(tpl)[1]; const itemTemplate = this.itemHelper.getItem(tpl)[1];
let width = itemTemplate._props.Width; let width = itemTemplate._props.Width;
@ -772,8 +904,8 @@ export class LocationGenerator
let items: Item[] = [ let items: Item[] = [
{ {
_id: this.objectId.generate(), _id: this.objectId.generate(),
_tpl: tpl _tpl: tpl,
} },
]; ];
// Use passed in parentId as override for new item // Use passed in parentId as override for new item
@ -782,13 +914,16 @@ export class LocationGenerator
items[0].parentId = parentId; items[0].parentId = parentId;
} }
if (this.itemHelper.isOfBaseclass(tpl, BaseClasses.MONEY) || this.itemHelper.isOfBaseclass(tpl, BaseClasses.AMMO)) if (
this.itemHelper.isOfBaseclass(tpl, BaseClasses.MONEY) ||
this.itemHelper.isOfBaseclass(tpl, BaseClasses.AMMO)
)
{ {
// Edge case - some ammos e.g. flares or M406 grenades shouldn't be stacked // Edge case - some ammos e.g. flares or M406 grenades shouldn't be stacked
const stackCount = itemTemplate._props.StackMaxSize === 1 const stackCount = itemTemplate._props.StackMaxSize === 1 ?
? 1 1 :
: this.randomUtil.getInt(itemTemplate._props.StackMinRandom, itemTemplate._props.StackMaxRandom); this.randomUtil.getInt(itemTemplate._props.StackMinRandom, itemTemplate._props.StackMaxRandom);
items[0].upd = { StackObjectsCount: stackCount }; items[0].upd = {StackObjectsCount: stackCount};
} }
// No spawn point, use default template // No spawn point, use default template
else if (this.itemHelper.isOfBaseclass(tpl, BaseClasses.WEAPON)) else if (this.itemHelper.isOfBaseclass(tpl, BaseClasses.WEAPON))
@ -806,21 +941,30 @@ export class LocationGenerator
// this item already broke it once without being reproducible tpl = "5839a40f24597726f856b511"; AKS-74UB Default // this item already broke it once without being reproducible tpl = "5839a40f24597726f856b511"; AKS-74UB Default
// 5ea03f7400685063ec28bfa8 // ppsh default // 5ea03f7400685063ec28bfa8 // ppsh default
// 5ba26383d4351e00334c93d9 //mp7_devgru // 5ba26383d4351e00334c93d9 //mp7_devgru
this.logger.warning(this.localisationService.getText("location-preset_not_found", {tpl: tpl, defaultId: defaultPreset._id, defaultName: defaultPreset._name, parentId: parentId})); this.logger.warning(
this.localisationService.getText("location-preset_not_found", {
tpl: tpl,
defaultId: defaultPreset._id,
defaultName: defaultPreset._name,
parentId: parentId,
}),
);
throw error; throw error;
} }
} }
else else
{ {
// RSP30 (62178be9d0050232da3485d9/624c0b3340357b5f566e8766/6217726288ed9f0845317459) doesnt have any default presets and kills this code below as it has no chidren to reparent // RSP30 (62178be9d0050232da3485d9/624c0b3340357b5f566e8766/6217726288ed9f0845317459) doesn't have any default presets and kills this code below as it has no children to reparent
this.logger.debug(`createItem() No preset found for weapon: ${tpl}`); this.logger.debug(`createItem() No preset found for weapon: ${tpl}`);
} }
const rootItem = items[0]; const rootItem = items[0];
if (!rootItem) if (!rootItem)
{ {
this.logger.error(this.localisationService.getText("location-missing_root_item", {tpl: tpl, parentId: parentId})); this.logger.error(
this.localisationService.getText("location-missing_root_item", {tpl: tpl, parentId: parentId}),
);
throw new Error(this.localisationService.getText("location-critical_error_see_log")); throw new Error(this.localisationService.getText("location-critical_error_see_log"));
} }
@ -834,17 +978,21 @@ export class LocationGenerator
} }
catch (error) catch (error)
{ {
this.logger.error(this.localisationService.getText("location-unable_to_reparent_item", {tpl: tpl, parentId: parentId})); this.logger.error(
this.localisationService.getText("location-unable_to_reparent_item", {
tpl: tpl,
parentId: parentId,
}),
);
throw error; throw error;
} }
// Here we should use generalized BotGenerators functions e.g. fillExistingMagazines in the future since // Here we should use generalized BotGenerators functions e.g. fillExistingMagazines in the future since
// it can handle revolver ammo (it's not restructured to be used here yet.) // it can handle revolver ammo (it's not restructured to be used here yet.)
// General: Make a WeaponController for Ragfair preset stuff and the generating weapons and ammo stuff from // General: Make a WeaponController for Ragfair preset stuff and the generating weapons and ammo stuff from
// BotGenerator // BotGenerator
const magazine = items.filter(x => x.slotId === "mod_magazine")[0]; const magazine = items.filter((x) => x.slotId === "mod_magazine")[0];
// some weapon presets come without magazine; only fill the mag if it exists // some weapon presets come without magazine; only fill the mag if it exists
if (magazine) if (magazine)
{ {
@ -853,7 +1001,12 @@ export class LocationGenerator
// Create array with just magazine // Create array with just magazine
const magazineWithCartridges = [magazine]; const magazineWithCartridges = [magazine];
this.itemHelper.fillMagazineWithRandomCartridge(magazineWithCartridges, magTemplate, staticAmmoDist, weaponTemplate._props.ammoCaliber); this.itemHelper.fillMagazineWithRandomCartridge(
magazineWithCartridges,
magTemplate,
staticAmmoDist,
weaponTemplate._props.ammoCaliber,
);
// Replace existing magazine with above array // Replace existing magazine with above array
items.splice(items.indexOf(magazine), 1, ...magazineWithCartridges); items.splice(items.indexOf(magazine), 1, ...magazineWithCartridges);
@ -872,7 +1025,13 @@ export class LocationGenerator
{ {
// Create array with just magazine // Create array with just magazine
const magazineWithCartridges = [items[0]]; const magazineWithCartridges = [items[0]];
this.itemHelper.fillMagazineWithRandomCartridge(magazineWithCartridges, itemTemplate, staticAmmoDist, null, this.locationConfig.minFillStaticMagazinePercent / 100); this.itemHelper.fillMagazineWithRandomCartridge(
magazineWithCartridges,
itemTemplate,
staticAmmoDist,
null,
this.locationConfig.minFillStaticMagazinePercent / 100,
);
// Replace existing magazine with above array // Replace existing magazine with above array
items.splice(items.indexOf(items[0]), 1, ...magazineWithCartridges); items.splice(items.indexOf(items[0]), 1, ...magazineWithCartridges);
@ -881,7 +1040,7 @@ export class LocationGenerator
return { return {
items: items, items: items,
width: width, width: width,
height: height height: height,
}; };
} }
} }

View File

@ -20,8 +20,8 @@ import { HashUtil } from "@spt-aki/utils/HashUtil";
import { RandomUtil } from "@spt-aki/utils/RandomUtil"; import { RandomUtil } from "@spt-aki/utils/RandomUtil";
type ItemLimit = { type ItemLimit = {
current: number, current: number;
max: number max: number;
}; };
@injectable() @injectable()
@ -38,7 +38,7 @@ export class LootGenerator
@inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper, @inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper,
@inject("LocalisationService") protected localisationService: LocalisationService, @inject("LocalisationService") protected localisationService: LocalisationService,
@inject("RagfairLinkedItemService") protected ragfairLinkedItemService: RagfairLinkedItemService, @inject("RagfairLinkedItemService") protected ragfairLinkedItemService: RagfairLinkedItemService,
@inject("ItemFilterService") protected itemFilterService: ItemFilterService @inject("ItemFilterService") protected itemFilterService: ItemFilterService,
) )
{} {}
@ -54,7 +54,10 @@ export class LootGenerator
const itemTypeCounts = this.initItemLimitCounter(options.itemLimits); const itemTypeCounts = this.initItemLimitCounter(options.itemLimits);
const tables = this.databaseServer.getTables(); const tables = this.databaseServer.getTables();
const itemBlacklist = new Set<string>([...this.itemFilterService.getBlacklistedItems(), ...options.itemBlacklist]); const itemBlacklist = new Set<string>([
...this.itemFilterService.getBlacklistedItems(),
...options.itemBlacklist,
]);
if (!options.allowBossItems) if (!options.allowBossItems)
{ {
for (const bossItem of this.itemFilterService.getBossItems()) for (const bossItem of this.itemFilterService.getBossItems())
@ -64,11 +67,16 @@ export class LootGenerator
} }
// Handle sealed weapon containers // Handle sealed weapon containers
const desiredWeaponCrateCount = this.randomUtil.getInt(options.weaponCrateCount.min, options.weaponCrateCount.max); const desiredWeaponCrateCount = this.randomUtil.getInt(
options.weaponCrateCount.min,
options.weaponCrateCount.max,
);
if (desiredWeaponCrateCount > 0) if (desiredWeaponCrateCount > 0)
{ {
// Get list of all sealed containers from db // Get list of all sealed containers from db
const sealedWeaponContainerPool = Object.values(tables.templates.items).filter(x => x._name.includes("event_container_airdrop")); const sealedWeaponContainerPool = Object.values(tables.templates.items).filter((x) =>
x._name.includes("event_container_airdrop")
);
for (let index = 0; index < desiredWeaponCrateCount; index++) for (let index = 0; index < desiredWeaponCrateCount; index++)
{ {
@ -78,16 +86,18 @@ export class LootGenerator
id: this.hashUtil.generate(), id: this.hashUtil.generate(),
tpl: chosenSealedContainer._id, tpl: chosenSealedContainer._id,
isPreset: false, isPreset: false,
stackCount: 1 stackCount: 1,
}); });
} }
} }
// Get items from items.json that have a type of item + not in global blacklist + basetype is in whitelist // Get items from items.json that have a type of item + not in global blacklist + basetype is in whitelist
const items = Object.entries(tables.templates.items).filter(x => !itemBlacklist.has(x[1]._id) const items = Object.entries(tables.templates.items).filter((x) =>
&& x[1]._type.toLowerCase() === "item" !itemBlacklist.has(x[1]._id) &&
&& !x[1]._props.QuestItem x[1]._type.toLowerCase() === "item" &&
&& options.itemTypeWhitelist.includes(x[1]._parent)); !x[1]._props.QuestItem &&
options.itemTypeWhitelist.includes(x[1]._parent)
);
const randomisedItemCount = this.randomUtil.getInt(options.itemCount.min, options.itemCount.max); const randomisedItemCount = this.randomUtil.getInt(options.itemCount.min, options.itemCount.max);
for (let index = 0; index < randomisedItemCount; index++) for (let index = 0; index < randomisedItemCount; index++)
@ -98,7 +108,9 @@ export class LootGenerator
} }
} }
const globalDefaultPresets = Object.entries(tables.globals.ItemPresets).filter(x => x[1]._encyclopedia !== undefined); const globalDefaultPresets = Object.entries(tables.globals.ItemPresets).filter((x) =>
x[1]._encyclopedia !== undefined
);
const randomisedPresetCount = this.randomUtil.getInt(options.presetCount.min, options.presetCount.max); const randomisedPresetCount = this.randomUtil.getInt(options.presetCount.min, options.presetCount.max);
const itemBlacklistArray = Array.from(itemBlacklist); const itemBlacklistArray = Array.from(itemBlacklist);
for (let index = 0; index < randomisedPresetCount; index++) for (let index = 0; index < randomisedPresetCount; index++)
@ -124,7 +136,7 @@ export class LootGenerator
{ {
itemTypeCounts[itemTypeId] = { itemTypeCounts[itemTypeId] = {
current: 0, current: 0,
max: limits[itemTypeId] max: limits[itemTypeId],
}; };
} }
@ -141,9 +153,10 @@ export class LootGenerator
*/ */
protected findAndAddRandomItemToLoot( protected findAndAddRandomItemToLoot(
items: [string, ITemplateItem][], items: [string, ITemplateItem][],
itemTypeCounts: Record<string, { current: number; max: number; }>, itemTypeCounts: Record<string, {current: number; max: number;}>,
options: LootRequest, options: LootRequest,
result: LootItem[]): boolean result: LootItem[],
): boolean
{ {
const randomItem = this.randomUtil.getArrayValue(items)[1]; const randomItem = this.randomUtil.getArrayValue(items)[1];
@ -157,12 +170,14 @@ export class LootGenerator
id: this.hashUtil.generate(), id: this.hashUtil.generate(),
tpl: randomItem._id, tpl: randomItem._id,
isPreset: false, isPreset: false,
stackCount: 1 stackCount: 1,
}; };
// Check if armor has level in allowed whitelist // Check if armor has level in allowed whitelist
if (randomItem._parent === BaseClasses.ARMOR if (
|| randomItem._parent === BaseClasses.VEST) randomItem._parent === BaseClasses.ARMOR ||
randomItem._parent === BaseClasses.VEST
)
{ {
if (!options.armorLevelWhitelist.includes(Number(randomItem._props.armorClass))) if (!options.armorLevelWhitelist.includes(Number(randomItem._props.armorClass)))
{ {
@ -219,9 +234,10 @@ export class LootGenerator
*/ */
protected findAndAddRandomPresetToLoot( protected findAndAddRandomPresetToLoot(
globalDefaultPresets: [string, IPreset][], globalDefaultPresets: [string, IPreset][],
itemTypeCounts: Record<string, { current: number; max: number; }>, itemTypeCounts: Record<string, {current: number; max: number;}>,
itemBlacklist: string[], itemBlacklist: string[],
result: LootItem[]): boolean result: LootItem[],
): boolean
{ {
// Choose random preset and get details from item.json using encyclopedia value (encyclopedia === tplId) // Choose random preset and get details from item.json using encyclopedia value (encyclopedia === tplId)
const randomPreset = this.randomUtil.getArrayValue(globalDefaultPresets)[1]; const randomPreset = this.randomUtil.getArrayValue(globalDefaultPresets)[1];
@ -264,7 +280,7 @@ export class LootGenerator
const newLootItem: LootItem = { const newLootItem: LootItem = {
tpl: randomPreset._items[0]._tpl, tpl: randomPreset._items[0]._tpl,
isPreset: true, isPreset: true,
stackCount: 1 stackCount: 1,
}; };
result.push(newLootItem); result.push(newLootItem);
@ -289,19 +305,23 @@ export class LootGenerator
const itemsToReturn: AddItem[] = []; const itemsToReturn: AddItem[] = [];
// choose a weapon to give to the player (weighted) // choose a weapon to give to the player (weighted)
const chosenWeaponTpl = this.weightedRandomHelper.getWeightedValue<string>(containerSettings.weaponRewardWeight); const chosenWeaponTpl = this.weightedRandomHelper.getWeightedValue<string>(
containerSettings.weaponRewardWeight,
);
const weaponDetailsDb = this.itemHelper.getItem(chosenWeaponTpl); const weaponDetailsDb = this.itemHelper.getItem(chosenWeaponTpl);
if (!weaponDetailsDb[0]) if (!weaponDetailsDb[0])
{ {
this.logger.error(this.localisationService.getText("loot-non_item_picked_as_sealed_weapon_crate_reward", chosenWeaponTpl)); this.logger.error(
this.localisationService.getText("loot-non_item_picked_as_sealed_weapon_crate_reward", chosenWeaponTpl),
);
return itemsToReturn; return itemsToReturn;
} }
// Get weapon preset - default or choose a random one from all possible // Get weapon preset - default or choose a random one from all possible
let chosenWeaponPreset = containerSettings.defaultPresetsOnly let chosenWeaponPreset = containerSettings.defaultPresetsOnly ?
? this.presetHelper.getDefaultPreset(chosenWeaponTpl) this.presetHelper.getDefaultPreset(chosenWeaponTpl) :
: this.randomUtil.getArrayValue(this.presetHelper.getPresets(chosenWeaponTpl)); this.randomUtil.getArrayValue(this.presetHelper.getPresets(chosenWeaponTpl));
if (!chosenWeaponPreset) if (!chosenWeaponPreset)
{ {
@ -314,12 +334,14 @@ export class LootGenerator
count: 1, count: 1,
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
item_id: chosenWeaponPreset._id, item_id: chosenWeaponPreset._id,
isPreset: true isPreset: true,
}); });
// Get items related to chosen weapon // Get items related to chosen weapon
const linkedItemsToWeapon = this.ragfairLinkedItemService.getLinkedDbItems(chosenWeaponTpl); const linkedItemsToWeapon = this.ragfairLinkedItemService.getLinkedDbItems(chosenWeaponTpl);
itemsToReturn.push(...this.getSealedContainerWeaponModRewards(containerSettings, linkedItemsToWeapon, chosenWeaponPreset)); itemsToReturn.push(
...this.getSealedContainerWeaponModRewards(containerSettings, linkedItemsToWeapon, chosenWeaponPreset),
);
// Handle non-weapon mod reward types // Handle non-weapon mod reward types
itemsToReturn.push(...this.getSealedContainerNonWeaponModRewards(containerSettings, weaponDetailsDb[1])); itemsToReturn.push(...this.getSealedContainerNonWeaponModRewards(containerSettings, weaponDetailsDb[1]));
@ -333,7 +355,10 @@ export class LootGenerator
* @param weaponDetailsDb Details for the weapon to reward player * @param weaponDetailsDb Details for the weapon to reward player
* @returns AddItem array * @returns AddItem array
*/ */
protected getSealedContainerNonWeaponModRewards(containerSettings: ISealedAirdropContainerSettings, weaponDetailsDb: ITemplateItem): AddItem[] protected getSealedContainerNonWeaponModRewards(
containerSettings: ISealedAirdropContainerSettings,
weaponDetailsDb: ITemplateItem,
): AddItem[]
{ {
const rewards: AddItem[] = []; const rewards: AddItem[] = [];
@ -351,7 +376,7 @@ export class LootGenerator
if (rewardTypeId === BaseClasses.AMMO_BOX) if (rewardTypeId === BaseClasses.AMMO_BOX)
{ {
// Get ammoboxes from db // Get ammoboxes from db
const ammoBoxesDetails = containerSettings.ammoBoxWhitelist.map(x => const ammoBoxesDetails = containerSettings.ammoBoxWhitelist.map((x) =>
{ {
const itemDetails = this.itemHelper.getItem(x); const itemDetails = this.itemHelper.getItem(x);
return itemDetails[1]; return itemDetails[1];
@ -359,7 +384,7 @@ export class LootGenerator
// Need to find boxes that matches weapons caliber // Need to find boxes that matches weapons caliber
const weaponCaliber = weaponDetailsDb._props.ammoCaliber; const weaponCaliber = weaponDetailsDb._props.ammoCaliber;
const ammoBoxesMatchingCaliber = ammoBoxesDetails.filter(x => x._props.ammoCaliber === weaponCaliber); const ammoBoxesMatchingCaliber = ammoBoxesDetails.filter((x) => x._props.ammoCaliber === weaponCaliber);
if (ammoBoxesMatchingCaliber.length === 0) if (ammoBoxesMatchingCaliber.length === 0)
{ {
this.logger.debug(`No ammo box with caliber ${weaponCaliber} found, skipping`); this.logger.debug(`No ammo box with caliber ${weaponCaliber} found, skipping`);
@ -373,7 +398,7 @@ export class LootGenerator
count: rewardCount, count: rewardCount,
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
item_id: chosenAmmoBox._id, item_id: chosenAmmoBox._id,
isPreset: false isPreset: false,
}); });
continue; continue;
@ -381,11 +406,13 @@ export class LootGenerator
// Get all items of the desired type + not quest items + not globally blacklisted // Get all items of the desired type + not quest items + not globally blacklisted
const rewardItemPool = Object.values(this.databaseServer.getTables().templates.items) const rewardItemPool = Object.values(this.databaseServer.getTables().templates.items)
.filter(x => x._parent === rewardTypeId .filter((x) =>
&& x._type.toLowerCase() === "item" x._parent === rewardTypeId &&
&& !this.itemFilterService.isItemBlacklisted(x._id) x._type.toLowerCase() === "item" &&
&& (!containerSettings.allowBossItems && !this.itemFilterService.isBossItem(x._id)) !this.itemFilterService.isItemBlacklisted(x._id) &&
&& !x._props.QuestItem); (!containerSettings.allowBossItems && !this.itemFilterService.isBossItem(x._id)) &&
!x._props.QuestItem
);
if (rewardItemPool.length === 0) if (rewardItemPool.length === 0)
{ {
@ -412,7 +439,11 @@ export class LootGenerator
* @param chosenWeaponPreset The weapon preset given to player as reward * @param chosenWeaponPreset The weapon preset given to player as reward
* @returns AddItem array * @returns AddItem array
*/ */
protected getSealedContainerWeaponModRewards(containerSettings: ISealedAirdropContainerSettings, linkedItemsToWeapon: ITemplateItem[], chosenWeaponPreset: IPreset): AddItem[] protected getSealedContainerWeaponModRewards(
containerSettings: ISealedAirdropContainerSettings,
linkedItemsToWeapon: ITemplateItem[],
chosenWeaponPreset: IPreset,
): AddItem[]
{ {
const modRewards: AddItem[] = []; const modRewards: AddItem[] = [];
for (const rewardTypeId in containerSettings.weaponModRewardLimits) for (const rewardTypeId in containerSettings.weaponModRewardLimits)
@ -426,11 +457,15 @@ export class LootGenerator
continue; continue;
} }
// Get items that fulfil reward type critera from items that fit on gun // Get items that fulfil reward type criteria from items that fit on gun
const relatedItems = linkedItemsToWeapon.filter(x => x._parent === rewardTypeId && !this.itemFilterService.isItemBlacklisted(x._id)); const relatedItems = linkedItemsToWeapon.filter((x) =>
x._parent === rewardTypeId && !this.itemFilterService.isItemBlacklisted(x._id)
);
if (!relatedItems || relatedItems.length === 0) if (!relatedItems || relatedItems.length === 0)
{ {
this.logger.debug(`No items found to fulfil reward type ${rewardTypeId} for weapon: ${chosenWeaponPreset._name}, skipping type`); this.logger.debug(
`No items found to fulfil reward type ${rewardTypeId} for weapon: ${chosenWeaponPreset._name}, skipping type`,
);
continue; continue;
} }
@ -458,7 +493,9 @@ export class LootGenerator
for (let index = 0; index < rewardContainerDetails.rewardCount; index++) for (let index = 0; index < rewardContainerDetails.rewardCount; index++)
{ {
// Pick random reward from pool, add to request object // Pick random reward from pool, add to request object
const chosenRewardItemTpl = this.weightedRandomHelper.getWeightedValue<string>(rewardContainerDetails.rewardTplPool); const chosenRewardItemTpl = this.weightedRandomHelper.getWeightedValue<string>(
rewardContainerDetails.rewardTplPool,
);
this.addOrIncrementItemToArray(chosenRewardItemTpl, itemsToReturn); this.addOrIncrementItemToArray(chosenRewardItemTpl, itemsToReturn);
} }
@ -473,7 +510,7 @@ export class LootGenerator
*/ */
protected addOrIncrementItemToArray(itemTplToAdd: string, resultsArray: AddItem[]): void protected addOrIncrementItemToArray(itemTplToAdd: string, resultsArray: AddItem[]): void
{ {
const existingItemIndex = resultsArray.findIndex(x => x.item_id === itemTplToAdd); const existingItemIndex = resultsArray.findIndex((x) => x.item_id === itemTplToAdd);
if (existingItemIndex > -1) if (existingItemIndex > -1)
{ {
// Exists in array already, increment count // Exists in array already, increment count

View File

@ -14,7 +14,6 @@ import { SeasonalEventService } from "@spt-aki/services/SeasonalEventService";
* and the removal of blacklisted items * and the removal of blacklisted items
*/ */
@injectable() @injectable()
export class PMCLootGenerator export class PMCLootGenerator
{ {
protected pocketLootPool: string[] = []; protected pocketLootPool: string[] = [];
@ -27,7 +26,7 @@ export class PMCLootGenerator
@inject("DatabaseServer") protected databaseServer: DatabaseServer, @inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("ConfigServer") protected configServer: ConfigServer, @inject("ConfigServer") protected configServer: ConfigServer,
@inject("ItemFilterService") protected itemFilterService: ItemFilterService, @inject("ItemFilterService") protected itemFilterService: ItemFilterService,
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService @inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService,
) )
{ {
this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC); this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC);
@ -56,14 +55,16 @@ export class PMCLootGenerator
itemBlacklist.push(...this.seasonalEventService.getInactiveSeasonalEventItems()); itemBlacklist.push(...this.seasonalEventService.getInactiveSeasonalEventItems());
} }
const itemsToAdd = Object.values(items).filter(item => allowedItemTypes.includes(item._parent) const itemsToAdd = Object.values(items).filter((item) =>
&& this.itemHelper.isValidItem(item._id) allowedItemTypes.includes(item._parent) &&
&& !pmcItemBlacklist.includes(item._id) this.itemHelper.isValidItem(item._id) &&
&& !itemBlacklist.includes(item._id) !pmcItemBlacklist.includes(item._id) &&
&& item._props.Width === 1 !itemBlacklist.includes(item._id) &&
&& item._props.Height === 1); item._props.Width === 1 &&
item._props.Height === 1
);
this.pocketLootPool = itemsToAdd.map(x => x._id); this.pocketLootPool = itemsToAdd.map((x) => x._id);
} }
return this.pocketLootPool; return this.pocketLootPool;
@ -92,13 +93,15 @@ export class PMCLootGenerator
itemBlacklist.push(...this.seasonalEventService.getInactiveSeasonalEventItems()); itemBlacklist.push(...this.seasonalEventService.getInactiveSeasonalEventItems());
} }
const itemsToAdd = Object.values(items).filter(item => allowedItemTypes.includes(item._parent) const itemsToAdd = Object.values(items).filter((item) =>
&& this.itemHelper.isValidItem(item._id) allowedItemTypes.includes(item._parent) &&
&& !pmcItemBlacklist.includes(item._id) this.itemHelper.isValidItem(item._id) &&
&& !itemBlacklist.includes(item._id) !pmcItemBlacklist.includes(item._id) &&
&& this.itemFitsInto2By2Slot(item)); !itemBlacklist.includes(item._id) &&
this.itemFitsInto2By2Slot(item)
);
this.vestLootPool = itemsToAdd.map(x => x._id); this.vestLootPool = itemsToAdd.map((x) => x._id);
} }
return this.vestLootPool; return this.vestLootPool;
@ -137,12 +140,14 @@ export class PMCLootGenerator
itemBlacklist.push(...this.seasonalEventService.getInactiveSeasonalEventItems()); itemBlacklist.push(...this.seasonalEventService.getInactiveSeasonalEventItems());
} }
const itemsToAdd = Object.values(items).filter(item => allowedItemTypes.includes(item._parent) const itemsToAdd = Object.values(items).filter((item) =>
&& this.itemHelper.isValidItem(item._id) allowedItemTypes.includes(item._parent) &&
&& !pmcItemBlacklist.includes(item._id) this.itemHelper.isValidItem(item._id) &&
&& !itemBlacklist.includes(item._id)); !pmcItemBlacklist.includes(item._id) &&
!itemBlacklist.includes(item._id)
);
this.backpackLootPool = itemsToAdd.map(x => x._id); this.backpackLootPool = itemsToAdd.map((x) => x._id);
} }
return this.backpackLootPool; return this.backpackLootPool;

View File

@ -47,7 +47,7 @@ export class PlayerScavGenerator
@inject("BotLootCacheService") protected botLootCacheService: BotLootCacheService, @inject("BotLootCacheService") protected botLootCacheService: BotLootCacheService,
@inject("LocalisationService") protected localisationService: LocalisationService, @inject("LocalisationService") protected localisationService: LocalisationService,
@inject("BotGenerator") protected botGenerator: BotGenerator, @inject("BotGenerator") protected botGenerator: BotGenerator,
@inject("ConfigServer") protected configServer: ConfigServer @inject("ConfigServer") protected configServer: ConfigServer,
) )
{ {
this.playerScavConfig = this.configServer.getConfig(ConfigTypes.PLAYERSCAV); this.playerScavConfig = this.configServer.getConfig(ConfigTypes.PLAYERSCAV);
@ -66,9 +66,9 @@ export class PlayerScavGenerator
const existingScavData = this.jsonUtil.clone(profile.characters.scav); const existingScavData = this.jsonUtil.clone(profile.characters.scav);
// scav profile can be empty on first profile creation // scav profile can be empty on first profile creation
const scavKarmaLevel = ((Object.keys(existingScavData).length === 0)) const scavKarmaLevel = (Object.keys(existingScavData).length === 0) ?
? 0 0 :
: this.getScavKarmaLevel(pmcData); this.getScavKarmaLevel(pmcData);
// use karma level to get correct karmaSettings // use karma level to get correct karmaSettings
const playerScavKarmaSettings = this.playerScavConfig.karmaLevel[scavKarmaLevel]; const playerScavKarmaSettings = this.playerScavConfig.karmaLevel[scavKarmaLevel];
@ -83,7 +83,12 @@ export class PlayerScavGenerator
const baseBotNode: IBotType = this.constructBotBaseTemplate(playerScavKarmaSettings.botTypeForLoot); const baseBotNode: IBotType = this.constructBotBaseTemplate(playerScavKarmaSettings.botTypeForLoot);
this.adjustBotTemplateWithKarmaSpecificSettings(playerScavKarmaSettings, baseBotNode); this.adjustBotTemplateWithKarmaSpecificSettings(playerScavKarmaSettings, baseBotNode);
let scavData = this.botGenerator.generatePlayerScav(sessionID, playerScavKarmaSettings.botTypeForLoot.toLowerCase(), "easy", baseBotNode); let scavData = this.botGenerator.generatePlayerScav(
sessionID,
playerScavKarmaSettings.botTypeForLoot.toLowerCase(),
"easy",
baseBotNode,
);
// Remove cached bot data after scav was generated // Remove cached bot data after scav was generated
this.botLootCacheService.clearCache(); this.botLootCacheService.clearCache();
@ -113,7 +118,6 @@ export class PlayerScavGenerator
scavData.Notes = existingScavData.Notes ?? {Notes: []}; scavData.Notes = existingScavData.Notes ?? {Notes: []};
scavData.WishList = existingScavData.WishList ?? []; scavData.WishList = existingScavData.WishList ?? [];
// Add an extra labs card to pscav backpack based on config chance // Add an extra labs card to pscav backpack based on config chance
if (this.randomUtil.getChance100(playerScavKarmaSettings.labsAccessCardChancePercent)) if (this.randomUtil.getChance100(playerScavKarmaSettings.labsAccessCardChancePercent))
{ {
@ -121,9 +125,15 @@ export class PlayerScavGenerator
const itemsToAdd: Item[] = [{ const itemsToAdd: Item[] = [{
_id: this.hashUtil.generate(), _id: this.hashUtil.generate(),
_tpl: labsCard._id, _tpl: labsCard._id,
...this.botGeneratorHelper.generateExtraPropertiesForItem(labsCard) ...this.botGeneratorHelper.generateExtraPropertiesForItem(labsCard),
}]; }];
this.botWeaponGeneratorHelper.addItemWithChildrenToEquipmentSlot(["TacticalVest", "Pockets", "Backpack"], itemsToAdd[0]._id, labsCard._id, itemsToAdd, scavData.Inventory); this.botWeaponGeneratorHelper.addItemWithChildrenToEquipmentSlot(
["TacticalVest", "Pockets", "Backpack"],
itemsToAdd[0]._id,
labsCard._id,
itemsToAdd,
scavData.Inventory,
);
} }
// Remove secure container // Remove secure container
@ -251,7 +261,7 @@ export class PlayerScavGenerator
return { return {
Common: [], Common: [],
Mastering: [], Mastering: [],
Points: 0 Points: 0,
}; };
} }

View File

@ -24,7 +24,7 @@ export class RagfairAssortGenerator
@inject("ItemHelper") protected itemHelper: ItemHelper, @inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("DatabaseServer") protected databaseServer: DatabaseServer, @inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService, @inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService,
@inject("ConfigServer") protected configServer: ConfigServer @inject("ConfigServer") protected configServer: ConfigServer,
) )
{ {
this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR); this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR);
@ -62,9 +62,9 @@ export class RagfairAssortGenerator
const results: Item[] = []; const results: Item[] = [];
const items = this.itemHelper.getItems(); const items = this.itemHelper.getItems();
const weaponPresets = (this.ragfairConfig.dynamic.showDefaultPresetsOnly) const weaponPresets = (this.ragfairConfig.dynamic.showDefaultPresetsOnly) ?
? this.getDefaultPresets() this.getDefaultPresets() :
: this.getPresets(); this.getPresets();
const ragfairItemInvalidBaseTypes: string[] = [ const ragfairItemInvalidBaseTypes: string[] = [
BaseClasses.LOOT_CONTAINER, // safe, barrel cache etc BaseClasses.LOOT_CONTAINER, // safe, barrel cache etc
@ -72,7 +72,7 @@ export class RagfairAssortGenerator
BaseClasses.SORTING_TABLE, BaseClasses.SORTING_TABLE,
BaseClasses.INVENTORY, BaseClasses.INVENTORY,
BaseClasses.STATIONARY_CONTAINER, BaseClasses.STATIONARY_CONTAINER,
BaseClasses.POCKETS BaseClasses.POCKETS,
]; ];
const seasonalEventActive = this.seasonalEventService.seasonalEventEnabled(); const seasonalEventActive = this.seasonalEventService.seasonalEventEnabled();
@ -84,7 +84,10 @@ export class RagfairAssortGenerator
continue; continue;
} }
if (this.ragfairConfig.dynamic.removeSeasonalItemsWhenNotInEvent && !seasonalEventActive && seasonalItemTplBlacklist.includes(item._id)) if (
this.ragfairConfig.dynamic.removeSeasonalItemsWhenNotInEvent && !seasonalEventActive &&
seasonalItemTplBlacklist.includes(item._id)
)
{ {
continue; continue;
} }
@ -116,7 +119,7 @@ export class RagfairAssortGenerator
*/ */
protected getDefaultPresets(): IPreset[] protected getDefaultPresets(): IPreset[]
{ {
return this.getPresets().filter(x => x._encyclopedia); return this.getPresets().filter((x) => x._encyclopedia);
} }
/** /**
@ -134,8 +137,8 @@ export class RagfairAssortGenerator
slotId: "hideout", slotId: "hideout",
upd: { upd: {
StackObjectsCount: 99999999, StackObjectsCount: 99999999,
UnlimitedCount: true UnlimitedCount: true,
} },
}; };
} }
} }

View File

@ -33,7 +33,7 @@ import { TimeUtil } from "@spt-aki/utils/TimeUtil";
export class RagfairOfferGenerator export class RagfairOfferGenerator
{ {
protected ragfairConfig: IRagfairConfig; protected ragfairConfig: IRagfairConfig;
protected allowedFleaPriceItemsForBarter: { tpl: string; price: number; }[]; protected allowedFleaPriceItemsForBarter: {tpl: string; price: number;}[];
constructor( constructor(
@inject("WinstonLogger") protected logger: ILogger, @inject("WinstonLogger") protected logger: ILogger,
@ -54,7 +54,7 @@ export class RagfairOfferGenerator
@inject("RagfairCategoriesService") protected ragfairCategoriesService: RagfairCategoriesService, @inject("RagfairCategoriesService") protected ragfairCategoriesService: RagfairCategoriesService,
@inject("FenceService") protected fenceService: FenceService, @inject("FenceService") protected fenceService: FenceService,
@inject("ItemHelper") protected itemHelper: ItemHelper, @inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("ConfigServer") protected configServer: ConfigServer @inject("ConfigServer") protected configServer: ConfigServer,
) )
{ {
this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR); this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR);
@ -70,7 +70,14 @@ export class RagfairOfferGenerator
* @param sellInOnePiece Flags sellInOnePiece to be true * @param sellInOnePiece Flags sellInOnePiece to be true
* @returns IRagfairOffer * @returns IRagfairOffer
*/ */
public createFleaOffer(userID: string, time: number, items: Item[], barterScheme: IBarterScheme[], loyalLevel: number, sellInOnePiece = false): IRagfairOffer public createFleaOffer(
userID: string,
time: number,
items: Item[],
barterScheme: IBarterScheme[],
loyalLevel: number,
sellInOnePiece = false,
): IRagfairOffer
{ {
const offer = this.createOffer(userID, time, items, barterScheme, loyalLevel, sellInOnePiece); const offer = this.createOffer(userID, time, items, barterScheme, loyalLevel, sellInOnePiece);
this.ragfairOfferService.addOffer(offer); this.ragfairOfferService.addOffer(offer);
@ -88,7 +95,14 @@ export class RagfairOfferGenerator
* @param sellInOnePiece Set StackObjectsCount to 1 * @param sellInOnePiece Set StackObjectsCount to 1
* @returns IRagfairOffer * @returns IRagfairOffer
*/ */
protected createOffer(userID: string, time: number, items: Item[], barterScheme: IBarterScheme[], loyalLevel: number, sellInOnePiece = false): IRagfairOffer protected createOffer(
userID: string,
time: number,
items: Item[],
barterScheme: IBarterScheme[],
loyalLevel: number,
sellInOnePiece = false,
): IRagfairOffer
{ {
const isTrader = this.ragfairServerHelper.isTrader(userID); const isTrader = this.ragfairServerHelper.isTrader(userID);
@ -98,13 +112,13 @@ export class RagfairOfferGenerator
const requirement: OfferRequirement = { const requirement: OfferRequirement = {
_tpl: barter._tpl, _tpl: barter._tpl,
count: +barter.count.toFixed(2), count: +barter.count.toFixed(2),
onlyFunctional: barter.onlyFunctional ?? false onlyFunctional: barter.onlyFunctional ?? false,
}; };
offerRequirements.push(requirement); offerRequirements.push(requirement);
} }
const itemCount = items.filter(x => x.slotId === "hideout").length; const itemCount = items.filter((x) => x.slotId === "hideout").length;
const roublePrice = Math.round(this.convertOfferRequirementsIntoRoubles(offerRequirements)); const roublePrice = Math.round(this.convertOfferRequirementsIntoRoubles(offerRequirements));
const offer: IRagfairOffer = { const offer: IRagfairOffer = {
@ -112,13 +126,13 @@ export class RagfairOfferGenerator
intId: 0, intId: 0,
user: { user: {
id: this.getTraderId(userID), id: this.getTraderId(userID),
memberType: (userID === "ragfair") memberType: (userID === "ragfair") ?
? MemberCategory.DEFAULT MemberCategory.DEFAULT :
: this.ragfairServerHelper.getMemberType(userID), this.ragfairServerHelper.getMemberType(userID),
nickname: this.ragfairServerHelper.getNickname(userID), nickname: this.ragfairServerHelper.getNickname(userID),
rating: this.getRating(userID), rating: this.getRating(userID),
isRatingGrowing: this.getRatingGrowing(userID), isRatingGrowing: this.getRatingGrowing(userID),
avatar: this.getAvatarUrl(isTrader, userID) avatar: this.getAvatarUrl(isTrader, userID),
}, },
root: items[0]._id, root: items[0]._id,
items: this.jsonUtil.clone(items), items: this.jsonUtil.clone(items),
@ -134,7 +148,7 @@ export class RagfairOfferGenerator
locked: false, locked: false,
unlimitedCount: false, unlimitedCount: false,
notAvailable: false, notAvailable: false,
CurrentItemCount: itemCount CurrentItemCount: itemCount,
}; };
return offer; return offer;
@ -150,9 +164,9 @@ export class RagfairOfferGenerator
let roublePrice = 0; let roublePrice = 0;
for (const requirement of offerRequirements) for (const requirement of offerRequirements)
{ {
roublePrice += this.paymentHelper.isMoneyTpl(requirement._tpl) roublePrice += this.paymentHelper.isMoneyTpl(requirement._tpl) ?
? Math.round(this.calculateRoublePrice(requirement.count, requirement._tpl)) Math.round(this.calculateRoublePrice(requirement.count, requirement._tpl)) :
: this.ragfairPriceService.getFleaPriceForItem(requirement._tpl) * requirement.count; // get flea price for barter offer items this.ragfairPriceService.getFleaPriceForItem(requirement._tpl) * requirement.count; // get flea price for barter offer items
} }
return roublePrice; return roublePrice;
@ -275,7 +289,13 @@ export class RagfairOfferGenerator
} }
// Generated fake-player offer // Generated fake-player offer
return Math.round(time + this.randomUtil.getInt(this.ragfairConfig.dynamic.endTimeSeconds.min, this.ragfairConfig.dynamic.endTimeSeconds.max)); return Math.round(
time +
this.randomUtil.getInt(
this.ragfairConfig.dynamic.endTimeSeconds.min,
this.ragfairConfig.dynamic.endTimeSeconds.max,
),
);
} }
/** /**
@ -287,28 +307,34 @@ export class RagfairOfferGenerator
const config = this.ragfairConfig.dynamic; const config = this.ragfairConfig.dynamic;
// get assort items from param if they exist, otherwise grab freshly generated assorts // get assort items from param if they exist, otherwise grab freshly generated assorts
const assortItemsToProcess: Item[] = (expiredOffers) const assortItemsToProcess: Item[] = expiredOffers ?
? expiredOffers expiredOffers :
: this.ragfairAssortGenerator.getAssortItems(); this.ragfairAssortGenerator.getAssortItems();
// Store all functions to create an offer for every item and pass into Promise.all to run async // Store all functions to create an offer for every item and pass into Promise.all to run async
const assorOffersForItemsProcesses = []; const assorOffersForItemsProcesses = [];
for (const assortItemIndex in assortItemsToProcess) for (const assortItemIndex in assortItemsToProcess)
{ {
assorOffersForItemsProcesses.push(this.createOffersForItems(assortItemIndex, assortItemsToProcess, expiredOffers, config)); assorOffersForItemsProcesses.push(
this.createOffersForItems(assortItemIndex, assortItemsToProcess, expiredOffers, config),
);
} }
await Promise.all(assorOffersForItemsProcesses); await Promise.all(assorOffersForItemsProcesses);
} }
/** /**
*
* @param assortItemIndex Index of assort item * @param assortItemIndex Index of assort item
* @param assortItemsToProcess Item array containing index * @param assortItemsToProcess Item array containing index
* @param expiredOffers Currently expired offers on flea * @param expiredOffers Currently expired offers on flea
* @param config Ragfair dynamic config * @param config Ragfair dynamic config
*/ */
protected async createOffersForItems(assortItemIndex: string, assortItemsToProcess: Item[], expiredOffers: Item[], config: Dynamic): Promise<void> protected async createOffersForItems(
assortItemIndex: string,
assortItemsToProcess: Item[],
expiredOffers: Item[],
config: Dynamic,
): Promise<void>
{ {
const assortItem = assortItemsToProcess[assortItemIndex]; const assortItem = assortItemsToProcess[assortItemIndex];
const itemDetails = this.itemHelper.getItem(assortItem._tpl); const itemDetails = this.itemHelper.getItem(assortItem._tpl);
@ -322,15 +348,21 @@ export class RagfairOfferGenerator
} }
// Get item + sub-items if preset, otherwise just get item // Get item + sub-items if preset, otherwise just get item
const items: Item[] = (isPreset) const items: Item[] = isPreset ?
? this.ragfairServerHelper.getPresetItems(assortItem) this.ragfairServerHelper.getPresetItems(assortItem) :
: [...[assortItem], ...this.itemHelper.findAndReturnChildrenByAssort(assortItem._id, this.ragfairAssortGenerator.getAssortItems())]; [
...[assortItem],
...this.itemHelper.findAndReturnChildrenByAssort(
assortItem._id,
this.ragfairAssortGenerator.getAssortItems(),
),
];
// Get number of offers to create // Get number of offers to create
// Limit to 1 offer when processing expired // Limit to 1 offer when processing expired
const offerCount = (expiredOffers) const offerCount = expiredOffers ?
? 1 1 :
: Math.round(this.randomUtil.getInt(config.offerItemCount.min, config.offerItemCount.max)); Math.round(this.randomUtil.getInt(config.offerItemCount.min, config.offerItemCount.max));
// Store all functions to create offers for this item and pass into Promise.all to run async // Store all functions to create offers for this item and pass into Promise.all to run async
const assortSingleOfferProcesses = []; const assortSingleOfferProcesses = [];
@ -342,7 +374,6 @@ export class RagfairOfferGenerator
await Promise.all(assortSingleOfferProcesses); await Promise.all(assortSingleOfferProcesses);
} }
/** /**
* Create one flea offer for a specific item * Create one flea offer for a specific item
* @param items Item to create offer for * @param items Item to create offer for
@ -350,23 +381,30 @@ export class RagfairOfferGenerator
* @param itemDetails raw db item details * @param itemDetails raw db item details
* @returns Item array * @returns Item array
*/ */
protected async createSingleOfferForItem(items: Item[], isPreset: boolean, itemDetails: [boolean, ITemplateItem]): Promise<void> protected async createSingleOfferForItem(
items: Item[],
isPreset: boolean,
itemDetails: [boolean, ITemplateItem],
): Promise<void>
{ {
// Set stack size to random value // Set stack size to random value
items[0].upd.StackObjectsCount = this.ragfairServerHelper.calculateDynamicStackCount(items[0]._tpl, isPreset); items[0].upd.StackObjectsCount = this.ragfairServerHelper.calculateDynamicStackCount(items[0]._tpl, isPreset);
const isBarterOffer = this.randomUtil.getChance100(this.ragfairConfig.dynamic.barter.chancePercent); const isBarterOffer = this.randomUtil.getChance100(this.ragfairConfig.dynamic.barter.chancePercent);
const isPackOffer = this.randomUtil.getChance100(this.ragfairConfig.dynamic.pack.chancePercent) const isPackOffer = this.randomUtil.getChance100(this.ragfairConfig.dynamic.pack.chancePercent) &&
&& !isBarterOffer !isBarterOffer &&
&& items.length === 1 items.length === 1 &&
&& this.itemHelper.isOfBaseclasses(items[0]._tpl, this.ragfairConfig.dynamic.pack.itemTypeWhitelist); this.itemHelper.isOfBaseclasses(items[0]._tpl, this.ragfairConfig.dynamic.pack.itemTypeWhitelist);
const randomUserId = this.hashUtil.generate(); const randomUserId = this.hashUtil.generate();
let barterScheme: IBarterScheme[]; let barterScheme: IBarterScheme[];
if (isPackOffer) if (isPackOffer)
{ {
// Set pack size // Set pack size
const stackSize = this.randomUtil.getInt(this.ragfairConfig.dynamic.pack.itemCountMin, this.ragfairConfig.dynamic.pack.itemCountMax); const stackSize = this.randomUtil.getInt(
this.ragfairConfig.dynamic.pack.itemCountMin,
this.ragfairConfig.dynamic.pack.itemCountMax,
);
items[0].upd.StackObjectsCount = stackSize; items[0].upd.StackObjectsCount = stackSize;
// Don't randomise pack items // Don't randomise pack items
@ -391,7 +429,8 @@ export class RagfairOfferGenerator
items, items,
barterScheme, barterScheme,
1, 1,
isPreset || isPackOffer); // sellAsOnePiece isPreset || isPackOffer,
); // sellAsOnePiece
this.ragfairCategoriesService.incrementCategory(offer); this.ragfairCategoriesService.incrementCategory(offer);
} }
@ -413,7 +452,12 @@ export class RagfairOfferGenerator
// Trader assorts / assort items are missing // Trader assorts / assort items are missing
if (!assorts?.items?.length) if (!assorts?.items?.length)
{ {
this.logger.error(this.localisationService.getText("ragfair-no_trader_assorts_cant_generate_flea_offers", trader.base.nickname)); this.logger.error(
this.localisationService.getText(
"ragfair-no_trader_assorts_cant_generate_flea_offers",
trader.base.nickname,
),
);
return; return;
} }
@ -444,14 +488,20 @@ export class RagfairOfferGenerator
} }
const isPreset = this.presetHelper.isPreset(item._id); const isPreset = this.presetHelper.isPreset(item._id);
const items: Item[] = (isPreset) const items: Item[] = isPreset ?
? this.ragfairServerHelper.getPresetItems(item) this.ragfairServerHelper.getPresetItems(item) :
: [...[item], ...this.itemHelper.findAndReturnChildrenByAssort(item._id, assorts.items)]; [...[item], ...this.itemHelper.findAndReturnChildrenByAssort(item._id, assorts.items)];
const barterScheme = assorts.barter_scheme[item._id]; const barterScheme = assorts.barter_scheme[item._id];
if (!barterScheme) if (!barterScheme)
{ {
this.logger.warning(this.localisationService.getText("ragfair-missing_barter_scheme", {itemId: item._id, tpl: item._tpl, name: trader.base.nickname})); this.logger.warning(
this.localisationService.getText("ragfair-missing_barter_scheme", {
itemId: item._id,
tpl: item._tpl,
name: trader.base.nickname,
}),
);
continue; continue;
} }
@ -527,7 +577,10 @@ export class RagfairOfferGenerator
*/ */
protected randomiseItemCondition(conditionSettingsId: string, item: Item, itemDetails: ITemplateItem): void protected randomiseItemCondition(conditionSettingsId: string, item: Item, itemDetails: ITemplateItem): void
{ {
const multiplier = this.randomUtil.getFloat(this.ragfairConfig.dynamic.condition[conditionSettingsId].min, this.ragfairConfig.dynamic.condition[conditionSettingsId].max); const multiplier = this.randomUtil.getFloat(
this.ragfairConfig.dynamic.condition[conditionSettingsId].min,
this.ragfairConfig.dynamic.condition[conditionSettingsId].max,
);
// Armor or weapons // Armor or weapons
if (item.upd.Repairable) if (item.upd.Repairable)
@ -585,7 +638,7 @@ export class RagfairOfferGenerator
const remainingFuel = Math.round(totalCapacity * multiplier); const remainingFuel = Math.round(totalCapacity * multiplier);
item.upd.Resource = { item.upd.Resource = {
UnitsConsumed: totalCapacity - remainingFuel, UnitsConsumed: totalCapacity - remainingFuel,
Value: remainingFuel Value: remainingFuel,
}; };
} }
} }
@ -600,7 +653,9 @@ export class RagfairOfferGenerator
item.upd.Repairable.Durability = Math.round(item.upd.Repairable.Durability * multiplier) || 1; item.upd.Repairable.Durability = Math.round(item.upd.Repairable.Durability * multiplier) || 1;
// randomize max durability, store to a temporary value so we can still compare the max durability // randomize max durability, store to a temporary value so we can still compare the max durability
let tempMaxDurability = Math.round(this.randomUtil.getFloat(item.upd.Repairable.Durability - 5, item.upd.Repairable.MaxDurability + 5)) || item.upd.Repairable.Durability; let tempMaxDurability = Math.round(
this.randomUtil.getFloat(item.upd.Repairable.Durability - 5, item.upd.Repairable.MaxDurability + 5),
) || item.upd.Repairable.Durability;
// clamp values to max/current // clamp values to max/current
if (tempMaxDurability >= item.upd.Repairable.MaxDurability) if (tempMaxDurability >= item.upd.Repairable.MaxDurability)
@ -626,45 +681,45 @@ export class RagfairOfferGenerator
protected addMissingConditions(item: Item): Item protected addMissingConditions(item: Item): Item
{ {
const props = this.itemHelper.getItem(item._tpl)[1]._props; const props = this.itemHelper.getItem(item._tpl)[1]._props;
const isRepairable = ("Durability" in props); const isRepairable = "Durability" in props;
const isMedkit = ("MaxHpResource" in props); const isMedkit = "MaxHpResource" in props;
const isKey = ("MaximumNumberOfUsage" in props); const isKey = "MaximumNumberOfUsage" in props;
const isConsumable = (props.MaxResource > 1 && "foodUseTime" in props); const isConsumable = props.MaxResource > 1 && "foodUseTime" in props;
const isRepairKit = ("MaxRepairResource" in props); const isRepairKit = "MaxRepairResource" in props;
if (isRepairable && props.Durability > 0) if (isRepairable && props.Durability > 0)
{ {
item.upd.Repairable = { item.upd.Repairable = {
Durability: props.Durability, Durability: props.Durability,
MaxDurability: props.Durability MaxDurability: props.Durability,
}; };
} }
if (isMedkit && props.MaxHpResource > 0) if (isMedkit && props.MaxHpResource > 0)
{ {
item.upd.MedKit = { item.upd.MedKit = {
HpResource: props.MaxHpResource HpResource: props.MaxHpResource,
}; };
} }
if (isKey) if (isKey)
{ {
item.upd.Key = { item.upd.Key = {
NumberOfUsages: 0 NumberOfUsages: 0,
}; };
} }
if (isConsumable) if (isConsumable)
{ {
item.upd.FoodDrink = { item.upd.FoodDrink = {
HpPercent: props.MaxResource HpPercent: props.MaxResource,
}; };
} }
if (isRepairKit) if (isRepairKit)
{ {
item.upd.RepairKit = { item.upd.RepairKit = {
Resource: props.MaxRepairResource Resource: props.MaxRepairResource,
}; };
} }
@ -679,7 +734,11 @@ export class RagfairOfferGenerator
protected createBarterBarterScheme(offerItems: Item[]): IBarterScheme[] protected createBarterBarterScheme(offerItems: Item[]): IBarterScheme[]
{ {
// get flea price of item being sold // get flea price of item being sold
const priceOfItemOffer = this.ragfairPriceService.getDynamicOfferPriceForOffer(offerItems, Money.ROUBLES, false); const priceOfItemOffer = this.ragfairPriceService.getDynamicOfferPriceForOffer(
offerItems,
Money.ROUBLES,
false,
);
// Dont make items under a designated rouble value into barter offers // Dont make items under a designated rouble value into barter offers
if (priceOfItemOffer < this.ragfairConfig.dynamic.barter.minRoubleCostToBecomeBarter) if (priceOfItemOffer < this.ragfairConfig.dynamic.barter.minRoubleCostToBecomeBarter)
@ -688,7 +747,10 @@ export class RagfairOfferGenerator
} }
// Get a randomised number of barter items to list offer for // Get a randomised number of barter items to list offer for
const barterItemCount = this.randomUtil.getInt(this.ragfairConfig.dynamic.barter.itemCountMin, this.ragfairConfig.dynamic.barter.itemCountMax); const barterItemCount = this.randomUtil.getInt(
this.ragfairConfig.dynamic.barter.itemCountMin,
this.ragfairConfig.dynamic.barter.itemCountMax,
);
// Get desired cost of individual item offer will be listed for e.g. offer = 15k, item count = 3, desired item cost = 5k // Get desired cost of individual item offer will be listed for e.g. offer = 15k, item count = 3, desired item cost = 5k
const desiredItemCost = Math.round(priceOfItemOffer / barterItemCount); const desiredItemCost = Math.round(priceOfItemOffer / barterItemCount);
@ -699,7 +761,10 @@ export class RagfairOfferGenerator
const fleaPrices = this.getFleaPricesAsArray(); const fleaPrices = this.getFleaPricesAsArray();
// Filter possible barters to items that match the price range + not itself // Filter possible barters to items that match the price range + not itself
const filtered = fleaPrices.filter(x => x.price >= desiredItemCost - offerCostVariance && x.price <= desiredItemCost + offerCostVariance && x.tpl !== offerItems[0]._tpl); const filtered = fleaPrices.filter((x) =>
x.price >= desiredItemCost - offerCostVariance && x.price <= desiredItemCost + offerCostVariance &&
x.tpl !== offerItems[0]._tpl
);
// No items on flea have a matching price, fall back to currency // No items on flea have a matching price, fall back to currency
if (filtered.length === 0) if (filtered.length === 0)
@ -713,8 +778,8 @@ export class RagfairOfferGenerator
return [ return [
{ {
count: barterItemCount, count: barterItemCount,
_tpl: randomItem.tpl _tpl: randomItem.tpl,
} },
]; ];
} }
@ -722,18 +787,20 @@ export class RagfairOfferGenerator
* Get an array of flea prices + item tpl, cached in generator class inside `allowedFleaPriceItemsForBarter` * Get an array of flea prices + item tpl, cached in generator class inside `allowedFleaPriceItemsForBarter`
* @returns array with tpl/price values * @returns array with tpl/price values
*/ */
protected getFleaPricesAsArray(): { tpl: string; price: number; }[] protected getFleaPricesAsArray(): {tpl: string; price: number;}[]
{ {
// Generate if needed // Generate if needed
if (!this.allowedFleaPriceItemsForBarter) if (!this.allowedFleaPriceItemsForBarter)
{ {
const fleaPrices = this.databaseServer.getTables().templates.prices; const fleaPrices = this.databaseServer.getTables().templates.prices;
const fleaArray = Object.entries(fleaPrices).map(([tpl, price]) => ({ tpl: tpl, price: price })); const fleaArray = Object.entries(fleaPrices).map(([tpl, price]) => ({tpl: tpl, price: price}));
// Only get item prices for items that also exist in items.json // Only get item prices for items that also exist in items.json
const filteredItems = fleaArray.filter(x => this.itemHelper.getItem(x.tpl)[0]); const filteredItems = fleaArray.filter((x) => this.itemHelper.getItem(x.tpl)[0]);
this.allowedFleaPriceItemsForBarter = filteredItems.filter(x => !this.itemHelper.isOfBaseclasses(x.tpl, this.ragfairConfig.dynamic.barter.itemTypeBlacklist)); this.allowedFleaPriceItemsForBarter = filteredItems.filter((x) =>
!this.itemHelper.isOfBaseclasses(x.tpl, this.ragfairConfig.dynamic.barter.itemTypeBlacklist)
);
} }
return this.allowedFleaPriceItemsForBarter; return this.allowedFleaPriceItemsForBarter;
@ -749,13 +816,14 @@ export class RagfairOfferGenerator
protected createCurrencyBarterScheme(offerItems: Item[], isPackOffer: boolean, multipler = 1): IBarterScheme[] protected createCurrencyBarterScheme(offerItems: Item[], isPackOffer: boolean, multipler = 1): IBarterScheme[]
{ {
const currency = this.ragfairServerHelper.getDynamicOfferCurrency(); const currency = this.ragfairServerHelper.getDynamicOfferCurrency();
const price = this.ragfairPriceService.getDynamicOfferPriceForOffer(offerItems, currency, isPackOffer) * multipler; const price = this.ragfairPriceService.getDynamicOfferPriceForOffer(offerItems, currency, isPackOffer) *
multipler;
return [ return [
{ {
count: price, count: price,
_tpl: currency _tpl: currency,
} },
]; ];
} }
} }

View File

@ -15,16 +15,25 @@ import {
IEliminationCondition, IEliminationCondition,
IEquipmentConditionProps, IEquipmentConditionProps,
IExploration, IExploration,
IExplorationCondition, IKillConditionProps, IExplorationCondition,
IKillConditionProps,
IPickup, IPickup,
IRepeatableQuest, IReward, IRewards IRepeatableQuest,
IReward,
IRewards,
} from "@spt-aki/models/eft/common/tables/IRepeatableQuests"; } from "@spt-aki/models/eft/common/tables/IRepeatableQuests";
import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem"; import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem";
import { BaseClasses } from "@spt-aki/models/enums/BaseClasses"; import { BaseClasses } from "@spt-aki/models/enums/BaseClasses";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { Money } from "@spt-aki/models/enums/Money"; import { Money } from "@spt-aki/models/enums/Money";
import { Traders } from "@spt-aki/models/enums/Traders"; import { Traders } from "@spt-aki/models/enums/Traders";
import { IBaseQuestConfig, IBossInfo, IEliminationConfig, IQuestConfig, IRepeatableQuestConfig } from "@spt-aki/models/spt/config/IQuestConfig"; import {
IBaseQuestConfig,
IBossInfo,
IEliminationConfig,
IQuestConfig,
IRepeatableQuestConfig,
} from "@spt-aki/models/spt/config/IQuestConfig";
import { IQuestTypePool } from "@spt-aki/models/spt/repeatable/IQuestTypePool"; import { IQuestTypePool } from "@spt-aki/models/spt/repeatable/IQuestTypePool";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { EventOutputHolder } from "@spt-aki/routers/EventOutputHolder"; import { EventOutputHolder } from "@spt-aki/routers/EventOutputHolder";
@ -66,7 +75,7 @@ export class RepeatableQuestGenerator
@inject("ObjectId") protected objectId: ObjectId, @inject("ObjectId") protected objectId: ObjectId,
@inject("ItemFilterService") protected itemFilterService: ItemFilterService, @inject("ItemFilterService") protected itemFilterService: ItemFilterService,
@inject("RepeatableQuestHelper") protected repeatableQuestHelper: RepeatableQuestHelper, @inject("RepeatableQuestHelper") protected repeatableQuestHelper: RepeatableQuestHelper,
@inject("ConfigServer") protected configServer: ConfigServer @inject("ConfigServer") protected configServer: ConfigServer,
) )
{ {
this.questConfig = this.configServer.getConfig(ConfigTypes.QUEST); this.questConfig = this.configServer.getConfig(ConfigTypes.QUEST);
@ -85,15 +94,17 @@ export class RepeatableQuestGenerator
pmcLevel: number, pmcLevel: number,
pmcTraderInfo: Record<string, TraderInfo>, pmcTraderInfo: Record<string, TraderInfo>,
questTypePool: IQuestTypePool, questTypePool: IQuestTypePool,
repeatableConfig: IRepeatableQuestConfig repeatableConfig: IRepeatableQuestConfig,
): IRepeatableQuest ): IRepeatableQuest
{ {
const questType = this.randomUtil.drawRandomFromList<string>(questTypePool.types)[0]; const questType = this.randomUtil.drawRandomFromList<string>(questTypePool.types)[0];
// get traders from whitelist and filter by quest type availability // get traders from whitelist and filter by quest type availability
let traders = repeatableConfig.traderWhitelist.filter(x => x.questTypes.includes(questType)).map(x => x.traderId); let traders = repeatableConfig.traderWhitelist.filter((x) => x.questTypes.includes(questType)).map((x) =>
x.traderId
);
// filter out locked traders // filter out locked traders
traders = traders.filter(x => pmcTraderInfo[x].unlocked); traders = traders.filter((x) => pmcTraderInfo[x].unlocked);
const traderId = this.randomUtil.drawRandomFromList(traders)[0]; const traderId = this.randomUtil.drawRandomFromList(traders)[0];
switch (questType) switch (questType)
@ -123,15 +134,19 @@ export class RepeatableQuestGenerator
pmcLevel: number, pmcLevel: number,
traderId: string, traderId: string,
questTypePool: IQuestTypePool, questTypePool: IQuestTypePool,
repeatableConfig: IRepeatableQuestConfig repeatableConfig: IRepeatableQuestConfig,
): IElimination ): IElimination
{ {
const eliminationConfig = this.repeatableQuestHelper.getEliminationConfigByPmcLevel(pmcLevel, repeatableConfig); const eliminationConfig = this.repeatableQuestHelper.getEliminationConfigByPmcLevel(pmcLevel, repeatableConfig);
const locationsConfig = repeatableConfig.locations; const locationsConfig = repeatableConfig.locations;
let targetsConfig = this.repeatableQuestHelper.probabilityObjectArray(eliminationConfig.targets); let targetsConfig = this.repeatableQuestHelper.probabilityObjectArray(eliminationConfig.targets);
const bodypartsConfig = this.repeatableQuestHelper.probabilityObjectArray(eliminationConfig.bodyParts); const bodypartsConfig = this.repeatableQuestHelper.probabilityObjectArray(eliminationConfig.bodyParts);
const weaponCategoryRequirementConfig = this.repeatableQuestHelper.probabilityObjectArray(eliminationConfig.weaponCategoryRequirements); const weaponCategoryRequirementConfig = this.repeatableQuestHelper.probabilityObjectArray(
const weaponRequirementConfig = this.repeatableQuestHelper.probabilityObjectArray(eliminationConfig.weaponRequirements); eliminationConfig.weaponCategoryRequirements,
);
const weaponRequirementConfig = this.repeatableQuestHelper.probabilityObjectArray(
eliminationConfig.weaponRequirements,
);
// the difficulty of the quest varies in difficulty depending on the condition // the difficulty of the quest varies in difficulty depending on the condition
// possible conditions are // possible conditions are
@ -146,7 +161,7 @@ export class RepeatableQuestGenerator
// Savage: 7, // Savage: 7,
// AnyPmc: 2, // AnyPmc: 2,
// bossBully: 0.5 // bossBully: 0.5
//} // }
// higher is more likely. We define the difficulty to be the inverse of the relative probability. // higher is more likely. We define the difficulty to be the inverse of the relative probability.
// We want to generate a reward which is scaled by the difficulty of this mission. To get a upper bound with which we scale // We want to generate a reward which is scaled by the difficulty of this mission. To get a upper bound with which we scale
@ -165,18 +180,26 @@ export class RepeatableQuestGenerator
const maxKillDifficulty = eliminationConfig.maxKills; const maxKillDifficulty = eliminationConfig.maxKills;
function difficultyWeighing(target: number, bodyPart: number, dist: number, kill: number, weaponRequirement: number): number function difficultyWeighing(
target: number,
bodyPart: number,
dist: number,
kill: number,
weaponRequirement: number,
): number
{ {
return Math.sqrt(Math.sqrt(target) + bodyPart + dist + weaponRequirement) * kill; return Math.sqrt(Math.sqrt(target) + bodyPart + dist + weaponRequirement) * kill;
} }
targetsConfig = targetsConfig.filter(x => Object.keys(questTypePool.pool.Elimination.targets).includes(x.key)); targetsConfig = targetsConfig.filter((x) =>
if (targetsConfig.length === 0 || targetsConfig.every(x => x.data.isBoss)) Object.keys(questTypePool.pool.Elimination.targets).includes(x.key)
);
if (targetsConfig.length === 0 || targetsConfig.every((x) => x.data.isBoss))
{ {
// There are no more targets left for elimination; delete it as a possible quest type // There are no more targets left for elimination; delete it as a possible quest type
// also if only bosses are left we need to leave otherwise it's a guaranteed boss elimination // also if only bosses are left we need to leave otherwise it's a guaranteed boss elimination
// -> then it would not be a quest with low probability anymore // -> then it would not be a quest with low probability anymore
questTypePool.types = questTypePool.types.filter(t => t !== "Elimination"); questTypePool.types = questTypePool.types.filter((t) => t !== "Elimination");
return null; return null;
} }
@ -188,18 +211,23 @@ export class RepeatableQuestGenerator
// we use any as location if "any" is in the pool and we do not hit the specific location random // we use any as location if "any" is in the pool and we do not hit the specific location random
// we use any also if the random condition is not met in case only "any" was in the pool // we use any also if the random condition is not met in case only "any" was in the pool
let locationKey = "any"; let locationKey = "any";
if (locations.includes("any") && (eliminationConfig.specificLocationProb < Math.random() || locations.length <= 1)) if (
locations.includes("any") &&
(eliminationConfig.specificLocationProb < Math.random() || locations.length <= 1)
)
{ {
locationKey = "any"; locationKey = "any";
delete questTypePool.pool.Elimination.targets[targetKey]; delete questTypePool.pool.Elimination.targets[targetKey];
} }
else else
{ {
locations = locations.filter(l => l !== "any"); locations = locations.filter((l) => l !== "any");
if (locations.length > 0) if (locations.length > 0)
{ {
locationKey = this.randomUtil.drawRandomFromList<string>(locations)[0]; locationKey = this.randomUtil.drawRandomFromList<string>(locations)[0];
questTypePool.pool.Elimination.targets[targetKey].locations = locations.filter(l => l !== locationKey); questTypePool.pool.Elimination.targets[targetKey].locations = locations.filter((l) =>
l !== locationKey
);
if (questTypePool.pool.Elimination.targets[targetKey].locations.length === 0) if (questTypePool.pool.Elimination.targets[targetKey].locations.length === 0)
{ {
delete questTypePool.pool.Elimination.targets[targetKey]; delete questTypePool.pool.Elimination.targets[targetKey];
@ -243,15 +271,17 @@ export class RepeatableQuestGenerator
if (targetsConfig.data(targetKey).isBoss) if (targetsConfig.data(targetKey).isBoss)
{ {
// get all boss spawn information // get all boss spawn information
const bossSpawns = Object.values(this.databaseServer.getTables().locations).filter(x => "base" in x && "Id" in x.base).map( const bossSpawns = Object.values(this.databaseServer.getTables().locations).filter((x) =>
(x) => ({ Id: x.base.Id, BossSpawn: x.base.BossLocationSpawn }) "base" in x && "Id" in x.base
).map(
(x) => ({Id: x.base.Id, BossSpawn: x.base.BossLocationSpawn}),
); );
// filter for the current boss to spawn on map // filter for the current boss to spawn on map
const thisBossSpawns = bossSpawns.map( const thisBossSpawns = bossSpawns.map(
(x) => ({ Id: x.Id, BossSpawn: x.BossSpawn.filter(e => e.BossName === targetKey) }) (x) => ({Id: x.Id, BossSpawn: x.BossSpawn.filter((e) => e.BossName === targetKey)}),
).filter(x => x.BossSpawn.length > 0); ).filter((x) => x.BossSpawn.length > 0);
// remove blacklisted locations // remove blacklisted locations
const allowedSpawns = thisBossSpawns.filter(x => !eliminationConfig.distLocationBlacklist.includes(x.Id)); const allowedSpawns = thisBossSpawns.filter((x) => !eliminationConfig.distLocationBlacklist.includes(x.Id));
// if the boss spawns on nom-blacklisted locations and the current location is allowed we can generate a distance kill requirement // if the boss spawns on nom-blacklisted locations and the current location is allowed we can generate a distance kill requirement
isDistanceRequirementAllowed = isDistanceRequirementAllowed && (allowedSpawns.length > 0); isDistanceRequirementAllowed = isDistanceRequirementAllowed && (allowedSpawns.length > 0);
} }
@ -259,7 +289,10 @@ export class RepeatableQuestGenerator
if (eliminationConfig.distProb > Math.random() && isDistanceRequirementAllowed) if (eliminationConfig.distProb > Math.random() && isDistanceRequirementAllowed)
{ {
// random distance with lower values more likely; simple distribution for starters... // random distance with lower values more likely; simple distribution for starters...
distance = Math.floor(Math.abs(Math.random() - Math.random()) * (1 + eliminationConfig.maxDist - eliminationConfig.minDist) + eliminationConfig.minDist); distance = Math.floor(
Math.abs(Math.random() - Math.random()) * (1 + eliminationConfig.maxDist - eliminationConfig.minDist) +
eliminationConfig.minDist,
);
distance = Math.ceil(distance / 5) * 5; distance = Math.ceil(distance / 5) * 5;
distanceDifficulty = maxDistDifficulty * distance / eliminationConfig.maxDist; distanceDifficulty = maxDistDifficulty * distance / eliminationConfig.maxDist;
} }
@ -296,7 +329,7 @@ export class RepeatableQuestGenerator
bodyPartDifficulty / maxBodyPartsDifficulty, bodyPartDifficulty / maxBodyPartsDifficulty,
distanceDifficulty / maxDistDifficulty, distanceDifficulty / maxDistDifficulty,
killDifficulty / maxKillDifficulty, killDifficulty / maxKillDifficulty,
(allowedWeaponsCategory || allowedWeapon) ? 1 : 0 (allowedWeaponsCategory || allowedWeapon) ? 1 : 0,
); );
// Aforementioned issue makes it a bit crazy since now all easier quests give significantly lower rewards than Completion / Exploration // Aforementioned issue makes it a bit crazy since now all easier quests give significantly lower rewards than Completion / Exploration
@ -319,14 +352,30 @@ export class RepeatableQuestGenerator
// Only add specific location condition if specific map selected // Only add specific location condition if specific map selected
if (locationKey !== "any") if (locationKey !== "any")
{ {
availableForFinishCondition._props.counter.conditions.push(this.generateEliminationLocation(locationsConfig[locationKey])); availableForFinishCondition._props.counter.conditions.push(
this.generateEliminationLocation(locationsConfig[locationKey]),
);
} }
availableForFinishCondition._props.counter.conditions.push(this.generateEliminationCondition(targetKey, bodyPartsToClient, distance, allowedWeapon, allowedWeaponsCategory)); availableForFinishCondition._props.counter.conditions.push(
this.generateEliminationCondition(
targetKey,
bodyPartsToClient,
distance,
allowedWeapon,
allowedWeaponsCategory,
),
);
availableForFinishCondition._props.value = desiredKillCount; availableForFinishCondition._props.value = desiredKillCount;
availableForFinishCondition._props.id = this.objectId.generate(); availableForFinishCondition._props.id = this.objectId.generate();
quest.location = this.getQuestLocationByMapId(locationKey); quest.location = this.getQuestLocationByMapId(locationKey);
quest.rewards = this.generateReward(pmcLevel, Math.min(difficulty, 1), traderId, repeatableConfig, eliminationConfig); quest.rewards = this.generateReward(
pmcLevel,
Math.min(difficulty, 1),
traderId,
repeatableConfig,
eliminationConfig,
);
return quest; return quest;
} }
@ -338,7 +387,11 @@ export class RepeatableQuestGenerator
* @param eliminationConfig Config * @param eliminationConfig Config
* @returns Number of AI to kill * @returns Number of AI to kill
*/ */
protected getEliminationKillCount(targetKey: string, targetsConfig: ProbabilityObjectArray<string, IBossInfo>, eliminationConfig: IEliminationConfig): number protected getEliminationKillCount(
targetKey: string,
targetsConfig: ProbabilityObjectArray<string, IBossInfo>,
eliminationConfig: IEliminationConfig,
): number
{ {
if (targetsConfig.data(targetKey).isBoss) if (targetsConfig.data(targetKey).isBoss)
{ {
@ -366,9 +419,9 @@ export class RepeatableQuestGenerator
_props: { _props: {
target: location, target: location,
id: this.objectId.generate(), id: this.objectId.generate(),
dynamicLocale: true dynamicLocale: true,
}, },
_parent: "Location" _parent: "Location",
}; };
return propsObject; return propsObject;
@ -383,13 +436,19 @@ export class RepeatableQuestGenerator
* @param allowedWeaponCategory What category of weapon must be used - undefined = any * @param allowedWeaponCategory What category of weapon must be used - undefined = any
* @returns IEliminationCondition object * @returns IEliminationCondition object
*/ */
protected generateEliminationCondition(target: string, targetedBodyParts: string[], distance: number, allowedWeapon: string, allowedWeaponCategory: string): IEliminationCondition protected generateEliminationCondition(
target: string,
targetedBodyParts: string[],
distance: number,
allowedWeapon: string,
allowedWeaponCategory: string,
): IEliminationCondition
{ {
const killConditionProps: IKillConditionProps = { const killConditionProps: IKillConditionProps = {
target: target, target: target,
value: 1, value: 1,
id: this.objectId.generate(), id: this.objectId.generate(),
dynamicLocale: true dynamicLocale: true,
}; };
if (target.startsWith("boss")) if (target.startsWith("boss"))
@ -409,7 +468,7 @@ export class RepeatableQuestGenerator
{ {
killConditionProps.distance = { killConditionProps.distance = {
compareMethod: ">=", compareMethod: ">=",
value: distance value: distance,
}; };
} }
@ -427,7 +486,7 @@ export class RepeatableQuestGenerator
return { return {
_props: killConditionProps, _props: killConditionProps,
_parent: "Kills" _parent: "Kills",
}; };
} }
@ -442,7 +501,7 @@ export class RepeatableQuestGenerator
protected generateCompletionQuest( protected generateCompletionQuest(
pmcLevel: number, pmcLevel: number,
traderId: string, traderId: string,
repeatableConfig: IRepeatableQuestConfig repeatableConfig: IRepeatableQuestConfig,
): ICompletion ): ICompletion
{ {
const completionConfig = repeatableConfig.questConfig.Completion; const completionConfig = repeatableConfig.questConfig.Completion;
@ -456,48 +515,64 @@ export class RepeatableQuestGenerator
numberDistinctItems = 2; numberDistinctItems = 2;
} }
const quest = this.generateRepeatableTemplate("Completion", traderId,repeatableConfig.side) as ICompletion; const quest = this.generateRepeatableTemplate("Completion", traderId, repeatableConfig.side) as ICompletion;
// Filter the items.json items to items the player must retrieve to complete queist: shouldn't be a quest item or "non-existant" // Filter the items.json items to items the player must retrieve to complete queist: shouldn't be a quest item or "non-existant"
let itemSelection = this.getRewardableItems(repeatableConfig); let itemSelection = this.getRewardableItems(repeatableConfig);
// Be fair, don't let the items be more expensive than the reward // Be fair, don't let the items be more expensive than the reward
let roublesBudget = Math.floor(this.mathUtil.interp1(pmcLevel, levelsConfig, roublesConfig) * this.randomUtil.getFloat(0.5, 1)); let roublesBudget = Math.floor(
this.mathUtil.interp1(pmcLevel, levelsConfig, roublesConfig) * this.randomUtil.getFloat(0.5, 1),
);
roublesBudget = Math.max(roublesBudget, 5000); roublesBudget = Math.max(roublesBudget, 5000);
itemSelection = itemSelection.filter(x => this.itemHelper.getItemPrice(x[0]) < roublesBudget); itemSelection = itemSelection.filter((x) => this.itemHelper.getItemPrice(x[0]) < roublesBudget);
// We also have the option to use whitelist and/or blacklist which is defined in repeatableQuests.json as // We also have the option to use whitelist and/or blacklist which is defined in repeatableQuests.json as
// [{minPlayerLevel: 1, itemIds: ["id1",...]}, {minPlayerLevel: 15, itemIds: ["id3",...]}] // [{minPlayerLevel: 1, itemIds: ["id1",...]}, {minPlayerLevel: 15, itemIds: ["id3",...]}]
if (repeatableConfig.questConfig.Completion.useWhitelist) if (repeatableConfig.questConfig.Completion.useWhitelist)
{ {
const itemWhitelist = this.databaseServer.getTables().templates.repeatableQuests.data.Completion.itemsWhitelist; const itemWhitelist =
this.databaseServer.getTables().templates.repeatableQuests.data.Completion.itemsWhitelist;
// Filter and concatenate the arrays according to current player level // Filter and concatenate the arrays according to current player level
const itemIdsWhitelisted = itemWhitelist.filter(p => p.minPlayerLevel <= pmcLevel).reduce((a, p) => a.concat(p.itemIds), []); const itemIdsWhitelisted = itemWhitelist.filter((p) => p.minPlayerLevel <= pmcLevel).reduce(
itemSelection = itemSelection.filter(x => (a, p) => a.concat(p.itemIds),
[],
);
itemSelection = itemSelection.filter((x) =>
{ {
// Whitelist can contain item tpls and item base type ids // Whitelist can contain item tpls and item base type ids
return (itemIdsWhitelisted.some(v => this.itemHelper.isOfBaseclass(x[0], v)) || itemIdsWhitelisted.includes(x[0])); return (itemIdsWhitelisted.some((v) => this.itemHelper.isOfBaseclass(x[0], v)) ||
itemIdsWhitelisted.includes(x[0]));
}); });
// check if items are missing // check if items are missing
//const flatList = itemSelection.reduce((a, il) => a.concat(il[0]), []); // const flatList = itemSelection.reduce((a, il) => a.concat(il[0]), []);
//const missing = itemIdsWhitelisted.filter(l => !flatList.includes(l)); // const missing = itemIdsWhitelisted.filter(l => !flatList.includes(l));
} }
if (repeatableConfig.questConfig.Completion.useBlacklist) if (repeatableConfig.questConfig.Completion.useBlacklist)
{ {
const itemBlacklist = this.databaseServer.getTables().templates.repeatableQuests.data.Completion.itemsBlacklist; const itemBlacklist =
this.databaseServer.getTables().templates.repeatableQuests.data.Completion.itemsBlacklist;
// we filter and concatenate the arrays according to current player level // we filter and concatenate the arrays according to current player level
const itemIdsBlacklisted = itemBlacklist.filter(p => p.minPlayerLevel <= pmcLevel).reduce((a, p) => a.concat(p.itemIds), []); const itemIdsBlacklisted = itemBlacklist.filter((p) => p.minPlayerLevel <= pmcLevel).reduce(
itemSelection = itemSelection.filter(x => (a, p) => a.concat(p.itemIds),
[],
);
itemSelection = itemSelection.filter((x) =>
{ {
return itemIdsBlacklisted.every(v => !this.itemHelper.isOfBaseclass(x[0], v)) || !itemIdsBlacklisted.includes(x[0]); return itemIdsBlacklisted.every((v) => !this.itemHelper.isOfBaseclass(x[0], v)) ||
!itemIdsBlacklisted.includes(x[0]);
}); });
} }
if (itemSelection.length === 0) if (itemSelection.length === 0)
{ {
this.logger.error(this.localisationService.getText("repeatable-completion_quest_whitelist_too_small_or_blacklist_too_restrictive")); this.logger.error(
this.localisationService.getText(
"repeatable-completion_quest_whitelist_too_small_or_blacklist_too_restrictive",
),
);
return null; return null;
} }
@ -532,7 +607,7 @@ export class RepeatableQuestGenerator
if (roublesBudget > 0) if (roublesBudget > 0)
{ {
// reduce the list possible items to fulfill the new budget constraint // reduce the list possible items to fulfill the new budget constraint
itemSelection = itemSelection.filter(x => this.itemHelper.getItemPrice(x[0]) < roublesBudget); itemSelection = itemSelection.filter((x) => this.itemHelper.getItemPrice(x[0]) < roublesBudget);
if (itemSelection.length === 0) if (itemSelection.length === 0)
{ {
break; break;
@ -561,12 +636,18 @@ export class RepeatableQuestGenerator
{ {
let minDurability = 0; let minDurability = 0;
let onlyFoundInRaid = true; let onlyFoundInRaid = true;
if (this.itemHelper.isOfBaseclass(targetItemId, BaseClasses.WEAPON) || this.itemHelper.isOfBaseclass(targetItemId, BaseClasses.ARMOR)) if (
this.itemHelper.isOfBaseclass(targetItemId, BaseClasses.WEAPON) ||
this.itemHelper.isOfBaseclass(targetItemId, BaseClasses.ARMOR)
)
{ {
minDurability = 80; minDurability = 80;
} }
if (this.itemHelper.isOfBaseclass(targetItemId, BaseClasses.DOG_TAG_USEC) || this.itemHelper.isOfBaseclass(targetItemId, BaseClasses.DOG_TAG_BEAR)) if (
this.itemHelper.isOfBaseclass(targetItemId, BaseClasses.DOG_TAG_USEC) ||
this.itemHelper.isOfBaseclass(targetItemId, BaseClasses.DOG_TAG_BEAR)
)
{ {
onlyFoundInRaid = false; onlyFoundInRaid = false;
} }
@ -583,10 +664,10 @@ export class RepeatableQuestGenerator
minDurability: minDurability, minDurability: minDurability,
maxDurability: 100, maxDurability: 100,
dogtagLevel: 0, dogtagLevel: 0,
onlyFoundInRaid: onlyFoundInRaid onlyFoundInRaid: onlyFoundInRaid,
}, },
_parent: "HandoverItem", _parent: "HandoverItem",
dynamicLocale: true dynamicLocale: true,
}; };
} }
@ -603,7 +684,7 @@ export class RepeatableQuestGenerator
pmcLevel: number, pmcLevel: number,
traderId: string, traderId: string,
questTypePool: IQuestTypePool, questTypePool: IQuestTypePool,
repeatableConfig: IRepeatableQuestConfig repeatableConfig: IRepeatableQuestConfig,
): IExploration ): IExploration
{ {
const explorationConfig = repeatableConfig.questConfig.Exploration; const explorationConfig = repeatableConfig.questConfig.Exploration;
@ -611,7 +692,7 @@ export class RepeatableQuestGenerator
if (Object.keys(questTypePool.pool.Exploration.locations).length === 0) if (Object.keys(questTypePool.pool.Exploration.locations).length === 0)
{ {
// there are no more locations left for exploration; delete it as a possible quest type // there are no more locations left for exploration; delete it as a possible quest type
questTypePool.types = questTypePool.types.filter(t => t !== "Exploration"); questTypePool.types = questTypePool.types.filter((t) => t !== "Exploration");
return null; return null;
} }
@ -625,7 +706,7 @@ export class RepeatableQuestGenerator
const numExtracts = this.randomUtil.randInt(1, explorationConfig.maxExtracts + 1); const numExtracts = this.randomUtil.randInt(1, explorationConfig.maxExtracts + 1);
const quest = this.generateRepeatableTemplate("Exploration", traderId,repeatableConfig.side) as IExploration; const quest = this.generateRepeatableTemplate("Exploration", traderId, repeatableConfig.side) as IExploration;
const exitStatusCondition: IExplorationCondition = { const exitStatusCondition: IExplorationCondition = {
_parent: "ExitStatus", _parent: "ExitStatus",
@ -633,23 +714,23 @@ export class RepeatableQuestGenerator
id: this.objectId.generate(), id: this.objectId.generate(),
dynamicLocale: true, dynamicLocale: true,
status: [ status: [
"Survived" "Survived",
] ],
} },
}; };
const locationCondition: IExplorationCondition = { const locationCondition: IExplorationCondition = {
_parent: "Location", _parent: "Location",
_props: { _props: {
id: this.objectId.generate(), id: this.objectId.generate(),
dynamicLocale: true, dynamicLocale: true,
target: locationTarget target: locationTarget,
} },
}; };
quest.conditions.AvailableForFinish[0]._props.counter.id = this.objectId.generate(); quest.conditions.AvailableForFinish[0]._props.counter.id = this.objectId.generate();
quest.conditions.AvailableForFinish[0]._props.counter.conditions = [ quest.conditions.AvailableForFinish[0]._props.counter.conditions = [
exitStatusCondition, exitStatusCondition,
locationCondition locationCondition,
]; ];
quest.conditions.AvailableForFinish[0]._props.value = numExtracts; quest.conditions.AvailableForFinish[0]._props.value = numExtracts;
quest.conditions.AvailableForFinish[0]._props.id = this.objectId.generate(); quest.conditions.AvailableForFinish[0]._props.id = this.objectId.generate();
@ -659,11 +740,15 @@ export class RepeatableQuestGenerator
{ {
// Filter by whitelist, it's also possible that the field "PassageRequirement" does not exist (e.g. Shoreline) // Filter by whitelist, it's also possible that the field "PassageRequirement" does not exist (e.g. Shoreline)
// Scav exits are not listed at all in locations.base currently. If that changes at some point, additional filtering will be required // Scav exits are not listed at all in locations.base currently. If that changes at some point, additional filtering will be required
const mapExits = (this.databaseServer.getTables().locations[locationKey.toLowerCase()].base as ILocationBase).exits; const mapExits =
(this.databaseServer.getTables().locations[locationKey.toLowerCase()].base as ILocationBase).exits;
const possibleExists = mapExits.filter( const possibleExists = mapExits.filter(
x => (!("PassageRequirement" in x) (x) =>
|| repeatableConfig.questConfig.Exploration.specificExits.passageRequirementWhitelist.includes(x.PassageRequirement)) (!("PassageRequirement" in x) ||
&& x.Chance > 0 repeatableConfig.questConfig.Exploration.specificExits.passageRequirementWhitelist.includes(
x.PassageRequirement,
)) &&
x.Chance > 0,
); );
const exit = this.randomUtil.drawRandomFromList(possibleExists, 1)[0]; const exit = this.randomUtil.drawRandomFromList(possibleExists, 1)[0];
const exitCondition = this.generateExplorationExitCondition(exit); const exitCondition = this.generateExplorationExitCondition(exit);
@ -682,7 +767,7 @@ export class RepeatableQuestGenerator
pmcLevel: number, pmcLevel: number,
traderId: string, traderId: string,
questTypePool: IQuestTypePool, questTypePool: IQuestTypePool,
repeatableConfig: IRepeatableQuestConfig repeatableConfig: IRepeatableQuestConfig,
): IPickup ): IPickup
{ {
const pickupConfig = repeatableConfig.questConfig.Pickup; const pickupConfig = repeatableConfig.questConfig.Pickup;
@ -690,21 +775,28 @@ export class RepeatableQuestGenerator
const quest = this.generateRepeatableTemplate("Pickup", traderId, repeatableConfig.side) as IPickup; const quest = this.generateRepeatableTemplate("Pickup", traderId, repeatableConfig.side) as IPickup;
const itemTypeToFetchWithCount = this.randomUtil.getArrayValue(pickupConfig.ItemTypeToFetchWithMaxCount); const itemTypeToFetchWithCount = this.randomUtil.getArrayValue(pickupConfig.ItemTypeToFetchWithMaxCount);
const itemCountToFetch = this.randomUtil.randInt(itemTypeToFetchWithCount.minPickupCount, itemTypeToFetchWithCount.maxPickupCount + 1); const itemCountToFetch = this.randomUtil.randInt(
itemTypeToFetchWithCount.minPickupCount,
itemTypeToFetchWithCount.maxPickupCount + 1,
);
// Choose location - doesnt seem to work for anything other than 'any' // Choose location - doesnt seem to work for anything other than 'any'
//const locationKey: string = this.randomUtil.drawRandomFromDict(questTypePool.pool.Pickup.locations)[0]; // const locationKey: string = this.randomUtil.drawRandomFromDict(questTypePool.pool.Pickup.locations)[0];
//const locationTarget = questTypePool.pool.Pickup.locations[locationKey]; // const locationTarget = questTypePool.pool.Pickup.locations[locationKey];
const findCondition = quest.conditions.AvailableForFinish.find(x => x._parent === "FindItem"); const findCondition = quest.conditions.AvailableForFinish.find((x) => x._parent === "FindItem");
findCondition._props.target = [itemTypeToFetchWithCount.itemType]; findCondition._props.target = [itemTypeToFetchWithCount.itemType];
findCondition._props.value = itemCountToFetch; findCondition._props.value = itemCountToFetch;
const counterCreatorCondition = quest.conditions.AvailableForFinish.find(x => x._parent === "CounterCreator"); const counterCreatorCondition = quest.conditions.AvailableForFinish.find((x) => x._parent === "CounterCreator");
//const locationCondition = counterCreatorCondition._props.counter.conditions.find(x => x._parent === "Location"); // const locationCondition = counterCreatorCondition._props.counter.conditions.find(x => x._parent === "Location");
//(locationCondition._props as ILocationConditionProps).target = [...locationTarget]; // (locationCondition._props as ILocationConditionProps).target = [...locationTarget];
const equipmentCondition = counterCreatorCondition._props.counter.conditions.find(x => x._parent === "Equipment"); const equipmentCondition = counterCreatorCondition._props.counter.conditions.find((x) =>
(equipmentCondition._props as IEquipmentConditionProps).equipmentInclusive = [[itemTypeToFetchWithCount.itemType]]; x._parent === "Equipment"
);
(equipmentCondition._props as IEquipmentConditionProps).equipmentInclusive = [[
itemTypeToFetchWithCount.itemType,
]];
// Add rewards // Add rewards
quest.rewards = this.generateReward(pmcLevel, 1, traderId, repeatableConfig, pickupConfig); quest.rewards = this.generateReward(pmcLevel, 1, traderId, repeatableConfig, pickupConfig);
@ -736,8 +828,8 @@ export class RepeatableQuestGenerator
_props: { _props: {
exitName: exit.Name, exitName: exit.Name,
id: this.objectId.generate(), id: this.objectId.generate(),
dynamicLocale: true dynamicLocale: true,
} },
}; };
} }
@ -766,7 +858,7 @@ export class RepeatableQuestGenerator
difficulty: number, difficulty: number,
traderId: string, traderId: string,
repeatableConfig: IRepeatableQuestConfig, repeatableConfig: IRepeatableQuestConfig,
questConfig: IBaseQuestConfig questConfig: IBaseQuestConfig,
): IRewards ): IRewards
{ {
// difficulty could go from 0.2 ... -> for lowest diffuculty receive 0.2*nominal reward // difficulty could go from 0.2 ... -> for lowest diffuculty receive 0.2*nominal reward
@ -786,11 +878,22 @@ export class RepeatableQuestGenerator
} }
// rewards are generated based on pmcLevel, difficulty and a random spread // rewards are generated based on pmcLevel, difficulty and a random spread
const rewardXP = Math.floor(difficulty * this.mathUtil.interp1(pmcLevel, levelsConfig, xpConfig) * this.randomUtil.getFloat(1 - rewardSpreadConfig, 1 + rewardSpreadConfig)); const rewardXP = Math.floor(
const rewardRoubles = Math.floor(difficulty * this.mathUtil.interp1(pmcLevel, levelsConfig, roublesConfig) * this.randomUtil.getFloat(1 - rewardSpreadConfig, 1 + rewardSpreadConfig)); difficulty * this.mathUtil.interp1(pmcLevel, levelsConfig, xpConfig) *
const rewardNumItems = this.randomUtil.randInt(1, Math.round(this.mathUtil.interp1(pmcLevel, levelsConfig, itemsConfig)) + 1); this.randomUtil.getFloat(1 - rewardSpreadConfig, 1 + rewardSpreadConfig),
const rewardReputation = Math.round(100 * difficulty * this.mathUtil.interp1(pmcLevel, levelsConfig, reputationConfig) );
* this.randomUtil.getFloat(1 - rewardSpreadConfig, 1 + rewardSpreadConfig)) / 100; const rewardRoubles = Math.floor(
difficulty * this.mathUtil.interp1(pmcLevel, levelsConfig, roublesConfig) *
this.randomUtil.getFloat(1 - rewardSpreadConfig, 1 + rewardSpreadConfig),
);
const rewardNumItems = this.randomUtil.randInt(
1,
Math.round(this.mathUtil.interp1(pmcLevel, levelsConfig, itemsConfig)) + 1,
);
const rewardReputation = Math.round(
100 * difficulty * this.mathUtil.interp1(pmcLevel, levelsConfig, reputationConfig) *
this.randomUtil.getFloat(1 - rewardSpreadConfig, 1 + rewardSpreadConfig),
) / 100;
const skillRewardChance = this.mathUtil.interp1(pmcLevel, levelsConfig, skillRewardChanceConfig); const skillRewardChance = this.mathUtil.interp1(pmcLevel, levelsConfig, skillRewardChanceConfig);
const skillPointReward = this.mathUtil.interp1(pmcLevel, levelsConfig, skillPointRewardConfig); const skillPointReward = this.mathUtil.interp1(pmcLevel, levelsConfig, skillPointRewardConfig);
@ -804,16 +907,18 @@ export class RepeatableQuestGenerator
{ {
value: rewardXP, value: rewardXP,
type: "Experience", type: "Experience",
index: 0 index: 0,
} },
], ],
Fail: [] Fail: [],
}; };
if (traderId === Traders.PEACEKEEPER) if (traderId === Traders.PEACEKEEPER)
{ {
// convert to equivalent dollars // convert to equivalent dollars
rewards.Success.push(this.generateRewardItem(Money.EUROS, this.handbookHelper.fromRUB(rewardRoubles, Money.EUROS), 1)); rewards.Success.push(
this.generateRewardItem(Money.EUROS, this.handbookHelper.fromRUB(rewardRoubles, Money.EUROS), 1),
);
} }
else else
{ {
@ -837,14 +942,20 @@ export class RepeatableQuestGenerator
} }
// If we provide ammo we don't want to provide just one bullet // If we provide ammo we don't want to provide just one bullet
value = this.randomUtil.randInt(repeatableConfig.rewardAmmoStackMinSize, itemSelected._props.StackMaxSize); value = this.randomUtil.randInt(
repeatableConfig.rewardAmmoStackMinSize,
itemSelected._props.StackMaxSize,
);
} }
else if (this.itemHelper.isOfBaseclass(itemSelected._id, BaseClasses.WEAPON)) else if (this.itemHelper.isOfBaseclass(itemSelected._id, BaseClasses.WEAPON))
{ {
const defaultPreset = this.presetHelper.getDefaultPreset(itemSelected._id); const defaultPreset = this.presetHelper.getDefaultPreset(itemSelected._id);
if (defaultPreset) if (defaultPreset)
{ {
children = this.ragfairServerHelper.reparentPresets(defaultPreset._items[0], defaultPreset._items); children = this.ragfairServerHelper.reparentPresets(
defaultPreset._items[0],
defaultPreset._items,
);
} }
} }
rewards.Success.push(this.generateRewardItem(itemSelected._id, value, index, children)); rewards.Success.push(this.generateRewardItem(itemSelected._id, value, index, children));
@ -859,7 +970,9 @@ export class RepeatableQuestGenerator
if (roublesBudget > 0) if (roublesBudget > 0)
{ {
// Filter possible reward items to only items with a price below the remaining budget // Filter possible reward items to only items with a price below the remaining budget
chosenRewardItems = chosenRewardItems.filter(x => this.itemHelper.getStaticItemPrice(x._id) < roublesBudget); chosenRewardItems = chosenRewardItems.filter((x) =>
this.itemHelper.getStaticItemPrice(x._id) < roublesBudget
);
if (chosenRewardItems.length === 0) if (chosenRewardItems.length === 0)
{ {
break; // No reward items left, exit break; // No reward items left, exit
@ -879,7 +992,7 @@ export class RepeatableQuestGenerator
target: traderId, target: traderId,
value: rewardReputation, value: rewardReputation,
type: "TraderStanding", type: "TraderStanding",
index: index index: index,
}; };
rewards.Success.push(reward); rewards.Success.push(reward);
} }
@ -891,7 +1004,7 @@ export class RepeatableQuestGenerator
target: this.randomUtil.getArrayValue(questConfig.possibleSkillRewards), target: this.randomUtil.getArrayValue(questConfig.possibleSkillRewards),
value: skillPointReward, value: skillPointReward,
type: "Skill", type: "Skill",
index: index index: index,
}; };
rewards.Success.push(reward); rewards.Success.push(reward);
} }
@ -905,17 +1018,29 @@ export class RepeatableQuestGenerator
* @param roublesBudget Total value of items to return * @param roublesBudget Total value of items to return
* @returns Array of reward items that fit budget * @returns Array of reward items that fit budget
*/ */
protected chooseRewardItemsWithinBudget(repeatableConfig: IRepeatableQuestConfig, roublesBudget: number): ITemplateItem[] protected chooseRewardItemsWithinBudget(
repeatableConfig: IRepeatableQuestConfig,
roublesBudget: number,
): ITemplateItem[]
{ {
// First filter for type and baseclass to avoid lookup in handbook for non-available items // First filter for type and baseclass to avoid lookup in handbook for non-available items
const rewardableItems = this.getRewardableItems(repeatableConfig); const rewardableItems = this.getRewardableItems(repeatableConfig);
const minPrice = Math.min(25000, 0.5 * roublesBudget); const minPrice = Math.min(25000, 0.5 * roublesBudget);
let itemSelection = rewardableItems.filter(x => this.itemHelper.getItemPrice(x[0]) < roublesBudget && this.itemHelper.getItemPrice(x[0]) > minPrice).map(x => x[1]); let itemSelection = rewardableItems.filter((x) =>
this.itemHelper.getItemPrice(x[0]) < roublesBudget && this.itemHelper.getItemPrice(x[0]) > minPrice
).map((x) => x[1]);
if (itemSelection.length === 0) if (itemSelection.length === 0)
{ {
this.logger.warning(this.localisationService.getText("repeatable-no_reward_item_found_in_price_range", {minPrice: minPrice, roublesBudget: roublesBudget})); this.logger.warning(
this.localisationService.getText("repeatable-no_reward_item_found_in_price_range", {
minPrice: minPrice,
roublesBudget: roublesBudget,
}),
);
// In case we don't find any items in the price range // In case we don't find any items in the price range
itemSelection = rewardableItems.filter(x => this.itemHelper.getItemPrice(x[0]) < roublesBudget).map(x => x[1]); itemSelection = rewardableItems.filter((x) => this.itemHelper.getItemPrice(x[0]) < roublesBudget).map((x) =>
x[1]
);
} }
return itemSelection; return itemSelection;
@ -936,7 +1061,7 @@ export class RepeatableQuestGenerator
target: id, target: id,
value: value, value: value,
type: "Item", type: "Item",
index: index index: index,
}; };
const rootItem = { const rootItem = {
@ -944,8 +1069,8 @@ export class RepeatableQuestGenerator
_tpl: tpl, _tpl: tpl,
upd: { upd: {
StackObjectsCount: value, StackObjectsCount: value,
SpawnedInSession: true SpawnedInSession: true,
} },
}; };
if (preset) if (preset)
@ -970,7 +1095,6 @@ export class RepeatableQuestGenerator
// also check if the price is greater than 0; there are some items whose price can not be found // also check if the price is greater than 0; there are some items whose price can not be found
// those are not in the game yet (e.g. AGS grenade launcher) // those are not in the game yet (e.g. AGS grenade launcher)
return Object.entries(this.databaseServer.getTables().templates.items).filter( return Object.entries(this.databaseServer.getTables().templates.items).filter(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
([tpl, itemTemplate]) => ([tpl, itemTemplate]) =>
{ {
// Base "Item" item has no parent, ignore it // Base "Item" item has no parent, ignore it
@ -980,7 +1104,7 @@ export class RepeatableQuestGenerator
} }
return this.isValidRewardItem(tpl, repeatableQuestConfig); return this.isValidRewardItem(tpl, repeatableQuestConfig);
} },
); );
} }
@ -999,8 +1123,10 @@ export class RepeatableQuestGenerator
} }
// Item is on repeatable or global blacklist // Item is on repeatable or global blacklist
if (repeatableQuestConfig.rewardBlacklist.includes(tpl) if (
|| this.itemFilterService.isItemBlacklisted(tpl)) repeatableQuestConfig.rewardBlacklist.includes(tpl) ||
this.itemFilterService.isItemBlacklisted(tpl)
)
{ {
return false; return false;
} }
@ -1011,15 +1137,23 @@ export class RepeatableQuestGenerator
return false; return false;
} }
if (this.itemHelper.isOfBaseclasses(tpl, [BaseClasses.DOG_TAG_USEC, BaseClasses.DOG_TAG_BEAR, BaseClasses.MOUNT, BaseClasses.KEY, BaseClasses.ARMBAND])) if (
this.itemHelper.isOfBaseclasses(tpl, [
BaseClasses.DOG_TAG_USEC,
BaseClasses.DOG_TAG_BEAR,
BaseClasses.MOUNT,
BaseClasses.KEY,
BaseClasses.ARMBAND,
])
)
{ {
return false; return false;
} }
// Skip globally blacklisted items + boss items // Skip globally blacklisted items + boss items
// biome-ignore lint/complexity/useSimplifiedLogicExpression: <explanation> // biome-ignore lint/complexity/useSimplifiedLogicExpression: <explanation>
valid = !this.itemFilterService.isItemBlacklisted(tpl) valid = !this.itemFilterService.isItemBlacklisted(tpl) &&
&& !this.itemFilterService.isBossItem(tpl); !this.itemFilterService.isBossItem(tpl);
return valid; return valid;
} }
@ -1037,7 +1171,9 @@ export class RepeatableQuestGenerator
// @Incomplete: define Type for "type". // @Incomplete: define Type for "type".
protected generateRepeatableTemplate(type: string, traderId: string, side: string): IRepeatableQuest protected generateRepeatableTemplate(type: string, traderId: string, side: string): IRepeatableQuest
{ {
const quest = this.jsonUtil.clone<IRepeatableQuest>(this.databaseServer.getTables().templates.repeatableQuests.templates[type]); const quest = this.jsonUtil.clone<IRepeatableQuest>(
this.databaseServer.getTables().templates.repeatableQuests.templates[type],
);
quest._id = this.objectId.generate(); quest._id = this.objectId.generate();
quest.traderId = traderId; quest.traderId = traderId;
@ -1049,16 +1185,37 @@ export class RepeatableQuestGenerator
// Get template id from config based on side and type of quest // Get template id from config based on side and type of quest
quest.templateId = this.questConfig.questTemplateIds[side.toLowerCase()][type.toLowerCase()]; quest.templateId = this.questConfig.questTemplateIds[side.toLowerCase()][type.toLowerCase()];
quest.name = quest.name.replace("{traderId}", traderId).replace("{templateId}",quest.templateId); quest.name = quest.name.replace("{traderId}", traderId).replace("{templateId}", quest.templateId);
quest.note = quest.note.replace("{traderId}", traderId).replace("{templateId}",quest.templateId); quest.note = quest.note.replace("{traderId}", traderId).replace("{templateId}", quest.templateId);
quest.description = quest.description.replace("{traderId}", traderId).replace("{templateId}",quest.templateId); quest.description = quest.description.replace("{traderId}", traderId).replace("{templateId}", quest.templateId);
quest.successMessageText = quest.successMessageText.replace("{traderId}", traderId).replace("{templateId}",quest.templateId); quest.successMessageText = quest.successMessageText.replace("{traderId}", traderId).replace(
quest.failMessageText = quest.failMessageText.replace("{traderId}", traderId).replace("{templateId}",quest.templateId); "{templateId}",
quest.startedMessageText = quest.startedMessageText.replace("{traderId}", traderId).replace("{templateId}",quest.templateId); quest.templateId,
quest.changeQuestMessageText = quest.changeQuestMessageText.replace("{traderId}", traderId).replace("{templateId}",quest.templateId); );
quest.acceptPlayerMessage = quest.acceptPlayerMessage.replace("{traderId}", traderId).replace("{templateId}",quest.templateId); quest.failMessageText = quest.failMessageText.replace("{traderId}", traderId).replace(
quest.declinePlayerMessage = quest.declinePlayerMessage.replace("{traderId}", traderId).replace("{templateId}",quest.templateId); "{templateId}",
quest.completePlayerMessage = quest.completePlayerMessage.replace("{traderId}", traderId).replace("{templateId}",quest.templateId); quest.templateId,
);
quest.startedMessageText = quest.startedMessageText.replace("{traderId}", traderId).replace(
"{templateId}",
quest.templateId,
);
quest.changeQuestMessageText = quest.changeQuestMessageText.replace("{traderId}", traderId).replace(
"{templateId}",
quest.templateId,
);
quest.acceptPlayerMessage = quest.acceptPlayerMessage.replace("{traderId}", traderId).replace(
"{templateId}",
quest.templateId,
);
quest.declinePlayerMessage = quest.declinePlayerMessage.replace("{traderId}", traderId).replace(
"{templateId}",
quest.templateId,
);
quest.completePlayerMessage = quest.completePlayerMessage.replace("{traderId}", traderId).replace(
"{templateId}",
quest.templateId,
);
return quest; return quest;
} }

View File

@ -10,7 +10,8 @@ import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { Money } from "@spt-aki/models/enums/Money"; import { Money } from "@spt-aki/models/enums/Money";
import { IScavCaseConfig } from "@spt-aki/models/spt/config/IScavCaseConfig"; import { IScavCaseConfig } from "@spt-aki/models/spt/config/IScavCaseConfig";
import { import {
RewardCountAndPriceDetails, ScavCaseRewardCountsAndPrices RewardCountAndPriceDetails,
ScavCaseRewardCountsAndPrices,
} from "@spt-aki/models/spt/hideout/ScavCaseRewardCountsAndPrices"; } from "@spt-aki/models/spt/hideout/ScavCaseRewardCountsAndPrices";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { ConfigServer } from "@spt-aki/servers/ConfigServer"; import { ConfigServer } from "@spt-aki/servers/ConfigServer";
@ -38,7 +39,7 @@ export class ScavCaseRewardGenerator
@inject("DatabaseServer") protected databaseServer: DatabaseServer, @inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("RagfairPriceService") protected ragfairPriceService: RagfairPriceService, @inject("RagfairPriceService") protected ragfairPriceService: RagfairPriceService,
@inject("ItemFilterService") protected itemFilterService: ItemFilterService, @inject("ItemFilterService") protected itemFilterService: ItemFilterService,
@inject("ConfigServer") protected configServer: ConfigServer @inject("ConfigServer") protected configServer: ConfigServer,
) )
{ {
this.scavCaseConfig = this.configServer.getConfig(ConfigTypes.SCAVCASE); this.scavCaseConfig = this.configServer.getConfig(ConfigTypes.SCAVCASE);
@ -54,7 +55,7 @@ export class ScavCaseRewardGenerator
this.cacheDbItems(); this.cacheDbItems();
// Get scavcase details from hideout/scavcase.json // Get scavcase details from hideout/scavcase.json
const scavCaseDetails = this.databaseServer.getTables().hideout.scavcase.find(r => r._id === recipeId); const scavCaseDetails = this.databaseServer.getTables().hideout.scavcase.find((r) => r._id === recipeId);
const rewardItemCounts = this.getScavCaseRewardCountsAndPrices(scavCaseDetails); const rewardItemCounts = this.getScavCaseRewardCountsAndPrices(scavCaseDetails);
// Get items that fit the price criteria as set by the scavCase config // Get items that fit the price criteria as set by the scavCase config
@ -63,9 +64,17 @@ export class ScavCaseRewardGenerator
const superRarePricedItems = this.getFilteredItemsByPrice(this.dbItemsCache, rewardItemCounts.Superrare); const superRarePricedItems = this.getFilteredItemsByPrice(this.dbItemsCache, rewardItemCounts.Superrare);
// Get randomly picked items from each item collction, the count range of which is defined in hideout/scavcase.json // Get randomly picked items from each item collction, the count range of which is defined in hideout/scavcase.json
const randomlyPickedCommonRewards = this.pickRandomRewards(commonPricedItems, rewardItemCounts.Common, "common"); const randomlyPickedCommonRewards = this.pickRandomRewards(
commonPricedItems,
rewardItemCounts.Common,
"common",
);
const randomlyPickedRareRewards = this.pickRandomRewards(rarePricedItems, rewardItemCounts.Rare, "rare"); const randomlyPickedRareRewards = this.pickRandomRewards(rarePricedItems, rewardItemCounts.Rare, "rare");
const randomlyPickedSuperRareRewards = this.pickRandomRewards(superRarePricedItems, rewardItemCounts.Superrare, "superrare"); const randomlyPickedSuperRareRewards = this.pickRandomRewards(
superRarePricedItems,
rewardItemCounts.Superrare,
"superrare",
);
// Add randomised stack sizes to ammo and money rewards // Add randomised stack sizes to ammo and money rewards
const commonRewards = this.randomiseContainerItemRewards(randomlyPickedCommonRewards, "common"); const commonRewards = this.randomiseContainerItemRewards(randomlyPickedCommonRewards, "common");
@ -97,9 +106,11 @@ export class ScavCaseRewardGenerator
} }
// Skip item if item id is on blacklist // Skip item if item id is on blacklist
if ((item._type !== "Item") if (
|| this.scavCaseConfig.rewardItemBlacklist.includes(item._id) (item._type !== "Item") ||
|| this.itemFilterService.isItemBlacklisted(item._id)) this.scavCaseConfig.rewardItemBlacklist.includes(item._id) ||
this.itemFilterService.isItemBlacklisted(item._id)
)
{ {
return false; return false;
} }
@ -157,7 +168,11 @@ export class ScavCaseRewardGenerator
* @param itemFilters how the rewards should be filtered down (by item count) * @param itemFilters how the rewards should be filtered down (by item count)
* @returns * @returns
*/ */
protected pickRandomRewards(items: ITemplateItem[], itemFilters: RewardCountAndPriceDetails, rarity: string): ITemplateItem[] protected pickRandomRewards(
items: ITemplateItem[],
itemFilters: RewardCountAndPriceDetails,
rarity: string,
): ITemplateItem[]
{ {
const result: ITemplateItem[] = []; const result: ITemplateItem[] = [];
@ -166,16 +181,16 @@ export class ScavCaseRewardGenerator
const randomCount = this.randomUtil.getInt(itemFilters.minCount, itemFilters.maxCount); const randomCount = this.randomUtil.getInt(itemFilters.minCount, itemFilters.maxCount);
for (let i = 0; i < randomCount; i++) for (let i = 0; i < randomCount; i++)
{ {
if (this.rewardShouldBeMoney() && !rewardWasMoney) // Only allow one reward to be money if (this.rewardShouldBeMoney() && !rewardWasMoney)
{ { // Only allow one reward to be money
result.push(this.getRandomMoney()); result.push(this.getRandomMoney());
if (!this.scavCaseConfig.allowMultipleMoneyRewardsPerRarity) if (!this.scavCaseConfig.allowMultipleMoneyRewardsPerRarity)
{ {
rewardWasMoney = true; rewardWasMoney = true;
} }
} }
else if (this.rewardShouldBeAmmo() && !rewardWasAmmo) // Only allow one reward to be ammo else if (this.rewardShouldBeAmmo() && !rewardWasAmmo)
{ { // Only allow one reward to be ammo
result.push(this.getRandomAmmo(rarity)); result.push(this.getRandomAmmo(rarity));
if (!this.scavCaseConfig.allowMultipleAmmoRewardsPerRarity) if (!this.scavCaseConfig.allowMultipleAmmoRewardsPerRarity)
{ {
@ -214,10 +229,9 @@ export class ScavCaseRewardGenerator
*/ */
protected getRandomMoney(): ITemplateItem protected getRandomMoney(): ITemplateItem
{ {
const money: ITemplateItem[] = []; const money: ITemplateItem[] = [];
money.push(this.databaseServer.getTables().templates.items["5449016a4bdc2d6f028b456f"]); //rub money.push(this.databaseServer.getTables().templates.items["5449016a4bdc2d6f028b456f"]); // rub
money.push(this.databaseServer.getTables().templates.items["569668774bdc2da2298b4568"]); //euro money.push(this.databaseServer.getTables().templates.items["569668774bdc2da2298b4568"]); // euro
money.push(this.databaseServer.getTables().templates.items["5696686a4bdc2da3298b456a"]); // dollar money.push(this.databaseServer.getTables().templates.items["5696686a4bdc2da3298b456a"]); // dollar
return this.randomUtil.getArrayValue(money); return this.randomUtil.getArrayValue(money);
@ -234,8 +248,10 @@ export class ScavCaseRewardGenerator
{ {
// Is ammo handbook price between desired range // Is ammo handbook price between desired range
const handbookPrice = this.ragfairPriceService.getStaticPriceForItem(ammo._id); const handbookPrice = this.ragfairPriceService.getStaticPriceForItem(ammo._id);
if (handbookPrice >= this.scavCaseConfig.ammoRewards.ammoRewardValueRangeRub[rarity].min if (
&& handbookPrice <= this.scavCaseConfig.ammoRewards.ammoRewardValueRangeRub[rarity].max) handbookPrice >= this.scavCaseConfig.ammoRewards.ammoRewardValueRangeRub[rarity].min &&
handbookPrice <= this.scavCaseConfig.ammoRewards.ammoRewardValueRangeRub[rarity].max
)
{ {
return true; return true;
} }
@ -266,7 +282,7 @@ export class ScavCaseRewardGenerator
const resultItem = { const resultItem = {
_id: this.hashUtil.generate(), _id: this.hashUtil.generate(),
_tpl: item._id, _tpl: item._id,
upd: undefined upd: undefined,
}; };
this.addStackCountToAmmoAndMoney(item, resultItem, rarity); this.addStackCountToAmmoAndMoney(item, resultItem, rarity);
@ -288,29 +304,37 @@ export class ScavCaseRewardGenerator
* @param item money or ammo item * @param item money or ammo item
* @param resultItem money or ammo item with a randomise stack size * @param resultItem money or ammo item with a randomise stack size
*/ */
protected addStackCountToAmmoAndMoney(item: ITemplateItem, resultItem: { _id: string; _tpl: string; upd: Upd; }, rarity: string): void protected addStackCountToAmmoAndMoney(
item: ITemplateItem,
resultItem: {_id: string; _tpl: string; upd: Upd;},
rarity: string,
): void
{ {
if (item._parent === BaseClasses.AMMO || item._parent === BaseClasses.MONEY) if (item._parent === BaseClasses.AMMO || item._parent === BaseClasses.MONEY)
{ {
resultItem.upd = { resultItem.upd = {
StackObjectsCount: this.getRandomAmountRewardForScavCase(item, rarity) StackObjectsCount: this.getRandomAmountRewardForScavCase(item, rarity),
}; };
} }
} }
/** /**
*
* @param dbItems all items from the items.json * @param dbItems all items from the items.json
* @param itemFilters controls how the dbItems will be filtered and returned (handbook price) * @param itemFilters controls how the dbItems will be filtered and returned (handbook price)
* @returns filtered dbItems array * @returns filtered dbItems array
*/ */
protected getFilteredItemsByPrice(dbItems: ITemplateItem[], itemFilters: RewardCountAndPriceDetails): ITemplateItem[] protected getFilteredItemsByPrice(
dbItems: ITemplateItem[],
itemFilters: RewardCountAndPriceDetails,
): ITemplateItem[]
{ {
return dbItems.filter((item) => return dbItems.filter((item) =>
{ {
const handbookPrice = this.ragfairPriceService.getStaticPriceForItem(item._id); const handbookPrice = this.ragfairPriceService.getStaticPriceForItem(item._id);
if (handbookPrice >= itemFilters.minPriceRub if (
&& handbookPrice <= itemFilters.maxPriceRub) handbookPrice >= itemFilters.minPriceRub &&
handbookPrice <= itemFilters.maxPriceRub
)
{ {
return true; return true;
} }
@ -330,12 +354,11 @@ export class ScavCaseRewardGenerator
// Create reward min/max counts for each type // Create reward min/max counts for each type
for (const rewardType of rewardTypes) for (const rewardType of rewardTypes)
{ {
result[rewardType] = result[rewardType] = {
{
minCount: scavCaseDetails.EndProducts[rewardType].min, minCount: scavCaseDetails.EndProducts[rewardType].min,
maxCount: scavCaseDetails.EndProducts[rewardType].max, maxCount: scavCaseDetails.EndProducts[rewardType].max,
minPriceRub: this.scavCaseConfig.rewardItemValueRangeRub[rewardType.toLowerCase()].min, minPriceRub: this.scavCaseConfig.rewardItemValueRangeRub[rewardType.toLowerCase()].min,
maxPriceRub: this.scavCaseConfig.rewardItemValueRangeRub[rewardType.toLowerCase()].max maxPriceRub: this.scavCaseConfig.rewardItemValueRangeRub[rewardType.toLowerCase()].max,
}; };
} }
@ -353,20 +376,32 @@ export class ScavCaseRewardGenerator
let amountToGive = 1; let amountToGive = 1;
if (itemToCalculate._parent === BaseClasses.AMMO) if (itemToCalculate._parent === BaseClasses.AMMO)
{ {
amountToGive = this.randomUtil.getInt(this.scavCaseConfig.ammoRewards.minStackSize, itemToCalculate._props.StackMaxSize); amountToGive = this.randomUtil.getInt(
this.scavCaseConfig.ammoRewards.minStackSize,
itemToCalculate._props.StackMaxSize,
);
} }
else if (itemToCalculate._parent === BaseClasses.MONEY) else if (itemToCalculate._parent === BaseClasses.MONEY)
{ {
switch (itemToCalculate._id) switch (itemToCalculate._id)
{ {
case Money.ROUBLES: case Money.ROUBLES:
amountToGive = this.randomUtil.getInt(this.scavCaseConfig.moneyRewards.rubCount[rarity].min, this.scavCaseConfig.moneyRewards.rubCount[rarity].max); amountToGive = this.randomUtil.getInt(
this.scavCaseConfig.moneyRewards.rubCount[rarity].min,
this.scavCaseConfig.moneyRewards.rubCount[rarity].max,
);
break; break;
case Money.EUROS: case Money.EUROS:
amountToGive = this.randomUtil.getInt(this.scavCaseConfig.moneyRewards.eurCount[rarity].min, this.scavCaseConfig.moneyRewards.eurCount[rarity].max); amountToGive = this.randomUtil.getInt(
this.scavCaseConfig.moneyRewards.eurCount[rarity].min,
this.scavCaseConfig.moneyRewards.eurCount[rarity].max,
);
break; break;
case Money.DOLLARS: case Money.DOLLARS:
amountToGive = this.randomUtil.getInt(this.scavCaseConfig.moneyRewards.usdCount[rarity].min, this.scavCaseConfig.moneyRewards.usdCount[rarity].max); amountToGive = this.randomUtil.getInt(
this.scavCaseConfig.moneyRewards.usdCount[rarity].min,
this.scavCaseConfig.moneyRewards.usdCount[rarity].max,
);
break; break;
} }
} }

View File

@ -23,7 +23,7 @@ export class WeatherGenerator
@inject("RandomUtil") protected randomUtil: RandomUtil, @inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("TimeUtil") protected timeUtil: TimeUtil, @inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("ApplicationContext") protected applicationContext: ApplicationContext, @inject("ApplicationContext") protected applicationContext: ApplicationContext,
@inject("ConfigServer") protected configServer: ConfigServer @inject("ConfigServer") protected configServer: ConfigServer,
) )
{ {
this.weatherConfig = this.configServer.getConfig(ConfigTypes.WEATHER); this.weatherConfig = this.configServer.getConfig(ConfigTypes.WEATHER);
@ -67,11 +67,12 @@ export class WeatherGenerator
public getInRaidTime(currentDate: Date): Date public getInRaidTime(currentDate: Date): Date
{ {
// Get timestamp of when client conneted to server // Get timestamp of when client conneted to server
const gameStartTimeStampMS = this.applicationContext.getLatestValue(ContextVariableType.CLIENT_START_TIMESTAMP).getValue<number>(); const gameStartTimeStampMS = this.applicationContext.getLatestValue(ContextVariableType.CLIENT_START_TIMESTAMP)
.getValue<number>();
// Get delta between now and when client connected to server in milliseconds // Get delta between now and when client connected to server in milliseconds
const deltaMSFromNow = (Date.now() - gameStartTimeStampMS); const deltaMSFromNow = Date.now() - gameStartTimeStampMS;
const acceleratedMS = (deltaMSFromNow * (this.weatherConfig.acceleration - 1)); // For some reason nodejs moves faster than client time, reducing acceleration by 1 when client is 7 helps const acceleratedMS = deltaMSFromNow * (this.weatherConfig.acceleration - 1); // For some reason nodejs moves faster than client time, reducing acceleration by 1 when client is 7 helps
const clientAcceleratedDate = new Date(currentDate.valueOf() + acceleratedMS); const clientAcceleratedDate = new Date(currentDate.valueOf() + acceleratedMS);
return clientAcceleratedDate; return clientAcceleratedDate;
@ -105,15 +106,15 @@ export class WeatherGenerator
wind_gustiness: this.getRandomFloat("windGustiness"), wind_gustiness: this.getRandomFloat("windGustiness"),
rain: rain, rain: rain,
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
rain_intensity: (rain > 1) rain_intensity: (rain > 1) ?
? this.getRandomFloat("rainIntensity") this.getRandomFloat("rainIntensity") :
: 0, 0,
fog: this.getWeightedFog(), fog: this.getWeightedFog(),
temp: this.getRandomFloat("temp"), temp: this.getRandomFloat("temp"),
pressure: this.getRandomFloat("pressure"), pressure: this.getRandomFloat("pressure"),
time: "", time: "",
date: "", date: "",
timestamp: 0 timestamp: 0,
}; };
this.setCurrentDateTime(result); this.setCurrentDateTime(result);
@ -139,32 +140,49 @@ export class WeatherGenerator
protected getWeightedWindDirection(): WindDirection protected getWeightedWindDirection(): WindDirection
{ {
return this.weightedRandomHelper.weightedRandom(this.weatherConfig.weather.windDirection.values, this.weatherConfig.weather.windDirection.weights).item; return this.weightedRandomHelper.weightedRandom(
this.weatherConfig.weather.windDirection.values,
this.weatherConfig.weather.windDirection.weights,
).item;
} }
protected getWeightedClouds(): number protected getWeightedClouds(): number
{ {
return this.weightedRandomHelper.weightedRandom(this.weatherConfig.weather.clouds.values, this.weatherConfig.weather.clouds.weights).item; return this.weightedRandomHelper.weightedRandom(
this.weatherConfig.weather.clouds.values,
this.weatherConfig.weather.clouds.weights,
).item;
} }
protected getWeightedWindSpeed(): number protected getWeightedWindSpeed(): number
{ {
return this.weightedRandomHelper.weightedRandom(this.weatherConfig.weather.windSpeed.values, this.weatherConfig.weather.windSpeed.weights).item; return this.weightedRandomHelper.weightedRandom(
this.weatherConfig.weather.windSpeed.values,
this.weatherConfig.weather.windSpeed.weights,
).item;
} }
protected getWeightedFog(): number protected getWeightedFog(): number
{ {
return this.weightedRandomHelper.weightedRandom(this.weatherConfig.weather.fog.values, this.weatherConfig.weather.fog.weights).item; return this.weightedRandomHelper.weightedRandom(
this.weatherConfig.weather.fog.values,
this.weatherConfig.weather.fog.weights,
).item;
} }
protected getWeightedRain(): number protected getWeightedRain(): number
{ {
return this.weightedRandomHelper.weightedRandom(this.weatherConfig.weather.rain.values, this.weatherConfig.weather.rain.weights).item; return this.weightedRandomHelper.weightedRandom(
this.weatherConfig.weather.rain.values,
this.weatherConfig.weather.rain.weights,
).item;
} }
protected getRandomFloat(node: string): number protected getRandomFloat(node: string): number
{ {
return parseFloat(this.randomUtil.getFloat(this.weatherConfig.weather[node].min, return parseFloat(
this.weatherConfig.weather[node].max).toPrecision(3)); this.randomUtil.getFloat(this.weatherConfig.weather[node].min, this.weatherConfig.weather[node].max)
.toPrecision(3),
);
} }
} }

View File

@ -9,10 +9,9 @@ export class InventoryMagGen
private magazineTemplate: ITemplateItem, private magazineTemplate: ITemplateItem,
private weaponTemplate: ITemplateItem, private weaponTemplate: ITemplateItem,
private ammoTemplate: ITemplateItem, private ammoTemplate: ITemplateItem,
private pmcInventory: Inventory private pmcInventory: Inventory,
) )
{ {}
}
public getMagCount(): GenerationData public getMagCount(): GenerationData
{ {

View File

@ -8,12 +8,11 @@ import { RandomUtil } from "@spt-aki/utils/RandomUtil";
@injectable() @injectable()
export class BarrelInventoryMagGen implements IInventoryMagGen export class BarrelInventoryMagGen implements IInventoryMagGen
{ {
constructor( constructor(
@inject("RandomUtil") protected randomUtil: RandomUtil, @inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("BotWeaponGeneratorHelper") protected botWeaponGeneratorHelper: BotWeaponGeneratorHelper @inject("BotWeaponGeneratorHelper") protected botWeaponGeneratorHelper: BotWeaponGeneratorHelper,
) )
{ } {}
getPriority(): number getPriority(): number
{ {
@ -29,15 +28,23 @@ export class BarrelInventoryMagGen implements IInventoryMagGen
{ {
// Can't be done by _props.ammoType as grenade launcher shoots grenades with ammoType of "buckshot" // Can't be done by _props.ammoType as grenade launcher shoots grenades with ammoType of "buckshot"
let randomisedAmmoStackSize: number; let randomisedAmmoStackSize: number;
if (inventoryMagGen.getAmmoTemplate()._props.StackMaxRandom === 1) // doesnt stack if (inventoryMagGen.getAmmoTemplate()._props.StackMaxRandom === 1)
{ {
// doesnt stack
randomisedAmmoStackSize = this.randomUtil.getInt(3, 6); randomisedAmmoStackSize = this.randomUtil.getInt(3, 6);
} }
else else
{ {
randomisedAmmoStackSize = this.randomUtil.getInt(inventoryMagGen.getAmmoTemplate()._props.StackMinRandom, inventoryMagGen.getAmmoTemplate()._props.StackMaxRandom); randomisedAmmoStackSize = this.randomUtil.getInt(
inventoryMagGen.getAmmoTemplate()._props.StackMinRandom,
inventoryMagGen.getAmmoTemplate()._props.StackMaxRandom,
);
} }
this.botWeaponGeneratorHelper.addAmmoIntoEquipmentSlots(inventoryMagGen.getAmmoTemplate()._id, randomisedAmmoStackSize, inventoryMagGen.getPmcInventory()); this.botWeaponGeneratorHelper.addAmmoIntoEquipmentSlots(
inventoryMagGen.getAmmoTemplate()._id,
randomisedAmmoStackSize,
inventoryMagGen.getPmcInventory(),
);
} }
} }

View File

@ -12,21 +12,19 @@ import { LocalisationService } from "@spt-aki/services/LocalisationService";
@injectable() @injectable()
export class ExternalInventoryMagGen implements IInventoryMagGen export class ExternalInventoryMagGen implements IInventoryMagGen
{ {
constructor( constructor(
@inject("WinstonLogger") protected logger: ILogger, @inject("WinstonLogger") protected logger: ILogger,
@inject("ItemHelper") protected itemHelper: ItemHelper, @inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("LocalisationService") protected localisationService: LocalisationService, @inject("LocalisationService") protected localisationService: LocalisationService,
@inject("BotWeaponGeneratorHelper") protected botWeaponGeneratorHelper: BotWeaponGeneratorHelper @inject("BotWeaponGeneratorHelper") protected botWeaponGeneratorHelper: BotWeaponGeneratorHelper,
) )
{ } {}
getPriority(): number getPriority(): number
{ {
return 99; return 99;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
canHandleInventoryMagGen(inventoryMagGen: InventoryMagGen): boolean canHandleInventoryMagGen(inventoryMagGen: InventoryMagGen): boolean
{ {
return true; // Fallback, if code reaches here it means no other implementation can handle this type of magazine return true; // Fallback, if code reaches here it means no other implementation can handle this type of magazine
@ -36,36 +34,49 @@ export class ExternalInventoryMagGen implements IInventoryMagGen
{ {
let magTemplate = inventoryMagGen.getMagazineTemplate(); let magTemplate = inventoryMagGen.getMagazineTemplate();
let magazineTpl = magTemplate._id; let magazineTpl = magTemplate._id;
const randomizedMagazineCount = Number(this.botWeaponGeneratorHelper.getRandomizedMagazineCount(inventoryMagGen.getMagCount())); const randomizedMagazineCount = Number(
this.botWeaponGeneratorHelper.getRandomizedMagazineCount(inventoryMagGen.getMagCount()),
);
for (let i = 0; i < randomizedMagazineCount; i++) for (let i = 0; i < randomizedMagazineCount; i++)
{ {
const magazineWithAmmo = this.botWeaponGeneratorHelper.createMagazineWithAmmo(magazineTpl, inventoryMagGen.getAmmoTemplate()._id, magTemplate); const magazineWithAmmo = this.botWeaponGeneratorHelper.createMagazineWithAmmo(
magazineTpl,
inventoryMagGen.getAmmoTemplate()._id,
magTemplate,
);
const ableToFitMagazinesIntoBotInventory = this.botWeaponGeneratorHelper.addItemWithChildrenToEquipmentSlot( const ableToFitMagazinesIntoBotInventory = this.botWeaponGeneratorHelper.addItemWithChildrenToEquipmentSlot(
[EquipmentSlots.TACTICAL_VEST, EquipmentSlots.POCKETS], [EquipmentSlots.TACTICAL_VEST, EquipmentSlots.POCKETS],
magazineWithAmmo[0]._id, magazineWithAmmo[0]._id,
magazineTpl, magazineTpl,
magazineWithAmmo, magazineWithAmmo,
inventoryMagGen.getPmcInventory()); inventoryMagGen.getPmcInventory(),
);
if (ableToFitMagazinesIntoBotInventory === ItemAddedResult.NO_SPACE && i < randomizedMagazineCount) if (ableToFitMagazinesIntoBotInventory === ItemAddedResult.NO_SPACE && i < randomizedMagazineCount)
{ {
/* We were unable to fit at least the minimum amount of magazines, // We were unable to fit at least the minimum amount of magazines, so we fallback to default magazine
* so we fallback to default magazine and try again. // and try again. Temporary workaround to Killa spawning with no extras if he spawns with a drum mag.
* Temporary workaround to Killa spawning with no extras if he spawns with a drum mag */ // TODO: Fix this properly
if (
if (magazineTpl === this.botWeaponGeneratorHelper.getWeaponsDefaultMagazineTpl(inventoryMagGen.getWeaponTemplate())) magazineTpl ===
this.botWeaponGeneratorHelper.getWeaponsDefaultMagazineTpl(inventoryMagGen.getWeaponTemplate())
)
{ {
// We were already on default - stop here to prevent infinite looping // We were already on default - stop here to prevent infinite looping
break; break;
} }
// Get default magazine tpl, reset loop counter by 1 and try again // Get default magazine tpl, reset loop counter by 1 and try again
magazineTpl = this.botWeaponGeneratorHelper.getWeaponsDefaultMagazineTpl(inventoryMagGen.getWeaponTemplate()); magazineTpl = this.botWeaponGeneratorHelper.getWeaponsDefaultMagazineTpl(
inventoryMagGen.getWeaponTemplate(),
);
magTemplate = this.itemHelper.getItem(magazineTpl)[1]; magTemplate = this.itemHelper.getItem(magazineTpl)[1];
if (!magTemplate) if (!magTemplate)
{ {
this.logger.error(this.localisationService.getText("bot-unable_to_find_default_magazine_item", magazineTpl)); this.logger.error(
this.localisationService.getText("bot-unable_to_find_default_magazine_item", magazineTpl),
);
break; break;
} }
@ -78,5 +89,4 @@ export class ExternalInventoryMagGen implements IInventoryMagGen
} }
} }
} }
} }

View File

@ -7,11 +7,10 @@ import { BotWeaponGeneratorHelper } from "@spt-aki/helpers/BotWeaponGeneratorHel
@injectable() @injectable()
export class InternalMagazineInventoryMagGen implements IInventoryMagGen export class InternalMagazineInventoryMagGen implements IInventoryMagGen
{ {
constructor( constructor(
@inject("BotWeaponGeneratorHelper") protected botWeaponGeneratorHelper: BotWeaponGeneratorHelper @inject("BotWeaponGeneratorHelper") protected botWeaponGeneratorHelper: BotWeaponGeneratorHelper,
) )
{ } {}
public getPriority(): number public getPriority(): number
{ {
@ -25,7 +24,14 @@ export class InternalMagazineInventoryMagGen implements IInventoryMagGen
public process(inventoryMagGen: InventoryMagGen): void public process(inventoryMagGen: InventoryMagGen): void
{ {
const bulletCount = this.botWeaponGeneratorHelper.getRandomizedBulletCount(inventoryMagGen.getMagCount(), inventoryMagGen.getMagazineTemplate()); const bulletCount = this.botWeaponGeneratorHelper.getRandomizedBulletCount(
this.botWeaponGeneratorHelper.addAmmoIntoEquipmentSlots(inventoryMagGen.getAmmoTemplate()._id, bulletCount, inventoryMagGen.getPmcInventory()); inventoryMagGen.getMagCount(),
inventoryMagGen.getMagazineTemplate(),
);
this.botWeaponGeneratorHelper.addAmmoIntoEquipmentSlots(
inventoryMagGen.getAmmoTemplate()._id,
bulletCount,
inventoryMagGen.getPmcInventory(),
);
} }
} }

View File

@ -9,11 +9,10 @@ import { EquipmentSlots } from "@spt-aki/models/enums/EquipmentSlots";
@injectable() @injectable()
export class UbglExternalMagGen implements IInventoryMagGen export class UbglExternalMagGen implements IInventoryMagGen
{ {
constructor( constructor(
@inject("BotWeaponGeneratorHelper") protected botWeaponGeneratorHelper: BotWeaponGeneratorHelper @inject("BotWeaponGeneratorHelper") protected botWeaponGeneratorHelper: BotWeaponGeneratorHelper,
) )
{ } {}
public getPriority(): number public getPriority(): number
{ {
@ -27,7 +26,15 @@ export class UbglExternalMagGen implements IInventoryMagGen
public process(inventoryMagGen: InventoryMagGen): void public process(inventoryMagGen: InventoryMagGen): void
{ {
const bulletCount = this.botWeaponGeneratorHelper.getRandomizedBulletCount(inventoryMagGen.getMagCount(), inventoryMagGen.getMagazineTemplate()); const bulletCount = this.botWeaponGeneratorHelper.getRandomizedBulletCount(
this.botWeaponGeneratorHelper.addAmmoIntoEquipmentSlots(inventoryMagGen.getAmmoTemplate()._id, bulletCount, inventoryMagGen.getPmcInventory(), [EquipmentSlots.TACTICAL_VEST]); inventoryMagGen.getMagCount(),
inventoryMagGen.getMagazineTemplate(),
);
this.botWeaponGeneratorHelper.addAmmoIntoEquipmentSlots(
inventoryMagGen.getAmmoTemplate()._id,
bulletCount,
inventoryMagGen.getPmcInventory(),
[EquipmentSlots.TACTICAL_VEST],
);
} }
} }