Reworked fence assort generation and partial refresh

Split item generation values up, separate value for item/equipment/weapon
Fixed more assorts being generated than were being deleted each partial refresh
Added assort item options to discount assort config
This commit is contained in:
Dev 2024-02-09 12:39:58 +00:00
parent b7b08f99f2
commit e915d17019
4 changed files with 254 additions and 109 deletions

View File

@ -49,7 +49,15 @@
"discountOptions": { "discountOptions": {
"assortSize": 50, "assortSize": 50,
"itemPriceMult": 0.8, "itemPriceMult": 0.8,
"presetPriceMult": 1.2 "presetPriceMult": 1.2,
"weaponPresetMinMax": {
"min": 12,
"max": 19
},
"equipmentPresetMinMax": {
"min": 8,
"max": 15
}
}, },
"partialRefreshTimeSeconds": 240, "partialRefreshTimeSeconds": 240,
"partialRefreshChangePercent": 15, "partialRefreshChangePercent": 15,

View File

@ -60,4 +60,6 @@ export interface DiscountOptions
assortSize: number; assortSize: number;
itemPriceMult: number; itemPriceMult: number;
presetPriceMult: number; presetPriceMult: number;
weaponPresetMinMax: MinMax;
equipmentPresetMinMax: MinMax;
} }

View File

@ -0,0 +1,12 @@
export interface IFenceAssortGenerationValues
{
normal: IGenerationAssortValues;
discount: IGenerationAssortValues;
}
export interface IGenerationAssortValues
{
item: number;
weaponPreset: number;
equipmentPreset: number;
}

View File

@ -13,12 +13,14 @@ import { BaseClasses } from "@spt-aki/models/enums/BaseClasses";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { Traders } from "@spt-aki/models/enums/Traders"; import { Traders } from "@spt-aki/models/enums/Traders";
import { ITraderConfig } from "@spt-aki/models/spt/config/ITraderConfig"; import { ITraderConfig } from "@spt-aki/models/spt/config/ITraderConfig";
import {
IFenceAssortGenerationValues,
IGenerationAssortValues,
} from "@spt-aki/models/spt/fence/IFenceAssortGenerationValues";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { ConfigServer } from "@spt-aki/servers/ConfigServer"; import { ConfigServer } from "@spt-aki/servers/ConfigServer";
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer"; import { DatabaseServer } from "@spt-aki/servers/DatabaseServer";
import { ItemFilterService } from "@spt-aki/services/ItemFilterService";
import { LocalisationService } from "@spt-aki/services/LocalisationService"; import { LocalisationService } from "@spt-aki/services/LocalisationService";
import { HashUtil } from "@spt-aki/utils/HashUtil";
import { JsonUtil } from "@spt-aki/utils/JsonUtil"; import { JsonUtil } from "@spt-aki/utils/JsonUtil";
import { RandomUtil } from "@spt-aki/utils/RandomUtil"; import { RandomUtil } from "@spt-aki/utils/RandomUtil";
import { TimeUtil } from "@spt-aki/utils/TimeUtil"; import { TimeUtil } from "@spt-aki/utils/TimeUtil";
@ -30,16 +32,22 @@ import { TimeUtil } from "@spt-aki/utils/TimeUtil";
@injectable() @injectable()
export class FenceService export class FenceService
{ {
protected traderConfig: ITraderConfig;
/** Time when some items in assort will be replaced */
protected nextPartialRefreshTimestamp: number;
/** Main assorts you see at all rep levels */ /** Main assorts you see at all rep levels */
protected fenceAssort: ITraderAssort = undefined; protected fenceAssort: ITraderAssort = undefined;
/** Assorts shown on a separate tab when you max out fence rep */ /** Assorts shown on a separate tab when you max out fence rep */
protected fenceDiscountAssort: ITraderAssort = undefined; protected fenceDiscountAssort: ITraderAssort = undefined;
protected traderConfig: ITraderConfig;
protected nextMiniRefreshTimestamp: number; /** Hydrated on initial assort generation as part of generateFenceAssorts() */
protected desiredAssortCounts: IFenceAssortGenerationValues;
constructor( constructor(
@inject("WinstonLogger") protected logger: ILogger, @inject("WinstonLogger") protected logger: ILogger,
@inject("HashUtil") protected hashUtil: HashUtil,
@inject("JsonUtil") protected jsonUtil: JsonUtil, @inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("TimeUtil") protected timeUtil: TimeUtil, @inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("RandomUtil") protected randomUtil: RandomUtil, @inject("RandomUtil") protected randomUtil: RandomUtil,
@ -47,7 +55,6 @@ export class FenceService
@inject("HandbookHelper") protected handbookHelper: HandbookHelper, @inject("HandbookHelper") protected handbookHelper: HandbookHelper,
@inject("ItemHelper") protected itemHelper: ItemHelper, @inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("PresetHelper") protected presetHelper: PresetHelper, @inject("PresetHelper") protected presetHelper: PresetHelper,
@inject("ItemFilterService") protected itemFilterService: ItemFilterService,
@inject("LocalisationService") protected localisationService: LocalisationService, @inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ConfigServer") protected configServer: ConfigServer, @inject("ConfigServer") protected configServer: ConfigServer,
) )
@ -214,7 +221,7 @@ export class FenceService
*/ */
public needsPartialRefresh(): boolean public needsPartialRefresh(): boolean
{ {
return this.timeUtil.getTimestamp() > this.nextMiniRefreshTimestamp; return this.timeUtil.getTimestamp() > this.nextPartialRefreshTimestamp;
} }
/** /**
@ -222,40 +229,24 @@ export class FenceService
*/ */
public performPartialRefresh(): void public performPartialRefresh(): void
{ {
let itemCountToReplace = this.getCountOfItemsToReplace(this.traderConfig.fence.assortSize); const itemCountToReplace = this.getCountOfItemsToReplace(this.traderConfig.fence.assortSize);
const discountItemCountToReplace = this.getCountOfItemsToReplace( const discountItemCountToReplace = this.getCountOfItemsToReplace(
this.traderConfig.fence.discountOptions.assortSize, this.traderConfig.fence.discountOptions.assortSize,
); );
// Iterate x times to remove items (only remove if assort has items) // Simulate players buying items
if (this.fenceAssort?.items?.length > 0) this.deleteRandomAssorts(itemCountToReplace, this.fenceAssort);
{ this.deleteRandomAssorts(discountItemCountToReplace, this.fenceDiscountAssort);
const rootItems = this.fenceAssort.items.filter((item) => item.slotId === "hideout");
for (let index = 0; index < itemCountToReplace; index++)
{
this.removeRandomItemFromAssorts(this.fenceAssort, rootItems);
}
}
// Iterate x times to remove items (only remove if assort has items) // Get count of what item pools need new items (item/weapon/equipment)
if (this.fenceDiscountAssort?.items?.length > 0) const itemCountsToReplace = this.getCountOfItemsToGenerate();
{
const rootItems = this.fenceDiscountAssort.items.filter((item) => item.slotId === "hideout");
for (let index = 0; index < discountItemCountToReplace; index++)
{
this.removeRandomItemFromAssorts(this.fenceDiscountAssort, rootItems);
}
}
itemCountToReplace = this.getCountOfItemsToGenerate(itemCountToReplace);
const newItems = this.createFenceAssortSkeleton(); const newItems = this.createFenceAssortSkeleton();
const newDiscountItems = this.createFenceAssortSkeleton(); this.createAssorts(itemCountsToReplace.normal, newItems, 2);
this.createAssorts(itemCountToReplace, newItems, 1);
this.createAssorts(discountItemCountToReplace, newDiscountItems, 2);
// Add new items to fence assorts
this.fenceAssort.items.push(...newItems.items); this.fenceAssort.items.push(...newItems.items);
const newDiscountItems = this.createFenceAssortSkeleton();
this.createAssorts(itemCountsToReplace.discount, newDiscountItems, 2);
this.fenceDiscountAssort.items.push(...newDiscountItems.items); this.fenceDiscountAssort.items.push(...newDiscountItems.items);
// Add new barter items to fence barter scheme // Add new barter items to fence barter scheme
@ -283,6 +274,7 @@ export class FenceService
newDiscountItems.loyal_level_items[loyaltyItemKey]; newDiscountItems.loyal_level_items[loyaltyItemKey];
} }
// Reset the clock
this.incrementPartialRefreshTime(); this.incrementPartialRefreshTime();
} }
@ -291,7 +283,7 @@ export class FenceService
*/ */
protected incrementPartialRefreshTime(): void protected incrementPartialRefreshTime(): void
{ {
this.nextMiniRefreshTimestamp = this.timeUtil.getTimestamp() this.nextPartialRefreshTimestamp = this.timeUtil.getTimestamp()
+ this.traderConfig.fence.partialRefreshTimeSeconds; + this.traderConfig.fence.partialRefreshTimeSeconds;
} }
@ -301,17 +293,97 @@ export class FenceService
* @param existingItemCountToReplace count of items to generate * @param existingItemCountToReplace count of items to generate
* @returns number of items to generate * @returns number of items to generate
*/ */
protected getCountOfItemsToGenerate(existingItemCountToReplace: number): number protected getCountOfItemsToGenerate(): IFenceAssortGenerationValues
{ {
const desiredTotalCount = this.traderConfig.fence.assortSize; const currentItemAssortCount = Object.keys(this.fenceAssort.loyal_level_items).length;
const actualTotalCount = this.fenceAssort.items.reduce((count, item) =>
const rootPresetItems = this.fenceAssort.items.filter((item) =>
item.slotId === "hideout" && item.upd.sptPresetId
);
// Get count of weapons
const currentWeaponPresetCount = rootPresetItems.reduce((count, item) =>
{ {
return item.slotId === "hideout" ? count + 1 : count; return this.itemHelper.isOfBaseclass(item._tpl, BaseClasses.WEAPON) ? count + 1 : count;
}, 0); }, 0);
return actualTotalCount < desiredTotalCount // Get count of equipment
? (desiredTotalCount - actualTotalCount) + existingItemCountToReplace const currentEquipmentPresetCount = rootPresetItems.reduce((count, item) =>
: existingItemCountToReplace; {
return this.itemHelper.armorItemCanHoldMods(item._tpl) ? count + 1 : count;
}, 0);
const itemCountToGenerate = Math.max(this.desiredAssortCounts.normal.item - currentItemAssortCount, 0);
const weaponCountToGenerate = Math.max(
this.desiredAssortCounts.normal.weaponPreset - currentWeaponPresetCount,
0,
);
const equipmentCountToGenerate = Math.max(
this.desiredAssortCounts.normal.equipmentPreset - currentEquipmentPresetCount,
0,
);
const normalValues: IGenerationAssortValues = {
item: itemCountToGenerate,
weaponPreset: weaponCountToGenerate,
equipmentPreset: equipmentCountToGenerate,
};
// Discount tab handling
const currentDiscountItemAssortCount = Object.keys(this.fenceDiscountAssort.loyal_level_items).length;
const rootDiscountPresetItems = this.fenceDiscountAssort.items.filter((item) =>
item.slotId === "hideout" && item.upd.sptPresetId
);
// Get count of weapons
const currentDiscountWeaponPresetCount = rootDiscountPresetItems.reduce((count, item) =>
{
return this.itemHelper.isOfBaseclass(item._tpl, BaseClasses.WEAPON) ? count + 1 : count;
}, 0);
// Get count of equipment
const currentDiscountEquipmentPresetCount = rootDiscountPresetItems.reduce((count, item) =>
{
return this.itemHelper.armorItemCanHoldMods(item._tpl) ? count + 1 : count;
}, 0);
const itemDiscountCountToGenerate = Math.max(
this.desiredAssortCounts.discount.item - currentDiscountItemAssortCount,
0,
);
const weaponDiscountCountToGenerate = Math.max(
this.desiredAssortCounts.discount.weaponPreset - currentDiscountWeaponPresetCount,
0,
);
const equipmentDiscountCountToGenerate = Math.max(
this.desiredAssortCounts.discount.equipmentPreset - currentDiscountEquipmentPresetCount,
0,
);
const discountValues: IGenerationAssortValues = {
item: itemDiscountCountToGenerate,
weaponPreset: weaponDiscountCountToGenerate,
equipmentPreset: equipmentDiscountCountToGenerate,
};
return { normal: normalValues, discount: discountValues };
}
/**
* Delete desired number of items from assort (including children)
* @param itemCountToReplace
* @param discountItemCountToReplace
*/
protected deleteRandomAssorts(itemCountToReplace: number, assort: ITraderAssort): void
{
if (assort?.items?.length > 0)
{
const rootItems = assort.items.filter((item) => item.slotId === "hideout");
for (let index = 0; index < itemCountToReplace; index++)
{
this.removeRandomItemFromAssorts(assort, rootItems);
}
}
} }
/** /**
@ -368,19 +440,60 @@ export class FenceService
// Reset refresh time now assorts are being generated // Reset refresh time now assorts are being generated
this.incrementPartialRefreshTime(); this.incrementPartialRefreshTime();
// Choose assort counts using config
this.createInitialFenceAssortGenerationValues();
// Create basic fence assort // Create basic fence assort
const assorts = this.createFenceAssortSkeleton(); const assorts = this.createFenceAssortSkeleton();
this.createAssorts(this.traderConfig.fence.assortSize, assorts, 1); this.createAssorts(this.desiredAssortCounts.normal, assorts, 1);
// Store in this.fenceAssort // Store in this.fenceAssort
this.setFenceAssort(assorts); this.setFenceAssort(assorts);
// Create level 2 assorts accessible at rep level 6 // Create level 2 assorts accessible at rep level 6
const discountAssorts = this.createFenceAssortSkeleton(); const discountAssorts = this.createFenceAssortSkeleton();
this.createAssorts(this.traderConfig.fence.discountOptions.assortSize, discountAssorts, 2); this.createAssorts(this.desiredAssortCounts.discount, discountAssorts, 2);
// Store in this.fenceDiscountAssort // Store in this.fenceDiscountAssort
this.setFenceDiscountAssort(discountAssorts); this.setFenceDiscountAssort(discountAssorts);
} }
/**
* Create object that contains calculated fence assort item values to make based on config
* Stored in this.desiredAssortCounts
*/
protected createInitialFenceAssortGenerationValues(): void
{
const result: IFenceAssortGenerationValues = {
normal: { item: 0, weaponPreset: 0, equipmentPreset: 0 },
discount: { item: 0, weaponPreset: 0, equipmentPreset: 0 },
};
result.normal.item = this.traderConfig.fence.assortSize;
result.normal.weaponPreset = this.randomUtil.getInt(
this.traderConfig.fence.weaponPresetMinMax.min,
this.traderConfig.fence.weaponPresetMinMax.max,
);
result.normal.equipmentPreset = this.randomUtil.getInt(
this.traderConfig.fence.equipmentPresetMinMax.min,
this.traderConfig.fence.equipmentPresetMinMax.max,
);
result.discount.item = this.traderConfig.fence.discountOptions.assortSize;
result.discount.weaponPreset = this.randomUtil.getInt(
this.traderConfig.fence.discountOptions.weaponPresetMinMax.min,
this.traderConfig.fence.discountOptions.weaponPresetMinMax.max,
);
result.discount.equipmentPreset = this.randomUtil.getInt(
this.traderConfig.fence.discountOptions.equipmentPresetMinMax.min,
this.traderConfig.fence.discountOptions.equipmentPresetMinMax.max,
);
this.desiredAssortCounts = result;
}
/** /**
* Create skeleton to hold assort items * Create skeleton to hold assort items
* @returns ITraderAssort object * @returns ITraderAssort object
@ -400,25 +513,37 @@ export class FenceService
* @param assortCount Number of assorts to generate * @param assortCount Number of assorts to generate
* @param assorts object to add created assorts to * @param assorts object to add created assorts to
*/ */
protected createAssorts(assortCount: number, assorts: ITraderAssort, loyaltyLevel: number): void protected createAssorts(itemCounts: IGenerationAssortValues, assorts: ITraderAssort, loyaltyLevel: number): void
{ {
const baseFenceAssortClone = this.jsonUtil.clone(this.databaseServer.getTables().traders[Traders.FENCE].assort); const baseFenceAssortClone = this.jsonUtil.clone(this.databaseServer.getTables().traders[Traders.FENCE].assort);
const itemTypeCounts = this.initItemLimitCounter(this.traderConfig.fence.itemTypeLimits); const itemTypeLimitCounts = this.initItemLimitCounter(this.traderConfig.fence.itemTypeLimits);
this.addItemAssorts(assortCount, assorts, baseFenceAssortClone, itemTypeCounts, loyaltyLevel); if (itemCounts.item > 0)
{
// Add presets this.addItemAssorts(itemCounts.item, assorts, baseFenceAssortClone, itemTypeLimitCounts, loyaltyLevel);
const weaponPresetCount = this.randomUtil.getInt(
this.traderConfig.fence.weaponPresetMinMax.min,
this.traderConfig.fence.weaponPresetMinMax.max,
);
const equipmentPresetCount = this.randomUtil.getInt(
this.traderConfig.fence.equipmentPresetMinMax.min,
this.traderConfig.fence.equipmentPresetMinMax.max,
);
this.addPresetsToAssort(weaponPresetCount, equipmentPresetCount, assorts, baseFenceAssortClone, loyaltyLevel);
} }
if (itemCounts.weaponPreset > 0 || itemCounts.equipmentPreset > 0)
{
// Add presets
this.addPresetsToAssort(
itemCounts.weaponPreset,
itemCounts.equipmentPreset,
assorts,
baseFenceAssortClone,
loyaltyLevel,
);
}
}
/**
* Add item assorts to existing assort data
* @param assortCount Number to add
* @param assorts Assorts data to add to
* @param baseFenceAssort Base data to draw from
* @param itemTypeCounts
* @param loyaltyLevel Loyalty level to set new item to
*/
protected addItemAssorts( protected addItemAssorts(
assortCount: number, assortCount: number,
assorts: ITraderAssort, assorts: ITraderAssort,
@ -502,8 +627,8 @@ export class FenceService
/** /**
* Find presets in base fence assort and add desired number to 'assorts' parameter * Find presets in base fence assort and add desired number to 'assorts' parameter
* @param desiredWeaponPresetsCount * @param desiredWeaponPresetsCount
* @param assorts * @param assorts Assorts to add preset to
* @param baseFenceAssort * @param baseFenceAssort Base data to draw from
* @param loyaltyLevel Which loyalty level is required to see/buy item * @param loyaltyLevel Which loyalty level is required to see/buy item
*/ */
protected addPresetsToAssort( protected addPresetsToAssort(
@ -515,11 +640,8 @@ export class FenceService
): void ): void
{ {
let weaponPresetsAddedCount = 0; let weaponPresetsAddedCount = 0;
if (desiredWeaponPresetsCount <= 0) if (desiredWeaponPresetsCount > 0)
{ {
return;
}
const weaponPresetRootItems = baseFenceAssort.items.filter((item) => const weaponPresetRootItems = baseFenceAssort.items.filter((item) =>
item.upd?.sptPresetId && this.itemHelper.isOfBaseclass(item._tpl, BaseClasses.WEAPON) item.upd?.sptPresetId && this.itemHelper.isOfBaseclass(item._tpl, BaseClasses.WEAPON)
); );
@ -573,6 +695,7 @@ export class FenceService
weaponPresetsAddedCount++; weaponPresetsAddedCount++;
} }
}
let equipmentPresetsAddedCount = 0; let equipmentPresetsAddedCount = 0;
if (desiredEquipmentPresetsCount <= 0) if (desiredEquipmentPresetsCount <= 0)