From b84be5258409a241e1930cf3459e32b342a6f6bf Mon Sep 17 00:00:00 2001 From: Dev Date: Tue, 1 Aug 2023 17:36:55 +0100 Subject: [PATCH] Update example trader mod to use fluent api approach to adding assorts --- .../src/fluentTraderAssortCreator.ts | 175 ++++++++++++++++++ TypeScript/13AddTrader/src/mod.ts | 47 ++++- TypeScript/13AddTrader/src/traderHelpers.ts | 149 +-------------- .../types/controllers/GameController.d.ts | 11 +- 4 files changed, 230 insertions(+), 152 deletions(-) create mode 100644 TypeScript/13AddTrader/src/fluentTraderAssortCreator.ts diff --git a/TypeScript/13AddTrader/src/fluentTraderAssortCreator.ts b/TypeScript/13AddTrader/src/fluentTraderAssortCreator.ts new file mode 100644 index 0000000..07e8155 --- /dev/null +++ b/TypeScript/13AddTrader/src/fluentTraderAssortCreator.ts @@ -0,0 +1,175 @@ +import { Item } from "@spt-aki/models/eft/common/tables/IItem"; +import { IBarterScheme, ITrader } from "@spt-aki/models/eft/common/tables/ITrader"; +import { Money } from "@spt-aki/models/enums/Money"; +import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; +import { HashUtil } from "@spt-aki/utils/HashUtil"; + +export class FluentAssortConstructor +{ + protected itemsToSell: Item[] = []; + protected barterScheme: Record = {}; + protected loyaltyLevel: Record = {}; + protected hashUtil: HashUtil; + protected logger: ILogger; + + constructor(hashutil: HashUtil, logger: ILogger) + { + this.hashUtil = hashutil + this.logger = logger; + } + + /** + * Start selling item with tpl + * @param itemTpl Tpl id of the item you want trader to sell + * @param itemId Optional - set your own Id, otherwise unique id will be generated + */ + public createSingleAssortItem(itemTpl: string, itemId = undefined): FluentAssortConstructor + { + // Create item ready for insertion into assort table + const newItemToAdd: Item = { + _id: !itemId ? this.hashUtil.generate(): itemId, + _tpl: itemTpl, + parentId: "hideout", // Should always be "hideout" + slotId: "hideout", // Should always be "hideout" + upd: { + UnlimitedCount: false, + StackObjectsCount: 100 + } + }; + + this.itemsToSell.push(newItemToAdd); + + return this; + } + + public createComplexAssortItem(items: Item[]): FluentAssortConstructor + { + items[0].parentId = "hideout"; + items[0].slotId = "hideout"; + + if (!items[0].upd) + { + items[0].upd = {} + } + + items[0].upd.UnlimitedCount = false; + items[0].upd.StackObjectsCount = 100; + + this.itemsToSell.push(...items); + + return this; + } + + public addStackCount(stackCount: number): FluentAssortConstructor + { + this.itemsToSell[0].upd.StackObjectsCount = stackCount; + + return this; + } + + public addUnlimitedStackCount(): FluentAssortConstructor + { + this.itemsToSell[0].upd.StackObjectsCount = 999999; + this.itemsToSell[0].upd.UnlimitedCount = true; + + return this; + } + + public makeStackCountUnlimited(): FluentAssortConstructor + { + this.itemsToSell[0].upd.StackObjectsCount = 999999; + + return this; + } + + public addBuyRestriction(maxBuyLimit: number): FluentAssortConstructor + { + this.itemsToSell[0].upd.BuyRestrictionMax = maxBuyLimit; + this.itemsToSell[0].upd.BuyRestrictionCurrent = 0; + + return this; + } + + public addLoyaltyLevel(level: number) + { + this.loyaltyLevel[this.itemsToSell[0]._id] = level; + + return this; + } + + public addMoneyCost(currencyType: Money, amount: number): FluentAssortConstructor + { + this.barterScheme[this.itemsToSell[0]._id] = [ + [ + { + count: amount, + _tpl: currencyType + } + ] + ]; + + return this; + } + + public addBarterCost(itemTpl: string, count: number): FluentAssortConstructor + { + const sellableItemId = this.itemsToSell[0]._id; + + // No data at all, create + if (Object.keys(this.barterScheme).length === 0) + { + this.barterScheme[sellableItemId] = [[ + { + count: count, + _tpl: itemTpl + } + ]]; + } + else + { + // Item already exists, add to + const existingData = this.barterScheme[sellableItemId][0].find(x => x._tpl === itemTpl); + if (existingData) + { + // itemtpl already a barter for item, add to count + existingData.count+= count; + } + else + { + // No barter for item, add it fresh + this.barterScheme[sellableItemId][0].push({ + count: count, + _tpl: itemTpl + }) + } + + } + + return this; + } + + /** + * Reset objet ready for reuse + * @returns + */ + public export(data: ITrader): FluentAssortConstructor + { + const itemBeingSoldId = this.itemsToSell[0]._id; + if (data.assort.items.find(x => x._id === itemBeingSoldId)) + { + this.logger.error(`Unable to add complex item with item key ${this.itemsToSell[0]._id}, key already used`); + + return; + } + + data.assort.items.push(...this.itemsToSell); + data.assort.barter_scheme[itemBeingSoldId] = this.barterScheme[itemBeingSoldId]; + data.assort.loyal_level_items[itemBeingSoldId] = this.loyaltyLevel[itemBeingSoldId]; + + this.itemsToSell = []; + this.barterScheme = {}; + this.loyaltyLevel = {}; + + return this; + } +} \ No newline at end of file diff --git a/TypeScript/13AddTrader/src/mod.ts b/TypeScript/13AddTrader/src/mod.ts index 7c973d5..c8270f3 100644 --- a/TypeScript/13AddTrader/src/mod.ts +++ b/TypeScript/13AddTrader/src/mod.ts @@ -15,12 +15,16 @@ import { JsonUtil } from "@spt-aki/utils/JsonUtil"; // New trader settings import * as baseJson from "../db/base.json"; import { TraderHelper } from "./traderHelpers"; +import { FluentAssortConstructor } from "./fluentTraderAssortCreator"; +import { Money } from "@spt-aki/models/enums/Money"; +import { HashUtil } from "@spt-aki/utils/HashUtil"; class SampleTrader implements IPreAkiLoadMod, IPostDBLoadMod { private mod: string private logger: ILogger private traderHeper: TraderHelper + private fluentTraderAssortHeper: FluentAssortConstructor constructor() { this.mod = "13AddTrader"; // Set name of mod so we can log it to console later @@ -39,11 +43,13 @@ class SampleTrader implements IPreAkiLoadMod, IPostDBLoadMod // Get SPT code/data we need later const preAkiModLoader: PreAkiModLoader = container.resolve("PreAkiModLoader"); const imageRouter: ImageRouter = container.resolve("ImageRouter"); + const hashUtil: HashUtil = container.resolve("HashUtil"); const configServer = container.resolve("ConfigServer"); const traderConfig: ITraderConfig = configServer.getConfig(ConfigTypes.TRADER); // Create helper class and use it to register our traders image/icon + set its stock refresh time this.traderHeper = new TraderHelper(); + this.fluentTraderAssortHeper = new FluentAssortConstructor(hashUtil, this.logger); this.traderHeper.registerProfileImage(baseJson, this.mod, preAkiModLoader, imageRouter, "cat.jpg"); this.traderHeper.setTraderUpdateTime(traderConfig, baseJson, 3600); @@ -69,11 +75,48 @@ class SampleTrader implements IPreAkiLoadMod, IPostDBLoadMod // Add new trader to the trader dictionary in DatabaseServer - has no assorts (items) yet this.traderHeper.addTraderToDb(baseJson, tables, jsonUtil); + // Add milk + const MILK_ID = "575146b724597720a27126d5"; // Can find item ids in `database\templates\items.json` or with https://db.sp-tarkov.com/search + this.fluentTraderAssortHeper.createSingleAssortItem(MILK_ID) + .addStackCount(200) + .addBuyRestriction(10) + .addMoneyCost(Money.ROUBLES, 2000) + .addLoyaltyLevel(1) + .export(tables.traders[baseJson._id]); + + // Add 3x bitcoin + salewa for milk barter + const BITCOIN_ID = "59faff1d86f7746c51718c9c" + const SALEWA_ID = "544fb45d4bdc2dee738b4568"; + this.fluentTraderAssortHeper.createSingleAssortItem(MILK_ID) + .addStackCount(100) + .addBarterCost(BITCOIN_ID, 3) + .addBarterCost(SALEWA_ID, 1) + .addLoyaltyLevel(1) + .export(tables.traders[baseJson._id]); + + + // Add glock as money purchase + this.fluentTraderAssortHeper.createComplexAssortItem(this.traderHeper.createGlock()) + .addUnlimitedStackCount() + .addMoneyCost(Money.ROUBLES, 20000) + .addBuyRestriction(3) + .addLoyaltyLevel(1) + .export(tables.traders[baseJson._id]); + + // Add mp133 preset as mayo barter + this.fluentTraderAssortHeper.createComplexAssortItem(tables.globals.ItemPresets["584148f2245977598f1ad387"]._items) + .addStackCount(200) + .addBarterCost("5bc9b156d4351e00367fbce9", 1) + .addBuyRestriction(3) + .addLoyaltyLevel(1) + .export(tables.traders[baseJson._id]); + + // Add some singular items to trader (items without sub-items e.g. milk/bandage) - this.traderHeper.addSingleItemsToTrader(tables, baseJson._id); + //this.traderHeper.addSingleItemsToTrader(tables, baseJson._id); // Add more complex items to trader (items with sub-items, e.g. guns) - this.traderHeper.addComplexItemsToTrader(tables, baseJson._id, jsonUtil); + //this.traderHeper.addComplexItemsToTrader(tables, baseJson._id, jsonUtil); // Add trader to locale file, ensures trader text shows properly on screen // WARNING: adds the same text to ALL locales (e.g. chinese/french/english) diff --git a/TypeScript/13AddTrader/src/traderHelpers.ts b/TypeScript/13AddTrader/src/traderHelpers.ts index b65e3c1..31b6d1b 100644 --- a/TypeScript/13AddTrader/src/traderHelpers.ts +++ b/TypeScript/13AddTrader/src/traderHelpers.ts @@ -1,7 +1,6 @@ import { PreAkiModLoader } from "@spt-aki/loaders/PreAkiModLoader"; import { Item } from "@spt-aki/models/eft/common/tables/IItem"; import { ITraderBase, ITraderAssort } from "@spt-aki/models/eft/common/tables/ITrader"; -import { Money } from "@spt-aki/models/enums/Money"; import { ITraderConfig, UpdateTime } from "@spt-aki/models/spt/config/ITraderConfig"; import { IDatabaseTables } from "@spt-aki/models/spt/server/IDatabaseTables"; import { ImageRouter } from "@spt-aki/routers/ImageRouter"; @@ -81,50 +80,11 @@ export class TraderHelper return assortTable; } - /** - * Add basic items to trader - * @param tables SPT db - * @param traderId Traders id (basejson/_id value) - */ - public addSingleItemsToTrader(tables: IDatabaseTables, traderId: string) - { - // Get the table that can hold our new items - const traderAssortTable = tables.traders[traderId].assort - - // Add milk, unlimited stock for 1 rouble with no buy restrictions - const MILK_ID = "575146b724597720a27126d5"; // Can find item ids in `database\templates\items.json` or with https://db.sp-tarkov.com/search - this.addSingleItemToAssort(traderAssortTable, MILK_ID, true, 9999999, 1, Money.ROUBLES, 1, false, 0); - - // Add salewa with 50 stock for 500 dollars + buy restriction of 2 per refresh - const SALEWA_ID = "544fb45d4bdc2dee738b4568"; - this.addSingleItemToAssort(traderAssortTable, SALEWA_ID, false, 50, 1, Money.DOLLARS, 500, true, 2); - } - - /** - * Add items with sub items to trader - * @param tables SPT db - * @param traderId Traders id (basejson/_id value) - * @param jsonUtil SPT JSON utility class - */ - public addComplexItemsToTrader(tables: IDatabaseTables, traderId: string, jsonUtil: JsonUtil) - { - // Get the table that can hold our new items - const traderAssortTable = tables.traders[traderId].assort - - // Get the mp133 preset and add to the traders assort (Could make your own Items[] array, doesn't have to be from presets) - const mp133GunPreset = tables.globals.ItemPresets["584148f2245977598f1ad387"]._items; - this.addItemWithSubItemsToAssort(jsonUtil, traderAssortTable, mp133GunPreset, false, 5, 1, Money.ROUBLES, 500, false, 0); - - // Create a pistol with some mods + add to trader - const customGlock17 = this.createGlock(); - this.addItemWithSubItemsToAssort(jsonUtil, traderAssortTable, customGlock17, true, 69, 1, Money.EUROS, 5, true, 2); - } - /** * Create a weapon from scratch, ready to be added to trader * @returns Item[] */ - private createGlock(): Item[] + public createGlock(): Item[] { // Create an array ready to hold weapon + all mods const glock: Item[] = []; @@ -194,113 +154,6 @@ export class TraderHelper return glock; } - /** - * Add item to assortTable + barter scheme + loyalty level objects - * @param assortTable Trader assorts to add item to - * @param itemTpl Items tpl to add to traders assort - * @param unlimitedCount Can an unlimited number of this item be purchased from trader - * @param stackCount Total size of item stack trader sells - * @param loyaltyLevel Loyalty level item can be purchased at - * @param currencyType What currency does item sell for - * @param currencyValue Amount of currency item can be purchased for - * @param hasBuyRestriction Does the item have a max purchase amount - * @param buyRestrictionMax How many times can item be purchased per trader refresh - */ - private addSingleItemToAssort(assortTable: ITraderAssort, itemTpl: string, unlimitedCount: boolean, stackCount: number, loyaltyLevel: number, currencyType: Money, currencyValue: number, hasBuyRestriction: boolean, buyRestrictionMax: number) - { - // Create item ready for insertion into assort table - const newItemToAdd: Item = { - _id: itemTpl, - _tpl: itemTpl, - parentId: "hideout", // Should always be "hideout" - slotId: "hideout", // Should always be "hideout" - upd: { - UnlimitedCount: unlimitedCount, - StackObjectsCount: stackCount - } - }; - - // Items can have a buy restriction per trader refresh cycle, optional - if(hasBuyRestriction) - { - newItemToAdd.upd.BuyRestrictionMax = buyRestrictionMax; - newItemToAdd.upd.BuyRestrictionCurrent = 0; - } - - assortTable.items.push(newItemToAdd); - - // Barter scheme holds the cost of the item + the currency needed (doesnt need to be currency, can be any item, this is how barter traders are made) - assortTable.barter_scheme[itemTpl] = [ - [ - { - count: currencyValue, - _tpl: currencyType - } - ] - ]; - - // Set loyalty level needed to unlock item - assortTable.loyal_level_items[itemTpl] = loyaltyLevel; - } - - /** - * Add a complex item to trader assort (item with child items) - * @param assortTable trader assorts to add items to - * @param jsonUtil JSON utility class - * @param items Items array to add to assort - * @param unlimitedCount Can an unlimited number of this item be purchased from trader - * @param stackCount Total size of item stack trader sells - * @param loyaltyLevel Loyalty level item can be purchased at - * @param currencyType What currency does item sell for - * @param currencyValue Amount of currency item can be purchased for - * @param hasBuyRestriction Does the item have a max purchase amount - * @param buyRestrictionMax How many times can item be purchased per trader refresh - */ - private addItemWithSubItemsToAssort(jsonUtil: JsonUtil, assortTable: ITraderAssort, items: Item[], unlimitedCount: boolean, stackCount: number, loyaltyLevel: number, currencyType: Money, currencyValue: number, hasBuyRestriction: boolean, buyRestrictionMax: number): void - { - // Deserialize and serialize to ensure we dont alter the original data (clone it) - const collectionToAdd: Item[] = jsonUtil.deserialize(jsonUtil.serialize(items)); - - // Create upd object if its missing - if (!collectionToAdd[0].upd) - { - collectionToAdd[0].upd = {}; - } - - // Update item base with values needed to make item sellable by trader - collectionToAdd[0].upd = { - UnlimitedCount: unlimitedCount, - StackObjectsCount: stackCount - } - - // Items can have a buy restriction per trader refresh cycle, optional - if(hasBuyRestriction) - { - collectionToAdd[0].upd.BuyRestrictionMax = buyRestrictionMax; - collectionToAdd[0].upd.BuyRestrictionCurrent = 0; - } - - // First item should always have both properties set to 'hideout' - collectionToAdd[0].parentId = "hideout"; - collectionToAdd[0].slotId = "hideout"; - - // Push all the items into the traders assort table - assortTable.items.push(...collectionToAdd); - - // Barter scheme holds the cost of the item + the currency needed (doesnt need to be currency, can be any item, this is how barter traders are made) - assortTable.barter_scheme[collectionToAdd[0]._id] = [ - [ - { - count: currencyValue, - _tpl: currencyType - } - ] - ]; - - // Set loyalty level needed to unlock item - assortTable.loyal_level_items[collectionToAdd[0]._id] = loyaltyLevel; - } - /** * Add traders name/location/description to the locale table * @param baseJson json file for trader (db/base.json) diff --git a/TypeScript/13AddTrader/types/controllers/GameController.d.ts b/TypeScript/13AddTrader/types/controllers/GameController.d.ts index 6d0ce18..9a131a8 100644 --- a/TypeScript/13AddTrader/types/controllers/GameController.d.ts +++ b/TypeScript/13AddTrader/types/controllers/GameController.d.ts @@ -11,9 +11,11 @@ import { IGameConfigResponse } from "../models/eft/game/IGameConfigResponse"; import { IGameKeepAliveResponse } from "../models/eft/game/IGameKeepAliveResponse"; import { IServerDetails } from "../models/eft/game/IServerDetails"; import { IAkiProfile } from "../models/eft/profile/IAkiProfile"; +import { IBotConfig } from "../models/spt/config/IBotConfig"; import { ICoreConfig } from "../models/spt/config/ICoreConfig"; import { IHttpConfig } from "../models/spt/config/IHttpConfig"; import { ILocationConfig } from "../models/spt/config/ILocationConfig"; +import { IRagfairConfig } from "../models/spt/config/IRagfairConfig"; import { ILogger } from "../models/spt/utils/ILogger"; import { ConfigServer } from "../servers/ConfigServer"; import { DatabaseServer } from "../servers/DatabaseServer"; @@ -25,6 +27,7 @@ import { ProfileFixerService } from "../services/ProfileFixerService"; import { SeasonalEventService } from "../services/SeasonalEventService"; import { EncodingUtil } from "../utils/EncodingUtil"; import { JsonUtil } from "../utils/JsonUtil"; +import { RandomUtil } from "../utils/RandomUtil"; import { TimeUtil } from "../utils/TimeUtil"; export declare class GameController { protected logger: ILogger; @@ -33,6 +36,7 @@ export declare class GameController { protected timeUtil: TimeUtil; protected preAkiModLoader: PreAkiModLoader; protected httpServerHelper: HttpServerHelper; + protected randomUtil: RandomUtil; protected encodingUtil: EncodingUtil; protected hideoutHelper: HideoutHelper; protected profileHelper: ProfileHelper; @@ -48,7 +52,9 @@ export declare class GameController { protected httpConfig: IHttpConfig; protected coreConfig: ICoreConfig; protected locationConfig: ILocationConfig; - constructor(logger: ILogger, databaseServer: DatabaseServer, jsonUtil: JsonUtil, timeUtil: TimeUtil, preAkiModLoader: PreAkiModLoader, httpServerHelper: HttpServerHelper, encodingUtil: EncodingUtil, hideoutHelper: HideoutHelper, profileHelper: ProfileHelper, profileFixerService: ProfileFixerService, localisationService: LocalisationService, customLocationWaveService: CustomLocationWaveService, openZoneService: OpenZoneService, seasonalEventService: SeasonalEventService, giftService: GiftService, applicationContext: ApplicationContext, configServer: ConfigServer); + protected ragfairConfig: IRagfairConfig; + protected botConfig: IBotConfig; + constructor(logger: ILogger, databaseServer: DatabaseServer, jsonUtil: JsonUtil, timeUtil: TimeUtil, preAkiModLoader: PreAkiModLoader, httpServerHelper: HttpServerHelper, randomUtil: RandomUtil, encodingUtil: EncodingUtil, hideoutHelper: HideoutHelper, profileHelper: ProfileHelper, profileFixerService: ProfileFixerService, localisationService: LocalisationService, customLocationWaveService: CustomLocationWaveService, openZoneService: OpenZoneService, seasonalEventService: SeasonalEventService, giftService: GiftService, applicationContext: ApplicationContext, configServer: ConfigServer); /** * Handle client/game/start */ @@ -84,6 +90,7 @@ export declare class GameController { * @param pmcProfile Player profile */ protected warnOnActiveBotReloadSkill(pmcProfile: IPmcData): void; + protected flagAllItemsInDbAsSellableOnFlea(): void; /** * When player logs in, iterate over all active effects and reduce timer * TODO - add body part HP regen @@ -118,7 +125,7 @@ export declare class GameController { protected validateQuestAssortUnlocksExist(): void; /** * Add the logged in players name to PMC name pool - * @param pmcProfile + * @param pmcProfile Profile of player to get name from */ protected addPlayerToPMCNames(pmcProfile: IPmcData): void; /**