chore(webui): improving inconsistent, long string #2344
@ -43,7 +43,7 @@ export const focusController: RequestHandler = async (req, res) => {
 | 
			
		||||
            inventory.FocusAbility ??= focusType;
 | 
			
		||||
            inventory.FocusUpgrades.push({ ItemType: focusType });
 | 
			
		||||
            if (inventory.FocusXP) {
 | 
			
		||||
                inventory.FocusXP[focusPolarity] -= cost;
 | 
			
		||||
                inventory.FocusXP[focusPolarity]! -= cost;
 | 
			
		||||
            }
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            res.json({
 | 
			
		||||
@ -78,7 +78,7 @@ export const focusController: RequestHandler = async (req, res) => {
 | 
			
		||||
                cost += ExportFocusUpgrades[focusType].baseFocusPointCost;
 | 
			
		||||
                inventory.FocusUpgrades.push({ ItemType: focusType, Level: 0 });
 | 
			
		||||
            }
 | 
			
		||||
            inventory.FocusXP![focusPolarity] -= cost;
 | 
			
		||||
            inventory.FocusXP![focusPolarity]! -= cost;
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            res.json({
 | 
			
		||||
                FocusTypes: request.FocusTypes,
 | 
			
		||||
@ -96,7 +96,7 @@ export const focusController: RequestHandler = async (req, res) => {
 | 
			
		||||
                const focusUpgradeDb = inventory.FocusUpgrades.find(entry => entry.ItemType == focusUpgrade.ItemType)!;
 | 
			
		||||
                focusUpgradeDb.Level = focusUpgrade.Level;
 | 
			
		||||
            }
 | 
			
		||||
            inventory.FocusXP![focusPolarity] -= cost;
 | 
			
		||||
            inventory.FocusXP![focusPolarity]! -= cost;
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            res.json({
 | 
			
		||||
                FocusInfos: request.FocusInfos,
 | 
			
		||||
@ -123,7 +123,7 @@ export const focusController: RequestHandler = async (req, res) => {
 | 
			
		||||
            const request = JSON.parse(String(req.body)) as IUnbindUpgradeRequest;
 | 
			
		||||
            const focusPolarity = focusTypeToPolarity(request.FocusTypes[0]);
 | 
			
		||||
            const inventory = await getInventory(accountId);
 | 
			
		||||
            inventory.FocusXP![focusPolarity] -= 750_000 * request.FocusTypes.length;
 | 
			
		||||
            inventory.FocusXP![focusPolarity]! -= 750_000 * request.FocusTypes.length;
 | 
			
		||||
            addMiscItems(inventory, [
 | 
			
		||||
                {
 | 
			
		||||
                    ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantItem",
 | 
			
		||||
@ -168,8 +168,10 @@ export const focusController: RequestHandler = async (req, res) => {
 | 
			
		||||
                shard.ItemCount *= -1;
 | 
			
		||||
            }
 | 
			
		||||
            const inventory = await getInventory(accountId);
 | 
			
		||||
            inventory.FocusXP ??= { AP_POWER: 0, AP_TACTIC: 0, AP_DEFENSE: 0, AP_ATTACK: 0, AP_WARD: 0 };
 | 
			
		||||
            inventory.FocusXP[request.Polarity] += xp;
 | 
			
		||||
            const polarity = request.Polarity;
 | 
			
		||||
            inventory.FocusXP ??= {};
 | 
			
		||||
            inventory.FocusXP[polarity] ??= 0;
 | 
			
		||||
            inventory.FocusXP[polarity] += xp;
 | 
			
		||||
            addMiscItems(inventory, request.Shards);
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
@ -30,8 +30,9 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
 | 
			
		||||
            const request = getJSONfromString<IShardInstallRequest>(String(req.body));
 | 
			
		||||
            const inventory = await getInventory(account._id.toString());
 | 
			
		||||
            const suit = inventory.Suits.id(request.SuitId.$oid)!;
 | 
			
		||||
            if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) {
 | 
			
		||||
                suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}];
 | 
			
		||||
            suit.ArchonCrystalUpgrades ??= [];
 | 
			
		||||
            while (suit.ArchonCrystalUpgrades.length < request.Slot) {
 | 
			
		||||
                suit.ArchonCrystalUpgrades.push({});
 | 
			
		||||
            }
 | 
			
		||||
            suit.ArchonCrystalUpgrades[request.Slot] = {
 | 
			
		||||
                UpgradeType: request.UpgradeType,
 | 
			
		||||
@ -92,7 +93,8 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // remove from suit
 | 
			
		||||
            suit.ArchonCrystalUpgrades![request.Slot] = {};
 | 
			
		||||
            suit.ArchonCrystalUpgrades![request.Slot].UpgradeType = undefined;
 | 
			
		||||
            suit.ArchonCrystalUpgrades![request.Slot].Color = undefined;
 | 
			
		||||
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
import { version_compare } from "@/src/helpers/inventoryHelpers";
 | 
			
		||||
import {
 | 
			
		||||
    antivirusMods,
 | 
			
		||||
    consumeModCharge,
 | 
			
		||||
    decodeNemesisGuess,
 | 
			
		||||
    encodeNemesisGuess,
 | 
			
		||||
@ -134,34 +135,37 @@ export const nemesisController: RequestHandler = async (req, res) => {
 | 
			
		||||
                for (const upgrade of body.knife!.AttachedUpgrades) {
 | 
			
		||||
                    switch (upgrade.ItemType) {
 | 
			
		||||
                        case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod":
 | 
			
		||||
                            antivirusGain += 10;
 | 
			
		||||
                            consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
 | 
			
		||||
                            break;
 | 
			
		||||
                        case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod":
 | 
			
		||||
                        case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod":
 | 
			
		||||
                            antivirusGain += 10;
 | 
			
		||||
                            consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
 | 
			
		||||
                            break;
 | 
			
		||||
                        case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod": // Instant Secure
 | 
			
		||||
                            antivirusGain += 15;
 | 
			
		||||
                            consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
 | 
			
		||||
                            break;
 | 
			
		||||
                        case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod": // Immuno Shield
 | 
			
		||||
                            antivirusGain += 15;
 | 
			
		||||
                            consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
 | 
			
		||||
                            break;
 | 
			
		||||
                        case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod":
 | 
			
		||||
                            antivirusGain += 10;
 | 
			
		||||
                            consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
 | 
			
		||||
                            break;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                inventory.Nemesis!.HenchmenKilled += antivirusGain;
 | 
			
		||||
                if (inventory.Nemesis!.HenchmenKilled >= 100) {
 | 
			
		||||
                    inventory.Nemesis!.HenchmenKilled = 100;
 | 
			
		||||
                    // Client doesn't seem to request mode=w for infested liches, so weakening it here.
 | 
			
		||||
                    inventory.Nemesis!.InfNodes = [
 | 
			
		||||
                        {
 | 
			
		||||
                            Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode,
 | 
			
		||||
                            Influence: 1
 | 
			
		||||
                        }
 | 
			
		||||
                    ];
 | 
			
		||||
                    inventory.Nemesis!.Weakened = true;
 | 
			
		||||
                    const upgrade = getKnifeUpgrade(inventory, dataknifeUpgrades, antivirusMods[passcode]);
 | 
			
		||||
                    consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (inventory.Nemesis!.HenchmenKilled >= 100) {
 | 
			
		||||
                inventory.Nemesis!.HenchmenKilled = 100;
 | 
			
		||||
            if (inventory.Nemesis!.HenchmenKilled < 100) {
 | 
			
		||||
                inventory.Nemesis!.InfNodes = getInfNodes(getNemesisManifest(inventory.Nemesis!.manifest), 0);
 | 
			
		||||
            }
 | 
			
		||||
            inventory.Nemesis!.InfNodes = getInfNodes(getNemesisManifest(inventory.Nemesis!.manifest), 0);
 | 
			
		||||
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            res.json(response);
 | 
			
		||||
@ -283,6 +287,10 @@ export const nemesisController: RequestHandler = async (req, res) => {
 | 
			
		||||
        );
 | 
			
		||||
        //const body = getJSONfromString<INemesisWeakenRequest>(String(req.body));
 | 
			
		||||
 | 
			
		||||
        if (inventory.Nemesis!.Weakened) {
 | 
			
		||||
            logger.warn(`client is weakening an already-weakened nemesis?!`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        inventory.Nemesis!.InfNodes = [
 | 
			
		||||
            {
 | 
			
		||||
                Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										22
									
								
								src/controllers/api/setSuitInfectionController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/controllers/api/setSuitInfectionController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
import { fromMongoDate, fromOid } from "@/src/helpers/inventoryHelpers";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const setSuitInfectionController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "Suits");
 | 
			
		||||
    const payload = getJSONfromString<ISetSuitInfectionRequest>(String(req.body));
 | 
			
		||||
    for (const clientSuit of payload.Suits) {
 | 
			
		||||
        const dbSuit = inventory.Suits.id(fromOid(clientSuit.ItemId))!;
 | 
			
		||||
        dbSuit.InfestationDate = fromMongoDate(clientSuit.InfestationDate!);
 | 
			
		||||
    }
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ISetSuitInfectionRequest {
 | 
			
		||||
    Suits: IEquipmentClient[];
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								src/controllers/api/umbraController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/controllers/api/umbraController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
import { fromMongoDate, fromOid } from "@/src/helpers/inventoryHelpers";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { addMiscItem, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const umbraController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "Suits MiscItems");
 | 
			
		||||
    const payload = getJSONfromString<IUmbraRequest>(String(req.body));
 | 
			
		||||
    for (const clientSuit of payload.Suits) {
 | 
			
		||||
        const dbSuit = inventory.Suits.id(fromOid(clientSuit.ItemId))!;
 | 
			
		||||
        if (clientSuit.UmbraDate) {
 | 
			
		||||
            addMiscItem(inventory, "/Lotus/Types/Items/MiscItems/UmbraEchoes", -1);
 | 
			
		||||
            dbSuit.UmbraDate = fromMongoDate(clientSuit.UmbraDate);
 | 
			
		||||
        } else {
 | 
			
		||||
            dbSuit.UmbraDate = undefined;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IUmbraRequest {
 | 
			
		||||
    Suits: IEquipmentClient[];
 | 
			
		||||
}
 | 
			
		||||
@ -12,6 +12,7 @@ export const popArchonCrystalUpgradeController: RequestHandler = async (req, res
 | 
			
		||||
        );
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
        res.end();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    res.status(400).end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@ export const pushArchonCrystalUpgradeController: RequestHandler = async (req, re
 | 
			
		||||
            }
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            res.end();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    res.status(400).end();
 | 
			
		||||
 | 
			
		||||
@ -248,7 +248,7 @@ const requiemMods: readonly string[] = [
 | 
			
		||||
    "/Lotus/Upgrades/Mods/Immortal/ImmortalEightMod"
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const antivirusMods: readonly string[] = [
 | 
			
		||||
export const antivirusMods: readonly string[] = [
 | 
			
		||||
    "/Lotus/Upgrades/Mods/Immortal/AntivirusOneMod",
 | 
			
		||||
    "/Lotus/Upgrades/Mods/Immortal/AntivirusTwoMod",
 | 
			
		||||
    "/Lotus/Upgrades/Mods/Immortal/AntivirusThreeMod",
 | 
			
		||||
 | 
			
		||||
@ -251,12 +251,6 @@ const ArchonCrystalUpgradeSchema = new Schema<IArchonCrystalUpgrade>(
 | 
			
		||||
    { _id: false }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
ArchonCrystalUpgradeSchema.set("toJSON", {
 | 
			
		||||
    transform(_document, returnedObject) {
 | 
			
		||||
        delete returnedObject.__v;
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const boosterSchema = new Schema<IBooster>(
 | 
			
		||||
    {
 | 
			
		||||
        ExpiryDate: Number,
 | 
			
		||||
@ -1079,6 +1073,11 @@ EquipmentSchema.set("toJSON", {
 | 
			
		||||
        if (db.UmbraDate) {
 | 
			
		||||
            client.UmbraDate = toMongoDate(db.UmbraDate);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (client.ArchonCrystalUpgrades) {
 | 
			
		||||
            // For some reason, mongoose turns empty objects here into nulls, so we have to fix it.
 | 
			
		||||
            client.ArchonCrystalUpgrades = client.ArchonCrystalUpgrades.map(x => (x as unknown) ?? {});
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -132,6 +132,7 @@ import { setPlacedDecoInfoController } from "@/src/controllers/api/setPlacedDeco
 | 
			
		||||
import { setShipCustomizationsController } from "@/src/controllers/api/setShipCustomizationsController";
 | 
			
		||||
import { setShipFavouriteLoadoutController } from "@/src/controllers/api/setShipFavouriteLoadoutController";
 | 
			
		||||
import { setShipVignetteController } from "@/src/controllers/api/setShipVignetteController";
 | 
			
		||||
import { setSuitInfectionController } from "@/src/controllers/api/setSuitInfectionController";
 | 
			
		||||
import { setSupportedSyndicateController } from "@/src/controllers/api/setSupportedSyndicateController";
 | 
			
		||||
import { setWeaponSkillTreeController } from "@/src/controllers/api/setWeaponSkillTreeController";
 | 
			
		||||
import { shipDecorationsController } from "@/src/controllers/api/shipDecorationsController";
 | 
			
		||||
@ -147,6 +148,7 @@ import { syndicateStandingBonusController } from "@/src/controllers/api/syndicat
 | 
			
		||||
import { tauntHistoryController } from "@/src/controllers/api/tauntHistoryController";
 | 
			
		||||
import { tradingController } from "@/src/controllers/api/tradingController";
 | 
			
		||||
import { trainingResultController } from "@/src/controllers/api/trainingResultController";
 | 
			
		||||
import { umbraController } from "@/src/controllers/api/umbraController";
 | 
			
		||||
import { unlockShipFeatureController } from "@/src/controllers/api/unlockShipFeatureController";
 | 
			
		||||
import { updateAlignmentController } from "@/src/controllers/api/updateAlignmentController";
 | 
			
		||||
import { updateChallengeProgressController } from "@/src/controllers/api/updateChallengeProgressController";
 | 
			
		||||
@ -317,6 +319,7 @@ apiRouter.post("/setPlacedDecoInfo.php", setPlacedDecoInfoController);
 | 
			
		||||
apiRouter.post("/setShipCustomizations.php", setShipCustomizationsController);
 | 
			
		||||
apiRouter.post("/setShipFavouriteLoadout.php", setShipFavouriteLoadoutController);
 | 
			
		||||
apiRouter.post("/setShipVignette.php", setShipVignetteController);
 | 
			
		||||
apiRouter.post("/setSuitInfection.php", setSuitInfectionController);
 | 
			
		||||
apiRouter.post("/setWeaponSkillTree.php", setWeaponSkillTreeController);
 | 
			
		||||
apiRouter.post("/shipDecorations.php", shipDecorationsController);
 | 
			
		||||
apiRouter.post("/startCollectibleEntry.php", startCollectibleEntryController);
 | 
			
		||||
@ -327,6 +330,7 @@ apiRouter.post("/syndicateSacrifice.php", syndicateSacrificeController);
 | 
			
		||||
apiRouter.post("/syndicateStandingBonus.php", syndicateStandingBonusController);
 | 
			
		||||
apiRouter.post("/tauntHistory.php", tauntHistoryController);
 | 
			
		||||
apiRouter.post("/trainingResult.php", trainingResultController);
 | 
			
		||||
apiRouter.post("/umbra.php", umbraController);
 | 
			
		||||
apiRouter.post("/unlockShipFeature.php", unlockShipFeatureController);
 | 
			
		||||
apiRouter.post("/updateAlignment.php", updateAlignmentController);
 | 
			
		||||
apiRouter.post("/updateChallengeProgress.php", updateChallengeProgressController);
 | 
			
		||||
 | 
			
		||||
@ -1580,7 +1580,7 @@ export const addMiscItem = (
 | 
			
		||||
    inventory: TInventoryDatabaseDocument,
 | 
			
		||||
    type: string,
 | 
			
		||||
    count: number,
 | 
			
		||||
    inventoryChanges: IInventoryChanges
 | 
			
		||||
    inventoryChanges: IInventoryChanges = {}
 | 
			
		||||
): void => {
 | 
			
		||||
    const miscItemChanges: IMiscItem[] = [
 | 
			
		||||
        {
 | 
			
		||||
@ -1731,12 +1731,27 @@ export const addFocusXpIncreases = (inventory: TInventoryDatabaseDocument, focus
 | 
			
		||||
        AP_ANY
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    inventory.FocusXP ??= { AP_ATTACK: 0, AP_DEFENSE: 0, AP_TACTIC: 0, AP_POWER: 0, AP_WARD: 0 };
 | 
			
		||||
    inventory.FocusXP.AP_ATTACK += focusXpPlus[FocusType.AP_ATTACK];
 | 
			
		||||
    inventory.FocusXP.AP_DEFENSE += focusXpPlus[FocusType.AP_DEFENSE];
 | 
			
		||||
    inventory.FocusXP.AP_TACTIC += focusXpPlus[FocusType.AP_TACTIC];
 | 
			
		||||
    inventory.FocusXP.AP_POWER += focusXpPlus[FocusType.AP_POWER];
 | 
			
		||||
    inventory.FocusXP.AP_WARD += focusXpPlus[FocusType.AP_WARD];
 | 
			
		||||
    inventory.FocusXP ??= {};
 | 
			
		||||
    if (focusXpPlus[FocusType.AP_ATTACK]) {
 | 
			
		||||
        inventory.FocusXP.AP_ATTACK ??= 0;
 | 
			
		||||
        inventory.FocusXP.AP_ATTACK += focusXpPlus[FocusType.AP_ATTACK];
 | 
			
		||||
    }
 | 
			
		||||
    if (focusXpPlus[FocusType.AP_DEFENSE]) {
 | 
			
		||||
        inventory.FocusXP.AP_DEFENSE ??= 0;
 | 
			
		||||
        inventory.FocusXP.AP_DEFENSE += focusXpPlus[FocusType.AP_DEFENSE];
 | 
			
		||||
    }
 | 
			
		||||
    if (focusXpPlus[FocusType.AP_TACTIC]) {
 | 
			
		||||
        inventory.FocusXP.AP_TACTIC ??= 0;
 | 
			
		||||
        inventory.FocusXP.AP_TACTIC += focusXpPlus[FocusType.AP_TACTIC];
 | 
			
		||||
    }
 | 
			
		||||
    if (focusXpPlus[FocusType.AP_POWER]) {
 | 
			
		||||
        inventory.FocusXP.AP_POWER ??= 0;
 | 
			
		||||
        inventory.FocusXP.AP_POWER += focusXpPlus[FocusType.AP_POWER];
 | 
			
		||||
    }
 | 
			
		||||
    if (focusXpPlus[FocusType.AP_WARD]) {
 | 
			
		||||
        inventory.FocusXP.AP_WARD ??= 0;
 | 
			
		||||
        inventory.FocusXP.AP_WARD += focusXpPlus[FocusType.AP_WARD];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!config.noDailyFocusLimit) {
 | 
			
		||||
        inventory.DailyFocus -= focusXpPlus.reduce((a, b) => a + b, 0);
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,6 @@ import DeimosPetVendorManifest from "@/static/fixed_responses/getVendorInfo/Deim
 | 
			
		||||
import DuviriAcrithisVendorManifest from "@/static/fixed_responses/getVendorInfo/DuviriAcrithisVendorManifest.json";
 | 
			
		||||
import EntratiLabsEntratiLabsCommisionsManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabsCommisionsManifest.json";
 | 
			
		||||
import EntratiLabsEntratiLabVendorManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabVendorManifest.json";
 | 
			
		||||
import HubsRailjackCrewMemberVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json";
 | 
			
		||||
import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSalesmanManifest.json";
 | 
			
		||||
import Nova1999ConquestShopManifest from "@/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json";
 | 
			
		||||
import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json";
 | 
			
		||||
@ -42,7 +41,6 @@ const rawVendorManifests: IVendorManifest[] = [
 | 
			
		||||
    DuviriAcrithisVendorManifest,
 | 
			
		||||
    EntratiLabsEntratiLabsCommisionsManifest,
 | 
			
		||||
    EntratiLabsEntratiLabVendorManifest,
 | 
			
		||||
    HubsRailjackCrewMemberVendorManifest,
 | 
			
		||||
    MaskSalesmanManifest,
 | 
			
		||||
    Nova1999ConquestShopManifest,
 | 
			
		||||
    OstronPetVendorManifest,
 | 
			
		||||
@ -273,20 +271,39 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
 | 
			
		||||
        const offersToAdd: IVendorOffer[] = [];
 | 
			
		||||
        if (!manifest.isOneBinPerCycle) {
 | 
			
		||||
            const remainingItemCapacity: Record<string, number> = {};
 | 
			
		||||
            const missingItemsPerBin: Record<number, number> = {};
 | 
			
		||||
            let numOffersThatNeedToMatchABin = 0;
 | 
			
		||||
            if (manifest.numItemsPerBin) {
 | 
			
		||||
                for (let bin = 0; bin != manifest.numItemsPerBin.length; ++bin) {
 | 
			
		||||
                    missingItemsPerBin[bin] = manifest.numItemsPerBin[bin];
 | 
			
		||||
                    numOffersThatNeedToMatchABin += manifest.numItemsPerBin[bin];
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            for (const item of manifest.items) {
 | 
			
		||||
                remainingItemCapacity[item.storeItem] = 1 + item.duplicates;
 | 
			
		||||
            }
 | 
			
		||||
            for (const offer of info.ItemManifest) {
 | 
			
		||||
                remainingItemCapacity[offer.StoreItem] -= 1;
 | 
			
		||||
                const bin = parseInt(offer.Bin.substring(4));
 | 
			
		||||
                if (missingItemsPerBin[bin]) {
 | 
			
		||||
                    missingItemsPerBin[bin] -= 1;
 | 
			
		||||
                    numOffersThatNeedToMatchABin -= 1;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if (manifest.numItems && manifest.items.length != manifest.numItems.minValue) {
 | 
			
		||||
                const numItemsTarget = rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue);
 | 
			
		||||
                while (info.ItemManifest.length + offersToAdd.length < numItemsTarget) {
 | 
			
		||||
                    // TODO: Consider per-bin item limits
 | 
			
		||||
                    // TODO: Consider item probability weightings
 | 
			
		||||
                    const item = rng.randomElement(manifest.items)!;
 | 
			
		||||
                    if (remainingItemCapacity[item.storeItem] != 0) {
 | 
			
		||||
                    if (
 | 
			
		||||
                        remainingItemCapacity[item.storeItem] != 0 &&
 | 
			
		||||
                        (numOffersThatNeedToMatchABin == 0 || missingItemsPerBin[item.bin])
 | 
			
		||||
                    ) {
 | 
			
		||||
                        remainingItemCapacity[item.storeItem] -= 1;
 | 
			
		||||
                        if (missingItemsPerBin[item.bin]) {
 | 
			
		||||
                            missingItemsPerBin[item.bin] -= 1;
 | 
			
		||||
                            numOffersThatNeedToMatchABin -= 1;
 | 
			
		||||
                        }
 | 
			
		||||
                        offersToAdd.push(item);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
@ -383,6 +400,12 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
 | 
			
		||||
            info.ItemManifest.push(item);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        info.ItemManifest.sort((a, b) => {
 | 
			
		||||
            const aBin = parseInt(a.Bin.substring(4));
 | 
			
		||||
            const bBin = parseInt(b.Bin.substring(4));
 | 
			
		||||
            return aBin == bBin ? 0 : aBin < bBin ? +1 : -1;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Update vendor expiry
 | 
			
		||||
        let soonestOfferExpiry: number = Number.MAX_SAFE_INTEGER;
 | 
			
		||||
        for (const offer of info.ItemManifest) {
 | 
			
		||||
@ -424,4 +447,17 @@ if (isDev) {
 | 
			
		||||
    ) {
 | 
			
		||||
        logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const cms = getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Hubs/RailjackCrewMemberVendorManifest")!
 | 
			
		||||
        .VendorInfo.ItemManifest;
 | 
			
		||||
    if (
 | 
			
		||||
        cms.length != 9 ||
 | 
			
		||||
        cms[0].Bin != "BIN_2" ||
 | 
			
		||||
        cms[8].Bin != "BIN_0" ||
 | 
			
		||||
        cms.reduce((a, x) => a + (x.Bin == "BIN_2" ? 1 : 0), 0) < 2 ||
 | 
			
		||||
        cms.reduce((a, x) => a + (x.Bin == "BIN_1" ? 1 : 0), 0) < 2 ||
 | 
			
		||||
        cms.reduce((a, x) => a + (x.Bin == "BIN_0" ? 1 : 0), 0) < 4
 | 
			
		||||
    ) {
 | 
			
		||||
        logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/RailjackCrewMemberVendorManifest`);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@ import {
 | 
			
		||||
    ISortie,
 | 
			
		||||
    ISortieMission,
 | 
			
		||||
    ISyndicateMissionInfo,
 | 
			
		||||
    IVoidStorm,
 | 
			
		||||
    IWorldState
 | 
			
		||||
} from "../types/worldStateTypes";
 | 
			
		||||
import { version_compare } from "../helpers/inventoryHelpers";
 | 
			
		||||
@ -963,6 +964,61 @@ const getCalendarSeason = (week: number): ICalendarSeason => {
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Not very faithful, but to avoid the same node coming up back-to-back (which is not valid), I've split these into 2 arrays which we're alternating between.
 | 
			
		||||
 | 
			
		||||
const voidStormMissionsA = {
 | 
			
		||||
    VoidT1: ["CrewBattleNode519", "CrewBattleNode518", "CrewBattleNode515", "CrewBattleNode503"],
 | 
			
		||||
    VoidT2: ["CrewBattleNode501", "CrewBattleNode534", "CrewBattleNode530"],
 | 
			
		||||
    VoidT3: ["CrewBattleNode521", "CrewBattleNode516"],
 | 
			
		||||
    VoidT4: [
 | 
			
		||||
        "CrewBattleNode555",
 | 
			
		||||
        "CrewBattleNode553",
 | 
			
		||||
        "CrewBattleNode554",
 | 
			
		||||
        "CrewBattleNode539",
 | 
			
		||||
        "CrewBattleNode531",
 | 
			
		||||
        "CrewBattleNode527"
 | 
			
		||||
    ]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const voidStormMissionsB = {
 | 
			
		||||
    VoidT1: ["CrewBattleNode509", "CrewBattleNode522", "CrewBattleNode511", "CrewBattleNode512"],
 | 
			
		||||
    VoidT2: ["CrewBattleNode535", "CrewBattleNode533"],
 | 
			
		||||
    VoidT3: ["CrewBattleNode524", "CrewBattleNode525"],
 | 
			
		||||
    VoidT4: [
 | 
			
		||||
        "CrewBattleNode542",
 | 
			
		||||
        "CrewBattleNode538",
 | 
			
		||||
        "CrewBattleNode543",
 | 
			
		||||
        "CrewBattleNode536",
 | 
			
		||||
        "CrewBattleNode550",
 | 
			
		||||
        "CrewBattleNode529"
 | 
			
		||||
    ]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const pushVoidStorms = (arr: IVoidStorm[], hour: number): void => {
 | 
			
		||||
    const activation = hour * unixTimesInMs.hour + 40 * unixTimesInMs.minute;
 | 
			
		||||
    const expiry = activation + 90 * unixTimesInMs.minute;
 | 
			
		||||
    let accum = 0;
 | 
			
		||||
    const rng = new SRng(new SRng(hour).randomInt(0, 100_000));
 | 
			
		||||
    const voidStormMissions = structuredClone(hour & 1 ? voidStormMissionsA : voidStormMissionsB);
 | 
			
		||||
    for (const tier of ["VoidT1", "VoidT1", "VoidT2", "VoidT3", "VoidT4", "VoidT4"] as const) {
 | 
			
		||||
        const idx = rng.randomInt(0, voidStormMissions[tier].length - 1);
 | 
			
		||||
        const node = voidStormMissions[tier][idx];
 | 
			
		||||
        voidStormMissions[tier].splice(idx, 1);
 | 
			
		||||
        arr.push({
 | 
			
		||||
            _id: {
 | 
			
		||||
                $oid:
 | 
			
		||||
                    ((activation / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
 | 
			
		||||
                    "0321e89b" +
 | 
			
		||||
                    (accum++).toString().padStart(8, "0")
 | 
			
		||||
            },
 | 
			
		||||
            Node: node,
 | 
			
		||||
            Activation: { $date: { $numberLong: activation.toString() } },
 | 
			
		||||
            Expiry: { $date: { $numberLong: expiry.toString() } },
 | 
			
		||||
            ActiveMissionTier: tier
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const doesTimeSatsifyConstraints = (timeSecs: number): boolean => {
 | 
			
		||||
    if (config.worldState?.eidolonOverride) {
 | 
			
		||||
        const eidolonEpoch = 1391992660;
 | 
			
		||||
@ -1032,6 +1088,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
			
		||||
        Sorties: [],
 | 
			
		||||
        LiteSorties: [],
 | 
			
		||||
        GlobalUpgrades: [],
 | 
			
		||||
        VoidStorms: [],
 | 
			
		||||
        EndlessXpChoices: [],
 | 
			
		||||
        KnownCalendarSeasons: [],
 | 
			
		||||
        ...staticWorldState,
 | 
			
		||||
@ -1228,6 +1285,18 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
			
		||||
        worldState.KnownCalendarSeasons.push(getCalendarSeason(week + 1));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Void Storms
 | 
			
		||||
    const hour = Math.trunc(timeMs / unixTimesInMs.hour);
 | 
			
		||||
    const overLastHourStormExpiry = hour * unixTimesInMs.hour + 10 * unixTimesInMs.minute;
 | 
			
		||||
    const thisHourStormActivation = hour * unixTimesInMs.hour + 40 * unixTimesInMs.minute;
 | 
			
		||||
    if (overLastHourStormExpiry > timeMs) {
 | 
			
		||||
        pushVoidStorms(worldState.VoidStorms, hour - 2);
 | 
			
		||||
    }
 | 
			
		||||
    pushVoidStorms(worldState.VoidStorms, hour - 1);
 | 
			
		||||
    if (isBeforeNextExpectedWorldStateRefresh(timeMs, thisHourStormActivation)) {
 | 
			
		||||
        pushVoidStorms(worldState.VoidStorms, hour);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Sentient Anomaly cycling every 30 minutes
 | 
			
		||||
    const halfHour = Math.trunc(timeMs / (unixTimesInMs.hour / 2));
 | 
			
		||||
    const tmp = {
 | 
			
		||||
 | 
			
		||||
@ -641,11 +641,11 @@ export interface IFocusUpgrade {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IFocusXP {
 | 
			
		||||
    AP_POWER: number;
 | 
			
		||||
    AP_TACTIC: number;
 | 
			
		||||
    AP_DEFENSE: number;
 | 
			
		||||
    AP_ATTACK: number;
 | 
			
		||||
    AP_WARD: number;
 | 
			
		||||
    AP_POWER?: number;
 | 
			
		||||
    AP_TACTIC?: number;
 | 
			
		||||
    AP_DEFENSE?: number;
 | 
			
		||||
    AP_ATTACK?: number;
 | 
			
		||||
    AP_WARD?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type TFocusPolarity = keyof IFocusXP;
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@ export interface IWorldState {
 | 
			
		||||
    GlobalUpgrades: IGlobalUpgrade[];
 | 
			
		||||
    ActiveMissions: IFissure[];
 | 
			
		||||
    NodeOverrides: INodeOverride[];
 | 
			
		||||
    VoidStorms: IVoidStorm[];
 | 
			
		||||
    PVPChallengeInstances: IPVPChallengeInstance[];
 | 
			
		||||
    EndlessXpChoices: IEndlessXpChoice[];
 | 
			
		||||
    SeasonInfo?: {
 | 
			
		||||
@ -131,6 +132,14 @@ export interface ILiteSortie {
 | 
			
		||||
    }[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IVoidStorm {
 | 
			
		||||
    _id: IOid;
 | 
			
		||||
    Node: string;
 | 
			
		||||
    Activation: IMongoDate;
 | 
			
		||||
    Expiry: IMongoDate;
 | 
			
		||||
    ActiveMissionTier: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IPVPChallengeInstance {
 | 
			
		||||
    _id: IOid;
 | 
			
		||||
    challengeTypeRefID: string;
 | 
			
		||||
 | 
			
		||||
@ -1,244 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "VendorInfo": {
 | 
			
		||||
    "_id": {
 | 
			
		||||
      "$oid": "5fb70313c96976e97d6be787"
 | 
			
		||||
    },
 | 
			
		||||
    "TypeName": "/Lotus/Types/Game/VendorManifests/Hubs/RailjackCrewMemberVendorManifest",
 | 
			
		||||
    "ItemManifest": [
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/SteelMeridianCrewMemberGeneratorStrong",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/RailjackMiscItems/IsosRailjackItem",
 | 
			
		||||
            "ItemCount": 2220,
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "RegularPrice": [2180000, 2180000],
 | 
			
		||||
        "Bin": "BIN_2",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "9999999000000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "PurchaseQuantityLimit": 1,
 | 
			
		||||
        "Affiliation": "SteelMeridianSyndicate",
 | 
			
		||||
        "MinAffiliationRank": 0,
 | 
			
		||||
        "ReductionPerPositiveRank": 0.1,
 | 
			
		||||
        "IncreasePerNegativeRank": 0.5,
 | 
			
		||||
        "AllowMultipurchase": false,
 | 
			
		||||
        "LocTagRandSeed": 4185144421,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "670daf92d21f34757a5e73da"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/NewLokaCrewMemberGeneratorStrong",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/RailjackMiscItems/IsosRailjackItem",
 | 
			
		||||
            "ItemCount": 2130,
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "RegularPrice": [1890000, 1890000],
 | 
			
		||||
        "Bin": "BIN_2",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "9999999000000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "PurchaseQuantityLimit": 1,
 | 
			
		||||
        "Affiliation": "NewLokaSyndicate",
 | 
			
		||||
        "MinAffiliationRank": 0,
 | 
			
		||||
        "ReductionPerPositiveRank": 0.1,
 | 
			
		||||
        "IncreasePerNegativeRank": 0.5,
 | 
			
		||||
        "AllowMultipurchase": false,
 | 
			
		||||
        "LocTagRandSeed": 496053258,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "670daf92d21f34757a5e73db"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/SteelMeridianCrewMemberGeneratorMediumVersionTwo",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/RailjackMiscItems/IsosRailjackItem",
 | 
			
		||||
            "ItemCount": 440,
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_1",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "9999999000000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "PurchaseQuantityLimit": 1,
 | 
			
		||||
        "Affiliation": "SteelMeridianSyndicate",
 | 
			
		||||
        "MinAffiliationRank": 0,
 | 
			
		||||
        "ReductionPerPositiveRank": 0.1,
 | 
			
		||||
        "IncreasePerNegativeRank": 0.5,
 | 
			
		||||
        "AllowMultipurchase": false,
 | 
			
		||||
        "LocTagRandSeed": 2078883475,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "670daf92d21f34757a5e73dc"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/NewLokaCrewMemberGeneratorMediumVersionTwo",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/RailjackMiscItems/AsteriteRailjackItem",
 | 
			
		||||
            "ItemCount": 730,
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_1",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "9999999000000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "PurchaseQuantityLimit": 1,
 | 
			
		||||
        "Affiliation": "NewLokaSyndicate",
 | 
			
		||||
        "MinAffiliationRank": 0,
 | 
			
		||||
        "ReductionPerPositiveRank": 0.1,
 | 
			
		||||
        "IncreasePerNegativeRank": 0.5,
 | 
			
		||||
        "AllowMultipurchase": false,
 | 
			
		||||
        "LocTagRandSeed": 3890380934,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "670daf92d21f34757a5e73dd"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/CephalonSudaCrewMemberGeneratorMediumVersionTwo",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/RailjackMiscItems/AsteriteRailjackItem",
 | 
			
		||||
            "ItemCount": 720,
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_1",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "9999999000000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "PurchaseQuantityLimit": 1,
 | 
			
		||||
        "Affiliation": "CephalonSudaSyndicate",
 | 
			
		||||
        "MinAffiliationRank": 0,
 | 
			
		||||
        "ReductionPerPositiveRank": 0.1,
 | 
			
		||||
        "IncreasePerNegativeRank": 0.5,
 | 
			
		||||
        "AllowMultipurchase": false,
 | 
			
		||||
        "LocTagRandSeed": 3425148044,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "670daf92d21f34757a5e73de"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/ArbitersCrewMemberGeneratorMediumVersionTwo",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/RailjackMiscItems/CubicsRailjackItem",
 | 
			
		||||
            "ItemCount": 6500,
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_1",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "9999999000000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "PurchaseQuantityLimit": 1,
 | 
			
		||||
        "Affiliation": "ArbitersSyndicate",
 | 
			
		||||
        "MinAffiliationRank": 0,
 | 
			
		||||
        "ReductionPerPositiveRank": 0.1,
 | 
			
		||||
        "IncreasePerNegativeRank": 0.5,
 | 
			
		||||
        "AllowMultipurchase": false,
 | 
			
		||||
        "LocTagRandSeed": 2472754512,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "670daf92d21f34757a5e73df"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/PerrinCrewMemberGeneratorVersionTwo",
 | 
			
		||||
        "RegularPrice": [105000, 105000],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "9999999000000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "PurchaseQuantityLimit": 1,
 | 
			
		||||
        "Affiliation": "PerrinSyndicate",
 | 
			
		||||
        "MinAffiliationRank": 0,
 | 
			
		||||
        "ReductionPerPositiveRank": 0.1,
 | 
			
		||||
        "IncreasePerNegativeRank": 0.5,
 | 
			
		||||
        "AllowMultipurchase": false,
 | 
			
		||||
        "LocTagRandSeed": 966238763,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "670daf92d21f34757a5e73e0"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/NewLokaCrewMemberGeneratorVersionTwo",
 | 
			
		||||
        "RegularPrice": [120000, 120000],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "9999999000000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "PurchaseQuantityLimit": 1,
 | 
			
		||||
        "Affiliation": "NewLokaSyndicate",
 | 
			
		||||
        "MinAffiliationRank": 0,
 | 
			
		||||
        "ReductionPerPositiveRank": 0.1,
 | 
			
		||||
        "IncreasePerNegativeRank": 0.5,
 | 
			
		||||
        "AllowMultipurchase": false,
 | 
			
		||||
        "LocTagRandSeed": 356717213,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "670daf92d21f34757a5e73e1"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/ArbitersCrewMemberGeneratorVersionTwo",
 | 
			
		||||
        "RegularPrice": [120000, 120000],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "9999999000000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "PurchaseQuantityLimit": 1,
 | 
			
		||||
        "Affiliation": "ArbitersSyndicate",
 | 
			
		||||
        "MinAffiliationRank": 0,
 | 
			
		||||
        "ReductionPerPositiveRank": 0.1,
 | 
			
		||||
        "IncreasePerNegativeRank": 0.5,
 | 
			
		||||
        "AllowMultipurchase": false,
 | 
			
		||||
        "LocTagRandSeed": 1969797050,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "670daf92d21f34757a5e73e2"
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "PropertyTextHash": "BE543CCC0A4F50A1D80CD2B523796EAE",
 | 
			
		||||
    "RandomSeedType": "VRST_FLAVOUR_TEXT",
 | 
			
		||||
    "Expiry": {
 | 
			
		||||
      "$date": {
 | 
			
		||||
        "$numberLong": "9999999000000"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -2562,50 +2562,6 @@
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "VoidStorms": [
 | 
			
		||||
    {
 | 
			
		||||
      "_id": { "$oid": "663a7581ced28e18f694b550" },
 | 
			
		||||
      "Node": "CrewBattleNode519",
 | 
			
		||||
      "Activation": { "$date": { "$numberLong": "1715109601821" } },
 | 
			
		||||
      "Expiry": { "$date": { "$numberLong": "2000000000000" } },
 | 
			
		||||
      "ActiveMissionTier": "VoidT1"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "_id": { "$oid": "663a7581ced28e18f694b551" },
 | 
			
		||||
      "Node": "CrewBattleNode515",
 | 
			
		||||
      "Activation": { "$date": { "$numberLong": "1715109601825" } },
 | 
			
		||||
      "Expiry": { "$date": { "$numberLong": "2000000000000" } },
 | 
			
		||||
      "ActiveMissionTier": "VoidT1"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "_id": { "$oid": "663a7581ced28e18f694b554" },
 | 
			
		||||
      "Node": "CrewBattleNode536",
 | 
			
		||||
      "Activation": { "$date": { "$numberLong": "1715109601832" } },
 | 
			
		||||
      "Expiry": { "$date": { "$numberLong": "2000000000000" } },
 | 
			
		||||
      "ActiveMissionTier": "VoidT4"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "_id": { "$oid": "663a7581ced28e18f694b555" },
 | 
			
		||||
      "Node": "CrewBattleNode539",
 | 
			
		||||
      "Activation": { "$date": { "$numberLong": "1715109601834" } },
 | 
			
		||||
      "Expiry": { "$date": { "$numberLong": "2000000000000" } },
 | 
			
		||||
      "ActiveMissionTier": "VoidT4"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "_id": { "$oid": "663a7581ced28e18f694b553" },
 | 
			
		||||
      "Node": "CrewBattleNode521",
 | 
			
		||||
      "Activation": { "$date": { "$numberLong": "1715109601829" } },
 | 
			
		||||
      "Expiry": { "$date": { "$numberLong": "2000000000000" } },
 | 
			
		||||
      "ActiveMissionTier": "VoidT3"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "_id": { "$oid": "663a7581ced28e18f694b552" },
 | 
			
		||||
      "Node": "CrewBattleNode535",
 | 
			
		||||
      "Activation": { "$date": { "$numberLong": "1715109601827" } },
 | 
			
		||||
      "Expiry": { "$date": { "$numberLong": "2000000000000" } },
 | 
			
		||||
      "ActiveMissionTier": "VoidT2"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "PrimeAccessAvailability": { "State": "PRIME1" },
 | 
			
		||||
  "PrimeVaultAvailabilities": [false, false, false, false, false],
 | 
			
		||||
  "PrimeTokenAvailability": true,
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,7 @@ function loginFromLocalStorage() {
 | 
			
		||||
        },
 | 
			
		||||
        () => {
 | 
			
		||||
            logout();
 | 
			
		||||
            alert(isRegister ? "Registration failed. Account already exists?" : "Login failed");
 | 
			
		||||
            alert(loc(isRegister ? "code_regFail" : "code_loginFail"));
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,8 @@ dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `Hinweis: Änderungen, die hier vorgenommen werden, werden erst im Spiel angewendet, sobald das Inventar synchronisiert wird. Die Sternenkarte zu besuchen, sollte der einfachste Weg sein, dies auszulösen.`,
 | 
			
		||||
    general_addButton: `Hinzufügen`,
 | 
			
		||||
    general_bulkActions: `Massenaktionen`,
 | 
			
		||||
    code_loginFail: `[UNTRANSLATED] Login failed. Double-check the email and password.`,
 | 
			
		||||
    code_regFail: `[UNTRANSLATED] Registration failed. Account already exists?`,
 | 
			
		||||
    code_nonValidAuthz: `Deine Anmeldedaten sind nicht mehr gültig.`,
 | 
			
		||||
    code_changeNameConfirm: `In welchen Namen möchtest du deinen Account umbenennen?`,
 | 
			
		||||
    code_deleteAccountConfirm: `Bist du sicher, dass du deinen Account |DISPLAYNAME| (|EMAIL|) löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.`,
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,8 @@ dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `Note: Changes made here will only be applied in-game when the game syncs the inventory. Visiting the navigation should be the easiest way to trigger that.`,
 | 
			
		||||
    general_addButton: `Add`,
 | 
			
		||||
    general_bulkActions: `Bulk Actions`,
 | 
			
		||||
    code_loginFail: `Login failed. Double-check the email and password.`,
 | 
			
		||||
    code_regFail: `Registration failed. Account already exists?`,
 | 
			
		||||
    code_nonValidAuthz: `Your credentials are no longer valid.`,
 | 
			
		||||
    code_changeNameConfirm: `What would you like to change your account name to?`,
 | 
			
		||||
    code_deleteAccountConfirm: `Are you sure you want to delete your account |DISPLAYNAME| (|EMAIL|)? This action cannot be undone.`,
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,8 @@ dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `Nota: Los cambios realizados aquí se reflejarán en el juego cuando este sincronice el inventario. Usar la navegación debería ser la forma más sencilla de activar esto.`,
 | 
			
		||||
    general_addButton: `Agregar`,
 | 
			
		||||
    general_bulkActions: `Acciones masivas`,
 | 
			
		||||
    code_loginFail: `[UNTRANSLATED] Login failed. Double-check the email and password.`,
 | 
			
		||||
    code_regFail: `[UNTRANSLATED] Registration failed. Account already exists?`,
 | 
			
		||||
    code_nonValidAuthz: `Tus credenciales no son válidas.`,
 | 
			
		||||
    code_changeNameConfirm: `¿Qué nombre te gustaría ponerle a tu cuenta?`,
 | 
			
		||||
    code_deleteAccountConfirm: `¿Estás seguro de que deseas eliminar tu cuenta |DISPLAYNAME| (|EMAIL|)? Esta acción es permanente.`,
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,8 @@ dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `Note : Les changements effectués ici seront appliqués lors de la syncrhonisation. Visiter la navigation appliquera les changements apportés à l'inventaire.`,
 | 
			
		||||
    general_addButton: `Ajouter`,
 | 
			
		||||
    general_bulkActions: `Action groupée`,
 | 
			
		||||
    code_loginFail: `[UNTRANSLATED] Login failed. Double-check the email and password.`,
 | 
			
		||||
    code_regFail: `[UNTRANSLATED] Registration failed. Account already exists?`,
 | 
			
		||||
    code_nonValidAuthz: `Informations de connexion invalides`,
 | 
			
		||||
    code_changeNameConfirm: `Nouveau nom du compte :`,
 | 
			
		||||
    code_deleteAccountConfirm: `Supprimer |DISPLAYNAME| (|EMAIL|) ? Cette action est irreversible.`,
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,8 @@ dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `Примечание: изменения, внесенные здесь, отобразятся в игре только после повторной загрузки вашего инвентаря. Посещение навигации — самый простой способ этого добиться.`,
 | 
			
		||||
    general_addButton: `Добавить`,
 | 
			
		||||
    general_bulkActions: `Массовые действия`,
 | 
			
		||||
    code_loginFail: `[UNTRANSLATED] Login failed. Double-check the email and password.`,
 | 
			
		||||
    code_regFail: `[UNTRANSLATED] Registration failed. Account already exists?`,
 | 
			
		||||
    code_nonValidAuthz: `Ваши данные больше не действительны.`,
 | 
			
		||||
    code_changeNameConfirm: `Какое имя вы хотите установить для своей учетной записи?`,
 | 
			
		||||
    code_deleteAccountConfirm: `Вы уверены, что хотите удалить аккаунт |DISPLAYNAME| (|EMAIL|)? Это действие нельзя отменить.`,
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,8 @@ dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `注意:此处所做的更改只有在游戏同步仓库后才会生效。您可以通过访问星图来触发仓库更新。`,
 | 
			
		||||
    general_addButton: `添加`,
 | 
			
		||||
    general_bulkActions: `批量操作`,
 | 
			
		||||
    code_loginFail: `登录失败。请检查邮箱和密码。`,
 | 
			
		||||
    code_regFail: `注册失败。账号已存在。`,
 | 
			
		||||
    code_nonValidAuthz: `您的登录凭证已失效。`,
 | 
			
		||||
    code_changeNameConfirm: `您想将账户名称更改为什么?`,
 | 
			
		||||
    code_deleteAccountConfirm: `确定要删除账户 |DISPLAYNAME| (|EMAIL|) 吗?此操作不可撤销。`,
 | 
			
		||||
@ -25,7 +27,7 @@ dict = {
 | 
			
		||||
    code_renamePrompt: `输入新的自定义名称:`,
 | 
			
		||||
    code_remove: `移除`,
 | 
			
		||||
    code_addItemsConfirm: `确定要向账户添加 |COUNT| 件物品吗?`,
 | 
			
		||||
    code_succRankUp: `[UNTRANSLATED] Successfully ranked up.`,
 | 
			
		||||
    code_succRankUp: `等级已提升`,
 | 
			
		||||
    code_noEquipmentToRankUp: `没有可升级的装备。`,
 | 
			
		||||
    code_succAdded: `已成功添加。`,
 | 
			
		||||
    code_succRemoved: `已成功移除。`,
 | 
			
		||||
@ -34,8 +36,8 @@ dict = {
 | 
			
		||||
    code_rerollsNumber: `洗卡次数`,
 | 
			
		||||
    code_viewStats: `查看属性`,
 | 
			
		||||
    code_rank: `等级`,
 | 
			
		||||
    code_rankUp: `[UNTRANSLATED] Rank up`,
 | 
			
		||||
    code_rankDown: `[UNTRANSLATED] Rank down`,
 | 
			
		||||
    code_rankUp: `等级提升`,
 | 
			
		||||
    code_rankDown: `等级下降`,
 | 
			
		||||
    code_count: `数量`,
 | 
			
		||||
    code_focusAllUnlocked: `所有专精学派均已解锁。`,
 | 
			
		||||
    code_focusUnlocked: `已解锁 |COUNT| 个新专精学派!需要游戏内仓库更新才能生效,您可以通过访问星图来触发仓库更新。`,
 | 
			
		||||
@ -176,56 +178,56 @@ dict = {
 | 
			
		||||
    import_importNote: `您可以在此处提供完整或部分库存响应(客户端表示)。支持的所有字段<b>将被覆盖</b>到您的账户中。`,
 | 
			
		||||
    import_submit: `提交`,
 | 
			
		||||
 | 
			
		||||
    upgrade_Equilibrium: `[UNTRANSLATED] +|VAL|% Energy from Health pickups, +|VAL|% Health from Energy pickups`,
 | 
			
		||||
    upgrade_MeleeCritDamage: `[UNTRANSLATED] +|VAL|% Melee Critical Damage`,
 | 
			
		||||
    upgrade_PrimaryStatusChance: `[UNTRANSLATED] +|VAL|% Primary Status Chance`,
 | 
			
		||||
    upgrade_SecondaryCritChance: `[UNTRANSLATED] +|VAL|% Secondary Critical Chance`,
 | 
			
		||||
    upgrade_WarframeAbilityDuration: `[UNTRANSLATED] +|VAL|% Ability Duration`,
 | 
			
		||||
    upgrade_WarframeAbilityStrength: `[UNTRANSLATED] +|VAL|% Ability Strength`,
 | 
			
		||||
    upgrade_WarframeArmourMax: `[UNTRANSLATED] +|VAL| Armor`,
 | 
			
		||||
    upgrade_WarframeBlastProc: `[UNTRANSLATED] +|VAL| Shields on inflicting Blast Status`,
 | 
			
		||||
    upgrade_WarframeCastingSpeed: `[UNTRANSLATED] +|VAL|% Casting Speed`,
 | 
			
		||||
    upgrade_WarframeCorrosiveDamageBoost: `[UNTRANSLATED] +|VAL|% Ability Damage on enemies affected by Corrosion Status`,
 | 
			
		||||
    upgrade_WarframeCorrosiveStack: `[UNTRANSLATED] Increase max stacks of Corrosion Status by +|VAL|`,
 | 
			
		||||
    upgrade_WarframeCritDamageBoost: `[UNTRANSLATED] +|VAL|% Melee Critical Damage (Doubles over 500 Energy)`,
 | 
			
		||||
    upgrade_WarframeElectricDamage: `[UNTRANSLATED] +|VAL1|% Primary Electricity Damage (+|VAL2|% per additional Shard)`,
 | 
			
		||||
    upgrade_WarframeElectricDamageBoost: `[UNTRANSLATED] +|VAL|% Ability Damage on enemies affected by Electricity Status`,
 | 
			
		||||
    upgrade_WarframeEnergyMax: `[UNTRANSLATED] +|VAL| Energy Max`,
 | 
			
		||||
    upgrade_WarframeGlobeEffectEnergy: `[UNTRANSLATED] +|VAL|% Energy Orb Effectiveness`,
 | 
			
		||||
    upgrade_WarframeGlobeEffectHealth: `[UNTRANSLATED] +|VAL|% Health Orb Effectiveness`,
 | 
			
		||||
    upgrade_WarframeHealthMax: `[UNTRANSLATED] +|VAL| Health`,
 | 
			
		||||
    upgrade_WarframeHPBoostFromImpact: `[UNTRANSLATED] +|VAL1| Health per enemy killed with Blast Damage (Max |VAL2| Health)`,
 | 
			
		||||
    upgrade_WarframeParkourVelocity: `[UNTRANSLATED] +|VAL|% Parkour Velocity`,
 | 
			
		||||
    upgrade_WarframeRadiationDamageBoost: `[UNTRANSLATED] +|VAL|% Ability Damage on enemies affected by Radiation Status`,
 | 
			
		||||
    upgrade_WarframeRegen: `[UNTRANSLATED] +|VAL| Health Regen/s`,
 | 
			
		||||
    upgrade_WarframeShieldMax: `[UNTRANSLATED] +|VAL| Shield`,
 | 
			
		||||
    upgrade_WarframeStartingEnergy: `[UNTRANSLATED] +|VAL|% Energy on Spawn`,
 | 
			
		||||
    upgrade_WarframeToxinDamage: `[UNTRANSLATED] +|VAL|% Toxin Status Effect Damage`,
 | 
			
		||||
    upgrade_WarframeToxinHeal: `[UNTRANSLATED] +|VAL| Health on damaging enemies with Toxin Status`,
 | 
			
		||||
    upgrade_WeaponCritBoostFromHeat: `[UNTRANSLATED] +|VAL1|% Secondary Critical Chance per Heat-affected enemy killed (Max |VAL2|%)`,
 | 
			
		||||
    upgrade_AvatarAbilityRange: `[UNTRANSLATED] +7.5% Ability Range`,
 | 
			
		||||
    upgrade_AvatarAbilityEfficiency: `[UNTRANSLATED] +5% Ability Efficiency`,
 | 
			
		||||
    upgrade_AvatarEnergyRegen: `[UNTRANSLATED] +0.5 Energy Regen/s`,
 | 
			
		||||
    upgrade_AvatarEnemyRadar: `[UNTRANSLATED] +5m Enemy Radar`,
 | 
			
		||||
    upgrade_AvatarLootRadar: `[UNTRANSLATED] +7m Loot Radar`,
 | 
			
		||||
    upgrade_WeaponAmmoMax: `[UNTRANSLATED] +15% Ammo Max`,
 | 
			
		||||
    upgrade_EnemyArmorReductionAura: `[UNTRANSLATED] -3% Enemy Armor`,
 | 
			
		||||
    upgrade_OnExecutionAmmo: `[UNTRANSLATED] 100% Primary and Secondary Magazine Refill on Mercy`,
 | 
			
		||||
    upgrade_OnExecutionHealthDrop: `[UNTRANSLATED] 100% chance to drop a Health Orb on Mercy`,
 | 
			
		||||
    upgrade_OnExecutionEnergyDrop: `[UNTRANSLATED] 50% chance to drop an Energy Orb on Mercy`,
 | 
			
		||||
    upgrade_OnFailHackReset: `[UNTRANSLATED] +50% to retry on Hacking failure`,
 | 
			
		||||
    upgrade_DamageReductionOnHack: `[UNTRANSLATED] 75% Damage Reduction while Hacking`,
 | 
			
		||||
    upgrade_OnExecutionReviveCompanion: `[UNTRANSLATED] Mercy Kills reduce Companion Recovery by 15s`,
 | 
			
		||||
    upgrade_OnExecutionParkourSpeed: `[UNTRANSLATED] +60% Parkour Speed after a Mercy for 15s`,
 | 
			
		||||
    upgrade_AvatarTimeLimitIncrease: `[UNTRANSLATED] s to Hacking`,
 | 
			
		||||
    upgrade_ElectrifyOnHack: `[UNTRANSLATED] Shock enemies within 20m while Hacking`,
 | 
			
		||||
    upgrade_OnExecutionTerrify: `[UNTRANSLATED] 50% chance for enemies within 15m to cower in fear for 8 seconds on Mercy`,
 | 
			
		||||
    upgrade_OnHackLockers: `[UNTRANSLATED] Unlock 5 lockers within 20m after Hacking`,
 | 
			
		||||
    upgrade_OnExecutionBlind: `[UNTRANSLATED] Blind enemies within 18m on Mercy`,
 | 
			
		||||
    upgrade_OnExecutionDrainPower: `[UNTRANSLATED] 100% chance for next ability cast to gain +50% Ability Strength on Mercy`,
 | 
			
		||||
    upgrade_OnHackSprintSpeed: `[UNTRANSLATED] +75% Sprint Speed for 15s after Hacking`,
 | 
			
		||||
    upgrade_SwiftExecute: `[UNTRANSLATED] Speed of Mercy Kills increased by 50%`,
 | 
			
		||||
    upgrade_OnHackInvis: `[UNTRANSLATED] Invisible for 15 seconds after hacking`,
 | 
			
		||||
    upgrade_Equilibrium: `+|VAL|% 能量 来自生命球, +|VAL|% 生命 来自能量球`,
 | 
			
		||||
    upgrade_MeleeCritDamage: `+|VAL|% 近战暴击伤害`,
 | 
			
		||||
    upgrade_PrimaryStatusChance: `+|VAL|% 主武器触发几率`,
 | 
			
		||||
    upgrade_SecondaryCritChance: `+|VAL|% 次要武器暴击几率`,
 | 
			
		||||
    upgrade_WarframeAbilityDuration: `+|VAL|% 技能持续时间`,
 | 
			
		||||
    upgrade_WarframeAbilityStrength: `+|VAL|% 技能强度`,
 | 
			
		||||
    upgrade_WarframeArmourMax: `+|VAL| 护甲`,
 | 
			
		||||
    upgrade_WarframeBlastProc: `施加爆炸状态时,护盾 +|VAL|`,
 | 
			
		||||
    upgrade_WarframeCastingSpeed: `+|VAL|% 施放速度`,
 | 
			
		||||
    upgrade_WarframeCorrosiveDamageBoost: `对受腐蚀状态影响的敌人 +|VAL|% 技能伤害`,
 | 
			
		||||
    upgrade_WarframeCorrosiveStack: `腐蚀状态最大堆叠数 +|VAL|`,
 | 
			
		||||
    upgrade_WarframeCritDamageBoost: `+|VAL|% 近战暴击伤害 (500能量以上翻倍)`,
 | 
			
		||||
    upgrade_WarframeElectricDamage: `+|VAL1|% 主武器伤害效果 (+|VAL2|% 每附加一个碎片)`,
 | 
			
		||||
    upgrade_WarframeElectricDamageBoost: `对受电能状态影响的敌人 +|VAL|% 技能伤害`,
 | 
			
		||||
    upgrade_WarframeEnergyMax: `+|VAL| 最大能量`,
 | 
			
		||||
    upgrade_WarframeGlobeEffectEnergy: `+|VAL|% 能量球效果`,
 | 
			
		||||
    upgrade_WarframeGlobeEffectHealth: `+|VAL|% 生命球效果`,
 | 
			
		||||
    upgrade_WarframeHealthMax: `+|VAL| 生命`,
 | 
			
		||||
    upgrade_WarframeHPBoostFromImpact: `每个被爆炸伤害击杀的敌人,补充 |VAL1|生命 (最大 |VAL2| 生命)`,
 | 
			
		||||
    upgrade_WarframeParkourVelocity: `+|VAL|% 跑酷速度`,
 | 
			
		||||
    upgrade_WarframeRadiationDamageBoost: `对受辐射状态影响的敌人 +|VAL|% 技能伤害`,
 | 
			
		||||
    upgrade_WarframeRegen: `+|VAL| 生命再生/s`,
 | 
			
		||||
    upgrade_WarframeShieldMax: `+|VAL| 护盾`,
 | 
			
		||||
    upgrade_WarframeStartingEnergy: `+|VAL|% 能量出生时`,
 | 
			
		||||
    upgrade_WarframeToxinDamage: `+|VAL|% 毒素伤害效果`,
 | 
			
		||||
    upgrade_WarframeToxinHeal: `+|VAL| 生命 对毒素状态的敌人造成伤害时`,
 | 
			
		||||
    upgrade_WeaponCritBoostFromHeat: `每个被火焰伤害杀死的敌人, 增加|VAL1|% 次要武器暴击几率 (最大 |VAL2|%)`,
 | 
			
		||||
    upgrade_AvatarAbilityRange: `+7.5% 技能范围`,
 | 
			
		||||
    upgrade_AvatarAbilityEfficiency: `+5% 技能效率`,
 | 
			
		||||
    upgrade_AvatarEnergyRegen: `+0.5 能量再生/秒`,
 | 
			
		||||
    upgrade_AvatarEnemyRadar: `+5米 敌方雷达`,
 | 
			
		||||
    upgrade_AvatarLootRadar: `+7米 战利品雷达`,
 | 
			
		||||
    upgrade_WeaponAmmoMax: `+15% 弹药最大容量`,
 | 
			
		||||
    upgrade_EnemyArmorReductionAura: `-3% 敌方护甲`,
 | 
			
		||||
    upgrade_OnExecutionAmmo: `怜悯之击 100% 补充主次要武器弹匣`,
 | 
			
		||||
    upgrade_OnExecutionHealthDrop: `怜悯之击 100% 几率 掉落生命球`,
 | 
			
		||||
    upgrade_OnExecutionEnergyDrop: `怜悯之击 50% 几率 掉落生命球`,
 | 
			
		||||
    upgrade_OnFailHackReset: `+50% 在入侵失败时重试`,
 | 
			
		||||
    upgrade_DamageReductionOnHack: `入侵时,+75% 伤害减免`,
 | 
			
		||||
    upgrade_OnExecutionReviveCompanion: `怜悯之击 减少同伴复苏时间 15秒`,
 | 
			
		||||
    upgrade_OnExecutionParkourSpeed: `怜悯之击 15秒内 +60% 跑酷速度`,
 | 
			
		||||
    upgrade_AvatarTimeLimitIncrease: `增加入侵限制时间`,
 | 
			
		||||
    upgrade_ElectrifyOnHack: `入侵时震慑20米之内的敌人`,
 | 
			
		||||
    upgrade_OnExecutionTerrify: `怜悯之击 50% 几率让 15米 以内的敌人恐慌`,
 | 
			
		||||
    upgrade_OnHackLockers: `入侵后解锁20米内的5个储物柜`,
 | 
			
		||||
    upgrade_OnExecutionBlind: `怜悯之击 致盲18米之内的敌人`,
 | 
			
		||||
    upgrade_OnExecutionDrainPower: `怜悯之击会使下一个技能有100%的机会获得+50%的技能强度`,
 | 
			
		||||
    upgrade_OnHackSprintSpeed: `入侵后+75%冲刺速度,持续15秒`,
 | 
			
		||||
    upgrade_SwiftExecute: `怜悯之击速度提升50%`,
 | 
			
		||||
    upgrade_OnHackInvis: `入侵后隐身15秒`,
 | 
			
		||||
 | 
			
		||||
    prettier_sucks_ass: ``
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user