Moe majority of assort generation from FenceService into FenceBaseAssortGenerator

Generate an items children and store in fence assort base

Better handle presets
Fix `removeRandomItemFromAssorts()` not removing all of an items mods from memory
Correctly calculate an items price including its children
This commit is contained in:
Dev 2024-01-20 16:20:39 +00:00
parent 8b2fa7c8dd
commit 42b915990e
3 changed files with 331 additions and 128 deletions

View File

@ -2,6 +2,7 @@ import { inject, injectable } from "tsyringe";
import { HandbookHelper } from "@spt-aki/helpers/HandbookHelper";
import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
import { PresetHelper } from "@spt-aki/helpers/PresetHelper";
import { Item } from "@spt-aki/models/eft/common/tables/IItem";
import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem";
import { IBarterScheme } from "@spt-aki/models/eft/common/tables/ITrader";
@ -14,6 +15,8 @@ import { ConfigServer } from "@spt-aki/servers/ConfigServer";
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer";
import { ItemFilterService } from "@spt-aki/services/ItemFilterService";
import { SeasonalEventService } from "@spt-aki/services/SeasonalEventService";
import { HashUtil } from "@spt-aki/utils/HashUtil";
import { JsonUtil } from "@spt-aki/utils/JsonUtil";
@injectable()
export class FenceBaseAssortGenerator
@ -22,9 +25,12 @@ export class FenceBaseAssortGenerator
constructor(
@inject("WinstonLogger") protected logger: ILogger,
@inject("HashUtil") protected hashUtil: HashUtil,
@inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("HandbookHelper") protected handbookHelper: HandbookHelper,
@inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("PresetHelper") protected presetHelper: PresetHelper,
@inject("ItemFilterService") protected itemFilterService: ItemFilterService,
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService,
@inject("ConfigServer") protected configServer: ConfigServer,
@ -39,29 +45,27 @@ export class FenceBaseAssortGenerator
public generateFenceBaseAssorts(): void
{
const blockedSeasonalItems = this.seasonalEventService.getInactiveSeasonalEventItems();
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 rootItemDb of this.itemHelper.getItems().filter((item) => this.isValidFenceItem(item)))
{
// Skip blacklisted items
if (this.itemFilterService.isItemBlacklisted(item._id))
if (this.itemFilterService.isItemBlacklisted(rootItemDb._id))
{
continue;
}
if (!this.itemHelper.isValidItem(item._id))
// Invalid
if (!this.itemHelper.isValidItem(rootItemDb._id))
{
continue;
}
// Skip items on fence ignore list
// Item base type blacklisted
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(rootItemDb._id)
|| this.itemHelper.isOfBaseclasses(rootItemDb._id, this.traderConfig.fence.blacklist)
)
{
continue;
@ -69,37 +73,200 @@ export class FenceBaseAssortGenerator
}
// Skip seasonal event items when not in seasonal event
if (this.traderConfig.fence.blacklistSeasonalItems && blockedSeasonalItems.includes(item._id))
if (this.traderConfig.fence.blacklistSeasonalItems && blockedSeasonalItems.includes(rootItemDb._id))
{
continue;
}
// Create barter scheme object
// Create item object in array
const itemWithChildrenToAdd: Item[] = [{
_id: this.hashUtil.generate(),
_tpl: rootItemDb._id,
parentId: "hideout",
slotId: "hideout",
upd: { StackObjectsCount: 9999999, UnlimitedCount: true },
}];
// Need to add mods to armors so they dont show as red in the trade screen
let price = this.handbookHelper.getTemplatePrice(rootItemDb._id);
if (this.itemHelper.itemRequiresSoftInserts(rootItemDb._id))
{
this.addChildrenToArmorModSlots(itemWithChildrenToAdd, rootItemDb);
price = this.getHandbookItemPriceWithChildren(itemWithChildrenToAdd);
}
// Ensure IDs are unique
this.itemHelper.remapRootItemId(itemWithChildrenToAdd);
if (itemWithChildrenToAdd.length > 1)
{
this.itemHelper.reparentItemAndChildren(itemWithChildrenToAdd[0], itemWithChildrenToAdd);
}
// Create barter scheme (price)
const barterSchemeToAdd: IBarterScheme = {
count: Math.round(
this.handbookHelper.getTemplatePrice(item._id) * this.traderConfig.fence.itemPriceMult,
this.handbookHelper.getTemplatePrice(rootItemDb._id) * this.traderConfig.fence.itemPriceMult,
),
_tpl: Money.ROUBLES,
};
// Add barter data to base
baseFenceAssort.barter_scheme[item._id] = [[barterSchemeToAdd]];
// Create item object
const itemToAdd: Item = {
_id: item._id,
_tpl: item._id,
parentId: "hideout",
slotId: "hideout",
upd: { StackObjectsCount: 9999999, UnlimitedCount: true },
};
baseFenceAssort.barter_scheme[itemWithChildrenToAdd[0]._id] = [[barterSchemeToAdd]];
// Add item to base
baseFenceAssort.items.push(itemToAdd);
baseFenceAssort.items.push(...itemWithChildrenToAdd);
// Add loyalty data to base
baseFenceAssort.loyal_level_items[item._id] = 1;
baseFenceAssort.loyal_level_items[itemWithChildrenToAdd[0]._id] = 1;
}
// Add all default presets to base fence assort
const defaultPresets = Object.values(this.presetHelper.getDefaultPresets());
for (const defaultPreset of defaultPresets)
{
// Skip presets we've already added
if (baseFenceAssort.items.some((item) => item.upd && item.upd.sptPresetId === defaultPreset._id))
{
continue;
}
// Construct preset + mods
const presetAndMods: Item[] = this.itemHelper.replaceIDs(
null,
this.jsonUtil.clone(defaultPreset._items),
);
for (let i = 0; i < presetAndMods.length; i++)
{
const mod = presetAndMods[i];
// Build root Item info
if (!("parentId" in mod))
{
mod._id = presetAndMods[0]._id;
mod.parentId = "hideout";
mod.slotId = "hideout";
mod.upd = {
UnlimitedCount: false,
StackObjectsCount: 1,
BuyRestrictionCurrent: 0,
sptPresetId: defaultPreset._id, // Store preset id here so we can check it later to prevent preset dupes
};
// Updated root item, exit loop
break;
}
}
const presetDbItem = this.itemHelper.getItem(presetAndMods[0]._tpl)[1];
// Add constructed preset to assorts
baseFenceAssort.items.push(...presetAndMods);
// Calculate preset price
let rub = 0;
for (const it of presetAndMods)
{
rub += this.handbookHelper.getTemplatePrice(it._tpl);
}
// Multiply weapon+mods rouble price by multipler in config
baseFenceAssort.barter_scheme[presetAndMods[0]._id] = [[]];
baseFenceAssort.barter_scheme[presetAndMods[0]._id][0][0] = { _tpl: Money.ROUBLES, count: Math.round(rub) };
baseFenceAssort.loyal_level_items[presetAndMods[0]._id] = 1;
}
}
/**
* Add soft inserts + armor plates to an armor
* @param armor Armor item array to add mods into
* @param itemDbDetails Armor items db template
*/
protected addChildrenToArmorModSlots(armor: Item[], itemDbDetails: ITemplateItem): void
{
// Armor has no mods, make no additions
const hasMods = itemDbDetails._props.Slots.length > 0;
if (!hasMods)
{
return;
}
// Check for and add required soft inserts to armors
const requiredSlots = itemDbDetails._props.Slots.filter(slot => slot._required);
const hasRequiredSlots = requiredSlots.length > 0;
if (hasRequiredSlots)
{
for (const requiredSlot of requiredSlots)
{
const modItemDbDetails = this.itemHelper.getItem(requiredSlot._props.filters[0].Plate)[1];
const plateTpl = requiredSlot._props.filters[0].Plate; // `Plate` property appears to be the 'default' item for slot
if (plateTpl === "")
{
// Some bsg plate properties are empty, skip mod
continue;
}
const mod: Item = {
_id: this.hashUtil.generate(),
_tpl: plateTpl,
parentId: armor[0]._id,
slotId: requiredSlot._name,
upd: {
Repairable: {
Durability: modItemDbDetails._props.MaxDurability,
MaxDurability: modItemDbDetails._props.MaxDurability
}
}
};
armor.push(mod);
}
}
// Check for and add plate items
const plateSlots = itemDbDetails._props.Slots.filter(slot => this.itemHelper.isRemovablePlateSlot(slot._name));
if (plateSlots.length > 0)
{
for (const plateSlot of plateSlots)
{
const plateTpl = plateSlot._props.filters[0].Plate
if (!plateTpl)
{
// Bsg data lacks a default plate, skip adding mod
continue;
}
const modItemDbDetails = this.itemHelper.getItem(plateTpl)[1];
armor.push({
_id: this.hashUtil.generate(),
_tpl: plateSlot._props.filters[0].Plate, // `Plate` property appears to be the 'default' item for slot
parentId: armor[0]._id,
slotId: plateSlot._name,
upd: {
Repairable: {
Durability: modItemDbDetails._props.MaxDurability,
MaxDurability: modItemDbDetails._props.MaxDurability
}
}
});
}
}
}
/**
* Calculate and return the price of an item and its child mods
* @param itemWithChildren Item + mods to calcualte price of
* @returns price
*/
protected getHandbookItemPriceWithChildren(itemWithChildren: Item[]): number
{
let price = 0;
for (const item of itemWithChildren)
{
price += this.handbookHelper.getTemplatePrice(item._tpl);
}
return price;
}
/**

View File

@ -507,7 +507,8 @@ export class ItemHelper
for (const itemFromAssort of assort)
{
if (itemFromAssort.parentId === itemIdToFind && !list.find((item) => itemFromAssort._id === item._id))
if (itemFromAssort.parentId === itemIdToFind
&& !list.find((item) => itemFromAssort._id === item._id))
{
list.push(itemFromAssort);
list = list.concat(this.findAndReturnChildrenByAssort(itemFromAssort._id, assort));

View File

@ -114,7 +114,7 @@ export class FenceService
/**
* Adjust all items contained inside an assort by a multiplier
* @param assort Assort that contains items with prices to adjust
* @param assort (clone)Assort that contains items with prices to adjust
* @param itemMultipler multipler to use on items
* @param presetMultiplier preset multipler to use on presets
*/
@ -325,12 +325,16 @@ export class FenceService
itemToRemove = this.randomUtil.getArrayValue(assort.items);
}
const indexOfItemToRemove = assort.items.findIndex((x) => x._id === itemToRemove._id);
const indexOfItemToRemove = assort.items.findIndex((item) => item._id === itemToRemove._id);
assort.items.splice(indexOfItemToRemove, 1);
// Clean up any mods if item removed was a weapon
// TODO: also check for mods attached down the item chain
assort.items = assort.items.filter((x) => x.parentId !== itemToRemove._id);
const itemWithChildren = this.itemHelper.findAndReturnChildrenAsItems(assort.items, itemToRemove._id);
for (const itemToDelete of itemWithChildren)
{
// Delete item from assort items array
assort.items.splice(assort.items.indexOf(itemToDelete), 1);
}
delete assort.barter_scheme[itemToRemove._id];
delete assort.loyal_level_items[itemToRemove._id];
@ -402,22 +406,50 @@ export class FenceService
*/
protected createAssorts(assortCount: number, assorts: ITraderAssort, loyaltyLevel: number): void
{
const fenceAssort = this.databaseServer.getTables().traders[Traders.FENCE].assort;
const fenceAssortIds = Object.keys(fenceAssort.loyal_level_items);
const baseFenceAssort = this.databaseServer.getTables().traders[Traders.FENCE].assort;
const itemTypeCounts = this.initItemLimitCounter(this.traderConfig.fence.itemTypeLimits);
this.addItemAssorts(assortCount, fenceAssortIds, assorts, fenceAssort, itemTypeCounts, loyaltyLevel);
this.addItemAssorts(assortCount, assorts, baseFenceAssort, itemTypeCounts, loyaltyLevel);
// Add presets
const maxPresetCount = Math.round(assortCount * (this.traderConfig.fence.maxPresetsPercent / 100));
const randomisedPresetCount = this.randomUtil.getInt(0, maxPresetCount);
const defaultPresets = this.presetHelper.getDefaultPresets();
this.addPresets(randomisedPresetCount, defaultPresets, assorts, loyaltyLevel);
this.addPresetsToAssort(randomisedPresetCount, assorts, baseFenceAssort);
}
protected addPresetsToAssort(desiredPresetCount: number, assorts: ITraderAssort, baseFenceAssort: ITraderAssort): void
{
let presetsAddedCount = 0;
if (desiredPresetCount <= 0)
{
return;
}
const presetRootItems = baseFenceAssort.items.filter(item => item.upd?.sptPresetId);
while (presetsAddedCount < desiredPresetCount)
{
const randomPresetRoot = this.randomUtil.getArrayValue(presetRootItems);
const rootItemDb = this.itemHelper.getItem(randomPresetRoot._tpl)[1];
const presetWithChildrenClone = this.jsonUtil.clone(this.itemHelper.findAndReturnChildrenAsItems(baseFenceAssort.items, randomPresetRoot._id));
this.removeRandomModsOfItem(presetWithChildrenClone);
// Need to add mods to armors so they dont show as red in the trade screen
if (this.itemHelper.itemRequiresSoftInserts(randomPresetRoot._tpl))
{
this.randomiseArmorModDurability(presetWithChildrenClone, rootItemDb);
}
assorts.items.push(...presetWithChildrenClone);
assorts.barter_scheme[randomPresetRoot._id] = baseFenceAssort.barter_scheme[randomPresetRoot._id];
assorts.loyal_level_items[randomPresetRoot._id] = baseFenceAssort.loyal_level_items[randomPresetRoot._id];
presetsAddedCount++;
}
}
protected addItemAssorts(
assortCount: number,
fenceAssortIds: string[],
assorts: ITraderAssort,
fenceAssort: ITraderAssort,
itemTypeCounts: Record<string, { current: number; max: number; }>,
@ -425,89 +457,83 @@ export class FenceService
): void
{
const priceLimits = this.traderConfig.fence.itemCategoryRoublePriceLimit;
const assortRootItems = fenceAssort.items.filter(x => x.parentId === "hideout");
for (let i = 0; i < assortCount; i++)
{
const itemTpl = fenceAssortIds[this.randomUtil.getInt(0, fenceAssortIds.length - 1)];
const chosenAssortRoot = this.randomUtil.getArrayValue(assortRootItems);
if (!chosenAssortRoot)
{
this.logger.error(this.localisationService.getText("fence-unable_to_find_assort_by_id", chosenAssortRoot._id));
const price = this.handbookHelper.getTemplatePrice(itemTpl);
const itemIsPreset = this.presetHelper.isPreset(itemTpl);
continue;
}
const desiredAssortItemAndChildrenClone = this.jsonUtil.clone(this.itemHelper.findAndReturnChildrenAsItems(fenceAssort.items, chosenAssortRoot._id));
const itemDbDetails = this.itemHelper.getItem(chosenAssortRoot._tpl)[1];
const itemLimitCount = itemTypeCounts[itemDbDetails._parent];
if (itemLimitCount && itemLimitCount.current > itemLimitCount.max)
{
// Skip adding item as assort as limit reached, decrement i counter so we still get another item
i--;
continue;
}
const itemIsPreset = this.presetHelper.isPreset(chosenAssortRoot._id);
const price = fenceAssort.barter_scheme[chosenAssortRoot._id][0][0].count;
if (price === 0 || (price === 1 && !itemIsPreset) || price === 100)
{
// Don't allow "special" items
i--;
continue;
}
// It's a normal non-preset item
if (!itemIsPreset)
if (price > priceLimits[itemDbDetails._parent])
{
const desiredAssort = fenceAssort.items[fenceAssort.items.findIndex((i) => i._id === itemTpl)];
if (!desiredAssort)
{
this.logger.error(this.localisationService.getText("fence-unable_to_find_assort_by_id", itemTpl));
}
const itemDbDetails = this.itemHelper.getItem(desiredAssort._tpl)[1];
const itemLimitCount = itemTypeCounts[itemDbDetails._parent];
if (itemLimitCount && itemLimitCount.current > itemLimitCount.max)
{
// Skip adding item as assort as limit reached, decrement i counter so we still get another item
i--;
continue;
}
if (price > priceLimits[itemDbDetails._parent])
{
i--;
continue;
}
// Increment count as item is being added
if (itemLimitCount)
{
itemLimitCount.current++;
}
const itemsToPush: Item[] = [];
const rootItemToPush = this.jsonUtil.clone(desiredAssort);
this.randomiseItemUpdProperties(itemDbDetails, rootItemToPush);
itemsToPush.push(rootItemToPush);
rootItemToPush._id = this.hashUtil.generate();
rootItemToPush.upd.StackObjectsCount = this.getSingleItemStackCount(itemDbDetails);
rootItemToPush.upd.BuyRestrictionCurrent = 0;
rootItemToPush.upd.UnlimitedCount = false;
// Need to add mods to armors so they dont show as red in the trade screen
if (this.itemHelper.itemRequiresSoftInserts(rootItemToPush._tpl))
{
this.addModsToArmorModSlots(itemsToPush, itemDbDetails);
}
assorts.items.push(...itemsToPush);
assorts.barter_scheme[rootItemToPush._id] = fenceAssort.barter_scheme[itemTpl];
assorts.loyal_level_items[rootItemToPush._id] = loyaltyLevel;
i--;
continue;
}
// Increment count as item is being added
if (itemLimitCount)
{
itemLimitCount.current++;
}
const rootItemBeingAdded = desiredAssortItemAndChildrenClone[0];
this.randomiseItemUpdProperties(itemDbDetails, rootItemBeingAdded);
rootItemBeingAdded.upd.StackObjectsCount = this.getSingleItemStackCount(itemDbDetails);
rootItemBeingAdded.upd.BuyRestrictionCurrent = 0;
rootItemBeingAdded.upd.UnlimitedCount = false;
// Need to add mods to armors so they dont show as red in the trade screen
if (this.itemHelper.itemRequiresSoftInserts(rootItemBeingAdded._tpl))
{
this.randomiseArmorModDurability(desiredAssortItemAndChildrenClone, itemDbDetails);
}
assorts.items.push(...desiredAssortItemAndChildrenClone);
assorts.barter_scheme[rootItemBeingAdded._id] = fenceAssort.barter_scheme[chosenAssortRoot._id];
assorts.loyal_level_items[rootItemBeingAdded._id] = loyaltyLevel;
}
}
/**
* Add soft inserts + armor plates to an armor
* Adjust plate / soft insert durability values
* @param armor Armor item array to add mods into
* @param itemDbDetails Armor items db template
*/
protected addModsToArmorModSlots(armor: Item[], itemDbDetails: ITemplateItem): void
protected randomiseArmorModDurability(armor: Item[], itemDbDetails: ITemplateItem): void
{
// Armor has no mods, make no additions
// Armor has no mods, make no changes
const hasMods = itemDbDetails._props.Slots.length > 0;
if (!hasMods)
{
return;
}
// Check for and add required soft inserts to armors
// Check for and adjust soft insert durability values
const requiredSlots = itemDbDetails._props.Slots.filter(slot => slot._required);
const hasRequiredSlots = requiredSlots.length > 0;
if (hasRequiredSlots)
@ -515,7 +541,9 @@ export class FenceService
for (const requiredSlot of requiredSlots)
{
const modItemDbDetails = this.itemHelper.getItem(requiredSlot._props.filters[0].Plate)[1];
const durabilityValues = this.getRandomisedArmorDurabilityValues(modItemDbDetails, this.traderConfig.fence.armorMaxDurabilityPercentMinMax);
const durabilityValues = this.getRandomisedArmorDurabilityValues(
modItemDbDetails,
this.traderConfig.fence.armorMaxDurabilityPercentMinMax);
const plateTpl = requiredSlot._props.filters[0].Plate; // `Plate` property appears to be the 'default' item for slot
if (plateTpl === "")
{
@ -523,34 +551,36 @@ export class FenceService
continue;
}
const mod: Item = {
_id: this.hashUtil.generate(),
_tpl: plateTpl,
parentId: armor[0]._id,
slotId: requiredSlot._name,
upd: {
Repairable: {
Durability: durabilityValues.Durability,
MaxDurability: durabilityValues.MaxDurability
}
}
};
// Find items mod to apply dura changes to
const modItemToAdjust = armor.find(mod => mod.slotId === requiredSlot._name);
if (!modItemToAdjust.upd)
{
modItemToAdjust.upd = {}
}
if (!modItemToAdjust.upd.Repairable)
{
modItemToAdjust.upd.Repairable = {
Durability: modItemDbDetails._props.MaxDurability,
MaxDurability: modItemDbDetails._props.MaxDurability
};
}
modItemToAdjust.upd.Repairable.Durability = durabilityValues.Durability;
modItemToAdjust.upd.Repairable.MaxDurability = durabilityValues.MaxDurability;
// 25% chance to add shots to visor when its below max durability
if (this.randomUtil.getChance100(25)
&& mod.parentId === BaseClasses.ARMORED_EQUIPMENT && mod.slotId === "mod_equipment_000"
&& mod.upd.Repairable.Durability < modItemDbDetails._props.MaxDurability)
&& modItemToAdjust.parentId === BaseClasses.ARMORED_EQUIPMENT && modItemToAdjust.slotId === "mod_equipment_000"
&& modItemToAdjust.upd.Repairable.Durability < modItemDbDetails._props.MaxDurability) // Is damaged
{
mod.upd.FaceShield = {
modItemToAdjust.upd.FaceShield = {
Hits: this.randomUtil.getInt(1,3)
}
}
armor.push(mod);
}
}
// Check for and add plate items
// Check for and adjust plate durability values
const plateSlots = itemDbDetails._props.Slots.filter(slot => this.itemHelper.isRemovablePlateSlot(slot._name));
if (plateSlots.length > 0)
{
@ -569,19 +599,27 @@ export class FenceService
continue;
}
const modItemDbDetails = this.itemHelper.getItem(plateTpl)[1];
const durabilityValues = this.getRandomisedArmorDurabilityValues(modItemDbDetails, this.traderConfig.fence.armorMaxDurabilityPercentMinMax);
armor.push({
_id: this.hashUtil.generate(),
_tpl: plateSlot._props.filters[0].Plate, // `Plate` property appears to be the 'default' item for slot
parentId: armor[0]._id,
slotId: plateSlot._name,
upd: {
Repairable: {
Durability: durabilityValues.Durability,
MaxDurability: durabilityValues.MaxDurability
}
}
});
const durabilityValues = this.getRandomisedArmorDurabilityValues(
modItemDbDetails,
this.traderConfig.fence.armorMaxDurabilityPercentMinMax);
// Find items mod to apply dura changes to
const modItemToAdjust = armor.find(mod => mod.slotId === plateSlot._name);
if (!modItemToAdjust.upd)
{
modItemToAdjust.upd = {}
}
if (!modItemToAdjust.upd.Repairable)
{
modItemToAdjust.upd.Repairable = {
Durability: modItemDbDetails._props.MaxDurability,
MaxDurability: modItemDbDetails._props.MaxDurability
};
}
modItemToAdjust.upd.Repairable.Durability = durabilityValues.Durability;
modItemToAdjust.upd.Repairable.MaxDurability = durabilityValues.MaxDurability;
}
}
}
@ -776,10 +814,7 @@ export class FenceService
// Randomise armor durability
if (
(itemDetails._parent === BaseClasses.ARMOR
|| itemDetails._parent === BaseClasses.HEADWEAR
|| itemDetails._parent === BaseClasses.VEST
|| itemDetails._parent === BaseClasses.ARMORED_EQUIPMENT
(itemDetails._parent === BaseClasses.ARMORED_EQUIPMENT
|| itemDetails._parent === BaseClasses.FACECOVER
|| itemDetails._parent === BaseClasses.ARMOR_PLATE
) && itemDetails._props.MaxDurability > 0