From 870ff2dd2cd6952f834a805d0048c3391fcb8006 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 16 May 2025 20:01:20 -0700 Subject: [PATCH] feat: adjust server-side vendor prices according to syndicate standings (#2076) For buying crew members from ticker Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2076 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/getVendorInfoController.ts | 26 ++++--- src/services/purchaseService.ts | 7 +- src/services/serversideVendorsService.ts | 75 ++++++++++++++----- src/types/vendorTypes.ts | 6 ++ 4 files changed, 83 insertions(+), 31 deletions(-) diff --git a/src/controllers/api/getVendorInfoController.ts b/src/controllers/api/getVendorInfoController.ts index b161176e..5f9d3292 100644 --- a/src/controllers/api/getVendorInfoController.ts +++ b/src/controllers/api/getVendorInfoController.ts @@ -1,14 +1,20 @@ import { RequestHandler } from "express"; -import { getVendorManifestByTypeName } from "@/src/services/serversideVendorsService"; +import { applyStandingToVendorManifest, getVendorManifestByTypeName } from "@/src/services/serversideVendorsService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; -export const getVendorInfoController: RequestHandler = (req, res) => { - if (typeof req.query.vendor == "string") { - const manifest = getVendorManifestByTypeName(req.query.vendor); - if (!manifest) { - throw new Error(`Unknown vendor: ${req.query.vendor}`); - } - res.json(manifest); - } else { - res.status(400).end(); +export const getVendorInfoController: RequestHandler = async (req, res) => { + let manifest = getVendorManifestByTypeName(req.query.vendor as string); + if (!manifest) { + throw new Error(`Unknown vendor: ${req.query.vendor as string}`); } + + // For testing purposes, authenticating with this endpoint is optional here, but would be required on live. + if (req.query.accountId) { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + manifest = applyStandingToVendorManifest(inventory, manifest); + } + + res.json(manifest); }; diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 38cfd3d1..59a431c3 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -9,7 +9,7 @@ import { updateSlots } from "@/src/services/inventoryService"; import { getRandomWeightedRewardUc } from "@/src/services/rngService"; -import { getVendorManifestByOid } from "@/src/services/serversideVendorsService"; +import { applyStandingToVendorManifest, getVendorManifestByOid } from "@/src/services/serversideVendorsService"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IPurchaseRequest, IPurchaseResponse, SlotPurchase, IInventoryChanges } from "@/src/types/purchaseTypes"; import { logger } from "@/src/utils/logger"; @@ -53,8 +53,9 @@ export const handlePurchase = async ( const prePurchaseInventoryChanges: IInventoryChanges = {}; let seed: bigint | undefined; if (purchaseRequest.PurchaseParams.Source == 7) { - const manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!); + let manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!); if (manifest) { + manifest = applyStandingToVendorManifest(inventory, manifest); let ItemId: string | undefined; if (purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) { ItemId = (JSON.parse(purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) as { ItemId: string }) @@ -92,7 +93,7 @@ export const handlePurchase = async ( if (!config.noVendorPurchaseLimits && ItemId) { inventory.RecentVendorPurchases ??= []; let vendorPurchases = inventory.RecentVendorPurchases.find( - x => x.VendorType == manifest.VendorInfo.TypeName + x => x.VendorType == manifest!.VendorInfo.TypeName ); if (!vendorPurchases) { vendorPurchases = diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index a0e8e7b0..aa6685a6 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -1,5 +1,6 @@ import { unixTimesInMs } from "@/src/constants/timeConstants"; import { catBreadHash } from "@/src/helpers/stringHelpers"; +import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { mixSeeds, SRng } from "@/src/services/rngService"; import { IMongoDate } from "@/src/types/commonTypes"; import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes"; @@ -159,6 +160,43 @@ export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined return undefined; }; +export const applyStandingToVendorManifest = ( + inventory: TInventoryDatabaseDocument, + vendorManifest: IVendorManifest +): IVendorManifest => { + return { + VendorInfo: { + ...vendorManifest.VendorInfo, + ItemManifest: [...vendorManifest.VendorInfo.ItemManifest].map(offer => { + if (offer.Affiliation && offer.ReductionPerPositiveRank && offer.IncreasePerNegativeRank) { + const title: number = inventory.Affiliations.find(x => x.Tag == offer.Affiliation)?.Title ?? 0; + const factor = + 1 + (title < 0 ? offer.IncreasePerNegativeRank : offer.ReductionPerPositiveRank) * title * -1; + //console.log(offer.Affiliation, title, factor); + if (factor) { + offer = { ...offer }; + if (offer.RegularPrice) { + offer.RegularPriceBeforeDiscount = offer.RegularPrice; + offer.RegularPrice = [ + Math.trunc(offer.RegularPriceBeforeDiscount[0] * factor), + Math.trunc(offer.RegularPriceBeforeDiscount[1] * factor) + ]; + } + if (offer.ItemPrices) { + offer.ItemPricesBeforeDiscount = offer.ItemPrices; + offer.ItemPrices = []; + for (const item of offer.ItemPricesBeforeDiscount) { + offer.ItemPrices.push({ ...item, ItemCount: Math.trunc(item.ItemCount * factor) }); + } + } + } + } + return offer; + }) + } + }; +}; + const preprocessVendorManifest = (originalManifest: IVendorManifest): IVendorManifest => { if (Date.now() >= parseInt(originalManifest.VendorInfo.Expiry.$date.$numberLong)) { const manifest = structuredClone(originalManifest); @@ -190,24 +228,27 @@ const toRange = (value: IRange | number): IRange => { return value; }; -const vendorInfoCache: Record = {}; +const vendorManifestCache: Record = {}; const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifest => { - if (!(vendorInfo.TypeName in vendorInfoCache)) { + if (!(vendorInfo.TypeName in vendorManifestCache)) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { cycleOffset, cycleDuration, ...clientVendorInfo } = vendorInfo; - vendorInfoCache[vendorInfo.TypeName] = { - ...clientVendorInfo, - ItemManifest: [], - Expiry: { $date: { $numberLong: "0" } } + vendorManifestCache[vendorInfo.TypeName] = { + VendorInfo: { + ...clientVendorInfo, + ItemManifest: [], + Expiry: { $date: { $numberLong: "0" } } + } }; } - const processed = vendorInfoCache[vendorInfo.TypeName]; - if (Date.now() >= parseInt(processed.Expiry.$date.$numberLong)) { + const cacheEntry = vendorManifestCache[vendorInfo.TypeName]; + const info = cacheEntry.VendorInfo; + if (Date.now() >= parseInt(info.Expiry.$date.$numberLong)) { // Remove expired offers - for (let i = 0; i != processed.ItemManifest.length; ) { - if (Date.now() >= parseInt(processed.ItemManifest[i].Expiry.$date.$numberLong)) { - processed.ItemManifest.splice(i, 1); + for (let i = 0; i != info.ItemManifest.length; ) { + if (Date.now() >= parseInt(info.ItemManifest[i].Expiry.$date.$numberLong)) { + info.ItemManifest.splice(i, 1); } else { ++i; } @@ -228,7 +269,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani !manifest.isOneBinPerCycle ) { const numItemsTarget = rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue); - while (processed.ItemManifest.length + offersToAdd.length < numItemsTarget) { + while (info.ItemManifest.length + offersToAdd.length < numItemsTarget) { // TODO: Consider per-bin item limits // TODO: Consider item probability weightings offersToAdd.push(rng.randomElement(manifest.items)!); @@ -307,20 +348,18 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani item.LocTagRandSeed = (BigInt(highDword) << 32n) | (BigInt(item.LocTagRandSeed) & 0xffffffffn); } } - processed.ItemManifest.push(item); + info.ItemManifest.push(item); } // Update vendor expiry let soonestOfferExpiry: number = Number.MAX_SAFE_INTEGER; - for (const offer of processed.ItemManifest) { + for (const offer of info.ItemManifest) { const offerExpiry = parseInt(offer.Expiry.$date.$numberLong); if (soonestOfferExpiry > offerExpiry) { soonestOfferExpiry = offerExpiry; } } - processed.Expiry.$date.$numberLong = soonestOfferExpiry.toString(); + info.Expiry.$date.$numberLong = soonestOfferExpiry.toString(); } - return { - VendorInfo: processed - }; + return cacheEntry; }; diff --git a/src/types/vendorTypes.ts b/src/types/vendorTypes.ts index 2976ce07..9249a9a0 100644 --- a/src/types/vendorTypes.ts +++ b/src/types/vendorTypes.ts @@ -15,10 +15,16 @@ export interface IItemManifest { QuantityMultiplier: number; Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing. PurchaseQuantityLimit?: number; + Affiliation?: string; + MinAffiliationRank?: number; + ReductionPerPositiveRank?: number; + IncreasePerNegativeRank?: number; RotatedWeekly?: boolean; AllowMultipurchase: boolean; LocTagRandSeed?: number | bigint; Id: IOid; + RegularPriceBeforeDiscount?: number[]; + ItemPricesBeforeDiscount?: IItemPrice[]; } export interface IVendorInfo {