forked from OpenWF/SpaceNinjaServer
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,
|
ForceLogoutVersion: 0,
|
||||||
ConsentNeeded: false,
|
ConsentNeeded: false,
|
||||||
TrackedSettings: [],
|
TrackedSettings: [],
|
||||||
Nonce: nonce,
|
Nonce: nonce
|
||||||
LatestEventMessageDate: new Date(0)
|
|
||||||
});
|
});
|
||||||
logger.debug("created new account");
|
logger.debug("created new account");
|
||||||
response.json(createLoginResponse(myAddress, newAccount, buildLabel));
|
response.json(createLoginResponse(myAddress, newAccount, buildLabel));
|
||||||
|
@ -1,8 +1,40 @@
|
|||||||
import { RequestHandler } from "express";
|
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) => {
|
export const loginRewardsController: RequestHandler = async (req, res) => {
|
||||||
res.json(loginRewards);
|
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 { IAccountCreation } from "@/src/types/customTypes";
|
||||||
import { IDatabaseAccount } from "@/src/types/loginTypes";
|
import { IDatabaseAccountRequiredFields } from "@/src/types/loginTypes";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { isString, parseEmail, parseString } from "../general";
|
import { isString, parseEmail, parseString } from "../general";
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ const toAccountCreation = (accountCreation: unknown): IAccountCreation => {
|
|||||||
throw new Error("incorrect account creation data: incorrect properties");
|
throw new Error("incorrect account creation data: incorrect properties");
|
||||||
};
|
};
|
||||||
|
|
||||||
const toDatabaseAccount = (createAccount: IAccountCreation): IDatabaseAccount => {
|
const toDatabaseAccount = (createAccount: IAccountCreation): IDatabaseAccountRequiredFields => {
|
||||||
return {
|
return {
|
||||||
...createAccount,
|
...createAccount,
|
||||||
ClientType: "",
|
ClientType: "",
|
||||||
@ -48,9 +48,8 @@ const toDatabaseAccount = (createAccount: IAccountCreation): IDatabaseAccount =>
|
|||||||
CrossPlatformAllowed: true,
|
CrossPlatformAllowed: true,
|
||||||
ForceLogoutVersion: 0,
|
ForceLogoutVersion: 0,
|
||||||
TrackedSettings: [],
|
TrackedSettings: [],
|
||||||
Nonce: 0,
|
Nonce: 0
|
||||||
LatestEventMessageDate: new Date(0)
|
} satisfies IDatabaseAccountRequiredFields;
|
||||||
} satisfies IDatabaseAccount;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export { toDatabaseAccount, toAccountCreation as toCreateAccount };
|
export { toDatabaseAccount, toAccountCreation as toCreateAccount };
|
||||||
|
@ -1366,7 +1366,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
|
|||||||
ThemeSounds: String,
|
ThemeSounds: String,
|
||||||
|
|
||||||
//Daily LoginRewards
|
//Daily LoginRewards
|
||||||
LoginMilestoneRewards: [String],
|
LoginMilestoneRewards: { type: [String], default: [] },
|
||||||
|
|
||||||
//You first Dialog with NPC or use new Item
|
//You first Dialog with NPC or use new Item
|
||||||
NodeIntrosCompleted: [String],
|
NodeIntrosCompleted: [String],
|
||||||
|
@ -21,7 +21,9 @@ const databaseAccountSchema = new Schema<IDatabaseAccountJson>(
|
|||||||
TrackedSettings: { type: [String], default: [] },
|
TrackedSettings: { type: [String], default: [] },
|
||||||
Nonce: { type: Number, default: 0 },
|
Nonce: { type: Number, default: 0 },
|
||||||
Dropped: Boolean,
|
Dropped: Boolean,
|
||||||
LatestEventMessageDate: { type: Date, default: 0 }
|
LatestEventMessageDate: { type: Date, default: 0 },
|
||||||
|
LastLoginRewardDate: { type: Number, default: 0 },
|
||||||
|
LoginDays: { type: Number, default: 0 }
|
||||||
},
|
},
|
||||||
opts
|
opts
|
||||||
);
|
);
|
||||||
|
@ -63,6 +63,7 @@ import { inventorySlotsController } from "@/src/controllers/api/inventorySlotsCo
|
|||||||
import { joinSessionController } from "@/src/controllers/api/joinSessionController";
|
import { joinSessionController } from "@/src/controllers/api/joinSessionController";
|
||||||
import { loginController } from "@/src/controllers/api/loginController";
|
import { loginController } from "@/src/controllers/api/loginController";
|
||||||
import { loginRewardsController } from "@/src/controllers/api/loginRewardsController";
|
import { loginRewardsController } from "@/src/controllers/api/loginRewardsController";
|
||||||
|
import { loginRewardsSelectionController } from "@/src/controllers/api/loginRewardsSelectionController";
|
||||||
import { logoutController } from "@/src/controllers/api/logoutController";
|
import { logoutController } from "@/src/controllers/api/logoutController";
|
||||||
import { marketRecommendationsController } from "@/src/controllers/api/marketRecommendationsController";
|
import { marketRecommendationsController } from "@/src/controllers/api/marketRecommendationsController";
|
||||||
import { missionInventoryUpdateController } from "@/src/controllers/api/missionInventoryUpdateController";
|
import { missionInventoryUpdateController } from "@/src/controllers/api/missionInventoryUpdateController";
|
||||||
@ -202,6 +203,7 @@ apiRouter.post("/infestedFoundry.php", infestedFoundryController);
|
|||||||
apiRouter.post("/inventorySlots.php", inventorySlotsController);
|
apiRouter.post("/inventorySlots.php", inventorySlotsController);
|
||||||
apiRouter.post("/joinSession.php", joinSessionController);
|
apiRouter.post("/joinSession.php", joinSessionController);
|
||||||
apiRouter.post("/login.php", loginController);
|
apiRouter.post("/login.php", loginController);
|
||||||
|
apiRouter.post("/loginRewardsSelection.php", loginRewardsSelectionController);
|
||||||
apiRouter.post("/missionInventoryUpdate.php", missionInventoryUpdateController);
|
apiRouter.post("/missionInventoryUpdate.php", missionInventoryUpdateController);
|
||||||
apiRouter.post("/modularWeaponCrafting.php", modularWeaponCraftingController);
|
apiRouter.post("/modularWeaponCrafting.php", modularWeaponCraftingController);
|
||||||
apiRouter.post("/modularWeaponSale.php", modularWeaponSaleController);
|
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 { Account } from "@/src/models/loginModel";
|
||||||
import { createInventory } from "@/src/services/inventoryService";
|
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 { createShip } from "./shipService";
|
||||||
import { Document, Types } from "mongoose";
|
import { Document, Types } from "mongoose";
|
||||||
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
|
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 }));
|
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);
|
const account = new Account(accountData);
|
||||||
try {
|
try {
|
||||||
await account.save();
|
await account.save();
|
||||||
@ -62,7 +62,7 @@ export const createPersonalRooms = async (accountId: Types.ObjectId, shipId: Typ
|
|||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// 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 };
|
IDatabaseAccountJson & { _id: Types.ObjectId; __v: number };
|
||||||
|
|
||||||
export const getAccountForRequest = async (req: Request): Promise<TAccountDocument> => {
|
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;
|
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;
|
if (pool.length == 0) return;
|
||||||
|
|
||||||
const totalChance = pool.reduce((accum, item) => accum + item.probability, 0);
|
const totalChance = pool.reduce((accum, item) => accum + item.probability, 0);
|
||||||
const randomValue = Math.random() * totalChance;
|
const randomValue = percentage * totalChance;
|
||||||
|
|
||||||
let cumulativeChance = 0;
|
let cumulativeChance = 0;
|
||||||
for (const item of pool) {
|
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?");
|
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 }>(
|
export const getRandomWeightedReward = <T extends { rarity: TRarity }>(
|
||||||
pool: T[],
|
pool: T[],
|
||||||
weights: Record<TRarity, number>
|
weights: Record<TRarity, number>
|
||||||
@ -70,6 +74,15 @@ export const getRandomWeightedRewardUc = <T extends { Rarity: TRarity }>(
|
|||||||
return getRandomReward(resultPool);
|
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.
|
// Seeded RNG for internal usage. Based on recommendations in the ISO C standards.
|
||||||
export class CRng {
|
export class CRng {
|
||||||
state: number;
|
state: number;
|
||||||
@ -92,6 +105,10 @@ export class CRng {
|
|||||||
randomElement<T>(arr: T[]): T {
|
randomElement<T>(arr: T[]): T {
|
||||||
return arr[Math.floor(this.random() * arr.length)];
|
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.
|
// 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;
|
Nonce: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDatabaseAccount extends IAccountAndLoginResponseCommons {
|
export interface IDatabaseAccountRequiredFields extends IAccountAndLoginResponseCommons {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDatabaseAccount extends IDatabaseAccountRequiredFields {
|
||||||
Dropped?: boolean;
|
Dropped?: boolean;
|
||||||
LatestEventMessageDate: Date;
|
LatestEventMessageDate: Date;
|
||||||
|
LastLoginRewardDate: number;
|
||||||
|
LoginDays: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Includes virtual ID
|
// 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