Compare commits
	
		
			49 Commits
		
	
	
		
			ff3be4ecec
			...
			38d1e31ad8
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					38d1e31ad8 | ||
| 
						 | 
					a83e77ef21 | ||
| 
						 | 
					720e243f13 | ||
| 
						 | 
					92c59bcc3a | ||
| 
						 | 
					0796917740 | ||
| 
						 | 
					e192a36389 | ||
| 
						 | 
					3a3c90c9e3 | ||
| 
						 | 
					d829c3ce33 | ||
| 
						 | 
					676299923f | ||
| f5c1b83598 | |||
| 30f380f37e | |||
| 0f7a85db59 | |||
| 43bc12713a | |||
| 6022bf97b5 | |||
| 159e151dc0 | |||
| 56954260c8 | |||
| c535044af8 | |||
| f5146be129 | |||
| d38ec06ed6 | |||
| 060f65900f | |||
| 66d3057d40 | |||
| b14a5925df | |||
| 9da47c406a | |||
| 09065bdb4e | |||
| 8f04fc5fdf | |||
| 230ee5f638 | |||
| 21db6ce265 | |||
| 1ecf53c96b | |||
| e67ef63b77 | |||
| 5772ebe746 | |||
| 0136e4d152 | |||
| 8b3ee4b4f5 | |||
| 6e8800f048 | |||
| d65a667acd | |||
| c6a3e86d2b | |||
| a8e41c95e7 | |||
| 9426359370 | |||
| e5247700df | |||
| 1c3f1e2276 | |||
| 7710e7c13f | |||
| a64c5ea3c1 | |||
| 17e1eb86dd | |||
| de9dfb3d71 | |||
| fc38f818dd | |||
| e76f08db89 | |||
| 7bcb5f21ce | |||
| 3641d63f6f | |||
| 71c4835a69 | |||
| 86a63ace41 | 
@ -35,5 +35,5 @@ SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [confi
 | 
			
		||||
  - `RadioLegion2Syndicate` for The Emissary
 | 
			
		||||
  - `RadioLegionIntermissionSyndicate` for Intermission I
 | 
			
		||||
  - `RadioLegionSyndicate` for The Wolf of Saturn Six
 | 
			
		||||
- `allTheFissures` can be set to `normal` or `hard` to enable all fissures either in normal or steel path, respectively.
 | 
			
		||||
- `worldState.allTheFissures` can be set to `normal` or `hard` to enable all fissures either in normal or steel path, respectively.
 | 
			
		||||
- `worldState.circuitGameModes` can be set to an array of game modes which will override the otherwise-random pattern in The Circuit. Valid element values are `Survival`, `VoidFlood`, `Excavation`, `Defense`, `Exterminate`, `Assassination`, and `Alchemy`.
 | 
			
		||||
 | 
			
		||||
@ -38,6 +38,7 @@
 | 
			
		||||
    "anniversary": null,
 | 
			
		||||
    "hallowedNightmares": false,
 | 
			
		||||
    "hallowedNightmaresRewardsOverride": 0,
 | 
			
		||||
    "naberusNightsOverride": null,
 | 
			
		||||
    "proxyRebellion": false,
 | 
			
		||||
    "proxyRebellionRewardsOverride": 0,
 | 
			
		||||
    "galleonOfGhouls": 0,
 | 
			
		||||
 | 
			
		||||
@ -5,11 +5,23 @@ import { Guild, GuildMember } from "../../models/guildModel.ts";
 | 
			
		||||
import { createUniqueClanName, getGuildClient, giveClanKey } from "../../services/guildService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
			
		||||
import { sendWsBroadcastTo } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
export const createGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const account = await getAccountForRequest(req);
 | 
			
		||||
    const payload = getJSONfromString<ICreateGuildRequest>(String(req.body));
 | 
			
		||||
 | 
			
		||||
    const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes");
 | 
			
		||||
    if (inventory.GuildId) {
 | 
			
		||||
        const guild = await Guild.findById(inventory.GuildId);
 | 
			
		||||
        if (guild) {
 | 
			
		||||
            res.json({
 | 
			
		||||
                ...(await getGuildClient(guild, account))
 | 
			
		||||
            });
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Remove pending applications for this account
 | 
			
		||||
    await GuildMember.deleteMany({ accountId: account._id, status: 1 });
 | 
			
		||||
 | 
			
		||||
@ -27,7 +39,6 @@ export const createGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
        rank: 0
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes");
 | 
			
		||||
    inventory.GuildId = guild._id;
 | 
			
		||||
    const inventoryChanges: IInventoryChanges = {};
 | 
			
		||||
    giveClanKey(inventory, inventoryChanges);
 | 
			
		||||
@ -37,6 +48,7 @@ export const createGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
        ...(await getGuildClient(guild, account)),
 | 
			
		||||
        InventoryChanges: inventoryChanges
 | 
			
		||||
    });
 | 
			
		||||
    sendWsBroadcastTo(account._id.toString(), { update_inventory: true });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ICreateGuildRequest {
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,7 @@ export const getGuildDojoController: RequestHandler = async (req, res) => {
 | 
			
		||||
            _id: new Types.ObjectId(),
 | 
			
		||||
            pf: "/Lotus/Levels/ClanDojo/DojoHall.level",
 | 
			
		||||
            ppf: "",
 | 
			
		||||
            CompletionTime: new Date(Date.now()),
 | 
			
		||||
            CompletionTime: new Date(Date.now() - 1000),
 | 
			
		||||
            DecoCapacity: 600
 | 
			
		||||
        });
 | 
			
		||||
        await guild.save();
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,13 @@ export const getVoidProjectionRewardsController: RequestHandler = async (req, re
 | 
			
		||||
 | 
			
		||||
    if (data.ParticipantInfo.QualifiesForReward && !data.ParticipantInfo.HaveRewardResponse) {
 | 
			
		||||
        const inventory = await getInventory(accountId);
 | 
			
		||||
        await crackRelic(inventory, data.ParticipantInfo);
 | 
			
		||||
        const rewards = await crackRelic(inventory, data.ParticipantInfo);
 | 
			
		||||
        if (!inventory.MissionRelicRewards || inventory.MissionRelicRewards.length >= data.CurrentWave) {
 | 
			
		||||
            inventory.MissionRelicRewards = [];
 | 
			
		||||
        }
 | 
			
		||||
        rewards.forEach(reward => {
 | 
			
		||||
            (inventory.MissionRelicRewards ??= []).push({ ItemType: reward.type, ItemCount: reward.itemCount });
 | 
			
		||||
        });
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,13 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import type { Request, RequestHandler } from "express";
 | 
			
		||||
import { Inbox } from "../../models/inboxModel.ts";
 | 
			
		||||
import {
 | 
			
		||||
    createMessage,
 | 
			
		||||
    createNewEventMessages,
 | 
			
		||||
    deleteAllMessagesRead,
 | 
			
		||||
    deleteAllMessagesReadNonCin,
 | 
			
		||||
    deleteMessageRead,
 | 
			
		||||
    getAllMessagesSorted,
 | 
			
		||||
    getMessage
 | 
			
		||||
    getMessage,
 | 
			
		||||
    type IMessageCreationTemplate
 | 
			
		||||
} from "../../services/inboxService.ts";
 | 
			
		||||
import { getAccountForRequest, getAccountFromSuffixedName, getSuffixedName } from "../../services/loginService.ts";
 | 
			
		||||
import {
 | 
			
		||||
@ -21,6 +22,9 @@ import { ExportFlavour } from "warframe-public-export-plus";
 | 
			
		||||
import { handleStoreItemAcquisition } from "../../services/purchaseService.ts";
 | 
			
		||||
import { fromStoreItem, isStoreItem } from "../../services/itemDataService.ts";
 | 
			
		||||
import type { IOid } from "../../types/commonTypes.ts";
 | 
			
		||||
import { unixTimesInMs } from "../../constants/timeConstants.ts";
 | 
			
		||||
import { config } from "../../services/configService.ts";
 | 
			
		||||
import { Types } from "mongoose";
 | 
			
		||||
 | 
			
		||||
export const inboxController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const { deleteId, lastMessage: latestClientMessageId, messageId } = req.query;
 | 
			
		||||
@ -31,11 +35,11 @@ export const inboxController: RequestHandler = async (req, res) => {
 | 
			
		||||
    if (deleteId) {
 | 
			
		||||
        if (deleteId === "DeleteAllRead") {
 | 
			
		||||
            await deleteAllMessagesRead(accountId);
 | 
			
		||||
            res.status(200).end();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        } else if (deleteId === "DeleteAllReadNonCin") {
 | 
			
		||||
            await deleteAllMessagesReadNonCin(accountId);
 | 
			
		||||
        } else {
 | 
			
		||||
            await deleteMessageRead(parseOid(deleteId as string));
 | 
			
		||||
        }
 | 
			
		||||
        res.status(200).end();
 | 
			
		||||
    } else if (messageId) {
 | 
			
		||||
        const message = await getMessage(parseOid(messageId as string));
 | 
			
		||||
@ -134,6 +138,119 @@ export const inboxController: RequestHandler = async (req, res) => {
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const createNewEventMessages = async (req: Request): Promise<void> => {
 | 
			
		||||
    const account = await getAccountForRequest(req);
 | 
			
		||||
    const newEventMessages: IMessageCreationTemplate[] = [];
 | 
			
		||||
 | 
			
		||||
    // Baro
 | 
			
		||||
    const baroIndex = Math.trunc((Date.now() - 910800000) / (unixTimesInMs.day * 14));
 | 
			
		||||
    const baroStart = baroIndex * (unixTimesInMs.day * 14) + 910800000;
 | 
			
		||||
    const baroActualStart = baroStart + unixTimesInMs.day * (config.worldState?.baroAlwaysAvailable ? 0 : 12);
 | 
			
		||||
    if (Date.now() >= baroActualStart && account.LatestEventMessageDate.getTime() < baroActualStart) {
 | 
			
		||||
        newEventMessages.push({
 | 
			
		||||
            sndr: "/Lotus/Language/G1Quests/VoidTraderName",
 | 
			
		||||
            sub: "/Lotus/Language/CommunityMessages/VoidTraderAppearanceTitle",
 | 
			
		||||
            msg: "/Lotus/Language/CommunityMessages/VoidTraderAppearanceMessage",
 | 
			
		||||
            icon: "/Lotus/Interface/Icons/Npcs/BaroKiTeerPortrait.png",
 | 
			
		||||
            startDate: new Date(baroActualStart),
 | 
			
		||||
            endDate: new Date(baroStart + unixTimesInMs.day * 14),
 | 
			
		||||
            CrossPlatform: true,
 | 
			
		||||
            arg: [
 | 
			
		||||
                {
 | 
			
		||||
                    Key: "NODE_NAME",
 | 
			
		||||
                    Tag: ["EarthHUB", "MercuryHUB", "SaturnHUB", "PlutoHUB"][baroIndex % 4]
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            date: new Date(baroActualStart)
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // BUG: Deleting the inbox message manually means it'll just be automatically re-created. This is because we don't use startDate/endDate for these config-toggled events.
 | 
			
		||||
    const promises = [];
 | 
			
		||||
    if (config.worldState?.creditBoost) {
 | 
			
		||||
        promises.push(
 | 
			
		||||
            (async (): Promise<void> => {
 | 
			
		||||
                if (!(await Inbox.exists({ ownerId: account._id, globaUpgradeId: "5b23106f283a555109666672" }))) {
 | 
			
		||||
                    newEventMessages.push({
 | 
			
		||||
                        globaUpgradeId: new Types.ObjectId("5b23106f283a555109666672"),
 | 
			
		||||
                        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
                        sub: "/Lotus/Language/Items/EventDoubleCreditsName",
 | 
			
		||||
                        msg: "/Lotus/Language/Items/EventDoubleCreditsDesc",
 | 
			
		||||
                        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
                        startDate: new Date(),
 | 
			
		||||
                        CrossPlatform: true
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            })()
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    if (config.worldState?.affinityBoost) {
 | 
			
		||||
        promises.push(
 | 
			
		||||
            (async (): Promise<void> => {
 | 
			
		||||
                if (!(await Inbox.exists({ ownerId: account._id, globaUpgradeId: "5b23106f283a555109666673" }))) {
 | 
			
		||||
                    newEventMessages.push({
 | 
			
		||||
                        globaUpgradeId: new Types.ObjectId("5b23106f283a555109666673"),
 | 
			
		||||
                        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
                        sub: "/Lotus/Language/Items/EventDoubleAffinityName",
 | 
			
		||||
                        msg: "/Lotus/Language/Items/EventDoubleAffinityDesc",
 | 
			
		||||
                        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
                        startDate: new Date(),
 | 
			
		||||
                        CrossPlatform: true
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            })()
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    if (config.worldState?.resourceBoost) {
 | 
			
		||||
        promises.push(
 | 
			
		||||
            (async (): Promise<void> => {
 | 
			
		||||
                if (!(await Inbox.exists({ ownerId: account._id, globaUpgradeId: "5b23106f283a555109666674" }))) {
 | 
			
		||||
                    newEventMessages.push({
 | 
			
		||||
                        globaUpgradeId: new Types.ObjectId("5b23106f283a555109666674"),
 | 
			
		||||
                        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
                        sub: "/Lotus/Language/Items/EventDoubleResourceName",
 | 
			
		||||
                        msg: "/Lotus/Language/Items/EventDoubleResourceDesc",
 | 
			
		||||
                        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
                        startDate: new Date(),
 | 
			
		||||
                        CrossPlatform: true
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            })()
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    if (config.worldState?.galleonOfGhouls) {
 | 
			
		||||
        promises.push(
 | 
			
		||||
            (async (): Promise<void> => {
 | 
			
		||||
                if (!(await Inbox.exists({ ownerId: account._id, goalTag: "GalleonRobbery" }))) {
 | 
			
		||||
                    newEventMessages.push({
 | 
			
		||||
                        sndr: "/Lotus/Language/Bosses/BossCouncilorVayHek",
 | 
			
		||||
                        sub: "/Lotus/Language/Events/GalleonRobberyIntroMsgTitle",
 | 
			
		||||
                        msg: "/Lotus/Language/Events/GalleonRobberyIntroMsgDesc",
 | 
			
		||||
                        icon: "/Lotus/Interface/Icons/Npcs/VayHekPortrait.png",
 | 
			
		||||
                        transmission: "/Lotus/Sounds/Dialog/GalleonOfGhouls/DGhoulsWeekOneInbox0010VayHek",
 | 
			
		||||
                        att: ["/Lotus/Upgrades/Skins/Events/OgrisOldSchool"],
 | 
			
		||||
                        startDate: new Date(),
 | 
			
		||||
                        goalTag: "GalleonRobbery"
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            })()
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
    if (newEventMessages.length === 0) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await createMessage(account._id, newEventMessages);
 | 
			
		||||
 | 
			
		||||
    const latestEventMessage = newEventMessages.reduce((prev, current) =>
 | 
			
		||||
        prev.startDate! > current.startDate! ? prev : current
 | 
			
		||||
    );
 | 
			
		||||
    account.LatestEventMessageDate = new Date(latestEventMessage.startDate!);
 | 
			
		||||
    await account.save();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 33.6.0 has query arguments like lastMessage={"$oid":"68112baebf192e786d1502bb"} instead of lastMessage=68112baebf192e786d1502bb
 | 
			
		||||
const parseOid = (oid: string): string => {
 | 
			
		||||
    if (oid[0] == "{") {
 | 
			
		||||
 | 
			
		||||
@ -335,9 +335,9 @@ export const getInventoryResponse = async (
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (config.unlockAllSkins) {
 | 
			
		||||
        const missingWeaponSkins = new Set(Object.keys(ExportCustoms));
 | 
			
		||||
        inventoryResponse.WeaponSkins.forEach(x => missingWeaponSkins.delete(x.ItemType));
 | 
			
		||||
        for (const uniqueName of missingWeaponSkins) {
 | 
			
		||||
        const ownedWeaponSkins = new Set<string>(inventoryResponse.WeaponSkins.map(x => x.ItemType));
 | 
			
		||||
        for (const [uniqueName, meta] of Object.entries(ExportCustoms)) {
 | 
			
		||||
            if (!meta.alwaysAvailable && !ownedWeaponSkins.has(uniqueName)) {
 | 
			
		||||
                inventoryResponse.WeaponSkins.push({
 | 
			
		||||
                    ItemId: {
 | 
			
		||||
                        $oid: "ca70ca70ca70ca70" + catBreadHash(uniqueName).toString(16).padStart(8, "0")
 | 
			
		||||
@ -346,6 +346,7 @@ export const getInventoryResponse = async (
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (typeof config.spoofMasteryRank === "number" && config.spoofMasteryRank >= 0) {
 | 
			
		||||
        inventoryResponse.PlayerLevel = config.spoofMasteryRank;
 | 
			
		||||
@ -460,6 +461,9 @@ export const getInventoryResponse = async (
 | 
			
		||||
                        toLegacyOid(id);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                if (inventoryResponse.GuildId) {
 | 
			
		||||
                    toLegacyOid(inventoryResponse.GuildId);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,9 @@ import type { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "../../
 | 
			
		||||
import { logger } from "../../utils/logger.ts";
 | 
			
		||||
import { version_compare } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
import { handleNonceInvalidation } from "../../services/wsService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { createMessage } from "../../services/inboxService.ts";
 | 
			
		||||
import { fromStoreItem } from "../../services/itemDataService.ts";
 | 
			
		||||
 | 
			
		||||
export const loginController: RequestHandler = async (request, response) => {
 | 
			
		||||
    const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object
 | 
			
		||||
@ -76,6 +79,24 @@ export const loginController: RequestHandler = async (request, response) => {
 | 
			
		||||
 | 
			
		||||
    handleNonceInvalidation(account._id.toString());
 | 
			
		||||
 | 
			
		||||
    // If the client crashed during an endless fissure mission, discharge rewards to an inbox message. (https://www.reddit.com/r/Warframe/comments/5uwwjm/til_if_you_crash_during_a_fissure_you_keep_any/)
 | 
			
		||||
    const inventory = await getInventory(account._id.toString(), "MissionRelicRewards");
 | 
			
		||||
    if (inventory.MissionRelicRewards) {
 | 
			
		||||
        await createMessage(account._id, [
 | 
			
		||||
            {
 | 
			
		||||
                sndr: "/Lotus/Language/Bosses/Ordis",
 | 
			
		||||
                msg: "/Lotus/Language/Menu/VoidProjectionItemsMessage",
 | 
			
		||||
                sub: "/Lotus/Language/Menu/VoidProjectionItemsSubject",
 | 
			
		||||
                icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
 | 
			
		||||
                countedAtt: inventory.MissionRelicRewards.map(x => ({ ...x, ItemType: fromStoreItem(x.ItemType) })),
 | 
			
		||||
                attVisualOnly: true,
 | 
			
		||||
                highPriority: true // TOVERIFY
 | 
			
		||||
            }
 | 
			
		||||
        ]);
 | 
			
		||||
        inventory.MissionRelicRewards = undefined;
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    response.json(createLoginResponse(myAddress, myUrlBase, account.toJSON(), buildLabel));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -129,14 +129,22 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
 | 
			
		||||
        res.json(deltas);
 | 
			
		||||
    } else if (missionReport.RewardInfo) {
 | 
			
		||||
        logger.debug(`classic mission completion, sending everything`);
 | 
			
		||||
        const inventoryResponse = await getInventoryResponse(inventory, true, account.BuildLabel);
 | 
			
		||||
        const inventoryResponse = await getInventoryResponse(
 | 
			
		||||
            inventory,
 | 
			
		||||
            "xpBasedLevelCapDisabled" in req.query,
 | 
			
		||||
            account.BuildLabel
 | 
			
		||||
        );
 | 
			
		||||
        res.json({
 | 
			
		||||
            InventoryJson: JSON.stringify(inventoryResponse),
 | 
			
		||||
            ...deltas
 | 
			
		||||
        } satisfies IMissionInventoryUpdateResponse);
 | 
			
		||||
    } else {
 | 
			
		||||
        logger.debug(`no reward info, assuming this wasn't a mission completion and we should just sync inventory`);
 | 
			
		||||
        const inventoryResponse = await getInventoryResponse(inventory, true, account.BuildLabel);
 | 
			
		||||
        const inventoryResponse = await getInventoryResponse(
 | 
			
		||||
            inventory,
 | 
			
		||||
            "xpBasedLevelCapDisabled" in req.query,
 | 
			
		||||
            account.BuildLabel
 | 
			
		||||
        );
 | 
			
		||||
        res.json({
 | 
			
		||||
            InventoryJson: JSON.stringify(inventoryResponse)
 | 
			
		||||
        } satisfies IMissionInventoryUpdateResponseBackToDryDock);
 | 
			
		||||
 | 
			
		||||
@ -149,7 +149,10 @@ export const nemesisController: RequestHandler = async (req, res) => {
 | 
			
		||||
                            break;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                inventory.Nemesis!.HenchmenKilled += antivirusGain;
 | 
			
		||||
                const antivirusGainMultiplier = (
 | 
			
		||||
                    await getInventory(account._id.toString(), "nemesisAntivirusGainMultiplier")
 | 
			
		||||
                ).nemesisAntivirusGainMultiplier;
 | 
			
		||||
                inventory.Nemesis!.HenchmenKilled += antivirusGain * (antivirusGainMultiplier ?? 1);
 | 
			
		||||
                if (inventory.Nemesis!.HenchmenKilled >= 100) {
 | 
			
		||||
                    inventory.Nemesis!.HenchmenKilled = 100;
 | 
			
		||||
 | 
			
		||||
@ -307,6 +310,17 @@ export const nemesisController: RequestHandler = async (req, res) => {
 | 
			
		||||
        res.json({
 | 
			
		||||
            target: inventory.toJSON().Nemesis
 | 
			
		||||
        });
 | 
			
		||||
    } else if ((req.query.mode as string) == "d") {
 | 
			
		||||
        const inventory = await getInventory(account._id.toString(), "NemesisHistory");
 | 
			
		||||
        const body = getJSONfromString<IRelinquishAdversariesRequest>(String(req.body));
 | 
			
		||||
        for (const fp of body.nemesisFingerprints) {
 | 
			
		||||
            const index = inventory.NemesisHistory!.findIndex(x => x.fp == fp);
 | 
			
		||||
            if (index != -1) {
 | 
			
		||||
                inventory.NemesisHistory!.splice(index, 1);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
        res.json(body);
 | 
			
		||||
    } else if ((req.query.mode as string) == "w") {
 | 
			
		||||
        const inventory = await getInventory(account._id.toString(), "Nemesis");
 | 
			
		||||
        //const body = getJSONfromString<INemesisWeakenRequest>(String(req.body));
 | 
			
		||||
@ -444,3 +458,7 @@ const consumeModCharge = (
 | 
			
		||||
        response.UpgradeNew.push(true);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IRelinquishAdversariesRequest {
 | 
			
		||||
    nemesisFingerprints: (bigint | number)[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ import {
 | 
			
		||||
import { createMessage } from "../../services/inboxService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountForRequest, getSuffixedName } from "../../services/loginService.ts";
 | 
			
		||||
import { sendWsBroadcastTo } from "../../services/wsService.ts";
 | 
			
		||||
import { GuildPermission } from "../../types/guildTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
@ -85,6 +86,7 @@ export const removeFromGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
        ItemToRemove: "/Lotus/Types/Keys/DojoKey",
 | 
			
		||||
        RecipeToRemove: "/Lotus/Types/Keys/DojoKeyBlueprint"
 | 
			
		||||
    });
 | 
			
		||||
    sendWsBroadcastTo(payload.userId, { update_inventory: true });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IRemoveFromGuildRequest {
 | 
			
		||||
 | 
			
		||||
@ -57,7 +57,7 @@ export const setGuildMotdController: RequestHandler = async (req, res) => {
 | 
			
		||||
        await guild.save();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!account.BuildLabel || version_compare(account.BuildLabel, "2020.03.24.20.24") > 0) {
 | 
			
		||||
    if (!account.BuildLabel || version_compare(account.BuildLabel, "2020.11.04.18.58") > 0) {
 | 
			
		||||
        res.json({ IsLongMOTD, MOTD });
 | 
			
		||||
    } else {
 | 
			
		||||
        res.send(MOTD).end();
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@ import { Types } from "mongoose";
 | 
			
		||||
import { ExportDojoRecipes } from "warframe-public-export-plus";
 | 
			
		||||
import { getAccountForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { fromOid } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
 | 
			
		||||
interface IStartDojoRecipeRequest {
 | 
			
		||||
    PlacedComponent: IDojoComponentClient;
 | 
			
		||||
@ -50,7 +51,7 @@ export const startDojoRecipeController: RequestHandler = async (req, res) => {
 | 
			
		||||
                _id: componentId,
 | 
			
		||||
                pf: request.PlacedComponent.pf,
 | 
			
		||||
                ppf: request.PlacedComponent.ppf,
 | 
			
		||||
                pi: new Types.ObjectId(request.PlacedComponent.pi!.$oid),
 | 
			
		||||
                pi: new Types.ObjectId(fromOid(request.PlacedComponent.pi!)),
 | 
			
		||||
                op: request.PlacedComponent.op,
 | 
			
		||||
                pp: request.PlacedComponent.pp,
 | 
			
		||||
                DecoCapacity: room?.decoCapacity
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,7 @@ import type { IUpdateQuestRequest } from "../../services/questService.ts";
 | 
			
		||||
import { updateQuestKey } from "../../services/questService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
			
		||||
import { sendWsBroadcastTo } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
export const updateQuestController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = parseString(req.query.accountId);
 | 
			
		||||
@ -29,4 +30,5 @@ export const updateQuestController: RequestHandler = async (req, res) => {
 | 
			
		||||
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.send(updateQuestResponse);
 | 
			
		||||
    sendWsBroadcastTo(accountId, { update_inventory: true });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,16 +1,16 @@
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { hasAccessToDojo, getGuildForRequestEx, hasGuildPermission } from "../../services/guildService.ts";
 | 
			
		||||
import { getGuildForRequestEx, hasGuildPermission } from "../../services/guildService.ts";
 | 
			
		||||
import { GuildPermission } from "../../types/guildTypes.ts";
 | 
			
		||||
import type { ITypeCount } from "../../types/commonTypes.ts";
 | 
			
		||||
 | 
			
		||||
export const addVaultDecoRecipeController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const requests = req.body as ITypeCount[];
 | 
			
		||||
    const inventory = await getInventory(accountId, "LevelKeys GuildId");
 | 
			
		||||
    const inventory = await getInventory(accountId, "GuildId");
 | 
			
		||||
    const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
    if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Architect))) {
 | 
			
		||||
    if (!(await hasGuildPermission(guild, accountId, GuildPermission.Architect))) {
 | 
			
		||||
        res.status(400).send("-1").end();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getAccountForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { Account, Ignore } from "../../models/loginModel.ts";
 | 
			
		||||
import { Inbox } from "../../models/inboxModel.ts";
 | 
			
		||||
import { Inventory } from "../../models/inventoryModels/inventoryModel.ts";
 | 
			
		||||
@ -12,33 +12,44 @@ import { Leaderboard } from "../../models/leaderboardModel.ts";
 | 
			
		||||
import { deleteGuild } from "../../services/guildService.ts";
 | 
			
		||||
import { Friendship } from "../../models/friendModel.ts";
 | 
			
		||||
import { sendWsBroadcastTo } from "../../services/wsService.ts";
 | 
			
		||||
import { config } from "../../services/configService.ts";
 | 
			
		||||
import { saveConfig } from "../../services/configWriterService.ts";
 | 
			
		||||
 | 
			
		||||
export const deleteAccountController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const account = await getAccountForRequest(req);
 | 
			
		||||
 | 
			
		||||
    // If this account is an admin, remove it from administratorNames
 | 
			
		||||
    if (config.administratorNames) {
 | 
			
		||||
        const adminIndex = config.administratorNames.indexOf(account.DisplayName);
 | 
			
		||||
        if (adminIndex != -1) {
 | 
			
		||||
            config.administratorNames.splice(adminIndex, 1);
 | 
			
		||||
            await saveConfig();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // If account is the founding warlord of a guild, delete that guild as well.
 | 
			
		||||
    const guildMember = await GuildMember.findOne({ accountId, rank: 0, status: 0 });
 | 
			
		||||
    const guildMember = await GuildMember.findOne({ accountId: account._id, rank: 0, status: 0 });
 | 
			
		||||
    if (guildMember) {
 | 
			
		||||
        await deleteGuild(guildMember.guildId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        Account.deleteOne({ _id: accountId }),
 | 
			
		||||
        Friendship.deleteMany({ owner: accountId }),
 | 
			
		||||
        Friendship.deleteMany({ friend: accountId }),
 | 
			
		||||
        GuildMember.deleteMany({ accountId: accountId }),
 | 
			
		||||
        Ignore.deleteMany({ ignorer: accountId }),
 | 
			
		||||
        Ignore.deleteMany({ ignoree: accountId }),
 | 
			
		||||
        Inbox.deleteMany({ ownerId: accountId }),
 | 
			
		||||
        Inventory.deleteOne({ accountOwnerId: accountId }),
 | 
			
		||||
        Leaderboard.deleteMany({ ownerId: accountId }),
 | 
			
		||||
        Loadout.deleteOne({ loadoutOwnerId: accountId }),
 | 
			
		||||
        PersonalRooms.deleteOne({ personalRoomsOwnerId: accountId }),
 | 
			
		||||
        Ship.deleteMany({ ShipOwnerId: accountId }),
 | 
			
		||||
        Stats.deleteOne({ accountOwnerId: accountId })
 | 
			
		||||
        Account.deleteOne({ _id: account._id }),
 | 
			
		||||
        Friendship.deleteMany({ owner: account._id }),
 | 
			
		||||
        Friendship.deleteMany({ friend: account._id }),
 | 
			
		||||
        GuildMember.deleteMany({ accountId: account._id }),
 | 
			
		||||
        Ignore.deleteMany({ ignorer: account._id }),
 | 
			
		||||
        Ignore.deleteMany({ ignoree: account._id }),
 | 
			
		||||
        Inbox.deleteMany({ ownerId: account._id }),
 | 
			
		||||
        Inventory.deleteOne({ accountOwnerId: account._id }),
 | 
			
		||||
        Leaderboard.deleteMany({ ownerId: account._id }),
 | 
			
		||||
        Loadout.deleteOne({ loadoutOwnerId: account._id }),
 | 
			
		||||
        PersonalRooms.deleteOne({ personalRoomsOwnerId: account._id }),
 | 
			
		||||
        Ship.deleteMany({ ShipOwnerId: account._id }),
 | 
			
		||||
        Stats.deleteOne({ accountOwnerId: account._id })
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    sendWsBroadcastTo(accountId, { logged_out: true });
 | 
			
		||||
    sendWsBroadcastTo(account._id.toString(), { logged_out: true });
 | 
			
		||||
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,36 +0,0 @@
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
const DEFAULT_UPGRADE_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
 | 
			
		||||
 | 
			
		||||
export const editSuitInvigorationUpgradeController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const { oid, data } = req.body as {
 | 
			
		||||
        oid: string;
 | 
			
		||||
        data?: {
 | 
			
		||||
            DefensiveUpgrade: string;
 | 
			
		||||
            OffensiveUpgrade: string;
 | 
			
		||||
            UpgradesExpiry?: number;
 | 
			
		||||
        };
 | 
			
		||||
    };
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
    const suit = inventory.Suits.id(oid)!;
 | 
			
		||||
    if (data) {
 | 
			
		||||
        suit.DefensiveUpgrade = data.DefensiveUpgrade;
 | 
			
		||||
        suit.OffensiveUpgrade = data.OffensiveUpgrade;
 | 
			
		||||
        if (data.UpgradesExpiry) {
 | 
			
		||||
            suit.UpgradesExpiry = new Date(data.UpgradesExpiry);
 | 
			
		||||
        } else {
 | 
			
		||||
            suit.UpgradesExpiry = new Date(Date.now() + DEFAULT_UPGRADE_EXPIRY_MS);
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        suit.DefensiveUpgrade = undefined;
 | 
			
		||||
        suit.OffensiveUpgrade = undefined;
 | 
			
		||||
        suit.UpgradesExpiry = undefined;
 | 
			
		||||
    }
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
    broadcastInventoryUpdate(req);
 | 
			
		||||
};
 | 
			
		||||
@ -115,7 +115,7 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
 | 
			
		||||
                if (stage > 0) {
 | 
			
		||||
                    await giveKeyChainStageTriggered(inventory, {
 | 
			
		||||
                        KeyChain: questKey.ItemType,
 | 
			
		||||
                        ChainStage: stage
 | 
			
		||||
                        ChainStage: stage - 1
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -1,22 +1,31 @@
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { sendWsBroadcastTo } from "../../services/wsService.ts";
 | 
			
		||||
import { sendWsBroadcastEx, sendWsBroadcastTo } from "../../services/wsService.ts";
 | 
			
		||||
import type { IAccountCheats } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { logger } from "../../utils/logger.ts";
 | 
			
		||||
 | 
			
		||||
export const setAccountCheatController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const payload = req.body as ISetAccountCheatRequest;
 | 
			
		||||
    const inventory = await getInventory(accountId, payload.key);
 | 
			
		||||
    inventory[payload.key] = payload.value;
 | 
			
		||||
 | 
			
		||||
    if (payload.value == undefined) {
 | 
			
		||||
        logger.warn(`Aborting setting ${payload.key} as undefined!`);
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    inventory[payload.key] = payload.value as never;
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
    if (["infiniteCredits", "infinitePlatinum", "infiniteEndo", "infiniteRegalAya"].indexOf(payload.key) != -1) {
 | 
			
		||||
        sendWsBroadcastTo(accountId, { update_inventory: true, sync_inventory: true });
 | 
			
		||||
    } else {
 | 
			
		||||
        sendWsBroadcastEx({ update_inventory: true }, accountId, parseInt(String(req.query.wsid)));
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ISetAccountCheatRequest {
 | 
			
		||||
    key: keyof IAccountCheats;
 | 
			
		||||
    value: boolean;
 | 
			
		||||
    value: IAccountCheats[keyof IAccountCheats];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,44 +1,19 @@
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { ExportBoosters } from "warframe-public-export-plus";
 | 
			
		||||
import type { IBooster } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
const I32_MAX = 0x7fffffff;
 | 
			
		||||
 | 
			
		||||
export const setBoosterController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const requests = req.body as { ItemType: string; ExpiryDate: number }[];
 | 
			
		||||
    const requests = req.body as IBooster[];
 | 
			
		||||
    const inventory = await getInventory(accountId, "Boosters");
 | 
			
		||||
    const boosters = inventory.Boosters;
 | 
			
		||||
    if (
 | 
			
		||||
        requests.some(request => {
 | 
			
		||||
            if (typeof request.ItemType !== "string") return true;
 | 
			
		||||
            if (Object.entries(ExportBoosters).find(([_, item]) => item.typeName === request.ItemType) === undefined)
 | 
			
		||||
                return true;
 | 
			
		||||
            if (typeof request.ExpiryDate !== "number") return true;
 | 
			
		||||
            if (request.ExpiryDate < 0 || request.ExpiryDate > I32_MAX) return true;
 | 
			
		||||
            return false;
 | 
			
		||||
        })
 | 
			
		||||
    ) {
 | 
			
		||||
        res.status(400).send("Invalid ItemType provided.");
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    const now = Math.trunc(Date.now() / 1000);
 | 
			
		||||
    for (const { ItemType, ExpiryDate } of requests) {
 | 
			
		||||
        if (ExpiryDate <= now) {
 | 
			
		||||
            // remove expired boosters
 | 
			
		||||
            const index = boosters.findIndex(item => item.ItemType === ItemType);
 | 
			
		||||
    for (const request of requests) {
 | 
			
		||||
        const index = inventory.Boosters.findIndex(item => item.ItemType === request.ItemType);
 | 
			
		||||
        if (index !== -1) {
 | 
			
		||||
                boosters.splice(index, 1);
 | 
			
		||||
            }
 | 
			
		||||
            inventory.Boosters[index].ExpiryDate = request.ExpiryDate;
 | 
			
		||||
        } else {
 | 
			
		||||
            const boosterItem = boosters.find(item => item.ItemType === ItemType);
 | 
			
		||||
            if (boosterItem) {
 | 
			
		||||
                boosterItem.ExpiryDate = ExpiryDate;
 | 
			
		||||
            } else {
 | 
			
		||||
                boosters.push({ ItemType, ExpiryDate });
 | 
			
		||||
            }
 | 
			
		||||
            inventory.Boosters.push(request);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { GuildMember } from "../../models/guildModel.ts";
 | 
			
		||||
import { getGuildForRequestEx, hasAccessToDojo } from "../../services/guildService.ts";
 | 
			
		||||
import { getGuildForRequestEx } from "../../services/guildService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { IGuildCheats } from "../../types/guildTypes.ts";
 | 
			
		||||
@ -8,12 +8,12 @@ import type { RequestHandler } from "express";
 | 
			
		||||
export const setGuildCheatController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const payload = req.body as ISetGuildCheatRequest;
 | 
			
		||||
    const inventory = await getInventory(accountId, `${payload.key} GuildId LevelKeys`);
 | 
			
		||||
    const inventory = await getInventory(accountId, `GuildId`);
 | 
			
		||||
    const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
    const member = await GuildMember.findOne({ accountId: accountId, guildId: guild._id });
 | 
			
		||||
 | 
			
		||||
    if (member) {
 | 
			
		||||
        if (!hasAccessToDojo(inventory) || member.rank > 1) {
 | 
			
		||||
        if (member.rank > 1) {
 | 
			
		||||
            res.end();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										27
									
								
								src/controllers/custom/setInvigorationController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/controllers/custom/setInvigorationController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
export const setInvigorationController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const request = req.body as ISetInvigorationRequest;
 | 
			
		||||
    const inventory = await getInventory(accountId, "Suits");
 | 
			
		||||
    const suit = inventory.Suits.id(request.oid);
 | 
			
		||||
    if (suit) {
 | 
			
		||||
        const hasUpgrades = request.DefensiveUpgrade && request.OffensiveUpgrade && request.UpgradesExpiry;
 | 
			
		||||
        suit.DefensiveUpgrade = hasUpgrades ? request.DefensiveUpgrade : undefined;
 | 
			
		||||
        suit.OffensiveUpgrade = hasUpgrades ? request.OffensiveUpgrade : undefined;
 | 
			
		||||
        suit.UpgradesExpiry = hasUpgrades ? new Date(request.UpgradesExpiry) : undefined;
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
        broadcastInventoryUpdate(req);
 | 
			
		||||
    }
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ISetInvigorationRequest {
 | 
			
		||||
    oid: string;
 | 
			
		||||
    DefensiveUpgrade: string;
 | 
			
		||||
    OffensiveUpgrade: string;
 | 
			
		||||
    UpgradesExpiry: number;
 | 
			
		||||
}
 | 
			
		||||
@ -2,7 +2,6 @@ import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import {
 | 
			
		||||
    hasAccessToDojo,
 | 
			
		||||
    getGuildForRequestEx,
 | 
			
		||||
    setGuildTechLogState,
 | 
			
		||||
    processFundedGuildTechProject,
 | 
			
		||||
@ -19,9 +18,9 @@ import { GuildMember } from "../../models/guildModel.ts";
 | 
			
		||||
export const addTechProjectController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const requests = req.body as ITechProjectRequest[];
 | 
			
		||||
    const inventory = await getInventory(accountId, "LevelKeys GuildId");
 | 
			
		||||
    const inventory = await getInventory(accountId, "GuildId");
 | 
			
		||||
    const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
    if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
 | 
			
		||||
    if (!(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
 | 
			
		||||
        res.status(400).send("-1").end();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
@ -54,9 +53,9 @@ export const addTechProjectController: RequestHandler = async (req, res) => {
 | 
			
		||||
export const removeTechProjectController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const requests = req.body as ITechProjectRequest[];
 | 
			
		||||
    const inventory = await getInventory(accountId, "LevelKeys GuildId");
 | 
			
		||||
    const inventory = await getInventory(accountId, "GuildId");
 | 
			
		||||
    const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
    if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
 | 
			
		||||
    if (!(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
 | 
			
		||||
        res.status(400).send("-1").end();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
@ -74,13 +73,13 @@ export const removeTechProjectController: RequestHandler = async (req, res) => {
 | 
			
		||||
export const fundTechProjectController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const requests = req.body as ITechProjectRequest[];
 | 
			
		||||
    const inventory = await getInventory(accountId, "LevelKeys GuildId");
 | 
			
		||||
    const inventory = await getInventory(accountId, "GuildId");
 | 
			
		||||
    const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
    const guildMember = (await GuildMember.findOne(
 | 
			
		||||
        { accountId, guildId: guild._id },
 | 
			
		||||
        "RegularCreditsContributed MiscItemsContributed"
 | 
			
		||||
    ))!;
 | 
			
		||||
    if (!hasAccessToDojo(inventory)) {
 | 
			
		||||
    if (!(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
 | 
			
		||||
        res.status(400).send("-1").end();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
@ -105,9 +104,9 @@ export const fundTechProjectController: RequestHandler = async (req, res) => {
 | 
			
		||||
export const completeTechProjectsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const requests = req.body as ITechProjectRequest[];
 | 
			
		||||
    const inventory = await getInventory(accountId, "LevelKeys GuildId");
 | 
			
		||||
    const inventory = await getInventory(accountId, "GuildId");
 | 
			
		||||
    const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
    if (!hasAccessToDojo(inventory)) {
 | 
			
		||||
    if (!(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
 | 
			
		||||
        res.status(400).send("-1").end();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -9,12 +9,13 @@ import { addMiscItems, combineInventoryChanges } from "../services/inventoryServ
 | 
			
		||||
import { handleStoreItemAcquisition } from "../services/purchaseService.ts";
 | 
			
		||||
import type { IInventoryChanges } from "../types/purchaseTypes.ts";
 | 
			
		||||
import { config } from "../services/configService.ts";
 | 
			
		||||
import { log } from "winston";
 | 
			
		||||
 | 
			
		||||
export const crackRelic = async (
 | 
			
		||||
    inventory: TInventoryDatabaseDocument,
 | 
			
		||||
    participant: IVoidTearParticipantInfo,
 | 
			
		||||
    inventoryChanges: IInventoryChanges = {}
 | 
			
		||||
): Promise<IRngResult> => {
 | 
			
		||||
): Promise<IRngResult[]> => {
 | 
			
		||||
    const relic = ExportRelics[participant.VoidProjection];
 | 
			
		||||
    let weights = refinementToWeights[relic.quality];
 | 
			
		||||
    if (relic.quality == "VPQ_SILVER" && inventory.exceptionalRelicsAlwaysGiveBronzeReward) {
 | 
			
		||||
@ -25,6 +26,9 @@ export const crackRelic = async (
 | 
			
		||||
        weights = { COMMON: 0, UNCOMMON: 0, RARE: 1, LEGENDARY: 0 };
 | 
			
		||||
    }
 | 
			
		||||
    logger.debug(`opening a relic of quality ${relic.quality}; rarity weights are`, weights);
 | 
			
		||||
    const allRewards = [];
 | 
			
		||||
    const relicRewardCount = 1 + (inventory.extraRelicRewards ?? 0);
 | 
			
		||||
    for (let i = 0; i < relicRewardCount; i++) {
 | 
			
		||||
        let reward = getRandomWeightedReward(
 | 
			
		||||
            ExportRewards[relic.rewardManifest][0] as { type: string; itemCount: number; rarity: TRarity }[], // rarity is nullable in PE+ typings, but always present for relics
 | 
			
		||||
            weights
 | 
			
		||||
@ -37,6 +41,33 @@ export const crackRelic = async (
 | 
			
		||||
        }
 | 
			
		||||
        logger.debug(`relic rolled`, reward);
 | 
			
		||||
        participant.Reward = reward.type;
 | 
			
		||||
        allRewards.push(reward);
 | 
			
		||||
        // Give reward
 | 
			
		||||
        combineInventoryChanges(
 | 
			
		||||
            inventoryChanges,
 | 
			
		||||
            (await handleStoreItemAcquisition(reward.type, inventory, reward.itemCount)).InventoryChanges
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (inventory.crackRelicForPlatinum) {
 | 
			
		||||
            let platinumReward = 0;
 | 
			
		||||
            switch (reward.rarity) {
 | 
			
		||||
                case "COMMON":
 | 
			
		||||
                    platinumReward = inventory.relicPlatinumCommon ?? 2;
 | 
			
		||||
                    break;
 | 
			
		||||
                case "UNCOMMON":
 | 
			
		||||
                    platinumReward = inventory.relicPlatinumUncommon ?? 5;
 | 
			
		||||
                    break;
 | 
			
		||||
                case "RARE":
 | 
			
		||||
                    platinumReward = inventory.relicPlatinumRare ?? 12;
 | 
			
		||||
                    break;
 | 
			
		||||
                case "LEGENDARY":
 | 
			
		||||
                    logger.warn(`got a legendary reward for a relic!`);
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
            logger.debug(`adding ${platinumReward} platinum to inventory for a ${reward.rarity} reward`);
 | 
			
		||||
            inventory.PremiumCredits += platinumReward;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Remove relic
 | 
			
		||||
    const miscItemChanges = [
 | 
			
		||||
@ -48,13 +79,10 @@ export const crackRelic = async (
 | 
			
		||||
    addMiscItems(inventory, miscItemChanges);
 | 
			
		||||
    combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges });
 | 
			
		||||
 | 
			
		||||
    // Give reward
 | 
			
		||||
    combineInventoryChanges(
 | 
			
		||||
        inventoryChanges,
 | 
			
		||||
        (await handleStoreItemAcquisition(reward.type, inventory, reward.itemCount)).InventoryChanges
 | 
			
		||||
    );
 | 
			
		||||
    // Client has picked its own reward (for lack of choice)
 | 
			
		||||
    participant.ChosenRewardOwner = participant.AccountId;
 | 
			
		||||
 | 
			
		||||
    return reward;
 | 
			
		||||
    return allRewards;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const refinementToWeights = {
 | 
			
		||||
 | 
			
		||||
@ -1462,11 +1462,30 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
 | 
			
		||||
        flawlessRelicsAlwaysGiveSilverReward: Boolean,
 | 
			
		||||
        radiantRelicsAlwaysGiveGoldReward: Boolean,
 | 
			
		||||
        disableDailyTribute: Boolean,
 | 
			
		||||
        gainNoNegativeSyndicateStanding: Boolean,
 | 
			
		||||
        nemesisHenchmenKillsMultiplierGrineer: Number,
 | 
			
		||||
        nemesisHenchmenKillsMultiplierCorpus: Number,
 | 
			
		||||
        nemesisAntivirusGainMultiplier: Number,
 | 
			
		||||
        nemesisHintProgressMultiplierGrineer: Number,
 | 
			
		||||
        nemesisHintProgressMultiplierCorpus: Number,
 | 
			
		||||
        nemesisExtraWeapon: Number,
 | 
			
		||||
        playerSkillGainsMultiplierSpace: Number,
 | 
			
		||||
        playerSkillGainsMultiplierDrifter: Number,
 | 
			
		||||
        extraMissionRewards: Number,
 | 
			
		||||
        strippedItemRewardsMultiplier: Number,
 | 
			
		||||
        extraRelicRewards: Number,
 | 
			
		||||
        crackRelicForPlatinum: Boolean,
 | 
			
		||||
        relicPlatinumCommon: Number,
 | 
			
		||||
        relicPlatinumUncommon: Number,
 | 
			
		||||
        relicPlatinumRare: Number,
 | 
			
		||||
 | 
			
		||||
        SubscribedToEmails: { type: Number, default: 0 },
 | 
			
		||||
        SubscribedToEmailsPersonalized: { type: Number, default: 0 },
 | 
			
		||||
        RewardSeed: BigInt,
 | 
			
		||||
 | 
			
		||||
        // Temporary data so we can show all relic rewards from an endless mission at EOM
 | 
			
		||||
        MissionRelicRewards: { type: [typeCountSchema], default: undefined },
 | 
			
		||||
 | 
			
		||||
        //Credit
 | 
			
		||||
        RegularCredits: { type: Number, default: 0 },
 | 
			
		||||
        //Platinum
 | 
			
		||||
@ -1835,6 +1854,7 @@ inventorySchema.set("toJSON", {
 | 
			
		||||
        delete returnedObject._id;
 | 
			
		||||
        delete returnedObject.__v;
 | 
			
		||||
        delete returnedObject.accountOwnerId;
 | 
			
		||||
        delete returnedObject.MissionRelicRewards;
 | 
			
		||||
 | 
			
		||||
        const inventoryDatabase = returnedObject as Partial<IInventoryDatabase>;
 | 
			
		||||
        const inventoryResponse = returnedObject as IInventoryClient;
 | 
			
		||||
 | 
			
		||||
@ -43,7 +43,7 @@ import { setBoosterController } from "../controllers/custom/setBoosterController
 | 
			
		||||
import { updateFingerprintController } from "../controllers/custom/updateFingerprintController.ts";
 | 
			
		||||
import { unlockLevelCapController } from "../controllers/custom/unlockLevelCapController.ts";
 | 
			
		||||
import { changeModularPartsController } from "../controllers/custom/changeModularPartsController.ts";
 | 
			
		||||
import { editSuitInvigorationUpgradeController } from "../controllers/custom/editSuitInvigorationUpgradeController.ts";
 | 
			
		||||
import { setInvigorationController } from "../controllers/custom/setInvigorationController.ts";
 | 
			
		||||
import { setAccountCheatController } from "../controllers/custom/setAccountCheatController.ts";
 | 
			
		||||
import { setGuildCheatController } from "../controllers/custom/setGuildCheatController.ts";
 | 
			
		||||
 | 
			
		||||
@ -92,7 +92,7 @@ customRouter.post("/setBooster", setBoosterController);
 | 
			
		||||
customRouter.post("/updateFingerprint", updateFingerprintController);
 | 
			
		||||
customRouter.post("/unlockLevelCap", unlockLevelCapController);
 | 
			
		||||
customRouter.post("/changeModularParts", changeModularPartsController);
 | 
			
		||||
customRouter.post("/editSuitInvigorationUpgrade", editSuitInvigorationUpgradeController);
 | 
			
		||||
customRouter.post("/setInvigoration", setInvigorationController);
 | 
			
		||||
customRouter.post("/setAccountCheat", setAccountCheatController);
 | 
			
		||||
customRouter.post("/setGuildCheat", setGuildCheatController);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -48,6 +48,7 @@ export interface IConfig {
 | 
			
		||||
        anniversary?: number;
 | 
			
		||||
        hallowedNightmares?: boolean;
 | 
			
		||||
        hallowedNightmaresRewardsOverride?: number;
 | 
			
		||||
        naberusNightsOverride?: boolean;
 | 
			
		||||
        proxyRebellion?: boolean;
 | 
			
		||||
        proxyRebellionRewardsOverride?: number;
 | 
			
		||||
        galleonOfGhouls?: number;
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,7 @@ import type {
 | 
			
		||||
    ITechProjectDatabase
 | 
			
		||||
} from "../types/guildTypes.ts";
 | 
			
		||||
import { GuildPermission } from "../types/guildTypes.ts";
 | 
			
		||||
import { toMongoDate, toOid, toOid2 } from "../helpers/inventoryHelpers.ts";
 | 
			
		||||
import { toMongoDate, toOid, toOid2, version_compare } from "../helpers/inventoryHelpers.ts";
 | 
			
		||||
import type { Types } from "mongoose";
 | 
			
		||||
import type { IDojoBuild, IDojoResearch } from "warframe-public-export-plus";
 | 
			
		||||
import { ExportDojoRecipes, ExportResources } from "warframe-public-export-plus";
 | 
			
		||||
@ -68,9 +68,15 @@ export const getGuildClient = async (
 | 
			
		||||
    let missingEntry = true;
 | 
			
		||||
    const dataFillInPromises: Promise<void>[] = [];
 | 
			
		||||
    for (const guildMember of guildMembers) {
 | 
			
		||||
        // Use 1-based indexing for clan ranks for versions before U24. In my testing, 2018.06.14.23.21 and below used 1-based indexing and 2019.04.04.21.31 and above used 0-based indexing. I didn't narrow it down further, but I think U24 is a good spot for them to have changed it.
 | 
			
		||||
        let rankBase = 0;
 | 
			
		||||
        if (account.BuildLabel && version_compare(account.BuildLabel, "2018.11.08.14.45") < 0) {
 | 
			
		||||
            rankBase += 1;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const member: IGuildMemberClient = {
 | 
			
		||||
            _id: toOid2(guildMember.accountId, account.BuildLabel),
 | 
			
		||||
            Rank: guildMember.rank,
 | 
			
		||||
            Rank: guildMember.rank + rankBase,
 | 
			
		||||
            Status: guildMember.status,
 | 
			
		||||
            Note: guildMember.RequestMsg,
 | 
			
		||||
            RequestExpiry: guildMember.RequestExpiry ? toMongoDate(guildMember.RequestExpiry) : undefined
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ import type {
 | 
			
		||||
} from "../types/inventoryTypes/commonInventoryTypes.ts";
 | 
			
		||||
import type { IMongoDate } from "../types/commonTypes.ts";
 | 
			
		||||
import type {
 | 
			
		||||
    IBooster,
 | 
			
		||||
    IDialogueClient,
 | 
			
		||||
    IDialogueDatabase,
 | 
			
		||||
    IDialogueHistoryClient,
 | 
			
		||||
@ -463,6 +464,9 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
 | 
			
		||||
    if (client.Accolades !== undefined) {
 | 
			
		||||
        db.Accolades = client.Accolades;
 | 
			
		||||
    }
 | 
			
		||||
    if (client.Boosters !== undefined) {
 | 
			
		||||
        replaceArray<IBooster>(db.Boosters, client.Boosters);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const importLoadOutConfig = (client: ILoadoutConfigClient): ILoadoutConfigDatabase => {
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,6 @@
 | 
			
		||||
import type { IMessageDatabase } from "../models/inboxModel.ts";
 | 
			
		||||
import { Inbox } from "../models/inboxModel.ts";
 | 
			
		||||
import { getAccountForRequest } from "./loginService.ts";
 | 
			
		||||
import type { HydratedDocument } from "mongoose";
 | 
			
		||||
import { Types } from "mongoose";
 | 
			
		||||
import type { Request } from "express";
 | 
			
		||||
import { unixTimesInMs } from "../constants/timeConstants.ts";
 | 
			
		||||
import { config } from "./configService.ts";
 | 
			
		||||
import type { HydratedDocument, Types } from "mongoose";
 | 
			
		||||
 | 
			
		||||
export const getAllMessagesSorted = async (accountId: string): Promise<HydratedDocument<IMessageDatabase>[]> => {
 | 
			
		||||
    const inbox = await Inbox.find({ ownerId: accountId }).sort({ date: -1 });
 | 
			
		||||
@ -29,117 +24,8 @@ export const deleteAllMessagesRead = async (accountId: string): Promise<void> =>
 | 
			
		||||
    await Inbox.deleteMany({ ownerId: accountId, r: true });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const createNewEventMessages = async (req: Request): Promise<void> => {
 | 
			
		||||
    const account = await getAccountForRequest(req);
 | 
			
		||||
    const newEventMessages: IMessageCreationTemplate[] = [];
 | 
			
		||||
 | 
			
		||||
    // Baro
 | 
			
		||||
    const baroIndex = Math.trunc((Date.now() - 910800000) / (unixTimesInMs.day * 14));
 | 
			
		||||
    const baroStart = baroIndex * (unixTimesInMs.day * 14) + 910800000;
 | 
			
		||||
    const baroActualStart = baroStart + unixTimesInMs.day * (config.worldState?.baroAlwaysAvailable ? 0 : 12);
 | 
			
		||||
    if (Date.now() >= baroActualStart && account.LatestEventMessageDate.getTime() < baroActualStart) {
 | 
			
		||||
        newEventMessages.push({
 | 
			
		||||
            sndr: "/Lotus/Language/G1Quests/VoidTraderName",
 | 
			
		||||
            sub: "/Lotus/Language/CommunityMessages/VoidTraderAppearanceTitle",
 | 
			
		||||
            msg: "/Lotus/Language/CommunityMessages/VoidTraderAppearanceMessage",
 | 
			
		||||
            icon: "/Lotus/Interface/Icons/Npcs/BaroKiTeerPortrait.png",
 | 
			
		||||
            startDate: new Date(baroActualStart),
 | 
			
		||||
            endDate: new Date(baroStart + unixTimesInMs.day * 14),
 | 
			
		||||
            CrossPlatform: true,
 | 
			
		||||
            arg: [
 | 
			
		||||
                {
 | 
			
		||||
                    Key: "NODE_NAME",
 | 
			
		||||
                    Tag: ["EarthHUB", "MercuryHUB", "SaturnHUB", "PlutoHUB"][baroIndex % 4]
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            date: new Date(baroActualStart)
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // BUG: Deleting the inbox message manually means it'll just be automatically re-created. This is because we don't use startDate/endDate for these config-toggled events.
 | 
			
		||||
    const promises = [];
 | 
			
		||||
    if (config.worldState?.creditBoost) {
 | 
			
		||||
        promises.push(
 | 
			
		||||
            (async (): Promise<void> => {
 | 
			
		||||
                if (!(await Inbox.exists({ ownerId: account._id, globaUpgradeId: "5b23106f283a555109666672" }))) {
 | 
			
		||||
                    newEventMessages.push({
 | 
			
		||||
                        globaUpgradeId: new Types.ObjectId("5b23106f283a555109666672"),
 | 
			
		||||
                        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
                        sub: "/Lotus/Language/Items/EventDoubleCreditsName",
 | 
			
		||||
                        msg: "/Lotus/Language/Items/EventDoubleCreditsDesc",
 | 
			
		||||
                        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
                        startDate: new Date(),
 | 
			
		||||
                        CrossPlatform: true
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            })()
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    if (config.worldState?.affinityBoost) {
 | 
			
		||||
        promises.push(
 | 
			
		||||
            (async (): Promise<void> => {
 | 
			
		||||
                if (!(await Inbox.exists({ ownerId: account._id, globaUpgradeId: "5b23106f283a555109666673" }))) {
 | 
			
		||||
                    newEventMessages.push({
 | 
			
		||||
                        globaUpgradeId: new Types.ObjectId("5b23106f283a555109666673"),
 | 
			
		||||
                        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
                        sub: "/Lotus/Language/Items/EventDoubleAffinityName",
 | 
			
		||||
                        msg: "/Lotus/Language/Items/EventDoubleAffinityDesc",
 | 
			
		||||
                        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
                        startDate: new Date(),
 | 
			
		||||
                        CrossPlatform: true
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            })()
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    if (config.worldState?.resourceBoost) {
 | 
			
		||||
        promises.push(
 | 
			
		||||
            (async (): Promise<void> => {
 | 
			
		||||
                if (!(await Inbox.exists({ ownerId: account._id, globaUpgradeId: "5b23106f283a555109666674" }))) {
 | 
			
		||||
                    newEventMessages.push({
 | 
			
		||||
                        globaUpgradeId: new Types.ObjectId("5b23106f283a555109666674"),
 | 
			
		||||
                        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
                        sub: "/Lotus/Language/Items/EventDoubleResourceName",
 | 
			
		||||
                        msg: "/Lotus/Language/Items/EventDoubleResourceDesc",
 | 
			
		||||
                        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
                        startDate: new Date(),
 | 
			
		||||
                        CrossPlatform: true
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            })()
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    if (config.worldState?.galleonOfGhouls) {
 | 
			
		||||
        promises.push(
 | 
			
		||||
            (async (): Promise<void> => {
 | 
			
		||||
                if (!(await Inbox.exists({ ownerId: account._id, goalTag: "GalleonRobbery" }))) {
 | 
			
		||||
                    newEventMessages.push({
 | 
			
		||||
                        sndr: "/Lotus/Language/Bosses/BossCouncilorVayHek",
 | 
			
		||||
                        sub: "/Lotus/Language/Events/GalleonRobberyIntroMsgTitle",
 | 
			
		||||
                        msg: "/Lotus/Language/Events/GalleonRobberyIntroMsgDesc",
 | 
			
		||||
                        icon: "/Lotus/Interface/Icons/Npcs/VayHekPortrait.png",
 | 
			
		||||
                        transmission: "/Lotus/Sounds/Dialog/GalleonOfGhouls/DGhoulsWeekOneInbox0010VayHek",
 | 
			
		||||
                        att: ["/Lotus/Upgrades/Skins/Events/OgrisOldSchool"],
 | 
			
		||||
                        startDate: new Date(),
 | 
			
		||||
                        goalTag: "GalleonRobbery"
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            })()
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
    if (newEventMessages.length === 0) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await createMessage(account._id, newEventMessages);
 | 
			
		||||
 | 
			
		||||
    const latestEventMessage = newEventMessages.reduce((prev, current) =>
 | 
			
		||||
        prev.startDate! > current.startDate! ? prev : current
 | 
			
		||||
    );
 | 
			
		||||
    account.LatestEventMessageDate = new Date(latestEventMessage.startDate!);
 | 
			
		||||
    await account.save();
 | 
			
		||||
export const deleteAllMessagesReadNonCin = async (accountId: string): Promise<void> => {
 | 
			
		||||
    await Inbox.deleteMany({ ownerId: accountId, r: true, cinematic: null });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const createMessage = async (
 | 
			
		||||
 | 
			
		||||
@ -2181,6 +2181,10 @@ export const updateSyndicate = (
 | 
			
		||||
): void => {
 | 
			
		||||
    syndicateUpdate?.forEach(affiliation => {
 | 
			
		||||
        const syndicate = inventory.Affiliations.find(x => x.Tag == affiliation.Tag);
 | 
			
		||||
        if (inventory.gainNoNegativeSyndicateStanding) {
 | 
			
		||||
            affiliation.Standing = Math.max(0, affiliation.Standing);
 | 
			
		||||
            affiliation.Title = Math.max(0, affiliation.Title);
 | 
			
		||||
        }
 | 
			
		||||
        if (syndicate !== undefined) {
 | 
			
		||||
            syndicate.Standing += affiliation.Standing;
 | 
			
		||||
            syndicate.Title = syndicate.Title === undefined ? affiliation.Title : syndicate.Title + affiliation.Title;
 | 
			
		||||
 | 
			
		||||
@ -210,10 +210,29 @@ export const addMissionInventoryUpdates = async (
 | 
			
		||||
            inventory.NemesisAbandonedRewards = inventoryUpdates.RewardInfo.NemesisAbandonedRewards;
 | 
			
		||||
        }
 | 
			
		||||
        if (inventoryUpdates.RewardInfo.NemesisHenchmenKills && inventory.Nemesis) {
 | 
			
		||||
            inventory.Nemesis.HenchmenKilled += inventoryUpdates.RewardInfo.NemesisHenchmenKills;
 | 
			
		||||
            let HenchmenKilledMultiplier = 1;
 | 
			
		||||
            switch (inventory.Nemesis.Faction) {
 | 
			
		||||
                case "FC_GRINEER":
 | 
			
		||||
                    HenchmenKilledMultiplier = inventory.nemesisHenchmenKillsMultiplierGrineer ?? 1;
 | 
			
		||||
                    break;
 | 
			
		||||
                case "FC_CORPUS":
 | 
			
		||||
                    HenchmenKilledMultiplier = inventory.nemesisHenchmenKillsMultiplierCorpus ?? 1;
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
            inventory.Nemesis.HenchmenKilled +=
 | 
			
		||||
                inventoryUpdates.RewardInfo.NemesisHenchmenKills * HenchmenKilledMultiplier;
 | 
			
		||||
        }
 | 
			
		||||
        if (inventoryUpdates.RewardInfo.NemesisHintProgress && inventory.Nemesis) {
 | 
			
		||||
            inventory.Nemesis.HintProgress += inventoryUpdates.RewardInfo.NemesisHintProgress;
 | 
			
		||||
            let HintProgressMultiplier = 1;
 | 
			
		||||
            switch (inventory.Nemesis.Faction) {
 | 
			
		||||
                case "FC_GRINEER":
 | 
			
		||||
                    HintProgressMultiplier = inventory.nemesisHintProgressMultiplierGrineer ?? 1;
 | 
			
		||||
                    break;
 | 
			
		||||
                case "FC_CORPUS":
 | 
			
		||||
                    HintProgressMultiplier = inventory.nemesisHintProgressMultiplierCorpus ?? 1;
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
            inventory.Nemesis.HintProgress += inventoryUpdates.RewardInfo.NemesisHintProgress * HintProgressMultiplier;
 | 
			
		||||
            if (inventory.Nemesis.Faction != "FC_INFESTATION" && inventory.Nemesis.Hints.length != 3) {
 | 
			
		||||
                const progressNeeded = [35, 60, 100][inventory.Nemesis.Hints.length];
 | 
			
		||||
                if (inventory.Nemesis.HintProgress >= progressNeeded) {
 | 
			
		||||
@ -290,9 +309,6 @@ export const addMissionInventoryUpdates = async (
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
            case "Missions":
 | 
			
		||||
                addMissionComplete(inventory, value);
 | 
			
		||||
                break;
 | 
			
		||||
            case "LastRegionPlayed":
 | 
			
		||||
                if (!(config.unfaithfulBugFixes?.ignore1999LastRegionPlayed && value === "1999MapName")) {
 | 
			
		||||
                    inventory.LastRegionPlayed = value;
 | 
			
		||||
@ -356,8 +372,10 @@ export const addMissionInventoryUpdates = async (
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
            case "PlayerSkillGains": {
 | 
			
		||||
                inventory.PlayerSkills.LPP_SPACE += value.LPP_SPACE ?? 0;
 | 
			
		||||
                inventory.PlayerSkills.LPP_DRIFTER += value.LPP_DRIFTER ?? 0;
 | 
			
		||||
                inventory.PlayerSkills.LPP_SPACE +=
 | 
			
		||||
                    (value.LPP_SPACE ?? 0) * (inventory.playerSkillGainsMultiplierSpace ?? 1);
 | 
			
		||||
                inventory.PlayerSkills.LPP_DRIFTER +=
 | 
			
		||||
                    (value.LPP_DRIFTER ?? 0) * (inventory.playerSkillGainsMultiplierDrifter ?? 1);
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
            case "CustomMarkers": {
 | 
			
		||||
@ -819,6 +837,8 @@ export const addMissionInventoryUpdates = async (
 | 
			
		||||
                    const att: string[] = [];
 | 
			
		||||
                    let countedAtt: ITypeCount[] | undefined;
 | 
			
		||||
 | 
			
		||||
                    const extraWeaponCheat = inventory.nemesisExtraWeapon ?? 0; // 0 means no extra weapon and token
 | 
			
		||||
 | 
			
		||||
                    if (value.killed) {
 | 
			
		||||
                        if (
 | 
			
		||||
                            value.weaponLoc &&
 | 
			
		||||
@ -827,6 +847,20 @@ export const addMissionInventoryUpdates = async (
 | 
			
		||||
                            const weaponType = manifest.weapons[inventory.Nemesis.WeaponIdx];
 | 
			
		||||
                            giveNemesisWeaponRecipe(inventory, weaponType, value.nemesisName, value.weaponLoc, profile);
 | 
			
		||||
                            att.push(weaponType);
 | 
			
		||||
                            if (extraWeaponCheat >= 1) {
 | 
			
		||||
                                for (let i = 0; i < extraWeaponCheat; i++) {
 | 
			
		||||
                                    const randomIndex = Math.floor(Math.random() * manifest.weapons.length);
 | 
			
		||||
                                    const randomWeapon = manifest.weapons[randomIndex];
 | 
			
		||||
                                    giveNemesisWeaponRecipe(
 | 
			
		||||
                                        inventory,
 | 
			
		||||
                                        randomWeapon,
 | 
			
		||||
                                        value.nemesisName,
 | 
			
		||||
                                        undefined,
 | 
			
		||||
                                        profile
 | 
			
		||||
                                    );
 | 
			
		||||
                                    att.push(randomWeapon);
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        //if (value.petLoc) {
 | 
			
		||||
                        if (profile.petHead) {
 | 
			
		||||
@ -870,7 +904,7 @@ export const addMissionInventoryUpdates = async (
 | 
			
		||||
                            countedAtt = [
 | 
			
		||||
                                {
 | 
			
		||||
                                    ItemType: "/Lotus/Types/Items/MiscItems/CodaWeaponBucks",
 | 
			
		||||
                                    ItemCount: getKillTokenRewardCount(inventory.Nemesis.fp)
 | 
			
		||||
                                    ItemCount: getKillTokenRewardCount(inventory.Nemesis.fp) * (extraWeaponCheat + 1)
 | 
			
		||||
                                }
 | 
			
		||||
                            ];
 | 
			
		||||
                            addMiscItems(inventory, countedAtt);
 | 
			
		||||
@ -1114,6 +1148,22 @@ export const addMissionRewards = async (
 | 
			
		||||
        firstCompletion
 | 
			
		||||
    );
 | 
			
		||||
    logger.debug("random mission drops:", MissionRewards);
 | 
			
		||||
 | 
			
		||||
    if (inventory.extraMissionRewards) {
 | 
			
		||||
        for (let i = 0; i < inventory.extraMissionRewards; i++) {
 | 
			
		||||
            logger.debug("generating extra mission rewards with new seed, this will mismatch the mission report.");
 | 
			
		||||
            // otherwise would always get the same rewards
 | 
			
		||||
            const extraDrops = getRandomMissionDrops(
 | 
			
		||||
                inventory,
 | 
			
		||||
                { ...rewardInfo, rewardSeed: generateRewardSeed() },
 | 
			
		||||
                missions,
 | 
			
		||||
                wagerTier,
 | 
			
		||||
                firstCompletion
 | 
			
		||||
            );
 | 
			
		||||
            MissionRewards.push(...extraDrops);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const inventoryChanges: IInventoryChanges = {};
 | 
			
		||||
    let SyndicateXPItemReward;
 | 
			
		||||
    let ConquestCompletedMissionsCount;
 | 
			
		||||
@ -1173,6 +1223,9 @@ export const addMissionRewards = async (
 | 
			
		||||
    if (missions && missions.Tag in ExportRegions) {
 | 
			
		||||
        const node = ExportRegions[missions.Tag];
 | 
			
		||||
 | 
			
		||||
        // cannot add this with normal updates because { Tier: 1 } would mark the SP node as completed even on a failure
 | 
			
		||||
        addMissionComplete(inventory, missions);
 | 
			
		||||
 | 
			
		||||
        //node based credit rewards for mission completion
 | 
			
		||||
        if (isEligibleForCreditReward(rewardInfo, missions, node)) {
 | 
			
		||||
            const levelCreditReward = getLevelCreditRewards(node);
 | 
			
		||||
@ -1299,13 +1352,23 @@ export const addMissionRewards = async (
 | 
			
		||||
        rngRewardCredits: inventoryChanges.RegularCredits ?? 0
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        voidTearWave &&
 | 
			
		||||
        voidTearWave.Participants[0].QualifiesForReward &&
 | 
			
		||||
        !voidTearWave.Participants[0].HaveRewardResponse
 | 
			
		||||
    ) {
 | 
			
		||||
        const reward = await crackRelic(inventory, voidTearWave.Participants[0], inventoryChanges);
 | 
			
		||||
    if (voidTearWave && voidTearWave.Participants[0].QualifiesForReward) {
 | 
			
		||||
        if (!voidTearWave.Participants[0].HaveRewardResponse) {
 | 
			
		||||
            // non-endless fissure; giving reward now
 | 
			
		||||
            const rewards = await crackRelic(inventory, voidTearWave.Participants[0], inventoryChanges);
 | 
			
		||||
            rewards.forEach(reward => {
 | 
			
		||||
                MissionRewards.push({ StoreItem: reward.type, ItemCount: reward.itemCount });
 | 
			
		||||
            });
 | 
			
		||||
        } else if (inventory.MissionRelicRewards) {
 | 
			
		||||
            // endless fissure; already gave reward(s) but should still show in EOM screen
 | 
			
		||||
            for (const reward of inventory.MissionRelicRewards) {
 | 
			
		||||
                MissionRewards.push({
 | 
			
		||||
                    StoreItem: reward.ItemType,
 | 
			
		||||
                    ItemCount: reward.ItemCount
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            inventory.MissionRelicRewards = undefined;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (strippedItems) {
 | 
			
		||||
@ -1315,10 +1378,11 @@ export const addMissionRewards = async (
 | 
			
		||||
                si.DropTable = droptableAliases[si.DropTable];
 | 
			
		||||
            }
 | 
			
		||||
            const droptables = ExportEnemies.droptables[si.DropTable] ?? [];
 | 
			
		||||
            const strippedItemRewardsMultiplier = inventory.strippedItemRewardsMultiplier ?? 1;
 | 
			
		||||
            if (si.DROP_MOD) {
 | 
			
		||||
                const modDroptable = droptables.find(x => x.type == "mod");
 | 
			
		||||
                if (modDroptable) {
 | 
			
		||||
                    for (let i = 0; i != si.DROP_MOD.length; ++i) {
 | 
			
		||||
                    for (let i = 0; i != si.DROP_MOD.length * strippedItemRewardsMultiplier; ++i) {
 | 
			
		||||
                        const reward = getRandomReward(modDroptable.items)!;
 | 
			
		||||
                        logger.debug(`stripped droptable (mods pool) rolled`, reward);
 | 
			
		||||
                        await addItem(inventory, reward.type);
 | 
			
		||||
@ -1335,7 +1399,7 @@ export const addMissionRewards = async (
 | 
			
		||||
            if (si.DROP_BLUEPRINT) {
 | 
			
		||||
                const blueprintDroptable = droptables.find(x => x.type == "blueprint");
 | 
			
		||||
                if (blueprintDroptable) {
 | 
			
		||||
                    for (let i = 0; i != si.DROP_BLUEPRINT.length; ++i) {
 | 
			
		||||
                    for (let i = 0; i != si.DROP_BLUEPRINT.length * strippedItemRewardsMultiplier; ++i) {
 | 
			
		||||
                        const reward = getRandomReward(blueprintDroptable.items)!;
 | 
			
		||||
                        logger.debug(`stripped droptable (blueprints pool) rolled`, reward);
 | 
			
		||||
                        await addItem(inventory, reward.type);
 | 
			
		||||
@ -1353,7 +1417,7 @@ export const addMissionRewards = async (
 | 
			
		||||
            if (si.DROP_MISC_ITEM) {
 | 
			
		||||
                const resourceDroptable = droptables.find(x => x.type == "resource");
 | 
			
		||||
                if (resourceDroptable) {
 | 
			
		||||
                    for (let i = 0; i != si.DROP_MISC_ITEM.length; ++i) {
 | 
			
		||||
                    for (let i = 0; i != si.DROP_MISC_ITEM.length * strippedItemRewardsMultiplier; ++i) {
 | 
			
		||||
                        const reward = getRandomReward(resourceDroptable.items)!;
 | 
			
		||||
                        logger.debug(`stripped droptable (resources pool) rolled`, reward);
 | 
			
		||||
                        if (Object.keys(await addItem(inventory, reward.type)).length == 0) {
 | 
			
		||||
@ -1400,7 +1464,9 @@ export const addMissionRewards = async (
 | 
			
		||||
 | 
			
		||||
            if (inventory.Nemesis.Faction == "FC_INFESTATION") {
 | 
			
		||||
                inventory.Nemesis.MissionCount += 1;
 | 
			
		||||
                inventory.Nemesis.HenchmenKilled = Math.min(inventory.Nemesis.HenchmenKilled + 5, 95); // 5 progress per mission until 95
 | 
			
		||||
                let antivirusGain = 5;
 | 
			
		||||
                antivirusGain *= inventory.nemesisAntivirusGainMultiplier ?? 1;
 | 
			
		||||
                inventory.Nemesis.HenchmenKilled = Math.min(inventory.Nemesis.HenchmenKilled + antivirusGain, 95); // 5 progress per mission until 95
 | 
			
		||||
 | 
			
		||||
                inventoryChanges.Nemesis.MissionCount ??= 0;
 | 
			
		||||
                inventoryChanges.Nemesis.MissionCount += 1;
 | 
			
		||||
@ -2438,95 +2504,95 @@ const goalMessagesByKey: Record<string, { sndr: string; msg: string; sub: string
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Seasonal/NoraNight.png"
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/LanternEndlessEventKeyA": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/G1Quests/GenericEventRewardMsgDesc",
 | 
			
		||||
        sub: "/Lotus/Language/G1Quests/GenericTacAlertRewardMsgTitle",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/LotusVamp_d.png"
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/LanternEndlessEventKeyB": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/G1Quests/GenericEventRewardMsgDesc",
 | 
			
		||||
        sub: "/Lotus/Language/G1Quests/GenericTacAlertRewardMsgTitle",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/LotusVamp_d.png"
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/LanternEndlessEventKeyD": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/G1Quests/GenericEventRewardMsgDesc",
 | 
			
		||||
        sub: "/Lotus/Language/G1Quests/GenericTacAlertRewardMsgTitle",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/LotusVamp_d.png"
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/LanternEndlessEventKeyC": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/G1Quests/GenericEventRewardMsgDesc",
 | 
			
		||||
        sub: "/Lotus/Language/G1Quests/GenericTacAlertRewardMsgTitle",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/LotusVamp_d.png"
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyHalloween": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/G1Quests/TacAlertHalloweenRewardsBonusBody",
 | 
			
		||||
        sub: "/Lotus/Language/G1Quests/TacAlertHalloweenRewardsBonusTitle",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/LotusVamp_d.png"
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyHalloweenBonus": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/G1Quests/TacAlertHalloweenRewardsBody",
 | 
			
		||||
        sub: "/Lotus/Language/G1Quests/TacAlertHalloweenRewardsTitle",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/LotusVamp_d.png"
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyHalloweenTimeAttack": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/G1Quests/TacAlertHalloweenRewardsBody",
 | 
			
		||||
        sub: "/Lotus/Language/G1Quests/TacAlertHalloweenRewardsTitle",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/LotusVamp_d.png"
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyProxyRebellionOne": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/G1Quests/RazorbackArmadaRewardBody",
 | 
			
		||||
        sub: "/Lotus/Language/G1Quests/GenericTacAlertSmallRewardMsgTitle",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
        arg: ["CREDIT_REWARD"]
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyProxyRebellionTwo": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/G1Quests/RazorbackArmadaRewardBody",
 | 
			
		||||
        sub: "/Lotus/Language/G1Quests/GenericTacAlertSmallRewardMsgTitle",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
        arg: ["CREDIT_REWARD"]
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyProxyRebellionThree": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/G1Quests/RazorbackArmadaRewardBody",
 | 
			
		||||
        sub: "/Lotus/Language/G1Quests/GenericTacAlertSmallRewardMsgTitle",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
        arg: ["CREDIT_REWARD"]
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyProxyRebellionFour": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/G1Quests/GenericTacAlertBadgeRewardMsgDesc",
 | 
			
		||||
        sub: "/Lotus/Language/G1Quests/GenericTacAlertBadgeRewardMsgTitle",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png"
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyProjectNightwatchEasy": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/G1Quests/ProjectNightwatchRewardMsgA",
 | 
			
		||||
        sub: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertMissionOneTitle",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
        arg: ["CREDIT_REWARD"]
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyProjectNightwatch": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertMissionRewardBody",
 | 
			
		||||
        sub: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertMissionTwoTitle",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png"
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyProjectNightwatchHard": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertMissionRewardBody",
 | 
			
		||||
        sub: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertMissionThreeTitle",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png"
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyProjectNightwatchBonus": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertMissionRewardBody",
 | 
			
		||||
        sub: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertMissionFourTitle",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png"
 | 
			
		||||
@ -2562,140 +2628,140 @@ const goalMessagesByKey: Record<string, { sndr: string; msg: string; sub: string
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Entrati/Father.png"
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyAnniversary2019E": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/Messages/Anniversary2024RewardMsgB",
 | 
			
		||||
        sub: "/Lotus/Language/Messages/Anniversary2024MissionTitleB",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
        arg: ["PLAYER_NAME"]
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyAnniversary2020F": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/Messages/Anniversary2024RewardMsgC",
 | 
			
		||||
        sub: "/Lotus/Language/Messages/Anniversary2024MissionTitleB",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
        arg: ["PLAYER_NAME"]
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyAnniversary2024ChallengeModeA": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/Messages/Anniversary2024RewardMsgD",
 | 
			
		||||
        sub: "/Lotus/Language/Messages/Anniversary2024MissionTitleD",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
        arg: ["PLAYER_NAME"]
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyAnniversary2017C": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/Messages/Anniversary2019RewardMsgC",
 | 
			
		||||
        sub: "/Lotus/Language/Messages/Anniversary2019MissionTitleC",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
        arg: ["PLAYER_NAME"]
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyAnniversary2020H": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/Messages/Anniversary2020RewardMsgH",
 | 
			
		||||
        sub: "/Lotus/Language/Messages/Anniversary2020MissionTitleH",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
        arg: ["PLAYER_NAME"]
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyAnniversary2022J": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/Messages/Anniversary2022RewardMsgJ",
 | 
			
		||||
        sub: "/Lotus/Language/Messages/Anniversary2022MissionTitleJ",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
        arg: ["PLAYER_NAME"]
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyAnniversary2025D": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/Messages/Anniversary2025RewardMsgB",
 | 
			
		||||
        sub: "/Lotus/Language/Messages/Anniversary2025MissionTitleB",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
        arg: ["PLAYER_NAME"]
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyAnniversary2025ChallengeModeA": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/Messages/Anniversary2025RewardMsgC",
 | 
			
		||||
        sub: "/Lotus/Language/Messages/Anniversary2025MissionTitleC",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
        arg: ["PLAYER_NAME"]
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyAnniversary2020G": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/Messages/Anniversary2020RewardMsgG",
 | 
			
		||||
        sub: "/Lotus/Language/Messages/Anniversary2020MissionTitleG",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
        arg: ["PLAYER_NAME"]
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyAnniversary2017B": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/Messages/Anniversary2019RewardMsgB",
 | 
			
		||||
        sub: "/Lotus/Language/Messages/Anniversary2019MissionTitleB",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
        arg: ["PLAYER_NAME"]
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyAnniversary2017A": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/Messages/Anniversary2019RewardMsgA",
 | 
			
		||||
        sub: "/Lotus/Language/Messages/Anniversary2019MissionTitleA",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
        arg: ["PLAYER_NAME"]
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyAnniversary2023K": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/Messages/Anniversary2025RewardMsgG",
 | 
			
		||||
        sub: "/Lotus/Language/Messages/Anniversary2025MissionTitleG",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
        arg: ["PLAYER_NAME"]
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyAnniversary2025ChallengeModeB": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/Messages/Anniversary2025RewardMsgD",
 | 
			
		||||
        sub: "/Lotus/Language/Messages/Anniversary2025MissionTitleD",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
        arg: ["PLAYER_NAME"]
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyAnniversary2025A": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/Messages/Anniversary2025RewardMsgA",
 | 
			
		||||
        sub: "/Lotus/Language/Messages/Anniversary2025MissionTitleA",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
        arg: ["PLAYER_NAME"]
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyAnniversary2018D": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/Messages/Anniversary2024RewardMsgG",
 | 
			
		||||
        sub: "/Lotus/Language/Messages/Anniversary2024MissionTitleG",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
        arg: ["PLAYER_NAME"]
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyAnniversary2025C": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/Messages/Anniversary2024RewardMsgF",
 | 
			
		||||
        sub: "/Lotus/Language/Messages/Anniversary2024MissionTitleF",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
        arg: ["PLAYER_NAME"]
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyAnniversary2024L": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/Messages/Anniversary2024RewardMsgA",
 | 
			
		||||
        sub: "/Lotus/Language/Messages/Anniversary2024MissionTitleA",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
        arg: ["PLAYER_NAME"]
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyAnniversary2024ChallengeModeB": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/Messages/Anniversary2024RewardMsgE",
 | 
			
		||||
        sub: "/Lotus/Language/Messages/Anniversary2024MissionTitleE",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
        arg: ["PLAYER_NAME"]
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyAnniversary2021I": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/Messages/Anniversary2024RewardMsgH",
 | 
			
		||||
        sub: "/Lotus/Language/Messages/Anniversary2024MissionTitleH",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
        arg: ["PLAYER_NAME"]
 | 
			
		||||
    },
 | 
			
		||||
    "/Lotus/Types/Keys/TacAlertKeyAnniversary2025B": {
 | 
			
		||||
        sndr: "/Lotus/Language/Bosses/Lotus",
 | 
			
		||||
        sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
        msg: "/Lotus/Language/Messages/Anniversary2025RewardMsgE",
 | 
			
		||||
        sub: "/Lotus/Language/Messages/Anniversary2025MissionTitleE",
 | 
			
		||||
        icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
 | 
			
		||||
 | 
			
		||||
@ -74,7 +74,7 @@ export const updateQuestStage = (
 | 
			
		||||
    if (!questStage) {
 | 
			
		||||
        const questStageIndex =
 | 
			
		||||
            quest.Progress.push({
 | 
			
		||||
                c: questStageUpdate.c ?? 0,
 | 
			
		||||
                c: questStageUpdate.c ?? -1,
 | 
			
		||||
                i: questStageUpdate.i ?? false,
 | 
			
		||||
                m: questStageUpdate.m ?? false,
 | 
			
		||||
                b: questStageUpdate.b ?? []
 | 
			
		||||
@ -331,7 +331,7 @@ export const giveKeyChainMessage = async (
 | 
			
		||||
): Promise<void> => {
 | 
			
		||||
    const keyChainMessage = getKeyChainMessage(keyChainInfo);
 | 
			
		||||
 | 
			
		||||
    if (questKey.Progress![0].c > 0) {
 | 
			
		||||
    if ((questKey.Progress?.[0]?.c ?? 0) > 0) {
 | 
			
		||||
        keyChainMessage.att = [];
 | 
			
		||||
        keyChainMessage.countedAtt = [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -280,6 +280,14 @@ export const getSortie = (day: number): ISortie => {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const willHaveAssassination = boss != "SORTIE_BOSS_CORRUPTED_VOR" && rng.randomInt(0, 2) == 2;
 | 
			
		||||
    if (willHaveAssassination) {
 | 
			
		||||
        const index = nodes.indexOf(sortieBossNode[boss]);
 | 
			
		||||
        if (index != -1) {
 | 
			
		||||
            nodes.splice(index, 1);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const selectedNodes: ISortieMission[] = [];
 | 
			
		||||
    const missionTypes = new Set();
 | 
			
		||||
 | 
			
		||||
@ -309,7 +317,7 @@ export const getSortie = (day: number): ISortie => {
 | 
			
		||||
            "SORTIE_MODIFIER_BOW_ONLY"
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        if (i == 2 && boss != "SORTIE_BOSS_CORRUPTED_VOR" && rng.randomInt(0, 2) == 2) {
 | 
			
		||||
        if (i == 2 && willHaveAssassination) {
 | 
			
		||||
            const tileset = sortieTilesets[sortieBossNode[boss] as keyof typeof sortieTilesets] as TSortieTileset;
 | 
			
		||||
            pushTilesetModifiers(modifiers, tileset);
 | 
			
		||||
 | 
			
		||||
@ -361,7 +369,9 @@ export const getSortie = (day: number): ISortie => {
 | 
			
		||||
        Activation: { $date: { $numberLong: dayStart.toString() } },
 | 
			
		||||
        Expiry: { $date: { $numberLong: dayEnd.toString() } },
 | 
			
		||||
        Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards",
 | 
			
		||||
        Seed: seed,
 | 
			
		||||
        Seed: selectedNodes.find(x => x.tileset == "CorpusIcePlanetTileset")
 | 
			
		||||
            ? 2081 // this seed produces 12 zeroes in a row if asked to pick (0, 1); this way the CorpusIcePlanetTileset image is always index 0, the 'correct' choice.
 | 
			
		||||
            : seed,
 | 
			
		||||
        Boss: boss,
 | 
			
		||||
        Variants: selectedNodes
 | 
			
		||||
    };
 | 
			
		||||
@ -2504,6 +2514,37 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
			
		||||
            BonusReward: { items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"] }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const isOctober = date.getUTCMonth() == 9; // October = month index 9
 | 
			
		||||
    if (config.worldState?.naberusNightsOverride ?? isOctober) {
 | 
			
		||||
        worldState.Goals.push({
 | 
			
		||||
            _id: { $oid: "66fd602de1778d583419e8e7" },
 | 
			
		||||
            Activation: {
 | 
			
		||||
                $date: {
 | 
			
		||||
                    $numberLong: config.worldState?.naberusNightsOverride
 | 
			
		||||
                        ? "1727881200000"
 | 
			
		||||
                        : Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1).toString()
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            Expiry: {
 | 
			
		||||
                $date: {
 | 
			
		||||
                    $numberLong: config.worldState?.naberusNightsOverride
 | 
			
		||||
                        ? "2000000000000"
 | 
			
		||||
                        : Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1).toString()
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            Count: 0,
 | 
			
		||||
            Goal: 0,
 | 
			
		||||
            Success: 0,
 | 
			
		||||
            Personal: true,
 | 
			
		||||
            Desc: "/Lotus/Language/Events/HalloweenNaberusName",
 | 
			
		||||
            ToolTip: "/Lotus/Language/Events/HalloweenNaberusDesc",
 | 
			
		||||
            Icon: "/Lotus/Interface/Icons/JackOLanternColour.png",
 | 
			
		||||
            Tag: "DeimosHalloween",
 | 
			
		||||
            Node: "DeimosHub"
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (config.worldState?.bellyOfTheBeast) {
 | 
			
		||||
        worldState.Goals.push({
 | 
			
		||||
            _id: { $oid: "67a5035c2a198564d62e165e" },
 | 
			
		||||
 | 
			
		||||
@ -55,6 +55,22 @@ export interface IAccountCheats {
 | 
			
		||||
    flawlessRelicsAlwaysGiveSilverReward?: boolean;
 | 
			
		||||
    radiantRelicsAlwaysGiveGoldReward?: boolean;
 | 
			
		||||
    disableDailyTribute?: boolean;
 | 
			
		||||
    gainNoNegativeSyndicateStanding?: boolean;
 | 
			
		||||
    nemesisHenchmenKillsMultiplierGrineer?: number;
 | 
			
		||||
    nemesisHenchmenKillsMultiplierCorpus?: number;
 | 
			
		||||
    nemesisAntivirusGainMultiplier?: number;
 | 
			
		||||
    nemesisHintProgressMultiplierGrineer?: number;
 | 
			
		||||
    nemesisHintProgressMultiplierCorpus?: number;
 | 
			
		||||
    nemesisExtraWeapon?: number;
 | 
			
		||||
    playerSkillGainsMultiplierSpace?: number;
 | 
			
		||||
    playerSkillGainsMultiplierDrifter?: number;
 | 
			
		||||
    extraMissionRewards?: number;
 | 
			
		||||
    strippedItemRewardsMultiplier?: number;
 | 
			
		||||
    extraRelicRewards?: number;
 | 
			
		||||
    crackRelicForPlatinum?: boolean;
 | 
			
		||||
    relicPlatinumCommon?: number;
 | 
			
		||||
    relicPlatinumUncommon?: number;
 | 
			
		||||
    relicPlatinumRare?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IInventoryDatabase
 | 
			
		||||
@ -141,6 +157,7 @@ export interface IInventoryDatabase
 | 
			
		||||
    LastInventorySync?: Types.ObjectId;
 | 
			
		||||
    EndlessXP?: IEndlessXpProgressDatabase[];
 | 
			
		||||
    PersonalGoalProgress?: IGoalProgressDatabase[];
 | 
			
		||||
    MissionRelicRewards?: ITypeCount[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IQuestKeyDatabase {
 | 
			
		||||
 | 
			
		||||
@ -476,9 +476,9 @@
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="col-lg-6">
 | 
			
		||||
                        <div class="card" style="height: 400px;">
 | 
			
		||||
                            <h5 class="card-header" data-loc="inventory_Boosters"></h5>
 | 
			
		||||
                            <h5 class="card-header" data-loc="inventory_boosters"></h5>
 | 
			
		||||
                            <div class="card-body d-flex flex-column">
 | 
			
		||||
                                <form class="input-group mb-3" onsubmit="doAcquireBoosters();return false;">
 | 
			
		||||
                                <form class="input-group mb-3" onsubmit="doAcquireBooster();return false;">
 | 
			
		||||
                                    <input class="form-control" id="acquire-type-Boosters" list="datalist-Boosters" />
 | 
			
		||||
                                    <button class="btn btn-primary" type="submit" data-loc="general_addButton"></button>
 | 
			
		||||
                                </form>
 | 
			
		||||
@ -719,12 +719,12 @@
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div id="edit-suit-invigorations-card" class="card mb-3 d-none">
 | 
			
		||||
                    <h5 class="card-header" data-loc="detailedView_suitInvigorationLabel"></h5>
 | 
			
		||||
                    <h5 class="card-header" data-loc="detailedView_invigorationLabel"></h5>
 | 
			
		||||
                    <div class="card-body">
 | 
			
		||||
                        <form onsubmit="submitSuitInvigorationUpgrade(event)">
 | 
			
		||||
                        <form onsubmit="submitInvigoration(event)">
 | 
			
		||||
                            <div class="mb-3">
 | 
			
		||||
                                <label for="invigoration-offensive" class="form-label" data-loc="invigorations_offensiveLabel"></label>
 | 
			
		||||
                                <select class="form-select" id="dv-invigoration-offensive">
 | 
			
		||||
                                <label for="invigoration-offensive" class="form-label" data-loc="detailedView_invigorationOffensiveLabel"></label>
 | 
			
		||||
                                <select class="form-select" id="invigoration-offensive">
 | 
			
		||||
                                    <option value="" data-loc="general_none"></option>
 | 
			
		||||
                                    <option value="/Lotus/Upgrades/Invigorations/Offensive/OffensiveInvigorationPowerStrength" data-loc="invigorations_offensive_AbilityStrength"></option>
 | 
			
		||||
                                    <option value="/Lotus/Upgrades/Invigorations/Offensive/OffensiveInvigorationPowerRange" data-loc="invigorations_offensive_AbilityRange"></option>
 | 
			
		||||
@ -737,10 +737,9 @@
 | 
			
		||||
                                    <option value="/Lotus/Upgrades/Invigorations/Offensive/OffensiveInvigorationMeleeCritChance" data-loc="invigorations_offensive_MeleeCritChance"></option>
 | 
			
		||||
                                </select>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            
 | 
			
		||||
                            <div class="mb-3">
 | 
			
		||||
                                <label for="invigoration-defensive" class="form-label" data-loc="invigorations_defensiveLabel"></label>
 | 
			
		||||
                                <select class="form-select" id="dv-invigoration-defensive">
 | 
			
		||||
                                <label for="invigoration-defensive" class="form-label" data-loc="detailedView_invigorationUtilityLabel"></label>
 | 
			
		||||
                                <select class="form-select" id="invigoration-defensive">
 | 
			
		||||
                                    <option value="" data-loc="general_none"></option>
 | 
			
		||||
                                    <option value="/Lotus/Upgrades/Invigorations/Utility/UtilityInvigorationPowerEfficiency" data-loc="invigorations_utility_AbilityEfficiency"></option>
 | 
			
		||||
                                    <option value="/Lotus/Upgrades/Invigorations/Utility/UtilityInvigorationMovementSpeed" data-loc="invigorations_utility_SprintSpeed"></option>
 | 
			
		||||
@ -755,15 +754,13 @@
 | 
			
		||||
                                    <option value="/Lotus/Upgrades/Invigorations/Utility/UtilityInvigorationEnergyRegen" data-loc="invigorations_utility_EnergyRegen"></option>
 | 
			
		||||
                                </select>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            
 | 
			
		||||
                            <div class="mb-3">
 | 
			
		||||
                                <label for="invigoration-expiry" class="form-label" data-loc="invigorations_expiryLabel"></label>
 | 
			
		||||
                                <input type="datetime-local" class="form-control" id="dv-invigoration-expiry" />
 | 
			
		||||
                                <label for="invigoration-expiry" class="form-label" data-loc="detailedView_invigorationExpiryLabel"></label>
 | 
			
		||||
                                <input type="datetime-local" class="form-control" max="2038-01-19T03:14" id="invigoration-expiry" onblur="this.value=new Date(this.value)>new Date(this.max)?new Date(this.max).toISOString().slice(0,16):this.value"/>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            
 | 
			
		||||
                            <div class="d-flex gap-2">
 | 
			
		||||
                                <button type="submit" class="btn btn-primary" data-loc="general_setButton"></button>
 | 
			
		||||
                                <button type="button" class="btn btn-danger" onclick="clearSuitInvigorationUpgrades()" data-loc="code_remove"></button>
 | 
			
		||||
                                <button type="button" class="btn btn-danger" onclick="setInvigoration()" data-loc="code_remove"></button>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </form>
 | 
			
		||||
                    </div>
 | 
			
		||||
@ -1018,6 +1015,112 @@
 | 
			
		||||
                                    <input class="form-check-input" type="checkbox" id="finishInvasionsInOneMission" />
 | 
			
		||||
                                    <label class="form-check-label" for="finishInvasionsInOneMission" data-loc="cheats_finishInvasionsInOneMission"></label>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="form-check">
 | 
			
		||||
                                    <input class="form-check-input" type="checkbox" id="gainNoNegativeSyndicateStanding" />
 | 
			
		||||
                                    <label class="form-check-label" for="gainNoNegativeSyndicateStanding" data-loc="cheats_gainNoNegativeSyndicateStanding"></label>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <form class="form-group mt-2">
 | 
			
		||||
                                    <label class="form-label" for="nemesisHenchmenKillsMultiplierGrineer" data-loc="cheats_nemesisHenchmenKillsMultiplierGrineer"></label>
 | 
			
		||||
                                    <div class="input-group">
 | 
			
		||||
                                        <input class="form-control" id="nemesisHenchmenKillsMultiplierGrineer" type="number" min="0" max="65535" data-default="1" />
 | 
			
		||||
                                        <button class="btn btn-secondary" type="button" data-loc="cheats_save"></button>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </form>
 | 
			
		||||
                                <form class="form-group mt-2">
 | 
			
		||||
                                    <label class="form-label" for="nemesisHenchmenKillsMultiplierCorpus" data-loc="cheats_nemesisHenchmenKillsMultiplierCorpus"></label>
 | 
			
		||||
                                    <div class="input-group">
 | 
			
		||||
                                        <input class="form-control" id="nemesisHenchmenKillsMultiplierCorpus" type="number" min="0" max="65535" data-default="1" />
 | 
			
		||||
                                        <button class="btn btn-secondary" type="button" data-loc="cheats_save"></button>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </form>
 | 
			
		||||
                                <form class="form-group mt-2">
 | 
			
		||||
                                    <label class="form-label" for="nemesisAntivirusGainMultiplier" data-loc="cheats_nemesisAntivirusGainMultiplier"></label>
 | 
			
		||||
                                    <div class="input-group">
 | 
			
		||||
                                        <input class="form-control" id="nemesisAntivirusGainMultiplier" type="number" min="0" max="65535" data-default="1" />
 | 
			
		||||
                                        <button class="btn btn-secondary" type="button" data-loc="cheats_save"></button>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </form>
 | 
			
		||||
                                <form class="form-group mt-2">
 | 
			
		||||
                                    <label class="form-label" for="nemesisHintProgressMultiplierGrineer" data-loc="cheats_nemesisHintProgressMultiplierGrineer"></label>
 | 
			
		||||
                                    <div class="input-group">
 | 
			
		||||
                                        <input class="form-control" id="nemesisHintProgressMultiplierGrineer" type="number" min="0" max="65535" data-default="1" />
 | 
			
		||||
                                        <button class="btn btn-secondary" type="button" data-loc="cheats_save"></button>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </form>
 | 
			
		||||
                                <form class="form-group mt-2">
 | 
			
		||||
                                    <label class="form-label" for="nemesisHintProgressMultiplierCorpus" data-loc="cheats_nemesisHintProgressMultiplierCorpus"></label>
 | 
			
		||||
                                    <div class="input-group">
 | 
			
		||||
                                        <input class="form-control" id="nemesisHintProgressMultiplierCorpus" type="number" min="0" max="65535" data-default="1" />
 | 
			
		||||
                                        <button class="btn btn-secondary" type="button" data-loc="cheats_save"></button>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </form>
 | 
			
		||||
                                <form class="form-group mt-2">
 | 
			
		||||
                                    <label class="form-label" for="nemesisExtraWeapon" data-loc="cheats_nemesisExtraWeapon"></label>
 | 
			
		||||
                                    <div class="input-group">
 | 
			
		||||
                                        <input class="form-control" id="nemesisExtraWeapon" type="number" min="0" max="65535" data-default="0" />
 | 
			
		||||
                                        <button class="btn btn-secondary" type="button" data-loc="cheats_save"></button>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </form>
 | 
			
		||||
                                <form class="form-group mt-2">
 | 
			
		||||
                                    <label class="form-label" for="playerSkillGainsMultiplierSpace" data-loc="cheats_playerSkillGainsMultiplierSpace"></label>
 | 
			
		||||
                                    <div class="input-group">
 | 
			
		||||
                                        <input class="form-control" id="playerSkillGainsMultiplierSpace" type="number" min="1" max="65535" data-default="1" />
 | 
			
		||||
                                        <button class="btn btn-secondary" type="button" data-loc="cheats_save"></button>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </form>
 | 
			
		||||
                                <form class="form-group mt-2">
 | 
			
		||||
                                    <label class="form-label" for="playerSkillGainsMultiplierDrifter" data-loc="cheats_playerSkillGainsMultiplierDrifter"></label>
 | 
			
		||||
                                    <div class="input-group">
 | 
			
		||||
                                        <input class="form-control" id="playerSkillGainsMultiplierDrifter" type="number" min="1" max="65535" data-default="1" />
 | 
			
		||||
                                        <button class="btn btn-secondary" type="button" data-loc="cheats_save"></button>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </form>
 | 
			
		||||
                                <form class="form-group mt-2">
 | 
			
		||||
                                    <label class="form-label" for="extraMissionRewards" data-loc="cheats_extraMissionRewards"></label>
 | 
			
		||||
                                    <div class="input-group">
 | 
			
		||||
                                        <input class="form-control" id="extraMissionRewards" type="number" min="0" max="65535" data-default="0" />
 | 
			
		||||
                                        <button class="btn btn-secondary" type="button" data-loc="cheats_save"></button>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </form>
 | 
			
		||||
                                <form class="form-group mt-2">
 | 
			
		||||
                                    <label class="form-label" for="strippedItemRewardsMultiplier" data-loc="cheats_strippedItemRewardsMultiplier"></label>
 | 
			
		||||
                                    <div class="input-group">
 | 
			
		||||
                                        <input class="form-control" id="strippedItemRewardsMultiplier" type="number" min="0" max="65535" data-default="1" />
 | 
			
		||||
                                        <button class="btn btn-secondary" type="button" data-loc="cheats_save"></button>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </form>
 | 
			
		||||
                                <form class="form-group mt-2">
 | 
			
		||||
                                    <label class="form-label" for="extraRelicRewards" data-loc="cheats_extraRelicRewards"></label>
 | 
			
		||||
                                    <div class="input-group">
 | 
			
		||||
                                        <input class="form-control" id="extraRelicRewards" type="number" min="0" max="65535" data-default="0" />
 | 
			
		||||
                                        <button class="btn btn-secondary" type="button" data-loc="cheats_save"></button>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </form>
 | 
			
		||||
                                <div class="form-check">
 | 
			
		||||
                                    <input class="form-check-input" type="checkbox" id="crackRelicForPlatinum" />
 | 
			
		||||
                                    <label class="form-check-label" for="crackRelicForPlatinum" data-loc="cheats_crackRelicForPlatinum"></label>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <form class="form-group mt-2">
 | 
			
		||||
                                    <label class="form-label" for="relicPlatinumCommon" data-loc="cheats_relicPlatinumCommon"></label>
 | 
			
		||||
                                    <div class="input-group">
 | 
			
		||||
                                        <input class="form-control" id="relicPlatinumCommon" type="number" min="0" max="65535" data-default="2" />
 | 
			
		||||
                                        <button class="btn btn-secondary" type="button" data-loc="cheats_save"></button>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </form>
 | 
			
		||||
                                <form class="form-group mt-2">
 | 
			
		||||
                                    <label class="form-label" for="relicPlatinumUncommon" data-loc="cheats_relicPlatinumUncommon"></label>
 | 
			
		||||
                                    <div class="input-group">
 | 
			
		||||
                                        <input class="form-control" id="relicPlatinumUncommon" type="number" min="0" max="65535" data-default="5" />
 | 
			
		||||
                                        <button class="btn btn-secondary" type="button" data-loc="cheats_save"></button>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </form>
 | 
			
		||||
                                <form class="form-group mt-2">
 | 
			
		||||
                                    <label class="form-label" for="relicPlatinumRare" data-loc="cheats_relicPlatinumRare"></label>
 | 
			
		||||
                                    <div class="input-group">
 | 
			
		||||
                                        <input class="form-control" id="relicPlatinumRare" type="number" min="0" max="65535" data-default="12" />
 | 
			
		||||
                                        <button class="btn btn-secondary" type="button" data-loc="cheats_save"></button>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </form>
 | 
			
		||||
                                <div class="mt-2 mb-2 d-flex flex-wrap gap-2">
 | 
			
		||||
                                    <button class="btn btn-primary" onclick="debounce(doUnlockAllShipFeatures);" data-loc="cheats_unlockAllShipFeatures"></button>
 | 
			
		||||
                                    <button class="btn btn-primary" onclick="debounce(unlockAllMissions);" data-loc="cheats_unlockAllMissions"></button>
 | 
			
		||||
@ -1036,7 +1139,7 @@
 | 
			
		||||
                                    <label class="form-label" for="changeSyndicate" data-loc="cheats_changeSupportedSyndicate"></label>
 | 
			
		||||
                                    <div class="input-group">
 | 
			
		||||
                                        <select class="form-control" id="changeSyndicate"></select>
 | 
			
		||||
                                        <button class="btn btn-primary" type="submit" data-loc="cheats_changeButton"></button>
 | 
			
		||||
                                        <button class="btn btn-secondary" type="submit" data-loc="cheats_changeButton"></button>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </form>
 | 
			
		||||
                            </div>
 | 
			
		||||
@ -1168,6 +1271,14 @@
 | 
			
		||||
                                        </select>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="form-group mt-2">
 | 
			
		||||
                                    <label class="form-label" for="worldState.naberusNightsOverride" data-loc="worldState_naberusNights"></label>
 | 
			
		||||
                                    <select class="form-control" id="worldState.naberusNightsOverride" data-default="null">
 | 
			
		||||
                                        <option value="null" data-loc="normal"></option>
 | 
			
		||||
                                        <option value="true" data-loc="enabled"></option>
 | 
			
		||||
                                        <option value="false" data-loc="disabled"></option>
 | 
			
		||||
                                    </select>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="form-group mt-2 d-flex gap-2">
 | 
			
		||||
                                    <div class="flex-fill">
 | 
			
		||||
                                        <label class="form-label" for="worldState.proxyRebellion" data-loc="worldState_proxyRebellion"></label>
 | 
			
		||||
 | 
			
		||||
@ -837,10 +837,9 @@ function updateInventory() {
 | 
			
		||||
                                a.href = "#";
 | 
			
		||||
                                a.onclick = function (event) {
 | 
			
		||||
                                    event.preventDefault();
 | 
			
		||||
                                    revalidateAuthz().then(() => {
 | 
			
		||||
                                        const promises = [];
 | 
			
		||||
                                    revalidateAuthz().then(async () => {
 | 
			
		||||
                                        if (item.XP < maxXP) {
 | 
			
		||||
                                            promises.push(addGearExp(category, item.ItemId.$oid, maxXP - item.XP));
 | 
			
		||||
                                            await addGearExp(category, item.ItemId.$oid, maxXP - item.XP);
 | 
			
		||||
                                        }
 | 
			
		||||
                                        if ("exalted" in itemMap[item.ItemType]) {
 | 
			
		||||
                                            for (const exaltedType of itemMap[item.ItemType].exalted) {
 | 
			
		||||
@ -851,21 +850,17 @@ function updateInventory() {
 | 
			
		||||
                                                    const exaltedCap =
 | 
			
		||||
                                                        itemMap[exaltedType]?.type == "weapons" ? 800_000 : 1_600_000;
 | 
			
		||||
                                                    if (exaltedItem.XP < exaltedCap) {
 | 
			
		||||
                                                        promises.push(
 | 
			
		||||
                                                            addGearExp(
 | 
			
		||||
                                                        await addGearExp(
 | 
			
		||||
                                                            "SpecialItems",
 | 
			
		||||
                                                            exaltedItem.ItemId.$oid,
 | 
			
		||||
                                                            exaltedCap - exaltedItem.XP
 | 
			
		||||
                                                            )
 | 
			
		||||
                                                        );
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                }
 | 
			
		||||
                                            }
 | 
			
		||||
                                        }
 | 
			
		||||
                                        Promise.all(promises).then(() => {
 | 
			
		||||
                                        updateInventory();
 | 
			
		||||
                                    });
 | 
			
		||||
                                    });
 | 
			
		||||
                                };
 | 
			
		||||
                                a.title = loc("code_maxRank");
 | 
			
		||||
                                a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.2L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z"/></svg>`;
 | 
			
		||||
@ -1007,6 +1002,67 @@ function updateInventory() {
 | 
			
		||||
                document.getElementById("EvolutionProgress-list").appendChild(tr);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            document.getElementById("Boosters-list").innerHTML = "";
 | 
			
		||||
            data.Boosters.forEach(item => {
 | 
			
		||||
                if (item.ExpiryDate < Math.floor(Date.now() / 1000)) {
 | 
			
		||||
                    // Booster has expired, skip it
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                const tr = document.createElement("tr");
 | 
			
		||||
                {
 | 
			
		||||
                    const td = document.createElement("td");
 | 
			
		||||
                    td.textContent = itemMap[item.ItemType]?.name ?? item.ItemType;
 | 
			
		||||
                    tr.appendChild(td);
 | 
			
		||||
                }
 | 
			
		||||
                {
 | 
			
		||||
                    const td = document.createElement("td");
 | 
			
		||||
                    td.classList = "text-end text-nowrap";
 | 
			
		||||
                    {
 | 
			
		||||
                        const form = document.createElement("form");
 | 
			
		||||
                        form.style.display = "inline-block";
 | 
			
		||||
                        form.onsubmit = function (event) {
 | 
			
		||||
                            event.preventDefault();
 | 
			
		||||
                            const maxDate = new Date(input.max);
 | 
			
		||||
                            const selectedDate = new Date(input.value);
 | 
			
		||||
                            if (selectedDate > maxDate) {
 | 
			
		||||
                                input.value = maxDate.toISOString().slice(0, 16);
 | 
			
		||||
                            }
 | 
			
		||||
                            doChangeBoosterExpiry(item.ItemType, input);
 | 
			
		||||
                        };
 | 
			
		||||
 | 
			
		||||
                        const input = document.createElement("input");
 | 
			
		||||
                        input.type = "datetime-local";
 | 
			
		||||
                        input.classList = "form-control form-control-sm";
 | 
			
		||||
                        input.value = formatDatetime("%Y-%m-%d %H:%M:%s", item.ExpiryDate * 1000);
 | 
			
		||||
                        input.max = "2038-01-19T03:14";
 | 
			
		||||
                        input.onblur = function () {
 | 
			
		||||
                            const maxDate = new Date(input.max);
 | 
			
		||||
                            const selectedDate = new Date(input.value);
 | 
			
		||||
                            if (selectedDate > maxDate) {
 | 
			
		||||
                                input.value = maxDate.toISOString().slice(0, 16);
 | 
			
		||||
                            }
 | 
			
		||||
                            doChangeBoosterExpiry(item.ItemType, input);
 | 
			
		||||
                        };
 | 
			
		||||
 | 
			
		||||
                        form.appendChild(input);
 | 
			
		||||
                        td.appendChild(form);
 | 
			
		||||
                    }
 | 
			
		||||
                    {
 | 
			
		||||
                        const a = document.createElement("a");
 | 
			
		||||
                        a.href = "#";
 | 
			
		||||
                        a.onclick = function (event) {
 | 
			
		||||
                            event.preventDefault();
 | 
			
		||||
                            setBooster(item.ItemType, 0);
 | 
			
		||||
                        };
 | 
			
		||||
                        a.title = loc("code_remove");
 | 
			
		||||
                        a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"/></svg>`;
 | 
			
		||||
                        td.appendChild(a);
 | 
			
		||||
                    }
 | 
			
		||||
                    tr.appendChild(td);
 | 
			
		||||
                }
 | 
			
		||||
                document.getElementById("Boosters-list").appendChild(tr);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            document.getElementById("FlavourItems-list").innerHTML = "";
 | 
			
		||||
            data.FlavourItems.forEach(item => {
 | 
			
		||||
                const datalist = document.getElementById("datalist-FlavourItems");
 | 
			
		||||
@ -1146,7 +1202,7 @@ function updateInventory() {
 | 
			
		||||
                        a.href = "#";
 | 
			
		||||
                        a.onclick = function (event) {
 | 
			
		||||
                            event.preventDefault();
 | 
			
		||||
                            doQuestUpdate("setInactive", item.ItemType);
 | 
			
		||||
                            debounce(doQuestUpdate, "setInactive", item.ItemType);
 | 
			
		||||
                        };
 | 
			
		||||
                        a.title = loc("code_setInactive");
 | 
			
		||||
                        a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M464 256A208 208 0 1 0 48 256a208 208 0 1 0 416 0zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256zm192-96l128 0c17.7 0 32 14.3 32 32l0 128c0 17.7-14.3 32-32 32l-128 0c-17.7 0-32-14.3-32-32l0-128c0-17.7 14.3-32 32-32z"/></svg>`;
 | 
			
		||||
@ -1157,7 +1213,7 @@ function updateInventory() {
 | 
			
		||||
                        a.href = "#";
 | 
			
		||||
                        a.onclick = function (event) {
 | 
			
		||||
                            event.preventDefault();
 | 
			
		||||
                            doQuestUpdate("resetKey", item.ItemType);
 | 
			
		||||
                            debounce(doQuestUpdate, "resetKey", item.ItemType);
 | 
			
		||||
                        };
 | 
			
		||||
                        a.title = loc("code_reset");
 | 
			
		||||
                        a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M463.5 224l8.5 0c13.3 0 24-10.7 24-24l0-128c0-9.7-5.8-18.5-14.8-22.2s-19.3-1.7-26.2 5.2L413.4 96.6c-87.6-86.5-228.7-86.2-315.8 1c-87.5 87.5-87.5 229.3 0 316.8s229.3 87.5 316.8 0c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0c-62.5 62.5-163.8 62.5-226.3 0s-62.5-163.8 0-226.3c62.2-62.2 162.7-62.5 225.3-1L327 183c-6.9 6.9-8.9 17.2-5.2 26.2s12.5 14.8 22.2 14.8l119.5 0z"/></svg>`;
 | 
			
		||||
@ -1168,7 +1224,7 @@ function updateInventory() {
 | 
			
		||||
                        a.href = "#";
 | 
			
		||||
                        a.onclick = function (event) {
 | 
			
		||||
                            event.preventDefault();
 | 
			
		||||
                            doQuestUpdate("completeKey", item.ItemType);
 | 
			
		||||
                            debounce(doQuestUpdate, "completeKey", item.ItemType);
 | 
			
		||||
                        };
 | 
			
		||||
                        a.title = loc("code_complete");
 | 
			
		||||
                        a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M438.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L160 338.7 393.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"/></svg>`;
 | 
			
		||||
@ -1179,7 +1235,7 @@ function updateInventory() {
 | 
			
		||||
                        a.href = "#";
 | 
			
		||||
                        a.onclick = function (event) {
 | 
			
		||||
                            event.preventDefault();
 | 
			
		||||
                            doQuestUpdate("prevStage", item.ItemType);
 | 
			
		||||
                            debounce(doQuestUpdate, "prevStage", item.ItemType);
 | 
			
		||||
                        };
 | 
			
		||||
                        a.title = loc("code_prevStage");
 | 
			
		||||
                        a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M41.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.3 256 246.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160z"/></svg>`;
 | 
			
		||||
@ -1194,7 +1250,7 @@ function updateInventory() {
 | 
			
		||||
                        a.href = "#";
 | 
			
		||||
                        a.onclick = function (event) {
 | 
			
		||||
                            event.preventDefault();
 | 
			
		||||
                            doQuestUpdate("nextStage", item.ItemType);
 | 
			
		||||
                            debounce(doQuestUpdate, "nextStage", item.ItemType);
 | 
			
		||||
                        };
 | 
			
		||||
                        a.title = loc("code_nextStage");
 | 
			
		||||
                        a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M278.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-160 160c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L210.7 256 73.4 118.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l160 160z"/></svg>`;
 | 
			
		||||
@ -1206,7 +1262,7 @@ function updateInventory() {
 | 
			
		||||
                        a.onclick = function (event) {
 | 
			
		||||
                            event.preventDefault();
 | 
			
		||||
                            reAddToItemList(itemMap, "QuestKeys", item.ItemType);
 | 
			
		||||
                            doQuestUpdate("deleteKey", item.ItemType);
 | 
			
		||||
                            debounce(doQuestUpdate, "deleteKey", item.ItemType);
 | 
			
		||||
                        };
 | 
			
		||||
                        a.title = loc("code_remove");
 | 
			
		||||
                        a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"/></svg>`;
 | 
			
		||||
@ -1452,12 +1508,13 @@ function updateInventory() {
 | 
			
		||||
                            document.getElementById("crystals-list").appendChild(tr);
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                        {
 | 
			
		||||
                            document.getElementById("edit-suit-invigorations-card").classList.remove("d-none");
 | 
			
		||||
                        const { OffensiveUpgrade, DefensiveUpgrade, UpgradesExpiry } =
 | 
			
		||||
                            suitInvigorationUpgradeData(item);
 | 
			
		||||
                        document.getElementById("dv-invigoration-offensive").value = OffensiveUpgrade;
 | 
			
		||||
                        document.getElementById("dv-invigoration-defensive").value = DefensiveUpgrade;
 | 
			
		||||
                        document.getElementById("dv-invigoration-expiry").value = UpgradesExpiry;
 | 
			
		||||
                            document.getElementById("invigoration-offensive").value = item.OffensiveUpgrade || "";
 | 
			
		||||
                            document.getElementById("invigoration-defensive").value = item.DefensiveUpgrade || "";
 | 
			
		||||
                            document.getElementById("invigoration-expiry").value =
 | 
			
		||||
                                formatDatetime("%Y-%m-%d %H:%M", Number(item.UpgradesExpiry?.$date.$numberLong)) || "";
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        {
 | 
			
		||||
                            document.getElementById("loadout-card").classList.remove("d-none");
 | 
			
		||||
@ -1619,63 +1676,6 @@ function updateInventory() {
 | 
			
		||||
            }
 | 
			
		||||
            document.getElementById("changeSyndicate").value = data.SupportedSyndicate ?? "";
 | 
			
		||||
 | 
			
		||||
            document.getElementById("Boosters-list").innerHTML = "";
 | 
			
		||||
            const now = Math.floor(Date.now() / 1000);
 | 
			
		||||
            data.Boosters.forEach(({ ItemType, ExpiryDate }) => {
 | 
			
		||||
                if (ExpiryDate < now) {
 | 
			
		||||
                    // Booster has expired, skip it
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                const tr = document.createElement("tr");
 | 
			
		||||
                {
 | 
			
		||||
                    const td = document.createElement("td");
 | 
			
		||||
                    td.textContent = itemMap[ItemType]?.name ?? ItemType;
 | 
			
		||||
                    tr.appendChild(td);
 | 
			
		||||
                }
 | 
			
		||||
                {
 | 
			
		||||
                    const td = document.createElement("td");
 | 
			
		||||
                    td.classList = "text-end text-nowrap";
 | 
			
		||||
                    const timeString = formatDatetime("%Y-%m-%d %H:%M:%s", ExpiryDate * 1000);
 | 
			
		||||
                    const inlineForm = document.createElement("form");
 | 
			
		||||
                    const input = document.createElement("input");
 | 
			
		||||
 | 
			
		||||
                    inlineForm.style.display = "inline-block";
 | 
			
		||||
                    inlineForm.onsubmit = function (event) {
 | 
			
		||||
                        event.preventDefault();
 | 
			
		||||
                        doChangeBoosterExpiry(ItemType, input);
 | 
			
		||||
                    };
 | 
			
		||||
                    input.type = "datetime-local";
 | 
			
		||||
                    input.classList.add("form-control");
 | 
			
		||||
                    input.classList.add("form-control-sm");
 | 
			
		||||
                    input.value = timeString;
 | 
			
		||||
                    let changed = false;
 | 
			
		||||
                    input.onchange = function () {
 | 
			
		||||
                        changed = true;
 | 
			
		||||
                    };
 | 
			
		||||
                    input.onblur = function () {
 | 
			
		||||
                        if (changed) {
 | 
			
		||||
                            doChangeBoosterExpiry(ItemType, input);
 | 
			
		||||
                        }
 | 
			
		||||
                    };
 | 
			
		||||
                    inlineForm.appendChild(input);
 | 
			
		||||
 | 
			
		||||
                    td.appendChild(inlineForm);
 | 
			
		||||
 | 
			
		||||
                    const removeButton = document.createElement("a");
 | 
			
		||||
                    removeButton.title = loc("code_remove");
 | 
			
		||||
                    removeButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"/></svg>`;
 | 
			
		||||
                    removeButton.href = "#";
 | 
			
		||||
                    removeButton.onclick = function (event) {
 | 
			
		||||
                        event.preventDefault();
 | 
			
		||||
                        setBooster(ItemType, 0);
 | 
			
		||||
                    };
 | 
			
		||||
                    td.appendChild(removeButton);
 | 
			
		||||
 | 
			
		||||
                    tr.appendChild(td);
 | 
			
		||||
                }
 | 
			
		||||
                document.getElementById("Boosters-list").appendChild(tr);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (single.getCurrentPath().startsWith("/webui/guildView")) {
 | 
			
		||||
                const guildReq = $.get("/custom/getGuild?guildId=" + window.guildId);
 | 
			
		||||
                guildReq.done(guildData => {
 | 
			
		||||
@ -1986,34 +1986,12 @@ function updateInventory() {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            for (const elm of accountCheats) {
 | 
			
		||||
                if (elm.type === "checkbox") {
 | 
			
		||||
                    elm.checked = !!data[elm.id];
 | 
			
		||||
                } else if (elm.type === "number") {
 | 
			
		||||
                    elm.value = data[elm.id] !== undefined ? data[elm.id] : elm.getAttribute("data-default") || "";
 | 
			
		||||
                }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addVaultDecoRecipe() {
 | 
			
		||||
    const uniqueName = getKey(document.getElementById("acquire-type-VaultDecoRecipes"));
 | 
			
		||||
    if (!guildId) {
 | 
			
		||||
        return;
 | 
			
		||||
            }
 | 
			
		||||
    if (!uniqueName) {
 | 
			
		||||
        $("acquire-type-VaultDecoRecipes").addClass("is-invalid").focus();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    revalidateAuthz().then(() => {
 | 
			
		||||
        const req = $.post({
 | 
			
		||||
            url: "/custom/addVaultDecoRecipe?" + window.authz + "&guildId=" + window.guildId,
 | 
			
		||||
            contentType: "application/json",
 | 
			
		||||
            data: JSON.stringify([
 | 
			
		||||
                {
 | 
			
		||||
                    ItemType: uniqueName,
 | 
			
		||||
                    ItemCount: 1
 | 
			
		||||
                }
 | 
			
		||||
            ])
 | 
			
		||||
        });
 | 
			
		||||
        req.done(() => {
 | 
			
		||||
            updateInventory();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
@ -3217,13 +3195,53 @@ function doIntrinsicsUnlockAll() {
 | 
			
		||||
document.querySelectorAll("#account-cheats input[type=checkbox]").forEach(elm => {
 | 
			
		||||
    elm.onchange = function () {
 | 
			
		||||
        revalidateAuthz().then(() => {
 | 
			
		||||
            const value = elm.checked;
 | 
			
		||||
            $.post({
 | 
			
		||||
                url: "/custom/setAccountCheat?" + window.authz,
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                data: JSON.stringify({
 | 
			
		||||
                    key: elm.id,
 | 
			
		||||
                    value: elm.checked
 | 
			
		||||
                    value: value
 | 
			
		||||
                })
 | 
			
		||||
            }).done(() => {
 | 
			
		||||
                elm.checked = value;
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
document.querySelectorAll("#account-cheats .input-group").forEach(grp => {
 | 
			
		||||
    const input = grp.querySelector("input");
 | 
			
		||||
    const select = grp.querySelector("select");
 | 
			
		||||
    const btn = grp.querySelector("button");
 | 
			
		||||
    if (input) {
 | 
			
		||||
        input.oninput = input.onchange = function () {
 | 
			
		||||
            btn.classList.remove("btn-secondary");
 | 
			
		||||
            btn.classList.add("btn-primary");
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
    if (select) {
 | 
			
		||||
        select.oninput = select.onchange = function () {
 | 
			
		||||
            btn.classList.remove("btn-secondary");
 | 
			
		||||
            btn.classList.add("btn-primary");
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
    btn.onclick = function () {
 | 
			
		||||
        btn.classList.remove("btn-primary");
 | 
			
		||||
        btn.classList.add("btn-secondary");
 | 
			
		||||
        const input = btn.closest(".input-group").querySelector('input[type="number"]');
 | 
			
		||||
        if (!input) return;
 | 
			
		||||
        revalidateAuthz().then(() => {
 | 
			
		||||
            const value = input.value;
 | 
			
		||||
            $.post({
 | 
			
		||||
                url: "/custom/setAccountCheat?" + window.authz,
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                data: JSON.stringify({
 | 
			
		||||
                    key: input.id,
 | 
			
		||||
                    value: parseInt(value)
 | 
			
		||||
                })
 | 
			
		||||
            }).done(() => {
 | 
			
		||||
                btn.value = value;
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
@ -3467,9 +3485,9 @@ function doBulkQuestUpdate(operation) {
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toast(text) {
 | 
			
		||||
function toast(text, type = "primary") {
 | 
			
		||||
    const toast = document.createElement("div");
 | 
			
		||||
    toast.className = "toast align-items-center text-bg-primary border-0";
 | 
			
		||||
    toast.className = `toast align-items-center text-bg-${type} border-0`;
 | 
			
		||||
    const div = document.createElement("div");
 | 
			
		||||
    div.className = "d-flex";
 | 
			
		||||
    const body = document.createElement("div");
 | 
			
		||||
@ -3542,7 +3560,7 @@ function handleModularSelection(category) {
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setBooster(ItemType, ExpiryDate, callback) {
 | 
			
		||||
function setBooster(ItemType, ExpiryDate) {
 | 
			
		||||
    revalidateAuthz().then(() => {
 | 
			
		||||
        $.post({
 | 
			
		||||
            url: "/custom/setBooster?" + window.authz,
 | 
			
		||||
@ -3555,33 +3573,27 @@ function setBooster(ItemType, ExpiryDate, callback) {
 | 
			
		||||
            ])
 | 
			
		||||
        }).done(function () {
 | 
			
		||||
            updateInventory();
 | 
			
		||||
            if (callback) callback();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function doAcquireBoosters() {
 | 
			
		||||
function doAcquireBooster() {
 | 
			
		||||
    const uniqueName = getKey(document.getElementById("acquire-type-Boosters"));
 | 
			
		||||
    if (!uniqueName) {
 | 
			
		||||
        $("#acquire-type-Boosters").addClass("is-invalid").focus();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    const ExpiryDate = Date.now() / 1000 + 3 * 24 * 60 * 60; // default 3 days
 | 
			
		||||
    setBooster(uniqueName, ExpiryDate, () => {
 | 
			
		||||
        $("#acquire-type-Boosters").val("");
 | 
			
		||||
    });
 | 
			
		||||
    setBooster(uniqueName, Math.floor(Date.now() / 1000 + 3 * 24 * 60 * 60));
 | 
			
		||||
    document.getElementById("acquire-type-Boosters").value = "";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function doChangeBoosterExpiry(ItemType, ExpiryDateInput) {
 | 
			
		||||
    console.log("Changing booster expiry for", ItemType, "to", ExpiryDateInput.value);
 | 
			
		||||
    // cast local datetime string to unix timestamp
 | 
			
		||||
    const ExpiryDate = Math.trunc(new Date(ExpiryDateInput.value).getTime() / 1000);
 | 
			
		||||
    const ExpiryDate = Math.floor(new Date(ExpiryDateInput.value).getTime() / 1000);
 | 
			
		||||
    if (isNaN(ExpiryDate)) {
 | 
			
		||||
        ExpiryDateInput.addClass("is-invalid").focus();
 | 
			
		||||
        return false;
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    setBooster(ItemType, ExpiryDate);
 | 
			
		||||
    return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function formatDatetime(fmt, date) {
 | 
			
		||||
@ -4027,73 +4039,31 @@ function handleModularPartsChange(event) {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
function suitInvigorationUpgradeData(suitData) {
 | 
			
		||||
    let expiryDate = "";
 | 
			
		||||
    if (suitData.UpgradesExpiry) {
 | 
			
		||||
        if (suitData.UpgradesExpiry.$date) {
 | 
			
		||||
            expiryDate = new Date(parseInt(suitData.UpgradesExpiry.$date.$numberLong));
 | 
			
		||||
        } else if (typeof suitData.UpgradesExpiry === "number") {
 | 
			
		||||
            expiryDate = new Date(suitData.UpgradesExpiry);
 | 
			
		||||
        } else if (suitData.UpgradesExpiry instanceof Date) {
 | 
			
		||||
            expiryDate = suitData.UpgradesExpiry;
 | 
			
		||||
        }
 | 
			
		||||
        if (expiryDate && !isNaN(expiryDate.getTime())) {
 | 
			
		||||
            const year = expiryDate.getFullYear();
 | 
			
		||||
            const month = String(expiryDate.getMonth() + 1).padStart(2, "0");
 | 
			
		||||
            const day = String(expiryDate.getDate()).padStart(2, "0");
 | 
			
		||||
            const hours = String(expiryDate.getHours()).padStart(2, "0");
 | 
			
		||||
            const minutes = String(expiryDate.getMinutes()).padStart(2, "0");
 | 
			
		||||
            expiryDate = `${year}-${month}-${day}T${hours}:${minutes}`;
 | 
			
		||||
        } else {
 | 
			
		||||
            expiryDate = "";
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
        oid: suitData.ItemId.$oid,
 | 
			
		||||
        OffensiveUpgrade: suitData.OffensiveUpgrade || "",
 | 
			
		||||
        DefensiveUpgrade: suitData.DefensiveUpgrade || "",
 | 
			
		||||
        UpgradesExpiry: expiryDate
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function submitSuitInvigorationUpgrade(event) {
 | 
			
		||||
function submitInvigoration(event) {
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
    const oid = new URLSearchParams(window.location.search).get("itemId");
 | 
			
		||||
    const offensiveUpgrade = document.getElementById("dv-invigoration-offensive").value;
 | 
			
		||||
    const defensiveUpgrade = document.getElementById("dv-invigoration-defensive").value;
 | 
			
		||||
    const expiry = document.getElementById("dv-invigoration-expiry").value;
 | 
			
		||||
    const OffensiveUpgrade = document.getElementById("invigoration-offensive").value;
 | 
			
		||||
    const DefensiveUpgrade = document.getElementById("invigoration-defensive").value;
 | 
			
		||||
    const expiry = document.getElementById("invigoration-expiry").value;
 | 
			
		||||
 | 
			
		||||
    if (!offensiveUpgrade || !defensiveUpgrade) {
 | 
			
		||||
        alert(loc("code_requiredInvigorationUpgrade"));
 | 
			
		||||
    if (!OffensiveUpgrade || !DefensiveUpgrade) {
 | 
			
		||||
        toast(loc("code_requiredInvigorationUpgrade"), "warning");
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const data = {
 | 
			
		||||
        OffensiveUpgrade: offensiveUpgrade,
 | 
			
		||||
        DefensiveUpgrade: defensiveUpgrade
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (expiry) {
 | 
			
		||||
        data.UpgradesExpiry = new Date(expiry).getTime();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    editSuitInvigorationUpgrade(oid, data);
 | 
			
		||||
    setInvigoration({
 | 
			
		||||
        OffensiveUpgrade,
 | 
			
		||||
        DefensiveUpgrade,
 | 
			
		||||
        UpgradesExpiry: expiry ? new Date(expiry).getTime() : Date.now() + 7 * 24 * 60 * 60 * 1000
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function clearSuitInvigorationUpgrades() {
 | 
			
		||||
    editSuitInvigorationUpgrade(new URLSearchParams(window.location.search).get("itemId"), null);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function editSuitInvigorationUpgrade(oid, data) {
 | 
			
		||||
    /* data?: {
 | 
			
		||||
            DefensiveUpgrade: string;
 | 
			
		||||
            OffensiveUpgrade: string;
 | 
			
		||||
            UpgradesExpiry?: number;
 | 
			
		||||
    }*/
 | 
			
		||||
function setInvigoration(data) {
 | 
			
		||||
    const oid = new URLSearchParams(window.location.search).get("itemId");
 | 
			
		||||
    $.post({
 | 
			
		||||
        url: "/custom/editSuitInvigorationUpgrade?" + window.authz,
 | 
			
		||||
        url: "/custom/setInvigoration?" + window.authz,
 | 
			
		||||
        contentType: "application/json",
 | 
			
		||||
        data: JSON.stringify({ oid, data })
 | 
			
		||||
        data: JSON.stringify({ oid, ...data })
 | 
			
		||||
    }).done(function () {
 | 
			
		||||
        updateInventory();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -76,7 +76,7 @@ dict = {
 | 
			
		||||
    code_replays: `[UNTRANSLATED] Replays`,
 | 
			
		||||
    code_stalker: `Stalker`,
 | 
			
		||||
    code_succChange: `Erfolgreich geändert.`,
 | 
			
		||||
    code_requiredInvigorationUpgrade: `Du musst sowohl ein offensives & defensives Upgrade auswählen.`,
 | 
			
		||||
    code_requiredInvigorationUpgrade: `[UNTRANSLATED] You must select both an offensive & utility upgrade.`,
 | 
			
		||||
    login_description: `Melde dich mit deinem OpenWF-Account an (denselben Angaben wie im Spiel, wenn du dich mit diesem Server verbindest).`,
 | 
			
		||||
    login_emailLabel: `E-Mail-Adresse`,
 | 
			
		||||
    login_passwordLabel: `Passwort`,
 | 
			
		||||
@ -109,7 +109,7 @@ dict = {
 | 
			
		||||
    inventory_moaPets: `Moas`,
 | 
			
		||||
    inventory_kubrowPets: `Bestien`,
 | 
			
		||||
    inventory_evolutionProgress: `Incarnon-Entwicklungsfortschritte`,
 | 
			
		||||
    inventory_Boosters: `Booster`,
 | 
			
		||||
    inventory_boosters: `Booster`,
 | 
			
		||||
    inventory_flavourItems: `<abbr title="Animationssets, Glyphen, Farbpaletten usw.">Sammlerstücke</abbr>`,
 | 
			
		||||
    inventory_shipDecorations: `Schiffsdekorationen`,
 | 
			
		||||
    inventory_bulkAddSuits: `Fehlende Warframes hinzufügen`,
 | 
			
		||||
@ -147,7 +147,7 @@ dict = {
 | 
			
		||||
    detailedView_valenceBonusLabel: `Valenz-Bonus`,
 | 
			
		||||
    detailedView_valenceBonusDescription: `Du kannst den Valenz-Bonus deiner Waffe festlegen oder entfernen.`,
 | 
			
		||||
    detailedView_modularPartsLabel: `Modulare Teile ändern`,
 | 
			
		||||
    detailedView_suitInvigorationLabel: `Warframe-Kräftigung`,
 | 
			
		||||
    detailedView_invigorationLabel: `Kräftigung`,
 | 
			
		||||
    detailedView_loadoutLabel: `Loadouts`,
 | 
			
		||||
 | 
			
		||||
    invigorations_offensive_AbilityStrength: `+200% Fähigkeitsstärke`,
 | 
			
		||||
@ -172,9 +172,9 @@ dict = {
 | 
			
		||||
    invigorations_utility_Jumps: `+5 Sprung-Zurücksetzungen`,
 | 
			
		||||
    invigorations_utility_EnergyRegen: `+2 Energieregeneration pro Sekunde`,
 | 
			
		||||
 | 
			
		||||
    invigorations_offensiveLabel: `Offensives Upgrade`,
 | 
			
		||||
    invigorations_defensiveLabel: `Defensives Upgrade`,
 | 
			
		||||
    invigorations_expiryLabel: `Upgrades Ablaufdatum (optional)`,
 | 
			
		||||
    detailedView_invigorationOffensiveLabel: `Offensives Upgrade`,
 | 
			
		||||
    detailedView_invigorationUtilityLabel: `[UNTRANSLATED] Utility Upgrade`,
 | 
			
		||||
    detailedView_invigorationExpiryLabel: `[UNTRANSLATED] Invigoration Expiry (optional)`,
 | 
			
		||||
 | 
			
		||||
    abilityOverride_label: `Fähigkeitsüberschreibung`,
 | 
			
		||||
    abilityOverride_onSlot: `auf Slot`,
 | 
			
		||||
@ -193,7 +193,7 @@ dict = {
 | 
			
		||||
    cheats_skipTutorial: `Tutorial überspringen`,
 | 
			
		||||
    cheats_skipAllDialogue: `Alle Dialoge überspringen`,
 | 
			
		||||
    cheats_unlockAllScans: `Alle Scans freischalten`,
 | 
			
		||||
    cheats_unlockSuccRelog: `[UNTRANSLATED] Success. Please that you'll need to relog for the client to refresh this.`,
 | 
			
		||||
    cheats_unlockSuccRelog: `[UNTRANSLATED] Success. Please note that you'll need to relog for the client to refresh this.`,
 | 
			
		||||
    cheats_unlockAllMissions: `Alle Missionen freischalten`,
 | 
			
		||||
    cheats_unlockAllMissions_ok: `Erfolgreich. Bitte beachte, dass du ein Dojo/Relais besuchen oder dich neu einloggen musst, damit die Sternenkarte aktualisiert wird.`,
 | 
			
		||||
    cheats_infiniteCredits: `Unendlich Credits`,
 | 
			
		||||
@ -227,7 +227,7 @@ dict = {
 | 
			
		||||
    cheats_baroFullyStocked: `Baro hat volles Inventar`,
 | 
			
		||||
    cheats_syndicateMissionsRepeatable: `Syndikat-Missionen wiederholbar`,
 | 
			
		||||
    cheats_unlockAllProfitTakerStages: `Alle Profiteintreiber-Phasen freischalten`,
 | 
			
		||||
    cheats_unlockSuccInventory: `[UNTRANSLATED] Success. Please note that you'll need to resync your inventory, e.g. using the bootstrapper's /sync command, visiting a dojo/relay, or relogging..`,
 | 
			
		||||
    cheats_unlockSuccInventory: `[UNTRANSLATED] Success. Please note that you'll need to resync your inventory, e.g. using the bootstrapper's /sync command in game chat, visiting a dojo/relay, or relogging.`,
 | 
			
		||||
    cheats_instantFinishRivenChallenge: `Riven-Mod Herausforderung sofort abschließen`,
 | 
			
		||||
    cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`,
 | 
			
		||||
    cheats_noResourceExtractorDronesDamage: `Kein Schaden für Ressourcen-Extraktor-Drohnen`,
 | 
			
		||||
@ -257,6 +257,17 @@ dict = {
 | 
			
		||||
    cheats_changeButton: `Ändern`,
 | 
			
		||||
    cheats_markAllAsRead: `Posteingang als gelesen markieren`,
 | 
			
		||||
    cheats_finishInvasionsInOneMission: `[UNTRANSLATED] Finish Invasions in One Mission`,
 | 
			
		||||
    cheats_gainNoNegativeSyndicateStanding: `[UNTRANSLATED] Gain No Negative Syndicate Standing`,
 | 
			
		||||
    cheats_nemesisHenchmenKillsMultiplierGrineer: `[UNTRANSLATED] Rage Progess Multiplier (Grineer)`,
 | 
			
		||||
    cheats_nemesisHenchmenKillsMultiplierCorpus: `[UNTRANSLATED] Rage Progess Multiplier (Corpus)`,
 | 
			
		||||
    cheats_nemesisAntivirusGainMultiplier: `[UNTRANSLATED] Antivirus Progress Multiplier`,
 | 
			
		||||
    cheats_nemesisHintProgressMultiplierGrineer: `[UNTRANSLATED] Hint Progress Multiplier (Grineer)`,
 | 
			
		||||
    cheats_nemesisHintProgressMultiplierCorpus: `[UNTRANSLATED] Hint Progress Multiplier (Corpus)`,
 | 
			
		||||
    cheats_nemesisExtraWeapon: `[UNTRANSLATED] Extra Nemesis Weapon / Token On Vanquish (0 to disable)`,
 | 
			
		||||
    cheats_playerSkillGainsMultiplierSpace: `[UNTRANSLATED] Intrinsics Gains Multiplier (Space)`,
 | 
			
		||||
    cheats_playerSkillGainsMultiplierDrifter: `[UNTRANSLATED] Intrinsics Gains Multiplier (Drifter)`,
 | 
			
		||||
    cheats_extraMissionRewards: `[UNTRANSLATED] Extra Mission Rewards (0 to disable)`,
 | 
			
		||||
    cheats_strippedItemRewardsMultiplier: `[UNTRANSLATED] Stripped Item Rewards Multiplier`,
 | 
			
		||||
 | 
			
		||||
    worldState: `Weltstatus`,
 | 
			
		||||
    worldState_creditBoost: `Event Booster: Credit`,
 | 
			
		||||
@ -278,6 +289,7 @@ dict = {
 | 
			
		||||
    worldState_hallowedFlame: `Geweihte Flamme`,
 | 
			
		||||
    worldState_hallowedNightmares: `Geweihte Albträume`,
 | 
			
		||||
    worldState_hallowedNightmaresRewards: `[UNTRANSLATED] Hallowed Nightmares Rewards`,
 | 
			
		||||
    worldState_naberusNights: `[UNTRANSLATED] Nights of Naberus`,
 | 
			
		||||
    worldState_proxyRebellion: `Proxy-Rebellion`,
 | 
			
		||||
    worldState_proxyRebellionRewards: `[UNTRANSLATED] Proxy Rebellion Rewards`,
 | 
			
		||||
    worldState_bellyOfTheBeast: `Das Innere der Bestie`,
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `Note: To see changes in-game, you need to resync your inventory, e.g. using the bootstrapper's /sync command, visiting a dojo/relay, or relogging.`,
 | 
			
		||||
    general_inventoryUpdateNote: `Note: To see changes in-game, you need to resync your inventory, e.g. using the bootstrapper's /sync command in game chat, visiting a dojo/relay, or relogging.`,
 | 
			
		||||
    general_inventoryUpdateNoteGameWs: `Note: You may need to reopen any menu you are on for changes to be reflected.`,
 | 
			
		||||
    general_addButton: `Add`,
 | 
			
		||||
    general_setButton: `Set`,
 | 
			
		||||
@ -75,7 +75,7 @@ dict = {
 | 
			
		||||
    code_replays: `Replays`,
 | 
			
		||||
    code_stalker: `Stalker`,
 | 
			
		||||
    code_succChange: `Successfully changed.`,
 | 
			
		||||
    code_requiredInvigorationUpgrade: `You must select both an offensive & defensive upgrade.`,
 | 
			
		||||
    code_requiredInvigorationUpgrade: `You must select both an offensive & utility upgrade.`,
 | 
			
		||||
    login_description: `Login using your OpenWF account credentials (same as in-game when connecting to this server).`,
 | 
			
		||||
    login_emailLabel: `Email address`,
 | 
			
		||||
    login_passwordLabel: `Password`,
 | 
			
		||||
@ -108,7 +108,7 @@ dict = {
 | 
			
		||||
    inventory_moaPets: `Moas`,
 | 
			
		||||
    inventory_kubrowPets: `Beasts`,
 | 
			
		||||
    inventory_evolutionProgress: `Incarnon Evolution Progress`,
 | 
			
		||||
    inventory_Boosters: `Boosters`,
 | 
			
		||||
    inventory_boosters: `Boosters`,
 | 
			
		||||
    inventory_flavourItems: `<abbr title="Animation Sets, Glyphs, Palettes, etc.">Flavour Items</abbr>`,
 | 
			
		||||
    inventory_shipDecorations: `Ship Decorations`,
 | 
			
		||||
    inventory_bulkAddSuits: `Add Missing Warframes`,
 | 
			
		||||
@ -146,7 +146,7 @@ dict = {
 | 
			
		||||
    detailedView_valenceBonusLabel: `Valence Bonus`,
 | 
			
		||||
    detailedView_valenceBonusDescription: `You can set or remove the Valence Bonus from your weapon.`,
 | 
			
		||||
    detailedView_modularPartsLabel: `Change Modular Parts`,
 | 
			
		||||
    detailedView_suitInvigorationLabel: `Warframe Invigoration`,
 | 
			
		||||
    detailedView_invigorationLabel: `Invigoration`,
 | 
			
		||||
    detailedView_loadoutLabel: `Loadouts`,
 | 
			
		||||
 | 
			
		||||
    invigorations_offensive_AbilityStrength: `+200% Ability Strength`,
 | 
			
		||||
@ -171,9 +171,9 @@ dict = {
 | 
			
		||||
    invigorations_utility_Jumps: `+5 Jump Resets`,
 | 
			
		||||
    invigorations_utility_EnergyRegen: `+2 Energy Regen/s`,
 | 
			
		||||
 | 
			
		||||
    invigorations_offensiveLabel: `Offensive Upgrade`,
 | 
			
		||||
    invigorations_defensiveLabel: `Defensive Upgrade`,
 | 
			
		||||
    invigorations_expiryLabel: `Upgrades Expiry (optional)`,
 | 
			
		||||
    detailedView_invigorationOffensiveLabel: `Offensive Upgrade`,
 | 
			
		||||
    detailedView_invigorationUtilityLabel: `Utility Upgrade`,
 | 
			
		||||
    detailedView_invigorationExpiryLabel: `Invigoration Expiry (optional)`,
 | 
			
		||||
 | 
			
		||||
    abilityOverride_label: `Ability Override`,
 | 
			
		||||
    abilityOverride_onSlot: `on slot`,
 | 
			
		||||
@ -192,7 +192,7 @@ dict = {
 | 
			
		||||
    cheats_skipTutorial: `Skip Tutorial`,
 | 
			
		||||
    cheats_skipAllDialogue: `Skip All Dialogue`,
 | 
			
		||||
    cheats_unlockAllScans: `Unlock All Scans`,
 | 
			
		||||
    cheats_unlockSuccRelog: `Success. Please that you'll need to relog for the client to refresh this.`,
 | 
			
		||||
    cheats_unlockSuccRelog: `Success. Please note that you'll need to relog for the client to refresh this.`,
 | 
			
		||||
    cheats_unlockAllMissions: `Unlock All Missions`,
 | 
			
		||||
    cheats_unlockAllMissions_ok: `Success. Please note that you'll need to enter a dojo/relay or relog for the client to refresh the star chart.`,
 | 
			
		||||
    cheats_infiniteCredits: `Infinite Credits`,
 | 
			
		||||
@ -226,7 +226,7 @@ dict = {
 | 
			
		||||
    cheats_baroFullyStocked: `Baro Fully Stocked`,
 | 
			
		||||
    cheats_syndicateMissionsRepeatable: `Syndicate Missions Repeatable`,
 | 
			
		||||
    cheats_unlockAllProfitTakerStages: `Unlock All Profit Taker Stages`,
 | 
			
		||||
    cheats_unlockSuccInventory: `Success. Please note that you'll need to resync your inventory, e.g. using the bootstrapper's /sync command, visiting a dojo/relay, or relogging..`,
 | 
			
		||||
    cheats_unlockSuccInventory: `Success. Please note that you'll need to resync your inventory, e.g. using the bootstrapper's /sync command in game chat, visiting a dojo/relay, or relogging.`,
 | 
			
		||||
    cheats_instantFinishRivenChallenge: `Instant Finish Riven Challenge`,
 | 
			
		||||
    cheats_instantResourceExtractorDrones: `Instant Resource Extractor Drones`,
 | 
			
		||||
    cheats_noResourceExtractorDronesDamage: `No Resource Extractor Drones Damage`,
 | 
			
		||||
@ -256,6 +256,22 @@ dict = {
 | 
			
		||||
    cheats_changeButton: `Change`,
 | 
			
		||||
    cheats_markAllAsRead: `Mark Inbox As Read`,
 | 
			
		||||
    cheats_finishInvasionsInOneMission: `Finish Invasions in One Mission`,
 | 
			
		||||
    cheats_gainNoNegativeSyndicateStanding: `Gain No Negative Syndicate Standing`,
 | 
			
		||||
    cheats_nemesisHenchmenKillsMultiplierGrineer: `Rage Progess Multiplier (Grineer)`,
 | 
			
		||||
    cheats_nemesisHenchmenKillsMultiplierCorpus: `Rage Progess Multiplier (Corpus)`,
 | 
			
		||||
    cheats_nemesisAntivirusGainMultiplier: `Antivirus Progress Multiplier`,
 | 
			
		||||
    cheats_nemesisHintProgressMultiplierGrineer: `Hint Progress Multiplier (Grineer)`,
 | 
			
		||||
    cheats_nemesisHintProgressMultiplierCorpus: `Hint Progress Multiplier (Corpus)`,
 | 
			
		||||
    cheats_nemesisExtraWeapon: `Extra Nemesis Weapon / Token On Vanquish (0 to disable)`,
 | 
			
		||||
    cheats_playerSkillGainsMultiplierSpace: `Intrinsics Gains Multiplier (Space)`,
 | 
			
		||||
    cheats_playerSkillGainsMultiplierDrifter: `Intrinsics Gains Multiplier (Drifter)`,
 | 
			
		||||
    cheats_extraMissionRewards: `Extra Mission Rewards (0 to disable)`,
 | 
			
		||||
    cheats_strippedItemRewardsMultiplier: `Stripped Item Rewards Multiplier`,
 | 
			
		||||
    cheats_extraRelicRewards: `Extra Relic Rewards`,
 | 
			
		||||
    cheats_crackRelicForPlatinum: `Crack Relic for Platinum`,
 | 
			
		||||
    cheats_relicPlatinumCommon: `Platinum on Common Rewards`,
 | 
			
		||||
    cheats_relicPlatinumUncommon: `Platinum on Uncommon Rewards`,
 | 
			
		||||
    cheats_relicPlatinumRare: `Platinum on Rare Rewards`,
 | 
			
		||||
 | 
			
		||||
    worldState: `World State`,
 | 
			
		||||
    worldState_creditBoost: `Credit Boost`,
 | 
			
		||||
@ -277,6 +293,7 @@ dict = {
 | 
			
		||||
    worldState_hallowedFlame: `Hallowed Flame`,
 | 
			
		||||
    worldState_hallowedNightmares: `Hallowed Nightmares`,
 | 
			
		||||
    worldState_hallowedNightmaresRewards: `Hallowed Nightmares Rewards`,
 | 
			
		||||
    worldState_naberusNights: `Nights of Naberus`,
 | 
			
		||||
    worldState_proxyRebellion: `Proxy Rebellion`,
 | 
			
		||||
    worldState_proxyRebellionRewards: `Proxy Rebellion Rewards`,
 | 
			
		||||
    worldState_bellyOfTheBeast: `Belly of the Beast`,
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
// Spanish translation by hxedcl
 | 
			
		||||
// Spanish translation by hxedcl, Slayer55555
 | 
			
		||||
dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `Para ver los cambios en el juego, necesitas volver a sincronizar tu inventario, por ejemplo, usando el comando /sync del bootstrapper, visitando un dojo o repetidor, o volviendo a iniciar sesión.`,
 | 
			
		||||
    general_inventoryUpdateNoteGameWs: `[UNTRANSLATED] Note: You may need to reopen any menu you are on for changes to be reflected.`,
 | 
			
		||||
    general_inventoryUpdateNoteGameWs: `Nota: Puede que necesites reabrir cualquier menú en el que te encuentres para que los cambios se reflejen.`,
 | 
			
		||||
    general_addButton: `Agregar`,
 | 
			
		||||
    general_setButton: `Establecer`,
 | 
			
		||||
    general_none: `Ninguno`,
 | 
			
		||||
@ -32,8 +32,8 @@ dict = {
 | 
			
		||||
    code_renamePrompt: `Escribe tu nuevo nombre personalizado:`,
 | 
			
		||||
    code_remove: `Quitar`,
 | 
			
		||||
    code_addItemsConfirm: `¿Estás seguro de que deseas agregar |COUNT| objetos a tu cuenta?`,
 | 
			
		||||
    code_addTechProjectsConfirm: `[UNTRANSLATED] Are you sure you want to add |COUNT| research to your clan?`,
 | 
			
		||||
    code_addDecoRecipesConfirm: `[UNTRANSLATED] Are you sure you want to add |COUNT| deco recipes to your clan?`,
 | 
			
		||||
    code_addTechProjectsConfirm: `¿Estás seguro de que quieres añadir |COUNT| proyectos de investigación a tu clan?`,
 | 
			
		||||
    code_addDecoRecipesConfirm: `¿Estás seguro de que quieres añadir |COUNT| planos de decoración a tu clan?`,
 | 
			
		||||
    code_succRankUp: `Ascenso exitoso.`,
 | 
			
		||||
    code_noEquipmentToRankUp: `No hay equipo para ascender.`,
 | 
			
		||||
    code_succAdded: `Agregado exitosamente.`,
 | 
			
		||||
@ -45,7 +45,7 @@ dict = {
 | 
			
		||||
    code_rank: `Rango`,
 | 
			
		||||
    code_rankUp: `Subir de rango`,
 | 
			
		||||
    code_rankDown: `Bajar de rango`,
 | 
			
		||||
    code_unlockLevelCap: `[UNTRANSLATED] Unlock level cap`,
 | 
			
		||||
    code_unlockLevelCap: `Desbloquear level cap`,
 | 
			
		||||
    code_count: `Cantidad`,
 | 
			
		||||
    code_focusAllUnlocked: `Todas las escuelas de enfoque ya están desbloqueadas.`,
 | 
			
		||||
    code_focusUnlocked: `¡Desbloqueadas |COUNT| nuevas escuelas de enfoque! Se necesita una actualización del inventario para reflejar los cambios en el juego. Visitar la navegación debería ser la forma más sencilla de activarlo.`,
 | 
			
		||||
@ -65,18 +65,18 @@ dict = {
 | 
			
		||||
    code_completed: `Completada`,
 | 
			
		||||
    code_active: `Activa`,
 | 
			
		||||
    code_pigment: `Pigmento`,
 | 
			
		||||
    code_controller: `[UNTRANSLATED] Controller cursor`,
 | 
			
		||||
    code_mouseLine: `[UNTRANSLATED] Line cursor`,
 | 
			
		||||
    code_mouse: `[UNTRANSLATED] Cursor`,
 | 
			
		||||
    code_controller: `Cursor de Mando`,
 | 
			
		||||
    code_mouseLine: `Cursor de línea`,
 | 
			
		||||
    code_mouse: `Cursor`,
 | 
			
		||||
    code_itemColorPalette: `Paleta de colores |ITEM|`,
 | 
			
		||||
    code_mature: `Listo para el combate`,
 | 
			
		||||
    code_unmature: `Regresar el envejecimiento genético`,
 | 
			
		||||
    code_fund: `[UNTRANSLATED] Fund`,
 | 
			
		||||
    code_funded: `[UNTRANSLATED] Funded`,
 | 
			
		||||
    code_replays: `[UNTRANSLATED] Replays`,
 | 
			
		||||
    code_fund: `Financiar`,
 | 
			
		||||
    code_funded: `Financiado`,
 | 
			
		||||
    code_replays: `Repeticiones`,
 | 
			
		||||
    code_stalker: `Stalker`,
 | 
			
		||||
    code_succChange: `Cambiado correctamente`,
 | 
			
		||||
    code_requiredInvigorationUpgrade: `Debes seleccionar una mejora ofensiva y una defensiva.`,
 | 
			
		||||
    code_requiredInvigorationUpgrade: `Debes seleccionar una mejora ofensiva y una mejora de utilidad.`,
 | 
			
		||||
    login_description: `Inicia sesión con las credenciales de tu cuenta OpenWF (las mismas que usas en el juego al conectarte a este servidor).`,
 | 
			
		||||
    login_emailLabel: `Dirección de correo electrónico`,
 | 
			
		||||
    login_passwordLabel: `Contraseña`,
 | 
			
		||||
@ -109,7 +109,7 @@ dict = {
 | 
			
		||||
    inventory_moaPets: `Moas`,
 | 
			
		||||
    inventory_kubrowPets: `Bestias`,
 | 
			
		||||
    inventory_evolutionProgress: `Progreso de evolución Incarnon`,
 | 
			
		||||
    inventory_Boosters: `Potenciadores`,
 | 
			
		||||
    inventory_boosters: `Potenciadores`,
 | 
			
		||||
    inventory_flavourItems: `<abbr title="Conjuntos de animaciones, glifos, paletas, etc.">Ítems estéticos</abbr>`,
 | 
			
		||||
    inventory_shipDecorations: `Decoraciones de nave`,
 | 
			
		||||
    inventory_bulkAddSuits: `Agregar Warframes faltantes`,
 | 
			
		||||
@ -118,8 +118,8 @@ dict = {
 | 
			
		||||
    inventory_bulkAddSpaceWeapons: `Agregar armas Archwing faltantes`,
 | 
			
		||||
    inventory_bulkAddSentinels: `Agregar centinelas faltantes`,
 | 
			
		||||
    inventory_bulkAddSentinelWeapons: `Agregar armas de centinela faltantes`,
 | 
			
		||||
    inventory_bulkAddFlavourItems: `[UNTRANSLATED] Add Missing Flavour Items`,
 | 
			
		||||
    inventory_bulkAddShipDecorations: `[UNTRANSLATED] Add Missing Ship Decorations`,
 | 
			
		||||
    inventory_bulkAddFlavourItems: `Añadir items estéticos faltantes`,
 | 
			
		||||
    inventory_bulkAddShipDecorations: `Añadir decoraciones de Nave Faltantes`,
 | 
			
		||||
    inventory_bulkAddEvolutionProgress: `Completar el progreso de evolución Incarnon faltante`,
 | 
			
		||||
    inventory_bulkRankUpSuits: `Maximizar rango de todos los Warframes`,
 | 
			
		||||
    inventory_bulkRankUpWeapons: `Maximizar rango de todas las armas`,
 | 
			
		||||
@ -147,7 +147,7 @@ dict = {
 | 
			
		||||
    detailedView_valenceBonusLabel: `Bonus de Valéncia`,
 | 
			
		||||
    detailedView_valenceBonusDescription: `Puedes establecer o quitar el bonus de valencia de tu arma.`,
 | 
			
		||||
    detailedView_modularPartsLabel: `Cambiar partes modulares`,
 | 
			
		||||
    detailedView_suitInvigorationLabel: `Vigorización de Warframe`,
 | 
			
		||||
    detailedView_invigorationLabel: `Fortalecimiento`,
 | 
			
		||||
    detailedView_loadoutLabel: `Equipamientos`,
 | 
			
		||||
 | 
			
		||||
    invigorations_offensive_AbilityStrength: `+200% Fuerza de Habilidad`,
 | 
			
		||||
@ -172,9 +172,9 @@ dict = {
 | 
			
		||||
    invigorations_utility_Jumps: `+5 Restablecimientos de Salto`,
 | 
			
		||||
    invigorations_utility_EnergyRegen: `+2 Regeneración de Energía/s`,
 | 
			
		||||
 | 
			
		||||
    invigorations_offensiveLabel: `Mejora Ofensiva`,
 | 
			
		||||
    invigorations_defensiveLabel: `Mejora Defensiva`,
 | 
			
		||||
    invigorations_expiryLabel: `Caducidad de Mejoras (opcional)`,
 | 
			
		||||
    detailedView_invigorationOffensiveLabel: `Mejora Ofensiva`,
 | 
			
		||||
    detailedView_invigorationUtilityLabel: `Mejora de Utilidad`,
 | 
			
		||||
    detailedView_invigorationExpiryLabel: `Caducidad del Fortalecimiento (opcional)`,
 | 
			
		||||
 | 
			
		||||
    abilityOverride_label: `Intercambio de Habilidad`,
 | 
			
		||||
    abilityOverride_onSlot: `en el espacio`,
 | 
			
		||||
@ -257,6 +257,17 @@ dict = {
 | 
			
		||||
    cheats_changeButton: `Cambiar`,
 | 
			
		||||
    cheats_markAllAsRead: `Marcar bandeja de entrada como leída`,
 | 
			
		||||
    cheats_finishInvasionsInOneMission: `Finaliza Invasión en una mision`,
 | 
			
		||||
    cheats_gainNoNegativeSyndicateStanding: `[UNTRANSLATED] Gain No Negative Syndicate Standing`,
 | 
			
		||||
    cheats_nemesisHenchmenKillsMultiplierGrineer: `[UNTRANSLATED] Rage Progess Multiplier (Grineer)`,
 | 
			
		||||
    cheats_nemesisHenchmenKillsMultiplierCorpus: `[UNTRANSLATED] Rage Progess Multiplier (Corpus)`,
 | 
			
		||||
    cheats_nemesisAntivirusGainMultiplier: `[UNTRANSLATED] Antivirus Progress Multiplier`,
 | 
			
		||||
    cheats_nemesisHintProgressMultiplierGrineer: `[UNTRANSLATED] Hint Progress Multiplier (Grineer)`,
 | 
			
		||||
    cheats_nemesisHintProgressMultiplierCorpus: `[UNTRANSLATED] Hint Progress Multiplier (Corpus)`,
 | 
			
		||||
    cheats_nemesisExtraWeapon: `[UNTRANSLATED] Extra Nemesis Weapon / Token On Vanquish (0 to disable)`,
 | 
			
		||||
    cheats_playerSkillGainsMultiplierSpace: `[UNTRANSLATED] Intrinsics Gains Multiplier (Space)`,
 | 
			
		||||
    cheats_playerSkillGainsMultiplierDrifter: `[UNTRANSLATED] Intrinsics Gains Multiplier (Drifter)`,
 | 
			
		||||
    cheats_extraMissionRewards: `[UNTRANSLATED] Extra Mission Rewards (0 to disable)`,
 | 
			
		||||
    cheats_strippedItemRewardsMultiplier: `[UNTRANSLATED] Stripped Item Rewards Multiplier`,
 | 
			
		||||
 | 
			
		||||
    worldState: `Estado del mundo`,
 | 
			
		||||
    worldState_creditBoost: `Potenciador de Créditos`,
 | 
			
		||||
@ -278,6 +289,7 @@ dict = {
 | 
			
		||||
    worldState_hallowedFlame: `Llama Sagrada`,
 | 
			
		||||
    worldState_hallowedNightmares: `Pesadillas Sagradas`,
 | 
			
		||||
    worldState_hallowedNightmaresRewards: `Recompensas de Pesadillas Sagradas`,
 | 
			
		||||
    worldState_naberusNights: `Noches de Naberus`,
 | 
			
		||||
    worldState_proxyRebellion: `Rebelión Proxy`,
 | 
			
		||||
    worldState_proxyRebellionRewards: `Recompensas de Rebelión Proxy`,
 | 
			
		||||
    worldState_bellyOfTheBeast: `Vientre de la Bestia`,
 | 
			
		||||
@ -401,9 +413,9 @@ dict = {
 | 
			
		||||
    theme_dark: `Tema Oscuro`,
 | 
			
		||||
    theme_light: `Tema Claro`,
 | 
			
		||||
 | 
			
		||||
    guildView_cheats: `[UNTRANSLATED] Clan Cheats`,
 | 
			
		||||
    guildView_cheats: `Trucos de Clan`,
 | 
			
		||||
    guildView_techProjects: `Investigación`,
 | 
			
		||||
    guildView_vaultDecoRecipes: `[UNTRANSLATED] Dojo Deco Recipes`,
 | 
			
		||||
    guildView_vaultDecoRecipes: `Planos de Decoración de Dojo`,
 | 
			
		||||
    guildView_alliance: `Alianza`,
 | 
			
		||||
    guildView_members: `Miembros`,
 | 
			
		||||
    guildView_pending: `Pendiente`,
 | 
			
		||||
@ -423,11 +435,11 @@ dict = {
 | 
			
		||||
    guildView_rank_soldier: `Soldado`,
 | 
			
		||||
    guildView_rank_utility: `Utilitario`,
 | 
			
		||||
    guildView_rank_warlord: `Señor de la guerra`,
 | 
			
		||||
    guildView_currency_owned: `[UNTRANSLATED] |COUNT| in Vault.`,
 | 
			
		||||
    guildView_bulkAddTechProjects: `[UNTRANSLATED] Add Missing Research`,
 | 
			
		||||
    guildView_bulkAddVaultDecoRecipes: `[UNTRANSLATED] Add Missing Dojo Deco Recipes`,
 | 
			
		||||
    guildView_bulkFundTechProjects: `[UNTRANSLATED] Fund All Research`,
 | 
			
		||||
    guildView_bulkCompleteTechProjects: `[UNTRANSLATED] Complete All Research`,
 | 
			
		||||
    guildView_currency_owned: `|COUNT| en la Bóveda.`,
 | 
			
		||||
    guildView_bulkAddTechProjects: `Añadir proyectos de Investigación Faltantes`,
 | 
			
		||||
    guildView_bulkAddVaultDecoRecipes: `Añadir planos de Decoración de Dojo Faltantes`,
 | 
			
		||||
    guildView_bulkFundTechProjects: `Financiar toda la Investigación`,
 | 
			
		||||
    guildView_bulkCompleteTechProjects: `Completar toda la Investigación`,
 | 
			
		||||
    guildView_promote: `Promover`,
 | 
			
		||||
    guildView_demote: `Degradar`,
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -76,7 +76,7 @@ dict = {
 | 
			
		||||
    code_replays: `[UNTRANSLATED] Replays`,
 | 
			
		||||
    code_stalker: `Stalker`,
 | 
			
		||||
    code_succChange: `Changement effectué.`,
 | 
			
		||||
    code_requiredInvigorationUpgrade: `Augmentation offensive et défensive requises.`,
 | 
			
		||||
    code_requiredInvigorationUpgrade: `[UNTRANSLATED] You must select both an offensive & utility upgrade.`,
 | 
			
		||||
    login_description: `Connexion avec les informations de connexion OpenWF.`,
 | 
			
		||||
    login_emailLabel: `Email`,
 | 
			
		||||
    login_passwordLabel: `Mot de passe`,
 | 
			
		||||
@ -109,7 +109,7 @@ dict = {
 | 
			
		||||
    inventory_moaPets: `Moas`,
 | 
			
		||||
    inventory_kubrowPets: `Bêtes`,
 | 
			
		||||
    inventory_evolutionProgress: `Progrès de l'évolution Incarnon`,
 | 
			
		||||
    inventory_Boosters: `Boosters`,
 | 
			
		||||
    inventory_boosters: `Boosters`,
 | 
			
		||||
    inventory_flavourItems: `[UNTRANSLATED] <abbr title="Animation Sets, Glyphs, Palettes, etc.">Flavour Items</abbr>`,
 | 
			
		||||
    inventory_shipDecorations: `Décorations du vaisseau`,
 | 
			
		||||
    inventory_bulkAddSuits: `Ajouter les Warframes manquantes`,
 | 
			
		||||
@ -147,7 +147,7 @@ dict = {
 | 
			
		||||
    detailedView_valenceBonusLabel: `Bonus de Valence`,
 | 
			
		||||
    detailedView_valenceBonusDescription: `Définir le Bonus Valence de l'arme.`,
 | 
			
		||||
    detailedView_modularPartsLabel: `Changer l'équipement modulaire`,
 | 
			
		||||
    detailedView_suitInvigorationLabel: `Invigoration de Warframe`,
 | 
			
		||||
    detailedView_invigorationLabel: `Dynamisation`,
 | 
			
		||||
    detailedView_loadoutLabel: `Équipements`,
 | 
			
		||||
 | 
			
		||||
    invigorations_offensive_AbilityStrength: `+200% de puissance de pouvoir`,
 | 
			
		||||
@ -172,9 +172,9 @@ dict = {
 | 
			
		||||
    invigorations_utility_Jumps: `+5 réinitialisations de saut`,
 | 
			
		||||
    invigorations_utility_EnergyRegen: `+2 d'énergie régénérés/s`,
 | 
			
		||||
 | 
			
		||||
    invigorations_offensiveLabel: `Amélioration offensive`,
 | 
			
		||||
    invigorations_defensiveLabel: `Amélioration défensive`,
 | 
			
		||||
    invigorations_expiryLabel: `Expiration de l'invigoration (optionnel)`,
 | 
			
		||||
    detailedView_invigorationOffensiveLabel: `Amélioration offensive`,
 | 
			
		||||
    detailedView_invigorationUtilityLabel: `[UNTRANSLATED] Utility Upgrade`,
 | 
			
		||||
    detailedView_invigorationExpiryLabel: `[UNTRANSLATED] Invigoration Expiry (optional)`,
 | 
			
		||||
 | 
			
		||||
    abilityOverride_label: `Remplacement de pouvoir`,
 | 
			
		||||
    abilityOverride_onSlot: `Sur l'emplacement`,
 | 
			
		||||
@ -257,6 +257,17 @@ dict = {
 | 
			
		||||
    cheats_changeButton: `Changer`,
 | 
			
		||||
    cheats_markAllAsRead: `Marquer la boîte de réception comme lue`,
 | 
			
		||||
    cheats_finishInvasionsInOneMission: `Compléter les invasions en une mission.`,
 | 
			
		||||
    cheats_gainNoNegativeSyndicateStanding: `[UNTRANSLATED] Gain No Negative Syndicate Standing`,
 | 
			
		||||
    cheats_nemesisHenchmenKillsMultiplierGrineer: `[UNTRANSLATED] Rage Progess Multiplier (Grineer)`,
 | 
			
		||||
    cheats_nemesisHenchmenKillsMultiplierCorpus: `[UNTRANSLATED] Rage Progess Multiplier (Corpus)`,
 | 
			
		||||
    cheats_nemesisAntivirusGainMultiplier: `[UNTRANSLATED] Antivirus Progress Multiplier`,
 | 
			
		||||
    cheats_nemesisHintProgressMultiplierGrineer: `[UNTRANSLATED] Hint Progress Multiplier (Grineer)`,
 | 
			
		||||
    cheats_nemesisHintProgressMultiplierCorpus: `[UNTRANSLATED] Hint Progress Multiplier (Corpus)`,
 | 
			
		||||
    cheats_nemesisExtraWeapon: `[UNTRANSLATED] Extra Nemesis Weapon / Token On Vanquish (0 to disable)`,
 | 
			
		||||
    cheats_playerSkillGainsMultiplierSpace: `[UNTRANSLATED] Intrinsics Gains Multiplier (Space)`,
 | 
			
		||||
    cheats_playerSkillGainsMultiplierDrifter: `[UNTRANSLATED] Intrinsics Gains Multiplier (Drifter)`,
 | 
			
		||||
    cheats_extraMissionRewards: `[UNTRANSLATED] Extra Mission Rewards (0 to disable)`,
 | 
			
		||||
    cheats_strippedItemRewardsMultiplier: `[UNTRANSLATED] Stripped Item Rewards Multiplier`,
 | 
			
		||||
 | 
			
		||||
    worldState: `Carte Solaire`,
 | 
			
		||||
    worldState_creditBoost: `Booster de Crédit`,
 | 
			
		||||
@ -278,6 +289,7 @@ dict = {
 | 
			
		||||
    worldState_hallowedFlame: `Flamme Hantée`,
 | 
			
		||||
    worldState_hallowedNightmares: `Cauchemars Hantés`,
 | 
			
		||||
    worldState_hallowedNightmaresRewards: `Récompenses Flamme Hantée Cauchemar`,
 | 
			
		||||
    worldState_naberusNights: `[UNTRANSLATED] Nights of Naberus`,
 | 
			
		||||
    worldState_proxyRebellion: `Rébellion Proxy`,
 | 
			
		||||
    worldState_proxyRebellionRewards: `Récompenses Rébellion Proxy`,
 | 
			
		||||
    worldState_bellyOfTheBeast: `Ventre de la Bête`,
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
// Russian translation by AMelonInsideLemon, LoseFace
 | 
			
		||||
dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `Примечание: Чтобы увидеть изменения в игре, вам нужно повторно синхронизировать свой инвентарь, например, используя команду /sync в программе bootstrapper, посетив Додзё/Реле или перезагрузив игру.`,
 | 
			
		||||
    general_inventoryUpdateNote: `Примечание: Чтобы увидеть изменения в игре, вам нужно повторно синхронизировать свой инвентарь, например, используя команду загрузчика /sync в чате игры, посетив Додзё/Реле или перезагрузив игру.`,
 | 
			
		||||
    general_inventoryUpdateNoteGameWs: `Примечание: для того, чтобы изменения вступили в силу, может потребоваться повторно открыть меню, в котором вы находитесь.`,
 | 
			
		||||
    general_addButton: `Добавить`,
 | 
			
		||||
    general_setButton: `Установить`,
 | 
			
		||||
@ -45,7 +45,7 @@ dict = {
 | 
			
		||||
    code_rank: `Ранг`,
 | 
			
		||||
    code_rankUp: `Повысить ранг`,
 | 
			
		||||
    code_rankDown: `Понизить ранг`,
 | 
			
		||||
    code_unlockLevelCap: `[UNTRANSLATED] Unlock level cap`,
 | 
			
		||||
    code_unlockLevelCap: `Разблокировать ограничение уровня`,
 | 
			
		||||
    code_count: `Количество`,
 | 
			
		||||
    code_focusAllUnlocked: `Все школы Фокуса уже разблокированы.`,
 | 
			
		||||
    code_focusUnlocked: `Разблокировано |COUNT| новых школ Фокуса! Для отображения изменений в игре потребуется обновление инвентаря. Посещение навигации — самый простой способ этого добиться.`,
 | 
			
		||||
@ -109,7 +109,7 @@ dict = {
 | 
			
		||||
    inventory_moaPets: `МОА`,
 | 
			
		||||
    inventory_kubrowPets: `Звери`,
 | 
			
		||||
    inventory_evolutionProgress: `Прогресс эволюции Инкарнонов`,
 | 
			
		||||
    inventory_Boosters: `Бустеры`,
 | 
			
		||||
    inventory_boosters: `Бустеры`,
 | 
			
		||||
    inventory_flavourItems: `<abbr title="Наборы анимаций, глифы, палитры и т. д.">Уникальные предметы</abbr>`,
 | 
			
		||||
    inventory_shipDecorations: `Украшения корабля`,
 | 
			
		||||
    inventory_bulkAddSuits: `Добавить отсутствующие Варфреймы`,
 | 
			
		||||
@ -147,7 +147,7 @@ dict = {
 | 
			
		||||
    detailedView_valenceBonusLabel: `Бонус Валентности`,
 | 
			
		||||
    detailedView_valenceBonusDescription: `Вы можете установить или убрать бонус Валентности с вашего оружия.`,
 | 
			
		||||
    detailedView_modularPartsLabel: `Изменить модульные части`,
 | 
			
		||||
    detailedView_suitInvigorationLabel: `Воодушевление Варфрейма`,
 | 
			
		||||
    detailedView_invigorationLabel: `Воодушевление`,
 | 
			
		||||
    detailedView_loadoutLabel: `Конфигурации`,
 | 
			
		||||
 | 
			
		||||
    invigorations_offensive_AbilityStrength: `+200% к силе способностей.`,
 | 
			
		||||
@ -172,9 +172,9 @@ dict = {
 | 
			
		||||
    invigorations_utility_Jumps: `+5 сбросов прыжка.`,
 | 
			
		||||
    invigorations_utility_EnergyRegen: `+2 к регенерации энергии в секунду.`,
 | 
			
		||||
 | 
			
		||||
    invigorations_offensiveLabel: `Атакующее улучшение`,
 | 
			
		||||
    invigorations_defensiveLabel: `Вспомогательное улучшение`,
 | 
			
		||||
    invigorations_expiryLabel: `Срок действия Воодушевления (необязательно)`,
 | 
			
		||||
    detailedView_invigorationOffensiveLabel: `Атакующее улучшение`,
 | 
			
		||||
    detailedView_invigorationUtilityLabel: `Вспомогательное улучшение`,
 | 
			
		||||
    detailedView_invigorationExpiryLabel: `Срок действия Воодушевления (необязательно)`,
 | 
			
		||||
 | 
			
		||||
    abilityOverride_label: `Переопределение способности`,
 | 
			
		||||
    abilityOverride_onSlot: `в ячейке`,
 | 
			
		||||
@ -227,7 +227,7 @@ dict = {
 | 
			
		||||
    cheats_baroFullyStocked: `Баро полностью укомплектован`,
 | 
			
		||||
    cheats_syndicateMissionsRepeatable: `Повторять миссии синдиката`,
 | 
			
		||||
    cheats_unlockAllProfitTakerStages: `Разблокировать все этапы Сферы извлечения прибыли`,
 | 
			
		||||
    cheats_unlockSuccInventory: `Успех. Обратите внимание, что вам необходимо будет повторно синхронизировать свой инвентарь, например, с помощью команды /sync в программе bootstrapper, посетив Додзё/Реле или повторно войдя в игру.`,
 | 
			
		||||
    cheats_unlockSuccInventory: `Успех. Обратите внимание, что вам необходимо будет повторно синхронизировать свой инвентарь, например, с помощью команды загрузчика /sync в чате игры, посетив Додзё/Реле или повторно войдя в игру.`,
 | 
			
		||||
    cheats_instantFinishRivenChallenge: `Мгновенное завершение испытания мода Разлома`,
 | 
			
		||||
    cheats_instantResourceExtractorDrones: `Мгновенно добывающие Дроны-сборщики`,
 | 
			
		||||
    cheats_noResourceExtractorDronesDamage: `Без урона по Дронам-сборщикам`,
 | 
			
		||||
@ -257,6 +257,17 @@ dict = {
 | 
			
		||||
    cheats_changeButton: `Изменить`,
 | 
			
		||||
    cheats_markAllAsRead: `Пометить все входящие как прочитанные`,
 | 
			
		||||
    cheats_finishInvasionsInOneMission: `Завершать вторжение за одну миссию`,
 | 
			
		||||
    cheats_gainNoNegativeSyndicateStanding: `[UNTRANSLATED] Gain No Negative Syndicate Standing`,
 | 
			
		||||
    cheats_nemesisHenchmenKillsMultiplierGrineer: `Мультипликатор прогресса ярости (Гринир)`,
 | 
			
		||||
    cheats_nemesisHenchmenKillsMultiplierCorpus: `Мультипликатор прогресса ярости (Корпус)`,
 | 
			
		||||
    cheats_nemesisAntivirusGainMultiplier: `Мультипликатор прогресса антивируса`,
 | 
			
		||||
    cheats_nemesisHintProgressMultiplierGrineer: `Мультипликатор прогресса подсказки (Гринир)`,
 | 
			
		||||
    cheats_nemesisHintProgressMultiplierCorpus: `Мультипликатор прогресса подсказки (Корпус)`,
 | 
			
		||||
    cheats_nemesisExtraWeapon: `Дополнительное оружие/активный Кардиомиоцит за победу над Противником (0 для отключения)`,
 | 
			
		||||
    cheats_playerSkillGainsMultiplierSpace: `[UNTRANSLATED] Intrinsics Gains Multiplier (Space)`,
 | 
			
		||||
    cheats_playerSkillGainsMultiplierDrifter: `[UNTRANSLATED] Intrinsics Gains Multiplier (Drifter)`,
 | 
			
		||||
    cheats_extraMissionRewards: `[UNTRANSLATED] Extra Mission Rewards (0 to disable)`,
 | 
			
		||||
    cheats_strippedItemRewardsMultiplier: `[UNTRANSLATED] Stripped Item Rewards Multiplier`,
 | 
			
		||||
 | 
			
		||||
    worldState: `Состояние мира`,
 | 
			
		||||
    worldState_creditBoost: `Глобальный бустер Кредитов`,
 | 
			
		||||
@ -278,6 +289,7 @@ dict = {
 | 
			
		||||
    worldState_hallowedFlame: `Священное пламя`,
 | 
			
		||||
    worldState_hallowedNightmares: `Священные кошмары`,
 | 
			
		||||
    worldState_hallowedNightmaresRewards: `Награды Священных кошмаров`,
 | 
			
		||||
    worldState_naberusNights: `Ночи Наберуса`,
 | 
			
		||||
    worldState_proxyRebellion: `Восстание роботов`,
 | 
			
		||||
    worldState_proxyRebellionRewards: `Награды Восстания роботов`,
 | 
			
		||||
    worldState_bellyOfTheBeast: `Чрево зверя`,
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
// Ukrainian translation by LoseFace
 | 
			
		||||
dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `Пам'ятка: Щоб побачити зміни в грі, вам потрібно повторно синхронізувати своє спорядження, наприклад, використовуючи команду /sync в програмі bootstrapper, відвідавши Доджьо/Реле або перезавантаживши гру.`,
 | 
			
		||||
    general_inventoryUpdateNote: `Пам'ятка: Щоб побачити зміни в грі, вам потрібно повторно синхронізувати своє спорядження, наприклад, використовуючи команду завантажувача /sync у чаті гри, відвідавши Доджьо/Реле або перезавантаживши гру.`,
 | 
			
		||||
    general_inventoryUpdateNoteGameWs: `Примітка: для відображення змін може знадобитися повторно відкрити меню, в якому ви перебуваєте.`,
 | 
			
		||||
    general_addButton: `Добавити`,
 | 
			
		||||
    general_setButton: `Встановити`,
 | 
			
		||||
@ -45,7 +45,7 @@ dict = {
 | 
			
		||||
    code_rank: `Рівень`,
 | 
			
		||||
    code_rankUp: `Підвищити рівень`,
 | 
			
		||||
    code_rankDown: `Понизити рівень`,
 | 
			
		||||
    code_unlockLevelCap: `[UNTRANSLATED] Unlock level cap`,
 | 
			
		||||
    code_unlockLevelCap: `Розблокувати обмеження рівня`,
 | 
			
		||||
    code_count: `Кількість`,
 | 
			
		||||
    code_focusAllUnlocked: `Всі школи Фокусу вже розблоковані.`,
 | 
			
		||||
    code_focusUnlocked: `Розблоковано |COUNT| нових шкіл Фокусу! Для відображення змін в грі знадобиться оновлення спорядження. Відвідування навігації — найпростіший спосіб цього досягти.`,
 | 
			
		||||
@ -109,7 +109,7 @@ dict = {
 | 
			
		||||
    inventory_moaPets: `МОА`,
 | 
			
		||||
    inventory_kubrowPets: `Тварини`,
 | 
			
		||||
    inventory_evolutionProgress: `Прогрес еволюції Інкарнонів`,
 | 
			
		||||
    inventory_Boosters: `Посилення`,
 | 
			
		||||
    inventory_boosters: `Посилення`,
 | 
			
		||||
    inventory_flavourItems: `<abbr title="Набори анімацій, гліфи, палітри і т. д.">Унікальні предмети</abbr>`,
 | 
			
		||||
    inventory_shipDecorations: `Прикраси судна`,
 | 
			
		||||
    inventory_bulkAddSuits: `Додати відсутні Ворфрейми`,
 | 
			
		||||
@ -147,7 +147,7 @@ dict = {
 | 
			
		||||
    detailedView_valenceBonusLabel: `Ознака Валентності`,
 | 
			
		||||
    detailedView_valenceBonusDescription: `Ви можете встановити або прибрати ознаку Валентності з вашої зброї.`,
 | 
			
		||||
    detailedView_modularPartsLabel: `Змінити модульні частини`,
 | 
			
		||||
    detailedView_suitInvigorationLabel: `Зміцнення Ворфрейма`,
 | 
			
		||||
    detailedView_invigorationLabel: `Зміцнення`,
 | 
			
		||||
    detailedView_loadoutLabel: `Конфігурації`,
 | 
			
		||||
 | 
			
		||||
    invigorations_offensive_AbilityStrength: `+200% до потужності здібностей.`,
 | 
			
		||||
@ -172,9 +172,9 @@ dict = {
 | 
			
		||||
    invigorations_utility_Jumps: `+5 Оновлень стрибків.`,
 | 
			
		||||
    invigorations_utility_EnergyRegen: `+2 до відновлення енергії на секунду.`,
 | 
			
		||||
 | 
			
		||||
    invigorations_offensiveLabel: `Атакуюче вдосконалення`,
 | 
			
		||||
    invigorations_defensiveLabel: `Допоміжне вдосконалення`,
 | 
			
		||||
    invigorations_expiryLabel: `Термін дії Зміцнення (необов'язково)`,
 | 
			
		||||
    detailedView_invigorationOffensiveLabel: `Атакуюче вдосконалення`,
 | 
			
		||||
    detailedView_invigorationUtilityLabel: `Допоміжне вдосконалення`,
 | 
			
		||||
    detailedView_invigorationExpiryLabel: `Термін дії Зміцнення (необов'язково)`,
 | 
			
		||||
 | 
			
		||||
    abilityOverride_label: `Перевизначення здібностей`,
 | 
			
		||||
    abilityOverride_onSlot: `у комірці`,
 | 
			
		||||
@ -227,7 +227,7 @@ dict = {
 | 
			
		||||
    cheats_baroFullyStocked: `Баро повністю укомплектований`,
 | 
			
		||||
    cheats_syndicateMissionsRepeatable: `Повторювати місії синдиката`,
 | 
			
		||||
    cheats_unlockAllProfitTakerStages: `Розблокувати всі етапи Привласнювачки`,
 | 
			
		||||
    cheats_unlockSuccInventory: `Успішно. Зверніть увагу, що вам потрібно буде повторно синхронізувати своє спорядження, наприклад, за допомогою команди /sync в програмі bootstrapper, відвідавши Доджьо/Реле або повторно увійшовши в гру.`,
 | 
			
		||||
    cheats_unlockSuccInventory: `Успішно. Зверніть увагу, що вам потрібно буде повторно синхронізувати своє спорядження, наприклад, за допомогою команди завантажувача /sync у чаті гри, відвідавши Доджьо/Реле або повторно увійшовши в гру.`,
 | 
			
		||||
    cheats_instantFinishRivenChallenge: `Миттєве завершення випробування модифікатора Розколу`,
 | 
			
		||||
    cheats_instantResourceExtractorDrones: `Миттєво добуваючі Дрони-видобувачі`,
 | 
			
		||||
    cheats_noResourceExtractorDronesDamage: `Без шкоди по Дронам-видобувачам`,
 | 
			
		||||
@ -257,6 +257,17 @@ dict = {
 | 
			
		||||
    cheats_changeButton: `Змінити`,
 | 
			
		||||
    cheats_markAllAsRead: `Помітити всі вхідні як прочитані`,
 | 
			
		||||
    cheats_finishInvasionsInOneMission: `Завершувати вторгнення за одну місію`,
 | 
			
		||||
    cheats_gainNoNegativeSyndicateStanding: `[UNTRANSLATED] Gain No Negative Syndicate Standing`,
 | 
			
		||||
    cheats_nemesisHenchmenKillsMultiplierGrineer: `Множник прогресу люті (Ґрінери)`,
 | 
			
		||||
    cheats_nemesisHenchmenKillsMultiplierCorpus: `Множник прогресу люті (Корпус)`,
 | 
			
		||||
    cheats_nemesisAntivirusGainMultiplier: `Мультиплікатор прогресу антивіруса`,
 | 
			
		||||
    cheats_nemesisHintProgressMultiplierGrineer: `Множник прогресу підсказки (Ґрінери)`,
 | 
			
		||||
    cheats_nemesisHintProgressMultiplierCorpus: `Множник прогресу підсказки (Корпус)`,
 | 
			
		||||
    cheats_nemesisExtraWeapon: `Додаткова зброя/Жива сердцевина за перемогу над Недругом (0 для вимкнення)`,
 | 
			
		||||
    cheats_playerSkillGainsMultiplierSpace: `[UNTRANSLATED] Intrinsics Gains Multiplier (Space)`,
 | 
			
		||||
    cheats_playerSkillGainsMultiplierDrifter: `[UNTRANSLATED] Intrinsics Gains Multiplier (Drifter)`,
 | 
			
		||||
    cheats_extraMissionRewards: `[UNTRANSLATED] Extra Mission Rewards (0 to disable)`,
 | 
			
		||||
    cheats_strippedItemRewardsMultiplier: `[UNTRANSLATED] Stripped Item Rewards Multiplier`,
 | 
			
		||||
 | 
			
		||||
    worldState: `Стан світу`,
 | 
			
		||||
    worldState_creditBoost: `Глобальне посилення Кредитів`,
 | 
			
		||||
@ -278,6 +289,7 @@ dict = {
 | 
			
		||||
    worldState_hallowedFlame: `Священне полум'я`,
 | 
			
		||||
    worldState_hallowedNightmares: `Священні жахіття`,
 | 
			
		||||
    worldState_hallowedNightmaresRewards: `Нагороди Священних жахіть`,
 | 
			
		||||
    worldState_naberusNights: `Наберові ночі`,
 | 
			
		||||
    worldState_proxyRebellion: `Повстання роботів`,
 | 
			
		||||
    worldState_proxyRebellionRewards: `Нагороди Повстання роботів`,
 | 
			
		||||
    worldState_bellyOfTheBeast: `У лігві звіра`,
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
// Chinese translation by meb154, bishan178, nyaoouo, qianlishun, CrazyZhang, Corvus, & qingchun
 | 
			
		||||
// Chinese translation by meb154, bishan178, nyaoouo, qianlishun, CrazyZhang, Corvus, qingchun
 | 
			
		||||
dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `注意: 要在游戏中查看更改,您需要重新同步库存,例如使用客户端的 /sync 命令,访问道场/中继站或重新登录.`,
 | 
			
		||||
    general_inventoryUpdateNoteGameWs: `[UNTRANSLATED] Note: You may need to reopen any menu you are on for changes to be reflected.`,
 | 
			
		||||
@ -76,7 +76,7 @@ dict = {
 | 
			
		||||
    code_replays: `[UNTRANSLATED] Replays`,
 | 
			
		||||
    code_stalker: `追猎者`,
 | 
			
		||||
    code_succChange: `更改成功`,
 | 
			
		||||
    code_requiredInvigorationUpgrade: `您必须同时选择一个进攻型和一个功能型活化属性.`,
 | 
			
		||||
    code_requiredInvigorationUpgrade: `[UNTRANSLATED] You must select both an offensive & utility upgrade.`,
 | 
			
		||||
    login_description: `使用您的 OpenWF 账户凭证登录(与游戏内连接本服务器时使用的昵称相同)`,
 | 
			
		||||
    login_emailLabel: `电子邮箱`,
 | 
			
		||||
    login_passwordLabel: `密码`,
 | 
			
		||||
@ -109,7 +109,7 @@ dict = {
 | 
			
		||||
    inventory_moaPets: `恐鸟`,
 | 
			
		||||
    inventory_kubrowPets: `动物同伴`,
 | 
			
		||||
    inventory_evolutionProgress: `灵化之源进度`,
 | 
			
		||||
    inventory_Boosters: `加成器`,
 | 
			
		||||
    inventory_boosters: `加成器`,
 | 
			
		||||
    inventory_flavourItems: `<abbr title="动作表情、浮印、调色板等">装饰物品</abbr>`,
 | 
			
		||||
    inventory_shipDecorations: `飞船装饰`,
 | 
			
		||||
    inventory_bulkAddSuits: `添加缺失战甲`,
 | 
			
		||||
@ -147,7 +147,7 @@ dict = {
 | 
			
		||||
    detailedView_valenceBonusLabel: `效价加成`,
 | 
			
		||||
    detailedView_valenceBonusDescription: `您可以设置或移除武器上的效价加成.`,
 | 
			
		||||
    detailedView_modularPartsLabel: `更换部件`,
 | 
			
		||||
    detailedView_suitInvigorationLabel: `编辑战甲活化属性`,
 | 
			
		||||
    detailedView_invigorationLabel: `活化`,
 | 
			
		||||
    detailedView_loadoutLabel: `配置`,
 | 
			
		||||
 | 
			
		||||
    invigorations_offensive_AbilityStrength: `+200%技能强度`,
 | 
			
		||||
@ -172,9 +172,9 @@ dict = {
 | 
			
		||||
    invigorations_utility_Jumps: `+5跳跃次数`,
 | 
			
		||||
    invigorations_utility_EnergyRegen: `+2/秒能量恢复`,
 | 
			
		||||
 | 
			
		||||
    invigorations_offensiveLabel: `进攻型属性`,
 | 
			
		||||
    invigorations_defensiveLabel: `功能型属性`,
 | 
			
		||||
    invigorations_expiryLabel: `活化时效(可选)`,
 | 
			
		||||
    detailedView_invigorationOffensiveLabel: `进攻型属性`,
 | 
			
		||||
    detailedView_invigorationUtilityLabel: `[UNTRANSLATED] Utility Upgrade`,
 | 
			
		||||
    detailedView_invigorationExpiryLabel: `活化时效(可选)`,
 | 
			
		||||
 | 
			
		||||
    abilityOverride_label: `技能替换`,
 | 
			
		||||
    abilityOverride_onSlot: `槽位`,
 | 
			
		||||
@ -193,7 +193,7 @@ dict = {
 | 
			
		||||
    cheats_skipTutorial: `跳过教程`,
 | 
			
		||||
    cheats_skipAllDialogue: `跳过所有对话`,
 | 
			
		||||
    cheats_unlockAllScans: `解锁所有扫描`,
 | 
			
		||||
    cheats_unlockSuccRelog: `[UNTRANSLATED] Success. Please that you'll need to relog for the client to refresh this.`,
 | 
			
		||||
    cheats_unlockSuccRelog: `[UNTRANSLATED] Success. Please note that you'll need to relog for the client to refresh this.`,
 | 
			
		||||
    cheats_unlockAllMissions: `解锁所有星图`,
 | 
			
		||||
    cheats_unlockAllMissions_ok: `操作成功.请注意,您需要进入道场/中继站或重新登录以刷新星图数据.`,
 | 
			
		||||
    cheats_infiniteCredits: `无限现金`,
 | 
			
		||||
@ -227,7 +227,7 @@ dict = {
 | 
			
		||||
    cheats_baroFullyStocked: `虚空商人贩卖所有商品`,
 | 
			
		||||
    cheats_syndicateMissionsRepeatable: `集团任务可重复完成`,
 | 
			
		||||
    cheats_unlockAllProfitTakerStages: `解锁利润收割者圆蛛所有阶段`,
 | 
			
		||||
    cheats_unlockSuccInventory: `[UNTRANSLATED] Success. Please note that you'll need to resync your inventory, e.g. using the bootstrapper's /sync command, visiting a dojo/relay, or relogging..`,
 | 
			
		||||
    cheats_unlockSuccInventory: `[UNTRANSLATED] Success. Please note that you'll need to resync your inventory, e.g. using the bootstrapper's /sync command in game chat, visiting a dojo/relay, or relogging.`,
 | 
			
		||||
    cheats_instantFinishRivenChallenge: `立即完成裂罅挑战`,
 | 
			
		||||
    cheats_instantResourceExtractorDrones: `资源无人机即时完成`,
 | 
			
		||||
    cheats_noResourceExtractorDronesDamage: `资源无人机不会损毁`,
 | 
			
		||||
@ -257,6 +257,22 @@ dict = {
 | 
			
		||||
    cheats_changeButton: `更改`,
 | 
			
		||||
    cheats_markAllAsRead: `收件箱全部标记为已读`,
 | 
			
		||||
    cheats_finishInvasionsInOneMission: `一场任务完成整场入侵`,
 | 
			
		||||
    cheats_gainNoNegativeSyndicateStanding: `集团声望不倒扣不掉段`,
 | 
			
		||||
    cheats_nemesisHenchmenKillsMultiplierGrineer: `玄骸怒气倍率 (Grineer)`,
 | 
			
		||||
    cheats_nemesisHenchmenKillsMultiplierCorpus: `玄骸怒气倍率 (Corpus)`,
 | 
			
		||||
    cheats_nemesisAntivirusGainMultiplier: `杀毒进度倍率 (科腐者)`,
 | 
			
		||||
    cheats_nemesisHintProgressMultiplierGrineer: `解密进度倍率 (Grineer)`,
 | 
			
		||||
    cheats_nemesisHintProgressMultiplierCorpus: `解密进度倍率 (Corpus)`,
 | 
			
		||||
    cheats_nemesisExtraWeapon: `额外玄骸武器/代币 (0为禁用)`,
 | 
			
		||||
    cheats_playerSkillGainsMultiplierSpace: `內源之力获取倍率 (九重天)`,
 | 
			
		||||
    cheats_playerSkillGainsMultiplierDrifter: `內源之力获取倍率 (漂泊者)`,
 | 
			
		||||
    cheats_extraMissionRewards: `额外任务奖励 (0为禁用)`,
 | 
			
		||||
    cheats_strippedItemRewardsMultiplier: `隐藏战利品奖励倍率`,
 | 
			
		||||
    cheats_extraRelicRewards: `额外遗物奖励`,
 | 
			
		||||
    cheats_crackRelicForPlatinum: `打开遗物时获得白金`,
 | 
			
		||||
    cheats_relicPlatinumCommon: `普通奖励的白金`,
 | 
			
		||||
    cheats_relicPlatinumUncommon: `罕见奖励的白金`,
 | 
			
		||||
    cheats_relicPlatinumRare: `稀有奖励的白金`,
 | 
			
		||||
 | 
			
		||||
    worldState: `世界状态配置`,
 | 
			
		||||
    worldState_creditBoost: `现金加成`,
 | 
			
		||||
@ -278,6 +294,7 @@ dict = {
 | 
			
		||||
    worldState_hallowedFlame: `万圣之焰`,
 | 
			
		||||
    worldState_hallowedNightmares: `万圣噩梦`,
 | 
			
		||||
    worldState_hallowedNightmaresRewards: `万圣噩梦奖励设置`,
 | 
			
		||||
    worldState_naberusNights: `[UNTRANSLATED] Nights of Naberus`,
 | 
			
		||||
    worldState_proxyRebellion: `机械叛乱`,
 | 
			
		||||
    worldState_proxyRebellionRewards: `机械叛乱奖励设置`,
 | 
			
		||||
    worldState_bellyOfTheBeast: `兽之腹`,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user