feat: daily tribute
All checks were successful
Build / build (22) (push) Successful in 42s
Build / build (18) (push) Successful in 1m11s
Build / build (20) (push) Successful in 1m7s
Build / build (18) (pull_request) Successful in 45s
Build / build (20) (pull_request) Successful in 1m10s
Build / build (22) (pull_request) Successful in 1m10s

This commit is contained in:
Sainan 2025-03-20 14:30:37 +01:00
parent 88c5999d07
commit c3802b49ca
12 changed files with 444 additions and 28 deletions

View File

@ -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));

View File

@ -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 };

View File

@ -0,0 +1,42 @@
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;
} 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;
}

View File

@ -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 };

View File

@ -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
); );

View File

@ -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";
@ -201,6 +202,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);

View 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 if used the same account and day.
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}`);
};

View File

@ -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> => {

View File

@ -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.

View File

@ -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

View File

@ -1,9 +0,0 @@
{
"DailyTributeInfo": {
"IsMilestoneDay": false,
"IsChooseRewardSet": true,
"LoginDays": 1337,
"NextMilestoneReward": "",
"NextMilestoneDay": 50
}
}

View 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"
}
]