feat: daily tribute (#1241)
Closes #367 Reviewed-on: OpenWF/SpaceNinjaServer#1241
This commit is contained in:
		
							parent
							
								
									e83970d326
								
							
						
					
					
						commit
						6598318fc5
					
				@ -42,8 +42,7 @@ export const loginController: RequestHandler = async (request, response) => {
 | 
			
		||||
                ForceLogoutVersion: 0,
 | 
			
		||||
                ConsentNeeded: false,
 | 
			
		||||
                TrackedSettings: [],
 | 
			
		||||
                Nonce: nonce,
 | 
			
		||||
                LatestEventMessageDate: new Date(0)
 | 
			
		||||
                Nonce: nonce
 | 
			
		||||
            });
 | 
			
		||||
            logger.debug("created new account");
 | 
			
		||||
            response.json(createLoginResponse(myAddress, newAccount, buildLabel));
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,40 @@
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import loginRewards from "@/static/fixed_responses/loginRewards.json";
 | 
			
		||||
import { getAccountForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { claimLoginReward, getRandomLoginRewards, ILoginRewardsReponse } from "@/src/services/loginRewardService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
 | 
			
		||||
const loginRewardsController: RequestHandler = (_req, res) => {
 | 
			
		||||
    res.json(loginRewards);
 | 
			
		||||
export const loginRewardsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const account = await getAccountForRequest(req);
 | 
			
		||||
    const today = Math.trunc(Date.now() / 86400000) * 86400;
 | 
			
		||||
 | 
			
		||||
    if (today == account.LastLoginRewardDate) {
 | 
			
		||||
        res.end();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    account.LoginDays += 1;
 | 
			
		||||
    account.LastLoginRewardDate = today;
 | 
			
		||||
    await account.save();
 | 
			
		||||
 | 
			
		||||
    const inventory = await getInventory(account._id.toString());
 | 
			
		||||
    const randomRewards = getRandomLoginRewards(account, inventory);
 | 
			
		||||
    const isMilestoneDay = account.LoginDays == 5 || account.LoginDays % 50 == 0;
 | 
			
		||||
    const response: ILoginRewardsReponse = {
 | 
			
		||||
        DailyTributeInfo: {
 | 
			
		||||
            Rewards: randomRewards,
 | 
			
		||||
            IsMilestoneDay: isMilestoneDay,
 | 
			
		||||
            IsChooseRewardSet: randomRewards.length != 1,
 | 
			
		||||
            LoginDays: account.LoginDays,
 | 
			
		||||
            //NextMilestoneReward: "",
 | 
			
		||||
            NextMilestoneDay: account.LoginDays < 5 ? 5 : (Math.trunc(account.LoginDays / 50) + 1) * 50,
 | 
			
		||||
            HasChosenReward: false
 | 
			
		||||
        },
 | 
			
		||||
        LastLoginRewardDate: today
 | 
			
		||||
    };
 | 
			
		||||
    if (!isMilestoneDay && randomRewards.length == 1) {
 | 
			
		||||
        response.DailyTributeInfo.HasChosenReward = true;
 | 
			
		||||
        response.DailyTributeInfo.ChosenReward = randomRewards[0];
 | 
			
		||||
        response.DailyTributeInfo.NewInventory = await claimLoginReward(inventory, randomRewards[0]);
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
    }
 | 
			
		||||
    res.json(response);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export { loginRewardsController };
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										57
									
								
								src/controllers/api/loginRewardsSelectionController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/controllers/api/loginRewardsSelectionController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,57 @@
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { claimLoginReward, getRandomLoginRewards } from "@/src/services/loginRewardService";
 | 
			
		||||
import { getAccountForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const loginRewardsSelectionController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const account = await getAccountForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(account._id.toString());
 | 
			
		||||
    const body = JSON.parse(String(req.body)) as ILoginRewardsSelectionRequest;
 | 
			
		||||
    const isMilestoneDay = account.LoginDays == 5 || account.LoginDays % 50 == 0;
 | 
			
		||||
    if (body.IsMilestoneReward != isMilestoneDay) {
 | 
			
		||||
        logger.warn(`Client disagrees on login milestone (got ${body.IsMilestoneReward}, expected ${isMilestoneDay})`);
 | 
			
		||||
    }
 | 
			
		||||
    let chosenReward;
 | 
			
		||||
    let inventoryChanges: IInventoryChanges;
 | 
			
		||||
    if (body.IsMilestoneReward) {
 | 
			
		||||
        chosenReward = {
 | 
			
		||||
            RewardType: "RT_STORE_ITEM",
 | 
			
		||||
            StoreItemType: body.ChosenReward
 | 
			
		||||
        };
 | 
			
		||||
        inventoryChanges = (await handleStoreItemAcquisition(body.ChosenReward, inventory)).InventoryChanges;
 | 
			
		||||
        if (!evergreenRewards.find(x => x == body.ChosenReward)) {
 | 
			
		||||
            inventory.LoginMilestoneRewards.push(body.ChosenReward);
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        const randomRewards = getRandomLoginRewards(account, inventory);
 | 
			
		||||
        chosenReward = randomRewards.find(x => x.StoreItemType == body.ChosenReward)!;
 | 
			
		||||
        inventoryChanges = await claimLoginReward(inventory, chosenReward);
 | 
			
		||||
    }
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json({
 | 
			
		||||
        DailyTributeInfo: {
 | 
			
		||||
            NewInventory: inventoryChanges,
 | 
			
		||||
            ChosenReward: chosenReward
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ILoginRewardsSelectionRequest {
 | 
			
		||||
    ChosenReward: string;
 | 
			
		||||
    IsMilestoneReward: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const evergreenRewards = [
 | 
			
		||||
    "/Lotus/Types/StoreItems/Packages/EvergreenTripleForma",
 | 
			
		||||
    "/Lotus/Types/StoreItems/Packages/EvergreenTripleRifleRiven",
 | 
			
		||||
    "/Lotus/Types/StoreItems/Packages/EvergreenTripleMeleeRiven",
 | 
			
		||||
    "/Lotus/Types/StoreItems/Packages/EvergreenTripleSecondaryRiven",
 | 
			
		||||
    "/Lotus/Types/StoreItems/Packages/EvergreenWeaponSlots",
 | 
			
		||||
    "/Lotus/Types/StoreItems/Packages/EvergreenKuva",
 | 
			
		||||
    "/Lotus/Types/StoreItems/Packages/EvergreenBoosters",
 | 
			
		||||
    "/Lotus/Types/StoreItems/Packages/EvergreenEndo",
 | 
			
		||||
    "/Lotus/Types/StoreItems/Packages/EvergreenExilus"
 | 
			
		||||
];
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { IAccountCreation } from "@/src/types/customTypes";
 | 
			
		||||
import { IDatabaseAccount } from "@/src/types/loginTypes";
 | 
			
		||||
import { IDatabaseAccountRequiredFields } from "@/src/types/loginTypes";
 | 
			
		||||
import crypto from "crypto";
 | 
			
		||||
import { isString, parseEmail, parseString } from "../general";
 | 
			
		||||
 | 
			
		||||
@ -40,7 +40,7 @@ const toAccountCreation = (accountCreation: unknown): IAccountCreation => {
 | 
			
		||||
    throw new Error("incorrect account creation data: incorrect properties");
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const toDatabaseAccount = (createAccount: IAccountCreation): IDatabaseAccount => {
 | 
			
		||||
const toDatabaseAccount = (createAccount: IAccountCreation): IDatabaseAccountRequiredFields => {
 | 
			
		||||
    return {
 | 
			
		||||
        ...createAccount,
 | 
			
		||||
        ClientType: "",
 | 
			
		||||
@ -48,9 +48,8 @@ const toDatabaseAccount = (createAccount: IAccountCreation): IDatabaseAccount =>
 | 
			
		||||
        CrossPlatformAllowed: true,
 | 
			
		||||
        ForceLogoutVersion: 0,
 | 
			
		||||
        TrackedSettings: [],
 | 
			
		||||
        Nonce: 0,
 | 
			
		||||
        LatestEventMessageDate: new Date(0)
 | 
			
		||||
    } satisfies IDatabaseAccount;
 | 
			
		||||
        Nonce: 0
 | 
			
		||||
    } satisfies IDatabaseAccountRequiredFields;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export { toDatabaseAccount, toAccountCreation as toCreateAccount };
 | 
			
		||||
 | 
			
		||||
@ -1366,7 +1366,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
 | 
			
		||||
        ThemeSounds: String,
 | 
			
		||||
 | 
			
		||||
        //Daily LoginRewards
 | 
			
		||||
        LoginMilestoneRewards: [String],
 | 
			
		||||
        LoginMilestoneRewards: { type: [String], default: [] },
 | 
			
		||||
 | 
			
		||||
        //You first Dialog with NPC or use new Item
 | 
			
		||||
        NodeIntrosCompleted: [String],
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,9 @@ const databaseAccountSchema = new Schema<IDatabaseAccountJson>(
 | 
			
		||||
        TrackedSettings: { type: [String], default: [] },
 | 
			
		||||
        Nonce: { type: Number, default: 0 },
 | 
			
		||||
        Dropped: Boolean,
 | 
			
		||||
        LatestEventMessageDate: { type: Date, default: 0 }
 | 
			
		||||
        LatestEventMessageDate: { type: Date, default: 0 },
 | 
			
		||||
        LastLoginRewardDate: { type: Number, default: 0 },
 | 
			
		||||
        LoginDays: { type: Number, default: 0 }
 | 
			
		||||
    },
 | 
			
		||||
    opts
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
@ -63,6 +63,7 @@ import { inventorySlotsController } from "@/src/controllers/api/inventorySlotsCo
 | 
			
		||||
import { joinSessionController } from "@/src/controllers/api/joinSessionController";
 | 
			
		||||
import { loginController } from "@/src/controllers/api/loginController";
 | 
			
		||||
import { loginRewardsController } from "@/src/controllers/api/loginRewardsController";
 | 
			
		||||
import { loginRewardsSelectionController } from "@/src/controllers/api/loginRewardsSelectionController";
 | 
			
		||||
import { logoutController } from "@/src/controllers/api/logoutController";
 | 
			
		||||
import { marketRecommendationsController } from "@/src/controllers/api/marketRecommendationsController";
 | 
			
		||||
import { missionInventoryUpdateController } from "@/src/controllers/api/missionInventoryUpdateController";
 | 
			
		||||
@ -202,6 +203,7 @@ apiRouter.post("/infestedFoundry.php", infestedFoundryController);
 | 
			
		||||
apiRouter.post("/inventorySlots.php", inventorySlotsController);
 | 
			
		||||
apiRouter.post("/joinSession.php", joinSessionController);
 | 
			
		||||
apiRouter.post("/login.php", loginController);
 | 
			
		||||
apiRouter.post("/loginRewardsSelection.php", loginRewardsSelectionController);
 | 
			
		||||
apiRouter.post("/missionInventoryUpdate.php", missionInventoryUpdateController);
 | 
			
		||||
apiRouter.post("/modularWeaponCrafting.php", modularWeaponCraftingController);
 | 
			
		||||
apiRouter.post("/modularWeaponSale.php", modularWeaponSaleController);
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										140
									
								
								src/services/loginRewardService.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								src/services/loginRewardService.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,140 @@
 | 
			
		||||
import randomRewards from "@/static/fixed_responses/loginRewards/randomRewards.json";
 | 
			
		||||
import { IInventoryChanges } from "../types/purchaseTypes";
 | 
			
		||||
import { TAccountDocument } from "./loginService";
 | 
			
		||||
import { CRng, mixSeeds } from "./rngService";
 | 
			
		||||
import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel";
 | 
			
		||||
import { addBooster, updateCurrency } from "./inventoryService";
 | 
			
		||||
import { handleStoreItemAcquisition } from "./purchaseService";
 | 
			
		||||
import { ExportBoosters, ExportRecipes, ExportWarframes, ExportWeapons } from "warframe-public-export-plus";
 | 
			
		||||
import { toStoreItem } from "./itemDataService";
 | 
			
		||||
 | 
			
		||||
export interface ILoginRewardsReponse {
 | 
			
		||||
    DailyTributeInfo: {
 | 
			
		||||
        Rewards?: ILoginReward[]; // only set on first call of the day
 | 
			
		||||
        IsMilestoneDay?: boolean;
 | 
			
		||||
        IsChooseRewardSet?: boolean;
 | 
			
		||||
        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: "";
 | 
			
		||||
    StoreItemType: string; // uniquely identifies the reward
 | 
			
		||||
    //ProductCategory: "Pistols";
 | 
			
		||||
    Amount: number;
 | 
			
		||||
    ScalingMultiplier: number;
 | 
			
		||||
    //Durability: "COMMON";
 | 
			
		||||
    //DisplayName: "";
 | 
			
		||||
    Duration: number;
 | 
			
		||||
    //CouponSku: number;
 | 
			
		||||
    //Rarity: number;
 | 
			
		||||
    Transmission: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 getRandomLoginRewards = (
 | 
			
		||||
    account: TAccountDocument,
 | 
			
		||||
    inventory: TInventoryDatabaseDocument
 | 
			
		||||
): ILoginReward[] => {
 | 
			
		||||
    const accountSeed = parseInt(account._id.toString().substring(16), 16);
 | 
			
		||||
    const rng = new CRng(mixSeeds(accountSeed, account.LoginDays));
 | 
			
		||||
    const rewards = [getRandomLoginReward(rng, account.LoginDays, inventory)];
 | 
			
		||||
    // Using 25% an approximate chance for pick-a-doors. More conclusive data analysis is needed.
 | 
			
		||||
    if (rng.random() < 0.25) {
 | 
			
		||||
        do {
 | 
			
		||||
            const reward = getRandomLoginReward(rng, account.LoginDays, inventory);
 | 
			
		||||
            if (!rewards.find(x => x.StoreItemType == reward.StoreItemType)) {
 | 
			
		||||
                rewards.push(reward);
 | 
			
		||||
            }
 | 
			
		||||
        } while (rewards.length != 3);
 | 
			
		||||
    }
 | 
			
		||||
    return rewards;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getRandomLoginReward = (rng: CRng, day: number, inventory: TInventoryDatabaseDocument): ILoginReward => {
 | 
			
		||||
    const reward = rng.randomReward(randomRewards)!;
 | 
			
		||||
    //const reward = randomRewards.find(x => x.RewardType == "RT_BOOSTER")!;
 | 
			
		||||
    if (reward.RewardType == "RT_RANDOM_RECIPE") {
 | 
			
		||||
        // Not very faithful implementation but roughly the same idea
 | 
			
		||||
        const masteredItems = new Set();
 | 
			
		||||
        for (const entry of inventory.XPInfo) {
 | 
			
		||||
            masteredItems.add(entry.ItemType);
 | 
			
		||||
        }
 | 
			
		||||
        const unmasteredItems = new Set();
 | 
			
		||||
        for (const uniqueName of Object.keys(ExportWeapons)) {
 | 
			
		||||
            if (!masteredItems.has(uniqueName)) {
 | 
			
		||||
                unmasteredItems.add(uniqueName);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        for (const uniqueName of Object.keys(ExportWarframes)) {
 | 
			
		||||
            if (!masteredItems.has(uniqueName)) {
 | 
			
		||||
                unmasteredItems.add(uniqueName);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        const eligibleRecipes: string[] = [];
 | 
			
		||||
        for (const [uniqueName, recipe] of Object.entries(ExportRecipes)) {
 | 
			
		||||
            if (unmasteredItems.has(recipe.resultType)) {
 | 
			
		||||
                eligibleRecipes.push(uniqueName);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        reward.StoreItemType = toStoreItem(rng.randomElement(eligibleRecipes));
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
        //_id: toOid(new Types.ObjectId()),
 | 
			
		||||
        RewardType: reward.RewardType,
 | 
			
		||||
        //CouponType: "CPT_PLATINUM",
 | 
			
		||||
        Icon: reward.Icon ?? "",
 | 
			
		||||
        //ItemType: "",
 | 
			
		||||
        StoreItemType: reward.StoreItemType,
 | 
			
		||||
        //ProductCategory: "Pistols",
 | 
			
		||||
        Amount: reward.Duration ? 1 : Math.round(scaleAmount(day, reward.Amount, reward.ScalingMultiplier)),
 | 
			
		||||
        ScalingMultiplier: reward.ScalingMultiplier,
 | 
			
		||||
        //Durability: "COMMON",
 | 
			
		||||
        //DisplayName: "",
 | 
			
		||||
        Duration: reward.Duration ? Math.round(reward.Duration * scaleAmount(day, 1, reward.ScalingMultiplier)) : 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)).InventoryChanges;
 | 
			
		||||
 | 
			
		||||
        case "RT_CREDITS":
 | 
			
		||||
            return updateCurrency(inventory, -reward.Amount, false);
 | 
			
		||||
 | 
			
		||||
        case "RT_BOOSTER": {
 | 
			
		||||
            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}`);
 | 
			
		||||
};
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { Account } from "@/src/models/loginModel";
 | 
			
		||||
import { createInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { IDatabaseAccount, IDatabaseAccountJson } from "@/src/types/loginTypes";
 | 
			
		||||
import { IDatabaseAccountJson, IDatabaseAccountRequiredFields } from "@/src/types/loginTypes";
 | 
			
		||||
import { createShip } from "./shipService";
 | 
			
		||||
import { Document, Types } from "mongoose";
 | 
			
		||||
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
 | 
			
		||||
@ -18,7 +18,7 @@ export const isNameTaken = async (name: string): Promise<boolean> => {
 | 
			
		||||
    return !!(await Account.findOne({ DisplayName: name }));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const createAccount = async (accountData: IDatabaseAccount): Promise<IDatabaseAccountJson> => {
 | 
			
		||||
export const createAccount = async (accountData: IDatabaseAccountRequiredFields): Promise<IDatabaseAccountJson> => {
 | 
			
		||||
    const account = new Account(accountData);
 | 
			
		||||
    try {
 | 
			
		||||
        await account.save();
 | 
			
		||||
@ -62,7 +62,7 @@ export const createPersonalRooms = async (accountId: Types.ObjectId, shipId: Typ
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/ban-types
 | 
			
		||||
type TAccountDocument = Document<unknown, {}, IDatabaseAccountJson> &
 | 
			
		||||
export type TAccountDocument = Document<unknown, {}, IDatabaseAccountJson> &
 | 
			
		||||
    IDatabaseAccountJson & { _id: Types.ObjectId; __v: number };
 | 
			
		||||
 | 
			
		||||
export const getAccountForRequest = async (req: Request): Promise<TAccountDocument> => {
 | 
			
		||||
 | 
			
		||||
@ -18,11 +18,11 @@ export const getRandomInt = (min: number, max: number): number => {
 | 
			
		||||
    return Math.floor(Math.random() * (max - min + 1)) + min;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getRandomReward = <T extends { probability: number }>(pool: T[]): T | undefined => {
 | 
			
		||||
const getRewardAtPercentage = <T extends { probability: number }>(pool: T[], percentage: number): T | undefined => {
 | 
			
		||||
    if (pool.length == 0) return;
 | 
			
		||||
 | 
			
		||||
    const totalChance = pool.reduce((accum, item) => accum + item.probability, 0);
 | 
			
		||||
    const randomValue = Math.random() * totalChance;
 | 
			
		||||
    const randomValue = percentage * totalChance;
 | 
			
		||||
 | 
			
		||||
    let cumulativeChance = 0;
 | 
			
		||||
    for (const item of pool) {
 | 
			
		||||
@ -34,6 +34,10 @@ export const getRandomReward = <T extends { probability: number }>(pool: T[]): T
 | 
			
		||||
    throw new Error("What the fuck?");
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getRandomReward = <T extends { probability: number }>(pool: T[]): T | undefined => {
 | 
			
		||||
    return getRewardAtPercentage(pool, Math.random());
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getRandomWeightedReward = <T extends { rarity: TRarity }>(
 | 
			
		||||
    pool: T[],
 | 
			
		||||
    weights: Record<TRarity, number>
 | 
			
		||||
@ -70,6 +74,15 @@ export const getRandomWeightedRewardUc = <T extends { Rarity: TRarity }>(
 | 
			
		||||
    return getRandomReward(resultPool);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// ChatGPT generated this. It seems to have a good enough distribution.
 | 
			
		||||
export const mixSeeds = (seed1: number, seed2: number): number => {
 | 
			
		||||
    let seed = seed1 ^ seed2;
 | 
			
		||||
    seed ^= seed >>> 21;
 | 
			
		||||
    seed ^= seed << 35;
 | 
			
		||||
    seed ^= seed >>> 4;
 | 
			
		||||
    return seed >>> 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Seeded RNG for internal usage. Based on recommendations in the ISO C standards.
 | 
			
		||||
export class CRng {
 | 
			
		||||
    state: number;
 | 
			
		||||
@ -92,6 +105,10 @@ export class CRng {
 | 
			
		||||
    randomElement<T>(arr: T[]): T {
 | 
			
		||||
        return arr[Math.floor(this.random() * arr.length)];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    randomReward<T extends { probability: number }>(pool: T[]): T | undefined {
 | 
			
		||||
        return getRewardAtPercentage(pool, this.random());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Seeded RNG for cases where we need identical results to the game client. Based on work by Donald Knuth.
 | 
			
		||||
 | 
			
		||||
@ -11,11 +11,16 @@ export interface IAccountAndLoginResponseCommons {
 | 
			
		||||
    Nonce: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IDatabaseAccount extends IAccountAndLoginResponseCommons {
 | 
			
		||||
export interface IDatabaseAccountRequiredFields extends IAccountAndLoginResponseCommons {
 | 
			
		||||
    email: string;
 | 
			
		||||
    password: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IDatabaseAccount extends IDatabaseAccountRequiredFields {
 | 
			
		||||
    Dropped?: boolean;
 | 
			
		||||
    LatestEventMessageDate: Date;
 | 
			
		||||
    LastLoginRewardDate: number;
 | 
			
		||||
    LoginDays: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Includes virtual ID
 | 
			
		||||
 | 
			
		||||
@ -1,9 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "DailyTributeInfo": {
 | 
			
		||||
    "IsMilestoneDay": false,
 | 
			
		||||
    "IsChooseRewardSet": true,
 | 
			
		||||
    "LoginDays": 1337,
 | 
			
		||||
    "NextMilestoneReward": "",
 | 
			
		||||
    "NextMilestoneDay": 50
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										187
									
								
								static/fixed_responses/loginRewards/randomRewards.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								static/fixed_responses/loginRewards/randomRewards.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,187 @@
 | 
			
		||||
[
 | 
			
		||||
  {
 | 
			
		||||
    "RewardType": "RT_RESOURCE",
 | 
			
		||||
    "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/OxiumAlloy",
 | 
			
		||||
    "Amount": 100,
 | 
			
		||||
    "ScalingMultiplier": 1,
 | 
			
		||||
    "Rarity": "COMMON",
 | 
			
		||||
    "probability": 0.055392516507703576,
 | 
			
		||||
    "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Ordis/DDayTribOrdis"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "RewardType": "RT_RESOURCE",
 | 
			
		||||
    "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/Gallium",
 | 
			
		||||
    "Amount": 1,
 | 
			
		||||
    "ScalingMultiplier": 1,
 | 
			
		||||
    "Rarity": "COMMON",
 | 
			
		||||
    "probability": 0.055392516507703576,
 | 
			
		||||
    "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "RewardType": "RT_RESOURCE",
 | 
			
		||||
    "StoreItemType": "/Lotus/StoreItems/Types/Items/Research/ChemFragment",
 | 
			
		||||
    "Amount": 2,
 | 
			
		||||
    "ScalingMultiplier": 1,
 | 
			
		||||
    "Rarity": "COMMON",
 | 
			
		||||
    "probability": 0.055392516507703576,
 | 
			
		||||
    "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "RewardType": "RT_RESOURCE",
 | 
			
		||||
    "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/Morphic",
 | 
			
		||||
    "Amount": 1,
 | 
			
		||||
    "ScalingMultiplier": 1,
 | 
			
		||||
    "Rarity": "COMMON",
 | 
			
		||||
    "probability": 0.055392516507703576,
 | 
			
		||||
    "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "RewardType": "RT_RESOURCE",
 | 
			
		||||
    "StoreItemType": "/Lotus/StoreItems/Types/Items/Research/EnergyFragment",
 | 
			
		||||
    "Amount": 2,
 | 
			
		||||
    "ScalingMultiplier": 1,
 | 
			
		||||
    "Rarity": "COMMON",
 | 
			
		||||
    "probability": 0.055392516507703576,
 | 
			
		||||
    "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "RewardType": "RT_RESOURCE",
 | 
			
		||||
    "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/NeuralSensor",
 | 
			
		||||
    "Amount": 1,
 | 
			
		||||
    "ScalingMultiplier": 1,
 | 
			
		||||
    "Rarity": "COMMON",
 | 
			
		||||
    "probability": 0.055392516507703576,
 | 
			
		||||
    "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "RewardType": "RT_RESOURCE",
 | 
			
		||||
    "StoreItemType": "/Lotus/StoreItems/Types/Items/Research/BioFragment",
 | 
			
		||||
    "Amount": 2,
 | 
			
		||||
    "ScalingMultiplier": 1,
 | 
			
		||||
    "Rarity": "COMMON",
 | 
			
		||||
    "probability": 0.055392516507703576,
 | 
			
		||||
    "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "RewardType": "RT_RESOURCE",
 | 
			
		||||
    "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/Neurode",
 | 
			
		||||
    "Amount": 1,
 | 
			
		||||
    "ScalingMultiplier": 1,
 | 
			
		||||
    "Rarity": "COMMON",
 | 
			
		||||
    "probability": 0.055392516507703576,
 | 
			
		||||
    "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "RewardType": "RT_RESOURCE",
 | 
			
		||||
    "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/OrokinCell",
 | 
			
		||||
    "Amount": 1,
 | 
			
		||||
    "ScalingMultiplier": 1,
 | 
			
		||||
    "Rarity": "COMMON",
 | 
			
		||||
    "probability": 0.055392516507703576,
 | 
			
		||||
    "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "RewardType": "RT_RESOURCE",
 | 
			
		||||
    "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/Cryotic",
 | 
			
		||||
    "Amount": 50,
 | 
			
		||||
    "ScalingMultiplier": 1,
 | 
			
		||||
    "Rarity": "COMMON",
 | 
			
		||||
    "probability": 0.055392516507703576,
 | 
			
		||||
    "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "RewardType": "RT_RESOURCE",
 | 
			
		||||
    "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/Tellurium",
 | 
			
		||||
    "Amount": 1,
 | 
			
		||||
    "ScalingMultiplier": 1,
 | 
			
		||||
    "Rarity": "COMMON",
 | 
			
		||||
    "probability": 0.055392516507703576,
 | 
			
		||||
    "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "RewardType": "RT_CREDITS",
 | 
			
		||||
    "StoreItemType": "",
 | 
			
		||||
    "Icon": "/Lotus/Interface/Icons/StoreIcons/Currency/CreditsLarge.png",
 | 
			
		||||
    "Amount": 10000,
 | 
			
		||||
    "ScalingMultiplier": 1,
 | 
			
		||||
    "Rarity": "COMMON",
 | 
			
		||||
    "probability": 0.055392516507703576,
 | 
			
		||||
    "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Simaris/DDayTribSmrs"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "RewardType": "RT_BOOSTER",
 | 
			
		||||
    "StoreItemType": "/Lotus/Types/StoreItems/Boosters/AffinityBoosterStoreItem",
 | 
			
		||||
    "Amount": 1,
 | 
			
		||||
    "ScalingMultiplier": 2,
 | 
			
		||||
    "Duration": 3,
 | 
			
		||||
    "Rarity": "COMMON",
 | 
			
		||||
    "probability": 0.055392516507703576,
 | 
			
		||||
    "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Simaris/DDayTribSmrs"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "RewardType": "RT_BOOSTER",
 | 
			
		||||
    "StoreItemType": "/Lotus/Types/StoreItems/Boosters/CreditBoosterStoreItem",
 | 
			
		||||
    "Amount": 1,
 | 
			
		||||
    "ScalingMultiplier": 2,
 | 
			
		||||
    "Duration": 3,
 | 
			
		||||
    "Rarity": "COMMON",
 | 
			
		||||
    "probability": 0.055392516507703576,
 | 
			
		||||
    "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Simaris/DDayTribSmrs"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "RewardType": "RT_BOOSTER",
 | 
			
		||||
    "StoreItemType": "/Lotus/Types/StoreItems/Boosters/ResourceAmountBoosterStoreItem",
 | 
			
		||||
    "Amount": 1,
 | 
			
		||||
    "ScalingMultiplier": 2,
 | 
			
		||||
    "Duration": 3,
 | 
			
		||||
    "Rarity": "COMMON",
 | 
			
		||||
    "probability": 0.055392516507703576,
 | 
			
		||||
    "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Simaris/DDayTribSmrs"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "RewardType": "RT_BOOSTER",
 | 
			
		||||
    "StoreItemType": "/Lotus/Types/StoreItems/Boosters/ResourceDropChanceBoosterStoreItem",
 | 
			
		||||
    "Amount": 1,
 | 
			
		||||
    "ScalingMultiplier": 2,
 | 
			
		||||
    "Duration": 3,
 | 
			
		||||
    "Rarity": "COMMON",
 | 
			
		||||
    "probability": 0.055392516507703576,
 | 
			
		||||
    "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Simaris/DDayTribSmrs"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "RewardType": "RT_STORE_ITEM",
 | 
			
		||||
    "StoreItemType": "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/RareFusionBundle",
 | 
			
		||||
    "Amount": 1,
 | 
			
		||||
    "ScalingMultiplier": 1,
 | 
			
		||||
    "Rarity": "COMMON",
 | 
			
		||||
    "probability": 0.055392516507703576,
 | 
			
		||||
    "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Maroo/DDayTribMaroo"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "RewardType": "RT_RECIPE",
 | 
			
		||||
    "StoreItemType": "/Lotus/StoreItems/Types/Recipes/Components/FormaBlueprint",
 | 
			
		||||
    "Amount": 1,
 | 
			
		||||
    "ScalingMultiplier": 0.5,
 | 
			
		||||
    "Rarity": "RARE",
 | 
			
		||||
    "probability": 0.001467351430667816,
 | 
			
		||||
    "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Maroo/DDayTribMaroo"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "RewardType": "RT_RANDOM_RECIPE",
 | 
			
		||||
    "StoreItemType": "",
 | 
			
		||||
    "Amount": 1,
 | 
			
		||||
    "ScalingMultiplier": 0,
 | 
			
		||||
    "Rarity": "COMMON",
 | 
			
		||||
    "probability": 0.055392516507703576,
 | 
			
		||||
    "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Teshin/DDayTribTeshin"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "RewardType": "RT_STORE_ITEM",
 | 
			
		||||
    "StoreItemType": "/Lotus/StoreItems/Types/BoosterPacks/LoginRewardRandomProjection",
 | 
			
		||||
    "Amount": 1,
 | 
			
		||||
    "ScalingMultiplier": 1,
 | 
			
		||||
    "Rarity": "RARE",
 | 
			
		||||
    "probability": 0.001467351430667816,
 | 
			
		||||
    "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Ordis/DDayTribOrdis"
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user