feat: adjust server-side vendor prices according to syndicate standings (#2076)

For buying crew members from ticker

Reviewed-on: OpenWF/SpaceNinjaServer#2076
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
This commit is contained in:
Sainan 2025-05-16 20:01:20 -07:00 committed by Sainan
parent 1ac71a9b28
commit 870ff2dd2c
4 changed files with 83 additions and 31 deletions

View File

@ -1,14 +1,20 @@
import { RequestHandler } from "express"; 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) => { export const getVendorInfoController: RequestHandler = async (req, res) => {
if (typeof req.query.vendor == "string") { let manifest = getVendorManifestByTypeName(req.query.vendor as string);
const manifest = getVendorManifestByTypeName(req.query.vendor); if (!manifest) {
if (!manifest) { throw new Error(`Unknown vendor: ${req.query.vendor as string}`);
throw new Error(`Unknown vendor: ${req.query.vendor}`);
}
res.json(manifest);
} else {
res.status(400).end();
} }
// 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);
}; };

View File

@ -9,7 +9,7 @@ import {
updateSlots updateSlots
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { getRandomWeightedRewardUc } from "@/src/services/rngService"; 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 { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { IPurchaseRequest, IPurchaseResponse, SlotPurchase, IInventoryChanges } from "@/src/types/purchaseTypes"; import { IPurchaseRequest, IPurchaseResponse, SlotPurchase, IInventoryChanges } from "@/src/types/purchaseTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
@ -53,8 +53,9 @@ export const handlePurchase = async (
const prePurchaseInventoryChanges: IInventoryChanges = {}; const prePurchaseInventoryChanges: IInventoryChanges = {};
let seed: bigint | undefined; let seed: bigint | undefined;
if (purchaseRequest.PurchaseParams.Source == 7) { if (purchaseRequest.PurchaseParams.Source == 7) {
const manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!); let manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!);
if (manifest) { if (manifest) {
manifest = applyStandingToVendorManifest(inventory, manifest);
let ItemId: string | undefined; let ItemId: string | undefined;
if (purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) { if (purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) {
ItemId = (JSON.parse(purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) as { ItemId: string }) ItemId = (JSON.parse(purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) as { ItemId: string })
@ -92,7 +93,7 @@ export const handlePurchase = async (
if (!config.noVendorPurchaseLimits && ItemId) { if (!config.noVendorPurchaseLimits && ItemId) {
inventory.RecentVendorPurchases ??= []; inventory.RecentVendorPurchases ??= [];
let vendorPurchases = inventory.RecentVendorPurchases.find( let vendorPurchases = inventory.RecentVendorPurchases.find(
x => x.VendorType == manifest.VendorInfo.TypeName x => x.VendorType == manifest!.VendorInfo.TypeName
); );
if (!vendorPurchases) { if (!vendorPurchases) {
vendorPurchases = vendorPurchases =

View File

@ -1,5 +1,6 @@
import { unixTimesInMs } from "@/src/constants/timeConstants"; import { unixTimesInMs } from "@/src/constants/timeConstants";
import { catBreadHash } from "@/src/helpers/stringHelpers"; import { catBreadHash } from "@/src/helpers/stringHelpers";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { mixSeeds, SRng } from "@/src/services/rngService"; import { mixSeeds, SRng } from "@/src/services/rngService";
import { IMongoDate } from "@/src/types/commonTypes"; import { IMongoDate } from "@/src/types/commonTypes";
import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes"; import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes";
@ -159,6 +160,43 @@ export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined
return 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 => { const preprocessVendorManifest = (originalManifest: IVendorManifest): IVendorManifest => {
if (Date.now() >= parseInt(originalManifest.VendorInfo.Expiry.$date.$numberLong)) { if (Date.now() >= parseInt(originalManifest.VendorInfo.Expiry.$date.$numberLong)) {
const manifest = structuredClone(originalManifest); const manifest = structuredClone(originalManifest);
@ -190,24 +228,27 @@ const toRange = (value: IRange | number): IRange => {
return value; return value;
}; };
const vendorInfoCache: Record<string, IVendorInfo> = {}; const vendorManifestCache: Record<string, IVendorManifest> = {};
const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifest => { const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifest => {
if (!(vendorInfo.TypeName in vendorInfoCache)) { if (!(vendorInfo.TypeName in vendorManifestCache)) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { cycleOffset, cycleDuration, ...clientVendorInfo } = vendorInfo; const { cycleOffset, cycleDuration, ...clientVendorInfo } = vendorInfo;
vendorInfoCache[vendorInfo.TypeName] = { vendorManifestCache[vendorInfo.TypeName] = {
...clientVendorInfo, VendorInfo: {
ItemManifest: [], ...clientVendorInfo,
Expiry: { $date: { $numberLong: "0" } } ItemManifest: [],
Expiry: { $date: { $numberLong: "0" } }
}
}; };
} }
const processed = vendorInfoCache[vendorInfo.TypeName]; const cacheEntry = vendorManifestCache[vendorInfo.TypeName];
if (Date.now() >= parseInt(processed.Expiry.$date.$numberLong)) { const info = cacheEntry.VendorInfo;
if (Date.now() >= parseInt(info.Expiry.$date.$numberLong)) {
// Remove expired offers // Remove expired offers
for (let i = 0; i != processed.ItemManifest.length; ) { for (let i = 0; i != info.ItemManifest.length; ) {
if (Date.now() >= parseInt(processed.ItemManifest[i].Expiry.$date.$numberLong)) { if (Date.now() >= parseInt(info.ItemManifest[i].Expiry.$date.$numberLong)) {
processed.ItemManifest.splice(i, 1); info.ItemManifest.splice(i, 1);
} else { } else {
++i; ++i;
} }
@ -228,7 +269,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
!manifest.isOneBinPerCycle !manifest.isOneBinPerCycle
) { ) {
const numItemsTarget = rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue); 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 per-bin item limits
// TODO: Consider item probability weightings // TODO: Consider item probability weightings
offersToAdd.push(rng.randomElement(manifest.items)!); offersToAdd.push(rng.randomElement(manifest.items)!);
@ -307,20 +348,18 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
item.LocTagRandSeed = (BigInt(highDword) << 32n) | (BigInt(item.LocTagRandSeed) & 0xffffffffn); item.LocTagRandSeed = (BigInt(highDword) << 32n) | (BigInt(item.LocTagRandSeed) & 0xffffffffn);
} }
} }
processed.ItemManifest.push(item); info.ItemManifest.push(item);
} }
// Update vendor expiry // Update vendor expiry
let soonestOfferExpiry: number = Number.MAX_SAFE_INTEGER; 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); const offerExpiry = parseInt(offer.Expiry.$date.$numberLong);
if (soonestOfferExpiry > offerExpiry) { if (soonestOfferExpiry > offerExpiry) {
soonestOfferExpiry = offerExpiry; soonestOfferExpiry = offerExpiry;
} }
} }
processed.Expiry.$date.$numberLong = soonestOfferExpiry.toString(); info.Expiry.$date.$numberLong = soonestOfferExpiry.toString();
} }
return { return cacheEntry;
VendorInfo: processed
};
}; };

View File

@ -15,10 +15,16 @@ export interface IItemManifest {
QuantityMultiplier: number; QuantityMultiplier: number;
Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing. Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing.
PurchaseQuantityLimit?: number; PurchaseQuantityLimit?: number;
Affiliation?: string;
MinAffiliationRank?: number;
ReductionPerPositiveRank?: number;
IncreasePerNegativeRank?: number;
RotatedWeekly?: boolean; RotatedWeekly?: boolean;
AllowMultipurchase: boolean; AllowMultipurchase: boolean;
LocTagRandSeed?: number | bigint; LocTagRandSeed?: number | bigint;
Id: IOid; Id: IOid;
RegularPriceBeforeDiscount?: number[];
ItemPricesBeforeDiscount?: IItemPrice[];
} }
export interface IVendorInfo { export interface IVendorInfo {