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,12 +46,12 @@ export class BotEquipmentModGenerator
@inject("BotWeaponGeneratorHelper") protected botWeaponGeneratorHelper: BotWeaponGeneratorHelper,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("BotEquipmentModPoolService") protected botEquipmentModPoolService: BotEquipmentModPoolService,
@inject("ConfigServer") protected configServer: ConfigServer
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
}
/**
* Check mods are compatible and add to array
* @param equipment Equipment item to add mods to
@ -63,7 +63,15 @@ export class BotEquipmentModGenerator
* @param forceSpawn should this mod be forced to spawn
* @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];
@ -73,7 +81,13 @@ export class BotEquipmentModGenerator
const itemSlot = this.getModItemSlot(modSlot, parentTemplate);
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;
}
@ -83,27 +97,33 @@ export class BotEquipmentModGenerator
}
// Ensure submods for nvgs all spawn together
forceSpawn = (modSlot === "mod_nvg")
? true
: false;
forceSpawn = (modSlot === "mod_nvg") ?
true :
false;
let modTpl: string;
let found = false;
// 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())
{
modTpl = exhaustableModPool.getRandomValue();
if (!this.botGeneratorHelper.isItemIncompatibleWithCurrentItems(equipment, modTpl, modSlot).incompatible)
if (
!this.botGeneratorHelper.isItemIncompatibleWithCurrentItems(equipment, modTpl, modSlot).incompatible
)
{
found = true;
break;
}
}
// Combatible item not found but slot REQUIRES item, get random item from db
const parentSlot = parentTemplate._props.Slots.find(i => i._name === modSlot);
// Compatible item not found but slot REQUIRES item, get random item from db
const parentSlot = parentTemplate._props.Slots.find((i) => i._name === modSlot);
if (!found && parentSlot !== undefined && parentSlot._required)
{
modTpl = this.getModTplFromItemDb(modTpl, parentSlot, modSlot, equipment);
@ -113,7 +133,7 @@ export class BotEquipmentModGenerator
// Compatible item not found + not required
if (!found && parentSlot !== undefined && !parentSlot._required)
{
// Dont add item
// Don't add item
continue;
}
@ -128,8 +148,16 @@ export class BotEquipmentModGenerator
if (Object.keys(modPool).includes(modTpl))
{
// Call self recursivly
this.generateModsForEquipment(equipment, modPool, modId, modTemplate[1], modSpawnChances, botRole, forceSpawn);
// Call self recursively
this.generateModsForEquipment(
equipment,
modPool,
modId,
modTemplate[1],
modSpawnChances,
botRole,
forceSpawn,
);
}
}
@ -146,8 +174,8 @@ export class BotEquipmentModGenerator
* @param modSpawnChances Mod spawn chances
* @param ammoTpl Ammo tpl to use when generating magazines/cartridges
* @param botRole Role of bot weapon is generated for
* @param botLevel lvel of the bot weapon is being generated for
* @param modLimits limits placed on certian mod types per gun
* @param botLevel Level of the bot weapon is being generated for
* @param modLimits limits placed on certain mod types per gun
* @param botEquipmentRole role of bot when accessing bot.json equipment config settings
* @returns Weapon + mods array
*/
@ -162,7 +190,8 @@ export class BotEquipmentModGenerator
botRole: string,
botLevel: number,
modLimits: BotModLimits,
botEquipmentRole: string): Item[]
botEquipmentRole: string,
): Item[]
{
const pmcProfile = this.profileHelper.getPmcProfile(sessionId);
@ -171,17 +200,27 @@ export class BotEquipmentModGenerator
// Null guard against bad input weapon
// biome-ignore lint/complexity/useSimplifiedLogicExpression: <explanation>
if (!parentTemplate._props.Slots.length
&& !parentTemplate._props.Cartridges?.length
&& !parentTemplate._props.Chambers?.length)
if (
!parentTemplate._props.Slots.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;
}
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 randomisationSettings = this.botHelper.getBotRandomizationDetails(botLevel, botEquipConfig);
@ -193,7 +232,14 @@ export class BotEquipmentModGenerator
const modsParentSlot = this.getModItemSlot(modSlot, parentTemplate);
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;
}
@ -205,10 +251,19 @@ export class BotEquipmentModGenerator
}
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
if (!modToAdd || typeof (modToAdd) === "undefined")
if (!modToAdd || typeof modToAdd === "undefined")
{
continue;
}
@ -220,7 +275,15 @@ export class BotEquipmentModGenerator
const modToAddTemplate = modToAdd[1];
// 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;
}
@ -234,7 +297,7 @@ export class BotEquipmentModGenerator
"mod_scope_000",
"mod_scope_001",
"mod_scope_002",
"mod_scope_003"
"mod_scope_003",
];
this.adjustSlotSpawnChances(modSpawnChances, scopeSlots, 100);
@ -252,7 +315,7 @@ export class BotEquipmentModGenerator
const muzzleSlots = [
"mod_muzzle",
"mod_muzzle_000",
"mod_muzzle_001"
"mod_muzzle_001",
];
// Make chance of muzzle devices 95%, nearly certain but not guaranteed
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)
// 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
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
// 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
const stockSlots = ["mod_stock", "mod_stock_000", "mod_stock_akms"];
@ -283,10 +352,12 @@ export class BotEquipmentModGenerator
}
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.
// 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
// this entry is not to be filled, we need a special handling for the CylinderMagazine
const modParentItem = this.databaseServer.getTables().templates.items[modToAddTemplate._parent];
@ -312,8 +383,20 @@ export class BotEquipmentModGenerator
}
if (containsModInPool)
{
// Call self recursivly to add mods to this mod
this.generateModsForWeapon(sessionId, weapon, modPool, modId, modToAddTemplate, modSpawnChances, ammoTpl, botRole, botLevel, modLimits, botEquipmentRole);
// Call self recursively to add mods to this mod
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
{
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;
}
@ -344,15 +427,27 @@ export class BotEquipmentModGenerator
*/
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())
&& modsParentId === BaseClasses.MOUNT;
return [
"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
* @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)
{
@ -418,7 +513,7 @@ export class BotEquipmentModGenerator
sortedKeys.push(modRecieverKey);
unsortedKeys.splice(unsortedKeys.indexOf(modRecieverKey), 1);
}
if (unsortedKeys.includes(modPistolGrip))
{
sortedKeys.push(modPistolGrip);
@ -467,11 +562,11 @@ export class BotEquipmentModGenerator
case "patron_in_weapon":
case "patron_in_weapon_000":
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":
return parentTemplate._props.Cartridges.find(c => c._name === modSlot);
return parentTemplate._props.Cartridges.find((c) => c._name === modSlot);
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
{
const modSpawnChance = itemSlot._required || this.getAmmoContainers().includes(modSlot) // Required OR it is ammo
? 100
: modSpawnChances[modSlot];
?
100 :
modSpawnChances[modSlot];
if (modSpawnChance === 100)
{
@ -498,7 +594,6 @@ export class BotEquipmentModGenerator
}
/**
*
* @param modSlot Slot mod will fit into
* @param isRandomisableSlot Will generate a randomised mod pool if true
* @param modsParent Parent slot the item will be a part of
@ -517,12 +612,13 @@ export class BotEquipmentModGenerator
itemModPool: Record<string, string[]>,
weapon: Item[],
ammoTpl: string,
parentTemplate: ITemplateItem): [boolean, ITemplateItem]
parentTemplate: ITemplateItem,
): [boolean, ITemplateItem]
{
let modTpl: string;
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
if (this.getAmmoContainers().includes(modSlot) && modSlot !== "mod_magazine")
{
@ -539,27 +635,37 @@ export class BotEquipmentModGenerator
// Ensure there's a pool of mods to pick from
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;
}
// Filter out non-whitelisted scopes, use full modpool if filtered pool would have no elements
if (modSlot.includes("mod_scope") && botWeaponSightWhitelist)
if (modSlot.includes("mod_scope") && botWeaponSightWhitelist)
{
// scope pool has more than one scope
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
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())
{
modTpl = exhaustableModPool.getRandomValue();
modCompatibilityResult = this.botGeneratorHelper.isItemIncompatibleWithCurrentItems(weapon, modTpl, modSlot);
modCompatibilityResult = this.botGeneratorHelper.isItemIncompatibleWithCurrentItems(
weapon,
modTpl,
modSlot,
);
if (!modCompatibilityResult.incompatible)
{
found = true;
@ -592,7 +698,11 @@ export class BotEquipmentModGenerator
{
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;
@ -607,21 +717,27 @@ export class BotEquipmentModGenerator
* @param modTpl _tpl
* @param parentId parentId
* @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
*/
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 {
_id: modId,
_tpl: modTpl,
parentId: parentId,
slotId: modSlot,
...this.botGeneratorHelper.generateExtraPropertiesForItem(modTemplate, botRole)
...this.botGeneratorHelper.generateExtraPropertiesForItem(modTemplate, botRole),
};
}
/**
* Get a list of containers that hold ammo
* 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
* @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 items items to ensure picked mod is compatible with
* @returns item tpl
*/
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;
// 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
* @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
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}`);
return false;
@ -686,16 +812,31 @@ export class BotEquipmentModGenerator
// Slot must be filled, show warning
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;
}
// 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 (!(itemSlot._props.filters[0].Filter.includes(modToAdd[1]._id) || this.itemHelper.isOfBaseclasses(modToAdd[1]._id, itemSlot._props.filters[0].Filter)))
// 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))
)
{
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;
}
@ -703,26 +844,39 @@ export class BotEquipmentModGenerator
return true;
}
/**
* 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 modTemplate db object for modItem we get compatible mods from
* @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)
{
const supportedSubMods = desiredSlotObject._props.filters[0].Filter;
if (supportedSubMods)
{
// Filter mods
let filteredMods = this.filterWeaponModsByBlacklist(supportedSubMods, botEquipBlacklist, desiredSlotName);
let filteredMods = this.filterWeaponModsByBlacklist(
supportedSubMods,
botEquipBlacklist,
desiredSlotName,
);
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;
}
@ -736,7 +890,6 @@ export class BotEquipmentModGenerator
}
}
/**
* Get the possible items that fit a slot
* @param parentItemId item tpl to get compatible items for
@ -744,14 +897,22 @@ export class BotEquipmentModGenerator
* @param botEquipBlacklist equipment that should not be picked
* @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);
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;
}
@ -765,18 +926,24 @@ export class BotEquipmentModGenerator
* @param modSlot slot mods belong to
* @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)
{
return allowedMods;
}
let result: string[] = [];
// Get item blacklist and mod equipmet blackist as one array
const blacklist = this.itemFilterService.getBlacklistedItems().concat(botEquipBlacklist.equipment[modSlot] || []);
result = allowedMods.filter(x => !blacklist.includes(x));
// Get item blacklist and mod equipment blacklist as one array
const blacklist = this.itemFilterService.getBlacklistedItems().concat(
botEquipBlacklist.equipment[modSlot] || [],
);
result = allowedMods.filter((x) => !blacklist.includes(x));
return result;
}
@ -796,8 +963,13 @@ export class BotEquipmentModGenerator
let itemModPool = modPool[parentTemplate._id];
if (!itemModPool)
{
this.logger.warning(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"));
this.logger.warning(
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
modPool[parentTemplate._id] = {};
@ -818,7 +990,11 @@ export class BotEquipmentModGenerator
else if (camoraFirstSlot in itemModPool)
{
modSlot = camoraFirstSlot;
exhaustableModPool = new ExhaustableArray(this.mergeCamoraPoolsTogether(itemModPool), this.randomUtil, this.jsonUtil);
exhaustableModPool = new ExhaustableArray(
this.mergeCamoraPoolsTogether(itemModPool),
this.randomUtil,
this.jsonUtil,
);
}
else
{
@ -854,17 +1030,17 @@ export class BotEquipmentModGenerator
_id: modId,
_tpl: modTpl,
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
* @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[] = [];
for (const camoraKey in camorasWithShells)
@ -889,10 +1065,14 @@ export class BotEquipmentModGenerator
* e.g. filter out rifle scopes from SMGs
* @param weapon Weapon scopes will be added to
* @param scopes Full scope pool
* @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
*/
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);
@ -900,7 +1080,11 @@ export class BotEquipmentModGenerator
const whitelistedSightTypes = botWeaponSightWhitelist[weaponDetails[1]._parent];
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;
}
@ -920,14 +1104,23 @@ export class BotEquipmentModGenerator
// Edge case, what if item is a mount for a scope and not directly a scope?
// Check item is mount + has child items
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)
// 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
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
filteredScopesAndMods.push(item);
@ -935,14 +1128,16 @@ 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)
{
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 filteredScopesAndMods;
}
}
}

View File

@ -8,9 +8,12 @@ import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper";
import { WeightedRandomHelper } from "@spt-aki/helpers/WeightedRandomHelper";
import {
Common,
IBaseJsonSkills, IBaseSkill, IBotBase, Info,
Health as PmcHealth,
Skills as botSkills
IBaseJsonSkills,
IBaseSkill,
IBotBase,
Info,
Skills as botSkills,
} from "@spt-aki/models/eft/common/tables/IBotBase";
import { Appearance, Health, IBotType } from "@spt-aki/models/eft/common/tables/IBotType";
import { Item, Upd } from "@spt-aki/models/eft/common/tables/IItem";
@ -53,7 +56,7 @@ export class BotGenerator
@inject("BotDifficultyHelper") protected botDifficultyHelper: BotDifficultyHelper,
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ConfigServer") protected configServer: ConfigServer
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
@ -65,7 +68,7 @@ export class BotGenerator
* @param role e.g. assault / pmcbot
* @param difficulty easy/normal/hard/impossible
* @param botTemplate base bot template to use (e.g. assault/pmcbot)
* @returns
* @returns
*/
public generatePlayerScav(sessionId: string, role: string, difficulty: string, botTemplate: IBotType): IBotBase
{
@ -82,7 +85,7 @@ export class BotGenerator
botRelativeLevelDeltaMax: 0,
botCountToGenerate: 1,
botDifficulty: difficulty,
isPlayerScav: true
isPlayerScav: true,
};
bot = this.generateBot(sessionId, bot, botTemplate, botGenDetails);
@ -93,12 +96,13 @@ export class BotGenerator
/**
* Create x number of bots of the type/side/difficulty defined in botGenerationDetails
* @param sessionId Session id
* @param botGenerationDetails details on how to generate bots
* @param botGenerationDetails details on how to generate bots
* @returns array of bots
*/
public prepareAndGenerateBots(
sessionId: string,
botGenerationDetails: BotGenerationDetails): IBotBase[]
botGenerationDetails: BotGenerationDetails,
): IBotBase[]
{
const output: IBotBase[] = [];
for (let i = 0; i < botGenerationDetails.botCountToGenerate; i++)
@ -108,19 +112,24 @@ export class BotGenerator
bot.Info.Settings.Role = botGenerationDetails.role;
bot.Info.Side = botGenerationDetails.side;
bot.Info.Settings.BotDifficulty = botGenerationDetails.botDifficulty;
// Get raw json data for bot (Cloned)
const botJsonTemplate = this.jsonUtil.clone(this.botHelper.getBotTemplate(
(botGenerationDetails.isPmc)
? bot.Info.Side
: botGenerationDetails.role));
(botGenerationDetails.isPmc) ?
bot.Info.Side :
botGenerationDetails.role,
));
bot = this.generateBot(sessionId, bot, botJsonTemplate, botGenerationDetails);
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;
}
@ -142,21 +151,43 @@ export class BotGenerator
* @param botGenerationDetails details on how to generate the bot
* @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 botLevel = this.botLevelGenerator.generateBotLevel(botJsonTemplate.experience.level, botGenerationDetails, bot);
const botLevel = this.botLevelGenerator.generateBotLevel(
botJsonTemplate.experience.level,
botGenerationDetails,
bot,
);
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())
{
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
@ -167,7 +198,10 @@ export class BotGenerator
bot.Info.Experience = botLevel.exp;
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.Voice = this.randomUtil.getArrayValue(botJsonTemplate.appearance.voice);
bot.Health = this.generateHealth(botJsonTemplate.health, bot.Info.Side === "Savage");
@ -175,7 +209,13 @@ export class BotGenerator
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))
{
@ -205,7 +245,6 @@ export class BotGenerator
* @param appearance Appearance settings to choose from
* @param botGenerationDetails Generation details
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected setBotAppearance(bot: IBotBase, appearance: Appearance, botGenerationDetails: BotGenerationDetails): void
{
bot.Customization.Head = this.randomUtil.getArrayValue(appearance.head);
@ -216,17 +255,24 @@ export class BotGenerator
/**
* Create a bot nickname
* @param botJsonTemplate x.json from database
* @param botJsonTemplate x.json from database
* @param isPlayerScav Will bot be player scav
* @param botRole role of bot e.g. assault
* @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();
const playerProfile = this.profileHelper.getPmcProfile(sessionId);
// Simulate bot looking like a Player scav with the pmc name in brackets
if (botRole === "assault" && this.randomUtil.getChance100(this.botConfig.chanceAssaultScavHasPlayerScavName))
{
@ -237,7 +283,7 @@ export class BotGenerator
const pmcNames = [
...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)})`;
@ -253,7 +299,6 @@ export class BotGenerator
{
if (this.randomUtil.getChance100(this.pmcConfig.addPrefixToSameNamePMCAsPlayerChance))
{
const prefix = this.localisationService.getRandomTextThatMatchesPartialKey("pmc-name_prefix_");
name = `${prefix} ${name}`;
}
@ -268,7 +313,10 @@ export class BotGenerator
*/
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`);
}
@ -280,68 +328,68 @@ export class BotGenerator
*/
protected generateHealth(healthObj: Health, playerScav = false): PmcHealth
{
const bodyParts = (playerScav)
? healthObj.BodyParts[0]
: this.randomUtil.getArrayValue(healthObj.BodyParts);
const bodyParts = playerScav ?
healthObj.BodyParts[0] :
this.randomUtil.getArrayValue(healthObj.BodyParts);
const newHealth: PmcHealth = {
Hydration: {
Current: this.randomUtil.getInt(healthObj.Hydration.min, healthObj.Hydration.max),
Maximum: healthObj.Hydration.max
Maximum: healthObj.Hydration.max,
},
Energy: {
Current: this.randomUtil.getInt(healthObj.Energy.min, healthObj.Energy.max),
Maximum: healthObj.Energy.max
Maximum: healthObj.Energy.max,
},
Temperature: {
Current: this.randomUtil.getInt(healthObj.Temperature.min, healthObj.Temperature.max),
Maximum: healthObj.Temperature.max
Maximum: healthObj.Temperature.max,
},
BodyParts: {
Head: {
Health: {
Current: this.randomUtil.getInt(bodyParts.Head.min, bodyParts.Head.max),
Maximum: Math.round(bodyParts.Head.max)
}
Maximum: Math.round(bodyParts.Head.max),
},
},
Chest: {
Health: {
Current: this.randomUtil.getInt(bodyParts.Chest.min, bodyParts.Chest.max),
Maximum: Math.round(bodyParts.Chest.max)
}
Maximum: Math.round(bodyParts.Chest.max),
},
},
Stomach: {
Health: {
Current: this.randomUtil.getInt(bodyParts.Stomach.min, bodyParts.Stomach.max),
Maximum: Math.round(bodyParts.Stomach.max)
}
Maximum: Math.round(bodyParts.Stomach.max),
},
},
LeftArm: {
Health: {
Current: this.randomUtil.getInt(bodyParts.LeftArm.min, bodyParts.LeftArm.max),
Maximum: Math.round(bodyParts.LeftArm.max)
}
Maximum: Math.round(bodyParts.LeftArm.max),
},
},
RightArm: {
Health: {
Current: this.randomUtil.getInt(bodyParts.RightArm.min, bodyParts.RightArm.max),
Maximum: Math.round(bodyParts.RightArm.max)
}
Maximum: Math.round(bodyParts.RightArm.max),
},
},
LeftLeg: {
Health: {
Current: this.randomUtil.getInt(bodyParts.LeftLeg.min, bodyParts.LeftLeg.max),
Maximum: Math.round(bodyParts.LeftLeg.max)
}
Maximum: Math.round(bodyParts.LeftLeg.max),
},
},
RightLeg: {
Health: {
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;
@ -350,14 +398,14 @@ export class BotGenerator
/**
* Get a bots skills with randomsied progress value between the min and max values
* @param botSkills Skills that should have their progress value randomised
* @returns
* @returns
*/
protected generateSkills(botSkills: IBaseJsonSkills): botSkills
{
const skillsToReturn: botSkills = {
Common: this.getSkillsWithRandomisedProgressValue(botSkills.Common, true),
Mastering: this.getSkillsWithRandomisedProgressValue(botSkills.Mastering, false),
Points: 0
Points: 0,
};
return skillsToReturn;
@ -369,7 +417,10 @@ export class BotGenerator
* @param isCommonSkills Are the skills 'common' skills
* @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)
{
@ -384,22 +435,22 @@ export class BotGenerator
{
return null;
}
// All skills have id and progress props
const skillToAdd: IBaseSkill = {
Id: skillKey,
Progress: this.randomUtil.getInt(skill.min, skill.max)
Progress: this.randomUtil.getInt(skill.min, skill.max),
};
// Common skills have additional props
if (isCommonSkills)
{
(skillToAdd as Common).PointsEarnedDuringSession = 0;
(skillToAdd as Common).LastAccess = 0;
}
return skillToAdd;
}).filter(x => x !== null);
}).filter((x) => x !== null);
}
/**
@ -422,7 +473,7 @@ export class BotGenerator
const defaultInventory = "55d7217a4bdc2d86028b456d";
const itemsByParentHash: Record<string, Item[]> = {};
const inventoryItemHash: Record<string, Item> = {};
// Generate inventoryItem list
let inventoryId = "";
for (const item of profile.Inventory.items)
@ -482,7 +533,9 @@ export class BotGenerator
}
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",
KillerProfileId: "Unknown",
KillerName: "Unknown",
WeaponName: "Unknown"
}
WeaponName: "Unknown",
},
};
const inventoryItem: Item = {
@ -515,11 +568,11 @@ export class BotGenerator
parentId: bot.Inventory.equipment,
slotId: "Dogtag",
location: undefined,
upd: upd
upd: upd,
};
bot.Inventory.items.push(inventoryItem);
return bot;
}
}
}

View File

@ -39,7 +39,7 @@ export class BotInventoryGenerator
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("BotEquipmentModPoolService") protected botEquipmentModPoolService: BotEquipmentModPoolService,
@inject("BotEquipmentModGenerator") protected botEquipmentModGenerator: BotEquipmentModGenerator,
@inject("ConfigServer") protected configServer: ConfigServer
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
@ -54,7 +54,13 @@ export class BotInventoryGenerator
* @param botLevel Level of bot being generated
* @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 equipmentChances = botJsonTemplate.chances;
@ -66,7 +72,16 @@ export class BotInventoryGenerator
this.generateAndAddEquipmentToBot(templateInventory, equipmentChances, botRole, botInventory, botLevel);
// 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)
this.botLootGenerator.generateLoot(sessionId, botJsonTemplate, isPmc, botRole, botInventory, botLevel);
@ -99,24 +114,24 @@ export class BotInventoryGenerator
items: [
{
_id: equipmentId,
_tpl: equipmentTpl
_tpl: equipmentTpl,
},
{
_id: stashId,
_tpl: stashTpl
_tpl: stashTpl,
},
{
_id: questRaidItemsId,
_tpl: questRaidItemsTpl
_tpl: questRaidItemsTpl,
},
{
_id: questStashItemsId,
_tpl: questStashItemsTpl
_tpl: questStashItemsTpl,
},
{
_id: sortingTableId,
_tpl: sortingTableTpl
}
_tpl: sortingTableTpl,
},
],
equipment: equipmentId,
stash: stashId,
@ -124,7 +139,7 @@ export class BotInventoryGenerator
questStashItems: questStashItemsId,
sortingTable: sortingTableId,
hideoutAreaStashes: {},
fastPanel: {}
fastPanel: {},
};
}
@ -136,7 +151,13 @@ export class BotInventoryGenerator
* @param botInventory Inventory to add equipment to
* @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
const excludedSlots: string[] = [
@ -147,7 +168,7 @@ export class BotInventoryGenerator
EquipmentSlots.TACTICAL_VEST,
EquipmentSlots.FACE_COVER,
EquipmentSlots.HEADWEAR,
EquipmentSlots.EARPIECE
EquipmentSlots.EARPIECE,
];
const botEquipConfig = this.botConfig.equipment[this.botGeneratorHelper.getBotEquipmentRole(botRole)];
@ -155,21 +176,69 @@ export class BotInventoryGenerator
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))
{
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
this.generateEquipment(EquipmentSlots.FACE_COVER, templateInventory.equipment.FaceCover, templateInventory.mods, 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);
this.generateEquipment(
EquipmentSlots.FACE_COVER,
templateInventory.equipment.FaceCover,
templateInventory.mods,
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,
botRole: string,
inventory: PmcInventory,
randomisationDetails: RandomisationDetails): void
randomisationDetails: RandomisationDetails,
): void
{
const spawnChance = ([EquipmentSlots.POCKETS, EquipmentSlots.SECURED_CONTAINER] as string[]).includes(equipmentSlot)
? 100
: spawnChances.equipment[equipmentSlot];
const spawnChance =
([EquipmentSlots.POCKETS, EquipmentSlots.SECURED_CONTAINER] as string[]).includes(equipmentSlot) ?
100 :
spawnChances.equipment[equipmentSlot];
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;
}
@ -216,7 +289,13 @@ export class BotInventoryGenerator
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
return;
@ -227,19 +306,35 @@ export class BotInventoryGenerator
_tpl: equipmentItemTpl,
parentId: inventory.equipment,
slotId: equipmentSlot,
...this.botGeneratorHelper.generateExtraPropertiesForItem(itemTemplate[1], botRole)
...this.botGeneratorHelper.generateExtraPropertiesForItem(itemTemplate[1], botRole),
};
// use dynamic mod pool if enabled in config
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);
}
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
* @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
* @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);
for (const modSlot of Object.keys(modPool ?? []))
{
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)
{
@ -283,7 +381,16 @@ export class BotInventoryGenerator
* @param botLevel level of bot having weapon generated
* @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);
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
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
* @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);
return [
{
slot: EquipmentSlots.FIRST_PRIMARY_WEAPON,
shouldSpawn: shouldSpawnPrimary
shouldSpawn: shouldSpawnPrimary,
},
{
slot: EquipmentSlots.SECOND_PRIMARY_WEAPON,
shouldSpawn: shouldSpawnPrimary
? this.randomUtil.getChance100(equipmentChances.equipment.SecondPrimaryWeapon)
: false
shouldSpawn: shouldSpawnPrimary ?
this.randomUtil.getChance100(equipmentChances.equipment.SecondPrimaryWeapon) :
false,
},
{
slot: EquipmentSlots.HOLSTER,
shouldSpawn: shouldSpawnPrimary
? this.randomUtil.getChance100(equipmentChances.equipment.Holster) // Primary weapon = roll for chance at pistol
: true // No primary = force pistol
}
shouldSpawn: shouldSpawnPrimary ?
this.randomUtil.getChance100(equipmentChances.equipment.Holster) // Primary weapon = roll for chance at pistol
:
true, // No primary = force pistol
},
];
}
@ -333,18 +451,19 @@ export class BotInventoryGenerator
* @param equipmentChances Chances bot can have equipment equipped
* @param botRole assault/pmcBot/bossTagilla etc
* @param isPmc Is the bot being generated as a pmc
* @param itemGenerationWeights
* @param itemGenerationWeights
*/
protected addWeaponAndMagazinesToInventory(
sessionId: string,
weaponSlot: { slot: EquipmentSlots; shouldSpawn: boolean; },
weaponSlot: {slot: EquipmentSlots; shouldSpawn: boolean;},
templateInventory: Inventory,
botInventory: PmcInventory,
equipmentChances: Chances,
botRole: string,
isPmc: boolean,
itemGenerationWeights: Generation,
botLevel: number): void
botLevel: number,
): void
{
const generatedWeapon = this.botWeaponGenerator.generateRandomWeapon(
sessionId,
@ -354,10 +473,16 @@ export class BotInventoryGenerator
equipmentChances.mods,
botRole,
isPmc,
botLevel);
botLevel,
);
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(
@inject("WinstonLogger") protected logger: ILogger,
@inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("DatabaseServer") protected databaseServer: DatabaseServer
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
)
{ }
{}
/**
* Return a randomised bot level and exp value
@ -26,12 +26,20 @@ export class BotLevelGenerator
* @param bot being level is being generated for
* @returns IRandomisedBotLevelResult object
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public generateBotLevel(levelDetails: MinMax, botGenerationDetails: BotGenerationDetails, bot: IBotBase): IRandomisedBotLevelResult
public generateBotLevel(
levelDetails: MinMax,
botGenerationDetails: BotGenerationDetails,
bot: IBotBase,
): IRandomisedBotLevelResult
{
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.
let exp = 0;
const level = this.randomUtil.getInt(1, highestLevel);
@ -47,16 +55,21 @@ export class BotLevelGenerator
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 relativeDeltaMax max delta above player level to go
* @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);
@ -68,4 +81,4 @@ export class BotLevelGenerator
return level;
}
}
}

View File

@ -30,7 +30,7 @@ export class BotLootGenerator
{
protected botConfig: IBotConfig;
protected pmcConfig: IPmcConfig;
constructor(
@inject("WinstonLogger") protected logger: ILogger,
@inject("HashUtil") protected hashUtil: HashUtil,
@ -44,7 +44,7 @@ export class BotLootGenerator
@inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper,
@inject("BotLootCacheService") protected botLootCacheService: BotLootCacheService,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ConfigServer") protected configServer: ConfigServer
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
@ -60,15 +60,24 @@ export class BotLootGenerator
* @param botInventory Inventory to add loot to
* @param botLevel Level of bot
*/
public generateLoot(sessionId: string, botJsonTemplate: IBotType, isPmc: boolean, botRole: string, botInventory: PmcInventory, botLevel: number): void
public generateLoot(
sessionId: string,
botJsonTemplate: IBotType,
isPmc: boolean,
botRole: string,
botInventory: PmcInventory,
botLevel: number,
): void
{
// Limits on item types to be added as loot
const itemCounts = botJsonTemplate.generation.items;
const backpackLootCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.backpackLoot.weights);
const pocketLootCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.pocketLoot.weights);
const vestLootCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.vestLoot.weights);
const specialLootItemCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.specialItems.weights);
const specialLootItemCount = this.weightedRandomHelper.getWeightedValue<number>(
itemCounts.specialItems.weights,
);
const healingItemCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.healing.weights);
const drugItemCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.drugs.weights);
const stimItemCount = this.weightedRandomHelper.getWeightedValue<number>(itemCounts.stims.weights);
@ -88,7 +97,8 @@ export class BotLootGenerator
containersBotHasAvailable,
specialLootItemCount,
botInventory,
botRole);
botRole,
);
// Healing items / Meds
this.addLootFromPool(
@ -99,7 +109,8 @@ export class BotLootGenerator
botRole,
false,
0,
isPmc);
isPmc,
);
// Drugs
this.addLootFromPool(
@ -110,7 +121,8 @@ export class BotLootGenerator
botRole,
false,
0,
isPmc);
isPmc,
);
// Stims
this.addLootFromPool(
@ -121,7 +133,8 @@ export class BotLootGenerator
botRole,
true,
0,
isPmc);
isPmc,
);
// Grenades
this.addLootFromPool(
@ -132,8 +145,8 @@ export class BotLootGenerator
botRole,
false,
0,
isPmc);
isPmc,
);
// Backpack - generate loot if they have one
if (containersBotHasAvailable.includes(EquipmentSlots.BACKPACK))
@ -141,7 +154,16 @@ export class BotLootGenerator
// Add randomly generated weapon to PMC backpacks
if (isPmc && this.randomUtil.getChance100(this.pmcConfig.looseWeaponInBackpackChancePercent))
{
this.addLooseWeaponsToInventorySlot(sessionId, botInventory, EquipmentSlots.BACKPACK, botJsonTemplate.inventory, botJsonTemplate.chances.mods, botRole, isPmc, botLevel);
this.addLooseWeaponsToInventorySlot(
sessionId,
botInventory,
EquipmentSlots.BACKPACK,
botJsonTemplate.inventory,
botJsonTemplate.chances.mods,
botRole,
isPmc,
botLevel,
);
}
this.addLootFromPool(
@ -152,9 +174,10 @@ export class BotLootGenerator
botRole,
true,
this.pmcConfig.maxBackpackLootTotalRub,
isPmc);
isPmc,
);
}
// TacticalVest - generate loot if they have one
if (containersBotHasAvailable.includes(EquipmentSlots.TACTICAL_VEST))
{
@ -167,10 +190,10 @@ export class BotLootGenerator
botRole,
true,
this.pmcConfig.maxVestLootTotalRub,
isPmc);
isPmc,
);
}
// Pockets
this.addLootFromPool(
this.botLootCacheService.getLootFromCache(botRole, isPmc, LootCacheType.POCKET, botJsonTemplate),
@ -180,7 +203,8 @@ export class BotLootGenerator
botRole,
true,
this.pmcConfig.maxPocketLootTotalRub,
isPmc);
isPmc,
);
}
/**
@ -192,13 +216,12 @@ export class BotLootGenerator
{
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);
}
if (botInventory.items.find(x => x.slotId === EquipmentSlots.BACKPACK))
if (botInventory.items.find((x) => x.slotId === EquipmentSlots.BACKPACK))
{
result.push(EquipmentSlots.BACKPACK);
}
@ -222,7 +245,8 @@ export class BotLootGenerator
botRole,
false,
0,
true);
true,
);
const surv12 = this.itemHelper.getItem("5d02797c86f774203f38e30a")[1];
this.addLootFromPool(
@ -233,7 +257,8 @@ export class BotLootGenerator
botRole,
false,
0,
true);
true,
);
const morphine = this.itemHelper.getItem("544fb3f34bdc2d03748b456a")[1];
this.addLootFromPool(
@ -244,7 +269,8 @@ export class BotLootGenerator
botRole,
false,
0,
true);
true,
);
const afak = this.itemHelper.getItem("60098ad7c2240c0fe85c570a")[1];
this.addLootFromPool(
@ -255,7 +281,8 @@ export class BotLootGenerator
botRole,
false,
0,
true);
true,
);
}
/**
@ -290,14 +317,15 @@ export class BotLootGenerator
botRole: string,
useLimits = false,
totalValueLimitRub = 0,
isPmc = false): void
isPmc = false,
): void
{
// Loot pool has items
if (pool.length)
{
let currentTotalRub = 0;
const itemLimits: Record<string, number> = {};
const itemSpawnLimits: Record<string,Record<string, number>> = {};
const itemSpawnLimits: Record<string, Record<string, number>> = {};
let fitItemIntoContainerAttempts = 0;
for (let i = 0; i < totalItemCount; i++)
{
@ -306,7 +334,7 @@ export class BotLootGenerator
const itemsToAdd: Item[] = [{
_id: id,
_tpl: itemToAddTemplate._id,
...this.botGeneratorHelper.generateExtraPropertiesForItem(itemToAddTemplate, botRole)
...this.botGeneratorHelper.generateExtraPropertiesForItem(itemToAddTemplate, botRole),
}];
if (useLimits)
@ -321,11 +349,19 @@ export class BotLootGenerator
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--;
continue;
}
}
}
// Fill ammo box
@ -345,13 +381,21 @@ export class BotLootGenerator
}
// 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)
{
fitItemIntoContainerAttempts++;
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;
}
@ -383,16 +427,48 @@ export class BotLootGenerator
* @param botRole bots role .e.g. pmcBot
* @param isPmc are we generating for a pmc
*/
protected addLooseWeaponsToInventorySlot(sessionId: string, botInventory: PmcInventory, equipmentSlot: string, templateInventory: Inventory, modChances: ModsChances, botRole: string, isPmc: boolean, botLevel: number): void
protected addLooseWeaponsToInventorySlot(
sessionId: string,
botInventory: PmcInventory,
equipmentSlot: string,
templateInventory: Inventory,
modChances: ModsChances,
botRole: string,
isPmc: boolean,
botLevel: number,
): void
{
const chosenWeaponType = this.randomUtil.getArrayValue([EquipmentSlots.FIRST_PRIMARY_WEAPON, EquipmentSlots.FIRST_PRIMARY_WEAPON, EquipmentSlots.FIRST_PRIMARY_WEAPON, EquipmentSlots.HOLSTER]);
const randomisedWeaponCount = this.randomUtil.getInt(this.pmcConfig.looseWeaponInBackpackLootMinMax.min, this.pmcConfig.looseWeaponInBackpackLootMinMax.max);
const chosenWeaponType = this.randomUtil.getArrayValue([
EquipmentSlots.FIRST_PRIMARY_WEAPON,
EquipmentSlots.FIRST_PRIMARY_WEAPON,
EquipmentSlots.FIRST_PRIMARY_WEAPON,
EquipmentSlots.HOLSTER,
]);
const randomisedWeaponCount = this.randomUtil.getInt(
this.pmcConfig.looseWeaponInBackpackLootMinMax.min,
this.pmcConfig.looseWeaponInBackpackLootMinMax.max,
);
if (randomisedWeaponCount > 0)
{
for (let i = 0; i < randomisedWeaponCount; i++)
{
const generatedWeapon = this.botWeaponGenerator.generateRandomWeapon(sessionId, chosenWeaponType, templateInventory, botInventory.equipment, modChances, botRole, isPmc, botLevel);
this.botWeaponGeneratorHelper.addItemWithChildrenToEquipmentSlot([equipmentSlot], generatedWeapon.weapon[0]._id, generatedWeapon.weapon[0]._tpl, [...generatedWeapon.weapon], botInventory);
const generatedWeapon = this.botWeaponGenerator.generateRandomWeapon(
sessionId,
chosenWeaponType,
templateInventory,
botInventory.equipment,
modChances,
botRole,
isPmc,
botLevel,
);
this.botWeaponGeneratorHelper.addItemWithChildrenToEquipmentSlot(
[equipmentSlot],
generatedWeapon.weapon[0]._id,
generatedWeapon.weapon[0]._tpl,
[...generatedWeapon.weapon],
botInventory,
);
}
}
}
@ -405,7 +481,12 @@ export class BotLootGenerator
*/
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];
}
@ -432,7 +513,7 @@ export class BotLootGenerator
* All values are set to 0
* @param isPmc Is the bot a pmc
* @param botRole Role the bot has
* @param limitCount
* @param limitCount
*/
protected initItemLimitArray(isPmc: boolean, botRole: string, limitCount: Record<string, number>): void
{
@ -443,7 +524,7 @@ export class BotLootGenerator
limitCount[limit] = 0;
}
}
/**
* Check if an item has reached its bot-specific spawn limit
* @param itemTemplate Item we check to see if its reached spawn limit
@ -453,7 +534,13 @@ export class BotLootGenerator
* @param itemSpawnLimits The limits this bot is allowed to have
* @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
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
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;
}
@ -505,9 +598,9 @@ export class BotLootGenerator
{
// PMCs have a different stack max size
const minStackSize = itemTemplate._props.StackMinRandom;
const maxStackSize = (isPmc)
? this.pmcConfig.dynamicLoot.moneyStackLimits[itemTemplate._id]
: itemTemplate._props.StackMaxRandom;
const maxStackSize = isPmc ?
this.pmcConfig.dynamicLoot.moneyStackLimits[itemTemplate._id] :
itemTemplate._props.StackMaxRandom;
const randomSize = this.randomUtil.getInt(minStackSize, maxStackSize);
if (!moneyItem.upd)
@ -515,7 +608,7 @@ export class BotLootGenerator
moneyItem.upd = {};
}
moneyItem.upd.StackObjectsCount = randomSize;
moneyItem.upd.StackObjectsCount = randomSize;
}
/**
@ -526,16 +619,16 @@ export class BotLootGenerator
*/
protected randomiseAmmoStackSize(isPmc: boolean, itemTemplate: ITemplateItem, ammoItem: Item): void
{
const randomSize = itemTemplate._props.StackMaxSize === 1
? 1
: this.randomUtil.getInt(itemTemplate._props.StackMinRandom, itemTemplate._props.StackMaxRandom);
const randomSize = itemTemplate._props.StackMaxSize === 1 ?
1 :
this.randomUtil.getInt(itemTemplate._props.StackMinRandom, itemTemplate._props.StackMaxRandom);
if (!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()];
}
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"];
}
@ -570,7 +665,6 @@ export class BotLootGenerator
*/
protected getMatchingIdFromSpawnLimits(itemTemplate: ITemplateItem, spawnLimits: Record<string, number>): string
{
if (itemTemplate._id in spawnLimits)
{
return itemTemplate._id;
@ -585,4 +679,4 @@ export class BotLootGenerator
// parentId and tplid not found
return undefined;
}
}
}

View File

@ -51,7 +51,7 @@ export class BotWeaponGenerator
@inject("BotEquipmentModGenerator") protected botEquipmentModGenerator: BotEquipmentModGenerator,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("RepairService") protected repairService: RepairService,
@injectAll("InventoryMagGen") protected inventoryMagGenComponents: IInventoryMagGen[]
@injectAll("InventoryMagGen") protected inventoryMagGenComponents: IInventoryMagGen[],
)
{
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
@ -64,16 +64,35 @@ export class BotWeaponGenerator
* Pick a random weapon based on weightings and generate a functional weapon
* @param equipmentSlot Primary/secondary/holster
* @param botTemplateInventory e.g. assault.json
* @param weaponParentId
* @param modChances
* @param weaponParentId
* @param modChances
* @param botRole role of bot, e.g. assault/followerBully
* @param isPmc Is weapon generated for a pmc
* @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);
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
* @returns GenerateWeaponResult object
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public generateWeaponByTpl(sessionId: string, weaponTpl: string, equipmentSlot: string, botTemplateInventory: Inventory, weaponParentId: string, modChances: ModsChances, botRole: string, isPmc: boolean, botLevel: number): GenerateWeaponResult
public generateWeaponByTpl(
sessionId: string,
weaponTpl: string,
equipmentSlot: string,
botTemplateInventory: Inventory,
weaponParentId: string,
modChances: ModsChances,
botRole: string,
isPmc: boolean,
botLevel: number,
): GenerateWeaponResult
{
const modPool = botTemplateInventory.mods;
const weaponItemTemplate = this.itemHelper.getItem(weaponTpl)[1];
@ -123,7 +151,13 @@ export class BotWeaponGenerator
const ammoTpl = this.getWeightedCompatibleAmmo(botTemplateInventory.Ammo, weaponItemTemplate);
// 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
if (isPmc && this.randomUtil.getChance100(this.pmcConfig.weaponHasEnhancementChancePercent))
@ -137,32 +171,52 @@ export class BotWeaponGenerator
{
const botEquipmentRole = this.botGeneratorHelper.getBotEquipmentRole(botRole);
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
if (!this.isWeaponValid(weaponWithModsArray, botRole))
{
// 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
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);
}
// Add cartridge to gun chamber if weapon has slot for it
if (weaponItemTemplate._props.Chambers?.length === 1
&& weaponItemTemplate._props.Chambers[0]?._name === "patron_in_weapon"
&& weaponItemTemplate._props.Chambers[0]?._props?.filters[0]?.Filter?.includes(ammoTpl))
if (
weaponItemTemplate._props.Chambers?.length === 1 &&
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");
}
// 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;
if (ubglMod)
{
@ -176,7 +230,7 @@ export class BotWeaponGenerator
chosenAmmoTpl: ammoTpl,
chosenUbglAmmoTpl: ubglAmmoTpl,
weaponMods: modPool,
weaponTemplate: weaponItemTemplate
weaponTemplate: weaponItemTemplate,
};
}
@ -189,7 +243,7 @@ export class BotWeaponGenerator
protected addCartridgeToChamber(weaponWithModsArray: Item[], ammoTpl: string, desiredSlotId: string): void
{
// Check for slot first
const existingItemWithSlot = weaponWithModsArray.find(x => x.slotId === desiredSlotId);
const existingItemWithSlot = weaponWithModsArray.find((x) => x.slotId === desiredSlotId);
if (!existingItemWithSlot)
{
// Not found, add fresh
@ -198,14 +252,14 @@ export class BotWeaponGenerator
_tpl: ammoTpl,
parentId: weaponWithModsArray[0]._id,
slotId: desiredSlotId,
upd: {StackObjectsCount: 1}
upd: {StackObjectsCount: 1},
});
}
else
{
// Already exists, update values
existingItemWithSlot.upd = {
StackObjectsCount: 1
StackObjectsCount: 1,
};
existingItemWithSlot._tpl = ammoTpl;
}
@ -216,19 +270,25 @@ export class BotWeaponGenerator
* add additional properties based on weapon type
* @param weaponTpl Weapon tpl to create item with
* @param weaponParentId Weapons parent id
* @param equipmentSlot e.g. primary/secondary/holster
* @param equipmentSlot e.g. primary/secondary/holster
* @param weaponItemTemplate db template for weapon
* @param botRole for durability values
* @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 [{
_id: this.hashUtil.generate(),
_tpl: weaponTpl,
parentId: weaponParentId,
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
* @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
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 = [];
// 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];
preset._items[0] = {
...parentItem, ...{
...parentItem,
...{
parentId: weaponParentId,
slotId: equipmentSlot,
...this.botGeneratorHelper.generateExtraPropertiesForItem(itemTemplate, botRole)
}
...this.botGeneratorHelper.generateExtraPropertiesForItem(itemTemplate, botRole),
},
};
weaponMods.push(...preset._items);
}
@ -304,17 +373,30 @@ export class BotWeaponGenerator
const allowedTpls = modSlot._props.filters[0].Filter;
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)
{
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;
}
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;
}
@ -332,12 +414,17 @@ export class BotWeaponGenerator
* @param inventory Inventory to add magazines to
* @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 weaponTemplate = generatedWeaponResult.weaponTemplate;
const magazineTpl = this.getMagazineTplFromWeaponTemplate(weaponAndMods, weaponTemplate, botRole);
const magTemplate = this.itemHelper.getItem(magazineTpl)[1];
if (!magTemplate)
{
@ -349,7 +436,9 @@ export class BotWeaponGenerator
const ammoTemplate = this.itemHelper.getItem(generatedWeaponResult.chosenAmmoTpl)[1];
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;
}
@ -360,11 +449,24 @@ export class BotWeaponGenerator
this.addUbglGrenadesToBotInventory(weaponAndMods, generatedWeaponResult, inventory);
}
const inventoryMagGenModel = new InventoryMagGen(magWeights, magTemplate, weaponTemplate, ammoTemplate, inventory);
this.inventoryMagGenComponents.find(v => v.canHandleInventoryMagGen(inventoryMagGenModel)).process(inventoryMagGenModel);
const inventoryMagGenModel = new InventoryMagGen(
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)
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 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
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];
// Define min/max of how many grenades bot will have
const ubglMinMax:GenerationData = {
const ubglMinMax: GenerationData = {
// eslint-disable-next-line @typescript-eslint/naming-convention
weights: {"1": 1, "2": 1},
whitelist: []
whitelist: [],
};
// get ammo template from db
const ubglAmmoDbTemplate = this.itemHelper.getItem(generatedWeaponResult.chosenUbglAmmoTpl)[1];
// Add greandes to bot inventory
const ubglAmmoGenModel = new InventoryMagGen(ubglMinMax, ubglDbTemplate, ubglDbTemplate, ubglAmmoDbTemplate, inventory);
this.inventoryMagGenComponents.find(v => v.canHandleInventoryMagGen(ubglAmmoGenModel)).process(ubglAmmoGenModel);
const ubglAmmoGenModel = new InventoryMagGen(
ubglMinMax,
ubglDbTemplate,
ubglDbTemplate,
ubglAmmoDbTemplate,
inventory,
);
this.inventoryMagGenComponents.find((v) => v.canHandleInventoryMagGen(ubglAmmoGenModel)).process(
ubglAmmoGenModel,
);
// Store extra grenades in secure container
this.addAmmoToSecureContainer(5, generatedWeaponResult.chosenUbglAmmoTpl, 20, inventory);
@ -404,17 +518,27 @@ export class BotWeaponGenerator
* @param stackSize Size of the ammo stack to add
* @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++)
{
const id = this.hashUtil.generate();
this.botWeaponGeneratorHelper.addItemWithChildrenToEquipmentSlot([EquipmentSlots.SECURED_CONTAINER], id, ammoTpl, [{
_id: id,
_tpl: ammoTpl,
upd: { StackObjectsCount: stackSize }
}],
inventory);
this.botWeaponGeneratorHelper.addItemWithChildrenToEquipmentSlot(
[EquipmentSlots.SECURED_CONTAINER],
id,
ammoTpl,
[{
_id: id,
_tpl: ammoTpl,
upd: {StackObjectsCount: stackSize},
}],
inventory,
);
}
}
@ -425,9 +549,13 @@ export class BotWeaponGenerator
* @param botRole the bot type we are getting the magazine for
* @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)
{
// Edge case - magazineless chamber loaded weapons dont have magazines, e.g. mp18
@ -441,11 +569,15 @@ export class BotWeaponGenerator
if (!weaponTemplate._props.isChamberLoad)
{
// 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);
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;
}
@ -459,23 +591,42 @@ export class BotWeaponGenerator
* @param weaponTemplate the weapon we want to pick ammo for
* @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 compatibleCartridges = ammo[desiredCaliber];
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
return weaponTemplate._props.defAmmo;
}
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)
return weaponTemplate._props.defAmmo;
@ -503,7 +654,9 @@ export class BotWeaponGenerator
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])
{
return;
@ -559,9 +712,9 @@ export class BotWeaponGenerator
parentId: ubglMod._id,
slotId: "patron_in_weapon",
upd: {
StackObjectsCount: 1
}
}
StackObjectsCount: 1,
},
},
);
}
@ -573,9 +726,16 @@ export class BotWeaponGenerator
* @param newStackSize how many cartridges should go into the magazine
* @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)
{
// 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".
// 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
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)
{
camora._tpl = ammoTpl;
@ -613,8 +773,8 @@ export class BotWeaponGenerator
}
else
{
camora.upd = { StackObjectsCount: 1 };
camora.upd = {StackObjectsCount: 1};
}
}
}
}
}

View File

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

View File

@ -6,9 +6,14 @@ import { PresetHelper } from "@spt-aki/helpers/PresetHelper";
import { RagfairServerHelper } from "@spt-aki/helpers/RagfairServerHelper";
import { IContainerMinMax, IStaticContainer } from "@spt-aki/models/eft/common/ILocation";
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 { 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 { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { Money } from "@spt-aki/models/enums/Money";
@ -25,17 +30,17 @@ import { ProbabilityObject, ProbabilityObjectArray, RandomUtil } from "@spt-aki/
export interface IContainerItem
{
items: Item[]
width: number
height: number
items: Item[];
width: number;
height: number;
}
export interface IContainerGroupCount
{
/** 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 */
chosenCount: number
chosenCount: number;
}
@injectable()
@ -56,7 +61,7 @@ export class LocationGenerator
@inject("ContainerHelper") protected containerHelper: ContainerHelper,
@inject("PresetHelper") protected presetHelper: PresetHelper,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ConfigServer") protected configServer: ConfigServer
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
this.locationConfig = this.configServer.getConfig(ConfigTypes.LOCATION);
@ -68,7 +73,10 @@ export class LocationGenerator
* @param staticAmmoDist Static ammo distribution - database.loot.staticAmmo
* @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 locationId = locationBase.Id.toLowerCase();
@ -84,7 +92,9 @@ export class LocationGenerator
// Add mounted weapons to output loot
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)
{
this.logger.error(`Unable to find static container data for map: ${locationBase.Name}`);
@ -105,23 +115,40 @@ export class LocationGenerator
const staticLootDist = db.loot.staticLoot;
const guaranteedContainers = this.getGuaranteedContainers(allStaticContainersOnMap);
staticContainerCount += guaranteedContainers.length;
// Add loot to guaranteed containers and add to result
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);
}
this.logger.success(`Added ${guaranteedContainers.length} guaranteed containers`);
// 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)
{
const containerWithLoot = this.addLootToContainer(container, staticForcedOnMap, staticLootDist, staticAmmoDist, locationId);
const containerWithLoot = this.addLootToContainer(
container,
staticForcedOnMap,
staticLootDist,
staticAmmoDist,
locationId,
);
result.push(containerWithLoot.template);
}
@ -129,7 +156,7 @@ export class LocationGenerator
}
// Group containers by their groupId
const staticContainerGroupData: IStaticContainer = db.locations[locationId].statics;
const staticContainerGroupData: IStaticContainer = db.locations[locationId].statics;
const mapping = this.getGroupIdToContainerMappings(staticContainerGroupData, staticRandomisableContainersOnMap);
// For each of the container groups, choose from the pool of containers, hydrate container with loot and add to result array
@ -145,7 +172,9 @@ export class LocationGenerator
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;
}
@ -178,48 +207,72 @@ export class LocationGenerator
for (const chosenContainerId of chosenContainerIds)
{
// 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)
{
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;
}
// 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);
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;
}
/**
* Get containers with a non-100% chance to spawn OR are NOT on the container type randomistion blacklist
* @param staticContainers
* @param staticContainers
* @returns IStaticContainerData array
*/
protected getRandomisableContainersOnMap(staticContainers: IStaticContainerData[]): IStaticContainerData[]
{
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,
)
);
}
/**
* Get containers with 100% spawn rate or have a type on the randomistion ignore list
* @param staticContainersOnMap
* @param staticContainersOnMap
* @returns IStaticContainerData array
*/
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 containerData Containers and probability values for a groupId
* @returns List of chosen container Ids
@ -231,15 +284,19 @@ export class LocationGenerator
const containerIds = Object.keys(containerData.containerIdsWithProbability);
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;
}
// 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);
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));
@ -254,7 +311,8 @@ export class LocationGenerator
*/
protected getGroupIdToContainerMappings(
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
const mapping: Record<string, IContainerGroupCount> = {};
@ -266,9 +324,15 @@ export class LocationGenerator
mapping[groupId] = {
containerIdsWithProbability: {},
chosenCount: this.randomUtil.getInt(
Math.round(groupData.minContainers * this.locationConfig.containerRandomisationSettings.containerGroupMinSizeMultiplier),
Math.round(groupData.maxContainers * this.locationConfig.containerRandomisationSettings.containerGroupMaxSizeMultiplier)
)
Math.round(
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)
{
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;
}
mapping[groupData.groupId].containerIdsWithProbability[container.template.Id] = container.probability;
@ -314,7 +380,8 @@ export class LocationGenerator
staticForced: IStaticForcedProps[],
staticLootDist: Record<string, IStaticLootDetails>,
staticAmmoDist: Record<string, IStaticAmmoDetails[]>,
locationName: string): IStaticContainerData
locationName: string,
): IStaticContainerData
{
const container = this.jsonUtil.clone(staticContainer);
const containerTpl = container.template.Items[0]._tpl;
@ -333,7 +400,7 @@ export class LocationGenerator
const containerLootPool = this.getPossibleLootItemsForContainer(containerTpl, staticLootDist);
// 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
// Money spawn more than once in container
@ -341,7 +408,11 @@ export class LocationGenerator
const locklist = [Money.ROUBLES, Money.DOLLARS, Money.EUROS];
// 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
const tplsToAddToContainer = tplsForced.concat(chosenTpls);
@ -368,11 +439,18 @@ export class LocationGenerator
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;
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
for (const item of items)
@ -409,15 +487,19 @@ export class LocationGenerator
* @param locationName Map name (to get per-map multiplier for from config)
* @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);
for (const itemCountDistribution of staticLootDist[containerTypeId].itemcountDistribution)
{
// Add each count of items into array
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
* 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
* @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 seasonalItemTplBlacklist = this.seasonalEventService.getInactiveSeasonalEventItems();
@ -446,7 +531,7 @@ export class LocationGenerator
}
itemDistribution.push(
new ProbabilityObject(icd.tpl, icd.relativeProbability)
new ProbabilityObject(icd.tpl, icd.relativeProbability),
);
}
@ -465,12 +550,16 @@ export class LocationGenerator
/**
* Create array of loose + forced loot using probability system
* @param dynamicLootDist
* @param staticAmmoDist
* @param dynamicLootDist
* @param staticAmmoDist
* @param locationName Location to generate loot for
* @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[] = [];
@ -478,14 +567,14 @@ export class LocationGenerator
this.addForcedLoot(loot, dynamicLootDist.spawnpointsForced, locationName);
const allDynamicSpawnpoints = dynamicLootDist.spawnpoints;
//Draw from random distribution
// Draw from random distribution
const desiredSpawnpointCount = Math.round(
this.getLooseLootMultiplerForLocation(locationName) *
this.randomUtil.randn(
dynamicLootDist.spawnpointCount.mean,
dynamicLootDist.spawnpointCount.std
)
this.randomUtil.randn(
dynamicLootDist.spawnpointCount.mean,
dynamicLootDist.spawnpointCount.std,
),
);
// Positions not in forced but have 100% chance to spawn
@ -496,7 +585,7 @@ export class LocationGenerator
for (const spawnpoint of allDynamicSpawnpoints)
{
// Point is blacklsited, skip
// Point is blacklisted, skip
if (blacklistedSpawnpoints?.includes(spawnpoint.template.Id))
{
this.logger.debug(`Ignoring loose loot location: ${spawnpoint.template.Id}`);
@ -510,7 +599,7 @@ export class LocationGenerator
}
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
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
const tooManySpawnPointsRequested = (desiredSpawnpointCount - chosenSpawnpoints.length) > 0;
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
@ -541,29 +636,37 @@ export class LocationGenerator
{
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;
}
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;
}
const itemArray = new ProbabilityObjectArray<string>(this.mathUtil, this.jsonUtil);
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
continue;
}
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 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];
if (lootToForceSingleAmountOnMap)
@ -596,27 +703,32 @@ export class LocationGenerator
for (const itemTpl of lootToForceSingleAmountOnMap)
{
// 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)
{
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;
}
// 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)
{
// use locationId as template.Id is the same across all items
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
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;
lootItem.Root = this.objectId.generate();
lootItem.Items[0]._id = lootItem.Root;
@ -656,29 +768,36 @@ export class LocationGenerator
* @param staticAmmoDist ammo distributions
* @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;
// Item array to return
const itemWithMods: Item[] = [];
// 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 stackCount = itemTemplate._props.StackMaxSize === 1
? 1
: this.randomUtil.getInt(itemTemplate._props.StackMinRandom, itemTemplate._props.StackMaxRandom);
const stackCount = itemTemplate._props.StackMaxSize === 1 ?
1 :
this.randomUtil.getInt(itemTemplate._props.StackMinRandom, itemTemplate._props.StackMaxRandom);
itemWithMods.push(
{
_id: this.objectId.generate(),
_tpl: chosenTpl,
upd: { StackObjectsCount: stackCount }
}
upd: {StackObjectsCount: stackCount},
},
);
}
else if (this.itemHelper.isOfBaseclass(chosenTpl, BaseClasses.AMMO_BOX))
@ -687,7 +806,7 @@ export class LocationGenerator
const ammoBoxTemplate = this.itemHelper.getItem(chosenTpl)[1];
const ammoBoxItem: Item[] = [{
_id: this.objectId.generate(),
_tpl: chosenTpl
_tpl: chosenTpl,
}];
this.itemHelper.addCartridgesToAmmoBox(ammoBoxItem, ammoBoxTemplate);
itemWithMods.push(...ammoBoxItem);
@ -698,15 +817,24 @@ export class LocationGenerator
const magazineTemplate = this.itemHelper.getItem(chosenTpl)[1];
const magazineItem: Item[] = [{
_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);
}
else
{
// 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
this.reparentItemAndChildren(itemWithChildren);
@ -720,7 +848,7 @@ export class LocationGenerator
return {
items: itemWithMods,
width: size.width,
height: size.height
height: size.height,
};
}
@ -757,14 +885,18 @@ export class LocationGenerator
{
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
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];
let width = itemTemplate._props.Width;
@ -772,8 +904,8 @@ export class LocationGenerator
let items: Item[] = [
{
_id: this.objectId.generate(),
_tpl: tpl
}
_tpl: tpl,
},
];
// Use passed in parentId as override for new item
@ -782,13 +914,16 @@ export class LocationGenerator
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
const stackCount = itemTemplate._props.StackMaxSize === 1
? 1
: this.randomUtil.getInt(itemTemplate._props.StackMinRandom, itemTemplate._props.StackMaxRandom);
items[0].upd = { StackObjectsCount: stackCount };
const stackCount = itemTemplate._props.StackMaxSize === 1 ?
1 :
this.randomUtil.getInt(itemTemplate._props.StackMinRandom, itemTemplate._props.StackMaxRandom);
items[0].upd = {StackObjectsCount: stackCount};
}
// No spawn point, use default template
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
// 5ea03f7400685063ec28bfa8 // ppsh default
// 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;
}
}
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}`);
}
const rootItem = items[0];
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"));
}
@ -830,21 +974,25 @@ export class LocationGenerator
if (children?.length > 0)
{
items = this.ragfairServerHelper.reparentPresets(rootItem, children);
}
}
}
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;
}
// 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.)
// General: Make a WeaponController for Ragfair preset stuff and the generating weapons and ammo stuff from
// 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
if (magazine)
{
@ -853,7 +1001,12 @@ export class LocationGenerator
// Create array with just 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
items.splice(items.indexOf(magazine), 1, ...magazineWithCartridges);
@ -872,7 +1025,13 @@ export class LocationGenerator
{
// Create array with just magazine
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
items.splice(items.indexOf(items[0]), 1, ...magazineWithCartridges);
@ -881,7 +1040,7 @@ export class LocationGenerator
return {
items: items,
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";
type ItemLimit = {
current: number,
max: number
current: number;
max: number;
};
@injectable()
@ -38,7 +38,7 @@ export class LootGenerator
@inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper,
@inject("LocalisationService") protected localisationService: LocalisationService,
@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 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)
{
for (const bossItem of this.itemFilterService.getBossItems())
@ -64,12 +67,17 @@ export class LootGenerator
}
// 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)
{
// 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++)
{
// Choose one at random + add to results array
@ -78,16 +86,18 @@ export class LootGenerator
id: this.hashUtil.generate(),
tpl: chosenSealedContainer._id,
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
const items = Object.entries(tables.templates.items).filter(x => !itemBlacklist.has(x[1]._id)
&& x[1]._type.toLowerCase() === "item"
&& !x[1]._props.QuestItem
&& options.itemTypeWhitelist.includes(x[1]._parent));
const items = Object.entries(tables.templates.items).filter((x) =>
!itemBlacklist.has(x[1]._id) &&
x[1]._type.toLowerCase() === "item" &&
!x[1]._props.QuestItem &&
options.itemTypeWhitelist.includes(x[1]._parent)
);
const randomisedItemCount = this.randomUtil.getInt(options.itemCount.min, options.itemCount.max);
for (let index = 0; index < randomisedItemCount; index++)
@ -95,10 +105,12 @@ export class LootGenerator
if (!this.findAndAddRandomItemToLoot(items, itemTypeCounts, options, result))
{
index--;
}
}
}
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 itemBlacklistArray = Array.from(itemBlacklist);
for (let index = 0; index < randomisedPresetCount; index++)
@ -124,7 +136,7 @@ export class LootGenerator
{
itemTypeCounts[itemTypeId] = {
current: 0,
max: limits[itemTypeId]
max: limits[itemTypeId],
};
}
@ -141,9 +153,10 @@ export class LootGenerator
*/
protected findAndAddRandomItemToLoot(
items: [string, ITemplateItem][],
itemTypeCounts: Record<string, { current: number; max: number; }>,
itemTypeCounts: Record<string, {current: number; max: number;}>,
options: LootRequest,
result: LootItem[]): boolean
result: LootItem[],
): boolean
{
const randomItem = this.randomUtil.getArrayValue(items)[1];
@ -157,16 +170,18 @@ export class LootGenerator
id: this.hashUtil.generate(),
tpl: randomItem._id,
isPreset: false,
stackCount: 1
stackCount: 1,
};
// Check if armor has level in allowed whitelist
if (randomItem._parent === BaseClasses.ARMOR
|| randomItem._parent === BaseClasses.VEST)
if (
randomItem._parent === BaseClasses.ARMOR ||
randomItem._parent === BaseClasses.VEST
)
{
if (!options.armorLevelWhitelist.includes(Number(randomItem._props.armorClass)))
{
return false;
return false;
}
}
@ -175,7 +190,7 @@ export class LootGenerator
{
newLootItem.stackCount = this.getRandomisedStackCount(randomItem, options);
}
newLootItem.tpl = randomItem._id;
result.push(newLootItem);
@ -219,9 +234,10 @@ export class LootGenerator
*/
protected findAndAddRandomPresetToLoot(
globalDefaultPresets: [string, IPreset][],
itemTypeCounts: Record<string, { current: number; max: number; }>,
itemTypeCounts: Record<string, {current: number; max: number;}>,
itemBlacklist: string[],
result: LootItem[]): boolean
result: LootItem[],
): boolean
{
// Choose random preset and get details from item.json using encyclopedia value (encyclopedia === tplId)
const randomPreset = this.randomUtil.getArrayValue(globalDefaultPresets)[1];
@ -264,9 +280,9 @@ export class LootGenerator
const newLootItem: LootItem = {
tpl: randomPreset._items[0]._tpl,
isPreset: true,
stackCount: 1
stackCount: 1,
};
result.push(newLootItem);
if (itemLimitCount)
@ -274,7 +290,7 @@ export class LootGenerator
// increment item count as its in limit array
itemLimitCount.current++;
}
// item added okay
return true;
}
@ -289,19 +305,23 @@ export class LootGenerator
const itemsToReturn: AddItem[] = [];
// 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);
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;
}
// Get weapon preset - default or choose a random one from all possible
let chosenWeaponPreset = containerSettings.defaultPresetsOnly
? this.presetHelper.getDefaultPreset(chosenWeaponTpl)
: this.randomUtil.getArrayValue(this.presetHelper.getPresets(chosenWeaponTpl));
let chosenWeaponPreset = containerSettings.defaultPresetsOnly ?
this.presetHelper.getDefaultPreset(chosenWeaponTpl) :
this.randomUtil.getArrayValue(this.presetHelper.getPresets(chosenWeaponTpl));
if (!chosenWeaponPreset)
{
@ -314,12 +334,14 @@ export class LootGenerator
count: 1,
// eslint-disable-next-line @typescript-eslint/naming-convention
item_id: chosenWeaponPreset._id,
isPreset: true
isPreset: true,
});
// Get items related to chosen weapon
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
itemsToReturn.push(...this.getSealedContainerNonWeaponModRewards(containerSettings, weaponDetailsDb[1]));
@ -333,7 +355,10 @@ export class LootGenerator
* @param weaponDetailsDb Details for the weapon to reward player
* @returns AddItem array
*/
protected getSealedContainerNonWeaponModRewards(containerSettings: ISealedAirdropContainerSettings, weaponDetailsDb: ITemplateItem): AddItem[]
protected getSealedContainerNonWeaponModRewards(
containerSettings: ISealedAirdropContainerSettings,
weaponDetailsDb: ITemplateItem,
): AddItem[]
{
const rewards: AddItem[] = [];
@ -351,15 +376,15 @@ export class LootGenerator
if (rewardTypeId === BaseClasses.AMMO_BOX)
{
// Get ammoboxes from db
const ammoBoxesDetails = containerSettings.ammoBoxWhitelist.map(x =>
const ammoBoxesDetails = containerSettings.ammoBoxWhitelist.map((x) =>
{
const itemDetails = this.itemHelper.getItem(x);
return itemDetails[1];
});
// Need to find boxes that matches weapons caliber
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)
{
this.logger.debug(`No ammo box with caliber ${weaponCaliber} found, skipping`);
@ -373,7 +398,7 @@ export class LootGenerator
count: rewardCount,
// eslint-disable-next-line @typescript-eslint/naming-convention
item_id: chosenAmmoBox._id,
isPreset: false
isPreset: false,
});
continue;
@ -381,11 +406,13 @@ export class LootGenerator
// Get all items of the desired type + not quest items + not globally blacklisted
const rewardItemPool = Object.values(this.databaseServer.getTables().templates.items)
.filter(x => x._parent === rewardTypeId
&& x._type.toLowerCase() === "item"
&& !this.itemFilterService.isItemBlacklisted(x._id)
&& (!containerSettings.allowBossItems && !this.itemFilterService.isBossItem(x._id))
&& !x._props.QuestItem);
.filter((x) =>
x._parent === rewardTypeId &&
x._type.toLowerCase() === "item" &&
!this.itemFilterService.isItemBlacklisted(x._id) &&
(!containerSettings.allowBossItems && !this.itemFilterService.isBossItem(x._id)) &&
!x._props.QuestItem
);
if (rewardItemPool.length === 0)
{
@ -398,7 +425,7 @@ export class LootGenerator
{
// choose a random item from pool
const chosenRewardItem = this.randomUtil.getArrayValue(rewardItemPool);
this.addOrIncrementItemToArray(chosenRewardItem._id, rewards);
this.addOrIncrementItemToArray(chosenRewardItem._id, rewards);
}
}
@ -412,7 +439,11 @@ export class LootGenerator
* @param chosenWeaponPreset The weapon preset given to player as reward
* @returns AddItem array
*/
protected getSealedContainerWeaponModRewards(containerSettings: ISealedAirdropContainerSettings, linkedItemsToWeapon: ITemplateItem[], chosenWeaponPreset: IPreset): AddItem[]
protected getSealedContainerWeaponModRewards(
containerSettings: ISealedAirdropContainerSettings,
linkedItemsToWeapon: ITemplateItem[],
chosenWeaponPreset: IPreset,
): AddItem[]
{
const modRewards: AddItem[] = [];
for (const rewardTypeId in containerSettings.weaponModRewardLimits)
@ -426,16 +457,20 @@ export class LootGenerator
continue;
}
// Get items that fulfil reward type critera from items that fit on gun
const relatedItems = linkedItemsToWeapon.filter(x => x._parent === rewardTypeId && !this.itemFilterService.isItemBlacklisted(x._id));
// 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)
);
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;
}
// Find a random item of the desired type and add as reward
for (let index = 0; index < rewardCount; index++)
for (let index = 0; index < rewardCount; index++)
{
const chosenItem = this.randomUtil.drawRandomFromList(relatedItems);
this.addOrIncrementItemToArray(chosenItem[0]._id, modRewards);
@ -447,7 +482,7 @@ export class LootGenerator
/**
* Handle event-related loot containers - currently just the halloween jack-o-lanterns that give food rewards
* @param rewardContainerDetails
* @param rewardContainerDetails
* @returns AddItem array
*/
public getRandomLootContainerLoot(rewardContainerDetails: RewardDetails): AddItem[]
@ -458,7 +493,9 @@ export class LootGenerator
for (let index = 0; index < rewardContainerDetails.rewardCount; index++)
{
// 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);
}
@ -473,7 +510,7 @@ export class LootGenerator
*/
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)
{
// Exists in array already, increment count
@ -485,4 +522,4 @@ export class LootGenerator
resultsArray.push({item_id: itemTplToAdd, count: 1, isPreset: false});
}
}
}
}

View File

@ -10,11 +10,10 @@ import { ItemFilterService } from "@spt-aki/services/ItemFilterService";
import { SeasonalEventService } from "@spt-aki/services/SeasonalEventService";
/**
* Handle the generation of dynamic PMC loot in pockets and backpacks
* Handle the generation of dynamic PMC loot in pockets and backpacks
* and the removal of blacklisted items
*/
@injectable()
export class PMCLootGenerator
{
protected pocketLootPool: string[] = [];
@ -27,7 +26,7 @@ export class PMCLootGenerator
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("ItemFilterService") protected itemFilterService: ItemFilterService,
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService,
)
{
this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC);
@ -47,7 +46,7 @@ export class PMCLootGenerator
const allowedItemTypes = this.pmcConfig.pocketLoot.whitelist;
const pmcItemBlacklist = this.pmcConfig.pocketLoot.blacklist;
const itemBlacklist = this.itemFilterService.getBlacklistedItems();
// Blacklist seasonal items if not inside seasonal event
// Blacklist seasonal items if not inside seasonal event
if (!this.seasonalEventService.seasonalEventEnabled())
@ -56,14 +55,16 @@ export class PMCLootGenerator
itemBlacklist.push(...this.seasonalEventService.getInactiveSeasonalEventItems());
}
const itemsToAdd = Object.values(items).filter(item => allowedItemTypes.includes(item._parent)
&& this.itemHelper.isValidItem(item._id)
&& !pmcItemBlacklist.includes(item._id)
&& !itemBlacklist.includes(item._id)
&& item._props.Width === 1
&& item._props.Height === 1);
const itemsToAdd = Object.values(items).filter((item) =>
allowedItemTypes.includes(item._parent) &&
this.itemHelper.isValidItem(item._id) &&
!pmcItemBlacklist.includes(item._id) &&
!itemBlacklist.includes(item._id) &&
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;
@ -83,7 +84,7 @@ export class PMCLootGenerator
const allowedItemTypes = this.pmcConfig.vestLoot.whitelist;
const pmcItemBlacklist = this.pmcConfig.vestLoot.blacklist;
const itemBlacklist = this.itemFilterService.getBlacklistedItems();
// Blacklist seasonal items if not inside seasonal event
// Blacklist seasonal items if not inside seasonal event
if (!this.seasonalEventService.seasonalEventEnabled())
@ -92,13 +93,15 @@ export class PMCLootGenerator
itemBlacklist.push(...this.seasonalEventService.getInactiveSeasonalEventItems());
}
const itemsToAdd = Object.values(items).filter(item => allowedItemTypes.includes(item._parent)
&& this.itemHelper.isValidItem(item._id)
&& !pmcItemBlacklist.includes(item._id)
&& !itemBlacklist.includes(item._id)
&& this.itemFitsInto2By2Slot(item));
const itemsToAdd = Object.values(items).filter((item) =>
allowedItemTypes.includes(item._parent) &&
this.itemHelper.isValidItem(item._id) &&
!pmcItemBlacklist.includes(item._id) &&
!itemBlacklist.includes(item._id) &&
this.itemFitsInto2By2Slot(item)
);
this.vestLootPool = itemsToAdd.map(x => x._id);
this.vestLootPool = itemsToAdd.map((x) => x._id);
}
return this.vestLootPool;
@ -129,7 +132,7 @@ export class PMCLootGenerator
const allowedItemTypes = this.pmcConfig.backpackLoot.whitelist;
const pmcItemBlacklist = this.pmcConfig.backpackLoot.blacklist;
const itemBlacklist = this.itemFilterService.getBlacklistedItems();
// blacklist event items if not inside seasonal event
if (!this.seasonalEventService.seasonalEventEnabled())
{
@ -137,14 +140,16 @@ export class PMCLootGenerator
itemBlacklist.push(...this.seasonalEventService.getInactiveSeasonalEventItems());
}
const itemsToAdd = Object.values(items).filter(item => allowedItemTypes.includes(item._parent)
&& this.itemHelper.isValidItem(item._id)
&& !pmcItemBlacklist.includes(item._id)
&& !itemBlacklist.includes(item._id));
const itemsToAdd = Object.values(items).filter((item) =>
allowedItemTypes.includes(item._parent) &&
this.itemHelper.isValidItem(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;
}
}
}

View File

@ -47,7 +47,7 @@ export class PlayerScavGenerator
@inject("BotLootCacheService") protected botLootCacheService: BotLootCacheService,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("BotGenerator") protected botGenerator: BotGenerator,
@inject("ConfigServer") protected configServer: ConfigServer
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
this.playerScavConfig = this.configServer.getConfig(ConfigTypes.PLAYERSCAV);
@ -66,9 +66,9 @@ export class PlayerScavGenerator
const existingScavData = this.jsonUtil.clone(profile.characters.scav);
// scav profile can be empty on first profile creation
const scavKarmaLevel = ((Object.keys(existingScavData).length === 0))
? 0
: this.getScavKarmaLevel(pmcData);
const scavKarmaLevel = (Object.keys(existingScavData).length === 0) ?
0 :
this.getScavKarmaLevel(pmcData);
// use karma level to get correct karmaSettings
const playerScavKarmaSettings = this.playerScavConfig.karmaLevel[scavKarmaLevel];
@ -83,7 +83,12 @@ export class PlayerScavGenerator
const baseBotNode: IBotType = this.constructBotBaseTemplate(playerScavKarmaSettings.botTypeForLoot);
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
this.botLootCacheService.clearCache();
@ -113,7 +118,6 @@ export class PlayerScavGenerator
scavData.Notes = existingScavData.Notes ?? {Notes: []};
scavData.WishList = existingScavData.WishList ?? [];
// Add an extra labs card to pscav backpack based on config chance
if (this.randomUtil.getChance100(playerScavKarmaSettings.labsAccessCardChancePercent))
{
@ -121,9 +125,15 @@ export class PlayerScavGenerator
const itemsToAdd: Item[] = [{
_id: this.hashUtil.generate(),
_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
@ -251,7 +261,7 @@ export class PlayerScavGenerator
return {
Common: [],
Mastering: [],
Points: 0
Points: 0,
};
}
@ -292,7 +302,7 @@ export class PlayerScavGenerator
* take into account scav cooldown bonus
* @param scavData scav profile
* @param pmcData pmc profile
* @returns
* @returns
*/
protected setScavCooldownTimer(scavData: IPmcData, pmcData: IPmcData): IPmcData
{
@ -314,7 +324,7 @@ export class PlayerScavGenerator
const fenceInfo = this.fenceService.getFenceInfo(pmcData);
modifier *= fenceInfo.SavageCooldownModifier;
scavLockDuration *= modifier;
const fullProfile = this.profileHelper.getFullProfile(pmcData?.sessionId);
if (fullProfile?.info?.edition?.toLowerCase?.().startsWith?.(AccountTypes.SPT_DEVELOPER))
{
@ -323,7 +333,7 @@ export class PlayerScavGenerator
}
scavData.Info.SavageLockTime = (Date.now() / 1000) + scavLockDuration;
return scavData;
}
}
}

View File

@ -24,7 +24,7 @@ export class RagfairAssortGenerator
@inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService,
@inject("ConfigServer") protected configServer: ConfigServer
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR);
@ -62,9 +62,9 @@ export class RagfairAssortGenerator
const results: Item[] = [];
const items = this.itemHelper.getItems();
const weaponPresets = (this.ragfairConfig.dynamic.showDefaultPresetsOnly)
? this.getDefaultPresets()
: this.getPresets();
const weaponPresets = (this.ragfairConfig.dynamic.showDefaultPresetsOnly) ?
this.getDefaultPresets() :
this.getPresets();
const ragfairItemInvalidBaseTypes: string[] = [
BaseClasses.LOOT_CONTAINER, // safe, barrel cache etc
@ -72,7 +72,7 @@ export class RagfairAssortGenerator
BaseClasses.SORTING_TABLE,
BaseClasses.INVENTORY,
BaseClasses.STATIONARY_CONTAINER,
BaseClasses.POCKETS
BaseClasses.POCKETS,
];
const seasonalEventActive = this.seasonalEventService.seasonalEventEnabled();
@ -84,7 +84,10 @@ export class RagfairAssortGenerator
continue;
}
if (this.ragfairConfig.dynamic.removeSeasonalItemsWhenNotInEvent && !seasonalEventActive && seasonalItemTplBlacklist.includes(item._id))
if (
this.ragfairConfig.dynamic.removeSeasonalItemsWhenNotInEvent && !seasonalEventActive &&
seasonalItemTplBlacklist.includes(item._id)
)
{
continue;
}
@ -99,7 +102,7 @@ export class RagfairAssortGenerator
return results;
}
/**
* Get presets from globals.json
* @returns Preset object array
@ -116,9 +119,9 @@ export class RagfairAssortGenerator
*/
protected getDefaultPresets(): IPreset[]
{
return this.getPresets().filter(x => x._encyclopedia);
return this.getPresets().filter((x) => x._encyclopedia);
}
/**
* Create a base assort item and return it with populated values + 999999 stack count + unlimited count = true
* @param tplId tplid to add to item
@ -134,8 +137,8 @@ export class RagfairAssortGenerator
slotId: "hideout",
upd: {
StackObjectsCount: 99999999,
UnlimitedCount: true
}
UnlimitedCount: true,
},
};
}
}
}

View File

@ -33,7 +33,7 @@ import { TimeUtil } from "@spt-aki/utils/TimeUtil";
export class RagfairOfferGenerator
{
protected ragfairConfig: IRagfairConfig;
protected allowedFleaPriceItemsForBarter: { tpl: string; price: number; }[];
protected allowedFleaPriceItemsForBarter: {tpl: string; price: number;}[];
constructor(
@inject("WinstonLogger") protected logger: ILogger,
@ -54,7 +54,7 @@ export class RagfairOfferGenerator
@inject("RagfairCategoriesService") protected ragfairCategoriesService: RagfairCategoriesService,
@inject("FenceService") protected fenceService: FenceService,
@inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("ConfigServer") protected configServer: ConfigServer
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR);
@ -70,7 +70,14 @@ export class RagfairOfferGenerator
* @param sellInOnePiece Flags sellInOnePiece to be true
* @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);
this.ragfairOfferService.addOffer(offer);
@ -88,7 +95,14 @@ export class RagfairOfferGenerator
* @param sellInOnePiece Set StackObjectsCount to 1
* @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);
@ -98,13 +112,13 @@ export class RagfairOfferGenerator
const requirement: OfferRequirement = {
_tpl: barter._tpl,
count: +barter.count.toFixed(2),
onlyFunctional: barter.onlyFunctional ?? false
onlyFunctional: barter.onlyFunctional ?? false,
};
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 offer: IRagfairOffer = {
@ -112,13 +126,13 @@ export class RagfairOfferGenerator
intId: 0,
user: {
id: this.getTraderId(userID),
memberType: (userID === "ragfair")
? MemberCategory.DEFAULT
: this.ragfairServerHelper.getMemberType(userID),
memberType: (userID === "ragfair") ?
MemberCategory.DEFAULT :
this.ragfairServerHelper.getMemberType(userID),
nickname: this.ragfairServerHelper.getNickname(userID),
rating: this.getRating(userID),
isRatingGrowing: this.getRatingGrowing(userID),
avatar: this.getAvatarUrl(isTrader, userID)
avatar: this.getAvatarUrl(isTrader, userID),
},
root: items[0]._id,
items: this.jsonUtil.clone(items),
@ -134,7 +148,7 @@ export class RagfairOfferGenerator
locked: false,
unlimitedCount: false,
notAvailable: false,
CurrentItemCount: itemCount
CurrentItemCount: itemCount,
};
return offer;
@ -150,9 +164,9 @@ export class RagfairOfferGenerator
let roublePrice = 0;
for (const requirement of offerRequirements)
{
roublePrice += this.paymentHelper.isMoneyTpl(requirement._tpl)
? Math.round(this.calculateRoublePrice(requirement.count, requirement._tpl))
: this.ragfairPriceService.getFleaPriceForItem(requirement._tpl) * requirement.count; // get flea price for barter offer items
roublePrice += this.paymentHelper.isMoneyTpl(requirement._tpl) ?
Math.round(this.calculateRoublePrice(requirement.count, requirement._tpl)) :
this.ragfairPriceService.getFleaPriceForItem(requirement._tpl) * requirement.count; // get flea price for barter offer items
}
return roublePrice;
@ -249,7 +263,7 @@ export class RagfairOfferGenerator
return true;
}
// generated offer
// generated offer
// 50/50 growing/falling
return this.randomUtil.getBool();
}
@ -275,7 +289,13 @@ export class RagfairOfferGenerator
}
// 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;
// get assort items from param if they exist, otherwise grab freshly generated assorts
const assortItemsToProcess: Item[] = (expiredOffers)
? expiredOffers
: this.ragfairAssortGenerator.getAssortItems();
const assortItemsToProcess: Item[] = expiredOffers ?
expiredOffers :
this.ragfairAssortGenerator.getAssortItems();
// Store all functions to create an offer for every item and pass into Promise.all to run async
const assorOffersForItemsProcesses = [];
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);
}
/**
*
* @param assortItemIndex Index of assort item
* @param assortItemsToProcess Item array containing index
* @param expiredOffers Currently expired offers on flea
* @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 itemDetails = this.itemHelper.getItem(assortItem._tpl);
@ -322,15 +348,21 @@ export class RagfairOfferGenerator
}
// Get item + sub-items if preset, otherwise just get item
const items: Item[] = (isPreset)
? this.ragfairServerHelper.getPresetItems(assortItem)
: [...[assortItem], ...this.itemHelper.findAndReturnChildrenByAssort(assortItem._id, this.ragfairAssortGenerator.getAssortItems())];
const items: Item[] = isPreset ?
this.ragfairServerHelper.getPresetItems(assortItem) :
[
...[assortItem],
...this.itemHelper.findAndReturnChildrenByAssort(
assortItem._id,
this.ragfairAssortGenerator.getAssortItems(),
),
];
// Get number of offers to create
// Limit to 1 offer when processing expired
const offerCount = (expiredOffers)
? 1
: Math.round(this.randomUtil.getInt(config.offerItemCount.min, config.offerItemCount.max));
const offerCount = expiredOffers ?
1 :
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
const assortSingleOfferProcesses = [];
@ -342,7 +374,6 @@ export class RagfairOfferGenerator
await Promise.all(assortSingleOfferProcesses);
}
/**
* Create one flea offer for a specific item
* @param items Item to create offer for
@ -350,23 +381,30 @@ export class RagfairOfferGenerator
* @param itemDetails raw db item details
* @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
items[0].upd.StackObjectsCount = this.ragfairServerHelper.calculateDynamicStackCount(items[0]._tpl, isPreset);
const isBarterOffer = this.randomUtil.getChance100(this.ragfairConfig.dynamic.barter.chancePercent);
const isPackOffer = this.randomUtil.getChance100(this.ragfairConfig.dynamic.pack.chancePercent)
&& !isBarterOffer
&& items.length === 1
&& this.itemHelper.isOfBaseclasses(items[0]._tpl, this.ragfairConfig.dynamic.pack.itemTypeWhitelist);
const isPackOffer = this.randomUtil.getChance100(this.ragfairConfig.dynamic.pack.chancePercent) &&
!isBarterOffer &&
items.length === 1 &&
this.itemHelper.isOfBaseclasses(items[0]._tpl, this.ragfairConfig.dynamic.pack.itemTypeWhitelist);
const randomUserId = this.hashUtil.generate();
let barterScheme: IBarterScheme[];
if (isPackOffer)
{
// 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;
// Don't randomise pack items
@ -391,7 +429,8 @@ export class RagfairOfferGenerator
items,
barterScheme,
1,
isPreset || isPackOffer); // sellAsOnePiece
isPreset || isPackOffer,
); // sellAsOnePiece
this.ragfairCategoriesService.incrementCategory(offer);
}
@ -413,7 +452,12 @@ export class RagfairOfferGenerator
// Trader assorts / assort items are missing
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;
}
@ -444,14 +488,20 @@ export class RagfairOfferGenerator
}
const isPreset = this.presetHelper.isPreset(item._id);
const items: Item[] = (isPreset)
? this.ragfairServerHelper.getPresetItems(item)
: [...[item], ...this.itemHelper.findAndReturnChildrenByAssort(item._id, assorts.items)];
const items: Item[] = isPreset ?
this.ragfairServerHelper.getPresetItems(item) :
[...[item], ...this.itemHelper.findAndReturnChildrenByAssort(item._id, assorts.items)];
const barterScheme = assorts.barter_scheme[item._id];
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;
}
@ -473,11 +523,11 @@ export class RagfairOfferGenerator
* @param userID id of owner of item
* @param itemWithMods Item and mods, get condition of first item (only first array item is used)
* @param itemDetails db details of first item
* @returns
* @returns
*/
protected randomiseItemUpdProperties(userID: string, itemWithMods: Item[], itemDetails: ITemplateItem): Item[]
{
// Add any missing properties to first item in array
// Add any missing properties to first item in array
itemWithMods[0] = this.addMissingConditions(itemWithMods[0]);
if (!(this.ragfairServerHelper.isPlayer(userID) || this.ragfairServerHelper.isTrader(userID)))
@ -508,9 +558,9 @@ export class RagfairOfferGenerator
{
// Get keys from condition config dictionary
const configConditions = Object.keys(this.ragfairConfig.dynamic.condition);
for (const baseClass of configConditions)
for (const baseClass of configConditions)
{
if (this.itemHelper.isOfBaseclass(tpl, baseClass))
if (this.itemHelper.isOfBaseclass(tpl, baseClass))
{
return baseClass;
}
@ -527,7 +577,10 @@ export class RagfairOfferGenerator
*/
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
if (item.upd.Repairable)
@ -571,7 +624,7 @@ export class RagfairOfferGenerator
return;
}
if (item.upd.RepairKit)
if (item.upd.RepairKit)
{
// randomize repair kit (armor/weapon) uses
item.upd.RepairKit.Resource = Math.round(itemDetails._props.MaxRepairResource * multiplier) || 1;
@ -585,7 +638,7 @@ export class RagfairOfferGenerator
const remainingFuel = Math.round(totalCapacity * multiplier);
item.upd.Resource = {
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;
// 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
if (tempMaxDurability >= item.upd.Repairable.MaxDurability)
@ -626,45 +681,45 @@ export class RagfairOfferGenerator
protected addMissingConditions(item: Item): Item
{
const props = this.itemHelper.getItem(item._tpl)[1]._props;
const isRepairable = ("Durability" in props);
const isMedkit = ("MaxHpResource" in props);
const isKey = ("MaximumNumberOfUsage" in props);
const isConsumable = (props.MaxResource > 1 && "foodUseTime" in props);
const isRepairKit = ("MaxRepairResource" in props);
const isRepairable = "Durability" in props;
const isMedkit = "MaxHpResource" in props;
const isKey = "MaximumNumberOfUsage" in props;
const isConsumable = props.MaxResource > 1 && "foodUseTime" in props;
const isRepairKit = "MaxRepairResource" in props;
if (isRepairable && props.Durability > 0)
{
item.upd.Repairable = {
Durability: props.Durability,
MaxDurability: props.Durability
MaxDurability: props.Durability,
};
}
if (isMedkit && props.MaxHpResource > 0)
{
item.upd.MedKit = {
HpResource: props.MaxHpResource
HpResource: props.MaxHpResource,
};
}
if (isKey)
if (isKey)
{
item.upd.Key = {
NumberOfUsages: 0
NumberOfUsages: 0,
};
}
if (isConsumable)
if (isConsumable)
{
item.upd.FoodDrink = {
HpPercent: props.MaxResource
HpPercent: props.MaxResource,
};
}
if (isRepairKit)
if (isRepairKit)
{
item.upd.RepairKit = {
Resource: props.MaxRepairResource
Resource: props.MaxRepairResource,
};
}
@ -679,7 +734,11 @@ export class RagfairOfferGenerator
protected createBarterBarterScheme(offerItems: Item[]): IBarterScheme[]
{
// 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
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
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
const desiredItemCost = Math.round(priceOfItemOffer / barterItemCount);
@ -699,7 +761,10 @@ export class RagfairOfferGenerator
const fleaPrices = this.getFleaPricesAsArray();
// 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
if (filtered.length === 0)
@ -713,8 +778,8 @@ export class RagfairOfferGenerator
return [
{
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`
* @returns array with tpl/price values
*/
protected getFleaPricesAsArray(): { tpl: string; price: number; }[]
protected getFleaPricesAsArray(): {tpl: string; price: number;}[]
{
// Generate if needed
if (!this.allowedFleaPriceItemsForBarter)
{
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
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;
@ -749,13 +816,14 @@ export class RagfairOfferGenerator
protected createCurrencyBarterScheme(offerItems: Item[], isPackOffer: boolean, multipler = 1): IBarterScheme[]
{
const currency = this.ragfairServerHelper.getDynamicOfferCurrency();
const price = this.ragfairPriceService.getDynamicOfferPriceForOffer(offerItems, currency, isPackOffer) * multipler;
const price = this.ragfairPriceService.getDynamicOfferPriceForOffer(offerItems, currency, isPackOffer) *
multipler;
return [
{
count: price,
_tpl: currency
}
_tpl: currency,
},
];
}
}
}

View File

@ -15,16 +15,25 @@ import {
IEliminationCondition,
IEquipmentConditionProps,
IExploration,
IExplorationCondition, IKillConditionProps,
IExplorationCondition,
IKillConditionProps,
IPickup,
IRepeatableQuest, IReward, IRewards
IRepeatableQuest,
IReward,
IRewards,
} from "@spt-aki/models/eft/common/tables/IRepeatableQuests";
import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem";
import { BaseClasses } from "@spt-aki/models/enums/BaseClasses";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { Money } from "@spt-aki/models/enums/Money";
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 { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { EventOutputHolder } from "@spt-aki/routers/EventOutputHolder";
@ -66,7 +75,7 @@ export class RepeatableQuestGenerator
@inject("ObjectId") protected objectId: ObjectId,
@inject("ItemFilterService") protected itemFilterService: ItemFilterService,
@inject("RepeatableQuestHelper") protected repeatableQuestHelper: RepeatableQuestHelper,
@inject("ConfigServer") protected configServer: ConfigServer
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
this.questConfig = this.configServer.getConfig(ConfigTypes.QUEST);
@ -85,15 +94,17 @@ export class RepeatableQuestGenerator
pmcLevel: number,
pmcTraderInfo: Record<string, TraderInfo>,
questTypePool: IQuestTypePool,
repeatableConfig: IRepeatableQuestConfig
repeatableConfig: IRepeatableQuestConfig,
): IRepeatableQuest
{
const questType = this.randomUtil.drawRandomFromList<string>(questTypePool.types)[0];
// 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
traders = traders.filter(x => pmcTraderInfo[x].unlocked);
traders = traders.filter((x) => pmcTraderInfo[x].unlocked);
const traderId = this.randomUtil.drawRandomFromList(traders)[0];
switch (questType)
@ -123,15 +134,19 @@ export class RepeatableQuestGenerator
pmcLevel: number,
traderId: string,
questTypePool: IQuestTypePool,
repeatableConfig: IRepeatableQuestConfig
repeatableConfig: IRepeatableQuestConfig,
): IElimination
{
const eliminationConfig = this.repeatableQuestHelper.getEliminationConfigByPmcLevel(pmcLevel, repeatableConfig);
const locationsConfig = repeatableConfig.locations;
let targetsConfig = this.repeatableQuestHelper.probabilityObjectArray(eliminationConfig.targets);
const bodypartsConfig = this.repeatableQuestHelper.probabilityObjectArray(eliminationConfig.bodyParts);
const weaponCategoryRequirementConfig = this.repeatableQuestHelper.probabilityObjectArray(eliminationConfig.weaponCategoryRequirements);
const weaponRequirementConfig = this.repeatableQuestHelper.probabilityObjectArray(eliminationConfig.weaponRequirements);
const weaponCategoryRequirementConfig = this.repeatableQuestHelper.probabilityObjectArray(
eliminationConfig.weaponCategoryRequirements,
);
const weaponRequirementConfig = this.repeatableQuestHelper.probabilityObjectArray(
eliminationConfig.weaponRequirements,
);
// the difficulty of the quest varies in difficulty depending on the condition
// possible conditions are
@ -146,7 +161,7 @@ export class RepeatableQuestGenerator
// Savage: 7,
// AnyPmc: 2,
// bossBully: 0.5
//}
// }
// 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
@ -165,18 +180,26 @@ export class RepeatableQuestGenerator
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;
}
targetsConfig = targetsConfig.filter(x => Object.keys(questTypePool.pool.Elimination.targets).includes(x.key));
if (targetsConfig.length === 0 || targetsConfig.every(x => x.data.isBoss))
targetsConfig = targetsConfig.filter((x) =>
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
// 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
questTypePool.types = questTypePool.types.filter(t => t !== "Elimination");
questTypePool.types = questTypePool.types.filter((t) => t !== "Elimination");
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 also if the random condition is not met in case only "any" was in the pool
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";
delete questTypePool.pool.Elimination.targets[targetKey];
}
else
{
locations = locations.filter(l => l !== "any");
locations = locations.filter((l) => l !== "any");
if (locations.length > 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)
{
delete questTypePool.pool.Elimination.targets[targetKey];
@ -243,15 +271,17 @@ export class RepeatableQuestGenerator
if (targetsConfig.data(targetKey).isBoss)
{
// get all boss spawn information
const bossSpawns = Object.values(this.databaseServer.getTables().locations).filter(x => "base" in x && "Id" in x.base).map(
(x) => ({ Id: x.base.Id, BossSpawn: x.base.BossLocationSpawn })
const bossSpawns = Object.values(this.databaseServer.getTables().locations).filter((x) =>
"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
const thisBossSpawns = bossSpawns.map(
(x) => ({ Id: x.Id, BossSpawn: x.BossSpawn.filter(e => e.BossName === targetKey) })
).filter(x => x.BossSpawn.length > 0);
(x) => ({Id: x.Id, BossSpawn: x.BossSpawn.filter((e) => e.BossName === targetKey)}),
).filter((x) => x.BossSpawn.length > 0);
// 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
isDistanceRequirementAllowed = isDistanceRequirementAllowed && (allowedSpawns.length > 0);
}
@ -259,7 +289,10 @@ export class RepeatableQuestGenerator
if (eliminationConfig.distProb > Math.random() && isDistanceRequirementAllowed)
{
// 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;
distanceDifficulty = maxDistDifficulty * distance / eliminationConfig.maxDist;
}
@ -296,7 +329,7 @@ export class RepeatableQuestGenerator
bodyPartDifficulty / maxBodyPartsDifficulty,
distanceDifficulty / maxDistDifficulty,
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
@ -305,7 +338,7 @@ export class RepeatableQuestGenerator
const difficulty = this.mathUtil.mapToRange(curDifficulty, minDifficulty, maxDifficulty, 0.5, 2);
const quest = this.generateRepeatableTemplate("Elimination", traderId, repeatableConfig.side) as IElimination;
// ASSUMPTION: All fence quests are for scavs
if (traderId === Traders.FENCE)
{
@ -319,14 +352,30 @@ export class RepeatableQuestGenerator
// Only add specific location condition if specific map selected
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.id = this.objectId.generate();
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;
}
@ -338,7 +387,11 @@ export class RepeatableQuestGenerator
* @param eliminationConfig Config
* @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)
{
@ -366,11 +419,11 @@ export class RepeatableQuestGenerator
_props: {
target: location,
id: this.objectId.generate(),
dynamicLocale: true
dynamicLocale: true,
},
_parent: "Location"
_parent: "Location",
};
return propsObject;
}
@ -383,13 +436,19 @@ export class RepeatableQuestGenerator
* @param allowedWeaponCategory What category of weapon must be used - undefined = any
* @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 = {
target: target,
value: 1,
id: this.objectId.generate(),
dynamicLocale: true
dynamicLocale: true,
};
if (target.startsWith("boss"))
@ -409,7 +468,7 @@ export class RepeatableQuestGenerator
{
killConditionProps.distance = {
compareMethod: ">=",
value: distance
value: distance,
};
}
@ -427,7 +486,7 @@ export class RepeatableQuestGenerator
return {
_props: killConditionProps,
_parent: "Kills"
_parent: "Kills",
};
}
@ -442,7 +501,7 @@ export class RepeatableQuestGenerator
protected generateCompletionQuest(
pmcLevel: number,
traderId: string,
repeatableConfig: IRepeatableQuestConfig
repeatableConfig: IRepeatableQuestConfig,
): ICompletion
{
const completionConfig = repeatableConfig.questConfig.Completion;
@ -456,48 +515,64 @@ export class RepeatableQuestGenerator
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"
let itemSelection = this.getRewardableItems(repeatableConfig);
// 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);
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
// [{minPlayerLevel: 1, itemIds: ["id1",...]}, {minPlayerLevel: 15, itemIds: ["id3",...]}]
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
const itemIdsWhitelisted = itemWhitelist.filter(p => p.minPlayerLevel <= pmcLevel).reduce((a, p) => a.concat(p.itemIds), []);
itemSelection = itemSelection.filter(x =>
const itemIdsWhitelisted = itemWhitelist.filter((p) => p.minPlayerLevel <= pmcLevel).reduce(
(a, p) => a.concat(p.itemIds),
[],
);
itemSelection = itemSelection.filter((x) =>
{
// 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
//const flatList = itemSelection.reduce((a, il) => a.concat(il[0]), []);
//const missing = itemIdsWhitelisted.filter(l => !flatList.includes(l));
// const flatList = itemSelection.reduce((a, il) => a.concat(il[0]), []);
// const missing = itemIdsWhitelisted.filter(l => !flatList.includes(l));
}
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
const itemIdsBlacklisted = itemBlacklist.filter(p => p.minPlayerLevel <= pmcLevel).reduce((a, p) => a.concat(p.itemIds), []);
itemSelection = itemSelection.filter(x =>
const itemIdsBlacklisted = itemBlacklist.filter((p) => p.minPlayerLevel <= pmcLevel).reduce(
(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)
{
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;
}
@ -532,7 +607,7 @@ export class RepeatableQuestGenerator
if (roublesBudget > 0)
{
// 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)
{
break;
@ -561,12 +636,18 @@ export class RepeatableQuestGenerator
{
let minDurability = 0;
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;
}
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;
}
@ -583,10 +664,10 @@ export class RepeatableQuestGenerator
minDurability: minDurability,
maxDurability: 100,
dogtagLevel: 0,
onlyFoundInRaid: onlyFoundInRaid
onlyFoundInRaid: onlyFoundInRaid,
},
_parent: "HandoverItem",
dynamicLocale: true
dynamicLocale: true,
};
}
@ -603,7 +684,7 @@ export class RepeatableQuestGenerator
pmcLevel: number,
traderId: string,
questTypePool: IQuestTypePool,
repeatableConfig: IRepeatableQuestConfig
repeatableConfig: IRepeatableQuestConfig,
): IExploration
{
const explorationConfig = repeatableConfig.questConfig.Exploration;
@ -611,7 +692,7 @@ export class RepeatableQuestGenerator
if (Object.keys(questTypePool.pool.Exploration.locations).length === 0)
{
// 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;
}
@ -625,7 +706,7 @@ export class RepeatableQuestGenerator
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 = {
_parent: "ExitStatus",
@ -633,23 +714,23 @@ export class RepeatableQuestGenerator
id: this.objectId.generate(),
dynamicLocale: true,
status: [
"Survived"
]
}
"Survived",
],
},
};
const locationCondition: IExplorationCondition = {
_parent: "Location",
_props: {
id: this.objectId.generate(),
dynamicLocale: true,
target: locationTarget
}
target: locationTarget,
},
};
quest.conditions.AvailableForFinish[0]._props.counter.id = this.objectId.generate();
quest.conditions.AvailableForFinish[0]._props.counter.conditions = [
exitStatusCondition,
locationCondition
locationCondition,
];
quest.conditions.AvailableForFinish[0]._props.value = numExtracts;
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)
// 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(
x => (!("PassageRequirement" in x)
|| repeatableConfig.questConfig.Exploration.specificExits.passageRequirementWhitelist.includes(x.PassageRequirement))
&& x.Chance > 0
(x) =>
(!("PassageRequirement" in x) ||
repeatableConfig.questConfig.Exploration.specificExits.passageRequirementWhitelist.includes(
x.PassageRequirement,
)) &&
x.Chance > 0,
);
const exit = this.randomUtil.drawRandomFromList(possibleExists, 1)[0];
const exitCondition = this.generateExplorationExitCondition(exit);
@ -682,7 +767,7 @@ export class RepeatableQuestGenerator
pmcLevel: number,
traderId: string,
questTypePool: IQuestTypePool,
repeatableConfig: IRepeatableQuestConfig
repeatableConfig: IRepeatableQuestConfig,
): IPickup
{
const pickupConfig = repeatableConfig.questConfig.Pickup;
@ -690,21 +775,28 @@ export class RepeatableQuestGenerator
const quest = this.generateRepeatableTemplate("Pickup", traderId, repeatableConfig.side) as IPickup;
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'
//const locationKey: string = this.randomUtil.drawRandomFromDict(questTypePool.pool.Pickup.locations)[0];
//const locationTarget = questTypePool.pool.Pickup.locations[locationKey];
// const locationKey: string = this.randomUtil.drawRandomFromDict(questTypePool.pool.Pickup.locations)[0];
// 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.value = itemCountToFetch;
const counterCreatorCondition = quest.conditions.AvailableForFinish.find(x => x._parent === "CounterCreator");
//const locationCondition = counterCreatorCondition._props.counter.conditions.find(x => x._parent === "Location");
//(locationCondition._props as ILocationConditionProps).target = [...locationTarget];
const counterCreatorCondition = quest.conditions.AvailableForFinish.find((x) => x._parent === "CounterCreator");
// const locationCondition = counterCreatorCondition._props.counter.conditions.find(x => x._parent === "Location");
// (locationCondition._props as ILocationConditionProps).target = [...locationTarget];
const equipmentCondition = counterCreatorCondition._props.counter.conditions.find(x => x._parent === "Equipment");
(equipmentCondition._props as IEquipmentConditionProps).equipmentInclusive = [[itemTypeToFetchWithCount.itemType]];
const equipmentCondition = counterCreatorCondition._props.counter.conditions.find((x) =>
x._parent === "Equipment"
);
(equipmentCondition._props as IEquipmentConditionProps).equipmentInclusive = [[
itemTypeToFetchWithCount.itemType,
]];
// Add rewards
quest.rewards = this.generateReward(pmcLevel, 1, traderId, repeatableConfig, pickupConfig);
@ -736,8 +828,8 @@ export class RepeatableQuestGenerator
_props: {
exitName: exit.Name,
id: this.objectId.generate(),
dynamicLocale: true
}
dynamicLocale: true,
},
};
}
@ -766,7 +858,7 @@ export class RepeatableQuestGenerator
difficulty: number,
traderId: string,
repeatableConfig: IRepeatableQuestConfig,
questConfig: IBaseQuestConfig
questConfig: IBaseQuestConfig,
): IRewards
{
// 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
const rewardXP = Math.floor(difficulty * this.mathUtil.interp1(pmcLevel, levelsConfig, xpConfig) * this.randomUtil.getFloat(1 - rewardSpreadConfig, 1 + rewardSpreadConfig));
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 rewardXP = Math.floor(
difficulty * this.mathUtil.interp1(pmcLevel, levelsConfig, xpConfig) *
this.randomUtil.getFloat(1 - rewardSpreadConfig, 1 + rewardSpreadConfig),
);
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 skillPointReward = this.mathUtil.interp1(pmcLevel, levelsConfig, skillPointRewardConfig);
@ -804,16 +907,18 @@ export class RepeatableQuestGenerator
{
value: rewardXP,
type: "Experience",
index: 0
}
index: 0,
},
],
Fail: []
Fail: [],
};
if (traderId === Traders.PEACEKEEPER)
{
// 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
{
@ -837,14 +942,20 @@ export class RepeatableQuestGenerator
}
// 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))
{
const defaultPreset = this.presetHelper.getDefaultPreset(itemSelected._id);
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));
@ -859,7 +970,9 @@ export class RepeatableQuestGenerator
if (roublesBudget > 0)
{
// 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)
{
break; // No reward items left, exit
@ -879,7 +992,7 @@ export class RepeatableQuestGenerator
target: traderId,
value: rewardReputation,
type: "TraderStanding",
index: index
index: index,
};
rewards.Success.push(reward);
}
@ -891,7 +1004,7 @@ export class RepeatableQuestGenerator
target: this.randomUtil.getArrayValue(questConfig.possibleSkillRewards),
value: skillPointReward,
type: "Skill",
index: index
index: index,
};
rewards.Success.push(reward);
}
@ -905,17 +1018,29 @@ export class RepeatableQuestGenerator
* @param roublesBudget Total value of items to return
* @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
const rewardableItems = this.getRewardableItems(repeatableConfig);
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)
{
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
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;
@ -936,7 +1061,7 @@ export class RepeatableQuestGenerator
target: id,
value: value,
type: "Item",
index: index
index: index,
};
const rootItem = {
@ -944,8 +1069,8 @@ export class RepeatableQuestGenerator
_tpl: tpl,
upd: {
StackObjectsCount: value,
SpawnedInSession: true
}
SpawnedInSession: true,
},
};
if (preset)
@ -960,7 +1085,7 @@ export class RepeatableQuestGenerator
}
/**
* Picks rewardable items from items.json. This means they need to fit into the inventory and they shouldn't be keys (debatable)
* Picks rewardable items from items.json. This means they need to fit into the inventory and they shouldn't be keys (debatable)
* @param repeatableQuestConfig Config file
* @returns List of rewardable items [[_tpl, itemTemplate],...]
*/
@ -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
// those are not in the game yet (e.g. AGS grenade launcher)
return Object.entries(this.databaseServer.getTables().templates.items).filter(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
([tpl, itemTemplate]) =>
{
// Base "Item" item has no parent, ignore it
@ -980,7 +1104,7 @@ export class RepeatableQuestGenerator
}
return this.isValidRewardItem(tpl, repeatableQuestConfig);
}
},
);
}
@ -999,8 +1123,10 @@ export class RepeatableQuestGenerator
}
// Item is on repeatable or global blacklist
if (repeatableQuestConfig.rewardBlacklist.includes(tpl)
|| this.itemFilterService.isItemBlacklisted(tpl))
if (
repeatableQuestConfig.rewardBlacklist.includes(tpl) ||
this.itemFilterService.isItemBlacklisted(tpl)
)
{
return false;
}
@ -1011,15 +1137,23 @@ export class RepeatableQuestGenerator
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;
}
// Skip globally blacklisted items + boss items
// biome-ignore lint/complexity/useSimplifiedLogicExpression: <explanation>
valid = !this.itemFilterService.isItemBlacklisted(tpl)
&& !this.itemFilterService.isBossItem(tpl);
valid = !this.itemFilterService.isItemBlacklisted(tpl) &&
!this.itemFilterService.isBossItem(tpl);
return valid;
}
@ -1030,36 +1164,59 @@ export class RepeatableQuestGenerator
*
* @param {string} type Quest type: "Elimination", "Completion" or "Extraction"
* @param {string} traderId Trader from which the quest will be provided
* @param {string} side Scav daily or pmc daily/weekly quest
* @param {string} side Scav daily or pmc daily/weekly quest
* @returns {object} Object which contains the base elements for repeatable quests of the requests type
* (needs to be filled with reward and conditions by called to make a valid quest)
*/
// @Incomplete: define Type for "type".
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.traderId = traderId;
/* in locale, these id correspond to the text of quests
template ids -pmc : Elimination = 616052ea3054fc0e2c24ce6e / Completion = 61604635c725987e815b1a46 / Exploration = 616041eb031af660100c9967
template ids -scav : Elimination = 62825ef60e88d037dc1eb428 / Completion = 628f588ebb558574b2260fe5 / Exploration = 62825ef60e88d037dc1eb42c
template ids -scav : Elimination = 62825ef60e88d037dc1eb428 / Completion = 628f588ebb558574b2260fe5 / Exploration = 62825ef60e88d037dc1eb42c
*/
// Get template id from config based on side and type of quest
quest.templateId = this.questConfig.questTemplateIds[side.toLowerCase()][type.toLowerCase()];
quest.name = quest.name.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.successMessageText = quest.successMessageText.replace("{traderId}", traderId).replace("{templateId}",quest.templateId);
quest.failMessageText = quest.failMessageText.replace("{traderId}", traderId).replace("{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);
quest.name = quest.name.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.successMessageText = quest.successMessageText.replace("{traderId}", traderId).replace(
"{templateId}",
quest.templateId,
);
quest.failMessageText = quest.failMessageText.replace("{traderId}", traderId).replace(
"{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;
}
}
}

View File

@ -10,7 +10,8 @@ import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { Money } from "@spt-aki/models/enums/Money";
import { IScavCaseConfig } from "@spt-aki/models/spt/config/IScavCaseConfig";
import {
RewardCountAndPriceDetails, ScavCaseRewardCountsAndPrices
RewardCountAndPriceDetails,
ScavCaseRewardCountsAndPrices,
} from "@spt-aki/models/spt/hideout/ScavCaseRewardCountsAndPrices";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { ConfigServer } from "@spt-aki/servers/ConfigServer";
@ -20,7 +21,7 @@ import { RagfairPriceService } from "@spt-aki/services/RagfairPriceService";
import { HashUtil } from "@spt-aki/utils/HashUtil";
import { RandomUtil } from "@spt-aki/utils/RandomUtil";
/**
/**
* Handle the creation of randomised scav case rewards
*/
@injectable()
@ -38,12 +39,12 @@ export class ScavCaseRewardGenerator
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("RagfairPriceService") protected ragfairPriceService: RagfairPriceService,
@inject("ItemFilterService") protected itemFilterService: ItemFilterService,
@inject("ConfigServer") protected configServer: ConfigServer
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
this.scavCaseConfig = this.configServer.getConfig(ConfigTypes.SCAVCASE);
}
/**
* Create an array of rewards that will be given to the player upon completing their scav case build
* @param recipeId recipe of the scav case craft
@ -54,7 +55,7 @@ export class ScavCaseRewardGenerator
this.cacheDbItems();
// 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);
// 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);
// 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 randomlyPickedSuperRareRewards = this.pickRandomRewards(superRarePricedItems, rewardItemCounts.Superrare, "superrare");
const randomlyPickedSuperRareRewards = this.pickRandomRewards(
superRarePricedItems,
rewardItemCounts.Superrare,
"superrare",
);
// Add randomised stack sizes to ammo and money rewards
const commonRewards = this.randomiseContainerItemRewards(randomlyPickedCommonRewards, "common");
@ -95,11 +104,13 @@ export class ScavCaseRewardGenerator
{
return false;
}
// Skip item if item id is on blacklist
if ((item._type !== "Item")
|| this.scavCaseConfig.rewardItemBlacklist.includes(item._id)
|| this.itemFilterService.isItemBlacklisted(item._id))
if (
(item._type !== "Item") ||
this.scavCaseConfig.rewardItemBlacklist.includes(item._id) ||
this.itemFilterService.isItemBlacklisted(item._id)
)
{
return false;
}
@ -108,13 +119,13 @@ export class ScavCaseRewardGenerator
{
return false;
}
// Skip item if parent id is blacklisted
if (this.itemHelper.isOfBaseclasses(item._id, this.scavCaseConfig.rewardItemParentBlacklist))
{
return false;
}
return true;
});
}
@ -139,13 +150,13 @@ export class ScavCaseRewardGenerator
{
return false;
}
// Skip ammo that doesn't stack as high as value in config
if (item._props.StackMaxSize < this.scavCaseConfig.ammoRewards.minStackSize)
{
return false;
}
return true;
});
}
@ -155,27 +166,31 @@ export class ScavCaseRewardGenerator
* Pick a number of items to be rewards, the count is defined by the values in `itemFilters` param
* @param items item pool to pick rewards from
* @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[] = [];
let rewardWasMoney = false;
let rewardWasAmmo = false;
const randomCount = this.randomUtil.getInt(itemFilters.minCount, itemFilters.maxCount);
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());
if (!this.scavCaseConfig.allowMultipleMoneyRewardsPerRarity)
{
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));
if (!this.scavCaseConfig.allowMultipleAmmoRewardsPerRarity)
{
@ -214,10 +229,9 @@ export class ScavCaseRewardGenerator
*/
protected getRandomMoney(): ITemplateItem
{
const money: ITemplateItem[] = [];
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["5449016a4bdc2d6f028b456f"]); // rub
money.push(this.databaseServer.getTables().templates.items["569668774bdc2da2298b4568"]); // euro
money.push(this.databaseServer.getTables().templates.items["5696686a4bdc2da3298b456a"]); // dollar
return this.randomUtil.getArrayValue(money);
@ -234,8 +248,10 @@ export class ScavCaseRewardGenerator
{
// Is ammo handbook price between desired range
const handbookPrice = this.ragfairPriceService.getStaticPriceForItem(ammo._id);
if (handbookPrice >= this.scavCaseConfig.ammoRewards.ammoRewardValueRangeRub[rarity].min
&& handbookPrice <= this.scavCaseConfig.ammoRewards.ammoRewardValueRangeRub[rarity].max)
if (
handbookPrice >= this.scavCaseConfig.ammoRewards.ammoRewardValueRangeRub[rarity].min &&
handbookPrice <= this.scavCaseConfig.ammoRewards.ammoRewardValueRangeRub[rarity].max
)
{
return true;
}
@ -266,7 +282,7 @@ export class ScavCaseRewardGenerator
const resultItem = {
_id: this.hashUtil.generate(),
_tpl: item._id,
upd: undefined
upd: undefined,
};
this.addStackCountToAmmoAndMoney(item, resultItem, rarity);
@ -288,29 +304,37 @@ export class ScavCaseRewardGenerator
* @param item money or ammo item
* @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)
{
resultItem.upd = {
StackObjectsCount: this.getRandomAmountRewardForScavCase(item, rarity)
StackObjectsCount: this.getRandomAmountRewardForScavCase(item, rarity),
};
}
}
/**
*
* @param dbItems all items from the items.json
* @param itemFilters controls how the dbItems will be filtered and returned (handbook price)
* @returns filtered dbItems array
*/
protected getFilteredItemsByPrice(dbItems: ITemplateItem[], itemFilters: RewardCountAndPriceDetails): ITemplateItem[]
protected getFilteredItemsByPrice(
dbItems: ITemplateItem[],
itemFilters: RewardCountAndPriceDetails,
): ITemplateItem[]
{
return dbItems.filter((item) =>
{
const handbookPrice = this.ragfairPriceService.getStaticPriceForItem(item._id);
if (handbookPrice >= itemFilters.minPriceRub
&& handbookPrice <= itemFilters.maxPriceRub)
if (
handbookPrice >= itemFilters.minPriceRub &&
handbookPrice <= itemFilters.maxPriceRub
)
{
return true;
}
@ -330,12 +354,11 @@ export class ScavCaseRewardGenerator
// Create reward min/max counts for each type
for (const rewardType of rewardTypes)
{
result[rewardType] =
{
result[rewardType] = {
minCount: scavCaseDetails.EndProducts[rewardType].min,
maxCount: scavCaseDetails.EndProducts[rewardType].max,
minPriceRub: this.scavCaseConfig.rewardItemValueRangeRub[rewardType.toLowerCase()].min,
maxPriceRub: this.scavCaseConfig.rewardItemValueRangeRub[rewardType.toLowerCase()].max
maxPriceRub: this.scavCaseConfig.rewardItemValueRangeRub[rewardType.toLowerCase()].max,
};
}
@ -353,23 +376,35 @@ export class ScavCaseRewardGenerator
let amountToGive = 1;
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)
{
switch (itemToCalculate._id)
{
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;
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;
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;
}
}
return amountToGive;
}
}
}

View File

@ -23,7 +23,7 @@ export class WeatherGenerator
@inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("ApplicationContext") protected applicationContext: ApplicationContext,
@inject("ConfigServer") protected configServer: ConfigServer
@inject("ConfigServer") protected configServer: ConfigServer,
)
{
this.weatherConfig = this.configServer.getConfig(ConfigTypes.WEATHER);
@ -62,16 +62,17 @@ export class WeatherGenerator
/**
* Get the current in-raid time
* @param currentDate (new Date())
* @returns Date object of current in-raid time
* @returns Date object of current in-raid time
*/
public getInRaidTime(currentDate: Date): Date
{
// 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
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 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 clientAcceleratedDate = new Date(currentDate.valueOf() + acceleratedMS);
return clientAcceleratedDate;
@ -105,15 +106,15 @@ export class WeatherGenerator
wind_gustiness: this.getRandomFloat("windGustiness"),
rain: rain,
// eslint-disable-next-line @typescript-eslint/naming-convention
rain_intensity: (rain > 1)
? this.getRandomFloat("rainIntensity")
: 0,
rain_intensity: (rain > 1) ?
this.getRandomFloat("rainIntensity") :
0,
fog: this.getWeightedFog(),
temp: this.getRandomFloat("temp"),
pressure: this.getRandomFloat("pressure"),
time: "",
date: "",
timestamp: 0
timestamp: 0,
};
this.setCurrentDateTime(result);
@ -139,32 +140,49 @@ export class WeatherGenerator
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
{
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
{
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
{
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
{
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
{
return parseFloat(this.randomUtil.getFloat(this.weatherConfig.weather[node].min,
this.weatherConfig.weather[node].max).toPrecision(3));
return parseFloat(
this.randomUtil.getFloat(this.weatherConfig.weather[node].min, this.weatherConfig.weather[node].max)
.toPrecision(3),
);
}
}
}

View File

@ -5,4 +5,4 @@ export interface IInventoryMagGen
getPriority(): number;
canHandleInventoryMagGen(inventoryMagGen: InventoryMagGen): boolean;
process(inventoryMagGen: InventoryMagGen): void;
}
}

View File

@ -2,40 +2,39 @@ import { Inventory } from "@spt-aki/models/eft/common/tables/IBotBase";
import { GenerationData } from "@spt-aki/models/eft/common/tables/IBotType";
import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem";
export class InventoryMagGen
export class InventoryMagGen
{
constructor(
private magCounts: GenerationData,
private magazineTemplate: ITemplateItem,
private weaponTemplate: ITemplateItem,
private ammoTemplate: ITemplateItem,
private pmcInventory: Inventory
)
{
}
private pmcInventory: Inventory,
)
{}
public getMagCount(): GenerationData
public getMagCount(): GenerationData
{
return this.magCounts;
}
public getMagazineTemplate(): ITemplateItem
public getMagazineTemplate(): ITemplateItem
{
return this.magazineTemplate;
}
public getWeaponTemplate(): ITemplateItem
public getWeaponTemplate(): ITemplateItem
{
return this.weaponTemplate;
}
public getAmmoTemplate(): ITemplateItem
public getAmmoTemplate(): ITemplateItem
{
return this.ammoTemplate;
}
public getPmcInventory(): Inventory
public getPmcInventory(): Inventory
{
return this.pmcInventory;
}
}
}

View File

@ -8,36 +8,43 @@ import { RandomUtil } from "@spt-aki/utils/RandomUtil";
@injectable()
export class BarrelInventoryMagGen implements IInventoryMagGen
{
constructor(
@inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("BotWeaponGeneratorHelper") protected botWeaponGeneratorHelper: BotWeaponGeneratorHelper
@inject("BotWeaponGeneratorHelper") protected botWeaponGeneratorHelper: BotWeaponGeneratorHelper,
)
{ }
{}
getPriority(): number
getPriority(): number
{
return 50;
}
canHandleInventoryMagGen(inventoryMagGen: InventoryMagGen): boolean
canHandleInventoryMagGen(inventoryMagGen: InventoryMagGen): boolean
{
return inventoryMagGen.getWeaponTemplate()._props.ReloadMode === "OnlyBarrel";
}
process(inventoryMagGen: InventoryMagGen): void
process(inventoryMagGen: InventoryMagGen): void
{
// Can't be done by _props.ammoType as grenade launcher shoots grenades with ammoType of "buckshot"
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);
}
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,60 +12,71 @@ import { LocalisationService } from "@spt-aki/services/LocalisationService";
@injectable()
export class ExternalInventoryMagGen implements IInventoryMagGen
{
constructor(
@inject("WinstonLogger") protected logger: ILogger,
@inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("BotWeaponGeneratorHelper") protected botWeaponGeneratorHelper: BotWeaponGeneratorHelper
@inject("BotWeaponGeneratorHelper") protected botWeaponGeneratorHelper: BotWeaponGeneratorHelper,
)
{ }
{}
getPriority(): number
getPriority(): number
{
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
}
process(inventoryMagGen: InventoryMagGen): void
process(inventoryMagGen: InventoryMagGen): void
{
let magTemplate = inventoryMagGen.getMagazineTemplate();
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++)
{
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(
[EquipmentSlots.TACTICAL_VEST, EquipmentSlots.POCKETS],
magazineWithAmmo[0]._id,
magazineTpl,
magazineWithAmmo,
inventoryMagGen.getPmcInventory());
inventoryMagGen.getPmcInventory(),
);
if (ableToFitMagazinesIntoBotInventory === ItemAddedResult.NO_SPACE && i < randomizedMagazineCount)
{
/* We were unable to fit at least the minimum amount of magazines,
* so we fallback to default magazine and try again.
* Temporary workaround to Killa spawning with no extras if he spawns with a drum mag */
if (magazineTpl === this.botWeaponGeneratorHelper.getWeaponsDefaultMagazineTpl(inventoryMagGen.getWeaponTemplate()))
// We were unable to fit at least the minimum amount of magazines, so we fallback to default magazine
// and try again. Temporary workaround to Killa spawning with no extras if he spawns with a drum mag.
// TODO: Fix this properly
if (
magazineTpl ===
this.botWeaponGeneratorHelper.getWeaponsDefaultMagazineTpl(inventoryMagGen.getWeaponTemplate())
)
{
// We were already on default - stop here to prevent infinite looping
break;
}
// 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];
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;
}
@ -78,5 +89,4 @@ export class ExternalInventoryMagGen implements IInventoryMagGen
}
}
}
}
}

View File

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

View File

@ -9,25 +9,32 @@ import { EquipmentSlots } from "@spt-aki/models/enums/EquipmentSlots";
@injectable()
export class UbglExternalMagGen implements IInventoryMagGen
{
constructor(
@inject("BotWeaponGeneratorHelper") protected botWeaponGeneratorHelper: BotWeaponGeneratorHelper
@inject("BotWeaponGeneratorHelper") protected botWeaponGeneratorHelper: BotWeaponGeneratorHelper,
)
{ }
{}
public getPriority(): number
public getPriority(): number
{
return 1;
}
public canHandleInventoryMagGen(inventoryMagGen: InventoryMagGen): boolean
public canHandleInventoryMagGen(inventoryMagGen: InventoryMagGen): boolean
{
return inventoryMagGen.getWeaponTemplate()._parent === BaseClasses.UBGL;
}
public process(inventoryMagGen: InventoryMagGen): void
public process(inventoryMagGen: InventoryMagGen): void
{
const bulletCount = this.botWeaponGeneratorHelper.getRandomizedBulletCount(inventoryMagGen.getMagCount(), inventoryMagGen.getMagazineTemplate());
this.botWeaponGeneratorHelper.addAmmoIntoEquipmentSlots(inventoryMagGen.getAmmoTemplate()._id, bulletCount, inventoryMagGen.getPmcInventory(), [EquipmentSlots.TACTICAL_VEST]);
const bulletCount = this.botWeaponGeneratorHelper.getRandomizedBulletCount(
inventoryMagGen.getMagCount(),
inventoryMagGen.getMagazineTemplate(),
);
this.botWeaponGeneratorHelper.addAmmoIntoEquipmentSlots(
inventoryMagGen.getAmmoTemplate()._id,
bulletCount,
inventoryMagGen.getPmcInventory(),
[EquipmentSlots.TACTICAL_VEST],
);
}
}
}