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 { 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);
 | 
			
		||||
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}`);
 | 
			
		||||
        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);
 | 
			
		||||
    } else {
 | 
			
		||||
        res.status(400).end();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -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 =
 | 
			
		||||
 | 
			
		||||
@ -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<string, IVendorInfo> = {};
 | 
			
		||||
const vendorManifestCache: Record<string, 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
 | 
			
		||||
        const { cycleOffset, cycleDuration, ...clientVendorInfo } = vendorInfo;
 | 
			
		||||
        vendorInfoCache[vendorInfo.TypeName] = {
 | 
			
		||||
        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;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user