forked from OpenWF/SpaceNinjaServer
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:
parent
1ac71a9b28
commit
870ff2dd2c
@ -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);
|
||||||
};
|
};
|
||||||
|
@ -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 =
|
||||||
|
@ -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
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
@ -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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user