XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESpaceNinjaServer

A simple server for a small space ninja game

公开
关注 0 Fork 0 Star 0
UTF-8
import { getSubstringFromKeyword } from "../helpers/stringHelpers.ts";
import {
    addBooster,
    addItem,
    addItems,
    addMiscItems,
    combineInventoryChanges,
    handleOneTimePurchasable,
    updateCredits,
    updateCurrency,
    updateSlots
} from "./inventoryService.ts";
import { getRandomReward, getRandomWeightedRewardUc } from "./rngService.ts";
import { applyStandingToVendorManifest, getVendorManifestByOid } from "./serversideVendorsService.ts";
import type { IMiscItem } from "../types/inventoryTypes/inventoryTypes.ts";
import type {
    IPurchaseRequest,
    IPurchaseResponse,
    IInventoryChanges,
    IPurchaseParams
} from "../types/purchaseTypes.ts";
import { ePurchaseSource } from "../types/purchaseTypes.ts";
import { logger } from "../utils/logger.ts";
import { getWorldState } from "./worldStateService.ts";
import {
    ExportBoosters,
    ExportBundles,
    ExportCreditBundles,
    ExportGear,
    ExportResources,
    ExportVendors
} from "warframe-public-export-plus";
import type { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel.ts";
import {
    fromStoreItem,
    getBoosterPack,
    getBundle,
    getPrice,
    getSyndicate,
    getVendor,
    slotPurchaseData,
    supplementalVendors,
    toStoreItem
} from "./itemDataService.ts";
import { DailyDeal } from "../models/worldStateModel.ts";
import { fromMongoDate, fromOid, toMongoDate } from "../helpers/inventoryHelpers.ts";
import { Guild } from "../models/guildModel.ts";
import { handleGuildGoalProgress } from "./guildService.ts";
import { Types } from "mongoose";
import { BL_LATEST } from "../constants/gameVersions.ts";
import { buildVersionToInt } from "../helpers/versionHelper.ts";

const getStoreItemCategory = (storeItem: string): string => {
    const storeItemString = getSubstringFromKeyword(storeItem, "StoreItems/");
    const storeItemElements = storeItemString.split("/");
    return storeItemElements[1];
};

const getStoreItemTypesCategory = (typesItem: string): string => {
    const typesString = getSubstringFromKeyword(typesItem, "Types");
    const typeElements = typesString.split("/");
    if (typesItem.includes("StoreItems")) {
        return typeElements[2];
    }
    return typeElements[1];
};

const tallyVendorPurchase = (
    inventory: TInventoryDatabaseDocument,
    inventoryChanges: IInventoryChanges,
    VendorType: string,
    ItemId: string,
    numPurchased: number,
    Expiry: Date
): void => {
    if (!inventory.noVendorPurchaseLimits) {
        inventory.RecentVendorPurchases ??= [];
        let vendorPurchases = inventory.RecentVendorPurchases.find(x => x.VendorType == VendorType);
        if (!vendorPurchases) {
            vendorPurchases =
                inventory.RecentVendorPurchases[
                    inventory.RecentVendorPurchases.push({
                        VendorType: VendorType,
                        PurchaseHistory: []
                    }) - 1
                ];
        }
        let historyEntry = vendorPurchases.PurchaseHistory.find(x => x.ItemId == ItemId);
        if (historyEntry) {
            if (Date.now() >= historyEntry.Expiry.getTime()) {
                historyEntry.NumPurchased = numPurchased;
                historyEntry.Expiry = Expiry;
            } else {
                historyEntry.NumPurchased += numPurchased;
            }
        } else {
            historyEntry =
                vendorPurchases.PurchaseHistory[
                    vendorPurchases.PurchaseHistory.push({
                        ItemId: ItemId,
                        NumPurchased: numPurchased,
                        Expiry: Expiry
                    }) - 1
                ];
        }
        inventoryChanges.NewVendorPurchase = {
            VendorType: VendorType,
            PurchaseHistory: [
                {
                    ItemId: ItemId,
                    NumPurchased: historyEntry.NumPurchased,
                    Expiry: toMongoDate(Expiry)
                }
            ]
        };
        inventoryChanges.RecentVendorPurchases = inventoryChanges.NewVendorPurchase;
    }
};

export const handlePurchase = async (
    purchaseRequest: IPurchaseRequest,
    inventory: TInventoryDatabaseDocument
): Promise<IPurchaseResponse> => {
    logger.debug("purchase request", purchaseRequest);
    const buildVersion = buildVersionToInt(purchaseRequest.buildLabel);

    // Fix this path for pre-U25
    if (
        purchaseRequest.PurchaseParams.StoreItem == "/Lotus/StoreItems/Weapons/Tenno/Melee/Brass Knuckles/BrassKnuckles"
    ) {
        purchaseRequest.PurchaseParams.StoreItem = "/Lotus/StoreItems/Weapons/Tenno/Melee/BrassKnuckles/BrassKnuckles";
    }

    const prePurchaseInventoryChanges: IInventoryChanges = {};
    let seed: bigint | undefined;
    if (purchaseRequest.PurchaseParams.Source == ePurchaseSource.Vendor) {
        let manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!, buildVersion);
        if (manifest) {
            manifest = applyStandingToVendorManifest(manifest, inventory.Affiliations);
            let ItemId: string | undefined;
            if (purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) {
                ItemId = (JSON.parse(purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) as { ItemId: string })
                    .ItemId;
            }
            const offer = ItemId
                ? manifest.VendorInfo.ItemManifest.find(x => x.Id.$oid == ItemId)
                : manifest.VendorInfo.ItemManifest.find(x => x.StoreItem == purchaseRequest.PurchaseParams.StoreItem);
            if (!offer) {
                throw new Error(`unknown vendor offer: ${ItemId ? ItemId : purchaseRequest.PurchaseParams.StoreItem}`);
            }
            if (!inventory.dontSubtractPurchaseCreditCost) {
                if (offer.RegularPrice) {
                    updateCurrency(
                        inventory,
                        offer.RegularPrice[0] * purchaseRequest.PurchaseParams.Quantity,
                        false,
                        prePurchaseInventoryChanges
                    );
                }
            }
            if (!inventory.dontSubtractPurchasePlatinumCost) {
                if (offer.PremiumPrice) {
                    updateCurrency(
                        inventory,
                        offer.PremiumPrice[0] * purchaseRequest.PurchaseParams.Quantity,
                        true,
                        prePurchaseInventoryChanges
                    );
                }
            }
            if (
                inventory.GuildId &&
                offer.ItemPrices &&
                manifest.VendorInfo.TypeName ==
                    "/Lotus/Types/Game/VendorManifests/Events/DuviriMurmurInvasionVendorManifest"
            ) {
                const guild = await Guild.findById(inventory.GuildId, "GoalProgress Tier VaultDecoRecipes");
                const goal = getWorldState(purchaseRequest.buildLabel).Goals.find(x => x.Tag == "DuviriMurmurEvent");
                if (guild && goal) {
                    await handleGuildGoalProgress(guild, {
                        Count: offer.ItemPrices[0].ItemCount * purchaseRequest.PurchaseParams.Quantity,
                        Tag: goal.Tag,
                        goalId: new Types.ObjectId(goal._id.$oid)
                    });
                }
            }
            if (!inventory.dontSubtractPurchaseItemCost) {
                if (offer.ItemPrices) {
                    await handleItemPrices(
                        inventory,
                        offer.ItemPrices,
                        purchaseRequest.PurchaseParams.Quantity,
                        prePurchaseInventoryChanges
                    );
                }
            }
            if (offer.LocTagRandSeed !== undefined) {
                seed = BigInt(offer.LocTagRandSeed);
            }
            if (ItemId) {
                let expiry = parseInt(offer.Expiry.$date.$numberLong);
                if (purchaseRequest.PurchaseParams.IsWeekly) {
                    const EPOCH = 1734307200 * 1000; // Monday
                    const week = Math.trunc((Date.now() - EPOCH) / 604800000);
                    const weekStart = EPOCH + week * 604800000;
                    expiry = weekStart + 604800000;
                }
                tallyVendorPurchase(
                    inventory,
                    prePurchaseInventoryChanges,
                    manifest.VendorInfo.TypeName,
                    ItemId,
                    purchaseRequest.PurchaseParams.Quantity,
                    new Date(expiry)
                );
            }
            purchaseRequest.PurchaseParams.Quantity *= offer.QuantityMultiplier;
        } else {
            if (!getVendor(purchaseRequest.PurchaseParams.SourceId!)) {
                throw new Error(`unknown vendor: ${purchaseRequest.PurchaseParams.SourceId!}`);
            }
        }
    }

    const purchaseResponse = await handleStoreItemAcquisition(
        purchaseRequest.PurchaseParams.StoreItem,
        inventory,
        purchaseRequest.PurchaseParams.Quantity,
        undefined,
        false,
        purchaseRequest.PurchaseParams.UsePremium,
        seed,
        purchaseRequest.buildLabel
    );
    combineInventoryChanges(purchaseResponse.InventoryChanges, prePurchaseInventoryChanges);

    switch (purchaseRequest.PurchaseParams.Source) {
        case ePurchaseSource.Market:
        case ePurchaseSource.Arsenal:
            if (!purchaseRequest.PurchaseParams.ExpectedPrice) {
                logger.debug(`client didn't provide ExpectedPrice`);
                purchaseRequest.PurchaseParams.ExpectedPrice = getPrice(
                    purchaseRequest.PurchaseParams.StoreItem,
                    purchaseRequest.PurchaseParams.Quantity,
                    purchaseRequest.PurchaseParams.Durability,
                    purchaseRequest.PurchaseParams.UsePremium,
                    purchaseRequest.buildLabel
                );
            }
            break;
        case ePurchaseSource.VoidTrader: {
            const worldState = getWorldState(purchaseRequest.buildLabel);
            const voidTrader = worldState.VoidTraders.find(
                x => fromOid(x._id) == purchaseRequest.PurchaseParams.SourceId!
            );
            if (!voidTrader) {
                throw new Error("invalid request source");
            }
            const offer = voidTrader.Manifest.find(x => x.ItemType == purchaseRequest.PurchaseParams.StoreItem);
            if (offer) {
                if (!inventory.dontSubtractPurchaseCreditCost) {
                    updateCurrency(inventory, offer.RegularPrice, false, purchaseResponse.InventoryChanges);
                }
                if (purchaseRequest.PurchaseParams.ExpectedPrice) {
                    throw new Error(`vendor purchase should not have an expected price`);
                }

                if (offer.PrimePrice && !inventory.dontSubtractPurchaseItemCost) {
                    const invItem: IMiscItem = {
                        ItemType: "/Lotus/Types/Items/MiscItems/PrimeBucks",
                        ItemCount: offer.PrimePrice * purchaseRequest.PurchaseParams.Quantity * -1
                    };
                    addMiscItems(inventory, [invItem]);
                    purchaseResponse.InventoryChanges.MiscItems ??= [];
                    purchaseResponse.InventoryChanges.MiscItems.push(invItem);
                }

                if (offer.Limit) {
                    tallyVendorPurchase(
                        inventory,
                        purchaseResponse.InventoryChanges,
                        "VoidTrader",
                        offer.ItemType,
                        purchaseRequest.PurchaseParams.Quantity,
                        fromMongoDate(voidTrader.Expiry)
                    );
                }
            }
            break;
        }
        case ePurchaseSource.SyndicateFavor:
            {
                const syndicateTag = purchaseRequest.PurchaseParams.SyndicateTag!;
                if (purchaseRequest.PurchaseParams.UseFreeFavor!) {
                    const affiliation = inventory.Affiliations.find(x => x.Tag == syndicateTag)!;
                    affiliation.FreeFavorsUsed ??= [];
                    const lastTitle = affiliation.FreeFavorsEarned![affiliation.FreeFavorsUsed.length];
                    affiliation.FreeFavorsUsed.push(lastTitle);
                    purchaseResponse.FreeFavorsUsed = [
                        {
                            Tag: syndicateTag,
                            Title: lastTitle
                        }
                    ];
                } else if (!inventory.dontSubtractPurchaseStandingCost) {
                    const syndicate = await getSyndicate(syndicateTag, purchaseRequest.buildLabel);
                    if (syndicate) {
                        const favour = syndicate.favours.find(
                            x => x.storeItem == purchaseRequest.PurchaseParams.StoreItem
                        );
                        if (favour) {
                            if (favour.creditsCost) {
                                updateCredits(
                                    inventory,
                                    favour.creditsCost * purchaseRequest.PurchaseParams.Quantity,
                                    purchaseResponse.InventoryChanges
                                );
                            }
                            if (favour.standingCost) {
                                const affiliation = inventory.Affiliations.find(x => x.Tag == syndicateTag);
                                if (affiliation) {
                                    purchaseResponse.Standing = [
                                        {
                                            Tag: syndicateTag,
                                            Standing: favour.standingCost * purchaseRequest.PurchaseParams.Quantity
                                        }
                                    ];
                                    affiliation.Standing -=
                                        favour.standingCost * purchaseRequest.PurchaseParams.Quantity;
                                }
                            }
                        }
                    }
                }
            }
            break;
        case ePurchaseSource.DailyDeal:
            if (purchaseRequest.PurchaseParams.ExpectedPrice) {
                throw new Error(`daily deal purchase should not have an expected price`);
            }
            await handleDailyDealPurchase(inventory, purchaseRequest.PurchaseParams, purchaseResponse);
            break;
        case ePurchaseSource.Vendor:
            if (purchaseRequest.PurchaseParams.ExpectedPrice) {
                throw new Error(`vendor purchase should not have an expected price`);
            }
            if (
                purchaseRequest.PurchaseParams.SourceId! in ExportVendors ||
                purchaseRequest.PurchaseParams.SourceId! in supplementalVendors
            ) {
                const vendor = getVendor(purchaseRequest.PurchaseParams.SourceId!)!;
                const offer = vendor.items.find(x => x.storeItem == purchaseRequest.PurchaseParams.StoreItem);
                if (offer) {
                    if (typeof offer.credits == "number" && !inventory.dontSubtractPurchaseCreditCost) {
                        updateCurrency(inventory, offer.credits, false, purchaseResponse.InventoryChanges);
                    }
                    if (typeof offer.platinum == "number" && !inventory.dontSubtractPurchasePlatinumCost) {
                        updateCurrency(inventory, offer.platinum, true, purchaseResponse.InventoryChanges);
                    }
                    if (offer.itemPrices && !inventory.dontSubtractPurchaseItemCost) {
                        await handleItemPrices(
                            inventory,
                            offer.itemPrices,
                            purchaseRequest.PurchaseParams.Quantity,
                            purchaseResponse.InventoryChanges
                        );
                    }
                    if (offer.focusXpCost) {
                        const polarity = offer.focusXpCost.polarity;
                        const cost = offer.focusXpCost.cost * purchaseRequest.PurchaseParams.Quantity;
                        inventory.FocusXP ??= {};
                        inventory.FocusXP[polarity] ??= 0;
                        inventory.FocusXP[polarity] -= cost;
                        purchaseResponse.FocusCost = {
                            Polarity: polarity,
                            Cost: cost
                        };
                    }
                }
            } else {
                let manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!, buildVersion);
                if (manifest) {
                    manifest = applyStandingToVendorManifest(manifest, inventory.Affiliations);
                    let ItemId: string | undefined;
                    if (purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) {
                        ItemId = (
                            JSON.parse(purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) as { ItemId: string }
                        ).ItemId;
                    }
                    const offer = ItemId
                        ? manifest.VendorInfo.ItemManifest.find(x => x.Id.$oid == ItemId)
                        : manifest.VendorInfo.ItemManifest.find(
                              x => x.StoreItem == purchaseRequest.PurchaseParams.StoreItem
                          );
                    if (!offer) {
                        throw new Error(
                            `unknown vendor offer: ${ItemId ? ItemId : purchaseRequest.PurchaseParams.StoreItem}`
                        );
                    }
                    if (offer.StandingCost && !inventory.dontSubtractPurchaseStandingCost) {
                        const affiliation = inventory.Affiliations.find(x => x.Tag == offer.Affiliation!);
                        if (affiliation) {
                            purchaseResponse.Standing = [
                                {
                                    Tag: offer.Affiliation!,
                                    Standing: offer.StandingCost * purchaseRequest.PurchaseParams.Quantity
                                }
                            ];
                            affiliation.Standing -= offer.StandingCost * purchaseRequest.PurchaseParams.Quantity;
                        }
                    }
                }
            }
            break;
        case ePurchaseSource.PrimeVaultTrader: {
            const worldState = getWorldState(purchaseRequest.buildLabel);
            if (purchaseRequest.PurchaseParams.SourceId! != worldState.PrimeVaultTraders[0]._id.$oid) {
                throw new Error("invalid request source");
            }
            const offer =
                worldState.PrimeVaultTraders[0].Manifest.find(
                    x => x.ItemType == purchaseRequest.PurchaseParams.StoreItem
                ) ??
                worldState.PrimeVaultTraders[0].EvergreenManifest.find(
                    x => x.ItemType == purchaseRequest.PurchaseParams.StoreItem
                );
            if (offer) {
                if (offer.RegularPrice) {
                    if (!inventory.dontSubtractPurchaseItemCost) {
                        const invItem: IMiscItem = {
                            ItemType: "/Lotus/Types/Items/MiscItems/SchismKey",
                            ItemCount: offer.RegularPrice * purchaseRequest.PurchaseParams.Quantity * -1
                        };

                        addMiscItems(inventory, [invItem]);

                        purchaseResponse.InventoryChanges.MiscItems ??= [];
                        purchaseResponse.InventoryChanges.MiscItems.push(invItem);
                    }
                } else if (!inventory.infiniteRegalAya) {
                    inventory.PrimeTokens -= offer.PrimePrice! * purchaseRequest.PurchaseParams.Quantity;

                    purchaseResponse.InventoryChanges.PrimeTokens ??= 0;
                    purchaseResponse.InventoryChanges.PrimeTokens -=
                        offer.PrimePrice! * purchaseRequest.PurchaseParams.Quantity;
                }
            }
            break;
        }
    }

    if (purchaseRequest.PurchaseParams.ExpectedPrice) {
        updateCurrency(
            inventory,
            purchaseRequest.PurchaseParams.ExpectedPrice,
            purchaseRequest.PurchaseParams.UsePremium,
            purchaseResponse.InventoryChanges
        );
    }

    return purchaseResponse;
};

const handleItemPrices = async (
    inventory: TInventoryDatabaseDocument,
    itemPrices: readonly IMiscItem[],
    purchaseQuantity: number,
    inventoryChanges: IInventoryChanges
): Promise<void> => {
    const items: IMiscItem[] = itemPrices.map(({ ItemType, ItemCount }) => ({
        ItemType,
        ItemCount: ItemCount * purchaseQuantity * -1
    }));
    await addItems(inventory, items, inventoryChanges);
};

export const handleDailyDealPurchase = async (
    inventory: TInventoryDatabaseDocument,
    purchaseParams: IPurchaseParams,
    purchaseResponse: IPurchaseResponse
): Promise<void> => {
    const dailyDeal = (await DailyDeal.findOne({ StoreItem: purchaseParams.StoreItem }))!;
    dailyDeal.AmountSold += 1;
    await dailyDeal.save();

    if (!inventory.dontSubtractPurchasePlatinumCost) {
        updateCurrency(inventory, dailyDeal.SalePrice, true, purchaseResponse.InventoryChanges);
    }

    if (!inventory.noVendorPurchaseLimits) {
        inventory.UsedDailyDeals.push(purchaseParams.StoreItem);
        purchaseResponse.DailyDealUsed = purchaseParams.StoreItem;
    }
};

export const handleBundleAcquisition = async (
    storeItemName: string,
    inventory: TInventoryDatabaseDocument,
    quantity: number = 1,
    inventoryChanges: IInventoryChanges = {},
    buildLabel: string = BL_LATEST
): Promise<IInventoryChanges> => {
    const bundle = getBundle(storeItemName, buildLabel)!;
    logger.debug("acquiring bundle", bundle);
    if (bundle.oneTimePurchasable) {
        handleOneTimePurchasable(inventory, storeItemName, inventoryChanges);
    }
    for (const component of bundle.components) {
        combineInventoryChanges(
            inventoryChanges,
            (
                await handleStoreItemAcquisition(
                    component.typeName,
                    inventory,
                    component.purchaseQuantity * quantity,
                    component.durabilityDays,
                    true,
                    true,
                    undefined,
                    buildLabel
                )
            ).InventoryChanges
        );
    }
    return inventoryChanges;
};

export const handleStoreItemAcquisition = async (
    storeItemName: string,
    inventory: TInventoryDatabaseDocument,
    quantity: number = 1,
    durabilityDays: number = 3,
    ignorePurchaseQuantity: boolean = false,
    premiumPurchase: boolean = true,
    seed?: bigint,
    buildLabel: string = BL_LATEST
): Promise<IPurchaseResponse> => {
    let purchaseResponse: IPurchaseResponse = {
        InventoryChanges: {}
    };
    logger.debug(`handling acquisition of ${storeItemName}`);
    if (storeItemName in ExportBundles) {
        await handleBundleAcquisition(
            storeItemName,
            inventory,
            quantity,
            purchaseResponse.InventoryChanges,
            buildLabel
        );
    } else {
        const storeCategory = getStoreItemCategory(storeItemName);
        const internalName = fromStoreItem(storeItemName);
        if (!ignorePurchaseQuantity) {
            if (internalName in ExportGear) {
                quantity *= ExportGear[internalName].purchaseQuantity || 1;
                logger.debug(`factored quantity is ${quantity}`);
            } else if (internalName in ExportResources) {
                quantity *= ExportResources[internalName].purchaseQuantity || 1;
                logger.debug(`factored quantity is ${quantity}`);
            }
        }
        logger.debug(`store category ${storeCategory}`);
        switch (storeCategory) {
            default: {
                purchaseResponse = {
                    InventoryChanges: await addItem(
                        inventory,
                        internalName,
                        quantity,
                        premiumPurchase,
                        seed,
                        undefined,
                        true,
                        buildLabel
                    )
                };
                break;
            }
            case "Types":
                purchaseResponse = await handleTypesPurchase(
                    internalName,
                    inventory,
                    quantity,
                    ignorePurchaseQuantity,
                    premiumPurchase,
                    seed,
                    buildLabel
                );
                break;
            case "Boosters":
                purchaseResponse = handleBoostersPurchase(storeItemName, inventory, durabilityDays);
                break;
        }
    }
    return purchaseResponse;
};

// // extra = everything above the base +2 slots (depending on slot type)
// // new slot above base = extra + 1 and slots +1
// // new frame = slots -1
// // number of frames = extra - slots + 2
const handleSlotPurchase = (
    slotPurchaseNameFull: string,
    inventory: TInventoryDatabaseDocument,
    quantity: number,
    ignorePurchaseQuantity: boolean
): IPurchaseResponse => {
    logger.debug(`slot name ${slotPurchaseNameFull}`);
    const slotPurchaseName = slotPurchaseNameFull.substring(slotPurchaseNameFull.lastIndexOf("/") + 1);
    if (!(slotPurchaseName in slotPurchaseData)) {
        throw new Error(`invalid slot purchase name ${slotPurchaseName}`);
    }
    logger.debug(`slot purchase name ${slotPurchaseName}`);

    const data = slotPurchaseData[slotPurchaseName];

    let slotsPurchased = quantity;
    if (!ignorePurchaseQuantity) {
        slotsPurchased *= data.amount;
    }

    updateSlots(inventory, data.bin, slotsPurchased, slotsPurchased);

    logger.debug(`added ${slotsPurchased} slot ${data.bin}`);

    const inventoryChanges: IInventoryChanges = {};
    inventoryChanges[data.bin] = {
        count: 0,
        platinum: 1,
        Slots: slotsPurchased,
        Extra: slotsPurchased
    };
    return { InventoryChanges: inventoryChanges };
};

const handleBoosterPackPurchase = async (
    typeName: string,
    inventory: TInventoryDatabaseDocument,
    quantity: number,
    buildLabel: string = BL_LATEST
): Promise<IPurchaseResponse> => {
    const pack = await getBoosterPack(typeName, buildLabel);
    if (!pack) {
        throw new Error(`unknown booster pack: ${typeName}`);
    }
    const purchaseResponse: IPurchaseResponse = {
        BoosterPackItems: "",
        Body: "",
        InventoryChanges: {}
    };
    if (quantity < 1) {
        throw new Error(`invalid quantity for booster pack purchase: ${quantity}`);
    }
    if (quantity > 100) {
        throw new Error(
            "attempt to roll over 100 booster packs in a single go. possible but unlikely to be desirable for the user or the server."
        );
    }
    const specialItemReward = pack.components.find(x => x.PityIncreaseRate);
    for (let i = 0; i != quantity; ++i) {
        if (specialItemReward) {
            {
                const normalComponents = [];
                for (const comp of pack.components) {
                    if (!comp.PityIncreaseRate) {
                        const { Probability, ...rest } = comp;
                        normalComponents.push({
                            ...rest,
                            probability: Probability!
                        });
                    }
                }
                const result = getRandomReward(normalComponents)!;
                logger.debug(`booster pack rolled`, result);
                purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + ',{"lvl":0};';
                combineInventoryChanges(
                    purchaseResponse.InventoryChanges,
                    await addItem(inventory, result.Item, result.Amount)
                );
            }

            if (!inventory.WeaponSkins.some(x => x.ItemType == specialItemReward.Item)) {
                inventory.SpecialItemRewardAttenuation ??= [];
                let atten = inventory.SpecialItemRewardAttenuation.find(x => x.Tag == specialItemReward.Item);
                if (!atten) {
                    atten =
                        inventory.SpecialItemRewardAttenuation[
                            inventory.SpecialItemRewardAttenuation.push({
                                Tag: specialItemReward.Item,
                                Atten: specialItemReward.Probability!
                            }) - 1
                        ];
                }
                if (Math.random() < atten.Atten) {
                    purchaseResponse.BoosterPackItems += toStoreItem(specialItemReward.Item) + ',{"lvl":0};';
                    combineInventoryChanges(
                        purchaseResponse.InventoryChanges,
                        await addItem(inventory, specialItemReward.Item)
                    );
                    atten.Atten = 0;
                } else {
                    atten.Atten += specialItemReward.PityIncreaseRate!;
                }
            }
        } else {
            const disallowedItems = new Set();
            for (let roll = 0; roll != pack.rarityWeightsPerRoll.length; ) {
                const weights = pack.rarityWeightsPerRoll[roll];
                const result = getRandomWeightedRewardUc(pack.components, weights)!;
                logger.debug(`booster pack rolled`, result);
                if (disallowedItems.has(result.Item)) {
                    logger.debug(`oops, can't use that one; trying again`);
                    continue;
                }
                if (!pack.canGiveDuplicates) {
                    disallowedItems.add(result.Item);
                }
                const fingerprint = { lvl: 0 };
                if (result.Item.startsWith("/Lotus/Upgrades/Mods/Fusers/")) {
                    if (result.Item.endsWith("RareModFuser")) {
                        fingerprint.lvl = Math.floor(Math.random() * 4) + 2;
                    } else if (result.Item.endsWith("CommonModFuser") || result.Item.endsWith("UncommonModFuser")) {
                        fingerprint.lvl = Math.floor(Math.random() * 3) + 1;
                    }
                }
                const stringifiedFingerprint = JSON.stringify(fingerprint);
                purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + `,${stringifiedFingerprint};`;
                combineInventoryChanges(
                    purchaseResponse.InventoryChanges,
                    await addItem(inventory, result.Item, result.Amount, false, undefined, stringifiedFingerprint)
                );
                ++roll;
            }
        }
    }
    if (purchaseResponse.BoosterPackItems)
        purchaseResponse.Body = purchaseResponse.BoosterPackItems.replace(/[{}"]/g, "").replace(/:/g, "=");
    return purchaseResponse;
};

const handleCreditBundlePurchase = async (
    typeName: string,
    inventory: TInventoryDatabaseDocument
): Promise<IPurchaseResponse> => {
    if (typeName && typeName in ExportCreditBundles) {
        const creditsAmount = ExportCreditBundles[typeName].credits;

        inventory.RegularCredits += creditsAmount;
        await inventory.save();

        return { InventoryChanges: { RegularCredits: creditsAmount } };
    } else {
        throw new Error(`unknown credit bundle: ${typeName}`);
    }
};

//TODO: change to getInventory, apply changes then save at the end
const handleTypesPurchase = async (
    typesName: string,
    inventory: TInventoryDatabaseDocument,
    quantity: number,
    ignorePurchaseQuantity: boolean,
    premiumPurchase: boolean = true,
    seed?: bigint,
    buildLabel: string = BL_LATEST
): Promise<IPurchaseResponse> => {
    const typeCategory = getStoreItemTypesCategory(typesName);
    logger.debug(`type category ${typeCategory}`);
    switch (typeCategory) {
        default:
            return {
                InventoryChanges: await addItem(
                    inventory,
                    typesName,
                    quantity,
                    premiumPurchase,
                    seed,
                    undefined,
                    true,
                    buildLabel
                )
            };
        case "BoosterPacks":
            return handleBoosterPackPurchase(typesName, inventory, quantity, buildLabel);
        case "SlotItems":
            return handleSlotPurchase(typesName, inventory, quantity, ignorePurchaseQuantity);
        case "CreditBundles":
            return handleCreditBundlePurchase(typesName, inventory);
    }
};

const handleBoostersPurchase = (
    boosterStoreName: string,
    inventory: TInventoryDatabaseDocument,
    durabilityDays: number
): { InventoryChanges: IInventoryChanges } => {
    if (!(boosterStoreName in ExportBoosters)) {
        logger.error(`unknown booster type: ${boosterStoreName}`);
        return { InventoryChanges: {} };
    }

    const ItemType = ExportBoosters[boosterStoreName].typeName;
    const ExpiryDate = durabilityDays * 86400;

    addBooster(ItemType, ExpiryDate, inventory);

    return {
        InventoryChanges: {
            Boosters: [{ ItemType, ExpiryDate }]
        }
    };
};