From 6598318fc5cfb2283a5e441a25d7f8d03244e3e9 Mon Sep 17 00:00:00 2001 From: Sainan Date: Fri, 21 Mar 2025 05:19:42 -0700 Subject: [PATCH] feat: daily tribute (#1241) Closes #367 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1241 --- src/controllers/api/loginController.ts | 3 +- src/controllers/api/loginRewardsController.ts | 42 +++- .../api/loginRewardsSelectionController.ts | 57 ++++++ src/helpers/customHelpers/customHelpers.ts | 9 +- src/models/inventoryModels/inventoryModel.ts | 2 +- src/models/loginModel.ts | 4 +- src/routes/api.ts | 2 + src/services/loginRewardService.ts | 140 +++++++++++++ src/services/loginService.ts | 6 +- src/services/rngService.ts | 21 +- src/types/loginTypes.ts | 7 +- static/fixed_responses/loginRewards.json | 9 - .../loginRewards/randomRewards.json | 187 ++++++++++++++++++ 13 files changed, 460 insertions(+), 29 deletions(-) create mode 100644 src/controllers/api/loginRewardsSelectionController.ts create mode 100644 src/services/loginRewardService.ts delete mode 100644 static/fixed_responses/loginRewards.json create mode 100644 static/fixed_responses/loginRewards/randomRewards.json diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index 0e7c53fb..9f70d0ad 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -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)); diff --git a/src/controllers/api/loginRewardsController.ts b/src/controllers/api/loginRewardsController.ts index fd4b40c4..f6430e28 100644 --- a/src/controllers/api/loginRewardsController.ts +++ b/src/controllers/api/loginRewardsController.ts @@ -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 }; diff --git a/src/controllers/api/loginRewardsSelectionController.ts b/src/controllers/api/loginRewardsSelectionController.ts new file mode 100644 index 00000000..b1ebc9a6 --- /dev/null +++ b/src/controllers/api/loginRewardsSelectionController.ts @@ -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" +]; diff --git a/src/helpers/customHelpers/customHelpers.ts b/src/helpers/customHelpers/customHelpers.ts index e1173d1f..a0163833 100644 --- a/src/helpers/customHelpers/customHelpers.ts +++ b/src/helpers/customHelpers/customHelpers.ts @@ -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 }; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 0f087d7e..238779f6 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1366,7 +1366,7 @@ const inventorySchema = new Schema( ThemeSounds: String, //Daily LoginRewards - LoginMilestoneRewards: [String], + LoginMilestoneRewards: { type: [String], default: [] }, //You first Dialog with NPC or use new Item NodeIntrosCompleted: [String], diff --git a/src/models/loginModel.ts b/src/models/loginModel.ts index 47cbe8ff..2218a27d 100644 --- a/src/models/loginModel.ts +++ b/src/models/loginModel.ts @@ -21,7 +21,9 @@ const databaseAccountSchema = new Schema( 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 ); diff --git a/src/routes/api.ts b/src/routes/api.ts index 15a6d32e..0a1c238b 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -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); diff --git a/src/services/loginRewardService.ts b/src/services/loginRewardService.ts new file mode 100644 index 00000000..4d5c562a --- /dev/null +++ b/src/services/loginRewardService.ts @@ -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 => { + 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}`); +}; diff --git a/src/services/loginService.ts b/src/services/loginService.ts index 08d12ee9..099103be 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -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 => { return !!(await Account.findOne({ DisplayName: name })); }; -export const createAccount = async (accountData: IDatabaseAccount): Promise => { +export const createAccount = async (accountData: IDatabaseAccountRequiredFields): Promise => { 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 & +export type TAccountDocument = Document & IDatabaseAccountJson & { _id: Types.ObjectId; __v: number }; export const getAccountForRequest = async (req: Request): Promise => { diff --git a/src/services/rngService.ts b/src/services/rngService.ts index 9791b5c2..cb8f2cba 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -18,11 +18,11 @@ export const getRandomInt = (min: number, max: number): number => { return Math.floor(Math.random() * (max - min + 1)) + min; }; -export const getRandomReward = (pool: T[]): T | undefined => { +const getRewardAtPercentage = (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 = (pool: T[]): T throw new Error("What the fuck?"); }; +export const getRandomReward = (pool: T[]): T | undefined => { + return getRewardAtPercentage(pool, Math.random()); +}; + export const getRandomWeightedReward = ( pool: T[], weights: Record @@ -70,6 +74,15 @@ export const getRandomWeightedRewardUc = ( 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(arr: T[]): T { return arr[Math.floor(this.random() * arr.length)]; } + + randomReward(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. diff --git a/src/types/loginTypes.ts b/src/types/loginTypes.ts index 0aaf2eed..8c443797 100644 --- a/src/types/loginTypes.ts +++ b/src/types/loginTypes.ts @@ -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 diff --git a/static/fixed_responses/loginRewards.json b/static/fixed_responses/loginRewards.json deleted file mode 100644 index b6632556..00000000 --- a/static/fixed_responses/loginRewards.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "DailyTributeInfo": { - "IsMilestoneDay": false, - "IsChooseRewardSet": true, - "LoginDays": 1337, - "NextMilestoneReward": "", - "NextMilestoneDay": 50 - } -} diff --git a/static/fixed_responses/loginRewards/randomRewards.json b/static/fixed_responses/loginRewards/randomRewards.json new file mode 100644 index 00000000..20d3b60c --- /dev/null +++ b/static/fixed_responses/loginRewards/randomRewards.json @@ -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" + } +]