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 type { IInventoryChanges } from "../types/purchaseTypes.ts";
import type { TAccountDocument } from "./loginService.ts";
import { mixSeeds, SRng } from "./rngService.ts";
import type { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel.ts";
import { addBooster, updateCurrency } from "./inventoryService.ts";
import { handleStoreItemAcquisition } from "./purchaseService.ts";
import {
    ExportBoosters,
    ExportRecipes,
    ExportWarframes,
    ExportWeapons,
    type TRarity
} from "warframe-public-export-plus";
import { fromStoreItem, getBoosterPack, toStoreItem } from "./itemDataService.ts";
import { logger } from "../utils/logger.ts";
import { version_compare } from "../helpers/inventoryHelpers.ts";
import gameToBuildVersion from "../constants/gameToBuildVersion.ts";
import { wikiDateToBuildVersion } from "../helpers/versionHelper.ts";

export interface ILoginRewardsReponse {
    DailyTributeInfo: {
        Rewards?: ILoginReward[]; // only set on first call of the day
        IsMilestoneDay?: boolean;
        IsChooseRewardSet?: boolean;
        IsHiddenRewardSet?: boolean; // Seems to be the ~U18 equivalent of IsChooseRewardSet
        LoginDays?: number; // when calling multiple times per day, this is already incremented to represent "tomorrow"
        NextMilestoneReward?: "";
        NextMilestoneDay?: number; // seems to not be used if IsMilestoneDay
        HasChosenReward?: boolean;
        NewInventory?: IInventoryChanges;
        ChosenReward?: ILoginReward;
    };
    LastLoginRewardDate?: number; // only set on first call of the day; today at 0 UTC
}

export interface ILoginReward {
    //_id: IOid;
    RewardType: string;
    //CouponType: "CPT_PLATINUM";
    Icon?: string;
    ItemType?: string; // pre-daily tribute
    StoreItemType: string; // uniquely identifies the reward
    //ProductCategory: "Pistols";
    Amount: number;
    ScalingMultiplier: number;
    //Durability: "COMMON";
    //DisplayName: "";
    Duration?: number;
    //CouponSku: number;
    //Rarity: number;
    Transmission: string;
    HiddenSetIndex?: number; // pre-U23.10, unsure what for
}

const scaleAmount = (day: number, amount: number, scalingMultiplier: number): number => {
    const divisor = 200 / (amount * scalingMultiplier);
    return amount + Math.min(day, 3000) / divisor;
};

// Always produces the same result for the same account _id & LoginDays pair.
export const isLoginRewardAChoice = (account: TAccountDocument): boolean => {
    const accountSeed = parseInt(account._id.toString().substring(16), 16);
    const rng = new SRng(mixSeeds(accountSeed, account.LoginDays));
    return rng.randomFloat() < 0.25;
};

// Always produces the same result for the same account _id & LoginDays pair.
export const getRandomLoginRewards = async (
    account: TAccountDocument,
    inventory: TInventoryDatabaseDocument,
    buildLabel: string
): Promise<ILoginReward[]> => {
    const accountSeed = parseInt(account._id.toString().substring(16), 16);
    const rng = new SRng(mixSeeds(accountSeed, account.LoginDays));
    const pick_a_door = rng.randomFloat() < 0.25;
    const rewards = [await getRandomLoginReward(rng, account.LoginDays, inventory, buildLabel)];
    if (pick_a_door) {
        do {
            const reward = await getRandomLoginReward(rng, account.LoginDays, inventory, buildLabel);
            if (!rewards.find(x => x.StoreItemType == reward.StoreItemType)) {
                rewards.push(reward);
            }
        } while (rewards.length != 3);
    }
    return rewards;
};

const getRandomLoginReward = async (
    rng: SRng,
    day: number,
    inventory: TInventoryDatabaseDocument,
    buildLabel: string
): Promise<ILoginReward> => {
    const filteredPool = randomRewards.filter(r => {
        return !r.minBuildLabel || version_compare(buildLabel, r.minBuildLabel) >= 0;
    });
    const resultPool: (ILoginReward & { probability: number })[] = [];
    const rarityCounts: Record<TRarity, number> = { COMMON: 0, UNCOMMON: 0, RARE: 0, LEGENDARY: 0 };
    for (const entry of filteredPool) {
        ++rarityCounts[entry.Rarity];
    }
    for (const entry of filteredPool) {
        resultPool.push({
            ...entry,
            probability: randomRewardsWeights[entry.Rarity] / rarityCounts[entry.Rarity]
        });
    }
    const reward = rng.randomReward(resultPool)!;
    //const reward = randomRewards.find(x => x.RewardType == "RT_BOOSTER")!;
    let storeItemType: string = reward.StoreItemType;
    if (reward.RewardType == "RT_RANDOM_RECIPE") {
        const masteredItems = new Set();
        for (const entry of inventory.XPInfo) {
            masteredItems.add(entry.ItemType);
        }
        const unmasteredItems = new Set();
        for (const [uniqueName, data] of Object.entries(ExportWeapons)) {
            if (
                data.totalDamage != 0 &&
                data.variantType == "VT_NORMAL" &&
                !masteredItems.has(uniqueName) &&
                data.introducedAt
            ) {
                const introducedAtBuildVersion = wikiDateToBuildVersion(data.introducedAt);
                if (version_compare(buildLabel, introducedAtBuildVersion) >= 0) {
                    unmasteredItems.add(uniqueName);
                }
            }
        }
        for (const [uniqueName, data] of Object.entries(ExportWarframes)) {
            if (data.variantType == "VT_NORMAL" && !masteredItems.has(uniqueName) && data.introducedAt) {
                const introducedAtBuildVersion = wikiDateToBuildVersion(data.introducedAt);
                if (version_compare(buildLabel, introducedAtBuildVersion) >= 0) {
                    unmasteredItems.add(uniqueName);
                }
            }
        }
        const eligibleRecipes: string[] = [];
        for (const [uniqueName, recipe] of Object.entries(ExportRecipes)) {
            if (!recipe.excludeFromMarket && unmasteredItems.has(recipe.resultType)) {
                eligibleRecipes.push(uniqueName);
            }
        }
        if (eligibleRecipes.length == 0) {
            // This account has all applicable warframes and weapons already mastered (filthy cheater), need a different reward.
            return await getRandomLoginReward(rng, day, inventory, buildLabel);
        }
        storeItemType = toStoreItem(rng.randomElement(eligibleRecipes)!);
    } else if (reward.StoreItemType == "/Lotus/StoreItems/Types/BoosterPacks/LoginRewardRandomProjection") {
        const boosterPackType =
            version_compare(buildLabel, gameToBuildVersion["18.16.0"]) >= 0
                ? "/Lotus/Types/BoosterPacks/LoginRewardRandomProjection"
                : "/Lotus/Types/BoosterPacks/RandomKey";
        const boosterPack = await getBoosterPack(boosterPackType, buildLabel);
        storeItemType = toStoreItem(rng.randomElement(boosterPack!.components)!.Item);
    }
    return {
        //_id: toOid(new Types.ObjectId()),
        RewardType: reward.RewardType,
        //CouponType: "CPT_PLATINUM",
        Icon: reward.Icon ?? "",
        ItemType: storeItemType ? fromStoreItem(storeItemType) : "",
        StoreItemType: storeItemType,
        //ProductCategory: "Pistols",
        Amount: reward.Duration
            ? 1
            : Math.round(
                  scaleAmount(day, reward.Amount, reward.ScalingMultiplier) *
                      (inventory.dailyTributeRewardMultiplier ?? 1)
              ),
        ScalingMultiplier: reward.ScalingMultiplier,
        //Durability: "COMMON",
        //DisplayName: "",
        Duration: reward.Duration
            ? Math.round(
                  reward.Duration *
                      scaleAmount(day, 1, reward.ScalingMultiplier) *
                      (inventory.dailyTributeRewardMultiplier ?? 1)
              )
            : 0,
        //CouponSku: 0,
        //Rarity: 0,
        Transmission: reward.Transmission
    };
};

export const claimLoginReward = async (
    inventory: TInventoryDatabaseDocument,
    reward: ILoginReward
): Promise<IInventoryChanges> => {
    switch (reward.RewardType) {
        case "RT_RESOURCE":
        case "RT_STORE_ITEM":
        case "RT_RECIPE":
        case "RT_RANDOM_RECIPE":
            return (await handleStoreItemAcquisition(reward.StoreItemType, inventory, reward.Amount, undefined, true))
                .InventoryChanges;

        case "RT_CREDITS":
            return updateCurrency(inventory, -reward.Amount, false);

        case "RT_BOOSTER": {
            logger.debug(`claim login reward`, reward); // all other paths already log in some other way
            const ItemType = ExportBoosters[reward.StoreItemType].typeName;
            const ExpiryDate = 3600 * reward.Duration!;
            addBooster(ItemType, ExpiryDate, inventory);
            return {
                Boosters: [{ ItemType, ExpiryDate }]
            };
        }
    }
    throw new Error(`unknown login reward type: ${reward.RewardType}`);
};

export const setAccountGotLoginRewardToday = (
    account: TAccountDocument,
    inventory: Pick<TInventoryDatabaseDocument, "incrementDailyTributeBy50">
): void => {
    if (inventory.incrementDailyTributeBy50) {
        account.LoginDays = Math.max(50, (Math.trunc(account.LoginDays / 50) + 1) * 50);
    } else {
        account.LoginDays += 1;
    }
    account.LastLoginRewardDate = Math.trunc(Date.now() / 86400000) * 86400;
};

const randomRewardsWeights: Record<TRarity, number> = {
    COMMON: 151,
    UNCOMMON: 0,
    RARE: 4,
    LEGENDARY: 0
};

const randomRewards: (ILoginReward & { Rarity: TRarity; minBuildLabel?: string })[] = [
    {
        RewardType: "RT_RESOURCE",
        StoreItemType: "/Lotus/StoreItems/Types/Items/MiscItems/OxiumAlloy",
        Amount: 100,
        ScalingMultiplier: 1,
        Rarity: "COMMON",
        Transmission: "/Lotus/Sounds/Dialog/DailyTribute/Ordis/DDayTribOrdis",
        minBuildLabel: gameToBuildVersion["12.1.2"] // Should be U11.9
    },
    {
        RewardType: "RT_RESOURCE",
        StoreItemType: "/Lotus/StoreItems/Types/Items/MiscItems/Gallium",
        Amount: 1,
        ScalingMultiplier: 1,
        Rarity: "COMMON",
        Transmission: "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo"
    },
    {
        RewardType: "RT_RESOURCE",
        StoreItemType: "/Lotus/StoreItems/Types/Items/Research/ChemFragment",
        Amount: 2,
        ScalingMultiplier: 1,
        Rarity: "COMMON",
        Transmission: "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo",
        minBuildLabel: gameToBuildVersion["8.0.0"]
    },
    {
        RewardType: "RT_RESOURCE",
        StoreItemType: "/Lotus/StoreItems/Types/Items/MiscItems/Morphic",
        Amount: 1,
        ScalingMultiplier: 1,
        Rarity: "COMMON",
        Transmission: "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo"
    },
    {
        RewardType: "RT_RESOURCE",
        StoreItemType: "/Lotus/StoreItems/Types/Items/Research/EnergyFragment",
        Amount: 2,
        ScalingMultiplier: 1,
        Rarity: "COMMON",
        Transmission: "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo",
        minBuildLabel: gameToBuildVersion["8.0.0"]
    },
    {
        RewardType: "RT_RESOURCE",
        StoreItemType: "/Lotus/StoreItems/Types/Items/MiscItems/NeuralSensor",
        Amount: 1,
        ScalingMultiplier: 1,
        Rarity: "COMMON",
        Transmission: "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo"
    },
    {
        RewardType: "RT_RESOURCE",
        StoreItemType: "/Lotus/StoreItems/Types/Items/Research/BioFragment",
        Amount: 2,
        ScalingMultiplier: 1,
        Rarity: "COMMON",
        Transmission: "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo",
        minBuildLabel: gameToBuildVersion["8.0.0"]
    },
    {
        RewardType: "RT_RESOURCE",
        StoreItemType: "/Lotus/StoreItems/Types/Items/MiscItems/Neurode",
        Amount: 1,
        ScalingMultiplier: 1,
        Rarity: "COMMON",
        Transmission: "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo"
    },
    {
        RewardType: "RT_RESOURCE",
        StoreItemType: "/Lotus/StoreItems/Types/Items/MiscItems/OrokinCell",
        Amount: 1,
        ScalingMultiplier: 1,
        Rarity: "COMMON",
        Transmission: "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo"
    },
    {
        RewardType: "RT_RESOURCE",
        StoreItemType: "/Lotus/StoreItems/Types/Items/MiscItems/Cryotic",
        Amount: 50,
        ScalingMultiplier: 1,
        Rarity: "COMMON",
        Transmission: "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo",
        minBuildLabel: gameToBuildVersion["15.0.0"] // Should be U14.5
    },
    {
        RewardType: "RT_RESOURCE",
        StoreItemType: "/Lotus/StoreItems/Types/Items/MiscItems/Tellurium",
        Amount: 1,
        ScalingMultiplier: 1,
        Rarity: "COMMON",
        Transmission: "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo",
        minBuildLabel: gameToBuildVersion["15.14.1"] // Should be U15.7.2
    },
    {
        RewardType: "RT_CREDITS",
        StoreItemType: "",
        Icon: "/Lotus/Interface/Icons/StoreIcons/Currency/CreditsLarge.png",
        Amount: 10000,
        ScalingMultiplier: 1,
        Rarity: "COMMON",
        Transmission: "/Lotus/Sounds/Dialog/DailyTribute/Simaris/DDayTribSmrs"
    },
    {
        RewardType: "RT_BOOSTER",
        StoreItemType: "/Lotus/Types/StoreItems/Boosters/AffinityBoosterStoreItem",
        Amount: 1,
        ScalingMultiplier: 2,
        Duration: 3,
        Rarity: "COMMON",
        Transmission: "/Lotus/Sounds/Dialog/DailyTribute/Simaris/DDayTribSmrs"
    },
    {
        RewardType: "RT_BOOSTER",
        StoreItemType: "/Lotus/Types/StoreItems/Boosters/CreditBoosterStoreItem",
        Amount: 1,
        ScalingMultiplier: 2,
        Duration: 3,
        Rarity: "COMMON",
        Transmission: "/Lotus/Sounds/Dialog/DailyTribute/Simaris/DDayTribSmrs"
    },
    {
        RewardType: "RT_BOOSTER",
        StoreItemType: "/Lotus/Types/StoreItems/Boosters/ResourceAmountBoosterStoreItem",
        Amount: 1,
        ScalingMultiplier: 2,
        Duration: 3,
        Rarity: "COMMON",
        Transmission: "/Lotus/Sounds/Dialog/DailyTribute/Simaris/DDayTribSmrs",
        minBuildLabel: gameToBuildVersion["11.1.3"] // Wiki says its in game since Vanilla
    },
    {
        RewardType: "RT_BOOSTER",
        StoreItemType: "/Lotus/Types/StoreItems/Boosters/ResourceDropChanceBoosterStoreItem",
        Amount: 1,
        ScalingMultiplier: 2,
        Duration: 3,
        Rarity: "COMMON",
        Transmission: "/Lotus/Sounds/Dialog/DailyTribute/Simaris/DDayTribSmrs",
        minBuildLabel: gameToBuildVersion["11.1.3"] // Wiki says its in game since Vanilla
    },
    {
        RewardType: "RT_STORE_ITEM",
        StoreItemType: "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/RareFusionBundle",
        Amount: 1,
        ScalingMultiplier: 1,
        Rarity: "COMMON",
        Transmission: "/Lotus/Sounds/Dialog/DailyTribute/Maroo/DDayTribMaroo",
        minBuildLabel: gameToBuildVersion["18.18.0"]
    },
    {
        RewardType: "RT_RECIPE",
        StoreItemType: "/Lotus/StoreItems/Types/Recipes/Components/FormaBlueprint",
        Amount: 1,
        ScalingMultiplier: 0.5,
        Rarity: "RARE",
        Transmission: "/Lotus/Sounds/Dialog/DailyTribute/Maroo/DDayTribMaroo"
    },
    {
        RewardType: "RT_RANDOM_RECIPE",
        StoreItemType: "",
        Amount: 1,
        ScalingMultiplier: 0,
        Rarity: "COMMON",
        Transmission: "/Lotus/Sounds/Dialog/DailyTribute/Teshin/DDayTribTeshin"
    },
    {
        RewardType: "RT_STORE_ITEM",
        StoreItemType: "/Lotus/StoreItems/Types/BoosterPacks/LoginRewardRandomProjection",
        Amount: 1,
        ScalingMultiplier: 1,
        Rarity: "RARE",
        Transmission: "/Lotus/Sounds/Dialog/DailyTribute/Ordis/DDayTribOrdis"
    }
];