diff --git a/project/assets/configs/trader.json b/project/assets/configs/trader.json index 91a7eb3f..116cfc0e 100644 --- a/project/assets/configs/trader.json +++ b/project/assets/configs/trader.json @@ -157,7 +157,7 @@ "543be6564bdc2df4348b4568": 0, "5448ecbe4bdc2d60728b4568": 0, "5671435f4bdc2d96058b4569": 0, - "543be5cb4bdc2deb348b4568": 3, + "543be5cb4bdc2deb348b4568": 5, "5448e53e4bdc2d60728b4567": 7 }, "preventDuplicateOffersOfCategory": [ diff --git a/project/src/models/spt/fence/ICreateFenceAssortsResult.ts b/project/src/models/spt/fence/ICreateFenceAssortsResult.ts new file mode 100644 index 00000000..b81fd1cb --- /dev/null +++ b/project/src/models/spt/fence/ICreateFenceAssortsResult.ts @@ -0,0 +1,9 @@ +import { Item } from "@spt-aki/models/eft/common/tables/IItem"; +import { IBarterScheme } from "@spt-aki/models/eft/common/tables/ITrader"; + +export interface ICreateFenceAssortsResult +{ + sptItems: Item[][]; + barter_scheme: Record; + loyal_level_items: Record; +} diff --git a/project/src/services/FenceService.ts b/project/src/services/FenceService.ts index db4f83b7..8ed5cd64 100644 --- a/project/src/services/FenceService.ts +++ b/project/src/services/FenceService.ts @@ -3,16 +3,16 @@ 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 { MinMax } from "@spt-aki/models/common/MinMax"; import { IFenceLevel } from "@spt-aki/models/eft/common/IGlobals"; import { IPmcData } from "@spt-aki/models/eft/common/IPmcData"; -import { Item, Repairable, Upd } from "@spt-aki/models/eft/common/tables/IItem"; +import { Item, Repairable } from "@spt-aki/models/eft/common/tables/IItem"; import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem"; import { IBarterScheme, ITraderAssort } from "@spt-aki/models/eft/common/tables/ITrader"; import { BaseClasses } from "@spt-aki/models/enums/BaseClasses"; import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; import { Traders } from "@spt-aki/models/enums/Traders"; import { IItemDurabilityCurrentMax, ITraderConfig } from "@spt-aki/models/spt/config/ITraderConfig"; +import { ICreateFenceAssortsResult } from "@spt-aki/models/spt/fence/ICreateFenceAssortsResult"; import { IFenceAssortGenerationValues, IGenerationAssortValues, @@ -234,13 +234,15 @@ export class FenceService // Get count of what item pools need new items (item/weapon/equipment) const itemCountsToReplace = this.getCountOfItemsToGenerate(); - const newItems = this.createFenceAssortSkeleton(); - this.createAssorts(itemCountsToReplace.normal, newItems, 1); - this.fenceAssort.items.push(...newItems.items); + const newItems = this.createAssorts(itemCountsToReplace.normal, 1); - const newDiscountItems = this.createFenceAssortSkeleton(); - this.createAssorts(itemCountsToReplace.discount, newDiscountItems, 2); - this.fenceDiscountAssort.items.push(...newDiscountItems.items); + // Push newly generated assorts into existing data + this.updateFenceAssorts(newItems, this.fenceAssort); + + const newDiscountItems = this.createAssorts(itemCountsToReplace.discount, 2); + + // Push newly generated discount assorts into existing data + this.updateFenceAssorts(newDiscountItems, this.fenceDiscountAssort); // Add new barter items to fence barter scheme for (const barterItemKey in newItems.barter_scheme) @@ -271,6 +273,46 @@ export class FenceService this.incrementPartialRefreshTime(); } + /** + * Handle the process of folding new assorts into existing assorts, when a new assort exists already, increment its StackObjectsCount instead + * @param newFenceAssorts Assorts to fold into existing fence assorts + * @param existingFenceAssorts Current fence assorts new assorts will be added to + */ + protected updateFenceAssorts(newFenceAssorts: ICreateFenceAssortsResult, existingFenceAssorts: ITraderAssort): void + { + for (const itemWithChildren of newFenceAssorts.sptItems) + { + // Find the root item + const newRootItem = itemWithChildren.find((item) => item.slotId === "hideout"); + + // Find a matching root item with same tpl in existing assort + const existingRootItem = existingFenceAssorts.items.find((item) => + item._tpl === newRootItem._tpl && item.slotId === "hideout" + ); + + // Check if same type of item exists + its on list of item types to always stack + if (existingRootItem && this.itemInPreventDupeCategoryList(newRootItem._tpl)) + { + // Guard against a missing stack count + if (!existingRootItem.upd.StackObjectsCount) + { + existingRootItem.upd.StackObjectsCount = 1; + } + + // Merge new items count into existing, dont add new loyalty/barter data as it already exists + existingRootItem.upd.StackObjectsCount += newRootItem.upd.StackObjectsCount; + + continue; + } + + // New assort to be added to existing assorts + existingFenceAssorts.items.push(...itemWithChildren); + existingFenceAssorts.barter_scheme[newRootItem._id] = newFenceAssorts.barter_scheme[newRootItem._id]; + existingFenceAssorts.loyal_level_items[newRootItem._id] = + newFenceAssorts.loyal_level_items[newRootItem._id]; + } + } + /** * Increment fence next refresh timestamp by current timestamp + partialRefreshTimeSeconds from config */ @@ -386,18 +428,26 @@ export class FenceService */ protected removeRandomItemFromAssorts(assort: ITraderAssort, rootItems: Item[]): void { - const rootItemToRemove = this.randomUtil.getArrayValue(rootItems); - - // Clean up any mods if item had them - const itemWithChildren = this.itemHelper.findAndReturnChildrenAsItems(assort.items, rootItemToRemove._id); - for (const itemToDelete of itemWithChildren) - { - // Delete item from assort items array - assort.items.splice(assort.items.indexOf(itemToDelete), 1); + const rootItemToAdjust = this.randomUtil.getArrayValue(rootItems); + const itemCountToRemove = this.randomUtil.getInt(1, rootItemToAdjust.upd.StackObjectsCount); + if (itemCountToRemove > 1 && itemCountToRemove < rootItemToAdjust.upd.StackObjectsCount) + { // More than 1 + less then full stack + // Reduce stack size but keep stack + rootItemToAdjust.upd.StackObjectsCount -= itemCountToRemove; } + else + { + // Remove up item + any mods + const itemWithChildren = this.itemHelper.findAndReturnChildrenAsItems(assort.items, rootItemToAdjust._id); + for (const itemToDelete of itemWithChildren) + { + // Delete item from assort items array + assort.items.splice(assort.items.indexOf(itemToDelete), 1); + } - delete assort.barter_scheme[rootItemToRemove._id]; - delete assort.loyal_level_items[rootItemToRemove._id]; + delete assort.barter_scheme[rootItemToAdjust._id]; + delete assort.loyal_level_items[rootItemToAdjust._id]; + } } /** @@ -437,16 +487,35 @@ export class FenceService this.createInitialFenceAssortGenerationValues(); // Create basic fence assort - const assorts = this.createFenceAssortSkeleton(); - this.createAssorts(this.desiredAssortCounts.normal, assorts, 1); + const assorts = this.createAssorts(this.desiredAssortCounts.normal, 1); + // Store in this.fenceAssort - this.setFenceAssort(assorts); + this.setFenceAssort(this.convertIntoFenceAssort(assorts)); // Create level 2 assorts accessible at rep level 6 - const discountAssorts = this.createFenceAssortSkeleton(); - this.createAssorts(this.desiredAssortCounts.discount, discountAssorts, 2); + const discountAssorts = this.createAssorts(this.desiredAssortCounts.discount, 2); + // Store in this.fenceDiscountAssort - this.setFenceDiscountAssort(discountAssorts); + this.setFenceDiscountAssort(this.convertIntoFenceAssort(discountAssorts)); + } + + /** + * Convert the intermediary assort data generated into format client can process + * @param intermediaryAssorts Generated assorts that will be converted + * @returns ITraderAssort + */ + protected convertIntoFenceAssort(intermediaryAssorts: ICreateFenceAssortsResult): ITraderAssort + { + const result = this.createFenceAssortSkeleton(); + for (const itemWithChilden of intermediaryAssorts.sptItems) + { + result.items.push(...itemWithChilden); + } + + result.barter_scheme = intermediaryAssorts.barter_scheme; + result.loyal_level_items = intermediaryAssorts.loyal_level_items; + + return result; } /** @@ -506,14 +575,22 @@ export class FenceService * @param assortCount Number of assorts to generate * @param assorts object to add created assorts to */ - protected createAssorts(itemCounts: IGenerationAssortValues, assorts: ITraderAssort, loyaltyLevel: number): void + protected createAssorts(itemCounts: IGenerationAssortValues, loyaltyLevel: number): ICreateFenceAssortsResult { + const result: ICreateFenceAssortsResult = { sptItems: [], barter_scheme: {}, loyal_level_items: {} }; + const baseFenceAssortClone = this.jsonUtil.clone(this.databaseServer.getTables().traders[Traders.FENCE].assort); const itemTypeLimitCounts = this.initItemLimitCounter(this.traderConfig.fence.itemTypeLimits); if (itemCounts.item > 0) { - this.addItemAssorts(itemCounts.item, assorts, baseFenceAssortClone, itemTypeLimitCounts, loyaltyLevel); + const itemResult = this.addItemAssorts( + itemCounts.item, + result, + baseFenceAssortClone, + itemTypeLimitCounts, + loyaltyLevel, + ); } if (itemCounts.weaponPreset > 0 || itemCounts.equipmentPreset > 0) @@ -522,11 +599,13 @@ export class FenceService this.addPresetsToAssort( itemCounts.weaponPreset, itemCounts.equipmentPreset, - assorts, + result, baseFenceAssortClone, loyaltyLevel, ); } + + return result; } /** @@ -539,15 +618,15 @@ export class FenceService */ protected addItemAssorts( assortCount: number, - assorts: ITraderAssort, + assorts: ICreateFenceAssortsResult, baseFenceAssortClone: ITraderAssort, itemTypeLimits: Record, loyaltyLevel: number, ): void { const priceLimits = this.traderConfig.fence.itemCategoryRoublePriceLimit; - const assortRootItems = baseFenceAssortClone.items.filter((x) => - x.parentId === "hideout" && !x.upd?.sptPresetId + const assortRootItems = baseFenceAssortClone.items.filter((item) => + item.parentId === "hideout" && !item.upd?.sptPresetId ); for (let i = 0; i < assortCount; i++) @@ -614,7 +693,7 @@ export class FenceService } // Skip items already in the assort if it exists in the prevent duplicate list - const existingItemThatMatches = this.getMatchingItem(rootItemBeingAdded, itemDbDetails, assorts.items); + const existingItemThatMatches = this.getMatchingItem(rootItemBeingAdded, itemDbDetails, assorts.sptItems); const shouldBeStacked = this.itemShouldBeForceStacked(existingItemThatMatches, itemDbDetails); if (shouldBeStacked && existingItemThatMatches) { // Decrement loop counter so another items gets added @@ -630,7 +709,7 @@ export class FenceService this.randomiseArmorModDurability(desiredAssortItemAndChildrenClone, itemDbDetails); } - assorts.items.push(...desiredAssortItemAndChildrenClone); + assorts.sptItems.push(desiredAssortItemAndChildrenClone); assorts.barter_scheme[rootItemBeingAdded._id] = this.jsonUtil.clone( baseFenceAssortClone.barter_scheme[chosenBaseAssortRoot._id], @@ -651,15 +730,15 @@ export class FenceService * e.g. salewa hp resource units left * @param rootItemBeingAdded item to look for a match against * @param itemDbDetails Db details of matching item - * @param fenceItemAssorts Items to search through + * @param itemsWithChildren Items to search through * @returns Matching assort item */ - protected getMatchingItem(rootItemBeingAdded: Item, itemDbDetails: ITemplateItem, fenceItemAssorts: Item[]): Item + protected getMatchingItem(rootItemBeingAdded: Item, itemDbDetails: ITemplateItem, itemsWithChildren: Item[][]): Item { // Get matching root items - const matchingItems = fenceItemAssorts.filter((item) => - item._tpl === rootItemBeingAdded._tpl && item.parentId === "hideout" - ); + const matchingItems = itemsWithChildren.filter((itemWithChildren) => + itemWithChildren.find((item) => item._tpl === rootItemBeingAdded._tpl && item.parentId === "hideout") + ).flatMap((x) => x); if (matchingItems.length === 0) { // Nothing matches by tpl and is root item, exit early @@ -726,11 +805,13 @@ export class FenceService return false; } + return this.itemInPreventDupeCategoryList(itemDbDetails._id); + } + + protected itemInPreventDupeCategoryList(tpl: string): boolean + { // Item type in config list - return this.itemHelper.isOfBaseclasses( - itemDbDetails._id, - this.traderConfig.fence.preventDuplicateOffersOfCategory, - ); + return this.itemHelper.isOfBaseclasses(tpl, this.traderConfig.fence.preventDuplicateOffersOfCategory); } /** @@ -799,7 +880,7 @@ export class FenceService protected addPresetsToAssort( desiredWeaponPresetsCount: number, desiredEquipmentPresetsCount: number, - assorts: ITraderAssort, + assorts: ICreateFenceAssortsResult, baseFenceAssort: ITraderAssort, loyaltyLevel: number, ): void @@ -848,7 +929,7 @@ export class FenceService // Remapping IDs causes parentid to be altered presetWithChildrenClone[0].parentId = "hideout"; - assorts.items.push(...presetWithChildrenClone); + assorts.sptItems.push(presetWithChildrenClone); // Set assort price // Must be careful to use correct id as the item has had its IDs regenerated @@ -908,7 +989,7 @@ export class FenceService // Remapping IDs causes parentid to be altered presetWithChildrenClone[0].parentId = "hideout"; - assorts.items.push(...presetWithChildrenClone); + assorts.sptItems.push(presetWithChildrenClone); // Set assort price // Must be careful to use correct id as the item has had its IDs regenerated