Compare commits

..

28 Commits

Author SHA1 Message Date
1e2341af88 prettier
All checks were successful
Build / build (pull_request) Successful in 1m18s
2025-09-10 08:57:41 +02:00
22a9cd2436 update webui messaging around syncing when game ws is available 2025-09-10 08:57:41 +02:00
d52cbb9ac4 sync inventory when focus schools were unlocked 2025-09-10 08:57:41 +02:00
88f343f39a let webui know when game ws is available 2025-09-10 08:57:41 +02:00
ae9030d2ce add sendWsBroadcastToWebui 2025-09-10 08:57:41 +02:00
85db8e21c5 simplify broadcastInventoryUpdate 2025-09-10 08:57:41 +02:00
9b52c1c1db add sendWsBroadcastToGame 2025-09-10 08:57:41 +02:00
4443259724 refresh inventory for infinite cheats 2025-09-10 08:57:41 +02:00
8499f3ee67 NaN is not undefined 2025-09-10 08:57:41 +02:00
ed48e251f0 make sure all webui options trigger respective inventory syncs 2025-09-10 08:57:41 +02:00
7b99502cf8 note 2025-09-10 08:56:53 +02:00
3b10dc374f move wsid to authz 2025-09-10 08:56:53 +02:00
26d30105d6 add broadcastInventoryUpdate 2025-09-10 08:56:53 +02:00
831e72c691 add forEachClient 2025-09-10 08:56:53 +02:00
2e13fd2c46 change ws import 2025-09-10 08:56:53 +02:00
a6294a0324 ignore drop after logout 2025-09-10 08:56:53 +02:00
b03096d180 logging 2025-09-10 08:56:53 +02:00
07b9bc9415 forcibly close game ws connections when nonce is invalidated 2025-09-10 08:56:53 +02:00
a9d96d6725 feat: support websocket connections from game client 2025-09-10 08:56:52 +02:00
d64531f4b2 feat(webui): guild view (#2752)
All checks were successful
Build Docker image / docker-arm64 (push) Successful in 1m7s
Build Docker image / docker-amd64 (push) Successful in 51s
Build / build (push) Successful in 1m59s
Also moves guild-specific cheats to a switch for each guild
Closes #1403

Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Reviewed-on: #2752
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-09-09 23:55:10 -07:00
01b8f7acf3 chore(webui): better locale support for relics (#2764)
Some checks failed
Build Docker image / docker-arm64 (push) Waiting to run
Build Docker image / docker-amd64 (push) Has been cancelled
Build / build (push) Has been cancelled
Reviewed-on: #2764
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-09-09 23:55:01 -07:00
8a7db2cd85 chore: update PE+ (#2765)
Some checks failed
Build Docker image / docker-amd64 (push) Waiting to run
Build Docker image / docker-arm64 (push) Has been cancelled
Build / build (push) Has been cancelled
Some things were deprecated in it

Reviewed-on: #2765
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-09-09 23:54:46 -07:00
5a9415ae0c feat: bindAddress (#2766)
Some checks failed
Build Docker image / docker-amd64 (push) Waiting to run
Build Docker image / docker-arm64 (push) Has been cancelled
Build / build (push) Has been cancelled
so people can limit the server to only be reachable via 127.0.0.1 etc

Reviewed-on: #2766
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-09-09 23:54:33 -07:00
39f898cd30 chore: use inlineSourceMap instead of sourceMap (#2767)
Some checks failed
Build Docker image / docker-amd64 (push) Waiting to run
Build / build (push) Has been cancelled
Build Docker image / docker-arm64 (push) Has been cancelled
Windows filesystem is pretty slow, so avoiding creating an extra file per file makes `npm run build` ~20% faster (~1600ms to ~1300ms on my machine)

Reviewed-on: #2767
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-09-09 23:54:26 -07:00
9c55a8a4aa chore: enable no-deprecated warning (#2762)
All checks were successful
Build Docker image / docker-amd64 (push) Successful in 47s
Build Docker image / docker-arm64 (push) Successful in 1m4s
Build / build (push) Successful in 1m55s
Reviewed-on: #2762
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-09-08 20:43:31 -07:00
253ae09f24 fix(webui): use excludeFromCodex to detect arcane imposters (#2761)
Some checks failed
Build Docker image / docker-arm64 (push) Waiting to run
Build / build (push) Has been cancelled
Build Docker image / docker-amd64 (push) Has been cancelled
Closes #2760

Reviewed-on: #2761
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-09-08 20:43:15 -07:00
703e9007b0 fix: invasion reward message sender name (#2759)
Some checks failed
Build Docker image / docker-amd64 (push) Has been cancelled
Build Docker image / docker-arm64 (push) Has been cancelled
Build / build (push) Has been cancelled
Reviewed-on: #2759
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-09-08 20:43:02 -07:00
3e555b1753 feat: purchase additional conclave loadout slots (#2758)
Some checks failed
Build Docker image / docker-amd64 (push) Waiting to run
Build Docker image / docker-arm64 (push) Has been cancelled
Build / build (push) Has been cancelled
Closes #2756. Also just in general simplified the logic around purchasing loadout slots.

Reviewed-on: #2758
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-09-08 20:42:53 -07:00
46 changed files with 1664 additions and 330 deletions

View File

@ -31,7 +31,8 @@
"no-mixed-spaces-and-tabs": "error",
"@typescript-eslint/require-await": "error",
"import/no-named-as-default-member": "off",
"import/no-cycle": "warn"
"import/no-cycle": "warn",
"@typescript-eslint/no-deprecated": "warn"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {

View File

@ -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
View File

@ -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",

View File

@ -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",

View File

@ -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();

View File

@ -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);

View File

@ -231,7 +231,7 @@ interface ILensInstallRequest {
// Works for ways & upgrades
const focusTypeToPolarity = (type: string): TFocusPolarity => {
return ("AP_" + type.substr(1).split("/")[3].toUpperCase()) as TFocusPolarity;
return ("AP_" + type.substring(1).split("/")[3].toUpperCase()) as TFocusPolarity;
};
const shardValues = {

View File

@ -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/") {

View File

@ -10,7 +10,7 @@ import { equipmentKeys } from "../../types/inventoryTypes/inventoryTypes.ts";
import type { IPolarity } from "../../types/inventoryTypes/commonInventoryTypes.ts";
import { ArtifactPolarity } from "../../types/inventoryTypes/commonInventoryTypes.ts";
import type { ICountedItem } from "warframe-public-export-plus";
import { eFaction, ExportCustoms, ExportFlavour, ExportResources } from "warframe-public-export-plus";
import { ExportCustoms, ExportFlavour, ExportResources } from "warframe-public-export-plus";
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "../../services/infestedFoundryService.ts";
import {
addEmailItem,
@ -220,7 +220,10 @@ export const inventoryController: RequestHandler = async (request, response) =>
}
await createMessage(account._id, [
{
sndr: eFaction.find(x => x.tag == factionSidedWith)?.name ?? factionSidedWith, // TOVERIFY
sndr:
factionSidedWith == "FC_GRINEER"
? "/Lotus/Language/Menu/GrineerInvasionLeader"
: "/Lotus/Language/Menu/CorpusInvasionLeader",
msg: `/Lotus/Language/G1Quests/${factionSidedWith}_InvasionThankyouMessageBody`,
sub: `/Lotus/Language/G1Quests/${factionSidedWith}_InvasionThankyouMessageSubject`,
countedAtt: battlePay,

View File

@ -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);

View File

@ -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();

View File

@ -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);

View File

@ -1,23 +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();
broadcastInventoryUpdate(req);
};
interface IAddCurrencyRequest {
currency: "RegularCredits" | "PremiumCredits" | "FusionPoints" | "PrimeTokens";
currency:
| "RegularCredits"
| "PremiumCredits"
| "FusionPoints"
| "PrimeTokens"
| "VaultRegularCredits"
| "VaultPremiumCredits";
delta: number;
}

View 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();
};

View 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();
};

View 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();
}
}
};

View File

@ -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 => {
return str.replace(/[^\s-]+/g, word => word.charAt(0).toUpperCase() + word.substr(1).toLowerCase());
};*/
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",
@ -221,7 +227,7 @@ const getItemListsController: RequestHandler = (req, response) => {
}
if (
name &&
uniqueName.substr(0, 30) != "/Lotus/Types/Game/Projections/" &&
uniqueName.substring(0, 30) != "/Lotus/Types/Game/Projections/" &&
uniqueName != "/Lotus/Types/Gameplay/EntratiLab/Resources/EntratiLanthornBundle"
) {
res.miscitems.push({
@ -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)) {
@ -313,7 +324,7 @@ const getItemListsController: RequestHandler = (req, response) => {
uniqueName,
name: getString(arcane.name, lang)
};
if (arcane.isFrivolous) {
if (arcane.excludeFromCodex) {
mod.badReason = "frivolous";
}
res.mods.push(mod);
@ -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);
};

View 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;
}

View 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;
}

View File

@ -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)
) {

View File

@ -1,23 +0,0 @@
import type { SlotPurchase, SlotPurchaseName } from "../types/purchaseTypes.ts";
export const slotPurchaseNameToSlotName: SlotPurchase = {
SuitSlotItem: { name: "SuitBin", purchaseQuantity: 1 },
TwoSentinelSlotItem: { name: "SentinelBin", purchaseQuantity: 2 },
TwoWeaponSlotItem: { name: "WeaponBin", purchaseQuantity: 2 },
SpaceSuitSlotItem: { name: "SpaceSuitBin", purchaseQuantity: 1 },
TwoSpaceWeaponSlotItem: { name: "SpaceWeaponBin", purchaseQuantity: 2 },
MechSlotItem: { name: "MechBin", purchaseQuantity: 1 },
TwoOperatorWeaponSlotItem: { name: "OperatorAmpBin", purchaseQuantity: 2 },
RandomModSlotItem: { name: "RandomModBin", purchaseQuantity: 3 },
TwoCrewShipSalvageSlotItem: { name: "CrewShipSalvageBin", purchaseQuantity: 2 },
CrewMemberSlotItem: { name: "CrewMemberBin", purchaseQuantity: 1 }
};
export const isSlotPurchaseName = (slotPurchaseName: string): slotPurchaseName is SlotPurchaseName => {
return slotPurchaseName in slotPurchaseNameToSlotName;
};
export const parseSlotPurchaseName = (slotPurchaseName: string): SlotPurchaseName => {
if (!isSlotPurchaseName(slotPurchaseName)) throw new Error(`invalid slot name ${slotPurchaseName}`);
return slotPurchaseName;
};

View File

@ -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 },

View File

@ -14,8 +14,8 @@ cacheRouter.get(/^\/origin\/[a-zA-Z0-9]+\/[0-9]+\/H\.Cache\.bin.*$/, (req, res)
cacheRouter.get(/^\/0\/.+!.+$/, async (req, res) => {
try {
const dir = req.path.substr(0, req.path.lastIndexOf("/"));
const file = req.path.substr(dir.length + 1);
const dir = req.path.substring(0, req.path.lastIndexOf("/"));
const file = req.path.substring(dir.length + 1);
const filePath = `static/data${dir}/${file}`;
// Return file if we have it

View File

@ -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);

View File

@ -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")));

View File

@ -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
};
};

View File

@ -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 } });

View File

@ -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 => {

View File

@ -687,10 +687,10 @@ export const addItem = async (
}
// Path-based duck typing
switch (typeName.substr(1).split("/")[1]) {
switch (typeName.substring(1).split("/")[1]) {
case "Powersuits":
if (typeName.endsWith("AugmentCard")) break;
switch (typeName.substr(1).split("/")[2]) {
switch (typeName.substring(1).split("/")[2]) {
default: {
return {
...(await addPowerSuit(inventory, typeName, {
@ -725,7 +725,7 @@ export const addItem = async (
}
break;
case "Upgrades": {
switch (typeName.substr(1).split("/")[2]) {
switch (typeName.substring(1).split("/")[2]) {
case "Mods": // Legendary Core
case "CosmeticEnhancers": // Traumatic Peculiar
{
@ -782,12 +782,12 @@ export const addItem = async (
break;
}
case "Types":
switch (typeName.substr(1).split("/")[2]) {
switch (typeName.substring(1).split("/")[2]) {
case "Sentinels": {
return addSentinel(inventory, typeName, premiumPurchase);
}
case "Game": {
if (typeName.substr(1).split("/")[3] == "Projections") {
if (typeName.substring(1).split("/")[3] == "Projections") {
// Void Relics, e.g. /Lotus/Types/Game/Projections/T2VoidProjectionGaussPrimeDBronze
const miscItemChanges = [
{
@ -801,8 +801,8 @@ export const addItem = async (
MiscItems: miscItemChanges
};
} else if (
typeName.substr(1).split("/")[3] == "CatbrowPet" ||
typeName.substr(1).split("/")[3] == "KubrowPet"
typeName.substring(1).split("/")[3] == "CatbrowPet" ||
typeName.substring(1).split("/")[3] == "KubrowPet"
) {
if (
typeName != "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem" &&
@ -826,7 +826,7 @@ export const addItem = async (
break;
}
case "Items": {
if (typeName.substr(1).split("/")[3] == "Emotes") {
if (typeName.substring(1).split("/")[3] == "Emotes") {
return addCustomization(inventory, typeName);
}
break;
@ -875,8 +875,8 @@ export const addItem = async (
}
break;
case "Weapons": {
if (typeName.substr(1).split("/")[4] == "MeleeTrees") break;
const productCategory = typeName.substr(1).split("/")[3];
if (typeName.substring(1).split("/")[4] == "MeleeTrees") break;
const productCategory = typeName.substring(1).split("/")[3];
switch (productCategory) {
case "Pistols":
case "LongGuns":
@ -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 });
}
};

View File

@ -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;
};

View File

@ -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];
}
@ -1095,8 +1099,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
@ -1792,14 +1796,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") {
@ -2165,7 +2169,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;

View File

@ -1,4 +1,3 @@
import { parseSlotPurchaseName, slotPurchaseNameToSlotName } from "../helpers/purchaseHelpers.ts";
import { getSubstringFromKeyword } from "../helpers/stringHelpers.ts";
import {
addBooster,
@ -15,12 +14,12 @@ import type {
IPurchaseRequest,
IPurchaseResponse,
IInventoryChanges,
IPurchaseParams
IPurchaseParams,
SlotNames
} from "../types/purchaseTypes.ts";
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,13 +481,27 @@ export const handleStoreItemAcquisition = async (
);
break;
case "Boosters":
purchaseResponse = handleBoostersPurchase(storeItemName, inventory, durability);
purchaseResponse = handleBoostersPurchase(storeItemName, inventory, durabilityDays);
break;
}
}
return purchaseResponse;
};
const slotPurchaseNameToSlotName: Record<string, { name: SlotNames; purchaseQuantity: number }> = {
SuitSlotItem: { name: "SuitBin", purchaseQuantity: 1 },
TwoSentinelSlotItem: { name: "SentinelBin", purchaseQuantity: 2 },
TwoWeaponSlotItem: { name: "WeaponBin", purchaseQuantity: 2 },
SpaceSuitSlotItem: { name: "SpaceSuitBin", purchaseQuantity: 1 },
TwoSpaceWeaponSlotItem: { name: "SpaceWeaponBin", purchaseQuantity: 2 },
MechSlotItem: { name: "MechBin", purchaseQuantity: 1 },
TwoOperatorWeaponSlotItem: { name: "OperatorAmpBin", purchaseQuantity: 2 },
RandomModSlotItem: { name: "RandomModBin", purchaseQuantity: 3 },
TwoCrewShipSalvageSlotItem: { name: "CrewShipSalvageBin", purchaseQuantity: 2 },
CrewMemberSlotItem: { name: "CrewMemberBin", purchaseQuantity: 1 },
PvPLoadoutSlotItem: { name: "PvpBonusLoadoutBin", purchaseQuantity: 1 }
};
// // extra = everything above the base +2 slots (depending on slot type)
// // new slot above base = extra + 1 and slots +1
// // new frame = slots -1
@ -500,9 +513,8 @@ const handleSlotPurchase = (
ignorePurchaseQuantity: boolean
): IPurchaseResponse => {
logger.debug(`slot name ${slotPurchaseNameFull}`);
const slotPurchaseName = parseSlotPurchaseName(
slotPurchaseNameFull.substring(slotPurchaseNameFull.lastIndexOf("/") + 1)
);
const slotPurchaseName = slotPurchaseNameFull.substring(slotPurchaseNameFull.lastIndexOf("/") + 1);
if (!(slotPurchaseName in slotPurchaseNameToSlotName)) throw new Error(`invalid slot name ${slotPurchaseName}`);
logger.debug(`slot purchase name ${slotPurchaseName}`);
const slotName = slotPurchaseNameToSlotName[slotPurchaseName].name;
@ -659,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}`);
@ -667,7 +679,7 @@ const handleBoostersPurchase = (
}
const ItemType = ExportBoosters[boosterStoreName].typeName;
const ExpiryDate = ExportMisc.boosterDurations[durability];
const ExpiryDate = durabilityDays * 86400;
addBooster(ItemType, ExpiryDate, inventory);

View File

@ -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
};
};

View File

@ -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;

View File

@ -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;

View File

@ -120,18 +120,6 @@ export type IBinChanges = {
Extra?: number;
};
export type SlotPurchaseName =
| "SuitSlotItem"
| "TwoSentinelSlotItem"
| "TwoWeaponSlotItem"
| "SpaceSuitSlotItem"
| "TwoSpaceWeaponSlotItem"
| "MechSlotItem"
| "TwoOperatorWeaponSlotItem"
| "RandomModSlotItem"
| "TwoCrewShipSalvageSlotItem"
| "CrewMemberSlotItem";
export const slotNames = [
"SuitBin",
"WeaponBin",
@ -148,7 +136,3 @@ export const slotNames = [
] as const;
export type SlotNames = (typeof slotNames)[number];
export type SlotPurchase = {
[P in SlotPurchaseName]: { name: SlotNames; purchaseQuantity: number };
};

View File

@ -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"
]

View File

@ -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>
@ -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>
@ -855,10 +991,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>
@ -867,30 +999,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">
@ -1277,6 +1385,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>

View File

@ -267,6 +267,9 @@ function setLanguage(lang) {
// Not in prelogin state?
fetchItemList();
updateInventory();
if (single.getCurrentPath().startsWith("/webui/guildView")) {
updateInventory();
}
}
}
@ -501,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)) {
@ -634,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",
@ -946,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);
}
}
});
@ -1043,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");
@ -1513,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) {
elm.checked = !!data[elm.id];
}
@ -1520,6 +1865,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"))
@ -1744,6 +2157,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);
@ -2392,6 +3061,21 @@ document.querySelectorAll("#account-cheats input[type=checkbox]").forEach(elm =>
};
});
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,
value: elm.checked
})
});
});
};
});
// Mods route
function doAddAllMods() {
@ -2482,6 +3166,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']"));
@ -2545,7 +3255,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,
@ -2557,11 +3267,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) {

View File

@ -32,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.`,
@ -64,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).`,
@ -75,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`,
@ -197,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`,
@ -388,5 +392,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: ``
};

View File

@ -31,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.`,
@ -63,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).`,
@ -74,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`,
@ -196,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`,
@ -387,5 +391,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: ``
};

View File

@ -32,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.`,
@ -64,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).`,
@ -75,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`,
@ -197,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`,
@ -388,5 +392,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: ``
};

View File

@ -32,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é.`,
@ -64,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.`,
@ -75,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`,
@ -197,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`,
@ -388,5 +392,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: ``
};

View File

@ -32,6 +32,8 @@ dict = {
code_renamePrompt: `Введите новое имя:`,
code_remove: `Удалить`,
code_addItemsConfirm: `Вы уверены, что хотите добавить |COUNT| предметов на ваш аккаунт?`,
code_addTechProjectsConfirm: `Вы уверены, что хотите добавить |COUNT| исследований в свой клан?`,
code_addDecoRecipesConfirm: `Вы уверены, что хотите добавить |COUNT| рецептов декораций в свой клан?`,
code_succRankUp: `Ранг успешно повышен.`,
code_noEquipmentToRankUp: `Нет снаряжения для повышения ранга.`,
code_succAdded: `Успешно добавлено.`,
@ -64,6 +66,8 @@ dict = {
code_pigment: `Пигмент`,
code_mature: `Подготовить к сражениям`,
code_unmature: `Регрессия генетического старения`,
code_fund: `Профинансировать`,
code_funded: `Профинансировано`,
code_succChange: `Успешно изменено.`,
code_requiredInvigorationUpgrade: `Вы должны выбрать как атакующее, так и вспомогательное улучшение.`,
login_description: `Войдите, используя учетные данные OpenWF (те же, что и в игре при подключении к этому серверу).`,
@ -75,6 +79,7 @@ dict = {
navbar_renameAccount: `Переименовать аккаунт`,
navbar_deleteAccount: `Удалить аккаунт`,
navbar_inventory: `Инвентарь`,
navbar_guildView: `Клан`,
navbar_mods: `Моды`,
navbar_quests: `Квесты`,
navbar_cheats: `Читы`,
@ -197,7 +202,6 @@ dict = {
cheats_unlockAllFlavourItems: `Разблокировать все <abbr title="Наборы анимаций, глифы, палитры и т. д.">уникальные предметы</abbr>`,
cheats_unlockAllSkins: `Разблокировать все скины`,
cheats_unlockAllCapturaScenes: `Разблокировать все сцены Каптуры`,
cheats_unlockAllDecoRecipes: `Разблокировать все рецепты декораций Дoдзё`,
cheats_universalPolarityEverywhere: `Универсальная полярность везде`,
cheats_unlockDoubleCapacityPotatoesEverywhere: `Реакторы/Катализаторы орокин везде`,
cheats_unlockExilusEverywhere: `Адаптеры Эксилус везде`,
@ -388,5 +392,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: ``
};

View File

@ -32,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: `Успішно додано.`,
@ -64,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 (ті ж, що й у грі при підключенні до цього серверу).`,
@ -75,6 +79,7 @@ dict = {
navbar_renameAccount: `Перейменувати обліковий запис`,
navbar_deleteAccount: `Видалити обліковий запис`,
navbar_inventory: `Спорядження`,
navbar_guildView: `Клан`,
navbar_mods: `Модифікатори`,
navbar_quests: `Пригоди`,
navbar_cheats: `Чити`,
@ -197,7 +202,6 @@ dict = {
cheats_unlockAllFlavourItems: `Розблокувати всі <abbr title="Набори анімацій, гліфи, палітри і т. д.">унікальні предмети</abbr>`,
cheats_unlockAllSkins: `Розблокувати всі скіни`,
cheats_unlockAllCapturaScenes: `Розблокувати всі сцени Світлописця`,
cheats_unlockAllDecoRecipes: `Розблокувати всі рецепти декорацій Доджьо`,
cheats_universalPolarityEverywhere: `Будь-яка полярність скрізь`,
cheats_unlockDoubleCapacityPotatoesEverywhere: `Орокінські Реактори/Каталізатори скрізь`,
cheats_unlockExilusEverywhere: `Ексилотримач скрізь`,
@ -388,5 +392,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: ``
};

View File

@ -32,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: `添加成功`,
@ -64,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 账户凭证登录(与游戏内连接本服务器时使用的昵称相同)`,
@ -75,6 +79,7 @@ dict = {
navbar_renameAccount: `重命名账户`,
navbar_deleteAccount: `删除账户`,
navbar_inventory: `仓库`,
navbar_guildView: `氏族`,
navbar_mods: `Mods`,
navbar_quests: `系列任务`,
navbar_cheats: `作弊选项`,
@ -197,7 +202,6 @@ dict = {
cheats_unlockAllFlavourItems: `解锁所有<abbr title="动作表情、浮印、调色板等">装饰物品</abbr>`,
cheats_unlockAllSkins: `解锁所有外观`,
cheats_unlockAllCapturaScenes: `解锁所有Captura场景`,
cheats_unlockAllDecoRecipes: `解锁所有道场配方`,
cheats_universalPolarityEverywhere: `全局万用极性`,
cheats_unlockDoubleCapacityPotatoesEverywhere: `全物品自带Orokin反应堆`,
cheats_unlockExilusEverywhere: `全物品自带适配器`,
@ -388,5 +392,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: ``
};