merge upstream
This commit is contained in:
		
						commit
						c22c376828
					
				@ -14,7 +14,9 @@ import {
 | 
				
			|||||||
    addRecipes,
 | 
					    addRecipes,
 | 
				
			||||||
    occupySlot,
 | 
					    occupySlot,
 | 
				
			||||||
    combineInventoryChanges,
 | 
					    combineInventoryChanges,
 | 
				
			||||||
    addKubrowPetPrint
 | 
					    addKubrowPetPrint,
 | 
				
			||||||
 | 
					    addPowerSuit,
 | 
				
			||||||
 | 
					    addEquipment
 | 
				
			||||||
} from "@/src/services/inventoryService";
 | 
					} from "@/src/services/inventoryService";
 | 
				
			||||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
					import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
				
			||||||
import { InventorySlot, IPendingRecipeDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
					import { InventorySlot, IPendingRecipeDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
				
			||||||
@ -22,7 +24,7 @@ import { toOid2 } from "@/src/helpers/inventoryHelpers";
 | 
				
			|||||||
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
 | 
					import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
 | 
				
			||||||
import { IRecipe } from "warframe-public-export-plus";
 | 
					import { IRecipe } from "warframe-public-export-plus";
 | 
				
			||||||
import { config } from "@/src/services/configService";
 | 
					import { config } from "@/src/services/configService";
 | 
				
			||||||
import { IEquipmentClient, Status } from "@/src/types/equipmentTypes";
 | 
					import { EquipmentFeatures, IEquipmentClient, Status } from "@/src/types/equipmentTypes";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface IClaimCompletedRecipeRequest {
 | 
					interface IClaimCompletedRecipeRequest {
 | 
				
			||||||
    RecipeIds: IOid[];
 | 
					    RecipeIds: IOid[];
 | 
				
			||||||
@ -124,17 +126,122 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
 | 
				
			|||||||
            const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!;
 | 
					            const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!;
 | 
				
			||||||
            addKubrowPetPrint(inventory, pet, InventoryChanges);
 | 
					            addKubrowPetPrint(inventory, pet, InventoryChanges);
 | 
				
			||||||
        } else if (recipe.secretIngredientAction != "SIA_UNBRAND") {
 | 
					        } else if (recipe.secretIngredientAction != "SIA_UNBRAND") {
 | 
				
			||||||
            InventoryChanges = {
 | 
					            if (recipe.resultType == "/Lotus/Powersuits/Excalibur/ExcaliburUmbra") {
 | 
				
			||||||
                ...InventoryChanges,
 | 
					                // Quite the special case here...
 | 
				
			||||||
                ...(await addItem(
 | 
					                // We don't just get Umbra, but also Skiajati and Umbra Mods. Both items are max rank, potatoed, and with the mods are pre-installed.
 | 
				
			||||||
 | 
					                // Source: https://wiki.warframe.com/w/The_Sacrifice, https://wiki.warframe.com/w/Excalibur/Umbra, https://wiki.warframe.com/w/Skiajati
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const umbraModA = (
 | 
				
			||||||
 | 
					                    await addItem(
 | 
				
			||||||
 | 
					                        inventory,
 | 
				
			||||||
 | 
					                        "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModA",
 | 
				
			||||||
 | 
					                        1,
 | 
				
			||||||
 | 
					                        false,
 | 
				
			||||||
 | 
					                        undefined,
 | 
				
			||||||
 | 
					                        `{"lvl":5}`
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                ).Upgrades![0];
 | 
				
			||||||
 | 
					                const umbraModB = (
 | 
				
			||||||
 | 
					                    await addItem(
 | 
				
			||||||
 | 
					                        inventory,
 | 
				
			||||||
 | 
					                        "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModB",
 | 
				
			||||||
 | 
					                        1,
 | 
				
			||||||
 | 
					                        false,
 | 
				
			||||||
 | 
					                        undefined,
 | 
				
			||||||
 | 
					                        `{"lvl":5}`
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                ).Upgrades![0];
 | 
				
			||||||
 | 
					                const umbraModC = (
 | 
				
			||||||
 | 
					                    await addItem(
 | 
				
			||||||
 | 
					                        inventory,
 | 
				
			||||||
 | 
					                        "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModC",
 | 
				
			||||||
 | 
					                        1,
 | 
				
			||||||
 | 
					                        false,
 | 
				
			||||||
 | 
					                        undefined,
 | 
				
			||||||
 | 
					                        `{"lvl":5}`
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                ).Upgrades![0];
 | 
				
			||||||
 | 
					                const sacrificeModA = (
 | 
				
			||||||
 | 
					                    await addItem(
 | 
				
			||||||
 | 
					                        inventory,
 | 
				
			||||||
 | 
					                        "/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModA",
 | 
				
			||||||
 | 
					                        1,
 | 
				
			||||||
 | 
					                        false,
 | 
				
			||||||
 | 
					                        undefined,
 | 
				
			||||||
 | 
					                        `{"lvl":5}`
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                ).Upgrades![0];
 | 
				
			||||||
 | 
					                const sacrificeModB = (
 | 
				
			||||||
 | 
					                    await addItem(
 | 
				
			||||||
 | 
					                        inventory,
 | 
				
			||||||
 | 
					                        "/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModB",
 | 
				
			||||||
 | 
					                        1,
 | 
				
			||||||
 | 
					                        false,
 | 
				
			||||||
 | 
					                        undefined,
 | 
				
			||||||
 | 
					                        `{"lvl":5}`
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                ).Upgrades![0];
 | 
				
			||||||
 | 
					                InventoryChanges.Upgrades ??= [];
 | 
				
			||||||
 | 
					                InventoryChanges.Upgrades.push(umbraModA, umbraModB, umbraModC, sacrificeModA, sacrificeModB);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                await addPowerSuit(
 | 
				
			||||||
                    inventory,
 | 
					                    inventory,
 | 
				
			||||||
                    recipe.resultType,
 | 
					                    "/Lotus/Powersuits/Excalibur/ExcaliburUmbra",
 | 
				
			||||||
                    recipe.num,
 | 
					                    {
 | 
				
			||||||
                    false,
 | 
					                        Configs: [
 | 
				
			||||||
                    undefined,
 | 
					                            {
 | 
				
			||||||
                    pendingRecipe.TargetFingerprint
 | 
					                                Upgrades: [
 | 
				
			||||||
                ))
 | 
					                                    "",
 | 
				
			||||||
            };
 | 
					                                    "",
 | 
				
			||||||
 | 
					                                    "",
 | 
				
			||||||
 | 
					                                    "",
 | 
				
			||||||
 | 
					                                    "",
 | 
				
			||||||
 | 
					                                    umbraModA.ItemId.$oid,
 | 
				
			||||||
 | 
					                                    umbraModB.ItemId.$oid,
 | 
				
			||||||
 | 
					                                    umbraModC.ItemId.$oid
 | 
				
			||||||
 | 
					                                ]
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                        XP: 900_000,
 | 
				
			||||||
 | 
					                        Features: EquipmentFeatures.DOUBLE_CAPACITY
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    InventoryChanges
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                inventory.XPInfo.push({
 | 
				
			||||||
 | 
					                    ItemType: "/Lotus/Powersuits/Excalibur/ExcaliburUmbra",
 | 
				
			||||||
 | 
					                    XP: 900_000
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                addEquipment(
 | 
				
			||||||
 | 
					                    inventory,
 | 
				
			||||||
 | 
					                    "Melee",
 | 
				
			||||||
 | 
					                    "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana",
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        Configs: [
 | 
				
			||||||
 | 
					                            { Upgrades: ["", "", "", "", "", "", sacrificeModA.ItemId.$oid, sacrificeModB.ItemId.$oid] }
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                        XP: 450_000,
 | 
				
			||||||
 | 
					                        Features: EquipmentFeatures.DOUBLE_CAPACITY
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    InventoryChanges
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                inventory.XPInfo.push({
 | 
				
			||||||
 | 
					                    ItemType: "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana",
 | 
				
			||||||
 | 
					                    XP: 450_000
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                InventoryChanges = {
 | 
				
			||||||
 | 
					                    ...InventoryChanges,
 | 
				
			||||||
 | 
					                    ...(await addItem(
 | 
				
			||||||
 | 
					                        inventory,
 | 
				
			||||||
 | 
					                        recipe.resultType,
 | 
				
			||||||
 | 
					                        recipe.num,
 | 
				
			||||||
 | 
					                        false,
 | 
				
			||||||
 | 
					                        undefined,
 | 
				
			||||||
 | 
					                        pendingRecipe.TargetFingerprint
 | 
				
			||||||
 | 
					                    ))
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (
 | 
					        if (
 | 
				
			||||||
            config.claimingBlueprintRefundsIngredients &&
 | 
					            config.claimingBlueprintRefundsIngredients &&
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
				
			|||||||
import { getInventory, updateCurrency, updateSlots } from "@/src/services/inventoryService";
 | 
					import { getInventory, updateCurrency, updateSlots } from "@/src/services/inventoryService";
 | 
				
			||||||
import { RequestHandler } from "express";
 | 
					import { RequestHandler } from "express";
 | 
				
			||||||
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
					import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
				
			||||||
import { logger } from "@/src/utils/logger";
 | 
					import { exhaustive } from "@/src/utils/ts-utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
    loadout slots are additionally purchased slots only
 | 
					    loadout slots are additionally purchased slots only
 | 
				
			||||||
@ -22,13 +22,44 @@ export const inventorySlotsController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
    const accountId = await getAccountIdForRequest(req);
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
    const body = JSON.parse(req.body as string) as IInventorySlotsRequest;
 | 
					    const body = JSON.parse(req.body as string) as IInventorySlotsRequest;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (body.Bin != InventorySlot.SUITS && body.Bin != InventorySlot.PVE_LOADOUTS) {
 | 
					    let price;
 | 
				
			||||||
        logger.warn(`unexpected slot purchase of type ${body.Bin}, account may be overcharged`);
 | 
					    let amount;
 | 
				
			||||||
 | 
					    switch (body.Bin) {
 | 
				
			||||||
 | 
					        case InventorySlot.SUITS:
 | 
				
			||||||
 | 
					        case InventorySlot.MECHSUITS:
 | 
				
			||||||
 | 
					        case InventorySlot.PVE_LOADOUTS:
 | 
				
			||||||
 | 
					        case InventorySlot.CREWMEMBERS:
 | 
				
			||||||
 | 
					            price = 20;
 | 
				
			||||||
 | 
					            amount = 1;
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case InventorySlot.SPACESUITS:
 | 
				
			||||||
 | 
					            price = 12;
 | 
				
			||||||
 | 
					            amount = 1;
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case InventorySlot.WEAPONS:
 | 
				
			||||||
 | 
					        case InventorySlot.SPACEWEAPONS:
 | 
				
			||||||
 | 
					        case InventorySlot.SENTINELS:
 | 
				
			||||||
 | 
					        case InventorySlot.RJ_COMPONENT_AND_ARMAMENTS:
 | 
				
			||||||
 | 
					        case InventorySlot.AMPS:
 | 
				
			||||||
 | 
					            price = 12;
 | 
				
			||||||
 | 
					            amount = 2;
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case InventorySlot.RIVENS:
 | 
				
			||||||
 | 
					            price = 60;
 | 
				
			||||||
 | 
					            amount = 3;
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        default:
 | 
				
			||||||
 | 
					            exhaustive(body.Bin);
 | 
				
			||||||
 | 
					            throw new Error(`unexpected slot purchase of type ${body.Bin as string}`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const inventory = await getInventory(accountId);
 | 
					    const inventory = await getInventory(accountId);
 | 
				
			||||||
    const currencyChanges = updateCurrency(inventory, 20, true);
 | 
					    const currencyChanges = updateCurrency(inventory, price, true);
 | 
				
			||||||
    updateSlots(inventory, body.Bin, 1, 1);
 | 
					    updateSlots(inventory, body.Bin, amount, amount);
 | 
				
			||||||
    await inventory.save();
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.json({ InventoryChanges: currencyChanges });
 | 
					    res.json({ InventoryChanges: currencyChanges });
 | 
				
			||||||
 | 
				
			|||||||
@ -1,25 +1,39 @@
 | 
				
			|||||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
					import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
				
			||||||
import { getInventory } from "@/src/services/inventoryService";
 | 
					import { addConsumables, getInventory } from "@/src/services/inventoryService";
 | 
				
			||||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
					import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
				
			||||||
import { IPlayerSkills } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
					import { IPlayerSkills } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
				
			||||||
 | 
					import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
				
			||||||
import { RequestHandler } from "express";
 | 
					import { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const playerSkillsController: RequestHandler = async (req, res) => {
 | 
					export const playerSkillsController: RequestHandler = async (req, res) => {
 | 
				
			||||||
    const accountId = await getAccountIdForRequest(req);
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
    const inventory = await getInventory(accountId, "PlayerSkills");
 | 
					    const inventory = await getInventory(accountId, "PlayerSkills Consumables");
 | 
				
			||||||
    const request = getJSONfromString<IPlayerSkillsRequest>(String(req.body));
 | 
					    const request = getJSONfromString<IPlayerSkillsRequest>(String(req.body));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const oldRank: number = inventory.PlayerSkills[request.Skill as keyof IPlayerSkills];
 | 
					    const oldRank: number = inventory.PlayerSkills[request.Skill as keyof IPlayerSkills];
 | 
				
			||||||
    const cost = (request.Pool == "LPP_DRIFTER" ? drifterCosts[oldRank] : 1 << oldRank) * 1000;
 | 
					    const cost = (request.Pool == "LPP_DRIFTER" ? drifterCosts[oldRank] : 1 << oldRank) * 1000;
 | 
				
			||||||
    inventory.PlayerSkills[request.Pool as keyof IPlayerSkills] -= cost;
 | 
					    inventory.PlayerSkills[request.Pool as keyof IPlayerSkills] -= cost;
 | 
				
			||||||
    inventory.PlayerSkills[request.Skill as keyof IPlayerSkills]++;
 | 
					    inventory.PlayerSkills[request.Skill as keyof IPlayerSkills]++;
 | 
				
			||||||
    await inventory.save();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const inventoryChanges: IInventoryChanges = {};
 | 
				
			||||||
 | 
					    if (request.Skill == "LPS_COMMAND" && inventory.PlayerSkills.LPS_COMMAND == 9) {
 | 
				
			||||||
 | 
					        const consumablesChanges = [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                ItemType: "/Lotus/Types/Restoratives/Consumable/CrewmateBall",
 | 
				
			||||||
 | 
					                ItemCount: 1
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					        addConsumables(inventory, consumablesChanges);
 | 
				
			||||||
 | 
					        inventoryChanges.Consumables = consumablesChanges;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
    res.json({
 | 
					    res.json({
 | 
				
			||||||
        Pool: request.Pool,
 | 
					        Pool: request.Pool,
 | 
				
			||||||
        PoolInc: -cost,
 | 
					        PoolInc: -cost,
 | 
				
			||||||
        Skill: request.Skill,
 | 
					        Skill: request.Skill,
 | 
				
			||||||
        Rank: oldRank + 1
 | 
					        Rank: oldRank + 1,
 | 
				
			||||||
 | 
					        InventoryChanges: inventoryChanges
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -23,10 +23,16 @@ export const crackRelic = async (
 | 
				
			|||||||
        weights = { COMMON: 0, UNCOMMON: 0, RARE: 1, LEGENDARY: 0 };
 | 
					        weights = { COMMON: 0, UNCOMMON: 0, RARE: 1, LEGENDARY: 0 };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    logger.debug(`opening a relic of quality ${relic.quality}; rarity weights are`, weights);
 | 
					    logger.debug(`opening a relic of quality ${relic.quality}; rarity weights are`, weights);
 | 
				
			||||||
    const reward = getRandomWeightedReward(
 | 
					    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
 | 
					        ExportRewards[relic.rewardManifest][0] as { type: string; itemCount: number; rarity: TRarity }[], // rarity is nullable in PE+ typings, but always present for relics
 | 
				
			||||||
        weights
 | 
					        weights
 | 
				
			||||||
    )!;
 | 
					    )!;
 | 
				
			||||||
 | 
					    if (config.relicRewardItemCountMultiplier !== undefined && (config.relicRewardItemCountMultiplier ?? 1) != 1) {
 | 
				
			||||||
 | 
					        reward = {
 | 
				
			||||||
 | 
					            ...reward,
 | 
				
			||||||
 | 
					            itemCount: reward.itemCount * config.relicRewardItemCountMultiplier
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    logger.debug(`relic rolled`, reward);
 | 
					    logger.debug(`relic rolled`, reward);
 | 
				
			||||||
    participant.Reward = reward.type;
 | 
					    participant.Reward = reward.type;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -43,13 +49,7 @@ export const crackRelic = async (
 | 
				
			|||||||
    // Give reward
 | 
					    // Give reward
 | 
				
			||||||
    combineInventoryChanges(
 | 
					    combineInventoryChanges(
 | 
				
			||||||
        inventoryChanges,
 | 
					        inventoryChanges,
 | 
				
			||||||
        (
 | 
					        (await handleStoreItemAcquisition(reward.type, inventory, reward.itemCount)).InventoryChanges
 | 
				
			||||||
            await handleStoreItemAcquisition(
 | 
					 | 
				
			||||||
                reward.type,
 | 
					 | 
				
			||||||
                inventory,
 | 
					 | 
				
			||||||
                reward.itemCount * (config.relicRewardItemCountMultiplier ?? 1)
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        ).InventoryChanges
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return reward;
 | 
					    return reward;
 | 
				
			||||||
 | 
				
			|||||||
@ -482,11 +482,14 @@ export const addItem = async (
 | 
				
			|||||||
            if (quantity != 1) {
 | 
					            if (quantity != 1) {
 | 
				
			||||||
                logger.warn(`adding 1 of ${typeName} ${targetFingerprint} even tho quantity ${quantity} was requested`);
 | 
					                logger.warn(`adding 1 of ${typeName} ${targetFingerprint} even tho quantity ${quantity} was requested`);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            inventory.Upgrades.push({
 | 
					            const upgrade =
 | 
				
			||||||
                ItemType: typeName,
 | 
					                inventory.Upgrades[
 | 
				
			||||||
                UpgradeFingerprint: targetFingerprint
 | 
					                    inventory.Upgrades.push({
 | 
				
			||||||
            });
 | 
					                        ItemType: typeName,
 | 
				
			||||||
            return {}; // there's not exactly a common "InventoryChanges" format for these
 | 
					                        UpgradeFingerprint: targetFingerprint
 | 
				
			||||||
 | 
					                    }) - 1
 | 
				
			||||||
 | 
					                ];
 | 
				
			||||||
 | 
					            return { Upgrades: [upgrade.toJSON<IUpgradeClient>()] };
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        const changes = [
 | 
					        const changes = [
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@ -812,7 +815,7 @@ export const addItem = async (
 | 
				
			|||||||
                        if (!seed) {
 | 
					                        if (!seed) {
 | 
				
			||||||
                            throw new Error(`Expected crew member to have a seed`);
 | 
					                            throw new Error(`Expected crew member to have a seed`);
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        seed |= 0x33b81en << 32n;
 | 
					                        seed |= BigInt(Math.trunc(inventory.Created.getTime() / 1000) & 0xffffff) << 32n;
 | 
				
			||||||
                        return {
 | 
					                        return {
 | 
				
			||||||
                            ...addCrewMember(inventory, typeName, seed),
 | 
					                            ...addCrewMember(inventory, typeName, seed),
 | 
				
			||||||
                            ...occupySlot(inventory, InventorySlot.CREWMEMBERS, premiumPurchase)
 | 
					                            ...occupySlot(inventory, InventorySlot.CREWMEMBERS, premiumPurchase)
 | 
				
			||||||
@ -1106,6 +1109,10 @@ export const addKubrowPet = (
 | 
				
			|||||||
            };
 | 
					            };
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            dominantTraits = createRandomTraits(kubrowPetName, traitsPool);
 | 
					            dominantTraits = createRandomTraits(kubrowPetName, traitsPool);
 | 
				
			||||||
 | 
					            if (kubrowPetName == "/Lotus/Types/Game/KubrowPet/ChargerKubrowPetPowerSuit") {
 | 
				
			||||||
 | 
					                dominantTraits.BodyType = "/Lotus/Types/Game/KubrowPet/BodyTypes/ChargerKubrowPetBodyType";
 | 
				
			||||||
 | 
					                dominantTraits.FurPattern = "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternInfested";
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const recessiveTraits: ITraits = createRandomTraits(
 | 
					        const recessiveTraits: ITraits = createRandomTraits(
 | 
				
			||||||
 | 
				
			|||||||
@ -236,7 +236,7 @@ const handleQuestCompletion = async (
 | 
				
			|||||||
        setupKahlSyndicate(inventory);
 | 
					        setupKahlSyndicate(inventory);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Whispers in the Walls is unlocked once The New + Heart of Deimos are completed.
 | 
					    // Whispers in the Walls is unlocked once The New War + Heart of Deimos are completed.
 | 
				
			||||||
    if (
 | 
					    if (
 | 
				
			||||||
        doesQuestCompletionFinishSet(inventory, questKey, [
 | 
					        doesQuestCompletionFinishSet(inventory, questKey, [
 | 
				
			||||||
            "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain",
 | 
					            "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain",
 | 
				
			||||||
 | 
				
			|||||||
@ -971,25 +971,26 @@ 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.
 | 
					// 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 = {
 | 
					const voidStormMissions = {
 | 
				
			||||||
    VoidT1: ["CrewBattleNode519", "CrewBattleNode518", "CrewBattleNode515", "CrewBattleNode503"],
 | 
					    VoidT1: [
 | 
				
			||||||
    VoidT2: ["CrewBattleNode501", "CrewBattleNode534", "CrewBattleNode530"],
 | 
					        "CrewBattleNode519",
 | 
				
			||||||
    VoidT3: ["CrewBattleNode521", "CrewBattleNode516"],
 | 
					        "CrewBattleNode518",
 | 
				
			||||||
 | 
					        "CrewBattleNode515",
 | 
				
			||||||
 | 
					        "CrewBattleNode503",
 | 
				
			||||||
 | 
					        "CrewBattleNode509",
 | 
				
			||||||
 | 
					        "CrewBattleNode522",
 | 
				
			||||||
 | 
					        "CrewBattleNode511",
 | 
				
			||||||
 | 
					        "CrewBattleNode512"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    VoidT2: ["CrewBattleNode501", "CrewBattleNode534", "CrewBattleNode530", "CrewBattleNode535", "CrewBattleNode533"],
 | 
				
			||||||
 | 
					    VoidT3: ["CrewBattleNode521", "CrewBattleNode516", "CrewBattleNode524", "CrewBattleNode525"],
 | 
				
			||||||
    VoidT4: [
 | 
					    VoidT4: [
 | 
				
			||||||
        "CrewBattleNode555",
 | 
					        "CrewBattleNode555",
 | 
				
			||||||
        "CrewBattleNode553",
 | 
					        "CrewBattleNode553",
 | 
				
			||||||
        "CrewBattleNode554",
 | 
					        "CrewBattleNode554",
 | 
				
			||||||
        "CrewBattleNode539",
 | 
					        "CrewBattleNode539",
 | 
				
			||||||
        "CrewBattleNode531",
 | 
					        "CrewBattleNode531",
 | 
				
			||||||
        "CrewBattleNode527"
 | 
					        "CrewBattleNode527",
 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const voidStormMissionsB = {
 | 
					 | 
				
			||||||
    VoidT1: ["CrewBattleNode509", "CrewBattleNode522", "CrewBattleNode511", "CrewBattleNode512"],
 | 
					 | 
				
			||||||
    VoidT2: ["CrewBattleNode535", "CrewBattleNode533"],
 | 
					 | 
				
			||||||
    VoidT3: ["CrewBattleNode524", "CrewBattleNode525"],
 | 
					 | 
				
			||||||
    VoidT4: [
 | 
					 | 
				
			||||||
        "CrewBattleNode542",
 | 
					        "CrewBattleNode542",
 | 
				
			||||||
        "CrewBattleNode538",
 | 
					        "CrewBattleNode538",
 | 
				
			||||||
        "CrewBattleNode543",
 | 
					        "CrewBattleNode543",
 | 
				
			||||||
@ -997,18 +998,21 @@ const voidStormMissionsB = {
 | 
				
			|||||||
        "CrewBattleNode550",
 | 
					        "CrewBattleNode550",
 | 
				
			||||||
        "CrewBattleNode529"
 | 
					        "CrewBattleNode529"
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
};
 | 
					} as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const voidStormLookbehind = {
 | 
				
			||||||
 | 
					    VoidT1: 3,
 | 
				
			||||||
 | 
					    VoidT2: 1,
 | 
				
			||||||
 | 
					    VoidT3: 1,
 | 
				
			||||||
 | 
					    VoidT4: 3
 | 
				
			||||||
 | 
					} as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const pushVoidStorms = (arr: IVoidStorm[], hour: number): void => {
 | 
					const pushVoidStorms = (arr: IVoidStorm[], hour: number): void => {
 | 
				
			||||||
    const activation = hour * unixTimesInMs.hour + 40 * unixTimesInMs.minute;
 | 
					    const activation = hour * unixTimesInMs.hour + 40 * unixTimesInMs.minute;
 | 
				
			||||||
    const expiry = activation + 90 * unixTimesInMs.minute;
 | 
					    const expiry = activation + 90 * unixTimesInMs.minute;
 | 
				
			||||||
    let accum = 0;
 | 
					    let accum = 0;
 | 
				
			||||||
    const rng = new SRng(new SRng(hour).randomInt(0, 100_000));
 | 
					    const tierIdx = { VoidT1: hour * 2, VoidT2: hour, VoidT3: hour, VoidT4: hour * 2 };
 | 
				
			||||||
    const voidStormMissions = structuredClone(hour & 1 ? voidStormMissionsA : voidStormMissionsB);
 | 
					 | 
				
			||||||
    for (const tier of ["VoidT1", "VoidT1", "VoidT2", "VoidT3", "VoidT4", "VoidT4"] as const) {
 | 
					    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({
 | 
					        arr.push({
 | 
				
			||||||
            _id: {
 | 
					            _id: {
 | 
				
			||||||
                $oid:
 | 
					                $oid:
 | 
				
			||||||
@ -1016,7 +1020,12 @@ const pushVoidStorms = (arr: IVoidStorm[], hour: number): void => {
 | 
				
			|||||||
                    "0321e89b" +
 | 
					                    "0321e89b" +
 | 
				
			||||||
                    (accum++).toString().padStart(8, "0")
 | 
					                    (accum++).toString().padStart(8, "0")
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            Node: node,
 | 
					            Node: sequentiallyUniqueRandomElement(
 | 
				
			||||||
 | 
					                voidStormMissions[tier],
 | 
				
			||||||
 | 
					                tierIdx[tier]++,
 | 
				
			||||||
 | 
					                voidStormLookbehind[tier],
 | 
				
			||||||
 | 
					                2051969264
 | 
				
			||||||
 | 
					            )!,
 | 
				
			||||||
            Activation: { $date: { $numberLong: activation.toString() } },
 | 
					            Activation: { $date: { $numberLong: activation.toString() } },
 | 
				
			||||||
            Expiry: { $date: { $numberLong: expiry.toString() } },
 | 
					            Expiry: { $date: { $numberLong: expiry.toString() } },
 | 
				
			||||||
            ActiveMissionTier: tier
 | 
					            ActiveMissionTier: tier
 | 
				
			||||||
@ -1024,75 +1033,124 @@ const pushVoidStorms = (arr: IVoidStorm[], hour: number): void => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const doesTimeSatsifyConstraints = (timeSecs: number): boolean => {
 | 
					interface ITimeConstraint {
 | 
				
			||||||
    if (config.worldState?.eidolonOverride) {
 | 
					    //name: string;
 | 
				
			||||||
 | 
					    isValidTime: (timeSecs: number) => boolean;
 | 
				
			||||||
 | 
					    getIdealTimeBefore: (timeSecs: number) => number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const eidolonDayConstraint: ITimeConstraint = {
 | 
				
			||||||
 | 
					    //name: "eidolon day",
 | 
				
			||||||
 | 
					    isValidTime: (timeSecs: number): boolean => {
 | 
				
			||||||
        const eidolonEpoch = 1391992660;
 | 
					        const eidolonEpoch = 1391992660;
 | 
				
			||||||
        const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / 9000);
 | 
					        const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / 9000);
 | 
				
			||||||
        const eidolonCycleStart = eidolonEpoch + eidolonCycle * 9000;
 | 
					        const eidolonCycleStart = eidolonEpoch + eidolonCycle * 9000;
 | 
				
			||||||
        const eidolonCycleEnd = eidolonCycleStart + 9000;
 | 
					        const eidolonCycleEnd = eidolonCycleStart + 9000;
 | 
				
			||||||
        const eidolonCycleNightStart = eidolonCycleEnd - 3000;
 | 
					        const eidolonCycleNightStart = eidolonCycleEnd - 3000;
 | 
				
			||||||
        if (config.worldState.eidolonOverride == "day") {
 | 
					        return !isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, eidolonCycleNightStart * 1000);
 | 
				
			||||||
            if (
 | 
					    },
 | 
				
			||||||
                //timeSecs < eidolonCycleStart ||
 | 
					    getIdealTimeBefore: (timeSecs: number): number => {
 | 
				
			||||||
                isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, eidolonCycleNightStart * 1000)
 | 
					        const eidolonEpoch = 1391992660;
 | 
				
			||||||
            ) {
 | 
					        const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / 9000);
 | 
				
			||||||
                return false;
 | 
					        const eidolonCycleStart = eidolonEpoch + eidolonCycle * 9000;
 | 
				
			||||||
            }
 | 
					        return eidolonCycleStart;
 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            if (
 | 
					 | 
				
			||||||
                timeSecs < eidolonCycleNightStart ||
 | 
					 | 
				
			||||||
                isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, eidolonCycleEnd * 1000)
 | 
					 | 
				
			||||||
            ) {
 | 
					 | 
				
			||||||
                return false;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (config.worldState?.vallisOverride) {
 | 
					const eidolonNightConstraint: ITimeConstraint = {
 | 
				
			||||||
 | 
					    //name: "eidolon night",
 | 
				
			||||||
 | 
					    isValidTime: (timeSecs: number): boolean => {
 | 
				
			||||||
 | 
					        const eidolonEpoch = 1391992660;
 | 
				
			||||||
 | 
					        const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / 9000);
 | 
				
			||||||
 | 
					        const eidolonCycleStart = eidolonEpoch + eidolonCycle * 9000;
 | 
				
			||||||
 | 
					        const eidolonCycleEnd = eidolonCycleStart + 9000;
 | 
				
			||||||
 | 
					        const eidolonCycleNightStart = eidolonCycleEnd - 3000;
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					            timeSecs >= eidolonCycleNightStart &&
 | 
				
			||||||
 | 
					            !isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, eidolonCycleEnd * 1000)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    getIdealTimeBefore: (timeSecs: number): number => {
 | 
				
			||||||
 | 
					        const eidolonEpoch = 1391992660;
 | 
				
			||||||
 | 
					        const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / 9000);
 | 
				
			||||||
 | 
					        const eidolonCycleStart = eidolonEpoch + eidolonCycle * 9000;
 | 
				
			||||||
 | 
					        const eidolonCycleEnd = eidolonCycleStart + 9000;
 | 
				
			||||||
 | 
					        const eidolonCycleNightStart = eidolonCycleEnd - 3000;
 | 
				
			||||||
 | 
					        if (eidolonCycleNightStart > timeSecs) {
 | 
				
			||||||
 | 
					            // Night hasn't started yet, but we need to return a time in the past.
 | 
				
			||||||
 | 
					            return eidolonCycleNightStart - 9000;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return eidolonCycleNightStart;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const venusColdConstraint: ITimeConstraint = {
 | 
				
			||||||
 | 
					    //name: "venus cold",
 | 
				
			||||||
 | 
					    isValidTime: (timeSecs: number): boolean => {
 | 
				
			||||||
        const vallisEpoch = 1541837628;
 | 
					        const vallisEpoch = 1541837628;
 | 
				
			||||||
        const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600);
 | 
					        const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600);
 | 
				
			||||||
        const vallisCycleStart = vallisEpoch + vallisCycle * 1600;
 | 
					        const vallisCycleStart = vallisEpoch + vallisCycle * 1600;
 | 
				
			||||||
        const vallisCycleEnd = vallisCycleStart + 1600;
 | 
					        const vallisCycleEnd = vallisCycleStart + 1600;
 | 
				
			||||||
        const vallisCycleColdStart = vallisCycleStart + 400;
 | 
					        const vallisCycleColdStart = vallisCycleStart + 400;
 | 
				
			||||||
        if (config.worldState.vallisOverride == "cold") {
 | 
					        return (
 | 
				
			||||||
            if (
 | 
					            timeSecs >= vallisCycleColdStart &&
 | 
				
			||||||
                timeSecs < vallisCycleColdStart ||
 | 
					            !isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, vallisCycleEnd * 1000)
 | 
				
			||||||
                isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, vallisCycleEnd * 1000)
 | 
					        );
 | 
				
			||||||
            ) {
 | 
					    },
 | 
				
			||||||
                return false;
 | 
					    getIdealTimeBefore: (timeSecs: number): number => {
 | 
				
			||||||
            }
 | 
					        const vallisEpoch = 1541837628;
 | 
				
			||||||
        } else {
 | 
					        const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600);
 | 
				
			||||||
            if (
 | 
					        const vallisCycleStart = vallisEpoch + vallisCycle * 1600;
 | 
				
			||||||
                //timeSecs < vallisCycleStart ||
 | 
					        const vallisCycleColdStart = vallisCycleStart + 400;
 | 
				
			||||||
                isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, vallisCycleColdStart * 1000)
 | 
					        if (vallisCycleColdStart > timeSecs) {
 | 
				
			||||||
            ) {
 | 
					            // Cold hasn't started yet, but we need to return a time in the past.
 | 
				
			||||||
                return false;
 | 
					            return vallisCycleColdStart - 1600;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return vallisCycleColdStart;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const venusWarmConstraint: ITimeConstraint = {
 | 
				
			||||||
 | 
					    //name: "venus warm",
 | 
				
			||||||
 | 
					    isValidTime: (timeSecs: number): boolean => {
 | 
				
			||||||
 | 
					        const vallisEpoch = 1541837628;
 | 
				
			||||||
 | 
					        const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600);
 | 
				
			||||||
 | 
					        const vallisCycleStart = vallisEpoch + vallisCycle * 1600;
 | 
				
			||||||
 | 
					        const vallisCycleColdStart = vallisCycleStart + 400;
 | 
				
			||||||
 | 
					        return !isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, vallisCycleColdStart * 1000);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    getIdealTimeBefore: (timeSecs: number): number => {
 | 
				
			||||||
 | 
					        const vallisEpoch = 1541837628;
 | 
				
			||||||
 | 
					        const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600);
 | 
				
			||||||
 | 
					        const vallisCycleStart = vallisEpoch + vallisCycle * 1600;
 | 
				
			||||||
 | 
					        return vallisCycleStart;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getIdealTimeSatsifyingConstraints = (constraints: ITimeConstraint[]): number => {
 | 
				
			||||||
 | 
					    let timeSecs = Math.trunc(Date.now() / 1000);
 | 
				
			||||||
 | 
					    let allGood;
 | 
				
			||||||
 | 
					    do {
 | 
				
			||||||
 | 
					        allGood = true;
 | 
				
			||||||
 | 
					        for (const constraint of constraints) {
 | 
				
			||||||
 | 
					            if (!constraint.isValidTime(timeSecs)) {
 | 
				
			||||||
 | 
					                //logger.debug(`${constraint.name} is not happy with ${timeSecs}`);
 | 
				
			||||||
 | 
					                const prevTimeSecs = timeSecs;
 | 
				
			||||||
 | 
					                const suggestion = constraint.getIdealTimeBefore(timeSecs);
 | 
				
			||||||
 | 
					                timeSecs = suggestion;
 | 
				
			||||||
 | 
					                do {
 | 
				
			||||||
 | 
					                    timeSecs += 60;
 | 
				
			||||||
 | 
					                    if (timeSecs >= prevTimeSecs || !constraint.isValidTime(timeSecs)) {
 | 
				
			||||||
 | 
					                        timeSecs = suggestion; // Can't find a compromise; just take the suggestion and try to compromise on another constraint.
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } while (!constraints.every(constraint => constraint.isValidTime(timeSecs)));
 | 
				
			||||||
 | 
					                allGood = false;
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    } while (!allGood);
 | 
				
			||||||
 | 
					    return timeSecs;
 | 
				
			||||||
    if (config.worldState?.duviriOverride) {
 | 
					 | 
				
			||||||
        const duviriMoods = ["sorrow", "fear", "joy", "anger", "envy"];
 | 
					 | 
				
			||||||
        const desiredMood = duviriMoods.indexOf(config.worldState.duviriOverride);
 | 
					 | 
				
			||||||
        if (desiredMood == -1) {
 | 
					 | 
				
			||||||
            logger.warn(`ignoring invalid config value for worldState.duviriOverride`, {
 | 
					 | 
				
			||||||
                value: config.worldState.duviriOverride,
 | 
					 | 
				
			||||||
                valid_values: duviriMoods
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            const moodIndex = Math.trunc(timeSecs / 7200);
 | 
					 | 
				
			||||||
            const moodStart = moodIndex * 7200;
 | 
					 | 
				
			||||||
            const moodEnd = moodStart + 7200;
 | 
					 | 
				
			||||||
            if (
 | 
					 | 
				
			||||||
                moodIndex % 5 != desiredMood ||
 | 
					 | 
				
			||||||
                isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, moodEnd * 1000)
 | 
					 | 
				
			||||||
            ) {
 | 
					 | 
				
			||||||
                return false;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return true;
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getVarziaRotation = (week: number): string => {
 | 
					const getVarziaRotation = (week: number): string => {
 | 
				
			||||||
@ -1170,10 +1228,38 @@ const getAllVarziaManifests = (): IPrimeVaultTraderOffer[] => {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
					export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
				
			||||||
    let timeSecs = Math.round(Date.now() / 1000);
 | 
					    const constraints: ITimeConstraint[] = [];
 | 
				
			||||||
    while (!doesTimeSatsifyConstraints(timeSecs)) {
 | 
					    if (config.worldState?.eidolonOverride) {
 | 
				
			||||||
        timeSecs -= 60;
 | 
					        constraints.push(config.worldState.eidolonOverride == "day" ? eidolonDayConstraint : eidolonNightConstraint);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (config.worldState?.vallisOverride) {
 | 
				
			||||||
 | 
					        constraints.push(config.worldState.vallisOverride == "cold" ? venusColdConstraint : venusWarmConstraint);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (config.worldState?.duviriOverride) {
 | 
				
			||||||
 | 
					        const duviriMoods = ["sorrow", "fear", "joy", "anger", "envy"];
 | 
				
			||||||
 | 
					        const desiredMood = duviriMoods.indexOf(config.worldState.duviriOverride);
 | 
				
			||||||
 | 
					        if (desiredMood == -1) {
 | 
				
			||||||
 | 
					            logger.warn(`ignoring invalid config value for worldState.duviriOverride`, {
 | 
				
			||||||
 | 
					                value: config.worldState.duviriOverride,
 | 
				
			||||||
 | 
					                valid_values: duviriMoods
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            constraints.push({
 | 
				
			||||||
 | 
					                //name: `duviri ${config.worldState.duviriOverride}`,
 | 
				
			||||||
 | 
					                isValidTime: (timeSecs: number): boolean => {
 | 
				
			||||||
 | 
					                    const moodIndex = Math.trunc(timeSecs / 7200);
 | 
				
			||||||
 | 
					                    return moodIndex % 5 == desiredMood;
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                getIdealTimeBefore: (timeSecs: number): number => {
 | 
				
			||||||
 | 
					                    let moodIndex = Math.trunc(timeSecs / 7200);
 | 
				
			||||||
 | 
					                    moodIndex -= ((moodIndex % 5) - desiredMood + 5) % 5; // while (moodIndex % 5 != desiredMood) --moodIndex;
 | 
				
			||||||
 | 
					                    const moodStart = moodIndex * 7200;
 | 
				
			||||||
 | 
					                    return moodStart;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const timeSecs = getIdealTimeSatsifyingConstraints(constraints);
 | 
				
			||||||
    const timeMs = timeSecs * 1000;
 | 
					    const timeMs = timeSecs * 1000;
 | 
				
			||||||
    const day = Math.trunc((timeMs - EPOCH) / 86400000);
 | 
					    const day = Math.trunc((timeMs - EPOCH) / 86400000);
 | 
				
			||||||
    const week = Math.trunc(day / 7);
 | 
					    const week = Math.trunc(day / 7);
 | 
				
			||||||
 | 
				
			|||||||
@ -520,7 +520,8 @@ export enum InventorySlot {
 | 
				
			|||||||
    SENTINELS = "SentinelBin",
 | 
					    SENTINELS = "SentinelBin",
 | 
				
			||||||
    AMPS = "OperatorAmpBin",
 | 
					    AMPS = "OperatorAmpBin",
 | 
				
			||||||
    RJ_COMPONENT_AND_ARMAMENTS = "CrewShipSalvageBin",
 | 
					    RJ_COMPONENT_AND_ARMAMENTS = "CrewShipSalvageBin",
 | 
				
			||||||
    CREWMEMBERS = "CrewMemberBin"
 | 
					    CREWMEMBERS = "CrewMemberBin",
 | 
				
			||||||
 | 
					    RIVENS = "RandomModBin"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ISlots {
 | 
					export interface ISlots {
 | 
				
			||||||
 | 
				
			|||||||
@ -8,7 +8,8 @@ import {
 | 
				
			|||||||
    IRecentVendorPurchaseClient,
 | 
					    IRecentVendorPurchaseClient,
 | 
				
			||||||
    TEquipmentKey,
 | 
					    TEquipmentKey,
 | 
				
			||||||
    ICrewMemberClient,
 | 
					    ICrewMemberClient,
 | 
				
			||||||
    IKubrowPetPrintClient
 | 
					    IKubrowPetPrintClient,
 | 
				
			||||||
 | 
					    IUpgradeClient
 | 
				
			||||||
} from "@/src/types/inventoryTypes/inventoryTypes";
 | 
					} from "@/src/types/inventoryTypes/inventoryTypes";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export enum PurchaseSource {
 | 
					export enum PurchaseSource {
 | 
				
			||||||
@ -80,6 +81,7 @@ export type IInventoryChanges = {
 | 
				
			|||||||
    RecentVendorPurchases?: IRecentVendorPurchaseClient; // < 38.5.0
 | 
					    RecentVendorPurchases?: IRecentVendorPurchaseClient; // < 38.5.0
 | 
				
			||||||
    CrewMembers?: ICrewMemberClient[];
 | 
					    CrewMembers?: ICrewMemberClient[];
 | 
				
			||||||
    KubrowPetPrints?: IKubrowPetPrintClient[];
 | 
					    KubrowPetPrints?: IKubrowPetPrintClient[];
 | 
				
			||||||
 | 
					    Upgrades?: IUpgradeClient[]; // TOVERIFY
 | 
				
			||||||
} & Record<
 | 
					} & Record<
 | 
				
			||||||
        Exclude<
 | 
					        Exclude<
 | 
				
			||||||
            string,
 | 
					            string,
 | 
				
			||||||
 | 
				
			|||||||
@ -3,3 +3,5 @@ type Entries<T, K extends keyof T = keyof T> = (K extends unknown ? [K, T[K]] :
 | 
				
			|||||||
export function getEntriesUnsafe<T extends object>(object: T): Entries<T> {
 | 
					export function getEntriesUnsafe<T extends object>(object: T): Entries<T> {
 | 
				
			||||||
    return Object.entries(object) as Entries<T>;
 | 
					    return Object.entries(object) as Entries<T>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const exhaustive = (_: never): void => {};
 | 
				
			||||||
 | 
				
			|||||||
@ -135,5 +135,6 @@
 | 
				
			|||||||
  "/Lotus/Language/EntratiLab/EntratiGeneral/HumanLoidLoved",
 | 
					  "/Lotus/Language/EntratiLab/EntratiGeneral/HumanLoidLoved",
 | 
				
			||||||
  "ConquestSetupIntro",
 | 
					  "ConquestSetupIntro",
 | 
				
			||||||
  "EntratiLabConquestHardModeUnlocked",
 | 
					  "EntratiLabConquestHardModeUnlocked",
 | 
				
			||||||
  "/Lotus/Language/Npcs/KonzuPostNewWar"
 | 
					  "/Lotus/Language/Npcs/KonzuPostNewWar",
 | 
				
			||||||
 | 
					  "/Lotus/Language/SolarisVenus/EudicoPostNewWar"
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
				
			|||||||
@ -16,6 +16,10 @@
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
      "ItemType": "/Lotus/Types/Keys/1999PrologueQuest/1999PrologueQuestKeyChain",
 | 
					      "ItemType": "/Lotus/Types/Keys/1999PrologueQuest/1999PrologueQuestKeyChain",
 | 
				
			||||||
      "ItemCount": 1
 | 
					      "ItemCount": 1
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "ItemType": "/Lotus/Types/Items/EmailItems/TennokaiEmailItem",
 | 
				
			||||||
 | 
					      "ItemCount": 1
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  ]
 | 
					  ]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -457,7 +457,8 @@
 | 
				
			|||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div id="detailedView-route" data-route="/webui/detailedView" data-title="Inventory | OpenWF WebUI">
 | 
					            <div id="detailedView-route" data-route="/webui/detailedView" data-title="Inventory | OpenWF WebUI">
 | 
				
			||||||
                <h3 class="mb-0"></h3>
 | 
					                <h3 id="detailedView-loading" class="mb-0" data-loc="general_loading"></h3>
 | 
				
			||||||
 | 
					                <h3 id="detailedView-title" class="mb-0"></h3>
 | 
				
			||||||
                <p class="text-body-secondary"></p>
 | 
					                <p class="text-body-secondary"></p>
 | 
				
			||||||
                <div id="archonShards-card" class="card mb-3 d-none">
 | 
					                <div id="archonShards-card" class="card mb-3 d-none">
 | 
				
			||||||
                    <h5 class="card-header" data-loc="detailedView_archonShardsLabel"></h5>
 | 
					                    <h5 class="card-header" data-loc="detailedView_archonShardsLabel"></h5>
 | 
				
			||||||
@ -527,8 +528,8 @@
 | 
				
			|||||||
                                <form class="input-group mb-3" onsubmit="doAcquireMod();return false;">
 | 
					                                <form class="input-group mb-3" onsubmit="doAcquireMod();return false;">
 | 
				
			||||||
                                    <input class="form-control" id="mod-count" type="number" value="1"/>
 | 
					                                    <input class="form-control" id="mod-count" type="number" value="1"/>
 | 
				
			||||||
                                    <input class="form-control w-50" id="mod-to-acquire" list="datalist-mods" />
 | 
					                                    <input class="form-control w-50" id="mod-to-acquire" list="datalist-mods" />
 | 
				
			||||||
                                    <button class="btn btn-success" onclick="window.maxed=true" type="submit" data-loc="mods_addMax"></button>
 | 
					 | 
				
			||||||
                                    <button class="btn btn-primary" type="submit" data-loc="general_addButton"></button>
 | 
					                                    <button class="btn btn-primary" type="submit" data-loc="general_addButton"></button>
 | 
				
			||||||
 | 
					                                    <button class="btn btn-success" onclick="window.maxed=true" data-loc="mods_addMax"></button>
 | 
				
			||||||
                                </form>
 | 
					                                </form>
 | 
				
			||||||
                                <table class="table table-hover w-100">
 | 
					                                <table class="table table-hover w-100">
 | 
				
			||||||
                                    <tbody id="mods-list"></tbody>
 | 
					                                    <tbody id="mods-list"></tbody>
 | 
				
			||||||
@ -803,21 +804,21 @@
 | 
				
			|||||||
                                        <label class="form-label" for="spoofMasteryRank" data-loc="cheats_spoofMasteryRank"></label>
 | 
					                                        <label class="form-label" for="spoofMasteryRank" data-loc="cheats_spoofMasteryRank"></label>
 | 
				
			||||||
                                        <div class="input-group">
 | 
					                                        <div class="input-group">
 | 
				
			||||||
                                            <input class="form-control" id="spoofMasteryRank" type="number" min="-1" max="65535" data-default="-1" />
 | 
					                                            <input class="form-control" id="spoofMasteryRank" type="number" min="-1" max="65535" data-default="-1" />
 | 
				
			||||||
                                            <button class="btn btn-primary" type="submit" data-loc="cheats_save"></button>
 | 
					                                            <button class="btn btn-secondary" type="submit" data-loc="cheats_save"></button>
 | 
				
			||||||
                                        </div>
 | 
					                                        </div>
 | 
				
			||||||
                                    </form>
 | 
					                                    </form>
 | 
				
			||||||
                                    <form class="form-group mt-2" onsubmit="doSaveConfigInt('relicRewardItemCountMultiplier'); return false;">
 | 
					                                    <form class="form-group mt-2" onsubmit="doSaveConfigInt('relicRewardItemCountMultiplier'); return false;">
 | 
				
			||||||
                                        <label class="form-label" for="relicRewardItemCountMultiplier" data-loc="cheats_relicRewardItemCountMultiplier"></label>
 | 
					                                        <label class="form-label" for="relicRewardItemCountMultiplier" data-loc="cheats_relicRewardItemCountMultiplier"></label>
 | 
				
			||||||
                                        <div class="input-group">
 | 
					                                        <div class="input-group">
 | 
				
			||||||
                                            <input class="form-control" id="relicRewardItemCountMultiplier" type="number" min="1" max="1000000" data-default="1" />
 | 
					                                            <input class="form-control" id="relicRewardItemCountMultiplier" type="number" min="1" max="1000000" data-default="1" />
 | 
				
			||||||
                                            <button class="btn btn-primary" type="submit" data-loc="cheats_save"></button>
 | 
					                                            <button class="btn btn-secondary" type="submit" data-loc="cheats_save"></button>
 | 
				
			||||||
                                        </div>
 | 
					                                        </div>
 | 
				
			||||||
                                    </form>
 | 
					                                    </form>
 | 
				
			||||||
                                    <form class="form-group mt-2" onsubmit="doSaveConfigInt('nightwaveStandingMultiplier'); return false;">
 | 
					                                    <form class="form-group mt-2" onsubmit="doSaveConfigInt('nightwaveStandingMultiplier'); return false;">
 | 
				
			||||||
                                        <label class="form-label" for="nightwaveStandingMultiplier" data-loc="cheats_nightwaveStandingMultiplier"></label>
 | 
					                                        <label class="form-label" for="nightwaveStandingMultiplier" data-loc="cheats_nightwaveStandingMultiplier"></label>
 | 
				
			||||||
                                        <div class="input-group">
 | 
					                                        <div class="input-group">
 | 
				
			||||||
                                            <input class="form-control" id="nightwaveStandingMultiplier" type="number" min="1" max="1000000" data-default="1" />
 | 
					                                            <input class="form-control" id="nightwaveStandingMultiplier" type="number" min="1" max="1000000" data-default="1" />
 | 
				
			||||||
                                            <button class="btn btn-primary" type="submit" data-loc="cheats_save"></button>
 | 
					                                            <button class="btn btn-secondary" type="submit" data-loc="cheats_save"></button>
 | 
				
			||||||
                                        </div>
 | 
					                                        </div>
 | 
				
			||||||
                                    </form>
 | 
					                                    </form>
 | 
				
			||||||
                                </div>
 | 
					                                </div>
 | 
				
			||||||
@ -830,6 +831,7 @@
 | 
				
			|||||||
                            <div class="card-body">
 | 
					                            <div class="card-body">
 | 
				
			||||||
                                <div class="mb-2 d-flex flex-wrap gap-2">
 | 
					                                <div class="mb-2 d-flex flex-wrap gap-2">
 | 
				
			||||||
                                    <button class="btn btn-primary" onclick="debounce(doUnlockAllMissions);" data-loc="cheats_unlockAllMissions"></button>
 | 
					                                    <button class="btn btn-primary" onclick="debounce(doUnlockAllMissions);" data-loc="cheats_unlockAllMissions"></button>
 | 
				
			||||||
 | 
					                                    <button class="btn btn-primary" onclick="debounce(markAllAsRead);" data-loc="cheats_markAllAsRead"></button>
 | 
				
			||||||
                                    <button class="btn btn-primary" onclick="doUnlockAllFocusSchools();" data-loc="cheats_unlockAllFocusSchools"></button>
 | 
					                                    <button class="btn btn-primary" onclick="doUnlockAllFocusSchools();" data-loc="cheats_unlockAllFocusSchools"></button>
 | 
				
			||||||
                                    <button class="btn btn-primary" onclick="doHelminthUnlockAll();" data-loc="cheats_helminthUnlockAll"></button>
 | 
					                                    <button class="btn btn-primary" onclick="doHelminthUnlockAll();" data-loc="cheats_helminthUnlockAll"></button>
 | 
				
			||||||
                                    <button class="btn btn-primary" onclick="debounce(addMissingHelminthRecipes);" data-loc="cheats_addMissingSubsumedAbilities"></button>
 | 
					                                    <button class="btn btn-primary" onclick="debounce(addMissingHelminthRecipes);" data-loc="cheats_addMissingSubsumedAbilities"></button>
 | 
				
			||||||
@ -942,14 +944,14 @@
 | 
				
			|||||||
                                    <label class="form-label" for="worldState.circuitGameModes" data-loc="worldState_theCircuitOverride"></label>
 | 
					                                    <label class="form-label" for="worldState.circuitGameModes" data-loc="worldState_theCircuitOverride"></label>
 | 
				
			||||||
                                    <div class="input-group">
 | 
					                                    <div class="input-group">
 | 
				
			||||||
                                        <input id="worldState.circuitGameModes" type="text" class="form-control tags-input" list="datalist-circuitGameModes" />
 | 
					                                        <input id="worldState.circuitGameModes" type="text" class="form-control tags-input" list="datalist-circuitGameModes" />
 | 
				
			||||||
                                        <button class="btn btn-primary" type="submit" data-loc="cheats_save"></button>
 | 
					                                        <button class="btn btn-secondary" type="submit" data-loc="cheats_save"></button>
 | 
				
			||||||
                                    </div>
 | 
					                                    </div>
 | 
				
			||||||
                                </form>
 | 
					                                </form>
 | 
				
			||||||
                                <form class="form-group mt-2" onsubmit="doSaveConfigFloat('worldState.darvoStockMultiplier'); return false;">
 | 
					                                <form class="form-group mt-2" onsubmit="doSaveConfigFloat('worldState.darvoStockMultiplier'); return false;">
 | 
				
			||||||
                                    <label class="form-label" for="worldState.darvoStockMultiplier" data-loc="worldState_darvoStockMultiplier"></label>
 | 
					                                    <label class="form-label" for="worldState.darvoStockMultiplier" data-loc="worldState_darvoStockMultiplier"></label>
 | 
				
			||||||
                                    <div class="input-group">
 | 
					                                    <div class="input-group">
 | 
				
			||||||
                                        <input id="worldState.darvoStockMultiplier" class="form-control" type="number" step="0.01" data-default="1" />
 | 
					                                        <input id="worldState.darvoStockMultiplier" class="form-control" type="number" step="0.01" data-default="1" />
 | 
				
			||||||
                                        <button class="btn btn-primary" type="submit" data-loc="cheats_save"></button>
 | 
					                                        <button class="btn btn-secondary" type="submit" data-loc="cheats_save"></button>
 | 
				
			||||||
                                    </div>
 | 
					                                    </div>
 | 
				
			||||||
                                </form>
 | 
					                                </form>
 | 
				
			||||||
                            </div>
 | 
					                            </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -273,6 +273,8 @@ function fetchItemList() {
 | 
				
			|||||||
    window.itemListPromise = new Promise(resolve => {
 | 
					    window.itemListPromise = new Promise(resolve => {
 | 
				
			||||||
        const req = $.get("/custom/getItemLists?lang=" + window.lang);
 | 
					        const req = $.get("/custom/getItemLists?lang=" + window.lang);
 | 
				
			||||||
        req.done(async data => {
 | 
					        req.done(async data => {
 | 
				
			||||||
 | 
					            window.allQuestKeys = data.QuestKeys;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await dictPromise;
 | 
					            await dictPromise;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            document.querySelectorAll('[id^="datalist-"]').forEach(datalist => {
 | 
					            document.querySelectorAll('[id^="datalist-"]').forEach(datalist => {
 | 
				
			||||||
@ -879,6 +881,14 @@ function updateInventory() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            // Populate quests route
 | 
					            // Populate quests route
 | 
				
			||||||
            document.getElementById("QuestKeys-list").innerHTML = "";
 | 
					            document.getElementById("QuestKeys-list").innerHTML = "";
 | 
				
			||||||
 | 
					            window.allQuestKeys.forEach(questKey => {
 | 
				
			||||||
 | 
					                if (!data.QuestKeys.some(x => x.ItemType == questKey.uniqueName)) {
 | 
				
			||||||
 | 
					                    const datalist = document.getElementById("datalist-QuestKeys");
 | 
				
			||||||
 | 
					                    if (!datalist.querySelector(`option[data-key="${questKey.uniqueName}"]`)) {
 | 
				
			||||||
 | 
					                        readdQuestKey(itemMap, questKey.uniqueName);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
            data.QuestKeys.forEach(item => {
 | 
					            data.QuestKeys.forEach(item => {
 | 
				
			||||||
                const tr = document.createElement("tr");
 | 
					                const tr = document.createElement("tr");
 | 
				
			||||||
                tr.setAttribute("data-item-type", item.ItemType);
 | 
					                tr.setAttribute("data-item-type", item.ItemType);
 | 
				
			||||||
@ -972,10 +982,7 @@ function updateInventory() {
 | 
				
			|||||||
                        a.href = "#";
 | 
					                        a.href = "#";
 | 
				
			||||||
                        a.onclick = function (event) {
 | 
					                        a.onclick = function (event) {
 | 
				
			||||||
                            event.preventDefault();
 | 
					                            event.preventDefault();
 | 
				
			||||||
                            const option = document.createElement("option");
 | 
					                            readdQuestKey(itemMap, item.ItemType);
 | 
				
			||||||
                            option.setAttribute("data-key", item.ItemType);
 | 
					 | 
				
			||||||
                            option.value = itemMap[item.ItemType]?.name ?? item.ItemType;
 | 
					 | 
				
			||||||
                            document.getElementById("datalist-QuestKeys").appendChild(option);
 | 
					 | 
				
			||||||
                            doQuestUpdate("deleteKey", item.ItemType);
 | 
					                            doQuestUpdate("deleteKey", item.ItemType);
 | 
				
			||||||
                        };
 | 
					                        };
 | 
				
			||||||
                        a.title = loc("code_remove");
 | 
					                        a.title = loc("code_remove");
 | 
				
			||||||
@ -1166,14 +1173,15 @@ function updateInventory() {
 | 
				
			|||||||
                const item = data[category].find(x => x.ItemId.$oid == oid);
 | 
					                const item = data[category].find(x => x.ItemId.$oid == oid);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (item) {
 | 
					                if (item) {
 | 
				
			||||||
 | 
					                    document.getElementById("detailedView-loading").classList.add("d-none");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (item.ItemName) {
 | 
					                    if (item.ItemName) {
 | 
				
			||||||
                        $("#detailedView-route h3").text(item.ItemName);
 | 
					                        $("#detailedView-title").text(item.ItemName);
 | 
				
			||||||
                        $("#detailedView-route .text-body-secondary").text(
 | 
					                        $("#detailedView-route .text-body-secondary").text(
 | 
				
			||||||
                            itemMap[item.ItemType]?.name ?? item.ItemType
 | 
					                            itemMap[item.ItemType]?.name ?? item.ItemType
 | 
				
			||||||
                        );
 | 
					                        );
 | 
				
			||||||
                    } else {
 | 
					                    } else {
 | 
				
			||||||
                        $("#detailedView-route h3").text(itemMap[item.ItemType]?.name ?? item.ItemType);
 | 
					                        $("#detailedView-title").text(itemMap[item.ItemType]?.name ?? item.ItemType);
 | 
				
			||||||
                        $("#detailedView-route .text-body-secondary").text("");
 | 
					 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (category == "Suits") {
 | 
					                    if (category == "Suits") {
 | 
				
			||||||
@ -1954,6 +1962,19 @@ for (const id of uiConfigs) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					document.querySelectorAll(".config-form .input-group").forEach(grp => {
 | 
				
			||||||
 | 
					    const input = grp.querySelector("input");
 | 
				
			||||||
 | 
					    const btn = grp.querySelector("button");
 | 
				
			||||||
 | 
					    input.oninput = input.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");
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function doSaveConfigInt(id) {
 | 
					function doSaveConfigInt(id) {
 | 
				
			||||||
    $.post({
 | 
					    $.post({
 | 
				
			||||||
        url: "/custom/setConfig?" + window.authz + "&wsid=" + wsid,
 | 
					        url: "/custom/setConfig?" + window.authz + "&wsid=" + wsid,
 | 
				
			||||||
@ -2171,7 +2192,9 @@ function doAddMissingMaxRankMods() {
 | 
				
			|||||||
// DetailedView Route
 | 
					// DetailedView Route
 | 
				
			||||||
 | 
					
 | 
				
			||||||
single.getRoute("#detailedView-route").on("beforeload", function () {
 | 
					single.getRoute("#detailedView-route").on("beforeload", function () {
 | 
				
			||||||
    this.element.querySelector("h3").textContent = "Loading...";
 | 
					    document.getElementById("detailedView-loading").classList.remove("d-none");
 | 
				
			||||||
 | 
					    document.getElementById("detailedView-title").textContent = "";
 | 
				
			||||||
 | 
					    document.querySelector("#detailedView-route .text-body-secondary").textContent = "";
 | 
				
			||||||
    document.getElementById("archonShards-card").classList.add("d-none");
 | 
					    document.getElementById("archonShards-card").classList.add("d-none");
 | 
				
			||||||
    document.getElementById("valenceBonus-card").classList.add("d-none");
 | 
					    document.getElementById("valenceBonus-card").classList.add("d-none");
 | 
				
			||||||
    if (window.didInitialInventoryUpdate) {
 | 
					    if (window.didInitialInventoryUpdate) {
 | 
				
			||||||
@ -2254,6 +2277,13 @@ function doAddCurrency(currency) {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function readdQuestKey(itemMap, itemType) {
 | 
				
			||||||
 | 
					    const option = document.createElement("option");
 | 
				
			||||||
 | 
					    option.setAttribute("data-key", itemType);
 | 
				
			||||||
 | 
					    option.value = itemMap[itemType]?.name ?? itemType;
 | 
				
			||||||
 | 
					    document.getElementById("datalist-QuestKeys").appendChild(option);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function doQuestUpdate(operation, itemType) {
 | 
					function doQuestUpdate(operation, itemType) {
 | 
				
			||||||
    revalidateAuthz().then(() => {
 | 
					    revalidateAuthz().then(() => {
 | 
				
			||||||
        $.post({
 | 
					        $.post({
 | 
				
			||||||
@ -2764,3 +2794,16 @@ document.querySelectorAll("#sidebar .nav-link").forEach(function (elm) {
 | 
				
			|||||||
        window.scrollTo(0, 0);
 | 
					        window.scrollTo(0, 0);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function markAllAsRead() {
 | 
				
			||||||
 | 
					    await revalidateAuthz();
 | 
				
			||||||
 | 
					    const { Inbox } = await fetch("/api/inbox.php?" + window.authz).then(x => x.json());
 | 
				
			||||||
 | 
					    let any = false;
 | 
				
			||||||
 | 
					    for (const msg of Inbox) {
 | 
				
			||||||
 | 
					        if (!msg.r) {
 | 
				
			||||||
 | 
					            await fetch("/api/inbox.php?" + window.authz + "&messageId=" + msg.messageId.$oid);
 | 
				
			||||||
 | 
					            any = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    toast(loc(any ? "code_succRelog" : "code_nothingToDo"));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ dict = {
 | 
				
			|||||||
    general_addButton: `Hinzufügen`,
 | 
					    general_addButton: `Hinzufügen`,
 | 
				
			||||||
    general_setButton: `[UNTRANSLATED] Set`,
 | 
					    general_setButton: `[UNTRANSLATED] Set`,
 | 
				
			||||||
    general_bulkActions: `Massenaktionen`,
 | 
					    general_bulkActions: `Massenaktionen`,
 | 
				
			||||||
 | 
					    general_loading: `[UNTRANSLATED] Loading...`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    code_loginFail: `[UNTRANSLATED] Login failed. Double-check the email and password.`,
 | 
					    code_loginFail: `[UNTRANSLATED] Login failed. Double-check the email and password.`,
 | 
				
			||||||
    code_regFail: `[UNTRANSLATED] Registration failed. Account already exists?`,
 | 
					    code_regFail: `[UNTRANSLATED] Registration failed. Account already exists?`,
 | 
				
			||||||
@ -44,6 +45,8 @@ dict = {
 | 
				
			|||||||
    code_focusUnlocked: `|COUNT| neue Fokus-Schulen freigeschaltet! Ein Inventar-Update wird benötigt, damit die Änderungen im Spiel sichtbar werden. Die Sternenkarte zu besuchen, sollte der einfachste Weg sein, dies auszulösen.`,
 | 
					    code_focusUnlocked: `|COUNT| neue Fokus-Schulen freigeschaltet! Ein Inventar-Update wird benötigt, damit die Änderungen im Spiel sichtbar werden. Die Sternenkarte zu besuchen, sollte der einfachste Weg sein, dies auszulösen.`,
 | 
				
			||||||
    code_addModsConfirm: `Bist du sicher, dass du |COUNT| Mods zu deinem Account hinzufügen möchtest?`,
 | 
					    code_addModsConfirm: `Bist du sicher, dass du |COUNT| Mods zu deinem Account hinzufügen möchtest?`,
 | 
				
			||||||
    code_succImport: `Erfolgreich importiert.`,
 | 
					    code_succImport: `Erfolgreich importiert.`,
 | 
				
			||||||
 | 
					    code_succRelog: `[UNTRANSLATED] Done. Please note that you'll need to relog to see a difference in-game.`,
 | 
				
			||||||
 | 
					    code_nothingToDo: `[UNTRANSLATED] Done. There was nothing to do.`,
 | 
				
			||||||
    code_gild: `Veredeln`,
 | 
					    code_gild: `Veredeln`,
 | 
				
			||||||
    code_moa: `Moa`,
 | 
					    code_moa: `Moa`,
 | 
				
			||||||
    code_zanuka: `Jagdhund`,
 | 
					    code_zanuka: `Jagdhund`,
 | 
				
			||||||
@ -199,6 +202,7 @@ dict = {
 | 
				
			|||||||
    cheats_changeSupportedSyndicate: `Unterstütztes Syndikat`,
 | 
					    cheats_changeSupportedSyndicate: `Unterstütztes Syndikat`,
 | 
				
			||||||
    cheats_changeButton: `Ändern`,
 | 
					    cheats_changeButton: `Ändern`,
 | 
				
			||||||
    cheats_none: `Keines`,
 | 
					    cheats_none: `Keines`,
 | 
				
			||||||
 | 
					    cheats_markAllAsRead: `[UNTRANSLATED] Mark Inbox As Read`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    worldState: `[UNTRANSLATED] World State`,
 | 
					    worldState: `[UNTRANSLATED] World State`,
 | 
				
			||||||
    worldState_creditBoost: `[UNTRANSLATED] Credit Boost`,
 | 
					    worldState_creditBoost: `[UNTRANSLATED] Credit Boost`,
 | 
				
			||||||
 | 
				
			|||||||
@ -3,6 +3,7 @@ dict = {
 | 
				
			|||||||
    general_addButton: `Add`,
 | 
					    general_addButton: `Add`,
 | 
				
			||||||
    general_setButton: `Set`,
 | 
					    general_setButton: `Set`,
 | 
				
			||||||
    general_bulkActions: `Bulk Actions`,
 | 
					    general_bulkActions: `Bulk Actions`,
 | 
				
			||||||
 | 
					    general_loading: `Loading...`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    code_loginFail: `Login failed. Double-check the email and password.`,
 | 
					    code_loginFail: `Login failed. Double-check the email and password.`,
 | 
				
			||||||
    code_regFail: `Registration failed. Account already exists?`,
 | 
					    code_regFail: `Registration failed. Account already exists?`,
 | 
				
			||||||
@ -43,6 +44,8 @@ dict = {
 | 
				
			|||||||
    code_focusUnlocked: `Unlocked |COUNT| new focus schools! An inventory update will be needed for the changes to be reflected in-game. Visiting the navigation should be the easiest way to trigger that.`,
 | 
					    code_focusUnlocked: `Unlocked |COUNT| new focus schools! An inventory update will be needed for the changes to be reflected in-game. Visiting the navigation should be the easiest way to trigger that.`,
 | 
				
			||||||
    code_addModsConfirm: `Are you sure you want to add |COUNT| mods to your account?`,
 | 
					    code_addModsConfirm: `Are you sure you want to add |COUNT| mods to your account?`,
 | 
				
			||||||
    code_succImport: `Successfully imported.`,
 | 
					    code_succImport: `Successfully imported.`,
 | 
				
			||||||
 | 
					    code_succRelog: `Done. Please note that you'll need to relog to see a difference in-game.`,
 | 
				
			||||||
 | 
					    code_nothingToDo: `Done. There was nothing to do.`,
 | 
				
			||||||
    code_gild: `Gild`,
 | 
					    code_gild: `Gild`,
 | 
				
			||||||
    code_moa: `Moa`,
 | 
					    code_moa: `Moa`,
 | 
				
			||||||
    code_zanuka: `Hound`,
 | 
					    code_zanuka: `Hound`,
 | 
				
			||||||
@ -198,6 +201,7 @@ dict = {
 | 
				
			|||||||
    cheats_changeSupportedSyndicate: `Supported syndicate`,
 | 
					    cheats_changeSupportedSyndicate: `Supported syndicate`,
 | 
				
			||||||
    cheats_changeButton: `Change`,
 | 
					    cheats_changeButton: `Change`,
 | 
				
			||||||
    cheats_none: `None`,
 | 
					    cheats_none: `None`,
 | 
				
			||||||
 | 
					    cheats_markAllAsRead: `Mark Inbox As Read`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    worldState: `World State`,
 | 
					    worldState: `World State`,
 | 
				
			||||||
    worldState_creditBoost: `Credit Boost`,
 | 
					    worldState_creditBoost: `Credit Boost`,
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ dict = {
 | 
				
			|||||||
    general_addButton: `Agregar`,
 | 
					    general_addButton: `Agregar`,
 | 
				
			||||||
    general_setButton: `Establecer`,
 | 
					    general_setButton: `Establecer`,
 | 
				
			||||||
    general_bulkActions: `Acciones masivas`,
 | 
					    general_bulkActions: `Acciones masivas`,
 | 
				
			||||||
 | 
					    general_loading: `[UNTRANSLATED] Loading...`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    code_loginFail: `Error al iniciar sesión. Verifica el correo electrónico y la contraseña.`,
 | 
					    code_loginFail: `Error al iniciar sesión. Verifica el correo electrónico y la contraseña.`,
 | 
				
			||||||
    code_regFail: `Error al registrar la cuenta. ¿Ya existe una cuenta con este correo?`,
 | 
					    code_regFail: `Error al registrar la cuenta. ¿Ya existe una cuenta con este correo?`,
 | 
				
			||||||
@ -44,6 +45,8 @@ dict = {
 | 
				
			|||||||
    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.`,
 | 
					    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.`,
 | 
				
			||||||
    code_addModsConfirm: `¿Estás seguro de que deseas agregar |COUNT| modificadores a tu cuenta?`,
 | 
					    code_addModsConfirm: `¿Estás seguro de que deseas agregar |COUNT| modificadores a tu cuenta?`,
 | 
				
			||||||
    code_succImport: `Importación exitosa.`,
 | 
					    code_succImport: `Importación exitosa.`,
 | 
				
			||||||
 | 
					    code_succRelog: `[UNTRANSLATED] Done. Please note that you'll need to relog to see a difference in-game.`,
 | 
				
			||||||
 | 
					    code_nothingToDo: `[UNTRANSLATED] Done. There was nothing to do.`,
 | 
				
			||||||
    code_gild: `Refinar`,
 | 
					    code_gild: `Refinar`,
 | 
				
			||||||
    code_moa: `Moa`,
 | 
					    code_moa: `Moa`,
 | 
				
			||||||
    code_zanuka: `Sabueso`,
 | 
					    code_zanuka: `Sabueso`,
 | 
				
			||||||
@ -199,6 +202,7 @@ dict = {
 | 
				
			|||||||
    cheats_changeSupportedSyndicate: `Sindicatos disponibles`,
 | 
					    cheats_changeSupportedSyndicate: `Sindicatos disponibles`,
 | 
				
			||||||
    cheats_changeButton: `Cambiar`,
 | 
					    cheats_changeButton: `Cambiar`,
 | 
				
			||||||
    cheats_none: `Ninguno`,
 | 
					    cheats_none: `Ninguno`,
 | 
				
			||||||
 | 
					    cheats_markAllAsRead: `[UNTRANSLATED] Mark Inbox As Read`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    worldState: `Estado del mundo`,
 | 
					    worldState: `Estado del mundo`,
 | 
				
			||||||
    worldState_creditBoost: `Potenciador de Créditos`,
 | 
					    worldState_creditBoost: `Potenciador de Créditos`,
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ dict = {
 | 
				
			|||||||
    general_addButton: `Ajouter`,
 | 
					    general_addButton: `Ajouter`,
 | 
				
			||||||
    general_setButton: `[UNTRANSLATED] Set`,
 | 
					    general_setButton: `[UNTRANSLATED] Set`,
 | 
				
			||||||
    general_bulkActions: `Action groupée`,
 | 
					    general_bulkActions: `Action groupée`,
 | 
				
			||||||
 | 
					    general_loading: `[UNTRANSLATED] Loading...`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    code_loginFail: `Connexion échouée. Vérifiez le mot de passe.`,
 | 
					    code_loginFail: `Connexion échouée. Vérifiez le mot de passe.`,
 | 
				
			||||||
    code_regFail: `Enregistrement impossible. Compte existant?`,
 | 
					    code_regFail: `Enregistrement impossible. Compte existant?`,
 | 
				
			||||||
@ -44,6 +45,8 @@ dict = {
 | 
				
			|||||||
    code_focusUnlocked: `|COUNT| écoles de Focus déverrouillées ! Synchronisation de l'inventaire nécessaire.`,
 | 
					    code_focusUnlocked: `|COUNT| écoles de Focus déverrouillées ! Synchronisation de l'inventaire nécessaire.`,
 | 
				
			||||||
    code_addModsConfirm: `Ajouter |COUNT| mods à l'inventaire ?`,
 | 
					    code_addModsConfirm: `Ajouter |COUNT| mods à l'inventaire ?`,
 | 
				
			||||||
    code_succImport: `Importé.`,
 | 
					    code_succImport: `Importé.`,
 | 
				
			||||||
 | 
					    code_succRelog: `[UNTRANSLATED] Done. Please note that you'll need to relog to see a difference in-game.`,
 | 
				
			||||||
 | 
					    code_nothingToDo: `[UNTRANSLATED] Done. There was nothing to do.`,
 | 
				
			||||||
    code_gild: `Polir`,
 | 
					    code_gild: `Polir`,
 | 
				
			||||||
    code_moa: `Moa`,
 | 
					    code_moa: `Moa`,
 | 
				
			||||||
    code_zanuka: `Molosse`,
 | 
					    code_zanuka: `Molosse`,
 | 
				
			||||||
@ -199,6 +202,7 @@ dict = {
 | 
				
			|||||||
    cheats_changeSupportedSyndicate: `Allégeance`,
 | 
					    cheats_changeSupportedSyndicate: `Allégeance`,
 | 
				
			||||||
    cheats_changeButton: `Changer`,
 | 
					    cheats_changeButton: `Changer`,
 | 
				
			||||||
    cheats_none: `Aucun`,
 | 
					    cheats_none: `Aucun`,
 | 
				
			||||||
 | 
					    cheats_markAllAsRead: `[UNTRANSLATED] Mark Inbox As Read`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    worldState: `[UNTRANSLATED] World State`,
 | 
					    worldState: `[UNTRANSLATED] World State`,
 | 
				
			||||||
    worldState_creditBoost: `[UNTRANSLATED] Credit Boost`,
 | 
					    worldState_creditBoost: `[UNTRANSLATED] Credit Boost`,
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ dict = {
 | 
				
			|||||||
    general_addButton: `Добавить`,
 | 
					    general_addButton: `Добавить`,
 | 
				
			||||||
    general_setButton: `Установить`,
 | 
					    general_setButton: `Установить`,
 | 
				
			||||||
    general_bulkActions: `Массовые действия`,
 | 
					    general_bulkActions: `Массовые действия`,
 | 
				
			||||||
 | 
					    general_loading: `[UNTRANSLATED] Loading...`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    code_loginFail: `[UNTRANSLATED] Login failed. Double-check the email and password.`,
 | 
					    code_loginFail: `[UNTRANSLATED] Login failed. Double-check the email and password.`,
 | 
				
			||||||
    code_regFail: `[UNTRANSLATED] Registration failed. Account already exists?`,
 | 
					    code_regFail: `[UNTRANSLATED] Registration failed. Account already exists?`,
 | 
				
			||||||
@ -44,6 +45,8 @@ dict = {
 | 
				
			|||||||
    code_focusUnlocked: `Разблокировано |COUNT| новых школ фокуса! Для отображения изменений в игре потребуется обновление инвентаря. Посещение навигации — самый простой способ этого добиться.`,
 | 
					    code_focusUnlocked: `Разблокировано |COUNT| новых школ фокуса! Для отображения изменений в игре потребуется обновление инвентаря. Посещение навигации — самый простой способ этого добиться.`,
 | 
				
			||||||
    code_addModsConfirm: `Вы уверены, что хотите добавить |COUNT| модов на ваш аккаунт?`,
 | 
					    code_addModsConfirm: `Вы уверены, что хотите добавить |COUNT| модов на ваш аккаунт?`,
 | 
				
			||||||
    code_succImport: `Успешно импортировано.`,
 | 
					    code_succImport: `Успешно импортировано.`,
 | 
				
			||||||
 | 
					    code_succRelog: `[UNTRANSLATED] Done. Please note that you'll need to relog to see a difference in-game.`,
 | 
				
			||||||
 | 
					    code_nothingToDo: `[UNTRANSLATED] Done. There was nothing to do.`,
 | 
				
			||||||
    code_gild: `Улучшить`,
 | 
					    code_gild: `Улучшить`,
 | 
				
			||||||
    code_moa: `МОА`,
 | 
					    code_moa: `МОА`,
 | 
				
			||||||
    code_zanuka: `Гончая`,
 | 
					    code_zanuka: `Гончая`,
 | 
				
			||||||
@ -199,6 +202,7 @@ dict = {
 | 
				
			|||||||
    cheats_changeSupportedSyndicate: `Поддерживаемый синдикат`,
 | 
					    cheats_changeSupportedSyndicate: `Поддерживаемый синдикат`,
 | 
				
			||||||
    cheats_changeButton: `Изменить`,
 | 
					    cheats_changeButton: `Изменить`,
 | 
				
			||||||
    cheats_none: `Отсутствует`,
 | 
					    cheats_none: `Отсутствует`,
 | 
				
			||||||
 | 
					    cheats_markAllAsRead: `[UNTRANSLATED] Mark Inbox As Read`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    worldState: `[UNTRANSLATED] World State`,
 | 
					    worldState: `[UNTRANSLATED] World State`,
 | 
				
			||||||
    worldState_creditBoost: `[UNTRANSLATED] Credit Boost`,
 | 
					    worldState_creditBoost: `[UNTRANSLATED] Credit Boost`,
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ dict = {
 | 
				
			|||||||
    general_addButton: `添加`,
 | 
					    general_addButton: `添加`,
 | 
				
			||||||
    general_setButton: `设置`,
 | 
					    general_setButton: `设置`,
 | 
				
			||||||
    general_bulkActions: `批量操作`,
 | 
					    general_bulkActions: `批量操作`,
 | 
				
			||||||
 | 
					    general_loading: `[UNTRANSLATED] Loading...`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    code_loginFail: `登录失败.请检查邮箱和密码.`,
 | 
					    code_loginFail: `登录失败.请检查邮箱和密码.`,
 | 
				
			||||||
    code_regFail: `注册失败.账号已存在.`,
 | 
					    code_regFail: `注册失败.账号已存在.`,
 | 
				
			||||||
@ -44,6 +45,8 @@ dict = {
 | 
				
			|||||||
    code_focusUnlocked: `已解锁|COUNT|个新专精学派!需要游戏内仓库更新才能生效,您可以通过访问星图来触发仓库更新.`,
 | 
					    code_focusUnlocked: `已解锁|COUNT|个新专精学派!需要游戏内仓库更新才能生效,您可以通过访问星图来触发仓库更新.`,
 | 
				
			||||||
    code_addModsConfirm: `确定要向账户添加|COUNT|张MOD吗?`,
 | 
					    code_addModsConfirm: `确定要向账户添加|COUNT|张MOD吗?`,
 | 
				
			||||||
    code_succImport: `导入成功。`,
 | 
					    code_succImport: `导入成功。`,
 | 
				
			||||||
 | 
					    code_succRelog: `[UNTRANSLATED] Done. Please note that you'll need to relog to see a difference in-game.`,
 | 
				
			||||||
 | 
					    code_nothingToDo: `[UNTRANSLATED] Done. There was nothing to do.`,
 | 
				
			||||||
    code_gild: `镀金`,
 | 
					    code_gild: `镀金`,
 | 
				
			||||||
    code_moa: `恐鸟`,
 | 
					    code_moa: `恐鸟`,
 | 
				
			||||||
    code_zanuka: `猎犬`,
 | 
					    code_zanuka: `猎犬`,
 | 
				
			||||||
@ -199,6 +202,7 @@ dict = {
 | 
				
			|||||||
    cheats_changeSupportedSyndicate: `支持的集团`,
 | 
					    cheats_changeSupportedSyndicate: `支持的集团`,
 | 
				
			||||||
    cheats_changeButton: `更改`,
 | 
					    cheats_changeButton: `更改`,
 | 
				
			||||||
    cheats_none: `无`,
 | 
					    cheats_none: `无`,
 | 
				
			||||||
 | 
					    cheats_markAllAsRead: `[UNTRANSLATED] Mark Inbox As Read`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    worldState: `世界状态配置`,
 | 
					    worldState: `世界状态配置`,
 | 
				
			||||||
    worldState_creditBoost: `现金加成`,
 | 
					    worldState_creditBoost: `现金加成`,
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user