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}`);
 | 
					        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);
 | 
					    res.json(manifest);
 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        res.status(400).end();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -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] = {
 | 
				
			||||||
 | 
					            VendorInfo: {
 | 
				
			||||||
                ...clientVendorInfo,
 | 
					                ...clientVendorInfo,
 | 
				
			||||||
                ItemManifest: [],
 | 
					                ItemManifest: [],
 | 
				
			||||||
                Expiry: { $date: { $numberLong: "0" } }
 | 
					                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