SpaceNinjaServer/inventoryService.ts
mahaoqqsx af258d1c7a
Some checks failed
Build / build (18) (pull_request) Has been cancelled
Build / build (20) (pull_request) Has been cancelled
Build / build (22) (pull_request) Has been cancelled
上传文件至 /
2025-03-18 07:14:16 -07:00

1450 lines
54 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
Inventory,
InventoryDocumentProps,
TInventoryDatabaseDocument
} from "@/src/models/inventoryModels/inventoryModel";
import { config } from "@/src/services/configService";
import { HydratedDocument, Types } from "mongoose";
import { IMissionDatabase, IInventoryDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
import { SlotNames, IInventoryChanges, IBinChanges, slotNames } from "@/src/types/purchaseTypes";
import {
IChallengeProgress,
IConsumable,
IFlavourItem,
IMiscItem,
IMission,
IRawUpgrade,
ISeasonChallenge,
ITypeCount,
InventorySlot,
IWeaponSkinClient,
TEquipmentKey,
IFusionTreasure,
IDailyAffiliations,
IKubrowPetEggDatabase,
IKubrowPetEggClient,
ILibraryDailyTaskInfo,
ICalendarProgress,
IDroneClient,
IUpgradeClient,
ICrewShipWeaponClient
} from "@/src/types/inventoryTypes/inventoryTypes";
import { IGenericUpdate } from "../types/genericUpdate";
import {
IMissionInventoryUpdateRequest,
IThemeUpdateRequest,
IUpdateChallengeProgressRequest
} from "../types/requestTypes";
import { logger } from "@/src/utils/logger";
import { convertInboxMessage, fromStoreItem, getExalted, getKeyChainItems } from "@/src/services/itemDataService";
import {
EquipmentFeatures,
IEquipmentClient,
IEquipmentDatabase,
IItemConfig
} from "../types/inventoryTypes/commonInventoryTypes";
import {
ExportArcanes,
ExportBundles,
ExportCustoms,
ExportDrones,
ExportEmailItems,
ExportEnemies,
ExportFlavour,
ExportFusionBundles,
ExportGear,
ExportKeys,
ExportMisc,
ExportRailjackWeapons,
ExportRecipes,
ExportResources,
ExportSentinels,
ExportSyndicates,
ExportUpgrades,
ExportWeapons,
ExportRegions,
IDefaultUpgrade,
TStandingLimitBin
} from "warframe-public-export-plus";
import { createShip } from "./shipService";
import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController";
import { toOid } from "../helpers/inventoryHelpers";
import { generateRewardSeed } from "@/src/controllers/api/getNewRewardSeedController";
import { addStartingGear } from "@/src/controllers/api/giveStartingGearController";
import { addQuestKey, completeQuest } from "@/src/services/questService";
import { handleBundleAcqusition } from "./purchaseService";
import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json";
import { getRandomElement, getRandomInt } from "./rngService";
import { createMessage } from "./inboxService";
export const createInventory = async (
accountOwnerId: Types.ObjectId,
defaultItemReferences: { loadOutPresetId: Types.ObjectId; ship: Types.ObjectId }
): Promise<void> => {
try {
const inventory = new Inventory({
accountOwnerId: accountOwnerId,
LoadOutPresets: defaultItemReferences.loadOutPresetId,
Ships: [defaultItemReferences.ship],
PlayedParkourTutorial: config.skipTutorial,
ReceivedStartingGear: config.skipTutorial
});
inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask();
inventory.CalendarProgress = createCalendar();
inventory.RewardSeed = generateRewardSeed();
inventory.DuviriInfo = {
Seed: generateRewardSeed(),
NumCompletions: 0
};
await addItem(inventory, "/Lotus/Types/Friendly/PlayerControllable/Weapons/DuviriDualSwords");
if (config.skipTutorial) {
await addStartingGear(inventory);
await completeQuest(inventory, "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain");
const completedMissions = ["SolNode27", "SolNode89", "SolNode63", "SolNode85", "SolNode15", "SolNode79"];
inventory.Missions.push(
...completedMissions.map(tag => ({
Completes: 1,
Tag: tag
}))
);
}
await inventory.save();
} catch (error) {
throw new Error(`Error creating inventory: ${error instanceof Error ? error.message : "Unknown error type"}`);
}
};
/**
* Combines two inventory changes objects into one.
*
* @param InventoryChanges - will hold the combined changes
* @param delta - inventory changes to be added
*/
//TODO: this fails silently when providing an incorrect object to delta
export const combineInventoryChanges = (InventoryChanges: IInventoryChanges, delta: IInventoryChanges): void => {
for (const key in delta) {
if (!(key in InventoryChanges)) {
InventoryChanges[key] = delta[key];
} else if (Array.isArray(delta[key])) {
const left = InventoryChanges[key] as object[];
const right: object[] = delta[key];
for (const item of right) {
left.push(item);
}
} else if (slotNames.indexOf(key as SlotNames) != -1) {
const left = InventoryChanges[key as SlotNames]!;
const right = delta[key as SlotNames]!;
if (right.count) {
left.count ??= 0;
left.count += right.count;
}
if (right.platinum) {
left.platinum ??= 0;
left.platinum += right.platinum;
}
left.Slots += right.Slots;
if (right.Extra) {
left.Extra ??= 0;
left.Extra += right.Extra;
}
} else if (typeof delta[key] === "number") {
(InventoryChanges[key] as number) += delta[key];
} else {
throw new Error(`inventory change not merged: unhandled type for inventory key ${key}`);
}
}
};
export const getInventory = async (
accountOwnerId: string,
projection: string | undefined = undefined
): Promise<TInventoryDatabaseDocument> => {
const inventory = await Inventory.findOne({ accountOwnerId: accountOwnerId }, projection);
if (!inventory) {
throw new Error(`Didn't find an inventory for ${accountOwnerId}`);
}
return inventory;
};
export const productCategoryToInventoryBin = (productCategory: string): InventorySlot | undefined => {
switch (productCategory) {
case "Suits":
return InventorySlot.SUITS;
case "Pistols":
case "LongGuns":
case "Melee":
return InventorySlot.WEAPONS;
case "Sentinels":
case "SentinelWeapons":
case "KubrowPets":
case "MoaPets":
return InventorySlot.SENTINELS;
case "SpaceSuits":
case "Hoverboards":
return InventorySlot.SPACESUITS;
case "SpaceGuns":
case "SpaceMelee":
return InventorySlot.SPACEWEAPONS;
case "OperatorAmps":
return InventorySlot.AMPS;
case "CrewShipWeapons":
case "CrewShipWeaponSkins":
return InventorySlot.RJ_COMPONENT_AND_ARMAMENTS;
case "MechSuits":
return InventorySlot.MECHSUITS;
case "CrewMembers":
return InventorySlot.CREWMEMBERS;
}
return undefined;
};
export const occupySlot = (
inventory: TInventoryDatabaseDocument,
bin: InventorySlot,
premiumPurchase: boolean
): IInventoryChanges => {
const slotChanges = {
Slots: 0,
Extra: 0
};
if (premiumPurchase) {
slotChanges.Extra += 1;
} else {
// { count: 1, platinum: 0, Slots: -1 }
slotChanges.Slots -= 1;
}
updateSlots(inventory, bin, slotChanges.Slots, slotChanges.Extra);
const inventoryChanges: IInventoryChanges = {};
inventoryChanges[bin] = slotChanges satisfies IBinChanges;
return inventoryChanges;
};
export const freeUpSlot = (inventory: TInventoryDatabaseDocument, bin: InventorySlot): void => {
// { count: -1, platinum: 0, Slots: 1 }
updateSlots(inventory, bin, 1, 0);
};
export const addItem = async (
inventory: TInventoryDatabaseDocument,
typeName: string,
quantity: number = 1,
premiumPurchase: boolean = false
): Promise<{ InventoryChanges: IInventoryChanges }> => {
// Bundles are technically StoreItems but a) they don't have a normal counterpart, and b) they are used in non-StoreItem contexts, e.g. email attachments.
if (typeName in ExportBundles) {
return { InventoryChanges: await handleBundleAcqusition(typeName, inventory, quantity) };
}
// Strict typing
if (typeName in ExportRecipes) {
const recipeChanges = [
{
ItemType: typeName,
ItemCount: quantity
} satisfies ITypeCount
];
addRecipes(inventory, recipeChanges);
return {
InventoryChanges: {
Recipes: recipeChanges
}
};
}
if (typeName in ExportResources) {
if (ExportResources[typeName].productCategory == "MiscItems") {
const miscItemChanges = [
{
ItemType: typeName,
ItemCount: quantity
} satisfies IMiscItem
];
addMiscItems(inventory, miscItemChanges);
return {
InventoryChanges: {
MiscItems: miscItemChanges
}
};
} else if (ExportResources[typeName].productCategory == "FusionTreasures") {
const fusionTreasureChanges = [
{
ItemType: typeName,
ItemCount: quantity,
Sockets: 0
} satisfies IFusionTreasure
];
addFusionTreasures(inventory, fusionTreasureChanges);
return {
InventoryChanges: {
FusionTreasures: fusionTreasureChanges
}
};
} else if (ExportResources[typeName].productCategory == "Ships") {
const oid = await createShip(inventory.accountOwnerId, typeName);
inventory.Ships.push(oid);
return {
InventoryChanges: {
Ships: [
{
ItemId: { $oid: oid.toString() },
ItemType: typeName
}
]
}
};
} else if (ExportResources[typeName].productCategory == "CrewShips") {
const inventoryChanges = {
...addCrewShip(inventory, typeName),
// fix to unlock railjack modding, item bellow supposed to be obtained from archwing quest
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
...(!inventory.CrewShipHarnesses?.length
? addCrewShipHarness(inventory, "/Lotus/Types/Game/CrewShip/RailJack/DefaultHarness")
: {})
};
return { InventoryChanges: inventoryChanges };
} else if (ExportResources[typeName].productCategory == "ShipDecorations") {
const changes = [
{
ItemType: typeName,
ItemCount: quantity
} satisfies IMiscItem
];
addShipDecorations(inventory, changes);
return {
InventoryChanges: {
ShipDecorations: changes
}
};
} else if (ExportResources[typeName].productCategory == "KubrowPetEggs") {
const changes: IKubrowPetEggClient[] = [];
if (quantity < 0) {
throw new Error(`removal of KubrowPetEggs not handled`);
}
for (let i = 0; i != quantity; ++i) {
const egg: IKubrowPetEggDatabase = {
ItemType: "/Lotus/Types/Game/KubrowPet/Eggs/KubrowEgg",
_id: new Types.ObjectId()
};
inventory.KubrowPetEggs ??= [];
inventory.KubrowPetEggs.push(egg);
changes.push({
ItemType: egg.ItemType,
ExpirationDate: { $date: { $numberLong: "2000000000000" } },
ItemId: toOid(egg._id)
});
}
return {
InventoryChanges: {
KubrowPetEggs: changes
}
};
} else {
throw new Error(`unknown product category: ${ExportResources[typeName].productCategory}`);
}
}
if (typeName in ExportCustoms) {
if (ExportCustoms[typeName].productCategory == "CrewShipWeaponSkins") {
return { InventoryChanges: addCrewShipWeaponSkin(inventory, typeName) };
} else {
return { InventoryChanges: addSkin(inventory, typeName) };
}
}
if (typeName in ExportFlavour) {
const inventoryChanges = addCustomization(inventory, typeName);
return { InventoryChanges: inventoryChanges };
}
if (typeName in ExportUpgrades || typeName in ExportArcanes) {
const changes = [
{
ItemType: typeName,
ItemCount: quantity
}
];
addMods(inventory, changes);
return {
InventoryChanges: {
RawUpgrades: changes
}
};
}
if (typeName in ExportGear) {
const consumablesChanges = [
{
ItemType: typeName,
ItemCount: quantity
} satisfies IConsumable
];
addConsumables(inventory, consumablesChanges);
return {
InventoryChanges: {
Consumables: consumablesChanges
}
};
}
if (typeName in ExportWeapons) {
const weapon = ExportWeapons[typeName];
if (weapon.totalDamage != 0) {
const inventoryChanges = addEquipment(
inventory,
weapon.productCategory,
typeName,
[],
{},
premiumPurchase ? { Features: EquipmentFeatures.DOUBLE_CAPACITY } : {}
);
if (weapon.additionalItems) {
for (const item of weapon.additionalItems) {
combineInventoryChanges(inventoryChanges, (await addItem(inventory, item, 1)).InventoryChanges);
}
}
return {
InventoryChanges: {
...inventoryChanges,
...occupySlot(inventory, InventorySlot.WEAPONS, premiumPurchase)
}
};
} else {
// Modular weapon parts
const miscItemChanges = [
{
ItemType: typeName,
ItemCount: quantity
} satisfies IMiscItem
];
addMiscItems(inventory, miscItemChanges);
return {
InventoryChanges: {
MiscItems: miscItemChanges
}
};
}
}
if (typeName in ExportRailjackWeapons) {
return {
InventoryChanges: {
...addCrewShipWeapon(inventory, typeName),
...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, premiumPurchase)
}
};
}
if (typeName in ExportMisc.creditBundles) {
const creditsTotal = ExportMisc.creditBundles[typeName] * quantity;
inventory.RegularCredits += creditsTotal;
return {
InventoryChanges: {
RegularCredits: creditsTotal
}
};
}
if (typeName in ExportFusionBundles) {
const fusionPointsTotal = ExportFusionBundles[typeName].fusionPoints * quantity;
inventory.FusionPoints += fusionPointsTotal;
return {
InventoryChanges: {
FusionPoints: fusionPointsTotal
}
};
}
if (typeName in ExportKeys) {
// Note: "/Lotus/Types/Keys/" contains some EmailItems
const key = ExportKeys[typeName];
if (key.chainStages) {
const key = addQuestKey(inventory, { ItemType: typeName });
if (!key) return { InventoryChanges: {} };
return { InventoryChanges: { QuestKeys: [key] } };
} else {
const key = { ItemType: typeName, ItemCount: quantity };
const index = inventory.LevelKeys.findIndex(levelKey => levelKey.ItemType == typeName);
if (index != -1) {
inventory.LevelKeys[index].ItemCount += quantity;
} else {
inventory.LevelKeys.push(key);
}
return { InventoryChanges: { LevelKeys: [key] } };
}
}
if (typeName in ExportDrones) {
const inventoryChanges = addDrone(inventory, typeName);
return {
InventoryChanges: inventoryChanges
};
}
if (typeName in ExportEmailItems) {
return {
InventoryChanges: await addEmailItem(inventory, typeName)
};
}
// Path-based duck typing
switch (typeName.substr(1).split("/")[1]) {
case "Powersuits":
switch (typeName.substr(1).split("/")[2]) {
default: {
return {
InventoryChanges: {
...addPowerSuit(
inventory,
typeName,
{},
premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined
),
...occupySlot(inventory, InventorySlot.SUITS, premiumPurchase)
}
};
}
case "Archwing": {
inventory.ArchwingEnabled = true;
return {
InventoryChanges: {
...addSpaceSuit(
inventory,
typeName,
{},
premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined
),
...occupySlot(inventory, InventorySlot.SPACESUITS, premiumPurchase)
}
};
}
case "EntratiMech": {
return {
InventoryChanges: {
...addMechSuit(
inventory,
typeName,
{},
premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined
),
...occupySlot(inventory, InventorySlot.MECHSUITS, premiumPurchase)
}
};
}
}
break;
case "Upgrades": {
switch (typeName.substr(1).split("/")[2]) {
case "Mods": // Legendary Core
case "CosmeticEnhancers": // Traumatic Peculiar
{
const changes = [
{
ItemType: typeName,
ItemCount: quantity
}
];
addMods(inventory, changes);
return {
InventoryChanges: {
RawUpgrades: changes
}
};
}
break;
}
break;
}
case "Types":
switch (typeName.substr(1).split("/")[2]) {
case "Sentinels": {
return {
InventoryChanges: addSentinel(inventory, typeName, premiumPurchase)
};
}
case "Game": {
if (typeName.substr(1).split("/")[3] == "Projections") {
// Void Relics, e.g. /Lotus/Types/Game/Projections/T2VoidProjectionGaussPrimeDBronze
const miscItemChanges = [
{
ItemType: typeName,
ItemCount: quantity
} satisfies IMiscItem
];
addMiscItems(inventory, miscItemChanges);
inventory.HasOwnedVoidProjectionsPreviously = true;
return {
InventoryChanges: {
MiscItems: miscItemChanges
}
};
}
break;
}
case "NeutralCreatures": {
const horseIndex = inventory.Horses.push({ ItemType: typeName });
return {
InventoryChanges: {
Horses: [inventory.Horses[horseIndex - 1].toJSON<IEquipmentClient>()]
}
};
}
case "Recipes": {
inventory.MiscItems.push({ ItemType: typeName, ItemCount: quantity });
return {
InventoryChanges: {
MiscItems: [
{
ItemType: typeName,
ItemCount: quantity
}
]
}
};
}
case "Vehicles":
if (typeName == "/Lotus/Types/Vehicles/Motorcycle/MotorcyclePowerSuit") {
return { InventoryChanges: addMotorcycle(inventory, typeName) };
}
break;
}
break;
}
throw new Error(`unable to add item: ${typeName}`);
};
export const addItems = async (
inventory: TInventoryDatabaseDocument,
items: ITypeCount[] | string[],
inventoryChanges: IInventoryChanges = {}
): Promise<IInventoryChanges> => {
let inventoryDelta;
for (const item of items) {
if (typeof item === "string") {
inventoryDelta = await addItem(inventory, item, 1, true);
} else {
inventoryDelta = await addItem(inventory, item.ItemType, item.ItemCount, true);
}
combineInventoryChanges(inventoryChanges, inventoryDelta.InventoryChanges);
}
return inventoryChanges;
};
export const applyDefaultUpgrades = (
inventory: TInventoryDatabaseDocument,
defaultUpgrades: IDefaultUpgrade[] | undefined
): IItemConfig[] => {
const modsToGive: IRawUpgrade[] = [];
const configs: IItemConfig[] = [];
if (defaultUpgrades) {
const upgrades = [];
for (const defaultUpgrade of defaultUpgrades) {
modsToGive.push({ ItemType: defaultUpgrade.ItemType, ItemCount: 1 });
if (defaultUpgrade.Slot != -1) {
while (upgrades.length < defaultUpgrade.Slot) {
upgrades.push("");
}
upgrades[defaultUpgrade.Slot] = defaultUpgrade.ItemType;
}
}
if (upgrades.length != 0) {
configs.push({ Upgrades: upgrades });
}
}
addMods(inventory, modsToGive);
return configs;
};
//TODO: maybe genericMethod for all the add methods, they share a lot of logic
const addSentinel = (
inventory: TInventoryDatabaseDocument,
sentinelName: string,
premiumPurchase: boolean,
inventoryChanges: IInventoryChanges = {}
): IInventoryChanges => {
// Sentinel itself occupies a slot in the sentinels bin
combineInventoryChanges(inventoryChanges, occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase));
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (ExportSentinels[sentinelName]?.defaultWeapon) {
addSentinelWeapon(inventory, ExportSentinels[sentinelName].defaultWeapon, premiumPurchase, inventoryChanges);
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const configs: IItemConfig[] = applyDefaultUpgrades(inventory, ExportSentinels[sentinelName]?.defaultUpgrades);
const features = premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined;
const sentinelIndex =
inventory.Sentinels.push({ ItemType: sentinelName, Configs: configs, XP: 0, Features: features }) - 1;
inventoryChanges.Sentinels ??= [];
inventoryChanges.Sentinels.push(inventory.Sentinels[sentinelIndex].toJSON<IEquipmentClient>());
return inventoryChanges;
};
const addSentinelWeapon = (
inventory: TInventoryDatabaseDocument,
typeName: string,
premiumPurchase: boolean,
inventoryChanges: IInventoryChanges
): void => {
// Sentinel weapons also occupy a slot in the sentinels bin
combineInventoryChanges(inventoryChanges, occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase));
const index = inventory.SentinelWeapons.push({ ItemType: typeName, XP: 0 }) - 1;
inventoryChanges.SentinelWeapons ??= [];
inventoryChanges.SentinelWeapons.push(inventory.SentinelWeapons[index].toJSON<IEquipmentClient>());
};
export const addPowerSuit = (
inventory: TInventoryDatabaseDocument,
powersuitName: string,
inventoryChanges: IInventoryChanges = {},
features: number | undefined = undefined
): IInventoryChanges => {
const specialItems = getExalted(powersuitName);
if (specialItems) {
for (const specialItem of specialItems) {
addSpecialItem(inventory, specialItem, inventoryChanges);
}
}
const suitIndex =
inventory.Suits.push({ ItemType: powersuitName, Configs: [], UpgradeVer: 101, XP: 0, Features: features }) - 1;
inventoryChanges.Suits ??= [];
inventoryChanges.Suits.push(inventory.Suits[suitIndex].toJSON<IEquipmentClient>());
return inventoryChanges;
};
export const addMechSuit = (
inventory: TInventoryDatabaseDocument,
mechsuitName: string,
inventoryChanges: IInventoryChanges = {},
features: number | undefined = undefined
): IInventoryChanges => {
const specialItems = getExalted(mechsuitName);
if (specialItems) {
for (const specialItem of specialItems) {
addSpecialItem(inventory, specialItem, inventoryChanges);
}
}
const suitIndex =
inventory.MechSuits.push({ ItemType: mechsuitName, Configs: [], UpgradeVer: 101, XP: 0, Features: features }) -
1;
inventoryChanges.MechSuits ??= [];
inventoryChanges.MechSuits.push(inventory.MechSuits[suitIndex].toJSON<IEquipmentClient>());
return inventoryChanges;
};
export const addSpecialItem = (
inventory: TInventoryDatabaseDocument,
itemName: string,
inventoryChanges: IInventoryChanges
): void => {
if (inventory.SpecialItems.find(x => x.ItemType == itemName)) {
return;
}
const specialItemIndex =
inventory.SpecialItems.push({
ItemType: itemName,
Configs: [],
Features: 1,
UpgradeVer: 101,
XP: 0
}) - 1;
inventoryChanges.SpecialItems ??= [];
inventoryChanges.SpecialItems.push(inventory.SpecialItems[specialItemIndex].toJSON<IEquipmentClient>());
};
export const addSpaceSuit = (
inventory: TInventoryDatabaseDocument,
spacesuitName: string,
inventoryChanges: IInventoryChanges = {},
features: number | undefined = undefined
): IInventoryChanges => {
const suitIndex =
inventory.SpaceSuits.push({
ItemType: spacesuitName,
Configs: [],
UpgradeVer: 101,
XP: 0,
Features: features
}) - 1;
inventoryChanges.SpaceSuits ??= [];
inventoryChanges.SpaceSuits.push(inventory.SpaceSuits[suitIndex].toJSON<IEquipmentClient>());
return inventoryChanges;
};
export const updateSlots = (
inventory: TInventoryDatabaseDocument,
slotName: SlotNames,
slotAmount: number,
extraAmount: number
): void => {
inventory[slotName].Slots += slotAmount;
if (extraAmount != 0) {
inventory[slotName].Extra ??= 0;
inventory[slotName].Extra += extraAmount;
}
};
const isCurrencyTracked = (usePremium: boolean): boolean => {
return usePremium ? !config.infinitePlatinum : !config.infiniteCredits;
};
export const updateCurrency = (
inventory: TInventoryDatabaseDocument,
price: number,
usePremium: boolean
): IInventoryChanges => {
const currencyChanges: IInventoryChanges = {};
if (price != 0 && isCurrencyTracked(usePremium)) {
if (usePremium) {
if (inventory.PremiumCreditsFree > 0) {
currencyChanges.PremiumCreditsFree = Math.min(price, inventory.PremiumCreditsFree) * -1;
inventory.PremiumCreditsFree += currencyChanges.PremiumCreditsFree;
}
currencyChanges.PremiumCredits = -price;
inventory.PremiumCredits += currencyChanges.PremiumCredits;
} else {
currencyChanges.RegularCredits = -price;
inventory.RegularCredits += currencyChanges.RegularCredits;
}
logger.debug(`currency changes `, currencyChanges);
}
return currencyChanges;
};
const standingLimitBinToInventoryKey: Record<
Exclude<TStandingLimitBin, "STANDING_LIMIT_BIN_NONE">,
keyof IDailyAffiliations
> = {
STANDING_LIMIT_BIN_NORMAL: "DailyAffiliation",
STANDING_LIMIT_BIN_PVP: "DailyAffiliationPvp",
STANDING_LIMIT_BIN_LIBRARY: "DailyAffiliationLibrary",
STANDING_LIMIT_BIN_CETUS: "DailyAffiliationCetus",
STANDING_LIMIT_BIN_QUILLS: "DailyAffiliationQuills",
STANDING_LIMIT_BIN_SOLARIS: "DailyAffiliationSolaris",
STANDING_LIMIT_BIN_VENTKIDS: "DailyAffiliationVentkids",
STANDING_LIMIT_BIN_VOX: "DailyAffiliationVox",
STANDING_LIMIT_BIN_ENTRATI: "DailyAffiliationEntrati",
STANDING_LIMIT_BIN_NECRALOID: "DailyAffiliationNecraloid",
STANDING_LIMIT_BIN_ZARIMAN: "DailyAffiliationZariman",
STANDING_LIMIT_BIN_KAHL: "DailyAffiliationKahl",
STANDING_LIMIT_BIN_CAVIA: "DailyAffiliationCavia",
STANDING_LIMIT_BIN_HEX: "DailyAffiliationHex"
};
export const allDailyAffiliationKeys: (keyof IDailyAffiliations)[] = Object.values(standingLimitBinToInventoryKey);
export const getStandingLimit = (inventory: IDailyAffiliations, bin: TStandingLimitBin): number => {
if (bin == "STANDING_LIMIT_BIN_NONE" || config.noDailyStandingLimits) {
return Number.MAX_SAFE_INTEGER;
}
return inventory[standingLimitBinToInventoryKey[bin]];
};
export const updateStandingLimit = (
inventory: IDailyAffiliations,
bin: TStandingLimitBin,
subtrahend: number
): void => {
if (bin != "STANDING_LIMIT_BIN_NONE" && !config.noDailyStandingLimits) {
inventory[standingLimitBinToInventoryKey[bin]] -= subtrahend;
}
};
// TODO: AffiliationMods support (Nightwave).
export const updateGeneric = async (data: IGenericUpdate, accountId: string): Promise<void> => {
const inventory = await getInventory(accountId);
// Make it an array for easier parsing.
if (typeof data.NodeIntrosCompleted === "string") {
data.NodeIntrosCompleted = [data.NodeIntrosCompleted];
}
// Combine the two arrays into one.
data.NodeIntrosCompleted = inventory.NodeIntrosCompleted.concat(data.NodeIntrosCompleted);
// Remove duplicate entries.
const nodes = [...new Set(data.NodeIntrosCompleted)];
inventory.NodeIntrosCompleted = nodes;
await inventory.save();
};
export const updateTheme = async (data: IThemeUpdateRequest, accountId: string): Promise<void> => {
const inventory = await getInventory(accountId);
if (data.Style) inventory.ThemeStyle = data.Style;
if (data.Background) inventory.ThemeBackground = data.Background;
if (data.Sounds) inventory.ThemeSounds = data.Sounds;
await inventory.save();
};
export const addEquipment = (
inventory: TInventoryDatabaseDocument,
category: TEquipmentKey,
type: string,
modularParts: string[] | undefined = undefined,
inventoryChanges: IInventoryChanges = {},
defaultOverwrites: Partial<IEquipmentDatabase> | undefined = undefined
): IInventoryChanges => {
const equipment = Object.assign(
{
ItemType: type,
Configs: [],
XP: 0,
ModularParts: modularParts
},
defaultOverwrites
);
const index = inventory[category].push(equipment) - 1;
inventoryChanges[category] ??= [];
inventoryChanges[category].push(inventory[category][index].toJSON<IEquipmentClient>());
return inventoryChanges;
};
export const addCustomization = (
inventory: TInventoryDatabaseDocument,
customizationName: string,
inventoryChanges: IInventoryChanges = {}
): IInventoryChanges => {
const flavourItemIndex = inventory.FlavourItems.push({ ItemType: customizationName }) - 1;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
inventoryChanges.FlavourItems ??= [];
(inventoryChanges.FlavourItems as IFlavourItem[]).push(
inventory.FlavourItems[flavourItemIndex].toJSON<IFlavourItem>()
);
return inventoryChanges;
};
export const addSkin = (
inventory: TInventoryDatabaseDocument,
typeName: string,
inventoryChanges: IInventoryChanges = {}
): IInventoryChanges => {
const index = inventory.WeaponSkins.push({ ItemType: typeName }) - 1;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
inventoryChanges.WeaponSkins ??= [];
(inventoryChanges.WeaponSkins as IWeaponSkinClient[]).push(
inventory.WeaponSkins[index].toJSON<IWeaponSkinClient>()
);
return inventoryChanges;
};
const addCrewShipWeapon = (
inventory: TInventoryDatabaseDocument,
typeName: string,
inventoryChanges: IInventoryChanges = {}
): IInventoryChanges => {
const index = inventory.CrewShipWeapons.push({ ItemType: typeName, _id: new Types.ObjectId() }) - 1;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
inventoryChanges.CrewShipWeapons ??= [];
(inventoryChanges.CrewShipWeapons as ICrewShipWeaponClient[]).push(
inventory.CrewShipWeapons[index].toJSON<ICrewShipWeaponClient>()
);
return inventoryChanges;
};
const addCrewShipWeaponSkin = (
inventory: TInventoryDatabaseDocument,
typeName: string,
inventoryChanges: IInventoryChanges = {}
): IInventoryChanges => {
const index = inventory.CrewShipWeaponSkins.push({ ItemType: typeName }) - 1;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
inventoryChanges.CrewShipWeaponSkins ??= [];
(inventoryChanges.CrewShipWeaponSkins as IUpgradeClient[]).push(
inventory.CrewShipWeaponSkins[index].toJSON<IUpgradeClient>()
);
return inventoryChanges;
};
const addCrewShip = (
inventory: TInventoryDatabaseDocument,
typeName: string,
inventoryChanges: IInventoryChanges = {}
): IInventoryChanges => {
if (inventory.CrewShips.length != 0) {
throw new Error("refusing to add CrewShip because account already has one");
}
const index = inventory.CrewShips.push({ ItemType: typeName }) - 1;
inventoryChanges.CrewShips ??= [];
inventoryChanges.CrewShips.push(inventory.CrewShips[index].toJSON<IEquipmentClient>());
return inventoryChanges;
};
const addCrewShipHarness = (
inventory: TInventoryDatabaseDocument,
typeName: string,
inventoryChanges: IInventoryChanges = {}
): IInventoryChanges => {
if (inventory.CrewShipHarnesses.length != 0) {
throw new Error("refusing to add CrewShipHarness because account already has one");
}
const index = inventory.CrewShipHarnesses.push({ ItemType: typeName }) - 1;
inventoryChanges.CrewShipHarnesses ??= [];
inventoryChanges.CrewShipHarnesses.push(inventory.CrewShipHarnesses[index].toJSON<IEquipmentClient>());
return inventoryChanges;
};
const addMotorcycle = (
inventory: TInventoryDatabaseDocument,
typeName: string,
inventoryChanges: IInventoryChanges = {}
): IInventoryChanges => {
if (inventory.Motorcycles.length != 0) {
throw new Error("refusing to add Motorcycle because account already has one");
}
const index = inventory.Motorcycles.push({ ItemType: typeName }) - 1;
inventoryChanges.Motorcycles ??= [];
inventoryChanges.Motorcycles.push(inventory.Motorcycles[index].toJSON<IEquipmentClient>());
return inventoryChanges;
};
const addDrone = (
inventory: TInventoryDatabaseDocument,
typeName: string,
inventoryChanges: IInventoryChanges = {}
): IInventoryChanges => {
const index = inventory.Drones.push({ ItemType: typeName, CurrentHP: ExportDrones[typeName].durability }) - 1;
inventoryChanges.Drones ??= [];
inventoryChanges.Drones.push(inventory.Drones[index].toJSON<IDroneClient>());
return inventoryChanges;
};
export const addEmailItem = async (
inventory: TInventoryDatabaseDocument,
typeName: string,
inventoryChanges: IInventoryChanges = {}
): Promise<IInventoryChanges> => {
const meta = ExportEmailItems[typeName];
const emailItem = inventory.EmailItems.find(x => x.ItemType == typeName);
if (!emailItem || !meta.sendOnlyOnce) {
await createMessage(inventory.accountOwnerId.toString(), [convertInboxMessage(meta.message)]);
if (emailItem) {
emailItem.ItemCount += 1;
} else {
inventory.EmailItems.push({ ItemType: typeName, ItemCount: 1 });
}
inventoryChanges.EmailItems ??= [];
inventoryChanges.EmailItems.push({ ItemType: typeName, ItemCount: 1 });
}
return inventoryChanges;
};
//TODO: wrong id is not erroring
export const addGearExpByCategory = (
inventory: TInventoryDatabaseDocument,
gearArray: IEquipmentClient[],
categoryName: TEquipmentKey
): void => {
const category = inventory[categoryName];
gearArray.forEach(({ ItemId, XP }) => {
if (!XP) {
return;
}
const item = category.id(ItemId.$oid);
if (item) {
item.XP ??= 0;
item.XP += XP;
const xpinfoIndex = inventory.XPInfo.findIndex(x => x.ItemType == item.ItemType);
if (xpinfoIndex !== -1) {
const xpinfo = inventory.XPInfo[xpinfoIndex];
xpinfo.XP += XP;
} else {
inventory.XPInfo.push({
ItemType: item.ItemType,
XP: XP
});
}
}
});
};
export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray: IMiscItem[]): void => {
const { MiscItems } = inventory;
itemsArray.forEach(({ ItemCount, ItemType }) => {
if (ItemCount == 0) {
return;
}
let itemIndex = MiscItems.findIndex(x => x.ItemType === ItemType);
if (itemIndex == -1) {
itemIndex = MiscItems.push({ ItemType, ItemCount: 0 }) - 1;
}
MiscItems[itemIndex].ItemCount += ItemCount;
if (ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal") {
inventory.FoundToday ??= [];
let foundTodayIndex = inventory.FoundToday.findIndex(x => x.ItemType == ItemType);
if (foundTodayIndex == -1) {
foundTodayIndex = inventory.FoundToday.push({ ItemType, ItemCount: 0 }) - 1;
}
inventory.FoundToday[foundTodayIndex].ItemCount += ItemCount;
if (inventory.FoundToday[foundTodayIndex].ItemCount <= 0) {
inventory.FoundToday.splice(foundTodayIndex, 1);
}
if (inventory.FoundToday.length == 0) {
inventory.FoundToday = undefined;
}
}
if (MiscItems[itemIndex].ItemCount == 0) {
MiscItems.splice(itemIndex, 1);
} else if (MiscItems[itemIndex].ItemCount <= 0) {
logger.warn(`account now owns a negative amount of ${ItemType}`);
}
});
};
export const addShipDecorations = (inventory: TInventoryDatabaseDocument, itemsArray: IConsumable[]): void => {
const { ShipDecorations } = inventory;
itemsArray.forEach(({ ItemCount, ItemType }) => {
const itemIndex = ShipDecorations.findIndex(miscItem => miscItem.ItemType === ItemType);
if (itemIndex !== -1) {
ShipDecorations[itemIndex].ItemCount += ItemCount;
} else {
ShipDecorations.push({ ItemCount, ItemType });
}
});
};
export const addConsumables = (inventory: TInventoryDatabaseDocument, itemsArray: IConsumable[]): void => {
const { Consumables } = inventory;
itemsArray.forEach(({ ItemCount, ItemType }) => {
const itemIndex = Consumables.findIndex(i => i.ItemType === ItemType);
if (itemIndex !== -1) {
Consumables[itemIndex].ItemCount += ItemCount;
} else {
Consumables.push({ ItemCount, ItemType });
}
});
};
export const addCrewShipRawSalvage = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => {
const { CrewShipRawSalvage } = inventory;
itemsArray.forEach(({ ItemCount, ItemType }) => {
const itemIndex = CrewShipRawSalvage.findIndex(i => i.ItemType === ItemType);
if (itemIndex !== -1) {
CrewShipRawSalvage[itemIndex].ItemCount += ItemCount;
} else {
CrewShipRawSalvage.push({ ItemCount, ItemType });
}
});
};
export const addCrewShipAmmo = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => {
const { CrewShipAmmo } = inventory;
itemsArray.forEach(({ ItemCount, ItemType }) => {
const itemIndex = CrewShipAmmo.findIndex(i => i.ItemType === ItemType);
if (itemIndex !== -1) {
CrewShipAmmo[itemIndex].ItemCount += ItemCount;
} else {
CrewShipAmmo.push({ ItemCount, ItemType });
}
});
};
export const addRecipes = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => {
const { Recipes } = inventory;
itemsArray.forEach(({ ItemCount, ItemType }) => {
const itemIndex = Recipes.findIndex(i => i.ItemType === ItemType);
if (itemIndex !== -1) {
Recipes[itemIndex].ItemCount += ItemCount;
} else {
Recipes.push({ ItemCount, ItemType });
}
});
};
export const addMods = (inventory: TInventoryDatabaseDocument, itemsArray: IRawUpgrade[]): void => {
const { RawUpgrades } = inventory;
itemsArray.forEach(({ ItemType, ItemCount }) => {
if (ItemCount == 0) {
return;
}
let itemIndex = RawUpgrades.findIndex(x => x.ItemType === ItemType);
if (itemIndex == -1) {
itemIndex = RawUpgrades.push({ ItemType, ItemCount: 0 }) - 1;
}
RawUpgrades[itemIndex].ItemCount += ItemCount;
if (RawUpgrades[itemIndex].ItemCount == 0) {
RawUpgrades.splice(itemIndex, 1);
} else if (RawUpgrades[itemIndex].ItemCount <= 0) {
logger.warn(`account now owns a negative amount of ${ItemType}`);
}
});
};
export const addFusionTreasures = (inventory: TInventoryDatabaseDocument, itemsArray: IFusionTreasure[]): void => {
const { FusionTreasures } = inventory;
itemsArray.forEach(({ ItemType, ItemCount, Sockets }) => {
const itemIndex = FusionTreasures.findIndex(i => i.ItemType == ItemType && (i.Sockets || 0) == (Sockets || 0));
if (itemIndex !== -1) {
FusionTreasures[itemIndex].ItemCount += ItemCount;
if (FusionTreasures[itemIndex].ItemCount == 0) {
FusionTreasures.splice(itemIndex, 1);
} else if (FusionTreasures[itemIndex].ItemCount <= 0) {
logger.warn(`account now owns a negative amount of ${ItemType}`);
}
} else {
FusionTreasures.push({ ItemCount, ItemType, Sockets });
}
});
};
export const addFocusXpIncreases = (inventory: TInventoryDatabaseDocument, focusXpPlus: number[]): void => {
enum FocusType {
AP_UNIVERSAL,
AP_ATTACK,
AP_DEFENSE,
AP_TACTIC,
AP_POWER,
AP_PRECEPT,
AP_FUSION,
AP_WARD,
AP_UMBRA,
AP_ANY
}
inventory.FocusXP ??= { AP_ATTACK: 0, AP_DEFENSE: 0, AP_TACTIC: 0, AP_POWER: 0, AP_WARD: 0 };
inventory.FocusXP.AP_ATTACK += focusXpPlus[FocusType.AP_ATTACK];
inventory.FocusXP.AP_DEFENSE += focusXpPlus[FocusType.AP_DEFENSE];
inventory.FocusXP.AP_TACTIC += focusXpPlus[FocusType.AP_TACTIC];
inventory.FocusXP.AP_POWER += focusXpPlus[FocusType.AP_POWER];
inventory.FocusXP.AP_WARD += focusXpPlus[FocusType.AP_WARD];
};
export const updateChallengeProgress = async (
challenges: IUpdateChallengeProgressRequest,
accountId: string
): Promise<void> => {
const inventory = await getInventory(accountId);
addChallenges(inventory, challenges.ChallengeProgress);
addSeasonalChallengeHistory(inventory, challenges.SeasonChallengeHistory);
await inventory.save();
};
export const addSeasonalChallengeHistory = (
inventory: TInventoryDatabaseDocument,
itemsArray: ISeasonChallenge[] | undefined
): void => {
const category = inventory.SeasonChallengeHistory;
itemsArray?.forEach(({ challenge, id }) => {
const itemIndex = category.findIndex(i => i.challenge === challenge);
if (itemIndex !== -1) {
category[itemIndex].id = id;
} else {
category.push({ challenge, id });
}
});
};
export const addChallenges = (
inventory: TInventoryDatabaseDocument,
itemsArray: IChallengeProgress[] | undefined
): void => {
const category = inventory.ChallengeProgress;
itemsArray?.forEach(({ Name, Progress }) => {
const itemIndex = category.findIndex(i => i.Name === Name);
if (itemIndex !== -1) {
category[itemIndex].Progress += Progress;
} else {
category.push({ Name, Progress });
}
});
};
export const addMissionComplete = (
inventory: TInventoryDatabaseDocument,
{ Tag, Completes, Tier }: IMissionDatabase
): void => {
const { Missions, NodeIntrosCompleted } = inventory;
const itemIndex = Missions.findIndex(item => item.Tag === Tag);
// 检查是否存在 TeshinHardModeUnlocked
const hasTeshinHardModeUnlocked = NodeIntrosCompleted.includes("TeshinHardModeUnlocked");
if (itemIndex !== -1) {
Missions[itemIndex].Completes += Completes;
// 如果存在 TeshinHardModeUnlocked增加 Tier
if (hasTeshinHardModeUnlocked) {
// 确保 Tier 是一个有效的数字
if (typeof Missions[itemIndex].Tier !== "number" || isNaN(Missions[itemIndex].Tier)) {
Missions[itemIndex].Tier = 0; // 如果 Tier 是 NaN 或未定义,重置为 0
}
Missions[itemIndex].Tier += Tier || 0; // 如果 Tier 未传入,默认为 0
}
} else {
const newMission: IMissionDatabase = { Tag, Completes, Tier: Tier || 0 }; // 如果 Tier 未传入,默认为 0
Missions.push(newMission);
}
// 当完成 SolNode89 时,自动添加 SolNode855 完成次数
if (Tag === "SolNode857") {
const solNode855Index = Missions.findIndex(item => item.Tag === "SolNode855");
if (solNode855Index !== -1) {
Missions[solNode855Index].Completes += 1;
} else {
Missions.push({ Tag: "SolNode855", Completes: 1, Tier: 0 }); // 默认 Tier 为 0
}
}
// 如果存在 TeshinHardModeUnlocked添加一项 Tier 默认为 0 的任务
if (hasTeshinHardModeUnlocked) {
const teshinMissionTag = "TeshinHardModeMission"; // 新任务的 Tag
const teshinMissionIndex = Missions.findIndex(item => item.Tag === teshinMissionTag);
if (teshinMissionIndex !== -1) {
// 如果任务已存在,增加 Completes
Missions[teshinMissionIndex].Completes += 1;
} else {
// 如果任务不存在,添加新任务
Missions.push({ Tag: teshinMissionTag, Completes: 1, Tier: 0 }); // 默认 Tier 为 0
}
}
};
export const addBooster = (ItemType: string, time: number, inventory: TInventoryDatabaseDocument): void => {
const currentTime = Math.floor(Date.now() / 1000) - 129600; // Value is wrong without 129600. Figure out why, please. :)
const { Boosters } = inventory;
const itemIndex = Boosters.findIndex(booster => booster.ItemType === ItemType);
if (itemIndex !== -1) {
const existingBooster = Boosters[itemIndex];
existingBooster.ExpiryDate = Math.max(existingBooster.ExpiryDate, currentTime) + time;
} else {
Boosters.push({ ItemType, ExpiryDate: currentTime + time });
}
};
export const updateSyndicate = (
inventory: HydratedDocument<IInventoryDatabase, InventoryDocumentProps>,
syndicateUpdate: IMissionInventoryUpdateRequest["AffiliationChanges"]
): void => {
syndicateUpdate?.forEach(affiliation => {
const syndicate = inventory.Affiliations.find(x => x.Tag === affiliation.Tag);
if (syndicate !== undefined) {
// 更新声望
syndicate.Standing += Number(affiliation.Standing);
// 如果 Title 是 number 类型,则更新 Title
if (typeof affiliation.Title === 'number') {
syndicate.Title = (syndicate.Title || 0) + affiliation.Title;
}
} else {
// 创建新的派系记录
inventory.Affiliations.push({
Standing: Number(affiliation.Standing), // 确保 Standing 是 number 类型
Title: typeof affiliation.Title === 'number' ? affiliation.Title : 0, // 确保 Title 是 number 类型
Tag: affiliation.Tag,
FreeFavorsEarned: [],
FreeFavorsUsed: []
});
}
// 额外增加 1000 点 RadioLegionIntermission12Syndicate 声望
if (affiliation.Tag === "RadioLegionIntermission12Syndicate") {
const radioLegionSyndicate = inventory.Affiliations.find(x => x.Tag === "RadioLegionIntermission12Syndicate");
if (radioLegionSyndicate !== undefined) {
radioLegionSyndicate.Standing += 1000; // 增加 1000 点声望
} else {
// 如果不存在,创建新的记录并设置初始声望
inventory.Affiliations.push({
Standing: Number(affiliation.Standing) + 1000, // 初始声望 + 1000
Title: typeof affiliation.Title === 'number' ? affiliation.Title : 0, // 确保 Title 是 number 类型
Tag: "RadioLegionIntermission12Syndicate",
FreeFavorsEarned: [],
FreeFavorsUsed: []
});
}
}
updateStandingLimit(inventory, ExportSyndicates[affiliation.Tag].dailyLimitBin, affiliation.Standing);
});
};
/**
* @returns object with inventory keys of changes or empty object when no items were added
*/
export const addKeyChainItems = async (
inventory: TInventoryDatabaseDocument,
keyChainData: IKeyChainRequest
): Promise<IInventoryChanges> => {
const keyChainItems = getKeyChainItems(keyChainData);
logger.debug(
`adding key chain items ${keyChainItems.join()} for ${keyChainData.KeyChain} at stage ${keyChainData.ChainStage}`
);
const nonStoreItems = keyChainItems.map(item => fromStoreItem(item));
//TODO: inventoryChanges is not typed correctly
const inventoryChanges = {};
for (const item of nonStoreItems) {
const inventoryChangesDelta = await addItem(inventory, item);
combineInventoryChanges(inventoryChanges, inventoryChangesDelta.InventoryChanges);
}
return inventoryChanges;
};
export const createLibraryDailyTask = (): ILibraryDailyTaskInfo => {
const enemyTypes = getRandomElement(libraryDailyTasks);
const enemyAvatar = ExportEnemies.avatars[enemyTypes[0]];
const scansRequired = getRandomInt(2, 4);
return {
EnemyTypes: enemyTypes,
EnemyLocTag: enemyAvatar.name,
EnemyIcon: enemyAvatar.icon!,
ScansRequired: scansRequired,
RewardStoreItem: "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/RareFusionBundle",
RewardQuantity: Math.trunc(scansRequired * 2.5),
RewardStanding: 2500 * scansRequired
};
};
const createCalendar = (): ICalendarProgress => {
return {
Version: 19,
Iteration: 2,
YearProgress: { Upgrades: [] },
SeasonProgress: {
SeasonType: "CST_SPRING",
LastCompletedDayIdx: -1,
LastCompletedChallengeDayIdx: -1,
ActivatedChallenges: []
}
};
};