Merge branch 'main' into 自用
This commit is contained in:
		
						commit
						12541dd87f
					
				@ -5,6 +5,7 @@
 | 
			
		||||
    "level": "trace"
 | 
			
		||||
  },
 | 
			
		||||
  "myAddress": "localhost",
 | 
			
		||||
  "bindAddress": "0.0.0.0",
 | 
			
		||||
  "httpPort": 80,
 | 
			
		||||
  "httpsPort": 443,
 | 
			
		||||
  "administratorNames": [],
 | 
			
		||||
@ -15,12 +16,6 @@
 | 
			
		||||
  "unlockAllSkins": false,
 | 
			
		||||
  "fullyStockedVendors": false,
 | 
			
		||||
  "skipClanKeyCrafting": false,
 | 
			
		||||
  "noDojoRoomBuildStage": false,
 | 
			
		||||
  "noDojoDecoBuildStage": false,
 | 
			
		||||
  "fastDojoRoomDestruction": false,
 | 
			
		||||
  "noDojoResearchCosts": false,
 | 
			
		||||
  "noDojoResearchTime": false,
 | 
			
		||||
  "fastClanAscension": false,
 | 
			
		||||
  "spoofMasteryRank": -1,
 | 
			
		||||
  "relicRewardItemCountMultiplier": 1,
 | 
			
		||||
  "nightwaveStandingMultiplier": 1,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										8
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -17,7 +17,7 @@
 | 
			
		||||
        "morgan": "^1.10.0",
 | 
			
		||||
        "ncp": "^2.0.0",
 | 
			
		||||
        "undici": "^7.10.0",
 | 
			
		||||
        "warframe-public-export-plus": "^0.5.83",
 | 
			
		||||
        "warframe-public-export-plus": "^0.5.86",
 | 
			
		||||
        "warframe-riven-info": "^0.1.2",
 | 
			
		||||
        "winston": "^3.17.0",
 | 
			
		||||
        "winston-daily-rotate-file": "^5.0.0",
 | 
			
		||||
@ -5532,9 +5532,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/warframe-public-export-plus": {
 | 
			
		||||
      "version": "0.5.84",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.84.tgz",
 | 
			
		||||
      "integrity": "sha512-ZpI1Y5CgWDmCwM4/oQpv9u0GD6KFvsJ9f1vJVXYhm5VD9DdOJcFzXgXgg98HXJ5JHbO16ZGIj83117qdpd0RQA=="
 | 
			
		||||
      "version": "0.5.86",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.86.tgz",
 | 
			
		||||
      "integrity": "sha512-tWJudKs4WdjFNiF6ipav9md3sboPXJFvSItTfSmT9ko+Xgg1QP75vS/qPsuPw67pqzMaSnAbHpEzNn/rZ4mCug=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/warframe-riven-info": {
 | 
			
		||||
      "version": "0.1.2",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
									
									
									
									
								
							@ -5,10 +5,10 @@
 | 
			
		||||
  "main": "index.ts",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "start": "node --enable-source-maps build/src/index.js",
 | 
			
		||||
    "build": "tsgo --sourceMap && ncp static/webui build/static/webui",
 | 
			
		||||
    "build:tsc": "tsc --incremental --sourceMap && ncp static/webui build/static/webui",
 | 
			
		||||
    "build:dev": "tsgo --sourceMap",
 | 
			
		||||
    "build:dev:tsc": "tsc --incremental --sourceMap",
 | 
			
		||||
    "build": "tsgo --inlineSourceMap && ncp static/webui build/static/webui",
 | 
			
		||||
    "build:tsc": "tsc --incremental --inlineSourceMap && ncp static/webui build/static/webui",
 | 
			
		||||
    "build:dev": "tsgo --inlineSourceMap",
 | 
			
		||||
    "build:dev:tsc": "tsc --incremental --inlineSourceMap",
 | 
			
		||||
    "build-and-start": "npm run build && npm run start",
 | 
			
		||||
    "build-and-start:bun": "npm run verify && npm run bun-run",
 | 
			
		||||
    "dev": "node scripts/dev.cjs",
 | 
			
		||||
@ -35,7 +35,7 @@
 | 
			
		||||
    "morgan": "^1.10.0",
 | 
			
		||||
    "ncp": "^2.0.0",
 | 
			
		||||
    "undici": "^7.10.0",
 | 
			
		||||
    "warframe-public-export-plus": "^0.5.83",
 | 
			
		||||
    "warframe-public-export-plus": "^0.5.86",
 | 
			
		||||
    "warframe-riven-info": "^0.1.2",
 | 
			
		||||
    "winston": "^3.17.0",
 | 
			
		||||
    "winston-daily-rotate-file": "^5.0.0",
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import type { IInventoryClient, IUpgradeClient } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import { addMods, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
export const artifactsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -57,6 +58,7 @@ export const artifactsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    res.send(itemId);
 | 
			
		||||
    broadcastInventoryUpdate(req);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IArtifactsRequest {
 | 
			
		||||
 | 
			
		||||
@ -95,10 +95,7 @@ export const confirmGuildInvitationPostController: RequestHandler = async (req,
 | 
			
		||||
        await GuildMember.deleteMany({ accountId: guildMember.accountId, status: 1 });
 | 
			
		||||
 | 
			
		||||
        // Update inventory of new member
 | 
			
		||||
        const inventory = await getInventory(
 | 
			
		||||
            guildMember.accountId.toString(),
 | 
			
		||||
            "GuildId LevelKeys Recipes skipClanKeyCrafting"
 | 
			
		||||
        );
 | 
			
		||||
        const inventory = await getInventory(guildMember.accountId.toString(), "GuildId LevelKeys Recipes");
 | 
			
		||||
        inventory.GuildId = new Types.ObjectId(req.query.clanId as string);
 | 
			
		||||
        giveClanKey(inventory);
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,7 @@ export const createGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
        rank: 0
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes skipClanKeyCrafting");
 | 
			
		||||
    const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes");
 | 
			
		||||
    inventory.GuildId = guild._id;
 | 
			
		||||
    const inventoryChanges: IInventoryChanges = {};
 | 
			
		||||
    giveClanKey(inventory, inventoryChanges);
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { sendWsBroadcastTo } from "../../services/wsService.ts";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { addMiscItems, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
@ -75,5 +75,5 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
 | 
			
		||||
        InventoryChanges: inventoryChanges,
 | 
			
		||||
        AffiliationMods: affiliationMods
 | 
			
		||||
    });
 | 
			
		||||
    sendWsBroadcastTo(accountId, { update_inventory: true });
 | 
			
		||||
    broadcastInventoryUpdate(req);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -28,7 +28,6 @@ import {
 | 
			
		||||
import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
			
		||||
import { config } from "../../services/configService.ts";
 | 
			
		||||
import type { ITechProjectClient } from "../../types/guildTypes.ts";
 | 
			
		||||
import { GuildPermission } from "../../types/guildTypes.ts";
 | 
			
		||||
import { GuildMember } from "../../models/guildModel.ts";
 | 
			
		||||
@ -83,16 +82,16 @@ export const guildTechController: RequestHandler = async (req, res) => {
 | 
			
		||||
                    guild.TechProjects[
 | 
			
		||||
                        guild.TechProjects.push({
 | 
			
		||||
                            ItemType: data.RecipeType,
 | 
			
		||||
                            ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price),
 | 
			
		||||
                            ReqCredits: guild.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price),
 | 
			
		||||
                            ReqItems: recipe.ingredients.map(x => ({
 | 
			
		||||
                                ItemType: x.ItemType,
 | 
			
		||||
                                ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount)
 | 
			
		||||
                                ItemCount: guild.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount)
 | 
			
		||||
                            })),
 | 
			
		||||
                            State: 0
 | 
			
		||||
                        }) - 1
 | 
			
		||||
                    ];
 | 
			
		||||
                setGuildTechLogState(guild, techProject.ItemType, 5);
 | 
			
		||||
                if (config.noDojoResearchCosts) {
 | 
			
		||||
                if (guild.noDojoResearchCosts) {
 | 
			
		||||
                    processFundedGuildTechProject(guild, techProject, recipe);
 | 
			
		||||
                } else {
 | 
			
		||||
                    if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") {
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@ import {
 | 
			
		||||
    applyCheatsToInfestedFoundry,
 | 
			
		||||
    handleSubsumeCompletion
 | 
			
		||||
} from "../../services/infestedFoundryService.ts";
 | 
			
		||||
import { sendWsBroadcastToGame } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
export const infestedFoundryController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const account = await getAccountForRequest(req);
 | 
			
		||||
@ -363,6 +364,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
 | 
			
		||||
                );
 | 
			
		||||
                addRecipes(inventory, recipeChanges);
 | 
			
		||||
                await inventory.save();
 | 
			
		||||
                sendWsBroadcastToGame(account._id.toString(), { sync_inventory: true });
 | 
			
		||||
            }
 | 
			
		||||
            res.end();
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@ import { createAccount, createNonce, getUsernameFromEmail, isCorrectPassword } f
 | 
			
		||||
import type { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "../../types/loginTypes.ts";
 | 
			
		||||
import { logger } from "../../utils/logger.ts";
 | 
			
		||||
import { version_compare } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
import { sendWsBroadcastTo } from "../../services/wsService.ts";
 | 
			
		||||
import { handleNonceInvalidation } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
export const loginController: RequestHandler = async (request, response) => {
 | 
			
		||||
    const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object
 | 
			
		||||
@ -74,7 +74,7 @@ export const loginController: RequestHandler = async (request, response) => {
 | 
			
		||||
    account.LastLogin = new Date();
 | 
			
		||||
    await account.save();
 | 
			
		||||
 | 
			
		||||
    sendWsBroadcastTo(account._id.toString(), { nonce_updated: true });
 | 
			
		||||
    handleNonceInvalidation(account._id.toString());
 | 
			
		||||
 | 
			
		||||
    response.json(createLoginResponse(myAddress, myUrlBase, account.toJSON(), buildLabel));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { Account } from "../../models/loginModel.ts";
 | 
			
		||||
import { sendWsBroadcastTo } from "../../services/wsService.ts";
 | 
			
		||||
import { handleNonceInvalidation } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
export const logoutController: RequestHandler = async (req, res) => {
 | 
			
		||||
    if (!req.query.accountId) {
 | 
			
		||||
@ -21,7 +21,7 @@ export const logoutController: RequestHandler = async (req, res) => {
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
    if (stat.modifiedCount) {
 | 
			
		||||
        sendWsBroadcastTo(req.query.accountId as string, { nonce_updated: true });
 | 
			
		||||
        handleNonceInvalidation(req.query.accountId as string);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    res.writeHead(200, {
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
export const maturePetController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -19,6 +20,7 @@ export const maturePetController: RequestHandler = async (req, res) => {
 | 
			
		||||
            : [details.DominantTraits.FurPattern, details.DominantTraits.FurPattern, details.DominantTraits.FurPattern],
 | 
			
		||||
        unmature: data.revert
 | 
			
		||||
    });
 | 
			
		||||
    broadcastInventoryUpdate(req);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IMaturePetRequest {
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { sendWsBroadcastTo } from "../../services/wsService.ts";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import {
 | 
			
		||||
    getInventory,
 | 
			
		||||
@ -197,5 +197,5 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res)
 | 
			
		||||
            MiscItems: miscItemChanges
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    sendWsBroadcastTo(accountId, { update_inventory: true });
 | 
			
		||||
    broadcastInventoryUpdate(req);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory, updateCurrency } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import type { TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import { sendWsBroadcastTo } from "../../services/wsService.ts";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
interface INameWeaponRequest {
 | 
			
		||||
    ItemName: string;
 | 
			
		||||
@ -28,5 +28,5 @@ export const nameWeaponController: RequestHandler = async (req, res) => {
 | 
			
		||||
    res.json({
 | 
			
		||||
        InventoryChanges: currencyChanges
 | 
			
		||||
    });
 | 
			
		||||
    sendWsBroadcastTo(accountId, { update_inventory: true });
 | 
			
		||||
    broadcastInventoryUpdate(req);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,6 @@ import { GuildPermission } from "../../types/guildTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { Types } from "mongoose";
 | 
			
		||||
import { ExportDojoRecipes, ExportResources } from "warframe-public-export-plus";
 | 
			
		||||
import { config } from "../../services/configService.ts";
 | 
			
		||||
 | 
			
		||||
export const placeDecoInComponentController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -74,7 +73,7 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) =
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (deco.Type != "/Lotus/Objects/Tenno/Props/TnoPaintBotDojoDeco") {
 | 
			
		||||
            if (!meta || (meta.price == 0 && meta.ingredients.length == 0) || config.noDojoDecoBuildStage) {
 | 
			
		||||
            if (!meta || (meta.price == 0 && meta.ingredients.length == 0) || guild.noDojoDecoBuildStage) {
 | 
			
		||||
                deco.CompletionTime = new Date();
 | 
			
		||||
                if (meta) {
 | 
			
		||||
                    processDojoBuildMaterialsGathered(guild, meta);
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,3 @@
 | 
			
		||||
import { config } from "../../services/configService.ts";
 | 
			
		||||
import {
 | 
			
		||||
    getDojoClient,
 | 
			
		||||
    getGuildForRequestEx,
 | 
			
		||||
@ -21,7 +20,7 @@ export const queueDojoComponentDestructionController: RequestHandler = async (re
 | 
			
		||||
    const componentId = req.query.componentId as string;
 | 
			
		||||
 | 
			
		||||
    guild.DojoComponents.id(componentId)!.DestructionTime = new Date(
 | 
			
		||||
        (Math.trunc(Date.now() / 1000) + (config.fastDojoRoomDestruction ? 5 : 2 * 3600)) * 1000
 | 
			
		||||
        (Math.trunc(Date.now() / 1000) + (guild.fastDojoRoomDestruction ? 5 : 2 * 3600)) * 1000
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    await guild.save();
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { getInventory, updateCurrency } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { sendWsBroadcastTo } from "../../services/wsService.ts";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const releasePetController: RequestHandler = async (req, res) => {
 | 
			
		||||
@ -20,7 +20,7 @@ export const releasePetController: RequestHandler = async (req, res) => {
 | 
			
		||||
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json({ inventoryChanges }); // Not a mistake; it's "inventoryChanges" here.
 | 
			
		||||
    sendWsBroadcastTo(accountId, { update_inventory: true });
 | 
			
		||||
    broadcastInventoryUpdate(req);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IReleasePetRequest {
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { getInventory, updateCurrency } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { sendWsBroadcastTo } from "../../services/wsService.ts";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,7 @@ export const renamePetController: RequestHandler = async (req, res) => {
 | 
			
		||||
        ...data,
 | 
			
		||||
        inventoryChanges: inventoryChanges
 | 
			
		||||
    });
 | 
			
		||||
    sendWsBroadcastTo(accountId, { update_inventory: true });
 | 
			
		||||
    broadcastInventoryUpdate(req);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IRenamePetRequest {
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,7 @@ import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import { ExportDojoRecipes } from "warframe-public-export-plus";
 | 
			
		||||
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
			
		||||
import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts";
 | 
			
		||||
import { sendWsBroadcastEx } from "../../services/wsService.ts";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
import { parseFusionTreasure } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
 | 
			
		||||
export const sellController: RequestHandler = async (req, res) => {
 | 
			
		||||
@ -307,7 +307,7 @@ export const sellController: RequestHandler = async (req, res) => {
 | 
			
		||||
    res.json({
 | 
			
		||||
        inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges"
 | 
			
		||||
    });
 | 
			
		||||
    sendWsBroadcastEx({ update_inventory: true }, accountId, parseInt(String(req.query.wsid)));
 | 
			
		||||
    broadcastInventoryUpdate(req);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ISellRequest {
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { Inventory } from "../../models/inventoryModels/inventoryModel.ts";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
export const setSupportedSyndicateController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -15,4 +16,5 @@ export const setSupportedSyndicateController: RequestHandler = async (req, res)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    res.end();
 | 
			
		||||
    broadcastInventoryUpdate(req);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,6 @@ import {
 | 
			
		||||
} from "../../services/guildService.ts";
 | 
			
		||||
import { Types } from "mongoose";
 | 
			
		||||
import { ExportDojoRecipes } from "warframe-public-export-plus";
 | 
			
		||||
import { config } from "../../services/configService.ts";
 | 
			
		||||
import { getAccountForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
 | 
			
		||||
@ -57,7 +56,7 @@ export const startDojoRecipeController: RequestHandler = async (req, res) => {
 | 
			
		||||
                DecoCapacity: room?.decoCapacity
 | 
			
		||||
            }) - 1
 | 
			
		||||
        ];
 | 
			
		||||
    if (config.noDojoRoomBuildStage) {
 | 
			
		||||
    if (guild.noDojoRoomBuildStage) {
 | 
			
		||||
        component.CompletionTime = new Date(Date.now());
 | 
			
		||||
        if (room) {
 | 
			
		||||
            processDojoBuildMaterialsGathered(guild, room);
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
import type { TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
@ -19,6 +20,7 @@ export const abilityOverrideController: RequestHandler = async (req, res) => {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    res.end();
 | 
			
		||||
    broadcastInventoryUpdate(req);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IAbilityOverrideRequest {
 | 
			
		||||
 | 
			
		||||
@ -1,21 +1,42 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { addFusionPoints, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getGuildForRequestEx, hasGuildPermission } from "../../services/guildService.ts";
 | 
			
		||||
import { GuildPermission } from "../../types/guildTypes.ts";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
export const addCurrencyController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const request = req.body as IAddCurrencyRequest;
 | 
			
		||||
    const inventory = await getInventory(accountId, request.currency);
 | 
			
		||||
    let projection = request.currency as string;
 | 
			
		||||
    if (request.currency.startsWith("Vault")) projection = "GuildId";
 | 
			
		||||
    const inventory = await getInventory(accountId, projection);
 | 
			
		||||
    if (request.currency == "FusionPoints") {
 | 
			
		||||
        addFusionPoints(inventory, request.delta);
 | 
			
		||||
    } else if (request.currency == "VaultRegularCredits" || request.currency == "VaultPremiumCredits") {
 | 
			
		||||
        const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
        if (await hasGuildPermission(guild, accountId, GuildPermission.Treasurer)) {
 | 
			
		||||
            guild[request.currency] ??= 0;
 | 
			
		||||
            guild[request.currency]! += request.delta;
 | 
			
		||||
            await guild.save();
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        inventory[request.currency] += request.delta;
 | 
			
		||||
    }
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    if (!request.currency.startsWith("Vault")) {
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
        broadcastInventoryUpdate(req);
 | 
			
		||||
    }
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IAddCurrencyRequest {
 | 
			
		||||
    currency: "RegularCredits" | "PremiumCredits" | "FusionPoints" | "PrimeTokens";
 | 
			
		||||
    currency:
 | 
			
		||||
        | "RegularCredits"
 | 
			
		||||
        | "PremiumCredits"
 | 
			
		||||
        | "FusionPoints"
 | 
			
		||||
        | "PrimeTokens"
 | 
			
		||||
        | "VaultRegularCredits"
 | 
			
		||||
        | "VaultPremiumCredits";
 | 
			
		||||
    delta: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory, addItem } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
export const addItemsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -11,6 +12,7 @@ export const addItemsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    }
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
    broadcastInventoryUpdate(req);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IAddItemRequest {
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory, addRecipes } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { ExportRecipes } from "warframe-public-export-plus";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
export const addMissingHelminthBlueprintsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -21,4 +22,5 @@ export const addMissingHelminthBlueprintsController: RequestHandler = async (req
 | 
			
		||||
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
    broadcastInventoryUpdate(req);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { ExportArcanes, ExportUpgrades } from "warframe-public-export-plus";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
export const addMissingMaxRankModsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -41,4 +42,5 @@ export const addMissingMaxRankModsController: RequestHandler = async (req, res)
 | 
			
		||||
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
    broadcastInventoryUpdate(req);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										36
									
								
								src/controllers/custom/addVaultDecoRecipeController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/controllers/custom/addVaultDecoRecipeController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,36 @@
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { hasAccessToDojo, getGuildForRequestEx, hasGuildPermission } from "../../services/guildService.ts";
 | 
			
		||||
import { GuildPermission } from "../../types/guildTypes.ts";
 | 
			
		||||
import type { ITypeCount } from "../../types/commonTypes.ts";
 | 
			
		||||
 | 
			
		||||
export const addVaultDecoRecipeController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const requests = req.body as ITypeCount[];
 | 
			
		||||
    const inventory = await getInventory(accountId, "LevelKeys GuildId");
 | 
			
		||||
    const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
    if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Architect))) {
 | 
			
		||||
        res.status(400).send("-1").end();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    guild.VaultDecoRecipes ??= [];
 | 
			
		||||
    for (const request of requests) {
 | 
			
		||||
        const index = guild.VaultDecoRecipes.findIndex(x => x.ItemType === request.ItemType);
 | 
			
		||||
 | 
			
		||||
        if (index == -1) {
 | 
			
		||||
            guild.VaultDecoRecipes.push({
 | 
			
		||||
                ItemType: request.ItemType,
 | 
			
		||||
                ItemCount: request.ItemCount
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            guild.VaultDecoRecipes[index].ItemCount += request.ItemCount;
 | 
			
		||||
 | 
			
		||||
            if (guild.VaultDecoRecipes[index].ItemCount < 1) {
 | 
			
		||||
                guild.VaultDecoRecipes.splice(index, 1);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    await guild.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
import { applyClientEquipmentUpdates, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
import type { IOid } from "../../types/commonTypes.ts";
 | 
			
		||||
import type { IEquipmentClient } from "../../types/equipmentTypes.ts";
 | 
			
		||||
import type { TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
@ -25,6 +26,7 @@ export const addXpController: RequestHandler = async (req, res) => {
 | 
			
		||||
    }
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
    broadcastInventoryUpdate(req);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type IAddXpRequest = {
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
import type { TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
@ -20,6 +21,7 @@ export const changeModularPartsController: RequestHandler = async (req, res) =>
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
    }
 | 
			
		||||
    res.end();
 | 
			
		||||
    broadcastInventoryUpdate(req);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IUpdateFingerPrintRequest {
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
const DEFAULT_UPGRADE_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
 | 
			
		||||
 | 
			
		||||
@ -31,4 +32,5 @@ export const editSuitInvigorationUpgradeController: RequestHandler = async (req,
 | 
			
		||||
    }
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
    broadcastInventoryUpdate(req);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										16
									
								
								src/controllers/custom/getAllianceController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/controllers/custom/getAllianceController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
			
		||||
import { Alliance, Guild } from "../../models/guildModel.ts";
 | 
			
		||||
import { getAllianceClient } from "../../services/guildService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const getAllianceController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const guildId = req.query.guildId;
 | 
			
		||||
    if (guildId) {
 | 
			
		||||
        const guild = await Guild.findById(guildId, "Name Tier AllianceId");
 | 
			
		||||
        if (guild && guild.AllianceId) {
 | 
			
		||||
            const alliance = (await Alliance.findById(guild.AllianceId))!;
 | 
			
		||||
            res.json(await getAllianceClient(alliance, guild));
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										40
									
								
								src/controllers/custom/getGuildController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/controllers/custom/getGuildController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,40 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { Guild, GuildMember } from "../../models/guildModel.ts";
 | 
			
		||||
import { toMongoDate, toOid2 } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
import { addAccountDataToFriendInfo, addInventoryDataToFriendInfo } from "../../services/friendService.ts";
 | 
			
		||||
import type { IGuildMemberClient } from "../../types/guildTypes.ts";
 | 
			
		||||
 | 
			
		||||
export const getGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const guildId = req.query.guildId;
 | 
			
		||||
    if (guildId) {
 | 
			
		||||
        const guild = await Guild.findById(guildId);
 | 
			
		||||
        if (guild) {
 | 
			
		||||
            const guildMembers = await GuildMember.find({ guildId: guild._id });
 | 
			
		||||
 | 
			
		||||
            const members: IGuildMemberClient[] = [];
 | 
			
		||||
            const dataFillInPromises: Promise<void>[] = [];
 | 
			
		||||
            for (const guildMember of guildMembers) {
 | 
			
		||||
                const member: IGuildMemberClient = {
 | 
			
		||||
                    _id: toOid2(guildMember.accountId, undefined),
 | 
			
		||||
                    Rank: guildMember.rank,
 | 
			
		||||
                    Status: guildMember.status,
 | 
			
		||||
                    Note: guildMember.RequestMsg,
 | 
			
		||||
                    RequestExpiry: guildMember.RequestExpiry ? toMongoDate(guildMember.RequestExpiry) : undefined
 | 
			
		||||
                };
 | 
			
		||||
                dataFillInPromises.push(addAccountDataToFriendInfo(member));
 | 
			
		||||
                dataFillInPromises.push(addInventoryDataToFriendInfo(member));
 | 
			
		||||
 | 
			
		||||
                members.push(member);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await Promise.all(dataFillInPromises);
 | 
			
		||||
 | 
			
		||||
            res.json({
 | 
			
		||||
                ...guild.toObject(),
 | 
			
		||||
                Members: members
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            res.status(400).end();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
@ -7,7 +7,9 @@ import {
 | 
			
		||||
    ExportAvionics,
 | 
			
		||||
    ExportBoosters,
 | 
			
		||||
    ExportCustoms,
 | 
			
		||||
    ExportDojoRecipes,
 | 
			
		||||
    ExportDrones,
 | 
			
		||||
    ExportFactions,
 | 
			
		||||
    ExportGear,
 | 
			
		||||
    ExportKeys,
 | 
			
		||||
    ExportMisc,
 | 
			
		||||
@ -59,19 +61,21 @@ interface ItemLists {
 | 
			
		||||
    Boosters: ListedItem[];
 | 
			
		||||
    VarziaOffers: ListedItem[];
 | 
			
		||||
    Abilities: ListedItem[];
 | 
			
		||||
    TechProjects: ListedItem[];
 | 
			
		||||
    VaultDecoRecipes: ListedItem[];
 | 
			
		||||
    //circuitGameModes: ListedItem[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const relicQualitySuffixes: Record<TRelicQuality, string> = {
 | 
			
		||||
    VPQ_BRONZE: "",
 | 
			
		||||
    VPQ_SILVER: " [Exceptional]",
 | 
			
		||||
    VPQ_GOLD: " [Flawless]",
 | 
			
		||||
    VPQ_PLATINUM: " [Radiant]"
 | 
			
		||||
    VPQ_SILVER: "/Lotus/Language/Relics/VoidProjectionQuality_Silver",
 | 
			
		||||
    VPQ_GOLD: "/Lotus/Language/Relics/VoidProjectionQuality_Gold",
 | 
			
		||||
    VPQ_PLATINUM: "/Lotus/Language/Relics/VoidProjectionQuality_Platinum"
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/*const toTitleCase = (str: string): string => {
 | 
			
		||||
const toTitleCase = (str: string): string => {
 | 
			
		||||
    return str.replace(/[^\s-]+/g, word => word.charAt(0).toUpperCase() + word.substring(1).toLowerCase());
 | 
			
		||||
};*/
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getItemListsController: RequestHandler = (req, response) => {
 | 
			
		||||
    const lang = getDict(typeof req.query.lang == "string" ? req.query.lang : "en");
 | 
			
		||||
@ -97,7 +101,9 @@ const getItemListsController: RequestHandler = (req, response) => {
 | 
			
		||||
        mods: [],
 | 
			
		||||
        Boosters: [],
 | 
			
		||||
        VarziaOffers: [],
 | 
			
		||||
        Abilities: []
 | 
			
		||||
        Abilities: [],
 | 
			
		||||
        TechProjects: [],
 | 
			
		||||
        VaultDecoRecipes: []
 | 
			
		||||
        /*circuitGameModes: [
 | 
			
		||||
            {
 | 
			
		||||
                uniqueName: "Survival",
 | 
			
		||||
@ -232,14 +238,19 @@ const getItemListsController: RequestHandler = (req, response) => {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    for (const [uniqueName, item] of Object.entries(ExportRelics)) {
 | 
			
		||||
        const qualitySuffix =
 | 
			
		||||
            item.quality !== "VPQ_BRONZE"
 | 
			
		||||
                ? ` [${toTitleCase(getString(relicQualitySuffixes[item.quality], lang))}]`
 | 
			
		||||
                : "";
 | 
			
		||||
 | 
			
		||||
        res.miscitems.push({
 | 
			
		||||
            uniqueName: uniqueName,
 | 
			
		||||
            name:
 | 
			
		||||
                getString("/Lotus/Language/Relics/VoidProjectionName", lang)
 | 
			
		||||
                    .split("|ERA|")
 | 
			
		||||
                    .join(item.era)
 | 
			
		||||
                    .join(getString(`/Lotus/Language/Relics/Era_${item.era.toUpperCase()}`, lang))
 | 
			
		||||
                    .split("|CATEGORY|")
 | 
			
		||||
                    .join(item.category) + relicQualitySuffixes[item.quality]
 | 
			
		||||
                    .join(item.category) + qualitySuffix
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    for (const [uniqueName, item] of Object.entries(ExportGear)) {
 | 
			
		||||
@ -367,6 +378,66 @@ const getItemListsController: RequestHandler = (req, response) => {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const uniqueName of Object.keys(ExportDojoRecipes.research)) {
 | 
			
		||||
        if (
 | 
			
		||||
            !["Zekti", "Vidar", "Lavan"].some(house => uniqueName.includes(house)) &&
 | 
			
		||||
            !uniqueName.startsWith("/Lotus/Types/Items/ShipFeatureItems/Railjack/")
 | 
			
		||||
        ) {
 | 
			
		||||
            let resultType;
 | 
			
		||||
            if (uniqueName in ExportRecipes) {
 | 
			
		||||
                resultType = ExportRecipes[uniqueName].resultType;
 | 
			
		||||
            } else if (uniqueName in ExportDojoRecipes.fabrications) {
 | 
			
		||||
                resultType = ExportDojoRecipes.fabrications[uniqueName].resultType;
 | 
			
		||||
            } else if (uniqueName.startsWith("/Lotus/Types/Game/")) {
 | 
			
		||||
                resultType = uniqueName.replace("Blueprint", "");
 | 
			
		||||
            } else {
 | 
			
		||||
                resultType = uniqueName;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let name = getString(getItemName(resultType) || resultType, lang);
 | 
			
		||||
 | 
			
		||||
            if (uniqueName in ExportRecipes) {
 | 
			
		||||
                const recipeNum = ExportRecipes[uniqueName].num;
 | 
			
		||||
                if (recipeNum > 1) {
 | 
			
		||||
                    name = `${name} X ${recipeNum}`;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            res.TechProjects.push({
 | 
			
		||||
                uniqueName,
 | 
			
		||||
                name
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const uniqueName of [
 | 
			
		||||
        ...Object.entries(ExportDojoRecipes.decos)
 | 
			
		||||
            .filter(([_, data]) => data.requiredInVault)
 | 
			
		||||
            .map(([uniqueName]) => uniqueName),
 | 
			
		||||
        // not requiredInVault:
 | 
			
		||||
        "/Lotus/Levels/ClanDojo/ComponentPropRecipes/PlagueStarEventTrophyBronzeRecipe",
 | 
			
		||||
        "/Lotus/Levels/ClanDojo/ComponentPropRecipes/PlagueStarEventTrophyGoldRecipe",
 | 
			
		||||
        "/Lotus/Levels/ClanDojo/ComponentPropRecipes/PlagueStarEventTrophySilverRecipe",
 | 
			
		||||
        "/Lotus/Levels/ClanDojo/ComponentPropRecipes/PlagueStarEventTrophyTerracottaRecipe"
 | 
			
		||||
        // removed in 38.6.0:
 | 
			
		||||
        // "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ThumperTrophyBronzeRecipe",
 | 
			
		||||
        // "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ThumperTrophyCrystalRecipe",
 | 
			
		||||
        // "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ThumperTrophyGoldRecipe",
 | 
			
		||||
        // "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ThumperTrophySilverRecipe",
 | 
			
		||||
        // "/Lotus/Levels/ClanDojo/ComponentPropRecipes/NaturalPlaceables/CoralChunkARecipe"
 | 
			
		||||
    ]) {
 | 
			
		||||
        let name = getString(getItemName(uniqueName) || uniqueName, lang);
 | 
			
		||||
        if (uniqueName.startsWith("/Lotus/Levels/ClanDojo/ComponentPropRecipes/GradivusDilemma")) {
 | 
			
		||||
            const factionTag = uniqueName.includes("Corpus") ? "FC_CORPUS" : "FC_GRINEER";
 | 
			
		||||
            const faction = ExportFactions[factionTag].name;
 | 
			
		||||
            name += ` [${getString(faction, lang)}]`;
 | 
			
		||||
        }
 | 
			
		||||
        res.VaultDecoRecipes.push({
 | 
			
		||||
            uniqueName,
 | 
			
		||||
            name
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    response.json(res);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getLoadout } from "../../services/loadoutService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getPersonalRooms } from "../../services/personalRoomsService.ts";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
import type { IInventoryClient } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import type { IGetShipResponse } from "../../types/personalRoomsTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
@ -32,6 +33,7 @@ export const importController: RequestHandler = async (req, res) => {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    res.end();
 | 
			
		||||
    broadcastInventoryUpdate(req);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IImportRequest {
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@ import {
 | 
			
		||||
import { logger } from "../../utils/logger.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { ExportKeys } from "warframe-public-export-plus";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
export const manageQuestsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -157,4 +158,5 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
 | 
			
		||||
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.status(200).end();
 | 
			
		||||
    broadcastInventoryUpdate(req);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
export const popArchonCrystalUpgradeController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -12,6 +13,7 @@ export const popArchonCrystalUpgradeController: RequestHandler = async (req, res
 | 
			
		||||
        );
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
        res.end();
 | 
			
		||||
        broadcastInventoryUpdate(req);
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    res.status(400).end();
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
export const pushArchonCrystalUpgradeController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -15,6 +16,7 @@ export const pushArchonCrystalUpgradeController: RequestHandler = async (req, re
 | 
			
		||||
            }
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            res.end();
 | 
			
		||||
            broadcastInventoryUpdate(req);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { sendWsBroadcastTo } from "../../services/wsService.ts";
 | 
			
		||||
import type { IAccountCheats } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
@ -10,6 +11,9 @@ export const setAccountCheatController: RequestHandler = async (req, res) => {
 | 
			
		||||
    inventory[payload.key] = payload.value as never;
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
    if (["infiniteCredits", "infinitePlatinum", "infiniteEndo", "infiniteRegalAya"].indexOf(payload.key) != -1) {
 | 
			
		||||
        sendWsBroadcastTo(accountId, { update_inventory: true, sync_inventory: true });
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ISetAccountCheatRequest {
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { ExportBoosters } from "warframe-public-export-plus";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
const I32_MAX = 0x7fffffff;
 | 
			
		||||
 | 
			
		||||
@ -42,4 +43,5 @@ export const setBoosterController: RequestHandler = async (req, res) => {
 | 
			
		||||
    }
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
    broadcastInventoryUpdate(req);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
export const setEvolutionProgressController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -25,6 +26,7 @@ export const setEvolutionProgressController: RequestHandler = async (req, res) =
 | 
			
		||||
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
    broadcastInventoryUpdate(req);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type ISetEvolutionProgressRequest = {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										29
									
								
								src/controllers/custom/setGuildCheatController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/controllers/custom/setGuildCheatController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
			
		||||
import { GuildMember } from "../../models/guildModel.ts";
 | 
			
		||||
import { getGuildForRequestEx, hasAccessToDojo } from "../../services/guildService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { IGuildCheats } from "../../types/guildTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const setGuildCheatController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const payload = req.body as ISetGuildCheatRequest;
 | 
			
		||||
    const inventory = await getInventory(accountId, `${payload.key} GuildId LevelKeys`);
 | 
			
		||||
    const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
    const member = await GuildMember.findOne({ accountId: accountId, guildId: guild._id });
 | 
			
		||||
 | 
			
		||||
    if (member) {
 | 
			
		||||
        if (!hasAccessToDojo(inventory) || member.rank > 1) {
 | 
			
		||||
            res.end();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        guild[payload.key] = payload.value;
 | 
			
		||||
        await guild.save();
 | 
			
		||||
    }
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ISetGuildCheatRequest {
 | 
			
		||||
    key: keyof IGuildCheats;
 | 
			
		||||
    value: boolean;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										128
									
								
								src/controllers/custom/techProjectController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								src/controllers/custom/techProjectController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,128 @@
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import {
 | 
			
		||||
    hasAccessToDojo,
 | 
			
		||||
    getGuildForRequestEx,
 | 
			
		||||
    setGuildTechLogState,
 | 
			
		||||
    processFundedGuildTechProject,
 | 
			
		||||
    scaleRequiredCount,
 | 
			
		||||
    hasGuildPermission,
 | 
			
		||||
    addGuildMemberMiscItemContribution,
 | 
			
		||||
    processGuildTechProjectContributionsUpdate,
 | 
			
		||||
    processCompletedGuildTechProject
 | 
			
		||||
} from "../../services/guildService.ts";
 | 
			
		||||
import { ExportDojoRecipes } from "warframe-public-export-plus";
 | 
			
		||||
import { GuildPermission } from "../../types/guildTypes.ts";
 | 
			
		||||
import { GuildMember } from "../../models/guildModel.ts";
 | 
			
		||||
 | 
			
		||||
export const addTechProjectController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const requests = req.body as ITechProjectRequest[];
 | 
			
		||||
    const inventory = await getInventory(accountId, "LevelKeys GuildId");
 | 
			
		||||
    const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
    if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
 | 
			
		||||
        res.status(400).send("-1").end();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    guild.TechProjects ??= [];
 | 
			
		||||
    for (const request of requests) {
 | 
			
		||||
        const recipe = ExportDojoRecipes.research[request.ItemType];
 | 
			
		||||
        if (!guild.TechProjects.find(x => x.ItemType == request.ItemType)) {
 | 
			
		||||
            const techProject =
 | 
			
		||||
                guild.TechProjects[
 | 
			
		||||
                    guild.TechProjects.push({
 | 
			
		||||
                        ItemType: request.ItemType,
 | 
			
		||||
                        ReqCredits: guild.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price),
 | 
			
		||||
                        ReqItems: recipe.ingredients.map(x => ({
 | 
			
		||||
                            ItemType: x.ItemType,
 | 
			
		||||
                            ItemCount: guild.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount)
 | 
			
		||||
                        })),
 | 
			
		||||
                        State: 0
 | 
			
		||||
                    }) - 1
 | 
			
		||||
                ];
 | 
			
		||||
            setGuildTechLogState(guild, techProject.ItemType, 5);
 | 
			
		||||
            if (guild.noDojoResearchCosts) {
 | 
			
		||||
                processFundedGuildTechProject(guild, techProject, recipe);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    await guild.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const removeTechProjectController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const requests = req.body as ITechProjectRequest[];
 | 
			
		||||
    const inventory = await getInventory(accountId, "LevelKeys GuildId");
 | 
			
		||||
    const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
    if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
 | 
			
		||||
        res.status(400).send("-1").end();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    guild.TechProjects ??= [];
 | 
			
		||||
    for (const request of requests) {
 | 
			
		||||
        const index = guild.TechProjects.findIndex(x => x.ItemType === request.ItemType);
 | 
			
		||||
        if (index !== -1) {
 | 
			
		||||
            guild.TechProjects.splice(index, 1);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    await guild.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const fundTechProjectController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const requests = req.body as ITechProjectRequest[];
 | 
			
		||||
    const inventory = await getInventory(accountId, "LevelKeys GuildId");
 | 
			
		||||
    const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
    const guildMember = (await GuildMember.findOne(
 | 
			
		||||
        { accountId, guildId: guild._id },
 | 
			
		||||
        "RegularCreditsContributed MiscItemsContributed"
 | 
			
		||||
    ))!;
 | 
			
		||||
    if (!hasAccessToDojo(inventory)) {
 | 
			
		||||
        res.status(400).send("-1").end();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    for (const request of requests) {
 | 
			
		||||
        const techProject = guild.TechProjects!.find(x => x.ItemType == request.ItemType)!;
 | 
			
		||||
 | 
			
		||||
        guildMember.RegularCreditsContributed ??= 0;
 | 
			
		||||
        guildMember.RegularCreditsContributed += techProject.ReqCredits;
 | 
			
		||||
        techProject.ReqCredits = 0;
 | 
			
		||||
 | 
			
		||||
        for (const reqItem of techProject.ReqItems) {
 | 
			
		||||
            addGuildMemberMiscItemContribution(guildMember, reqItem);
 | 
			
		||||
            reqItem.ItemCount = 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await processGuildTechProjectContributionsUpdate(guild, techProject);
 | 
			
		||||
    }
 | 
			
		||||
    await Promise.all([guild.save(), guildMember.save()]);
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const completeTechProjectsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const requests = req.body as ITechProjectRequest[];
 | 
			
		||||
    const inventory = await getInventory(accountId, "LevelKeys GuildId");
 | 
			
		||||
    const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
    if (!hasAccessToDojo(inventory)) {
 | 
			
		||||
        res.status(400).send("-1").end();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    for (const request of requests) {
 | 
			
		||||
        const techProject = guild.TechProjects!.find(x => x.ItemType == request.ItemType)!;
 | 
			
		||||
        techProject.CompletionDate = new Date();
 | 
			
		||||
 | 
			
		||||
        if (setGuildTechLogState(guild, techProject.ItemType, 4, techProject.CompletionDate)) {
 | 
			
		||||
            processCompletedGuildTechProject(guild, techProject.ItemType);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    await guild.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ITechProjectRequest {
 | 
			
		||||
    ItemType: string;
 | 
			
		||||
}
 | 
			
		||||
@ -2,19 +2,25 @@ import type { RequestHandler } from "express";
 | 
			
		||||
import { ExportResources, ExportVirtuals } from "warframe-public-export-plus";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { addItem, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { sendWsBroadcastToGame } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
export const unlockAllCapturaScenesController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
 | 
			
		||||
    let needSync = false;
 | 
			
		||||
    for (const uniqueName of Object.keys(ExportResources)) {
 | 
			
		||||
        if (resourceInheritsFrom(uniqueName, "/Lotus/Types/Items/MiscItems/PhotoboothTile")) {
 | 
			
		||||
            await addItem(inventory, uniqueName, 1);
 | 
			
		||||
            needSync = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
    if (needSync) {
 | 
			
		||||
        sendWsBroadcastToGame(accountId, { sync_inventory: true });
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const resourceInheritsFrom = (resourceName: string, targetName: string): boolean => {
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { sendWsBroadcastToGame } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
export const unlockAllIntrinsicsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -16,4 +17,5 @@ export const unlockAllIntrinsicsController: RequestHandler = async (req, res) =>
 | 
			
		||||
    inventory.PlayerSkills.LPS_DRIFT_ENDURANCE = 10;
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
    sendWsBroadcastToGame(accountId, { sync_inventory: true });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { sendWsBroadcastToGame } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
const allEudicoHeistJobs = [
 | 
			
		||||
    "/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyOne",
 | 
			
		||||
@ -21,4 +22,5 @@ export const unlockAllProfitTakerStagesController: RequestHandler = async (req,
 | 
			
		||||
    }
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
    sendWsBroadcastToGame(accountId, { sync_inventory: true });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { sendWsBroadcastToGame } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
export const unlockAllSimarisResearchEntriesController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -17,4 +18,5 @@ export const unlockAllSimarisResearchEntriesController: RequestHandler = async (
 | 
			
		||||
    ].map(type => ({ TargetType: type, Scans: 10, Completed: true }));
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
    sendWsBroadcastToGame(accountId, { sync_inventory: true });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { WeaponTypeInternal } from "../../services/itemDataService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { sendWsBroadcastToGame } from "../../services/wsService.ts";
 | 
			
		||||
 | 
			
		||||
export const updateFingerprintController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -22,6 +23,7 @@ export const updateFingerprintController: RequestHandler = async (req, res) => {
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
    }
 | 
			
		||||
    res.end();
 | 
			
		||||
    sendWsBroadcastToGame(accountId, { sync_inventory: true });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IUpdateFingerPrintRequest {
 | 
			
		||||
 | 
			
		||||
@ -212,12 +212,12 @@ export const getInfNodes = (manifest: INemesisManifest, rank: number): IInfNode[
 | 
			
		||||
            value.systemIndex === systemIndex &&
 | 
			
		||||
            value.nodeType != 3 && // not hub
 | 
			
		||||
            value.nodeType != 7 && // not junction
 | 
			
		||||
            value.missionIndex && // must have a mission type and not assassination
 | 
			
		||||
            value.missionIndex != 28 && // not open world
 | 
			
		||||
            value.missionIndex != 32 && // not railjack
 | 
			
		||||
            value.missionIndex != 41 && // not saya's visions
 | 
			
		||||
            value.missionIndex != 42 && // not face off
 | 
			
		||||
            value.name.indexOf("1999NodeI") == -1 && // not stage defence
 | 
			
		||||
            value.missionType != "MT_ASSASSINATION" &&
 | 
			
		||||
            value.missionType != "MT_LANDSCAPE" &&
 | 
			
		||||
            value.missionType != "MT_RAILJACK" &&
 | 
			
		||||
            value.missionType != "MT_OFFERING" &&
 | 
			
		||||
            value.missionType != "MT_PVPVE" &&
 | 
			
		||||
            value.name.indexOf("1999NodeI") == -1 && // not stage defense
 | 
			
		||||
            value.name.indexOf("1999NodeJ") == -1 && // not lich bounty
 | 
			
		||||
            !isArchwingMission(value)
 | 
			
		||||
        ) {
 | 
			
		||||
 | 
			
		||||
@ -201,6 +201,14 @@ goalProgressSchema.set("toJSON", {
 | 
			
		||||
 | 
			
		||||
const guildSchema = new Schema<IGuildDatabase>(
 | 
			
		||||
    {
 | 
			
		||||
        // SNS guild cheats
 | 
			
		||||
        noDojoRoomBuildStage: Boolean,
 | 
			
		||||
        noDojoDecoBuildStage: Boolean,
 | 
			
		||||
        fastDojoRoomDestruction: Boolean,
 | 
			
		||||
        noDojoResearchCosts: Boolean,
 | 
			
		||||
        noDojoResearchTime: Boolean,
 | 
			
		||||
        fastClanAscension: Boolean,
 | 
			
		||||
 | 
			
		||||
        Name: { type: String, required: true, unique: true },
 | 
			
		||||
        MOTD: { type: String, default: "" },
 | 
			
		||||
        LongMOTD: { type: longMOTDSchema, default: undefined },
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,8 @@ import { popArchonCrystalUpgradeController } from "../controllers/custom/popArch
 | 
			
		||||
import { deleteAccountController } from "../controllers/custom/deleteAccountController.ts";
 | 
			
		||||
import { getNameController } from "../controllers/custom/getNameController.ts";
 | 
			
		||||
import { getAccountInfoController } from "../controllers/custom/getAccountInfoController.ts";
 | 
			
		||||
import { getGuildController } from "../controllers/custom/getGuildController.ts";
 | 
			
		||||
import { getAllianceController } from "../controllers/custom/getAllianceController.ts";
 | 
			
		||||
import { renameAccountController } from "../controllers/custom/renameAccountController.ts";
 | 
			
		||||
import { ircDroppedController } from "../controllers/custom/ircDroppedController.ts";
 | 
			
		||||
import { unlockAllIntrinsicsController } from "../controllers/custom/unlockAllIntrinsicsController.ts";
 | 
			
		||||
@ -25,6 +27,13 @@ import { createAccountController } from "../controllers/custom/createAccountCont
 | 
			
		||||
import { createMessageController } from "../controllers/custom/createMessageController.ts";
 | 
			
		||||
import { addCurrencyController } from "../controllers/custom/addCurrencyController.ts";
 | 
			
		||||
import { addItemsController } from "../controllers/custom/addItemsController.ts";
 | 
			
		||||
import {
 | 
			
		||||
    addTechProjectController,
 | 
			
		||||
    completeTechProjectsController,
 | 
			
		||||
    fundTechProjectController,
 | 
			
		||||
    removeTechProjectController
 | 
			
		||||
} from "../controllers/custom/techProjectController.ts";
 | 
			
		||||
import { addVaultDecoRecipeController } from "../controllers/custom/addVaultDecoRecipeController.ts";
 | 
			
		||||
import { addXpController } from "../controllers/custom/addXpController.ts";
 | 
			
		||||
import { importController } from "../controllers/custom/importController.ts";
 | 
			
		||||
import { manageQuestsController } from "../controllers/custom/manageQuestsController.ts";
 | 
			
		||||
@ -34,6 +43,7 @@ import { updateFingerprintController } from "../controllers/custom/updateFingerp
 | 
			
		||||
import { changeModularPartsController } from "../controllers/custom/changeModularPartsController.ts";
 | 
			
		||||
import { editSuitInvigorationUpgradeController } from "../controllers/custom/editSuitInvigorationUpgradeController.ts";
 | 
			
		||||
import { setAccountCheatController } from "../controllers/custom/setAccountCheatController.ts";
 | 
			
		||||
import { setGuildCheatController } from "../controllers/custom/setGuildCheatController.ts";
 | 
			
		||||
 | 
			
		||||
import { getConfigController, setConfigController } from "../controllers/custom/configController.ts";
 | 
			
		||||
 | 
			
		||||
@ -46,6 +56,8 @@ customRouter.get("/popArchonCrystalUpgrade", popArchonCrystalUpgradeController);
 | 
			
		||||
customRouter.get("/deleteAccount", deleteAccountController);
 | 
			
		||||
customRouter.get("/getName", getNameController);
 | 
			
		||||
customRouter.get("/getAccountInfo", getAccountInfoController);
 | 
			
		||||
customRouter.get("/getGuild", getGuildController);
 | 
			
		||||
customRouter.get("/getAlliance", getAllianceController);
 | 
			
		||||
customRouter.get("/renameAccount", renameAccountController);
 | 
			
		||||
customRouter.get("/ircDropped", ircDroppedController);
 | 
			
		||||
customRouter.get("/unlockAllIntrinsics", unlockAllIntrinsicsController);
 | 
			
		||||
@ -64,6 +76,11 @@ customRouter.post("/createAccount", createAccountController);
 | 
			
		||||
customRouter.post("/createMessage", createMessageController);
 | 
			
		||||
customRouter.post("/addCurrency", addCurrencyController);
 | 
			
		||||
customRouter.post("/addItems", addItemsController);
 | 
			
		||||
customRouter.post("/addTechProject", addTechProjectController);
 | 
			
		||||
customRouter.post("/removeTechProject", removeTechProjectController);
 | 
			
		||||
customRouter.post("/addVaultDecoRecipe", addVaultDecoRecipeController);
 | 
			
		||||
customRouter.post("/fundTechProject", fundTechProjectController);
 | 
			
		||||
customRouter.post("/completeTechProject", completeTechProjectsController);
 | 
			
		||||
customRouter.post("/addXp", addXpController);
 | 
			
		||||
customRouter.post("/import", importController);
 | 
			
		||||
customRouter.post("/manageQuests", manageQuestsController);
 | 
			
		||||
@ -73,6 +90,7 @@ customRouter.post("/updateFingerprint", updateFingerprintController);
 | 
			
		||||
customRouter.post("/changeModularParts", changeModularPartsController);
 | 
			
		||||
customRouter.post("/editSuitInvigorationUpgrade", editSuitInvigorationUpgradeController);
 | 
			
		||||
customRouter.post("/setAccountCheat", setAccountCheatController);
 | 
			
		||||
customRouter.post("/setGuildCheat", setGuildCheatController);
 | 
			
		||||
 | 
			
		||||
customRouter.post("/getConfig", getConfigController);
 | 
			
		||||
customRouter.post("/setConfig", setConfigController);
 | 
			
		||||
 | 
			
		||||
@ -42,6 +42,9 @@ webuiRouter.get("/webui/cheats", (_req, res) => {
 | 
			
		||||
webuiRouter.get("/webui/import", (_req, res) => {
 | 
			
		||||
    res.sendFile(path.join(baseDir, "static/webui/index.html"));
 | 
			
		||||
});
 | 
			
		||||
webuiRouter.get("/webui/guildView", (_req, res) => {
 | 
			
		||||
    res.sendFile(path.join(baseDir, "static/webui/index.html"));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Serve static files
 | 
			
		||||
webuiRouter.use("/webui", express.static(path.join(baseDir, "static/webui")));
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@ export interface IConfig {
 | 
			
		||||
        level: string; // "fatal" | "error" | "warn" | "info" | "http" | "debug" | "trace";
 | 
			
		||||
    };
 | 
			
		||||
    myAddress: string;
 | 
			
		||||
    bindAddress?: string;
 | 
			
		||||
    httpPort?: number;
 | 
			
		||||
    httpsPort?: number;
 | 
			
		||||
    ircAddress?: string;
 | 
			
		||||
@ -23,15 +24,8 @@ export interface IConfig {
 | 
			
		||||
    unlockAllShipDecorations?: boolean;
 | 
			
		||||
    unlockAllFlavourItems?: boolean;
 | 
			
		||||
    unlockAllSkins?: boolean;
 | 
			
		||||
    unlockAllDecoRecipes?: boolean;
 | 
			
		||||
    fullyStockedVendors?: boolean;
 | 
			
		||||
    skipClanKeyCrafting?: boolean;
 | 
			
		||||
    noDojoRoomBuildStage?: boolean;
 | 
			
		||||
    noDojoDecoBuildStage?: boolean;
 | 
			
		||||
    fastDojoRoomDestruction?: boolean;
 | 
			
		||||
    noDojoResearchCosts?: boolean;
 | 
			
		||||
    noDojoResearchTime?: boolean;
 | 
			
		||||
    fastClanAscension?: boolean;
 | 
			
		||||
    spoofMasteryRank?: number;
 | 
			
		||||
    relicRewardItemCountMultiplier?: number;
 | 
			
		||||
    nightwaveStandingMultiplier?: number;
 | 
			
		||||
@ -127,7 +121,14 @@ export const configRemovedOptionsKeys = [
 | 
			
		||||
    "exceptionalRelicsAlwaysGiveBronzeReward",
 | 
			
		||||
    "flawlessRelicsAlwaysGiveSilverReward",
 | 
			
		||||
    "radiantRelicsAlwaysGiveGoldReward",
 | 
			
		||||
    "disableDailyTribute"
 | 
			
		||||
    "disableDailyTribute",
 | 
			
		||||
    "noDojoRoomBuildStage",
 | 
			
		||||
    "noDojoDecoBuildStage",
 | 
			
		||||
    "fastDojoRoomDestruction",
 | 
			
		||||
    "noDojoResearchCosts",
 | 
			
		||||
    "noDojoResearchTime",
 | 
			
		||||
    "fastClanAscension",
 | 
			
		||||
    "unlockAllDecoRecipes"
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const configPath = path.join(repoDir, args.configPath ?? "config.json");
 | 
			
		||||
@ -188,3 +189,17 @@ export const getReflexiveAddress = (request: Request): { myAddress: string; myUr
 | 
			
		||||
    }
 | 
			
		||||
    return { myAddress, myUrlBase };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface IBindings {
 | 
			
		||||
    address: string;
 | 
			
		||||
    httpPort: number;
 | 
			
		||||
    httpsPort: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const configGetWebBindings = (): IBindings => {
 | 
			
		||||
    return {
 | 
			
		||||
        address: config.bindAddress || "0.0.0.0",
 | 
			
		||||
        httpPort: config.httpPort || 80,
 | 
			
		||||
        httpsPort: config.httpsPort || 443
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ import chokidar from "chokidar";
 | 
			
		||||
import { logger } from "../utils/logger.ts";
 | 
			
		||||
import {
 | 
			
		||||
    config,
 | 
			
		||||
    configGetWebBindings,
 | 
			
		||||
    configPath,
 | 
			
		||||
    configRemovedOptionsKeys,
 | 
			
		||||
    loadConfig,
 | 
			
		||||
@ -9,7 +10,7 @@ import {
 | 
			
		||||
    type IConfig
 | 
			
		||||
} from "./configService.ts";
 | 
			
		||||
import { saveConfig, shouldReloadConfig } from "./configWriterService.ts";
 | 
			
		||||
import { getWebPorts, startWebServer, stopWebServer } from "./webService.ts";
 | 
			
		||||
import { getWebBindings, startWebServer, stopWebServer } from "./webService.ts";
 | 
			
		||||
import { sendWsBroadcast } from "./wsService.ts";
 | 
			
		||||
import varzia from "../../static/fixed_responses/worldState/varzia.json" with { type: "json" };
 | 
			
		||||
 | 
			
		||||
@ -25,9 +26,14 @@ chokidar.watch(configPath).on("change", () => {
 | 
			
		||||
        validateConfig();
 | 
			
		||||
        syncConfigWithDatabase();
 | 
			
		||||
 | 
			
		||||
        const webPorts = getWebPorts();
 | 
			
		||||
        if (config.httpPort != webPorts.http || config.httpsPort != webPorts.https) {
 | 
			
		||||
            logger.info(`Restarting web server to apply port changes.`);
 | 
			
		||||
        const configBindings = configGetWebBindings();
 | 
			
		||||
        const bindings = getWebBindings();
 | 
			
		||||
        if (
 | 
			
		||||
            configBindings.address != bindings.address ||
 | 
			
		||||
            configBindings.httpPort != bindings.httpPort ||
 | 
			
		||||
            configBindings.httpsPort != bindings.httpsPort
 | 
			
		||||
        ) {
 | 
			
		||||
            logger.info(`Restarting web server to apply binding changes.`);
 | 
			
		||||
 | 
			
		||||
            // Tell webui clients to reload with new port
 | 
			
		||||
            sendWsBroadcast({ ports: { http: config.httpPort, https: config.httpsPort } });
 | 
			
		||||
 | 
			
		||||
@ -33,7 +33,6 @@ import { Inbox } from "../models/inboxModel.ts";
 | 
			
		||||
import type { IFusionTreasure } from "../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import type { IInventoryChanges } from "../types/purchaseTypes.ts";
 | 
			
		||||
import { parallelForeach } from "../utils/async-utils.ts";
 | 
			
		||||
import allDecoRecipes from "../../static/fixed_responses/allDecoRecipes.json" with { type: "json" };
 | 
			
		||||
import { createMessage } from "./inboxService.ts";
 | 
			
		||||
import { addAccountDataToFriendInfo, addInventoryDataToFriendInfo } from "./friendService.ts";
 | 
			
		||||
import type { ITypeCount } from "../types/commonTypes.ts";
 | 
			
		||||
@ -136,9 +135,7 @@ export const getGuildVault = (guild: TGuildDatabaseDocument): IGuildVault => {
 | 
			
		||||
        DojoRefundPremiumCredits: guild.VaultPremiumCredits,
 | 
			
		||||
        ShipDecorations: guild.VaultShipDecorations,
 | 
			
		||||
        FusionTreasures: guild.VaultFusionTreasures,
 | 
			
		||||
        DecoRecipes: config.unlockAllDecoRecipes
 | 
			
		||||
            ? allDecoRecipes.map(recipe => ({ ItemType: recipe, ItemCount: 1 }))
 | 
			
		||||
            : guild.VaultDecoRecipes
 | 
			
		||||
        DecoRecipes: guild.VaultDecoRecipes
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -565,12 +562,12 @@ export const processFundedGuildTechProject = (
 | 
			
		||||
    recipe: IDojoResearch
 | 
			
		||||
): void => {
 | 
			
		||||
    techProject.State = 1;
 | 
			
		||||
    techProject.CompletionDate = new Date(Date.now() + (config.noDojoResearchTime ? 0 : recipe.time) * 1000);
 | 
			
		||||
    techProject.CompletionDate = new Date(Date.now() + (guild.noDojoResearchTime ? 0 : recipe.time) * 1000);
 | 
			
		||||
    if (recipe.guildXpValue) {
 | 
			
		||||
        guild.XP += recipe.guildXpValue;
 | 
			
		||||
    }
 | 
			
		||||
    setGuildTechLogState(guild, techProject.ItemType, config.noDojoResearchTime ? 4 : 3, techProject.CompletionDate);
 | 
			
		||||
    if (config.noDojoResearchTime) {
 | 
			
		||||
    setGuildTechLogState(guild, techProject.ItemType, guild.noDojoResearchTime ? 4 : 3, techProject.CompletionDate);
 | 
			
		||||
    if (guild.noDojoResearchTime) {
 | 
			
		||||
        processCompletedGuildTechProject(guild, techProject.ItemType);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
@ -657,8 +654,8 @@ export const checkClanAscensionHasRequiredContributors = async (guild: TGuildDat
 | 
			
		||||
    if (guild.CeremonyContributors!.length >= requiredContributors) {
 | 
			
		||||
        guild.Class = guild.CeremonyClass!;
 | 
			
		||||
        guild.CeremonyClass = undefined;
 | 
			
		||||
        guild.CeremonyResetDate = new Date(Date.now() + (config.fastClanAscension ? 5_000 : 72 * 3600_000));
 | 
			
		||||
        if (!config.fastClanAscension) {
 | 
			
		||||
        guild.CeremonyResetDate = new Date(Date.now() + (guild.fastClanAscension ? 5_000 : 72 * 3600_000));
 | 
			
		||||
        if (!guild.fastClanAscension) {
 | 
			
		||||
            // Send message to all active guild members
 | 
			
		||||
            const members = await GuildMember.find({ guildId: guild._id, status: 0 }, "accountId");
 | 
			
		||||
            await parallelForeach(members, async member => {
 | 
			
		||||
 | 
			
		||||
@ -2160,7 +2160,7 @@ export const addMissionComplete = (inventory: TInventoryDatabaseDocument, { Tag,
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const addBooster = (ItemType: string, time: number, inventory: TInventoryDatabaseDocument): void => {
 | 
			
		||||
export const addBooster = (ItemType: string, timeSecs: number, inventory: TInventoryDatabaseDocument): void => {
 | 
			
		||||
    const currentTime = Math.floor(Date.now() / 1000);
 | 
			
		||||
 | 
			
		||||
    const { Boosters } = inventory;
 | 
			
		||||
@ -2169,9 +2169,9 @@ export const addBooster = (ItemType: string, time: number, inventory: TInventory
 | 
			
		||||
 | 
			
		||||
    if (itemIndex !== -1) {
 | 
			
		||||
        const existingBooster = Boosters[itemIndex];
 | 
			
		||||
        existingBooster.ExpiryDate = Math.max(existingBooster.ExpiryDate, currentTime) + time;
 | 
			
		||||
        existingBooster.ExpiryDate = Math.max(existingBooster.ExpiryDate, currentTime) + timeSecs;
 | 
			
		||||
    } else {
 | 
			
		||||
        Boosters.push({ ItemType, ExpiryDate: currentTime + time });
 | 
			
		||||
        Boosters.push({ ItemType, ExpiryDate: currentTime + timeSecs });
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -27,9 +27,11 @@ import {
 | 
			
		||||
    ExportBoosters,
 | 
			
		||||
    ExportBundles,
 | 
			
		||||
    ExportCustoms,
 | 
			
		||||
    ExportDojoRecipes,
 | 
			
		||||
    ExportDrones,
 | 
			
		||||
    ExportGear,
 | 
			
		||||
    ExportKeys,
 | 
			
		||||
    ExportRailjackWeapons,
 | 
			
		||||
    ExportRecipes,
 | 
			
		||||
    ExportResources,
 | 
			
		||||
    ExportSentinels,
 | 
			
		||||
@ -149,6 +151,18 @@ export const getItemName = (uniqueName: string): string | undefined => {
 | 
			
		||||
    if (uniqueName in ExportWeapons) {
 | 
			
		||||
        return ExportWeapons[uniqueName].name;
 | 
			
		||||
    }
 | 
			
		||||
    if (uniqueName in ExportRailjackWeapons) {
 | 
			
		||||
        return ExportRailjackWeapons[uniqueName].name;
 | 
			
		||||
    }
 | 
			
		||||
    if (uniqueName in ExportDojoRecipes.colours) {
 | 
			
		||||
        return ExportDojoRecipes.colours[uniqueName].name;
 | 
			
		||||
    }
 | 
			
		||||
    if (uniqueName in ExportDojoRecipes.backdrops) {
 | 
			
		||||
        return ExportDojoRecipes.backdrops[uniqueName].name;
 | 
			
		||||
    }
 | 
			
		||||
    if (uniqueName in ExportDojoRecipes.decos) {
 | 
			
		||||
        return ExportDojoRecipes.decos[uniqueName].name;
 | 
			
		||||
    }
 | 
			
		||||
    return undefined;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,9 @@
 | 
			
		||||
import type { IMissionReward as IMissionRewardExternal, IRegion, IReward } from "warframe-public-export-plus";
 | 
			
		||||
import type {
 | 
			
		||||
    IMissionReward as IMissionRewardExternal,
 | 
			
		||||
    IRegion,
 | 
			
		||||
    IReward,
 | 
			
		||||
    TMissionType
 | 
			
		||||
} from "warframe-public-export-plus";
 | 
			
		||||
import {
 | 
			
		||||
    ExportEnemies,
 | 
			
		||||
    ExportFusionBundles,
 | 
			
		||||
@ -102,10 +107,9 @@ const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const region = ExportRegions[rewardInfo.node] as IRegion | undefined;
 | 
			
		||||
    const missionIndex: number | undefined = region?.missionIndex;
 | 
			
		||||
    const missionType: TMissionType | undefined = region?.missionType;
 | 
			
		||||
 | 
			
		||||
    // For Rescue missions
 | 
			
		||||
    if (missionIndex == 3 && rewardInfo.rewardTier) {
 | 
			
		||||
    if (missionType == "MT_RESCUE" && rewardInfo.rewardTier) {
 | 
			
		||||
        return [rewardInfo.rewardTier];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -123,7 +127,7 @@ const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[]
 | 
			
		||||
    // Empty or absent rewardQualifications should not give rewards when:
 | 
			
		||||
    // - Completing only 1 zone of (E)SO (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1823)
 | 
			
		||||
    // - Aborting a railjack mission (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1741)
 | 
			
		||||
    if (rotationCount == 0 && missionIndex != 30 && missionIndex != 32) {
 | 
			
		||||
    if (rotationCount == 0 && missionType != "MT_ENDLESS_EXTERMINATION" && missionType != "MT_RAILJACK") {
 | 
			
		||||
        return [0];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1132,8 +1136,8 @@ const isEligibleForCreditReward = (rewardInfo: IRewardInfo, missions: IMission,
 | 
			
		||||
    }
 | 
			
		||||
    // The rest here might not be needed anymore, but just to be sure we don't give undue credits...
 | 
			
		||||
    return (
 | 
			
		||||
        node.missionIndex != 23 && // junction
 | 
			
		||||
        node.missionIndex != 28 && // open world
 | 
			
		||||
        node.missionType != "MT_JUNCTION" &&
 | 
			
		||||
        node.missionType != "MT_LANDSCAPE" &&
 | 
			
		||||
        missions.Tag != "SolNode761" && // the index
 | 
			
		||||
        missions.Tag != "SolNode762" && // the index
 | 
			
		||||
        missions.Tag != "SolNode763" && // the index
 | 
			
		||||
@ -1850,14 +1854,14 @@ function getRandomMissionDrops(
 | 
			
		||||
        let rewardManifests: string[];
 | 
			
		||||
        if (RewardInfo.periodicMissionTag == "EliteAlert" || RewardInfo.periodicMissionTag == "EliteAlertB") {
 | 
			
		||||
            rewardManifests = ["/Lotus/Types/Game/MissionDecks/EliteAlertMissionRewards/EliteAlertMissionRewards"];
 | 
			
		||||
        } else if (RewardInfo.invasionId && region.missionIndex == 0) {
 | 
			
		||||
        } else if (RewardInfo.invasionId && region.missionType == "MT_ASSASSINATION") {
 | 
			
		||||
            // Invasion assassination has Phorid has the boss who should drop Nyx parts
 | 
			
		||||
            // TODO: Check that the invasion faction is indeed FC_INFESTATION once the Invasions in worldState are more dynamic
 | 
			
		||||
            rewardManifests = ["/Lotus/Types/Game/MissionDecks/BossMissionRewards/NyxRewards"];
 | 
			
		||||
        } else if (RewardInfo.sortieId) {
 | 
			
		||||
            // Sortie mission types differ from the underlying node and hence also don't give rewards from the underlying nodes.
 | 
			
		||||
            // Assassinations in non-lite sorties are an exception to this.
 | 
			
		||||
            if (region.missionIndex == 0) {
 | 
			
		||||
            if (region.missionType == "MT_ASSASSINATION") {
 | 
			
		||||
                const arr = RewardInfo.sortieId.split("_");
 | 
			
		||||
                let giveNodeReward = false;
 | 
			
		||||
                if (arr[1] != "Lite") {
 | 
			
		||||
@ -2223,7 +2227,7 @@ function getRandomMissionDrops(
 | 
			
		||||
            const deck = ExportRewards["/Lotus/Types/Game/MissionDecks/NightmareModeRewards"];
 | 
			
		||||
            let rotation = 0;
 | 
			
		||||
 | 
			
		||||
            if (region.missionIndex === 3 && RewardInfo.rewardTier) {
 | 
			
		||||
            if (region.missionType == "MT_RESCUE" && RewardInfo.rewardTier) {
 | 
			
		||||
                rotation = RewardInfo.rewardTier;
 | 
			
		||||
            } else if ([6, 7, 8, 10, 11].includes(region.systemIndex)) {
 | 
			
		||||
                rotation = 2;
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,6 @@ import type {
 | 
			
		||||
import { PurchaseSource } from "../types/purchaseTypes.ts";
 | 
			
		||||
import { logger } from "../utils/logger.ts";
 | 
			
		||||
import { getWorldState } from "./worldStateService.ts";
 | 
			
		||||
import type { TRarity } from "warframe-public-export-plus";
 | 
			
		||||
import {
 | 
			
		||||
    ExportBoosterPacks,
 | 
			
		||||
    ExportBoosters,
 | 
			
		||||
@ -419,7 +418,7 @@ export const handleBundleAcqusition = async (
 | 
			
		||||
                    component.typeName,
 | 
			
		||||
                    inventory,
 | 
			
		||||
                    component.purchaseQuantity * quantity,
 | 
			
		||||
                    component.durability,
 | 
			
		||||
                    component.durabilityDays,
 | 
			
		||||
                    true
 | 
			
		||||
                )
 | 
			
		||||
            ).InventoryChanges
 | 
			
		||||
@ -432,7 +431,7 @@ export const handleStoreItemAcquisition = async (
 | 
			
		||||
    storeItemName: string,
 | 
			
		||||
    inventory: TInventoryDatabaseDocument,
 | 
			
		||||
    quantity: number = 1,
 | 
			
		||||
    durability: TRarity = "COMMON",
 | 
			
		||||
    durabilityDays: number = 3,
 | 
			
		||||
    ignorePurchaseQuantity: boolean = false,
 | 
			
		||||
    premiumPurchase: boolean = true,
 | 
			
		||||
    seed?: bigint
 | 
			
		||||
@ -482,7 +481,7 @@ export const handleStoreItemAcquisition = async (
 | 
			
		||||
                );
 | 
			
		||||
                break;
 | 
			
		||||
            case "Boosters":
 | 
			
		||||
                purchaseResponse = handleBoostersPurchase(storeItemName, inventory, durability);
 | 
			
		||||
                purchaseResponse = handleBoostersPurchase(storeItemName, inventory, durabilityDays);
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -672,7 +671,7 @@ const handleTypesPurchase = async (
 | 
			
		||||
const handleBoostersPurchase = (
 | 
			
		||||
    boosterStoreName: string,
 | 
			
		||||
    inventory: TInventoryDatabaseDocument,
 | 
			
		||||
    durability: TRarity
 | 
			
		||||
    durabilityDays: number
 | 
			
		||||
): { InventoryChanges: IInventoryChanges } => {
 | 
			
		||||
    if (!(boosterStoreName in ExportBoosters)) {
 | 
			
		||||
        logger.error(`unknown booster type: ${boosterStoreName}`);
 | 
			
		||||
@ -680,7 +679,7 @@ const handleBoostersPurchase = (
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const ItemType = ExportBoosters[boosterStoreName].typeName;
 | 
			
		||||
    const ExpiryDate = ExportMisc.boosterDurations[durability];
 | 
			
		||||
    const ExpiryDate = durabilityDays * 86400;
 | 
			
		||||
 | 
			
		||||
    addBooster(ItemType, ExpiryDate, inventory);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import http from "http";
 | 
			
		||||
import https from "https";
 | 
			
		||||
import fs from "node:fs";
 | 
			
		||||
import { config } from "./configService.ts";
 | 
			
		||||
import { configGetWebBindings, type IBindings } from "./configService.ts";
 | 
			
		||||
import { logger } from "../utils/logger.ts";
 | 
			
		||||
import { app } from "../app.ts";
 | 
			
		||||
import type { AddressInfo } from "node:net";
 | 
			
		||||
@ -17,33 +17,35 @@ const tlsOptions = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const startWebServer = (): void => {
 | 
			
		||||
    const httpPort = config.httpPort || 80;
 | 
			
		||||
    const httpsPort = config.httpsPort || 443;
 | 
			
		||||
    const bindings = configGetWebBindings();
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
			
		||||
    httpServer = http.createServer(app);
 | 
			
		||||
    httpServer.listen(httpPort, () => {
 | 
			
		||||
    httpServer.listen(bindings.httpPort, bindings.address, () => {
 | 
			
		||||
        startWsServer(httpServer!);
 | 
			
		||||
 | 
			
		||||
        logger.info("HTTP server started on port " + httpPort);
 | 
			
		||||
        logger.info(`HTTP server started on ${bindings.address}:${bindings.httpPort}`);
 | 
			
		||||
 | 
			
		||||
        // eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
			
		||||
        httpsServer = https.createServer(tlsOptions, app);
 | 
			
		||||
        httpsServer.listen(httpsPort, () => {
 | 
			
		||||
        httpsServer.listen(bindings.httpsPort, bindings.address, () => {
 | 
			
		||||
            startWssServer(httpsServer!);
 | 
			
		||||
 | 
			
		||||
            logger.info("HTTPS server started on port " + httpsPort);
 | 
			
		||||
            logger.info(`HTTPS server started on ${bindings.address}:${bindings.httpsPort}`);
 | 
			
		||||
 | 
			
		||||
            logger.info(
 | 
			
		||||
                "Access the WebUI in your browser at http://localhost" + (httpPort == 80 ? "" : ":" + httpPort)
 | 
			
		||||
                "Access the WebUI in your browser at http://localhost" +
 | 
			
		||||
                    (bindings.httpPort == 80 ? "" : ":" + bindings.httpPort)
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            void runWsSelfTest("wss", httpsPort).then(ok => {
 | 
			
		||||
            void runWsSelfTest("wss", bindings.httpsPort).then(ok => {
 | 
			
		||||
                if (!ok) {
 | 
			
		||||
                    logger.warn(`WSS self-test failed. The server may not actually be reachable at port ${httpsPort}.`);
 | 
			
		||||
                    logger.warn(
 | 
			
		||||
                        `WSS self-test failed. The server may not be reachable locally on port ${bindings.httpsPort}.`
 | 
			
		||||
                    );
 | 
			
		||||
                    if (process.platform == "win32") {
 | 
			
		||||
                        logger.warn(
 | 
			
		||||
                            `You can check who actually has that port via powershell: Get-Process -Id (Get-NetTCPConnection -LocalPort ${httpsPort}).OwningProcess`
 | 
			
		||||
                            `You can check who has that port via powershell: Get-Process -Id (Get-NetTCPConnection -LocalPort ${bindings.httpsPort}).OwningProcess`
 | 
			
		||||
                        );
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
@ -80,10 +82,11 @@ const runWsSelfTest = (protocol: "ws" | "wss", port: number): Promise<boolean> =
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getWebPorts = (): Record<"http" | "https", number | undefined> => {
 | 
			
		||||
export const getWebBindings = (): Partial<IBindings> => {
 | 
			
		||||
    return {
 | 
			
		||||
        http: (httpServer?.address() as AddressInfo | undefined)?.port,
 | 
			
		||||
        https: (httpsServer?.address() as AddressInfo | undefined)?.port
 | 
			
		||||
        address: (httpServer?.address() as AddressInfo | undefined)?.address,
 | 
			
		||||
        httpPort: (httpServer?.address() as AddressInfo | undefined)?.port,
 | 
			
		||||
        httpsPort: (httpsServer?.address() as AddressInfo | undefined)?.port
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -13,8 +13,8 @@ import { buildConfig } from "./buildConfigService.ts";
 | 
			
		||||
import { unixTimesInMs } from "../constants/timeConstants.ts";
 | 
			
		||||
import { config } from "./configService.ts";
 | 
			
		||||
import { getRandomElement, getRandomInt, sequentiallyUniqueRandomElement, SRng } from "./rngService.ts";
 | 
			
		||||
import type { IMissionReward, IRegion } from "warframe-public-export-plus";
 | 
			
		||||
import { eMissionType, ExportRegions, ExportSyndicates } from "warframe-public-export-plus";
 | 
			
		||||
import type { IMissionReward, IRegion, TFaction } from "warframe-public-export-plus";
 | 
			
		||||
import { ExportRegions, ExportSyndicates } from "warframe-public-export-plus";
 | 
			
		||||
import type {
 | 
			
		||||
    ICalendarDay,
 | 
			
		||||
    ICalendarEvent,
 | 
			
		||||
@ -87,11 +87,11 @@ const sortieFactionToSystemIndexes: Record<string, number[]> = {
 | 
			
		||||
    FC_OROKIN: [14]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const sortieFactionToFactionIndexes: Record<string, number[]> = {
 | 
			
		||||
    FC_GRINEER: [0],
 | 
			
		||||
    FC_CORPUS: [1],
 | 
			
		||||
    FC_INFESTATION: [0, 1, 2],
 | 
			
		||||
    FC_OROKIN: [3]
 | 
			
		||||
const sortieFactionToFactions: Record<string, TFaction[]> = {
 | 
			
		||||
    FC_GRINEER: ["FC_GRINEER"],
 | 
			
		||||
    FC_CORPUS: ["FC_CORPUS"],
 | 
			
		||||
    FC_INFESTATION: ["FC_GRINEER", "FC_CORPUS", "FC_INFESTATION"],
 | 
			
		||||
    FC_OROKIN: ["FC_OROKIN"]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const sortieBossNode: Record<Exclude<TSortieBoss, "SORTIE_BOSS_CORRUPTED_VOR">, string> = {
 | 
			
		||||
@ -271,7 +271,7 @@ export const getSortie = (day: number): ISortie => {
 | 
			
		||||
    for (const [key, value] of Object.entries(ExportRegions)) {
 | 
			
		||||
        if (
 | 
			
		||||
            sortieFactionToSystemIndexes[sortieBossToFaction[boss]].includes(value.systemIndex) &&
 | 
			
		||||
            sortieFactionToFactionIndexes[sortieBossToFaction[boss]].includes(value.factionIndex!) &&
 | 
			
		||||
            sortieFactionToFactions[sortieBossToFaction[boss]].includes(value.faction!) &&
 | 
			
		||||
            key in sortieTilesets &&
 | 
			
		||||
            (key != "SolNode228" || sortieBossToFaction[boss] == "FC_GRINEER") // PoE does not work for non-infested enemies
 | 
			
		||||
        ) {
 | 
			
		||||
@ -339,10 +339,10 @@ export const getSortie = (day: number): ISortie => {
 | 
			
		||||
            modifiers.push("SORTIE_MODIFIER_HAZARD_RADIATION");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (ExportRegions[node].factionIndex == 0) {
 | 
			
		||||
        if (ExportRegions[node].faction == "FC_GRINEER") {
 | 
			
		||||
            // Grineer
 | 
			
		||||
            modifiers.push("SORTIE_MODIFIER_ARMOR");
 | 
			
		||||
        } else if (ExportRegions[node].factionIndex == 1) {
 | 
			
		||||
        } else if (ExportRegions[node].faction == "FC_CORPUS") {
 | 
			
		||||
            // Corpus
 | 
			
		||||
            modifiers.push("SORTIE_MODIFIER_SHIELDS");
 | 
			
		||||
        }
 | 
			
		||||
@ -1306,7 +1306,7 @@ const createInvasion = (day: number, idx: number): IInvasion => {
 | 
			
		||||
        ),
 | 
			
		||||
        Goal: 30000, // Value seems to range from 30000 to 98000 in intervals of 1000. Higher values are increasingly rare. I don't think this is relevant for the frontend besides dividing count by it.
 | 
			
		||||
        LocTag: isInfestationOutbreak
 | 
			
		||||
            ? ExportRegions[node].missionIndex == 0
 | 
			
		||||
            ? ExportRegions[node].missionType == "MT_ASSASSINATION"
 | 
			
		||||
                ? "/Lotus/Language/Menu/InfestedInvasionBoss"
 | 
			
		||||
                : "/Lotus/Language/Menu/InfestedInvasionGeneric"
 | 
			
		||||
            : attacker == "FC_CORPUS"
 | 
			
		||||
@ -3179,7 +3179,7 @@ export const populateFissures = async (worldState: IWorldState): Promise<void> =
 | 
			
		||||
                    Activation: { $date: { $numberLong: "1000000000000" } },
 | 
			
		||||
                    Expiry: { $date: { $numberLong: "2000000000000" } },
 | 
			
		||||
                    Node: node,
 | 
			
		||||
                    MissionType: eMissionType[meta.missionIndex].tag,
 | 
			
		||||
                    MissionType: meta.missionType,
 | 
			
		||||
                    Modifier: tier,
 | 
			
		||||
                    Hard: config.worldState.allTheFissures == "hard"
 | 
			
		||||
                });
 | 
			
		||||
@ -3199,7 +3199,7 @@ export const populateFissures = async (worldState: IWorldState): Promise<void> =
 | 
			
		||||
                        : toMongoDate(fissure.Activation),
 | 
			
		||||
                Expiry: toMongoDate(fissure.Expiry),
 | 
			
		||||
                Node: fissure.Node,
 | 
			
		||||
                MissionType: eMissionType[meta.missionIndex].tag,
 | 
			
		||||
                MissionType: meta.missionType,
 | 
			
		||||
                Modifier: fissure.Modifier,
 | 
			
		||||
                Hard: fissure.Hard
 | 
			
		||||
            });
 | 
			
		||||
@ -3246,13 +3246,12 @@ export const getLiteSortie = (week: number): ILiteSortie => {
 | 
			
		||||
    for (const [key, value] of Object.entries(ExportRegions)) {
 | 
			
		||||
        if (
 | 
			
		||||
            value.systemIndex === systemIndex &&
 | 
			
		||||
            value.factionIndex !== undefined &&
 | 
			
		||||
            value.factionIndex < 2 &&
 | 
			
		||||
            (value.faction == "FC_GRINEER" || value.faction == "FC_CORPUS") &&
 | 
			
		||||
            !isArchwingMission(value) &&
 | 
			
		||||
            value.missionIndex != 0 && // Exclude MT_ASSASSINATION
 | 
			
		||||
            value.missionIndex != 23 && // Exclude junctions
 | 
			
		||||
            value.missionIndex != 28 && // Exclude open worlds
 | 
			
		||||
            value.missionIndex != 32 // Exclude railjack
 | 
			
		||||
            value.missionType != "MT_ASSASSINATION" &&
 | 
			
		||||
            value.missionType != "MT_JUNCTION" &&
 | 
			
		||||
            value.missionType != "MT_LANDSCAPE" &&
 | 
			
		||||
            value.missionType != "MT_RAILJACK"
 | 
			
		||||
        ) {
 | 
			
		||||
            nodes.push(key);
 | 
			
		||||
        }
 | 
			
		||||
@ -3309,7 +3308,7 @@ export const isArchwingMission = (node: IRegion): boolean => {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
    // SettlementNode10
 | 
			
		||||
    if (node.missionIndex == 25) {
 | 
			
		||||
    if (node.missionType == "MT_RACE") {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,13 @@
 | 
			
		||||
import type http from "http";
 | 
			
		||||
import type https from "https";
 | 
			
		||||
import type { default as ws } from "ws";
 | 
			
		||||
import type { WebSocket } from "ws";
 | 
			
		||||
import { WebSocketServer } from "ws";
 | 
			
		||||
import { Account } from "../models/loginModel.ts";
 | 
			
		||||
import { createAccount, createNonce, getUsernameFromEmail, isCorrectPassword } from "./loginService.ts";
 | 
			
		||||
import type { IDatabaseAccountJson } from "../types/loginTypes.ts";
 | 
			
		||||
import type { HydratedDocument } from "mongoose";
 | 
			
		||||
import { logError } from "../utils/logger.ts";
 | 
			
		||||
import { logError, logger } from "../utils/logger.ts";
 | 
			
		||||
import type { Request } from "express";
 | 
			
		||||
 | 
			
		||||
let wsServer: WebSocketServer | undefined;
 | 
			
		||||
let wssServer: WebSocketServer | undefined;
 | 
			
		||||
@ -44,9 +45,10 @@ export const stopWsServers = (promises: Promise<void>[]): void => {
 | 
			
		||||
 | 
			
		||||
let lastWsid: number = 0;
 | 
			
		||||
 | 
			
		||||
interface IWsCustomData extends ws {
 | 
			
		||||
interface IWsCustomData extends WebSocket {
 | 
			
		||||
    id: number;
 | 
			
		||||
    accountId?: string;
 | 
			
		||||
    isGame?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IWsMsgFromClient {
 | 
			
		||||
@ -55,11 +57,19 @@ interface IWsMsgFromClient {
 | 
			
		||||
        password: string;
 | 
			
		||||
        isRegister: boolean;
 | 
			
		||||
    };
 | 
			
		||||
    auth_game?: {
 | 
			
		||||
        accountId: string;
 | 
			
		||||
        nonce: number;
 | 
			
		||||
    };
 | 
			
		||||
    logout?: boolean;
 | 
			
		||||
    sync_inventory?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IWsMsgToClient {
 | 
			
		||||
    //wsid?: number;
 | 
			
		||||
    // common
 | 
			
		||||
    wsid?: number;
 | 
			
		||||
 | 
			
		||||
    // to webui
 | 
			
		||||
    reload?: boolean;
 | 
			
		||||
    ports?: {
 | 
			
		||||
        http: number | undefined;
 | 
			
		||||
@ -77,9 +87,13 @@ interface IWsMsgToClient {
 | 
			
		||||
    nonce_updated?: boolean;
 | 
			
		||||
    update_inventory?: boolean;
 | 
			
		||||
    logged_out?: boolean;
 | 
			
		||||
    have_game_ws?: boolean;
 | 
			
		||||
 | 
			
		||||
    // to game
 | 
			
		||||
    sync_inventory?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const wsOnConnect = (ws: ws, req: http.IncomingMessage): void => {
 | 
			
		||||
const wsOnConnect = (ws: WebSocket, req: http.IncomingMessage): void => {
 | 
			
		||||
    if (req.url == "/custom/selftest") {
 | 
			
		||||
        ws.send("SpaceNinjaServer");
 | 
			
		||||
        ws.close();
 | 
			
		||||
@ -87,11 +101,12 @@ const wsOnConnect = (ws: ws, req: http.IncomingMessage): void => {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    (ws as IWsCustomData).id = ++lastWsid;
 | 
			
		||||
    ws.send(JSON.stringify({ wsid: lastWsid }));
 | 
			
		||||
    ws.send(JSON.stringify({ wsid: lastWsid } satisfies IWsMsgToClient));
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
			
		||||
    ws.on("message", async msg => {
 | 
			
		||||
        try {
 | 
			
		||||
            //console.log(String(msg));
 | 
			
		||||
            const data = JSON.parse(String(msg)) as IWsMsgFromClient;
 | 
			
		||||
            if (data.auth) {
 | 
			
		||||
                let account: IDatabaseAccountJson | null = await Account.findOne({ email: data.auth.email });
 | 
			
		||||
@ -124,7 +139,8 @@ const wsOnConnect = (ws: ws, req: http.IncomingMessage): void => {
 | 
			
		||||
                                id: account.id,
 | 
			
		||||
                                DisplayName: account.DisplayName,
 | 
			
		||||
                                Nonce: account.Nonce
 | 
			
		||||
                            }
 | 
			
		||||
                            },
 | 
			
		||||
                            have_game_ws: haveGameWs(account.id)
 | 
			
		||||
                        } satisfies IWsMsgToClient)
 | 
			
		||||
                    );
 | 
			
		||||
                } else {
 | 
			
		||||
@ -137,77 +153,152 @@ const wsOnConnect = (ws: ws, req: http.IncomingMessage): void => {
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if (data.auth_game) {
 | 
			
		||||
                (ws as IWsCustomData).isGame = true;
 | 
			
		||||
                if (data.auth_game.nonce) {
 | 
			
		||||
                    const account: IDatabaseAccountJson | null = await Account.findOne({
 | 
			
		||||
                        _id: data.auth_game.accountId,
 | 
			
		||||
                        Nonce: data.auth_game.nonce
 | 
			
		||||
                    });
 | 
			
		||||
                    if (account) {
 | 
			
		||||
                        (ws as IWsCustomData).accountId = account.id;
 | 
			
		||||
                        logger.debug(`got bootstrapper connection for ${account.id}`);
 | 
			
		||||
                        sendWsBroadcastToWebui({ have_game_ws: true }, account.id);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if (data.logout) {
 | 
			
		||||
                const accountId = (ws as IWsCustomData).accountId;
 | 
			
		||||
                (ws as IWsCustomData).accountId = undefined;
 | 
			
		||||
                await Account.updateOne(
 | 
			
		||||
                    {
 | 
			
		||||
                        _id: accountId,
 | 
			
		||||
                        ClientType: "webui"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        Nonce: 0
 | 
			
		||||
                    }
 | 
			
		||||
                );
 | 
			
		||||
                if (accountId) {
 | 
			
		||||
                    (ws as IWsCustomData).accountId = undefined;
 | 
			
		||||
                    await Account.updateOne(
 | 
			
		||||
                        {
 | 
			
		||||
                            _id: accountId,
 | 
			
		||||
                            ClientType: "webui"
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            Nonce: 0
 | 
			
		||||
                        }
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if (data.sync_inventory) {
 | 
			
		||||
                const accountId = (ws as IWsCustomData).accountId;
 | 
			
		||||
                if (accountId) {
 | 
			
		||||
                    sendWsBroadcastToGame(accountId, { sync_inventory: true });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            logError(e as Error, `processing websocket message`);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
			
		||||
    ws.on("close", async () => {
 | 
			
		||||
        if ((ws as IWsCustomData).isGame && (ws as IWsCustomData).accountId) {
 | 
			
		||||
            logger.debug(`lost bootstrapper connection for ${(ws as IWsCustomData).accountId}`);
 | 
			
		||||
            sendWsBroadcastToWebui({ have_game_ws: false }, (ws as IWsCustomData).accountId);
 | 
			
		||||
            await Account.updateOne(
 | 
			
		||||
                {
 | 
			
		||||
                    _id: (ws as IWsCustomData).accountId
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    Dropped: true
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const forEachClient = (cb: (client: IWsCustomData) => void): void => {
 | 
			
		||||
    if (wsServer) {
 | 
			
		||||
        for (const client of wsServer.clients) {
 | 
			
		||||
            cb(client as IWsCustomData);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if (wssServer) {
 | 
			
		||||
        for (const client of wssServer.clients) {
 | 
			
		||||
            cb(client as IWsCustomData);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const haveGameWs = (accountId: string): boolean => {
 | 
			
		||||
    let ret = false;
 | 
			
		||||
    forEachClient(client => {
 | 
			
		||||
        if (client.isGame && client.accountId == accountId) {
 | 
			
		||||
            ret = true;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    return ret;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const sendWsBroadcast = (data: IWsMsgToClient): void => {
 | 
			
		||||
    const msg = JSON.stringify(data);
 | 
			
		||||
    if (wsServer) {
 | 
			
		||||
        for (const client of wsServer.clients) {
 | 
			
		||||
            client.send(msg);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if (wssServer) {
 | 
			
		||||
        for (const client of wssServer.clients) {
 | 
			
		||||
            client.send(msg);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    forEachClient(client => {
 | 
			
		||||
        client.send(msg);
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const sendWsBroadcastTo = (accountId: string, data: IWsMsgToClient): void => {
 | 
			
		||||
    const msg = JSON.stringify(data);
 | 
			
		||||
    if (wsServer) {
 | 
			
		||||
        for (const client of wsServer.clients) {
 | 
			
		||||
            if ((client as IWsCustomData).accountId == accountId) {
 | 
			
		||||
                client.send(msg);
 | 
			
		||||
            }
 | 
			
		||||
    forEachClient(client => {
 | 
			
		||||
        if (client.accountId == accountId) {
 | 
			
		||||
            client.send(msg);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if (wssServer) {
 | 
			
		||||
        for (const client of wssServer.clients) {
 | 
			
		||||
            if ((client as IWsCustomData).accountId == accountId) {
 | 
			
		||||
                client.send(msg);
 | 
			
		||||
            }
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const sendWsBroadcastToGame = (accountId: string, data: IWsMsgToClient): void => {
 | 
			
		||||
    const msg = JSON.stringify(data);
 | 
			
		||||
    forEachClient(client => {
 | 
			
		||||
        if (client.isGame && client.accountId == accountId) {
 | 
			
		||||
            client.send(msg);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const sendWsBroadcastEx = (data: IWsMsgToClient, accountId?: string, excludeWsid?: number): void => {
 | 
			
		||||
    const msg = JSON.stringify(data);
 | 
			
		||||
    if (wsServer) {
 | 
			
		||||
        for (const client of wsServer.clients) {
 | 
			
		||||
            if (
 | 
			
		||||
                (!accountId || (client as IWsCustomData).accountId == accountId) &&
 | 
			
		||||
                (client as IWsCustomData).id != excludeWsid
 | 
			
		||||
            ) {
 | 
			
		||||
                client.send(msg);
 | 
			
		||||
            }
 | 
			
		||||
    forEachClient(client => {
 | 
			
		||||
        if ((!accountId || client.accountId == accountId) && client.id != excludeWsid) {
 | 
			
		||||
            client.send(msg);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if (wssServer) {
 | 
			
		||||
        for (const client of wssServer.clients) {
 | 
			
		||||
            if (
 | 
			
		||||
                (!accountId || (client as IWsCustomData).accountId == accountId) &&
 | 
			
		||||
                (client as IWsCustomData).id != excludeWsid
 | 
			
		||||
            ) {
 | 
			
		||||
                client.send(msg);
 | 
			
		||||
            }
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const sendWsBroadcastToWebui = (data: IWsMsgToClient, accountId?: string, excludeWsid?: number): void => {
 | 
			
		||||
    const msg = JSON.stringify(data);
 | 
			
		||||
    forEachClient(client => {
 | 
			
		||||
        if (!client.isGame && (!accountId || client.accountId == accountId) && client.id != excludeWsid) {
 | 
			
		||||
            client.send(msg);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const broadcastInventoryUpdate = (req: Request): void => {
 | 
			
		||||
    const accountId = req.query.accountId as string;
 | 
			
		||||
    if (req.query.wsid) {
 | 
			
		||||
        // for webui requests, let other tabs and the game know
 | 
			
		||||
        sendWsBroadcastEx(
 | 
			
		||||
            { sync_inventory: true, update_inventory: true },
 | 
			
		||||
            accountId,
 | 
			
		||||
            parseInt(String(req.query.wsid))
 | 
			
		||||
        );
 | 
			
		||||
    } else {
 | 
			
		||||
        // for game requests, let all webui tabs know
 | 
			
		||||
        sendWsBroadcastToWebui({ update_inventory: true }, accountId, parseInt(String(req.query.wsid)));
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const handleNonceInvalidation = (accountId: string): void => {
 | 
			
		||||
    forEachClient(client => {
 | 
			
		||||
        if (client.accountId == accountId) {
 | 
			
		||||
            if (client.isGame) {
 | 
			
		||||
                client.accountId = undefined; // prevent processing of the close event
 | 
			
		||||
                client.close();
 | 
			
		||||
            } else {
 | 
			
		||||
                client.send(JSON.stringify({ nonce_updated: true, have_game_ws: false } satisfies IWsMsgToClient));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -31,8 +31,17 @@ export interface IGuildClient {
 | 
			
		||||
 | 
			
		||||
    GoalProgress?: IGoalProgressClient[];
 | 
			
		||||
}
 | 
			
		||||
// Fields specific to SNS
 | 
			
		||||
export interface IGuildCheats {
 | 
			
		||||
    noDojoRoomBuildStage?: boolean;
 | 
			
		||||
    noDojoDecoBuildStage?: boolean;
 | 
			
		||||
    fastDojoRoomDestruction?: boolean;
 | 
			
		||||
    noDojoResearchCosts?: boolean;
 | 
			
		||||
    noDojoResearchTime?: boolean;
 | 
			
		||||
    fastClanAscension?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IGuildDatabase {
 | 
			
		||||
export interface IGuildDatabase extends IGuildCheats {
 | 
			
		||||
    _id: Types.ObjectId;
 | 
			
		||||
    Name: string;
 | 
			
		||||
    MOTD: string;
 | 
			
		||||
 | 
			
		||||
@ -1,104 +0,0 @@
 | 
			
		||||
[
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/AmbulasEventBronzeTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/AmbulasEventGoldTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/AmbulasEventSilverTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/AmbulasEventTerracottaTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/AridFearBronzeTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/AridFearGoldTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/AridFearSilverTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/BreedingGroundsBronzeTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/BreedingGroundsGoldTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/BreedingGroundsSilverTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/CiceroCrisisBronzeTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/CiceroCrisisGoldTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/CiceroCrisisSilverTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DisruptionEventBronzeTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DisruptionEventGoldTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DisruptionEventSilverTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DisruptionEventTerracottaTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DojoRemasterTrophyBronzeARecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DojoRemasterTrophyGoldARecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DojoRemasterTrophyPlatinumARecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DojoRemasterTrophySilverARecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DuviriMurmurEventBronzeTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DuviriMurmurEventClayTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DuviriMurmurEventGoldTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DuviriMurmurEventSilverTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EntratiEventBaseTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EntratiEventBronzeTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EntratiEventGoldTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EntratiEventSilverTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EntratiEventTerracottaTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EvacuationEventBronzeTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EvacuationEventGoldTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EvacuationEventSilverTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EvacuationEventTerracottaTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EyesOfBlightTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/FalseProfitBronzeTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/FalseProfitClayTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/FalseProfitGoldTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/FalseProfitSilverTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/FusionMoaTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/GradivusDilemmaCorpusBronzeTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/GradivusDilemmaCorpusGoldTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/GradivusDilemmaCorpusSilverTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/GradivusDilemmaGrineerBronzeTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/GradivusDilemmaGrineerGoldTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/GradivusDilemmaGrineerSilverTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/IcePlanetTrophyBronzeRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/IcePlanetTrophyGoldRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/IcePlanetTrophySilverRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventBaseTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventBronzeTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventGoldTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventPewterTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventSilverTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MechEventTrophyBronzeRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MechEventTrophyGoldRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MechEventTrophySilverRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MechEventTrophyTerracottaRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MutalistIncursionBronzeTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MutalistIncursionGoldTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MutalistIncursionSilverTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/OrokinMusicBoxRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/OrokinSabotageTrophyBronzeRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/OrokinSabotageTrophyGoldRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/OrokinSabotageTrophySilverRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ProjectSinisterBronzeTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ProjectSinisterClayTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ProjectSinisterGoldTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ProjectSinisterSilverTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/RailjackResearchTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/RathuumBronzeTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/RathuumClayTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/RathuumGoldTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/RathuumSilverTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ShipyardsEventBronzeTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ShipyardsEventGoldTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ShipyardsEventSilverTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/SlingStoneTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/SpyDroneTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/SurvivalEventBronzeTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/SurvivalEventGoldTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/SurvivalEventSilverTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/TennoConDojoGhostTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/TennoConDojoMoonTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/TennoConDojoMountainTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/TennoConDojoShadowTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/TennoConDojoStormTrophyRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ThumperTrophyBronzeRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ThumperTrophyCrystalRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ThumperTrophyGoldRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ThumperTrophySilverRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/CorpusPlaceables/GasTurbineConeRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/NaturalPlaceables/CoralChunkARecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/TennoPlaceables/TnoBeaconEmitterRecipe",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/OstronFemaleSitting",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/OstronFemaleStanding",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/OstronMaleStanding",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/OstronMaleStandingTwo",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/SolarisForeman",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/SolarisHazard",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/SolarisStrikerOne",
 | 
			
		||||
  "/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/SolarisStrikerThree"
 | 
			
		||||
]
 | 
			
		||||
@ -71,6 +71,9 @@
 | 
			
		||||
                        <li class="nav-item">
 | 
			
		||||
                            <a class="nav-link" href="/webui/import" data-bs-dismiss="offcanvas" data-bs-target="#sidebar" data-loc="navbar_import"></a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                        <li class="nav-item d-none" id="nav-guildView">
 | 
			
		||||
                            <a class="nav-link" href="/webui/guildView" data-bs-dismiss="offcanvas" data-bs-target="#sidebar" data-loc="navbar_guildView"></a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                    </ul>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
@ -90,7 +93,7 @@
 | 
			
		||||
                </form>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div data-route="/webui/inventory" data-title="Inventory | OpenWF WebUI">
 | 
			
		||||
                <p class="mb-3" data-loc="general_inventoryUpdateNote"></p>
 | 
			
		||||
                <p class="mb-3 inventory-update-note"></p>
 | 
			
		||||
                <div class="card mb-3">
 | 
			
		||||
                    <div class="card-header">
 | 
			
		||||
                        <ul class="nav nav-tabs card-header-tabs">
 | 
			
		||||
@ -480,6 +483,139 @@
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div id="guild-route" data-route="/webui/guildView" data-title="Guild | OpenWF WebUI">
 | 
			
		||||
                <h3 id="guildView-loading" class="mb-0" data-loc="general_loading"></h3>
 | 
			
		||||
                <h3 id="guildView-title" class="mb-0"></h3>
 | 
			
		||||
                <p id="guildView-tier" class="text-body-secondary mb-0"></p>
 | 
			
		||||
                <p id="guildView-class" class="text-body-secondary mb-0"></p>
 | 
			
		||||
                <p id="guildView-alliance" class="text-body-secondary"></p>
 | 
			
		||||
                <div class="row g-3 mb-3">
 | 
			
		||||
                    <div class="col-md-6">
 | 
			
		||||
                        <div class="card">
 | 
			
		||||
                            <h5 class="card-header" data-loc="currency_RegularCredits"></h5>
 | 
			
		||||
                            <div class="card-body">
 | 
			
		||||
                                <p class="card-text" id="VaultRegularCredits-owned"></p>
 | 
			
		||||
                                <form id="vaultRegularCredits-form" class="input-group d-none" onsubmit="doAddCurrency('VaultRegularCredits');return false;">
 | 
			
		||||
                                    <input class="form-control" id="VaultRegularCredits-delta" type="number" value="1000000" />
 | 
			
		||||
                                    <button class="btn btn-primary" type="submit" data-loc="general_addButton"></button>
 | 
			
		||||
                                </form>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="col-md-6">
 | 
			
		||||
                        <div class="card">
 | 
			
		||||
                            <h5 class="card-header" data-loc="currency_PremiumCredits"></h5>
 | 
			
		||||
                            <div class="card-body">
 | 
			
		||||
                                <p class="card-text" id="VaultPremiumCredits-owned"></p>
 | 
			
		||||
                                <form id="vaultPremiumCredits-form" class="input-group d-none" onsubmit="doAddCurrency('VaultPremiumCredits');return false;">
 | 
			
		||||
                                    <input class="form-control" id="VaultPremiumCredits-delta" type="number" value="100" />
 | 
			
		||||
                                    <button class="btn btn-primary" type="submit" data-loc="general_addButton"></button>
 | 
			
		||||
                                </form>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="row g-3 mb-3">
 | 
			
		||||
                    <div class="col-lg-6">
 | 
			
		||||
                        <div class="card" style="height: 400px;">
 | 
			
		||||
                            <h5 class="card-header" data-loc="guildView_techProjects"></h5>
 | 
			
		||||
                            <div class="card-body overflow-auto">
 | 
			
		||||
                                <form id="techProjects-form" class="input-group mb-3 d-none" onsubmit="addGuildTechProject();return false;">
 | 
			
		||||
                                    <input class="form-control" id="acquire-type-TechProjects" list="datalist-TechProjects" />
 | 
			
		||||
                                    <button class="btn btn-primary" type="submit" data-loc="general_addButton"></button>
 | 
			
		||||
                                </form>
 | 
			
		||||
                                <table class="table table-hover w-100">
 | 
			
		||||
                                    <tbody id="TechProjects-list"></tbody>
 | 
			
		||||
                                </table>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="col-lg-6">
 | 
			
		||||
                        <div class="card" style="height: 400px;">
 | 
			
		||||
                            <h5 class="card-header" data-loc="guildView_vaultDecoRecipes"></h5>
 | 
			
		||||
                            <div class="card-body overflow-auto">
 | 
			
		||||
                                <form id="vaultDecoRecipes-form" class="input-group mb-3 d-none" onsubmit="addVaultDecoRecipe();return false;">
 | 
			
		||||
                                    <input class="form-control" id="acquire-type-VaultDecoRecipes" list="datalist-VaultDecoRecipes" />
 | 
			
		||||
                                    <button class="btn btn-primary" type="submit" data-loc="general_addButton"></button>
 | 
			
		||||
                                </form>
 | 
			
		||||
                                <table class="table table-hover w-100">
 | 
			
		||||
                                    <tbody id="VaultDecoRecipes-list"></tbody>
 | 
			
		||||
                                </table>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="row g-3 mb-3">
 | 
			
		||||
                    <div class="col-lg-6">
 | 
			
		||||
                        <div class="card" style="height: 400px;">
 | 
			
		||||
                            <h5 class="card-header" data-loc="guildView_members"></h5>
 | 
			
		||||
                            <div class="card-body overflow-auto">
 | 
			
		||||
                                <table class="table table-hover w-100">
 | 
			
		||||
                                    <tbody id="Members-list"></tbody>
 | 
			
		||||
                                </table>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="col-lg-6">
 | 
			
		||||
                        <div class="card" style="height: 400px;">
 | 
			
		||||
                            <h5 class="card-header" data-loc="guildView_alliance"></h5>
 | 
			
		||||
                            <div class="card-body overflow-auto">
 | 
			
		||||
                                <table class="table table-hover w-100">
 | 
			
		||||
                                    <tbody id="Alliance-list"></tbody>
 | 
			
		||||
                                </table>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="row g-3 mb-3">
 | 
			
		||||
                    <div class="col-lg-6">
 | 
			
		||||
                        <div class="card">
 | 
			
		||||
                            <h5 class="card-header" data-loc="general_bulkActions"></h5>
 | 
			
		||||
                            <div class="card-body" id="guild-actions">
 | 
			
		||||
                                <div class="mb-2 d-flex flex-wrap gap-2">
 | 
			
		||||
                                    <button class="btn btn-primary" onclick="debounce(addMissingTechProjects);" data-loc="guildView_bulkAddTechProjects"></button>
 | 
			
		||||
                                    <button class="btn btn-primary" onclick="debounce(addMissingVaultDecoRecipes);" data-loc="guildView_bulkAddVaultDecoRecipes"></button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="mb-2 d-flex flex-wrap gap-2">
 | 
			
		||||
                                    <button class="btn btn-success" onclick="debounce(fundAllTechProjects);" data-loc="guildView_bulkFundTechProjects"></button>
 | 
			
		||||
                                    <button class="btn btn-success" onclick="debounce(completeAllTechProjects);" data-loc="guildView_bulkCompleteTechProjects"></button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="col-lg-6">
 | 
			
		||||
                        <div class="card mb-3">
 | 
			
		||||
                        <h5 class="card-header" data-loc="guildView_cheats"></h5>
 | 
			
		||||
                            <div class="card-body" id="guild-cheats">
 | 
			
		||||
                                <div class="form-check">
 | 
			
		||||
                                    <input class="form-check-input" type="checkbox" id="noDojoRoomBuildStage" />
 | 
			
		||||
                                    <label class="form-check-label" for="noDojoRoomBuildStage" data-loc="cheats_noDojoRoomBuildStage"></label>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="form-check">
 | 
			
		||||
                                    <input class="form-check-input" type="checkbox" id="noDojoDecoBuildStage" />
 | 
			
		||||
                                    <label class="form-check-label" for="noDojoDecoBuildStage" data-loc="cheats_noDojoDecoBuildStage"></label>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="form-check">
 | 
			
		||||
                                    <input class="form-check-input" type="checkbox" id="noDojoResearchCosts" />
 | 
			
		||||
                                    <label class="form-check-label" for="noDojoResearchCosts" data-loc="cheats_noDojoResearchCosts"></label>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="form-check">
 | 
			
		||||
                                    <input class="form-check-input" type="checkbox" id="noDojoResearchTime" />
 | 
			
		||||
                                    <label class="form-check-label" for="noDojoResearchTime" data-loc="cheats_noDojoResearchTime"></label>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="form-check">
 | 
			
		||||
                                    <input class="form-check-input" type="checkbox" id="fastDojoRoomDestruction" />
 | 
			
		||||
                                    <label class="form-check-label" for="fastDojoRoomDestruction" data-loc="cheats_fastDojoRoomDestruction"></label>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="form-check">
 | 
			
		||||
                                    <input class="form-check-input" type="checkbox" id="fastClanAscension" />
 | 
			
		||||
                                    <label class="form-check-label" for="fastClanAscension" data-loc="cheats_fastClanAscension"></label>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div id="detailedView-route" data-route="/webui/detailedView" data-title="Inventory | OpenWF WebUI">
 | 
			
		||||
                <h3 id="detailedView-loading" class="mb-0" data-loc="general_loading"></h3>
 | 
			
		||||
                <h3 id="detailedView-title" class="mb-0"></h3>
 | 
			
		||||
@ -579,7 +715,7 @@
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div data-route="/webui/mods" data-title="Mods | OpenWF WebUI">
 | 
			
		||||
                <p class="mb-3" data-loc="general_inventoryUpdateNote"></p>
 | 
			
		||||
                <p class="mb-3 inventory-update-note"></p>
 | 
			
		||||
                <div class="row g-3">
 | 
			
		||||
                    <div class="col-xxl-6">
 | 
			
		||||
                        <div class="card mb-3">
 | 
			
		||||
@ -635,7 +771,7 @@
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div data-route="/webui/quests" data-title="Quests | OpenWF WebUI">
 | 
			
		||||
                <p class="mb-3" data-loc="general_inventoryUpdateNote"></p>
 | 
			
		||||
                <p class="mb-3 inventory-update-note"></p>
 | 
			
		||||
                <div class="row g-3">
 | 
			
		||||
                    <div class="col-md-6">
 | 
			
		||||
                        <div class="card">
 | 
			
		||||
@ -972,10 +1108,6 @@
 | 
			
		||||
                                        <input class="form-check-input" type="checkbox" id="unlockAllSkins" />
 | 
			
		||||
                                        <label class="form-check-label" for="unlockAllSkins" data-loc="cheats_unlockAllSkins"></label>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    <div class="form-check">
 | 
			
		||||
                                        <input class="form-check-input" type="checkbox" id="unlockAllDecoRecipes" />
 | 
			
		||||
                                        <label class="form-check-label" for="unlockAllDecoRecipes" data-loc="cheats_unlockAllDecoRecipes"></label>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    <div class="form-check">
 | 
			
		||||
                                        <input class="form-check-input" type="checkbox" id="fullyStockedVendors" />
 | 
			
		||||
                                        <label class="form-check-label" for="fullyStockedVendors" data-loc="cheats_fullyStockedVendors"></label>
 | 
			
		||||
@ -984,30 +1116,6 @@
 | 
			
		||||
                                        <input class="form-check-input" type="checkbox" id="skipClanKeyCrafting" />
 | 
			
		||||
                                        <label class="form-check-label" for="skipClanKeyCrafting" data-loc="cheats_skipClanKeyCrafting"></label>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    <div class="form-check">
 | 
			
		||||
                                        <input class="form-check-input" type="checkbox" id="noDojoRoomBuildStage" />
 | 
			
		||||
                                        <label class="form-check-label" for="noDojoRoomBuildStage" data-loc="cheats_noDojoRoomBuildStage"></label>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    <div class="form-check">
 | 
			
		||||
                                        <input class="form-check-input" type="checkbox" id="noDojoDecoBuildStage" />
 | 
			
		||||
                                        <label class="form-check-label" for="noDojoDecoBuildStage" data-loc="cheats_noDojoDecoBuildStage"></label>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    <div class="form-check">
 | 
			
		||||
                                        <input class="form-check-input" type="checkbox" id="fastDojoRoomDestruction" />
 | 
			
		||||
                                        <label class="form-check-label" for="fastDojoRoomDestruction" data-loc="cheats_fastDojoRoomDestruction"></label>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    <div class="form-check">
 | 
			
		||||
                                        <input class="form-check-input" type="checkbox" id="noDojoResearchCosts" />
 | 
			
		||||
                                        <label class="form-check-label" for="noDojoResearchCosts" data-loc="cheats_noDojoResearchCosts"></label>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    <div class="form-check">
 | 
			
		||||
                                        <input class="form-check-input" type="checkbox" id="noDojoResearchTime" />
 | 
			
		||||
                                        <label class="form-check-label" for="noDojoResearchTime" data-loc="cheats_noDojoResearchTime"></label>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    <div class="form-check">
 | 
			
		||||
                                        <input class="form-check-input" type="checkbox" id="fastClanAscension" />
 | 
			
		||||
                                        <label class="form-check-label" for="fastClanAscension" data-loc="cheats_fastClanAscension"></label>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    <form class="form-group mt-2" onsubmit="doSaveConfigInt('spoofMasteryRank'); return false;">
 | 
			
		||||
                                        <label class="form-label" for="spoofMasteryRank" data-loc="cheats_spoofMasteryRank"></label>
 | 
			
		||||
                                        <div class="input-group">
 | 
			
		||||
@ -1394,6 +1502,8 @@
 | 
			
		||||
    <datalist id="datalist-ModularParts-KUBROW_MUTAGEN"></datalist>
 | 
			
		||||
    <datalist id="datalist-Boosters"></datalist>
 | 
			
		||||
    <datalist id="datalist-Abilities"></datalist>
 | 
			
		||||
    <datalist id="datalist-TechProjects"></datalist>
 | 
			
		||||
    <datalist id="datalist-VaultDecoRecipes"></datalist>
 | 
			
		||||
    <datalist id="datalist-circuitGameModes">
 | 
			
		||||
        <option>Survival</option>
 | 
			
		||||
        <option>VoidFlood</option>
 | 
			
		||||
 | 
			
		||||
@ -63,7 +63,7 @@ function openWebSocket() {
 | 
			
		||||
            }
 | 
			
		||||
            $(".displayname").text(data.DisplayName);
 | 
			
		||||
            window.accountId = data.id;
 | 
			
		||||
            window.authz = "accountId=" + data.id + "&nonce=" + data.Nonce;
 | 
			
		||||
            window.authz = "accountId=" + data.id + "&nonce=" + data.Nonce + "&wsid=" + wsid;
 | 
			
		||||
            if (window.dict) {
 | 
			
		||||
                updateLocElements();
 | 
			
		||||
            }
 | 
			
		||||
@ -90,6 +90,14 @@ function openWebSocket() {
 | 
			
		||||
        if ("logged_out" in msg) {
 | 
			
		||||
            logout();
 | 
			
		||||
        }
 | 
			
		||||
        if ("have_game_ws" in msg) {
 | 
			
		||||
            window.have_game_ws = msg.have_game_ws;
 | 
			
		||||
            if (window.dict) {
 | 
			
		||||
                $(".inventory-update-note").text(
 | 
			
		||||
                    loc(msg.have_game_ws ? "general_inventoryUpdateNoteGameWs" : "general_inventoryUpdateNote")
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    window.ws.onclose = function () {
 | 
			
		||||
        ws_is_open = false;
 | 
			
		||||
@ -223,6 +231,9 @@ function updateLocElements() {
 | 
			
		||||
    document.querySelectorAll("[data-loc-replace]").forEach(elm => {
 | 
			
		||||
        elm.innerHTML = elm.innerHTML.replace("|VAL|", elm.getAttribute("data-loc-replace"));
 | 
			
		||||
    });
 | 
			
		||||
    $(".inventory-update-note").text(
 | 
			
		||||
        loc(window.have_game_ws ? "general_inventoryUpdateNoteGameWs" : "general_inventoryUpdateNote")
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setActiveLanguage(lang) {
 | 
			
		||||
@ -256,6 +267,9 @@ function setLanguage(lang) {
 | 
			
		||||
        // Not in prelogin state?
 | 
			
		||||
        fetchItemList();
 | 
			
		||||
        updateInventory();
 | 
			
		||||
        if (single.getCurrentPath().startsWith("/webui/guildView")) {
 | 
			
		||||
            updateInventory();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -490,6 +504,33 @@ function fetchItemList() {
 | 
			
		||||
                    name: data.ModularParts.find(
 | 
			
		||||
                        i => i.uniqueName === "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC"
 | 
			
		||||
                    ).name
 | 
			
		||||
                },
 | 
			
		||||
                "/Lotus/Language/Game/Rank_Creator": {
 | 
			
		||||
                    name: loc("guildView_rank_creator")
 | 
			
		||||
                },
 | 
			
		||||
                "/Lotus/Language/Game/Rank_Warlord": {
 | 
			
		||||
                    name: loc("guildView_rank_warlord")
 | 
			
		||||
                },
 | 
			
		||||
                "/Lotus/Language/Game/Rank_General": {
 | 
			
		||||
                    name: loc("guildView_rank_general")
 | 
			
		||||
                },
 | 
			
		||||
                "/Lotus/Language/Game/Rank_Officer": {
 | 
			
		||||
                    name: loc("guildView_rank_officer")
 | 
			
		||||
                },
 | 
			
		||||
                "/Lotus/Language/Game/Rank_Leader": {
 | 
			
		||||
                    name: loc("guildView_rank_leader")
 | 
			
		||||
                },
 | 
			
		||||
                "/Lotus/Language/Game/Rank_Sage": {
 | 
			
		||||
                    name: loc("guildView_rank_sage")
 | 
			
		||||
                },
 | 
			
		||||
                "/Lotus/Language/Game/Rank_Soldier": {
 | 
			
		||||
                    name: loc("guildView_rank_soldier")
 | 
			
		||||
                },
 | 
			
		||||
                "/Lotus/Language/Game/Rank_Initiate": {
 | 
			
		||||
                    name: loc("guildView_rank_initiate")
 | 
			
		||||
                },
 | 
			
		||||
                "/Lotus/Language/Game/Rank_Utility": {
 | 
			
		||||
                    name: loc("guildView_rank_utility")
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
            for (const [type, items] of Object.entries(data)) {
 | 
			
		||||
@ -623,6 +664,12 @@ function updateInventory() {
 | 
			
		||||
    req.done(data => {
 | 
			
		||||
        window.itemListPromise.then(itemMap => {
 | 
			
		||||
            window.didInitialInventoryUpdate = true;
 | 
			
		||||
            if (data.GuildId.$oid) {
 | 
			
		||||
                window.guildId = data.GuildId.$oid;
 | 
			
		||||
                document.getElementById("nav-guildView").classList.remove("d-none");
 | 
			
		||||
            } else {
 | 
			
		||||
                document.getElementById("nav-guildView").classList.add("d-none");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const modularWeapons = [
 | 
			
		||||
                "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary",
 | 
			
		||||
@ -935,7 +982,7 @@ function updateInventory() {
 | 
			
		||||
                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);
 | 
			
		||||
                        reAddToItemList(itemMap, "QuestKeys", questKey.uniqueName);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
@ -1032,7 +1079,7 @@ function updateInventory() {
 | 
			
		||||
                        a.href = "#";
 | 
			
		||||
                        a.onclick = function (event) {
 | 
			
		||||
                            event.preventDefault();
 | 
			
		||||
                            readdQuestKey(itemMap, item.ItemType);
 | 
			
		||||
                            reAddToItemList(itemMap, "QuestKeys", item.ItemType);
 | 
			
		||||
                            doQuestUpdate("deleteKey", item.ItemType);
 | 
			
		||||
                        };
 | 
			
		||||
                        a.title = loc("code_remove");
 | 
			
		||||
@ -1502,6 +1549,315 @@ function updateInventory() {
 | 
			
		||||
                document.getElementById("Boosters-list").appendChild(tr);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (single.getCurrentPath().startsWith("/webui/guildView")) {
 | 
			
		||||
                const guildReq = $.get("/custom/getGuild?guildId=" + window.guildId);
 | 
			
		||||
                guildReq.done(guildData => {
 | 
			
		||||
                    window.itemListPromise.then(itemMap => {
 | 
			
		||||
                        document.getElementById("guildView-loading").classList.add("d-none");
 | 
			
		||||
 | 
			
		||||
                        document.getElementById("guildView-title").textContent = guildData.Name;
 | 
			
		||||
                        document.getElementById("guildView-tier").textContent = loc("guildView_tierDisplay")
 | 
			
		||||
                            .split("|TIER|")
 | 
			
		||||
                            .join(loc(`guildView_tier${guildData.Tier}`));
 | 
			
		||||
                        document.getElementById("guildView-class").textContent = loc("guildView_classDisplay")
 | 
			
		||||
                            .split("|CLASS|")
 | 
			
		||||
                            .join(guildData.Class);
 | 
			
		||||
 | 
			
		||||
                        ["VaultRegularCredits", "VaultPremiumCredits"].forEach(currency => {
 | 
			
		||||
                            document.getElementById(currency + "-owned").textContent = loc("guildView_currency_owned")
 | 
			
		||||
                                .split("|COUNT|")
 | 
			
		||||
                                .join((guildData[currency] ?? 0).toLocaleString());
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                        const userGuildMember = guildData.Members.find(m => m._id.$oid === window.accountId);
 | 
			
		||||
                        let userGuildPermissions;
 | 
			
		||||
                        if (userGuildMember) {
 | 
			
		||||
                            userGuildPermissions = guildData.Ranks[userGuildMember.Rank].Permissions;
 | 
			
		||||
                            // Ruler = 1, // Clan: Change hierarchy. Alliance (Creator only): Kick clans.
 | 
			
		||||
                            // Advertiser = 8192,
 | 
			
		||||
                            // Recruiter = 2, // Send invites (Clans & Alliances)
 | 
			
		||||
                            // Regulator = 4, // Kick members
 | 
			
		||||
                            // Promoter = 8, // Clan: Promote and demote members. Alliance (Creator only): Change clan permissions.
 | 
			
		||||
                            // Architect = 16, // Create and destroy rooms
 | 
			
		||||
                            // Host = 32, // No longer used in modern versions
 | 
			
		||||
                            // Decorator = 1024, // Create and destroy decos
 | 
			
		||||
                            // Treasurer = 64, // Clan: Contribute from vault and edit tax rate. Alliance: Divvy vault.
 | 
			
		||||
                            // Tech = 128, // Queue research
 | 
			
		||||
                            // ChatModerator = 512, // (Clans & Alliances)
 | 
			
		||||
                            // Herald = 2048, // Change MOTD
 | 
			
		||||
                            // Fabricator = 4096 // Replicate research
 | 
			
		||||
                            if (userGuildPermissions & 128) {
 | 
			
		||||
                                document.getElementById("techProjects-form").classList.remove("d-none");
 | 
			
		||||
                            }
 | 
			
		||||
                            if (userGuildPermissions & 16) {
 | 
			
		||||
                                document.getElementById("vaultDecoRecipes-form").classList.remove("d-none");
 | 
			
		||||
                            }
 | 
			
		||||
                            if (userGuildPermissions & 64) {
 | 
			
		||||
                                document.getElementById("vaultRegularCredits-form").classList.remove("d-none");
 | 
			
		||||
                                document.getElementById("VaultRegularCredits-owned").classList.remove("mb-0");
 | 
			
		||||
                                document.getElementById("vaultPremiumCredits-form").classList.remove("d-none");
 | 
			
		||||
                                document.getElementById("VaultPremiumCredits-owned").classList.remove("mb-0");
 | 
			
		||||
                            }
 | 
			
		||||
                            if (userGuildMember.Rank <= 1) {
 | 
			
		||||
                                document.querySelectorAll("#guild-actions button").forEach(btn => {
 | 
			
		||||
                                    btn.disabled = false;
 | 
			
		||||
                                });
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        const guildCheats = document.querySelectorAll("#guild-cheats input[id]");
 | 
			
		||||
                        for (const elm of guildCheats) {
 | 
			
		||||
                            elm.checked = !!guildData[elm.id];
 | 
			
		||||
                            if (!userGuildMember || userGuildMember.Rank > 1) {
 | 
			
		||||
                                elm.disabled = true;
 | 
			
		||||
                            } else {
 | 
			
		||||
                                elm.disabled = false;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        document.getElementById("TechProjects-list").innerHTML = "";
 | 
			
		||||
                        guildData.TechProjects ??= [];
 | 
			
		||||
                        guildData.TechProjects.forEach(item => {
 | 
			
		||||
                            const datalist = document.getElementById("datalist-TechProjects");
 | 
			
		||||
                            const optionToRemove = datalist.querySelector(`option[data-key="${item.ItemType}"]`);
 | 
			
		||||
                            if (optionToRemove) {
 | 
			
		||||
                                datalist.removeChild(optionToRemove);
 | 
			
		||||
                            }
 | 
			
		||||
                            const tr = document.createElement("tr");
 | 
			
		||||
                            tr.setAttribute("data-item-type", item.ItemType);
 | 
			
		||||
                            {
 | 
			
		||||
                                const td = document.createElement("td");
 | 
			
		||||
                                td.textContent = itemMap[item.ItemType]?.name ?? item.ItemType;
 | 
			
		||||
                                if (new Date(item.CompletionDate) < new Date()) {
 | 
			
		||||
                                    td.textContent += " | " + loc("code_completed");
 | 
			
		||||
                                } else if (item.State == 1) {
 | 
			
		||||
                                    td.textContent += " | " + loc("code_funded");
 | 
			
		||||
                                }
 | 
			
		||||
                                tr.appendChild(td);
 | 
			
		||||
                            }
 | 
			
		||||
                            {
 | 
			
		||||
                                const td = document.createElement("td");
 | 
			
		||||
                                td.classList = "text-end text-nowrap";
 | 
			
		||||
 | 
			
		||||
                                if (userGuildPermissions && userGuildPermissions & 128 && item.State != 1) {
 | 
			
		||||
                                    const a = document.createElement("a");
 | 
			
		||||
                                    a.href = "#";
 | 
			
		||||
                                    a.onclick = function (event) {
 | 
			
		||||
                                        event.preventDefault();
 | 
			
		||||
                                        fundGuildTechProject(item.ItemType);
 | 
			
		||||
                                    };
 | 
			
		||||
                                    a.title = loc("code_fund");
 | 
			
		||||
                                    a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--!Font Awesome Free v7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M214.6 17.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 117.3 160 488c0 17.7 14.3 32 32 32s32-14.3 32-32l0-370.7 105.4 105.4c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z"/></svg>`;
 | 
			
		||||
                                    td.appendChild(a);
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                                if (
 | 
			
		||||
                                    userGuildPermissions &&
 | 
			
		||||
                                    userGuildPermissions & 128 &&
 | 
			
		||||
                                    item.State == 1 &&
 | 
			
		||||
                                    new Date(item.CompletionDate) > new Date()
 | 
			
		||||
                                ) {
 | 
			
		||||
                                    const a = document.createElement("a");
 | 
			
		||||
                                    a.href = "#";
 | 
			
		||||
                                    a.onclick = function (event) {
 | 
			
		||||
                                        event.preventDefault();
 | 
			
		||||
                                        completeGuildTechProject(item.ItemType);
 | 
			
		||||
                                    };
 | 
			
		||||
                                    a.title = loc("code_complete");
 | 
			
		||||
                                    a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--!Font Awesome Free v7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M371.7 43.1C360.1 32 343 28.9 328.3 35.2S304 56 304 72l0 136.3-172.3-165.1C120.1 32 103 28.9 88.3 35.2S64 56 64 72l0 368c0 16 9.6 30.5 24.3 36.8s31.8 3.2 43.4-7.9L304 303.7 304 440c0 16 9.6 30.5 24.3 36.8s31.8 3.2 43.4-7.9l192-184c7.9-7.5 12.3-18 12.3-28.9s-4.5-21.3-12.3-28.9l-192-184z"/></svg>`;
 | 
			
		||||
                                    td.appendChild(a);
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                                if (userGuildMember && userGuildMember.Rank <= 1) {
 | 
			
		||||
                                    const a = document.createElement("a");
 | 
			
		||||
                                    a.href = "#";
 | 
			
		||||
                                    a.onclick = function (event) {
 | 
			
		||||
                                        event.preventDefault();
 | 
			
		||||
                                        reAddToItemList(itemMap, "TechProjects", item.ItemType);
 | 
			
		||||
                                        removeGuildTechProject(item.ItemType);
 | 
			
		||||
                                    };
 | 
			
		||||
                                    a.title = loc("code_remove");
 | 
			
		||||
                                    a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"/></svg>`;
 | 
			
		||||
                                    td.appendChild(a);
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                                tr.appendChild(td);
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            document.getElementById("TechProjects-list").appendChild(tr);
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                        document.getElementById("VaultDecoRecipes-list").innerHTML = "";
 | 
			
		||||
                        guildData.VaultDecoRecipes ??= [];
 | 
			
		||||
                        guildData.VaultDecoRecipes.forEach(item => {
 | 
			
		||||
                            const datalist = document.getElementById("datalist-VaultDecoRecipes");
 | 
			
		||||
                            const optionToRemove = datalist.querySelector(`option[data-key="${item.ItemType}"]`);
 | 
			
		||||
                            if (optionToRemove) {
 | 
			
		||||
                                datalist.removeChild(optionToRemove);
 | 
			
		||||
                            }
 | 
			
		||||
                            const tr = document.createElement("tr");
 | 
			
		||||
                            tr.setAttribute("data-item-type", item.ItemType);
 | 
			
		||||
                            {
 | 
			
		||||
                                const td = document.createElement("td");
 | 
			
		||||
                                td.textContent = itemMap[item.ItemType]?.name ?? item.ItemType;
 | 
			
		||||
                                tr.appendChild(td);
 | 
			
		||||
                            }
 | 
			
		||||
                            {
 | 
			
		||||
                                const td = document.createElement("td");
 | 
			
		||||
                                td.classList = "text-end text-nowrap";
 | 
			
		||||
 | 
			
		||||
                                if (userGuildMember && userGuildMember.Rank <= 1) {
 | 
			
		||||
                                    const a = document.createElement("a");
 | 
			
		||||
                                    a.href = "#";
 | 
			
		||||
                                    a.onclick = function (event) {
 | 
			
		||||
                                        event.preventDefault();
 | 
			
		||||
                                        reAddToItemList(itemMap, "VaultDecoRecipes", item.ItemType);
 | 
			
		||||
                                        removeVaultDecoRecipe(item.ItemType);
 | 
			
		||||
                                    };
 | 
			
		||||
                                    a.title = loc("code_remove");
 | 
			
		||||
                                    a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"/></svg>`;
 | 
			
		||||
                                    td.appendChild(a);
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                                tr.appendChild(td);
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            document.getElementById("VaultDecoRecipes-list").appendChild(tr);
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                        document.getElementById("Members-list").innerHTML = "";
 | 
			
		||||
                        guildData.Members.forEach(member => {
 | 
			
		||||
                            const tr = document.createElement("tr");
 | 
			
		||||
                            {
 | 
			
		||||
                                const td = document.createElement("td");
 | 
			
		||||
                                const memberRank = guildData.Ranks[member.Rank];
 | 
			
		||||
                                td.textContent = member.DisplayName;
 | 
			
		||||
                                td.textContent += " | " + itemMap[memberRank.Name]?.name ?? memberRank.Name;
 | 
			
		||||
                                if (member.Status != 0) {
 | 
			
		||||
                                    td.textContent += " | " + loc("guildView_pending");
 | 
			
		||||
                                }
 | 
			
		||||
                                tr.appendChild(td);
 | 
			
		||||
                            }
 | 
			
		||||
                            {
 | 
			
		||||
                                const td = document.createElement("td");
 | 
			
		||||
                                td.classList = "text-end text-nowrap";
 | 
			
		||||
 | 
			
		||||
                                if (
 | 
			
		||||
                                    userGuildMember &&
 | 
			
		||||
                                    member.Rank < 8 &&
 | 
			
		||||
                                    member.Rank > userGuildMember.Rank &&
 | 
			
		||||
                                    userGuildPermissions &&
 | 
			
		||||
                                    userGuildPermissions & 8
 | 
			
		||||
                                ) {
 | 
			
		||||
                                    const a = document.createElement("a");
 | 
			
		||||
                                    a.href = "#";
 | 
			
		||||
                                    a.onclick = function (event) {
 | 
			
		||||
                                        event.preventDefault();
 | 
			
		||||
                                        changeGuildRank(guildId, member._id.$oid, member.Rank + 1);
 | 
			
		||||
                                    };
 | 
			
		||||
                                    a.title = loc("guildView_demote");
 | 
			
		||||
                                    a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"/></svg>`;
 | 
			
		||||
                                    td.appendChild(a);
 | 
			
		||||
                                }
 | 
			
		||||
                                if (
 | 
			
		||||
                                    userGuildMember &&
 | 
			
		||||
                                    member.Rank > userGuildMember.Rank &&
 | 
			
		||||
                                    userGuildPermissions &&
 | 
			
		||||
                                    userGuildPermissions & 8
 | 
			
		||||
                                ) {
 | 
			
		||||
                                    const a = document.createElement("a");
 | 
			
		||||
                                    a.href = "#";
 | 
			
		||||
                                    a.onclick = function (event) {
 | 
			
		||||
                                        event.preventDefault();
 | 
			
		||||
                                        changeGuildRank(guildId, member._id.$oid, member.Rank - 1);
 | 
			
		||||
                                    };
 | 
			
		||||
                                    a.title = loc("guildView_promote");
 | 
			
		||||
                                    a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M233.4 105.4c12.5-12.5 32.8-12.5 45.3 0l192 192c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L256 173.3 86.6 342.6c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l192-192z"/></svg>`;
 | 
			
		||||
                                    td.appendChild(a);
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                                if (
 | 
			
		||||
                                    (userGuildMember &&
 | 
			
		||||
                                        member.Rank > userGuildMember.Rank &&
 | 
			
		||||
                                        userGuildPermissions &&
 | 
			
		||||
                                        userGuildPermissions & 4) ||
 | 
			
		||||
                                    (userGuildMember && userGuildMember.Rank != 0 && userGuildMember._id == member._id)
 | 
			
		||||
                                ) {
 | 
			
		||||
                                    const a = document.createElement("a");
 | 
			
		||||
                                    a.href = "#";
 | 
			
		||||
                                    a.onclick = function (event) {
 | 
			
		||||
                                        event.preventDefault();
 | 
			
		||||
                                        kickFromGuild(member._id.$oid);
 | 
			
		||||
                                    };
 | 
			
		||||
                                    a.title = loc("code_remove");
 | 
			
		||||
                                    a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"/></svg>`;
 | 
			
		||||
                                    td.appendChild(a);
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                                tr.appendChild(td);
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            document.getElementById("Members-list").appendChild(tr);
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                        if (guildData.AllianceId) {
 | 
			
		||||
                            const allianceReq = $.get("/custom/getAlliance?guildId=" + guildId);
 | 
			
		||||
                            allianceReq.done(allianceData => {
 | 
			
		||||
                                document.getElementById("guildView-alliance").textContent =
 | 
			
		||||
                                    loc("guildView_alliance") + ": " + allianceData.Name;
 | 
			
		||||
 | 
			
		||||
                                let userAlliancePermisssions;
 | 
			
		||||
                                if (userGuildMember && userGuildMember.Rank <= 1) {
 | 
			
		||||
                                    userAlliancePermisssions = allianceData.Clans.find(
 | 
			
		||||
                                        c => c._id.$oid === guildId
 | 
			
		||||
                                    ).Permissions;
 | 
			
		||||
                                }
 | 
			
		||||
                                document.getElementById("Alliance-list").innerHTML = "";
 | 
			
		||||
                                allianceData.Clans.forEach(clan => {
 | 
			
		||||
                                    const tr = document.createElement("tr");
 | 
			
		||||
                                    {
 | 
			
		||||
                                        const td = document.createElement("td");
 | 
			
		||||
                                        td.textContent = clan.Name;
 | 
			
		||||
                                        if (clan.Pending) {
 | 
			
		||||
                                            td.textContent += " | " + loc("guildView_pending");
 | 
			
		||||
                                        }
 | 
			
		||||
                                        tr.appendChild(td);
 | 
			
		||||
                                    }
 | 
			
		||||
                                    {
 | 
			
		||||
                                        const td = document.createElement("td");
 | 
			
		||||
                                        td.classList = "text-end text-nowrap";
 | 
			
		||||
 | 
			
		||||
                                        if (
 | 
			
		||||
                                            !(clan.Permissions & 1) &&
 | 
			
		||||
                                            userAlliancePermisssions &&
 | 
			
		||||
                                            userAlliancePermisssions & 1
 | 
			
		||||
                                        ) {
 | 
			
		||||
                                            const a = document.createElement("a");
 | 
			
		||||
                                            a.href = "#";
 | 
			
		||||
                                            a.onclick = function (event) {
 | 
			
		||||
                                                event.preventDefault();
 | 
			
		||||
                                                kickFromAlliance(clan._id.$oid);
 | 
			
		||||
                                            };
 | 
			
		||||
                                            a.title = loc("code_remove");
 | 
			
		||||
                                            a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"/></svg>`;
 | 
			
		||||
                                            td.appendChild(a);
 | 
			
		||||
                                        }
 | 
			
		||||
 | 
			
		||||
                                        tr.appendChild(td);
 | 
			
		||||
                                    }
 | 
			
		||||
 | 
			
		||||
                                    document.getElementById("Alliance-list").appendChild(tr);
 | 
			
		||||
                                });
 | 
			
		||||
                            });
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                guildReq.fail(() => {
 | 
			
		||||
                    single.loadRoute("/webui/inventory");
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            for (const elm of accountCheats) {
 | 
			
		||||
                if (elm.type === "checkbox") {
 | 
			
		||||
                    elm.checked = !!data[elm.id];
 | 
			
		||||
@ -1513,6 +1869,74 @@ function updateInventory() {
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addVaultDecoRecipe() {
 | 
			
		||||
    const uniqueName = getKey(document.getElementById("acquire-type-VaultDecoRecipes"));
 | 
			
		||||
    if (!guildId) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!uniqueName) {
 | 
			
		||||
        $("acquire-type-VaultDecoRecipes").addClass("is-invalid").focus();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    revalidateAuthz().then(() => {
 | 
			
		||||
        const req = $.post({
 | 
			
		||||
            url: "/custom/addVaultDecoRecipe?" + window.authz + "&guildId=" + window.guildId,
 | 
			
		||||
            contentType: "application/json",
 | 
			
		||||
            data: JSON.stringify([
 | 
			
		||||
                {
 | 
			
		||||
                    ItemType: uniqueName,
 | 
			
		||||
                    ItemCount: 1
 | 
			
		||||
                }
 | 
			
		||||
            ])
 | 
			
		||||
        });
 | 
			
		||||
        req.done(() => {
 | 
			
		||||
            updateInventory();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function changeGuildRank(guildId, targetId, rankChange) {
 | 
			
		||||
    revalidateAuthz().then(() => {
 | 
			
		||||
        const req = $.get(
 | 
			
		||||
            "/api/changeGuildRank.php?" +
 | 
			
		||||
                window.authz +
 | 
			
		||||
                "&guildId=" +
 | 
			
		||||
                guildId +
 | 
			
		||||
                "&targetId=" +
 | 
			
		||||
                targetId +
 | 
			
		||||
                "&rankChange=" +
 | 
			
		||||
                rankChange
 | 
			
		||||
        );
 | 
			
		||||
        req.done(() => {
 | 
			
		||||
            updateInventory();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function kickFromGuild(accountId) {
 | 
			
		||||
    revalidateAuthz().then(() => {
 | 
			
		||||
        const req = $.post({
 | 
			
		||||
            url: "/api/removeFromGuild.php?" + window.authz + "&guildId=" + window.guildId,
 | 
			
		||||
            contentType: "application/octet-stream",
 | 
			
		||||
            data: JSON.stringify({
 | 
			
		||||
                userId: accountId
 | 
			
		||||
            })
 | 
			
		||||
        });
 | 
			
		||||
        req.done(() => {
 | 
			
		||||
            updateInventory();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function kickFromAlliance(guildId) {
 | 
			
		||||
    revalidateAuthz().then(() => {
 | 
			
		||||
        const req = $.get("/api/removeFromAlliance.php?" + window.authz + "&guildId=" + guildId);
 | 
			
		||||
        req.done(() => {
 | 
			
		||||
            updateInventory();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getKey(input) {
 | 
			
		||||
    return document
 | 
			
		||||
        .getElementById(input.getAttribute("list"))
 | 
			
		||||
@ -1737,6 +2161,262 @@ function addMissingEquipment(categories) {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addVaultDecoRecipe() {
 | 
			
		||||
    const uniqueName = getKey(document.getElementById("acquire-type-VaultDecoRecipes"));
 | 
			
		||||
    if (!uniqueName) {
 | 
			
		||||
        $("#acquire-type-VaultDecoRecipes").addClass("is-invalid").focus();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    revalidateAuthz().then(() => {
 | 
			
		||||
        const req = $.post({
 | 
			
		||||
            url: "/custom/addVaultDecoRecipe?" + window.authz + "&guildId=" + window.guildId,
 | 
			
		||||
            contentType: "application/json",
 | 
			
		||||
            data: JSON.stringify([
 | 
			
		||||
                {
 | 
			
		||||
                    ItemType: uniqueName,
 | 
			
		||||
                    ItemCount: 1
 | 
			
		||||
                }
 | 
			
		||||
            ])
 | 
			
		||||
        });
 | 
			
		||||
        req.done(() => {
 | 
			
		||||
            document.getElementById("acquire-type-VaultDecoRecipes").value = "";
 | 
			
		||||
            updateInventory();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function removeVaultDecoRecipe(uniqueName) {
 | 
			
		||||
    revalidateAuthz().then(() => {
 | 
			
		||||
        const req = $.post({
 | 
			
		||||
            url: "/custom/addVaultDecoRecipe?" + window.authz + "&guildId=" + window.guildId,
 | 
			
		||||
            contentType: "application/json",
 | 
			
		||||
            data: JSON.stringify([
 | 
			
		||||
                {
 | 
			
		||||
                    ItemType: uniqueName,
 | 
			
		||||
                    ItemCount: -1
 | 
			
		||||
                }
 | 
			
		||||
            ])
 | 
			
		||||
        });
 | 
			
		||||
        req.done(() => {
 | 
			
		||||
            updateInventory();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addGuildTechProject() {
 | 
			
		||||
    const uniqueName = getKey(document.getElementById("acquire-type-TechProjects"));
 | 
			
		||||
    if (!uniqueName) {
 | 
			
		||||
        $("#acquire-type-TechProjects").addClass("is-invalid").focus();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    revalidateAuthz().then(() => {
 | 
			
		||||
        const req = $.post({
 | 
			
		||||
            url: "/custom/addTechProject?" + window.authz + "&guildId=" + window.guildId,
 | 
			
		||||
            contentType: "application/json",
 | 
			
		||||
            data: JSON.stringify([
 | 
			
		||||
                {
 | 
			
		||||
                    ItemType: uniqueName
 | 
			
		||||
                }
 | 
			
		||||
            ])
 | 
			
		||||
        });
 | 
			
		||||
        req.done(() => {
 | 
			
		||||
            document.getElementById("acquire-type-TechProjects").value = "";
 | 
			
		||||
            updateInventory();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function removeGuildTechProject(uniqueName) {
 | 
			
		||||
    revalidateAuthz().then(() => {
 | 
			
		||||
        const req = $.post({
 | 
			
		||||
            url: "/custom/removeTechProject?" + window.authz + "&guildId=" + window.guildId,
 | 
			
		||||
            contentType: "application/json",
 | 
			
		||||
            data: JSON.stringify([
 | 
			
		||||
                {
 | 
			
		||||
                    ItemType: uniqueName
 | 
			
		||||
                }
 | 
			
		||||
            ])
 | 
			
		||||
        });
 | 
			
		||||
        req.done(() => {
 | 
			
		||||
            updateInventory();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function completeGuildTechProject(uniqueName) {
 | 
			
		||||
    revalidateAuthz().then(() => {
 | 
			
		||||
        const req = $.post({
 | 
			
		||||
            url: "/custom/completeTechProject?" + window.authz + "&guildId=" + window.guildId,
 | 
			
		||||
            contentType: "application/json",
 | 
			
		||||
            data: JSON.stringify([
 | 
			
		||||
                {
 | 
			
		||||
                    ItemType: uniqueName
 | 
			
		||||
                }
 | 
			
		||||
            ])
 | 
			
		||||
        });
 | 
			
		||||
        req.done(() => {
 | 
			
		||||
            updateInventory();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function fundGuildTechProject(uniqueName) {
 | 
			
		||||
    revalidateAuthz().then(() => {
 | 
			
		||||
        const req = $.post({
 | 
			
		||||
            url: "/custom/fundTechProject?" + window.authz + "&guildId=" + window.guildId,
 | 
			
		||||
            contentType: "application/json",
 | 
			
		||||
            data: JSON.stringify([
 | 
			
		||||
                {
 | 
			
		||||
                    ItemType: uniqueName
 | 
			
		||||
                }
 | 
			
		||||
            ])
 | 
			
		||||
        });
 | 
			
		||||
        req.done(() => {
 | 
			
		||||
            updateInventory();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function dispatchAddVaultDecoRecipesBatch(requests) {
 | 
			
		||||
    return new Promise(resolve => {
 | 
			
		||||
        revalidateAuthz().then(() => {
 | 
			
		||||
            const req = $.post({
 | 
			
		||||
                url: "/custom/addVaultDecoRecipe?" + window.authz + "&guildId=" + window.guildId,
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                data: JSON.stringify(requests)
 | 
			
		||||
            });
 | 
			
		||||
            req.done(() => {
 | 
			
		||||
                updateInventory();
 | 
			
		||||
                resolve();
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addMissingVaultDecoRecipes() {
 | 
			
		||||
    const requests = [];
 | 
			
		||||
 | 
			
		||||
    document.querySelectorAll("#datalist-VaultDecoRecipes" + " option").forEach(elm => {
 | 
			
		||||
        if (!document.querySelector("#VaultDecoRecipes-list [data-item-type='" + elm.getAttribute("data-key") + "']")) {
 | 
			
		||||
            requests.push({ ItemType: elm.getAttribute("data-key"), ItemCount: 1 });
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        requests.length != 0 &&
 | 
			
		||||
        window.confirm(loc("code_addDecoRecipesConfirm").split("|COUNT|").join(requests.length))
 | 
			
		||||
    ) {
 | 
			
		||||
        return dispatchAddVaultDecoRecipesBatch(requests);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function dispatchAddTechProjectsBatch(requests) {
 | 
			
		||||
    return new Promise(resolve => {
 | 
			
		||||
        revalidateAuthz().then(() => {
 | 
			
		||||
            const req = $.post({
 | 
			
		||||
                url: "/custom/addTechProject?" + window.authz + "&guildId=" + window.guildId,
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                data: JSON.stringify(requests)
 | 
			
		||||
            });
 | 
			
		||||
            req.done(() => {
 | 
			
		||||
                updateInventory();
 | 
			
		||||
                resolve();
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addMissingTechProjects() {
 | 
			
		||||
    const requests = [];
 | 
			
		||||
 | 
			
		||||
    document.querySelectorAll("#datalist-TechProjects option").forEach(elm => {
 | 
			
		||||
        if (!document.querySelector("#TechProjects-list [data-item-type='" + elm.getAttribute("data-key") + "']")) {
 | 
			
		||||
            requests.push({ ItemType: elm.getAttribute("data-key") });
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        requests.length != 0 &&
 | 
			
		||||
        window.confirm(loc("code_addTechProjectsConfirm").split("|COUNT|").join(requests.length))
 | 
			
		||||
    ) {
 | 
			
		||||
        return dispatchAddTechProjectsBatch(requests);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function dispatchFundTechProjectsBatch(requests) {
 | 
			
		||||
    return new Promise(resolve => {
 | 
			
		||||
        revalidateAuthz().then(() => {
 | 
			
		||||
            const req = $.post({
 | 
			
		||||
                url: "/custom/fundTechProject?" + window.authz + "&guildId=" + window.guildId,
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                data: JSON.stringify(requests)
 | 
			
		||||
            });
 | 
			
		||||
            req.done(() => {
 | 
			
		||||
                updateInventory();
 | 
			
		||||
                resolve();
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function fundAllTechProjects() {
 | 
			
		||||
    revalidateAuthz().then(() => {
 | 
			
		||||
        const req = $.get("/custom/getGuild?guildId=" + window.guildId);
 | 
			
		||||
        req.done(data => {
 | 
			
		||||
            const requests = [];
 | 
			
		||||
            data.TechProjects ??= [];
 | 
			
		||||
            data.TechProjects.forEach(techProject => {
 | 
			
		||||
                if (techProject.State != 1) {
 | 
			
		||||
                    requests.push({
 | 
			
		||||
                        ItemType: techProject.ItemType
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (Object.keys(requests).length > 0) {
 | 
			
		||||
                return dispatchFundTechProjectsBatch(requests);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function dispatchCompleteTechProjectsBatch(requests) {
 | 
			
		||||
    return new Promise(resolve => {
 | 
			
		||||
        revalidateAuthz().then(() => {
 | 
			
		||||
            const req = $.post({
 | 
			
		||||
                url: "/custom/completeTechProject?" + window.authz + "&guildId=" + window.guildId,
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                data: JSON.stringify(requests)
 | 
			
		||||
            });
 | 
			
		||||
            req.done(() => {
 | 
			
		||||
                updateInventory();
 | 
			
		||||
                resolve();
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function completeAllTechProjects() {
 | 
			
		||||
    revalidateAuthz().then(() => {
 | 
			
		||||
        const req = $.get("/custom/getGuild?guildId=" + window.guildId);
 | 
			
		||||
        req.done(data => {
 | 
			
		||||
            const requests = [];
 | 
			
		||||
            data.TechProjects ??= [];
 | 
			
		||||
            data.TechProjects.forEach(techProject => {
 | 
			
		||||
                if (techProject.State == 1 && new Date(techProject.CompletionDate) > new Date()) {
 | 
			
		||||
                    requests.push({
 | 
			
		||||
                        ItemType: techProject.ItemType
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (Object.keys(requests).length > 0) {
 | 
			
		||||
                return dispatchCompleteTechProjectsBatch(requests);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function addMissingHelminthRecipes() {
 | 
			
		||||
    await revalidateAuthz();
 | 
			
		||||
    await fetch("/custom/addMissingHelminthBlueprints?" + window.authz);
 | 
			
		||||
@ -1917,7 +2597,7 @@ function disposeOfGear(category, oid) {
 | 
			
		||||
        ];
 | 
			
		||||
        revalidateAuthz().then(() => {
 | 
			
		||||
            $.post({
 | 
			
		||||
                url: "/api/sell.php?" + window.authz + "&wsid=" + wsid,
 | 
			
		||||
                url: "/api/sell.php?" + window.authz,
 | 
			
		||||
                contentType: "text/plain",
 | 
			
		||||
                data: JSON.stringify(data)
 | 
			
		||||
            });
 | 
			
		||||
@ -1939,7 +2619,7 @@ function disposeOfItems(category, type, count) {
 | 
			
		||||
    ];
 | 
			
		||||
    revalidateAuthz().then(() => {
 | 
			
		||||
        $.post({
 | 
			
		||||
            url: "/api/sell.php?" + window.authz + "&wsid=" + wsid,
 | 
			
		||||
            url: "/api/sell.php?" + window.authz,
 | 
			
		||||
            contentType: "text/plain",
 | 
			
		||||
            data: JSON.stringify(data)
 | 
			
		||||
        });
 | 
			
		||||
@ -2193,7 +2873,7 @@ for (const id of uiConfigs) {
 | 
			
		||||
                value = parseInt(value);
 | 
			
		||||
            }
 | 
			
		||||
            $.post({
 | 
			
		||||
                url: "/custom/setConfig?" + window.authz + "&wsid=" + wsid,
 | 
			
		||||
                url: "/custom/setConfig?" + window.authz,
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                data: JSON.stringify({ [id]: value })
 | 
			
		||||
            });
 | 
			
		||||
@ -2201,13 +2881,9 @@ for (const id of uiConfigs) {
 | 
			
		||||
    } else if (elm.type == "checkbox") {
 | 
			
		||||
        elm.onchange = function () {
 | 
			
		||||
            $.post({
 | 
			
		||||
                url: "/custom/setConfig?" + window.authz + "&wsid=" + wsid,
 | 
			
		||||
                url: "/custom/setConfig?" + window.authz,
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                data: JSON.stringify({ [id]: this.checked })
 | 
			
		||||
            }).then(() => {
 | 
			
		||||
                if (["infiniteCredits", "infinitePlatinum", "infiniteEndo", "infiniteRegalAya"].indexOf(id) != -1) {
 | 
			
		||||
                    updateInventory();
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
@ -2228,7 +2904,7 @@ document.querySelectorAll(".config-form .input-group").forEach(grp => {
 | 
			
		||||
 | 
			
		||||
function doSaveConfigInt(id) {
 | 
			
		||||
    $.post({
 | 
			
		||||
        url: "/custom/setConfig?" + window.authz + "&wsid=" + wsid,
 | 
			
		||||
        url: "/custom/setConfig?" + window.authz,
 | 
			
		||||
        contentType: "application/json",
 | 
			
		||||
        data: JSON.stringify({
 | 
			
		||||
            [id]: parseInt(document.getElementById(id).value)
 | 
			
		||||
@ -2238,7 +2914,7 @@ function doSaveConfigInt(id) {
 | 
			
		||||
 | 
			
		||||
function doSaveConfigFloat(id) {
 | 
			
		||||
    $.post({
 | 
			
		||||
        url: "/custom/setConfig?" + window.authz + "&wsid=" + wsid,
 | 
			
		||||
        url: "/custom/setConfig?" + window.authz,
 | 
			
		||||
        contentType: "application/json",
 | 
			
		||||
        data: JSON.stringify({
 | 
			
		||||
            [id]: parseFloat(document.getElementById(id).value)
 | 
			
		||||
@ -2248,7 +2924,7 @@ function doSaveConfigFloat(id) {
 | 
			
		||||
 | 
			
		||||
function doSaveConfigStringArray(id) {
 | 
			
		||||
    $.post({
 | 
			
		||||
        url: "/custom/setConfig?" + window.authz + "&wsid=" + wsid,
 | 
			
		||||
        url: "/custom/setConfig?" + window.authz,
 | 
			
		||||
        contentType: "application/json",
 | 
			
		||||
        data: JSON.stringify({
 | 
			
		||||
            [id]: document
 | 
			
		||||
@ -2332,6 +3008,9 @@ function doUnlockAllFocusSchools() {
 | 
			
		||||
                toast(loc("code_focusAllUnlocked"));
 | 
			
		||||
            } else {
 | 
			
		||||
                toast(loc("code_focusUnlocked").split("|COUNT|").join(Object.keys(missingFocusUpgrades).length));
 | 
			
		||||
                if (ws_is_open) {
 | 
			
		||||
                    window.ws.send(JSON.stringify({ sync_inventory: true }));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
@ -2376,7 +3055,22 @@ document.querySelectorAll("#account-cheats input[type=checkbox], #account-cheats
 | 
			
		||||
        revalidateAuthz().then(() => {
 | 
			
		||||
            const value = elm.type === "checkbox" ? elm.checked : elm.value;
 | 
			
		||||
            $.post({
 | 
			
		||||
                url: "/custom/setAccountCheat?" + window.authz /*+ "&wsid=" + wsid*/,
 | 
			
		||||
                url: "/custom/setAccountCheat?" + window.authz,
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                data: JSON.stringify({
 | 
			
		||||
                    key: elm.id,
 | 
			
		||||
                    value: elm.checked
 | 
			
		||||
                })
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
document.querySelectorAll("#guild-cheats input[type=checkbox]").forEach(elm => {
 | 
			
		||||
    elm.onchange = function () {
 | 
			
		||||
        revalidateAuthz().then(() => {
 | 
			
		||||
            $.post({
 | 
			
		||||
                url: "/custom/setGuildCheat?" + window.authz + "&guildId=" + window.guildId,
 | 
			
		||||
                contentType: "application/json",
 | 
			
		||||
                data: JSON.stringify({
 | 
			
		||||
                    key: elm.id,
 | 
			
		||||
@ -2433,7 +3127,7 @@ function doRemoveUnrankedMods() {
 | 
			
		||||
        req.done(inventory => {
 | 
			
		||||
            window.itemListPromise.then(itemMap => {
 | 
			
		||||
                $.post({
 | 
			
		||||
                    url: "/api/sell.php?" + window.authz + "&wsid=" + wsid,
 | 
			
		||||
                    url: "/api/sell.php?" + window.authz,
 | 
			
		||||
                    contentType: "text/plain",
 | 
			
		||||
                    data: JSON.stringify({
 | 
			
		||||
                        SellCurrency: "SC_RegularCredits",
 | 
			
		||||
@ -2477,6 +3171,32 @@ single.getRoute("#detailedView-route").on("beforeload", function () {
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
single.getRoute("#guild-route").on("beforeload", function () {
 | 
			
		||||
    document.getElementById("guildView-loading").classList.remove("d-none");
 | 
			
		||||
    document.getElementById("guildView-title").textContent = "";
 | 
			
		||||
    document.getElementById("guildView-tier").textContent = "";
 | 
			
		||||
    document.getElementById("guildView-class").textContent = "";
 | 
			
		||||
    document.getElementById("vaultRegularCredits-form").classList.add("d-none");
 | 
			
		||||
    document.getElementById("vaultPremiumCredits-form").classList.add("d-none");
 | 
			
		||||
    document.getElementById("VaultRegularCredits-owned").classList.add("mb-0");
 | 
			
		||||
    document.getElementById("VaultPremiumCredits-owned").classList.add("mb-0");
 | 
			
		||||
    document.getElementById("TechProjects-list").innerHTML = "";
 | 
			
		||||
    document.getElementById("techProjects-form").classList.add("d-none");
 | 
			
		||||
    document.getElementById("acquire-type-TechProjects").value = "";
 | 
			
		||||
    document.getElementById("VaultDecoRecipes-list").innerHTML = "";
 | 
			
		||||
    document.getElementById("vaultDecoRecipes-form").classList.add("d-none");
 | 
			
		||||
    document.getElementById("acquire-type-VaultDecoRecipes").value = "";
 | 
			
		||||
    document.getElementById("Alliance-list").innerHTML = "";
 | 
			
		||||
    document.getElementById("guildView-alliance").textContent = "";
 | 
			
		||||
    document.getElementById("Members-list").innerHTML = "";
 | 
			
		||||
    document.querySelectorAll("#guild-actions button").forEach(btn => {
 | 
			
		||||
        btn.disabled = true;
 | 
			
		||||
    });
 | 
			
		||||
    if (window.didInitialInventoryUpdate) {
 | 
			
		||||
        updateInventory();
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function doPushArchonCrystalUpgrade() {
 | 
			
		||||
    const urlParams = new URLSearchParams(window.location.search);
 | 
			
		||||
    const uniqueName = getKey(document.querySelector("[list='datalist-archonCrystalUpgrades']"));
 | 
			
		||||
@ -2540,7 +3260,7 @@ function doChangeSupportedSyndicate() {
 | 
			
		||||
function doAddCurrency(currency) {
 | 
			
		||||
    revalidateAuthz().then(() => {
 | 
			
		||||
        $.post({
 | 
			
		||||
            url: "/custom/addCurrency?" + window.authz,
 | 
			
		||||
            url: "/custom/addCurrency?" + window.authz + "&guildId=" + window.guildId,
 | 
			
		||||
            contentType: "application/json",
 | 
			
		||||
            data: JSON.stringify({
 | 
			
		||||
                currency,
 | 
			
		||||
@ -2552,11 +3272,11 @@ function doAddCurrency(currency) {
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function readdQuestKey(itemMap, itemType) {
 | 
			
		||||
function reAddToItemList(itemMap, datalist, itemType) {
 | 
			
		||||
    const option = document.createElement("option");
 | 
			
		||||
    option.setAttribute("data-key", itemType);
 | 
			
		||||
    option.value = itemMap[itemType]?.name ?? itemType;
 | 
			
		||||
    document.getElementById("datalist-QuestKeys").appendChild(option);
 | 
			
		||||
    document.getElementById("datalist-" + datalist).appendChild(option);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function doQuestUpdate(operation, itemType) {
 | 
			
		||||
@ -2764,13 +3484,13 @@ async function doUnlockAllScans() {
 | 
			
		||||
async function doUnlockAllShipFeatures() {
 | 
			
		||||
    await revalidateAuthz();
 | 
			
		||||
    await fetch("/custom/unlockAllShipFeatures?" + window.authz);
 | 
			
		||||
    toast(loc("cheats_unlockSuccInventory"));
 | 
			
		||||
    toast(loc(window.have_game_ws ? "code_succAdded" : "cheats_unlockSuccInventory"));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function doUnlockAllCapturaScenes() {
 | 
			
		||||
    await revalidateAuthz();
 | 
			
		||||
    await fetch("/custom/unlockAllCapturaScenes?" + window.authz);
 | 
			
		||||
    toast(loc("cheats_unlockSuccInventory"));
 | 
			
		||||
    toast(loc(window.have_game_ws ? "code_succAdded" : "cheats_unlockSuccInventory"));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function unlockAllMissions() {
 | 
			
		||||
@ -2782,13 +3502,13 @@ async function unlockAllMissions() {
 | 
			
		||||
async function unlockAllProfitTakerStages() {
 | 
			
		||||
    await revalidateAuthz();
 | 
			
		||||
    await fetch("/custom/unlockAllProfitTakerStages?" + window.authz);
 | 
			
		||||
    toast(loc("cheats_unlockSuccInventory"));
 | 
			
		||||
    toast(loc(window.have_game_ws ? "code_succAdded" : "cheats_unlockSuccInventory"));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function unlockAllSimarisResearchEntries() {
 | 
			
		||||
    await revalidateAuthz();
 | 
			
		||||
    await fetch("/custom/unlockAllSimarisResearchEntries?" + window.authz);
 | 
			
		||||
    toast(loc("cheats_unlockSuccInventory"));
 | 
			
		||||
    toast(loc(window.have_game_ws ? "code_succAdded" : "cheats_unlockSuccInventory"));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const importSamples = {
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
// German translation by Animan8000
 | 
			
		||||
dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `Hinweis: Um Änderungen im Spiel zu sehen, musst du dein Inventar neu synchronisieren, z. B. mit dem /sync Befehl des Bootstrappers, durch Besuch eines Dojo/Relais oder durch erneutes Einloggen.`,
 | 
			
		||||
    general_inventoryUpdateNoteGameWs: `[UNTRANSLATED] Note: You may need to reopen any menu you are on for changes to be reflected.`,
 | 
			
		||||
    general_addButton: `Hinzufügen`,
 | 
			
		||||
    general_setButton: `Festlegen`,
 | 
			
		||||
    general_none: `Keines`,
 | 
			
		||||
@ -31,6 +32,8 @@ dict = {
 | 
			
		||||
    code_renamePrompt: `Neuen benutzerdefinierten Namen eingeben:`,
 | 
			
		||||
    code_remove: `Entfernen`,
 | 
			
		||||
    code_addItemsConfirm: `Bist du sicher, dass du |COUNT| Gegenstände zu deinem Account hinzufügen möchtest?`,
 | 
			
		||||
    code_addTechProjectsConfirm: `[UNTRANSLATED] Are you sure you want to add |COUNT| research to your clan?`,
 | 
			
		||||
    code_addDecoRecipesConfirm: `[UNTRANSLATED] Are you sure you want to add |COUNT| deco recipes to your clan?`,
 | 
			
		||||
    code_succRankUp: `Erfolgreich aufgestiegen.`,
 | 
			
		||||
    code_noEquipmentToRankUp: `Keine Ausstattung zum Rangaufstieg verfügbar.`,
 | 
			
		||||
    code_succAdded: `Erfolgreich hinzugefügt.`,
 | 
			
		||||
@ -63,6 +66,8 @@ dict = {
 | 
			
		||||
    code_pigment: `Pigment`,
 | 
			
		||||
    code_mature: `Für den Kampf auswachsen lassen`,
 | 
			
		||||
    code_unmature: `Genetisches Altern zurücksetzen`,
 | 
			
		||||
    code_fund: `[UNTRANSLATED] Fund`,
 | 
			
		||||
    code_funded: `[UNTRANSLATED] Funded`,
 | 
			
		||||
    code_succChange: `Erfolgreich geändert.`,
 | 
			
		||||
    code_requiredInvigorationUpgrade: `Du musst sowohl ein offensives & defensives Upgrade auswählen.`,
 | 
			
		||||
    login_description: `Melde dich mit deinem OpenWF-Account an (denselben Angaben wie im Spiel, wenn du dich mit diesem Server verbindest).`,
 | 
			
		||||
@ -74,6 +79,7 @@ dict = {
 | 
			
		||||
    navbar_renameAccount: `Account umbenennen`,
 | 
			
		||||
    navbar_deleteAccount: `Account löschen`,
 | 
			
		||||
    navbar_inventory: `Inventar`,
 | 
			
		||||
    navbar_guildView: `Clan`,
 | 
			
		||||
    navbar_mods: `Mods`,
 | 
			
		||||
    navbar_quests: `Quests`,
 | 
			
		||||
    navbar_cheats: `Cheats`,
 | 
			
		||||
@ -196,7 +202,6 @@ dict = {
 | 
			
		||||
    cheats_unlockAllFlavourItems: `Alle <abbr title="Animationssets, Glyphen, Farbpaletten usw.">Sammlerstücke</abbr> freischalten`,
 | 
			
		||||
    cheats_unlockAllSkins: `Alle Skins freischalten`,
 | 
			
		||||
    cheats_unlockAllCapturaScenes: `Alle Photora-Szenen freischalten`,
 | 
			
		||||
    cheats_unlockAllDecoRecipes: `Alle Dojo-Deko-Baupläne freischalten`,
 | 
			
		||||
    cheats_universalPolarityEverywhere: `Universelle Polarität überall`,
 | 
			
		||||
    cheats_unlockDoubleCapacityPotatoesEverywhere: `Orokin Reaktor & Beschleuniger überall`,
 | 
			
		||||
    cheats_unlockExilusEverywhere: `Exilus-Adapter überall`,
 | 
			
		||||
@ -405,5 +410,35 @@ dict = {
 | 
			
		||||
    theme_dark: `Dunkles Design`,
 | 
			
		||||
    theme_light: `Helles Design`,
 | 
			
		||||
 | 
			
		||||
    guildView_cheats: `[UNTRANSLATED] Clan Cheats`,
 | 
			
		||||
    guildView_techProjects: `Forschung`,
 | 
			
		||||
    guildView_vaultDecoRecipes: `[UNTRANSLATED] Dojo Deco Recipes`,
 | 
			
		||||
    guildView_alliance: `Allianz`,
 | 
			
		||||
    guildView_members: `Mitglieder`,
 | 
			
		||||
    guildView_pending: `Ausstehend`,
 | 
			
		||||
    guildView_classDisplay: `Rang |CLASS|`,
 | 
			
		||||
    guildView_tierDisplay: `|TIER| Clan`,
 | 
			
		||||
    guildView_tier1: `Geist`,
 | 
			
		||||
    guildView_tier2: `Schatten`,
 | 
			
		||||
    guildView_tier3: `Sturm`,
 | 
			
		||||
    guildView_tier4: `Berg`,
 | 
			
		||||
    guildView_tier5: `Mond`,
 | 
			
		||||
    guildView_rank_creator: `Gründer Kriegsherr`,
 | 
			
		||||
    guildView_rank_general: `General`,
 | 
			
		||||
    guildView_rank_initiate: `Initiant`,
 | 
			
		||||
    guildView_rank_leader: `Anführer`,
 | 
			
		||||
    guildView_rank_officer: `Offizier`,
 | 
			
		||||
    guildView_rank_sage: `Weiser`,
 | 
			
		||||
    guildView_rank_soldier: `Soldat`,
 | 
			
		||||
    guildView_rank_utility: `Versorger`,
 | 
			
		||||
    guildView_rank_warlord: `Kriegsherr`,
 | 
			
		||||
    guildView_currency_owned: `[UNTRANSLATED] |COUNT| in Vault.`,
 | 
			
		||||
    guildView_bulkAddTechProjects: `[UNTRANSLATED] Add Missing Research`,
 | 
			
		||||
    guildView_bulkAddVaultDecoRecipes: `[UNTRANSLATED] Add Missing Dojo Deco Recipes`,
 | 
			
		||||
    guildView_bulkFundTechProjects: `[UNTRANSLATED] Fund All Research`,
 | 
			
		||||
    guildView_bulkCompleteTechProjects: `[UNTRANSLATED] Complete All Research`,
 | 
			
		||||
    guildView_promote: `Befördern`,
 | 
			
		||||
    guildView_demote: `Degradieren`,
 | 
			
		||||
 | 
			
		||||
    prettier_sucks_ass: ``
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `Note: To see changes in-game, you need to resync your inventory, e.g. using the bootstrapper's /sync command, visiting a dojo/relay, or relogging.`,
 | 
			
		||||
    general_inventoryUpdateNoteGameWs: `Note: You may need to reopen any menu you are on for changes to be reflected.`,
 | 
			
		||||
    general_addButton: `Add`,
 | 
			
		||||
    general_setButton: `Set`,
 | 
			
		||||
    general_none: `None`,
 | 
			
		||||
@ -30,6 +31,8 @@ dict = {
 | 
			
		||||
    code_renamePrompt: `Enter new custom name:`,
 | 
			
		||||
    code_remove: `Remove`,
 | 
			
		||||
    code_addItemsConfirm: `Are you sure you want to add |COUNT| items to your account?`,
 | 
			
		||||
    code_addTechProjectsConfirm: `Are you sure you want to add |COUNT| research to your clan?`,
 | 
			
		||||
    code_addDecoRecipesConfirm: `Are you sure you want to add |COUNT| deco recipes to your clan?`,
 | 
			
		||||
    code_succRankUp: `Successfully ranked up.`,
 | 
			
		||||
    code_noEquipmentToRankUp: `No equipment to rank up.`,
 | 
			
		||||
    code_succAdded: `Successfully added.`,
 | 
			
		||||
@ -62,6 +65,8 @@ dict = {
 | 
			
		||||
    code_pigment: `Pigment`,
 | 
			
		||||
    code_mature: `Mature for combat`,
 | 
			
		||||
    code_unmature: `Regress genetic aging`,
 | 
			
		||||
    code_fund: `Fund`,
 | 
			
		||||
    code_funded: `Funded`,
 | 
			
		||||
    code_succChange: `Successfully changed.`,
 | 
			
		||||
    code_requiredInvigorationUpgrade: `You must select both an offensive & defensive upgrade.`,
 | 
			
		||||
    login_description: `Login using your OpenWF account credentials (same as in-game when connecting to this server).`,
 | 
			
		||||
@ -73,6 +78,7 @@ dict = {
 | 
			
		||||
    navbar_renameAccount: `Rename Account`,
 | 
			
		||||
    navbar_deleteAccount: `Delete Account`,
 | 
			
		||||
    navbar_inventory: `Inventory`,
 | 
			
		||||
    navbar_guildView: `Clan`,
 | 
			
		||||
    navbar_mods: `Mods`,
 | 
			
		||||
    navbar_quests: `Quests`,
 | 
			
		||||
    navbar_cheats: `Cheats`,
 | 
			
		||||
@ -195,7 +201,6 @@ dict = {
 | 
			
		||||
    cheats_unlockAllFlavourItems: `Unlock All <abbr title="Animation Sets, Glyphs, Palettes, etc.">Flavor Items</abbr>`,
 | 
			
		||||
    cheats_unlockAllSkins: `Unlock All Skins`,
 | 
			
		||||
    cheats_unlockAllCapturaScenes: `Unlock All Captura Scenes`,
 | 
			
		||||
    cheats_unlockAllDecoRecipes: `Unlock All Dojo Deco Recipes`,
 | 
			
		||||
    cheats_universalPolarityEverywhere: `Universal Polarity Everywhere`,
 | 
			
		||||
    cheats_unlockDoubleCapacityPotatoesEverywhere: `Potatoes Everywhere`,
 | 
			
		||||
    cheats_unlockExilusEverywhere: `Exilus Adapters Everywhere`,
 | 
			
		||||
@ -404,5 +409,35 @@ dict = {
 | 
			
		||||
    theme_dark: `Dark Theme`,
 | 
			
		||||
    theme_light: `Light Theme`,
 | 
			
		||||
 | 
			
		||||
    guildView_cheats: `Clan Cheats`,
 | 
			
		||||
    guildView_techProjects: `Research`,
 | 
			
		||||
    guildView_vaultDecoRecipes: `Dojo Deco Recipes`,
 | 
			
		||||
    guildView_alliance: `Alliance`,
 | 
			
		||||
    guildView_members: `Members`,
 | 
			
		||||
    guildView_pending: `Pending`,
 | 
			
		||||
    guildView_classDisplay: `Rank |CLASS|`,
 | 
			
		||||
    guildView_tierDisplay: `|TIER| Clan`,
 | 
			
		||||
    guildView_tier1: `Ghost`,
 | 
			
		||||
    guildView_tier2: `Shadow`,
 | 
			
		||||
    guildView_tier3: `Storm`,
 | 
			
		||||
    guildView_tier4: `Mountain`,
 | 
			
		||||
    guildView_tier5: `Moon`,
 | 
			
		||||
    guildView_rank_creator: `Founding Warlord`,
 | 
			
		||||
    guildView_rank_general: `General`,
 | 
			
		||||
    guildView_rank_initiate: `Initiate`,
 | 
			
		||||
    guildView_rank_leader: `Leader`,
 | 
			
		||||
    guildView_rank_officer: `Officer`,
 | 
			
		||||
    guildView_rank_sage: `Sage`,
 | 
			
		||||
    guildView_rank_soldier: `Soldier`,
 | 
			
		||||
    guildView_rank_utility: `Utility`,
 | 
			
		||||
    guildView_rank_warlord: `Warlord`,
 | 
			
		||||
    guildView_currency_owned: `|COUNT| in Vault.`,
 | 
			
		||||
    guildView_bulkAddTechProjects: `Add Missing Research`,
 | 
			
		||||
    guildView_bulkAddVaultDecoRecipes: `Add Missing Dojo Deco Recipes`,
 | 
			
		||||
    guildView_bulkFundTechProjects: `Fund All Research`,
 | 
			
		||||
    guildView_bulkCompleteTechProjects: `Complete All Research`,
 | 
			
		||||
    guildView_promote: `Promote`,
 | 
			
		||||
    guildView_demote: `Demote`,
 | 
			
		||||
 | 
			
		||||
    prettier_sucks_ass: ``
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
// Spanish translation by hxedcl
 | 
			
		||||
dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `Para ver los cambios en el juego, necesitas volver a sincronizar tu inventario, por ejemplo, usando el comando /sync del bootstrapper, visitando un dojo o repetidor, o volviendo a iniciar sesión.`,
 | 
			
		||||
    general_inventoryUpdateNoteGameWs: `[UNTRANSLATED] Note: You may need to reopen any menu you are on for changes to be reflected.`,
 | 
			
		||||
    general_addButton: `Agregar`,
 | 
			
		||||
    general_setButton: `Establecer`,
 | 
			
		||||
    general_none: `Ninguno`,
 | 
			
		||||
@ -31,6 +32,8 @@ dict = {
 | 
			
		||||
    code_renamePrompt: `Escribe tu nuevo nombre personalizado:`,
 | 
			
		||||
    code_remove: `Quitar`,
 | 
			
		||||
    code_addItemsConfirm: `¿Estás seguro de que deseas agregar |COUNT| objetos a tu cuenta?`,
 | 
			
		||||
    code_addTechProjectsConfirm: `[UNTRANSLATED] Are you sure you want to add |COUNT| research to your clan?`,
 | 
			
		||||
    code_addDecoRecipesConfirm: `[UNTRANSLATED] Are you sure you want to add |COUNT| deco recipes to your clan?`,
 | 
			
		||||
    code_succRankUp: `Ascenso exitoso.`,
 | 
			
		||||
    code_noEquipmentToRankUp: `No hay equipo para ascender.`,
 | 
			
		||||
    code_succAdded: `Agregado exitosamente.`,
 | 
			
		||||
@ -63,6 +66,8 @@ dict = {
 | 
			
		||||
    code_pigment: `Pigmento`,
 | 
			
		||||
    code_mature: `Listo para el combate`,
 | 
			
		||||
    code_unmature: `Regresar el envejecimiento genético`,
 | 
			
		||||
    code_fund: `[UNTRANSLATED] Fund`,
 | 
			
		||||
    code_funded: `[UNTRANSLATED] Funded`,
 | 
			
		||||
    code_succChange: `Cambiado correctamente`,
 | 
			
		||||
    code_requiredInvigorationUpgrade: `Debes seleccionar una mejora ofensiva y una defensiva.`,
 | 
			
		||||
    login_description: `Inicia sesión con las credenciales de tu cuenta OpenWF (las mismas que usas en el juego al conectarte a este servidor).`,
 | 
			
		||||
@ -74,6 +79,7 @@ dict = {
 | 
			
		||||
    navbar_renameAccount: `Renombrar cuenta`,
 | 
			
		||||
    navbar_deleteAccount: `Eliminar cuenta`,
 | 
			
		||||
    navbar_inventory: `Inventario`,
 | 
			
		||||
    navbar_guildView: `Clan`,
 | 
			
		||||
    navbar_mods: `Mods`,
 | 
			
		||||
    navbar_quests: `Misiones`,
 | 
			
		||||
    navbar_cheats: `Trucos`,
 | 
			
		||||
@ -196,7 +202,6 @@ dict = {
 | 
			
		||||
    cheats_unlockAllFlavourItems: `Desbloquear todos los <abbr title="Conjuntos de animaciones, glifos, paletas, etc.">ítems estéticos</abbr>`,
 | 
			
		||||
    cheats_unlockAllSkins: `Desbloquear todas las skins`,
 | 
			
		||||
    cheats_unlockAllCapturaScenes: `Desbloquear todas las escenas de Captura`,
 | 
			
		||||
    cheats_unlockAllDecoRecipes: `Desbloquear todas las recetas decorativas del dojo`,
 | 
			
		||||
    cheats_universalPolarityEverywhere: `Polaridad universal en todas partes`,
 | 
			
		||||
    cheats_unlockDoubleCapacityPotatoesEverywhere: `Patatas en todas partes`,
 | 
			
		||||
    cheats_unlockExilusEverywhere: `Adaptadores Exilus en todas partes`,
 | 
			
		||||
@ -405,5 +410,35 @@ dict = {
 | 
			
		||||
    theme_dark: `Tema Oscuro`,
 | 
			
		||||
    theme_light: `Tema Claro`,
 | 
			
		||||
 | 
			
		||||
    guildView_cheats: `[UNTRANSLATED] Clan Cheats`,
 | 
			
		||||
    guildView_techProjects: `Investigación`,
 | 
			
		||||
    guildView_vaultDecoRecipes: `[UNTRANSLATED] Dojo Deco Recipes`,
 | 
			
		||||
    guildView_alliance: `Alianza`,
 | 
			
		||||
    guildView_members: `Miembros`,
 | 
			
		||||
    guildView_pending: `Pendiente`,
 | 
			
		||||
    guildView_classDisplay: `Rango |CLASS|`,
 | 
			
		||||
    guildView_tierDisplay: `Clan |TIER|`,
 | 
			
		||||
    guildView_tier1: `Fantasma`,
 | 
			
		||||
    guildView_tier2: `Sombra`,
 | 
			
		||||
    guildView_tier3: `Tormenta`,
 | 
			
		||||
    guildView_tier4: `Montaña`,
 | 
			
		||||
    guildView_tier5: `Luna`,
 | 
			
		||||
    guildView_rank_creator: `Señor de la guerra fundador`,
 | 
			
		||||
    guildView_rank_general: `General`,
 | 
			
		||||
    guildView_rank_initiate: `Iniciado`,
 | 
			
		||||
    guildView_rank_leader: `Líder`,
 | 
			
		||||
    guildView_rank_officer: `Oficial`,
 | 
			
		||||
    guildView_rank_sage: `Sabio`,
 | 
			
		||||
    guildView_rank_soldier: `Soldado`,
 | 
			
		||||
    guildView_rank_utility: `Utilitario`,
 | 
			
		||||
    guildView_rank_warlord: `Señor de la guerra`,
 | 
			
		||||
    guildView_currency_owned: `[UNTRANSLATED] |COUNT| in Vault.`,
 | 
			
		||||
    guildView_bulkAddTechProjects: `[UNTRANSLATED] Add Missing Research`,
 | 
			
		||||
    guildView_bulkAddVaultDecoRecipes: `[UNTRANSLATED] Add Missing Dojo Deco Recipes`,
 | 
			
		||||
    guildView_bulkFundTechProjects: `[UNTRANSLATED] Fund All Research`,
 | 
			
		||||
    guildView_bulkCompleteTechProjects: `[UNTRANSLATED] Complete All Research`,
 | 
			
		||||
    guildView_promote: `Promover`,
 | 
			
		||||
    guildView_demote: `Degradar`,
 | 
			
		||||
 | 
			
		||||
    prettier_sucks_ass: ``
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
// French translation by Vitruvio
 | 
			
		||||
dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `Note : Pour voir les changements en jeu, l'inventaire doit être actualisé. Cela se fait en tapant /sync dans le tchat, en visitant un dojo/relais ou en se reconnectant.`,
 | 
			
		||||
    general_inventoryUpdateNoteGameWs: `[UNTRANSLATED] Note: You may need to reopen any menu you are on for changes to be reflected.`,
 | 
			
		||||
    general_addButton: `Ajouter`,
 | 
			
		||||
    general_setButton: `Définir`,
 | 
			
		||||
    general_none: `Aucun`,
 | 
			
		||||
@ -31,6 +32,8 @@ dict = {
 | 
			
		||||
    code_renamePrompt: `Nouveau nom :`,
 | 
			
		||||
    code_remove: `Retirer`,
 | 
			
		||||
    code_addItemsConfirm: `Ajouter |COUNT| items à l'inventaire ?`,
 | 
			
		||||
    code_addTechProjectsConfirm: `[UNTRANSLATED] Are you sure you want to add |COUNT| research to your clan?`,
 | 
			
		||||
    code_addDecoRecipesConfirm: `[UNTRANSLATED] Are you sure you want to add |COUNT| deco recipes to your clan?`,
 | 
			
		||||
    code_succRankUp: `Montée de niveau effectuée.`,
 | 
			
		||||
    code_noEquipmentToRankUp: `Aucun équipement à monter de niveau.`,
 | 
			
		||||
    code_succAdded: `Ajouté.`,
 | 
			
		||||
@ -63,6 +66,8 @@ dict = {
 | 
			
		||||
    code_pigment: `Pigment`,
 | 
			
		||||
    code_mature: `Maturer pour le combat`,
 | 
			
		||||
    code_unmature: `Régrésser l'âge génétique`,
 | 
			
		||||
    code_fund: `[UNTRANSLATED] Fund`,
 | 
			
		||||
    code_funded: `[UNTRANSLATED] Funded`,
 | 
			
		||||
    code_succChange: `Changement effectué.`,
 | 
			
		||||
    code_requiredInvigorationUpgrade: `Augmentation offensive et défensive requises.`,
 | 
			
		||||
    login_description: `Connexion avec les informations de connexion OpenWF.`,
 | 
			
		||||
@ -74,6 +79,7 @@ dict = {
 | 
			
		||||
    navbar_renameAccount: `Renommer le compte`,
 | 
			
		||||
    navbar_deleteAccount: `Supprimer le compte`,
 | 
			
		||||
    navbar_inventory: `Inventaire`,
 | 
			
		||||
    navbar_guildView: `Clan`,
 | 
			
		||||
    navbar_mods: `Mods`,
 | 
			
		||||
    navbar_quests: `Quêtes`,
 | 
			
		||||
    navbar_cheats: `Cheats`,
 | 
			
		||||
@ -196,7 +202,6 @@ dict = {
 | 
			
		||||
    cheats_unlockAllFlavourItems: `Débloquer tous les <abbr title="Animations, Glyphes, Palettes, etc.">Flavor Items</abbr>`,
 | 
			
		||||
    cheats_unlockAllSkins: `Débloquer tous les skins`,
 | 
			
		||||
    cheats_unlockAllCapturaScenes: `Débloquer toutes les scènes captura`,
 | 
			
		||||
    cheats_unlockAllDecoRecipes: `Débloquer toutes les recherches dojo`,
 | 
			
		||||
    cheats_universalPolarityEverywhere: `Polarités universelles partout`,
 | 
			
		||||
    cheats_unlockDoubleCapacityPotatoesEverywhere: `Réacteurs et Catalyseurs partout`,
 | 
			
		||||
    cheats_unlockExilusEverywhere: `Adaptateurs Exilus partout`,
 | 
			
		||||
@ -405,5 +410,35 @@ dict = {
 | 
			
		||||
    theme_dark: `Thème sombre`,
 | 
			
		||||
    theme_light: `Thème clair`,
 | 
			
		||||
 | 
			
		||||
    guildView_cheats: `[UNTRANSLATED] Clan Cheats`,
 | 
			
		||||
    guildView_techProjects: `Recherche`,
 | 
			
		||||
    guildView_vaultDecoRecipes: `[UNTRANSLATED] Dojo Deco Recipes`,
 | 
			
		||||
    guildView_alliance: `Alliance`,
 | 
			
		||||
    guildView_members: `Members`,
 | 
			
		||||
    guildView_pending: `En Attente`,
 | 
			
		||||
    guildView_classDisplay: `Rang |CLASS|`,
 | 
			
		||||
    guildView_tierDisplay: `Clan |TIER|`,
 | 
			
		||||
    guildView_tier1: `Fantôme`,
 | 
			
		||||
    guildView_tier2: `Ombre`,
 | 
			
		||||
    guildView_tier3: `Tempête`,
 | 
			
		||||
    guildView_tier4: `Montagne`,
 | 
			
		||||
    guildView_tier5: `Lune`,
 | 
			
		||||
    guildView_rank_creator: `Seigneur de Guerre Fondateur`,
 | 
			
		||||
    guildView_rank_general: `Général`,
 | 
			
		||||
    guildView_rank_initiate: `Initié`,
 | 
			
		||||
    guildView_rank_leader: `Chef`,
 | 
			
		||||
    guildView_rank_officer: `Officier`,
 | 
			
		||||
    guildView_rank_sage: `Sage`,
 | 
			
		||||
    guildView_rank_soldier: `Soldat`,
 | 
			
		||||
    guildView_rank_utility: `Utilitaire`,
 | 
			
		||||
    guildView_rank_warlord: `Seigneur de guerre`,
 | 
			
		||||
    guildView_currency_owned: `[UNTRANSLATED] |COUNT| in Vault.`,
 | 
			
		||||
    guildView_bulkAddTechProjects: `[UNTRANSLATED] Add Missing Research`,
 | 
			
		||||
    guildView_bulkAddVaultDecoRecipes: `[UNTRANSLATED] Add Missing Dojo Deco Recipes`,
 | 
			
		||||
    guildView_bulkFundTechProjects: `[UNTRANSLATED] Fund All Research`,
 | 
			
		||||
    guildView_bulkCompleteTechProjects: `[UNTRANSLATED] Complete All Research`,
 | 
			
		||||
    guildView_promote: `Promouvoir`,
 | 
			
		||||
    guildView_demote: `Rétrograder`,
 | 
			
		||||
 | 
			
		||||
    prettier_sucks_ass: ``
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
// Russian translation by AMelonInsideLemon, LoseFace
 | 
			
		||||
dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `Примечание: Чтобы увидеть изменения в игре, вам нужно повторно синхронизировать свой инвентарь, например, используя команду /sync загрузчика, посетив Додзё/Реле или перезагрузив игру.`,
 | 
			
		||||
    general_inventoryUpdateNoteGameWs: `[UNTRANSLATED] Note: You may need to reopen any menu you are on for changes to be reflected.`,
 | 
			
		||||
    general_addButton: `Добавить`,
 | 
			
		||||
    general_setButton: `Установить`,
 | 
			
		||||
    general_none: `Отсутствует`,
 | 
			
		||||
@ -31,6 +32,8 @@ dict = {
 | 
			
		||||
    code_renamePrompt: `Введите новое имя:`,
 | 
			
		||||
    code_remove: `Удалить`,
 | 
			
		||||
    code_addItemsConfirm: `Вы уверены, что хотите добавить |COUNT| предметов на ваш аккаунт?`,
 | 
			
		||||
    code_addTechProjectsConfirm: `Вы уверены, что хотите добавить |COUNT| исследований в свой клан?`,
 | 
			
		||||
    code_addDecoRecipesConfirm: `Вы уверены, что хотите добавить |COUNT| рецептов декораций в свой клан?`,
 | 
			
		||||
    code_succRankUp: `Ранг успешно повышен.`,
 | 
			
		||||
    code_noEquipmentToRankUp: `Нет снаряжения для повышения ранга.`,
 | 
			
		||||
    code_succAdded: `Успешно добавлено.`,
 | 
			
		||||
@ -63,6 +66,8 @@ dict = {
 | 
			
		||||
    code_pigment: `Пигмент`,
 | 
			
		||||
    code_mature: `Подготовить к сражениям`,
 | 
			
		||||
    code_unmature: `Регрессия генетического старения`,
 | 
			
		||||
    code_fund: `Профинансировать`,
 | 
			
		||||
    code_funded: `Профинансировано`,
 | 
			
		||||
    code_succChange: `Успешно изменено.`,
 | 
			
		||||
    code_requiredInvigorationUpgrade: `Вы должны выбрать как атакующее, так и вспомогательное улучшение.`,
 | 
			
		||||
    login_description: `Войдите, используя учетные данные OpenWF (те же, что и в игре при подключении к этому серверу).`,
 | 
			
		||||
@ -74,6 +79,7 @@ dict = {
 | 
			
		||||
    navbar_renameAccount: `Переименовать аккаунт`,
 | 
			
		||||
    navbar_deleteAccount: `Удалить аккаунт`,
 | 
			
		||||
    navbar_inventory: `Инвентарь`,
 | 
			
		||||
    navbar_guildView: `Клан`,
 | 
			
		||||
    navbar_mods: `Моды`,
 | 
			
		||||
    navbar_quests: `Квесты`,
 | 
			
		||||
    navbar_cheats: `Читы`,
 | 
			
		||||
@ -196,7 +202,6 @@ dict = {
 | 
			
		||||
    cheats_unlockAllFlavourItems: `Разблокировать все <abbr title="Наборы анимаций, глифы, палитры и т. д.">уникальные предметы</abbr>`,
 | 
			
		||||
    cheats_unlockAllSkins: `Разблокировать все скины`,
 | 
			
		||||
    cheats_unlockAllCapturaScenes: `Разблокировать все сцены Каптуры`,
 | 
			
		||||
    cheats_unlockAllDecoRecipes: `Разблокировать все рецепты декораций Дoдзё`,
 | 
			
		||||
    cheats_universalPolarityEverywhere: `Универсальная полярность везде`,
 | 
			
		||||
    cheats_unlockDoubleCapacityPotatoesEverywhere: `Реакторы/Катализаторы орокин везде`,
 | 
			
		||||
    cheats_unlockExilusEverywhere: `Адаптеры Эксилус везде`,
 | 
			
		||||
@ -405,5 +410,35 @@ dict = {
 | 
			
		||||
    theme_dark: `Темная тема`,
 | 
			
		||||
    theme_light: `Светлая тема`,
 | 
			
		||||
 | 
			
		||||
    guildView_cheats: `Читы Клана`,
 | 
			
		||||
    guildView_techProjects: `Иследовения`,
 | 
			
		||||
    guildView_vaultDecoRecipes: `Рецепты декораций Додзё`,
 | 
			
		||||
    guildView_alliance: `Альянс`,
 | 
			
		||||
    guildView_members: `Товарищи`,
 | 
			
		||||
    guildView_pending: `Ожидание`,
 | 
			
		||||
    guildView_classDisplay: `Ранг |CLASS|`,
 | 
			
		||||
    guildView_tierDisplay: `|TIER| Клан`,
 | 
			
		||||
    guildView_tier1: `Призрачный`,
 | 
			
		||||
    guildView_tier2: `Теневой`,
 | 
			
		||||
    guildView_tier3: `Штормовой`,
 | 
			
		||||
    guildView_tier4: `Горный`,
 | 
			
		||||
    guildView_tier5: `Лунный`,
 | 
			
		||||
    guildView_rank_creator: `Основатель`,
 | 
			
		||||
    guildView_rank_general: `Генерал`,
 | 
			
		||||
    guildView_rank_initiate: `Неофит`,
 | 
			
		||||
    guildView_rank_leader: `Лидер`,
 | 
			
		||||
    guildView_rank_officer: `Офицер`,
 | 
			
		||||
    guildView_rank_sage: `Мудрец`,
 | 
			
		||||
    guildView_rank_soldier: `Солдат`,
 | 
			
		||||
    guildView_rank_utility: `Инженер`,
 | 
			
		||||
    guildView_rank_warlord: `Военачальник`,
 | 
			
		||||
    guildView_currency_owned: `В хранилище |COUNT|.`,
 | 
			
		||||
    guildView_bulkAddTechProjects: `Добавить отсутствующие Иследования`,
 | 
			
		||||
    guildView_bulkAddVaultDecoRecipes: `Добавить отсутствующие рецепты декораций Дoдзё`,
 | 
			
		||||
    guildView_bulkFundTechProjects: `Профинансировать все Иследования`,
 | 
			
		||||
    guildView_bulkCompleteTechProjects: `Завершить все Иследования`,
 | 
			
		||||
    guildView_promote: `Повысить`,
 | 
			
		||||
    guildView_demote: `Понизить`,
 | 
			
		||||
 | 
			
		||||
    prettier_sucks_ass: ``
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
// Ukrainian translation by LoseFace
 | 
			
		||||
dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `Пам'ятка: Щоб побачити зміни в грі, вам потрібно повторно синхронізувати своє спорядження, наприклад, використовуючи команду /sync завантажувача, відвідавши Доджьо/Реле або перезавантаживши гру.`,
 | 
			
		||||
    general_inventoryUpdateNoteGameWs: `[UNTRANSLATED] Note: You may need to reopen any menu you are on for changes to be reflected.`,
 | 
			
		||||
    general_addButton: `Добавити`,
 | 
			
		||||
    general_setButton: `Встановити`,
 | 
			
		||||
    general_none: `Відсутній`,
 | 
			
		||||
@ -31,6 +32,8 @@ dict = {
 | 
			
		||||
    code_renamePrompt: `Введіть нове ім'я:`,
 | 
			
		||||
    code_remove: `Видалити`,
 | 
			
		||||
    code_addItemsConfirm: `Ви впевнені, що хочете додати |COUNT| предметів на ваш обліковий запис?`,
 | 
			
		||||
    code_addTechProjectsConfirm: `[UNTRANSLATED] Are you sure you want to add |COUNT| research to your clan?`,
 | 
			
		||||
    code_addDecoRecipesConfirm: `[UNTRANSLATED] Are you sure you want to add |COUNT| deco recipes to your clan?`,
 | 
			
		||||
    code_succRankUp: `Рівень успішно підвищено`,
 | 
			
		||||
    code_noEquipmentToRankUp: `Немає спорядження для підвищення рівня.`,
 | 
			
		||||
    code_succAdded: `Успішно додано.`,
 | 
			
		||||
@ -63,6 +66,8 @@ dict = {
 | 
			
		||||
    code_pigment: `Барвник`,
 | 
			
		||||
    code_mature: `Виростити для бою`,
 | 
			
		||||
    code_unmature: `Обернути старіння`,
 | 
			
		||||
    code_fund: `[UNTRANSLATED] Fund`,
 | 
			
		||||
    code_funded: `[UNTRANSLATED] Funded`,
 | 
			
		||||
    code_succChange: `Успішно змінено.`,
 | 
			
		||||
    code_requiredInvigorationUpgrade: `Ви повинні вибрати як атакуюче, так і допоміжне вдосконалення.`,
 | 
			
		||||
    login_description: `Увійдіть, використовуючи облікові дані OpenWF (ті ж, що й у грі при підключенні до цього серверу).`,
 | 
			
		||||
@ -74,6 +79,7 @@ dict = {
 | 
			
		||||
    navbar_renameAccount: `Перейменувати обліковий запис`,
 | 
			
		||||
    navbar_deleteAccount: `Видалити обліковий запис`,
 | 
			
		||||
    navbar_inventory: `Спорядження`,
 | 
			
		||||
    navbar_guildView: `Клан`,
 | 
			
		||||
    navbar_mods: `Модифікатори`,
 | 
			
		||||
    navbar_quests: `Пригоди`,
 | 
			
		||||
    navbar_cheats: `Чити`,
 | 
			
		||||
@ -196,7 +202,6 @@ dict = {
 | 
			
		||||
    cheats_unlockAllFlavourItems: `Розблокувати всі <abbr title="Набори анімацій, гліфи, палітри і т. д.">унікальні предмети</abbr>`,
 | 
			
		||||
    cheats_unlockAllSkins: `Розблокувати всі скіни`,
 | 
			
		||||
    cheats_unlockAllCapturaScenes: `Розблокувати всі сцени Світлописця`,
 | 
			
		||||
    cheats_unlockAllDecoRecipes: `Розблокувати всі рецепти декорацій Доджьо`,
 | 
			
		||||
    cheats_universalPolarityEverywhere: `Будь-яка полярність скрізь`,
 | 
			
		||||
    cheats_unlockDoubleCapacityPotatoesEverywhere: `Орокінські Реактори/Каталізатори скрізь`,
 | 
			
		||||
    cheats_unlockExilusEverywhere: `Ексилотримач скрізь`,
 | 
			
		||||
@ -405,5 +410,35 @@ dict = {
 | 
			
		||||
    theme_dark: `Темна тема`,
 | 
			
		||||
    theme_light: `Світла тема`,
 | 
			
		||||
 | 
			
		||||
    guildView_cheats: `[UNTRANSLATED] Clan Cheats`,
 | 
			
		||||
    guildView_techProjects: `Дослідження`,
 | 
			
		||||
    guildView_vaultDecoRecipes: `[UNTRANSLATED] Dojo Deco Recipes`,
 | 
			
		||||
    guildView_alliance: `Альянс`,
 | 
			
		||||
    guildView_members: `Учасники`,
 | 
			
		||||
    guildView_pending: `Очікування`,
 | 
			
		||||
    guildView_classDisplay: `Ранг: |CLASS|`,
 | 
			
		||||
    guildView_tierDisplay: `|TIER| Клан`,
 | 
			
		||||
    guildView_tier1: `Примарний`,
 | 
			
		||||
    guildView_tier2: `Тіньовий`,
 | 
			
		||||
    guildView_tier3: `Грозовий`,
 | 
			
		||||
    guildView_tier4: `Гірський`,
 | 
			
		||||
    guildView_tier5: `Місячний`,
 | 
			
		||||
    guildView_rank_creator: `Воєвода-засновник`,
 | 
			
		||||
    guildView_rank_general: `Генерал`,
 | 
			
		||||
    guildView_rank_initiate: `Рекрут`,
 | 
			
		||||
    guildView_rank_leader: `Лідер`,
 | 
			
		||||
    guildView_rank_officer: `Офіцер`,
 | 
			
		||||
    guildView_rank_sage: `Ветеран`,
 | 
			
		||||
    guildView_rank_soldier: `Солдат`,
 | 
			
		||||
    guildView_rank_utility: `Наймит`,
 | 
			
		||||
    guildView_rank_warlord: `Воєвода`,
 | 
			
		||||
    guildView_currency_owned: `[UNTRANSLATED] |COUNT| in Vault.`,
 | 
			
		||||
    guildView_bulkAddTechProjects: `[UNTRANSLATED] Add Missing Research`,
 | 
			
		||||
    guildView_bulkAddVaultDecoRecipes: `[UNTRANSLATED] Add Missing Dojo Deco Recipes`,
 | 
			
		||||
    guildView_bulkFundTechProjects: `[UNTRANSLATED] Fund All Research`,
 | 
			
		||||
    guildView_bulkCompleteTechProjects: `[UNTRANSLATED] Complete All Research`,
 | 
			
		||||
    guildView_promote: `Підвищити звання`,
 | 
			
		||||
    guildView_demote: `Понизити звання`,
 | 
			
		||||
 | 
			
		||||
    prettier_sucks_ass: ``
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
// Chinese translation by meb154, bishan178, nyaoouo, qianlishun, CrazyZhang, Corvus, & qingchun
 | 
			
		||||
dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `注意: 要在游戏中查看更改,您需要重新同步库存,例如使用客户端的 /sync 命令,访问道场/中继站或重新登录.`,
 | 
			
		||||
    general_inventoryUpdateNoteGameWs: `[UNTRANSLATED] Note: You may need to reopen any menu you are on for changes to be reflected.`,
 | 
			
		||||
    general_addButton: `添加`,
 | 
			
		||||
    general_setButton: `设置`,
 | 
			
		||||
    general_none: `无`,
 | 
			
		||||
@ -31,6 +32,8 @@ dict = {
 | 
			
		||||
    code_renamePrompt: `输入新的自定义名称:`,
 | 
			
		||||
    code_remove: `移除`,
 | 
			
		||||
    code_addItemsConfirm: `确定要向您的账户添加 |COUNT| 件物品吗?`,
 | 
			
		||||
    code_addTechProjectsConfirm: `[UNTRANSLATED] Are you sure you want to add |COUNT| research to your clan?`,
 | 
			
		||||
    code_addDecoRecipesConfirm: `[UNTRANSLATED] Are you sure you want to add |COUNT| deco recipes to your clan?`,
 | 
			
		||||
    code_succRankUp: `等级已提升`,
 | 
			
		||||
    code_noEquipmentToRankUp: `没有可升级的装备`,
 | 
			
		||||
    code_succAdded: `添加成功`,
 | 
			
		||||
@ -63,6 +66,8 @@ dict = {
 | 
			
		||||
    code_pigment: `颜料`,
 | 
			
		||||
    code_mature: `成长并战备`,
 | 
			
		||||
    code_unmature: `逆转衰老基因`,
 | 
			
		||||
    code_fund: `[UNTRANSLATED] Fund`,
 | 
			
		||||
    code_funded: `[UNTRANSLATED] Funded`,
 | 
			
		||||
    code_succChange: `更改成功`,
 | 
			
		||||
    code_requiredInvigorationUpgrade: `您必须同时选择一个进攻型和一个功能型活化属性.`,
 | 
			
		||||
    login_description: `使用您的 OpenWF 账户凭证登录(与游戏内连接本服务器时使用的昵称相同)`,
 | 
			
		||||
@ -74,6 +79,7 @@ dict = {
 | 
			
		||||
    navbar_renameAccount: `重命名账户`,
 | 
			
		||||
    navbar_deleteAccount: `删除账户`,
 | 
			
		||||
    navbar_inventory: `仓库`,
 | 
			
		||||
    navbar_guildView: `氏族`,
 | 
			
		||||
    navbar_mods: `Mods`,
 | 
			
		||||
    navbar_quests: `系列任务`,
 | 
			
		||||
    navbar_cheats: `作弊选项`,
 | 
			
		||||
@ -196,7 +202,6 @@ dict = {
 | 
			
		||||
    cheats_unlockAllFlavourItems: `解锁所有<abbr title="动作表情、浮印、调色板等">装饰物品</abbr>`,
 | 
			
		||||
    cheats_unlockAllSkins: `解锁所有外观`,
 | 
			
		||||
    cheats_unlockAllCapturaScenes: `解锁所有Captura场景`,
 | 
			
		||||
    cheats_unlockAllDecoRecipes: `解锁所有道场配方`,
 | 
			
		||||
    cheats_universalPolarityEverywhere: `全局万用极性`,
 | 
			
		||||
    cheats_unlockDoubleCapacityPotatoesEverywhere: `全物品自带Orokin反应堆`,
 | 
			
		||||
    cheats_unlockExilusEverywhere: `全物品自带适配器`,
 | 
			
		||||
@ -405,5 +410,35 @@ dict = {
 | 
			
		||||
    theme_dark: `暗色主题`,
 | 
			
		||||
    theme_light: `亮色主题`,
 | 
			
		||||
 | 
			
		||||
    guildView_cheats: `[UNTRANSLATED] Clan Cheats`,
 | 
			
		||||
    guildView_techProjects: `研究`,
 | 
			
		||||
    guildView_vaultDecoRecipes: `[UNTRANSLATED] Dojo Deco Recipes`,
 | 
			
		||||
    guildView_alliance: `联盟`,
 | 
			
		||||
    guildView_members: `成员`,
 | 
			
		||||
    guildView_pending: `待处理`,
 | 
			
		||||
    guildView_classDisplay: `等級 |CLASS|`,
 | 
			
		||||
    guildView_tierDisplay: `|TIER| 氏族`,
 | 
			
		||||
    guildView_tier1: `幽灵`,
 | 
			
		||||
    guildView_tier2: `暗影`,
 | 
			
		||||
    guildView_tier3: `风暴`,
 | 
			
		||||
    guildView_tier4: `山脉`,
 | 
			
		||||
    guildView_tier5: `月亮`,
 | 
			
		||||
    guildView_rank_creator: `创始军阀`,
 | 
			
		||||
    guildView_rank_general: `将军`,
 | 
			
		||||
    guildView_rank_initiate: `新兵`,
 | 
			
		||||
    guildView_rank_leader: `首领`,
 | 
			
		||||
    guildView_rank_officer: `智者`,
 | 
			
		||||
    guildView_rank_sage: `贤者`,
 | 
			
		||||
    guildView_rank_soldier: `战士`,
 | 
			
		||||
    guildView_rank_utility: `实管`,
 | 
			
		||||
    guildView_rank_warlord: `军阀`,
 | 
			
		||||
    guildView_currency_owned: `[UNTRANSLATED] |COUNT| in Vault.`,
 | 
			
		||||
    guildView_bulkAddTechProjects: `[UNTRANSLATED] Add Missing Research`,
 | 
			
		||||
    guildView_bulkAddVaultDecoRecipes: `[UNTRANSLATED] Add Missing Dojo Deco Recipes`,
 | 
			
		||||
    guildView_bulkFundTechProjects: `[UNTRANSLATED] Fund All Research`,
 | 
			
		||||
    guildView_bulkCompleteTechProjects: `[UNTRANSLATED] Complete All Research`,
 | 
			
		||||
    guildView_promote: `升级`,
 | 
			
		||||
    guildView_demote: `降级`,
 | 
			
		||||
 | 
			
		||||
    prettier_sucks_ass: ``
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user