merge upstream

This commit is contained in:
hxedcl 2025-04-13 09:30:41 -07:00
commit 0acca4fe31
27 changed files with 761 additions and 166 deletions

2
.gitattributes vendored
View File

@ -1,4 +1,4 @@
# Auto detect text files and perform LF normalization # Auto detect text files and perform LF normalization
* text=auto * text=auto eol=lf
static/webui/libs/ linguist-vendored static/webui/libs/ linguist-vendored

View File

@ -15,11 +15,12 @@ jobs:
- run: npm run verify - run: npm run verify
- run: npm run lint:ci - run: npm run lint:ci
- run: npm run prettier - run: npm run prettier
- run: npm run update-translations
- name: Fail if there are uncommitted changes - name: Fail if there are uncommitted changes
run: | run: |
if [[ -n "$(git status --porcelain)" ]]; then if [[ -n "$(git status --porcelain)" ]]; then
echo "Uncommitted changes detected:" echo "Uncommitted changes detected:"
git status git status
git diff git --no-pager diff
exit 1 exit 1
fi fi

View File

@ -32,6 +32,7 @@
"noArgonCrystalDecay": false, "noArgonCrystalDecay": false,
"noMasteryRankUpCooldown": false, "noMasteryRankUpCooldown": false,
"noVendorPurchaseLimits": true, "noVendorPurchaseLimits": true,
"noKimCooldowns": false,
"instantResourceExtractorDrones": false, "instantResourceExtractorDrones": false,
"noDojoRoomBuildStage": false, "noDojoRoomBuildStage": false,
"noDecoBuildStage": false, "noDecoBuildStage": false,

8
package-lock.json generated
View File

@ -18,7 +18,7 @@
"morgan": "^1.10.0", "morgan": "^1.10.0",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"typescript": "^5.5", "typescript": "^5.5",
"warframe-public-export-plus": "^0.5.53", "warframe-public-export-plus": "^0.5.54",
"warframe-riven-info": "^0.1.2", "warframe-riven-info": "^0.1.2",
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0" "winston-daily-rotate-file": "^5.0.0"
@ -3789,9 +3789,9 @@
} }
}, },
"node_modules/warframe-public-export-plus": { "node_modules/warframe-public-export-plus": {
"version": "0.5.53", "version": "0.5.54",
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.53.tgz", "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.54.tgz",
"integrity": "sha512-FjYeCJ5OxvPWyETnV33YOeX7weVVeMy451RY7uewwSvRbSNFTDhmhvbrLhfwykulUX4RPakfZr8nO0S0a6lGCA==" "integrity": "sha512-27r6qLErr3P8UVDiEzhDAs/BjdAS3vI2CQ58jSI+LClDlj6QL+y1jQe8va/npl3Ft2K8PywLkZ8Yso0j9YzvOA=="
}, },
"node_modules/warframe-riven-info": { "node_modules/warframe-riven-info": {
"version": "0.1.2", "version": "0.1.2",

View File

@ -25,7 +25,7 @@
"morgan": "^1.10.0", "morgan": "^1.10.0",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"typescript": "^5.5", "typescript": "^5.5",
"warframe-public-export-plus": "^0.5.53", "warframe-public-export-plus": "^0.5.54",
"warframe-riven-info": "^0.1.2", "warframe-riven-info": "^0.1.2",
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0" "winston-daily-rotate-file": "^5.0.0"

View File

@ -4,7 +4,7 @@
const fs = require("fs"); const fs = require("fs");
function extractStrings(content) { function extractStrings(content) {
const regex = /([a-zA-Z_]+): `([^`]*)`,/g; const regex = /([a-zA-Z0-9_]+): `([^`]*)`,/g;
let matches; let matches;
const strings = {}; const strings = {};
while ((matches = regex.exec(content)) !== null) { while ((matches = regex.exec(content)) !== null) {
@ -15,7 +15,7 @@ function extractStrings(content) {
const source = fs.readFileSync("../static/webui/translations/en.js", "utf8"); const source = fs.readFileSync("../static/webui/translations/en.js", "utf8");
const sourceStrings = extractStrings(source); const sourceStrings = extractStrings(source);
const sourceLines = source.split("\n"); const sourceLines = source.substring(0, source.length - 1).split("\n");
fs.readdirSync("../static/webui/translations").forEach(file => { fs.readdirSync("../static/webui/translations").forEach(file => {
if (fs.lstatSync(`../static/webui/translations/${file}`).isFile() && file !== "en.js") { if (fs.lstatSync(`../static/webui/translations/${file}`).isFile() && file !== "en.js") {
@ -36,7 +36,7 @@ fs.readdirSync("../static/webui/translations").forEach(file => {
fs.writeSync(fileHandle, ` ${key}: \`[UNTRANSLATED] ${value}\`,\n`); fs.writeSync(fileHandle, ` ${key}: \`[UNTRANSLATED] ${value}\`,\n`);
} }
}); });
} else if (line.length) { } else {
fs.writeSync(fileHandle, line + "\n"); fs.writeSync(fileHandle, line + "\n");
} }
}); });

View File

@ -16,9 +16,9 @@ import { webuiRouter } from "@/src/routes/webui";
const app = express(); const app = express();
app.use((req, _res, next) => { app.use((req, _res, next) => {
// 38.5.0 introduced "ezip" for encrypted body blobs. // 38.5.0 introduced "ezip" for encrypted body blobs and "e" for request verification only (encrypted body blobs with no application data).
// The bootstrapper decrypts it for us but having an unsupported Content-Encoding here would still be an issue for Express, so removing it. // The bootstrapper decrypts it for us but having an unsupported Content-Encoding here would still be an issue for Express, so removing it.
if (req.headers["content-encoding"] == "ezip") { if (req.headers["content-encoding"] == "ezip" || req.headers["content-encoding"] == "e") {
req.headers["content-encoding"] = undefined; req.headers["content-encoding"] = undefined;
} }
next(); next();

View File

@ -32,11 +32,11 @@ import { logger } from "@/src/utils/logger";
export const guildTechController: RequestHandler = async (req, res) => { export const guildTechController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const guild = await getGuildForRequestEx(req, inventory);
const data = JSON.parse(String(req.body)) as TGuildTechRequest; const data = JSON.parse(String(req.body)) as TGuildTechRequest;
if (data.Action == "Sync") { if (data.Action == "Sync") {
let needSave = false; let needSave = false;
const techProjects: ITechProjectClient[] = []; const techProjects: ITechProjectClient[] = [];
const guild = await getGuildForRequestEx(req, inventory);
if (guild.TechProjects) { if (guild.TechProjects) {
for (const project of guild.TechProjects) { for (const project of guild.TechProjects) {
const techProject: ITechProjectClient = { const techProject: ITechProjectClient = {
@ -59,6 +59,8 @@ export const guildTechController: RequestHandler = async (req, res) => {
} }
res.json({ TechProjects: techProjects }); res.json({ TechProjects: techProjects });
} else if (data.Action == "Start") { } else if (data.Action == "Start") {
if (data.Mode == "Guild") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end(); res.status(400).send("-1").end();
return; return;
@ -89,38 +91,94 @@ export const guildTechController: RequestHandler = async (req, res) => {
} }
await guild.save(); await guild.save();
res.end(); res.end();
} else {
const recipe = ExportDojoRecipes.research[data.RecipeType];
const techProject =
inventory.PersonalTechProjects[
inventory.PersonalTechProjects.push({
State: 0,
ReqCredits: recipe.price,
ItemType: data.RecipeType,
ReqItems: recipe.ingredients
}) - 1
];
await inventory.save();
res.json({
isPersonal: true,
action: "Start",
personalTech: techProject.toJSON()
});
}
} else if (data.Action == "Contribute") { } else if (data.Action == "Contribute") {
if ((req.query.guildId as string) == "000000000000000000000000") {
const techProject = inventory.PersonalTechProjects.id(data.ResearchId)!;
techProject.ReqCredits -= data.RegularCredits;
const inventoryChanges: IInventoryChanges = updateCurrency(inventory, data.RegularCredits, false);
const miscItemChanges = [];
for (const miscItem of data.MiscItems) {
const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
if (reqItem) {
if (miscItem.ItemCount > reqItem.ItemCount) {
miscItem.ItemCount = reqItem.ItemCount;
}
reqItem.ItemCount -= miscItem.ItemCount;
miscItemChanges.push({
ItemType: miscItem.ItemType,
ItemCount: miscItem.ItemCount * -1
});
}
}
addMiscItems(inventory, miscItemChanges);
inventoryChanges.MiscItems = miscItemChanges;
techProject.HasContributions = true;
if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) {
techProject.State = 1;
const recipe = ExportDojoRecipes.research[techProject.ItemType];
techProject.CompletionDate = new Date(Date.now() + recipe.time * 1000);
}
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
PersonalResearch: { $oid: data.ResearchId },
PersonalResearchDate: techProject.CompletionDate ? toMongoDate(techProject.CompletionDate) : undefined
});
} else {
if (!hasAccessToDojo(inventory)) { if (!hasAccessToDojo(inventory)) {
res.status(400).send("-1").end(); res.status(400).send("-1").end();
return; return;
} }
const guild = await getGuildForRequestEx(req, inventory);
const guildMember = (await GuildMember.findOne( const guildMember = (await GuildMember.findOne(
{ accountId, guildId: guild._id }, { accountId, guildId: guild._id },
"RegularCreditsContributed MiscItemsContributed" "RegularCreditsContributed MiscItemsContributed"
))!; ))!;
const contributions = data; const techProject = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!;
const techProject = guild.TechProjects!.find(x => x.ItemType == contributions.RecipeType)!;
if (contributions.VaultCredits) { if (data.VaultCredits) {
if (contributions.VaultCredits > techProject.ReqCredits) { if (data.VaultCredits > techProject.ReqCredits) {
contributions.VaultCredits = techProject.ReqCredits; data.VaultCredits = techProject.ReqCredits;
} }
techProject.ReqCredits -= contributions.VaultCredits; techProject.ReqCredits -= data.VaultCredits;
guild.VaultRegularCredits! -= contributions.VaultCredits; guild.VaultRegularCredits! -= data.VaultCredits;
} }
if (contributions.RegularCredits > techProject.ReqCredits) { if (data.RegularCredits > techProject.ReqCredits) {
contributions.RegularCredits = techProject.ReqCredits; data.RegularCredits = techProject.ReqCredits;
} }
techProject.ReqCredits -= contributions.RegularCredits; techProject.ReqCredits -= data.RegularCredits;
guildMember.RegularCreditsContributed ??= 0; guildMember.RegularCreditsContributed ??= 0;
guildMember.RegularCreditsContributed += contributions.RegularCredits; guildMember.RegularCreditsContributed += data.RegularCredits;
if (contributions.VaultMiscItems.length) { if (data.VaultMiscItems.length) {
for (const miscItem of contributions.VaultMiscItems) { for (const miscItem of data.VaultMiscItems) {
const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
if (reqItem) { if (reqItem) {
if (miscItem.ItemCount > reqItem.ItemCount) { if (miscItem.ItemCount > reqItem.ItemCount) {
@ -135,7 +193,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
} }
const miscItemChanges = []; const miscItemChanges = [];
for (const miscItem of contributions.MiscItems) { for (const miscItem of data.MiscItems) {
const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
if (reqItem) { if (reqItem) {
if (miscItem.ItemCount > reqItem.ItemCount) { if (miscItem.ItemCount > reqItem.ItemCount) {
@ -151,7 +209,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
} }
} }
addMiscItems(inventory, miscItemChanges); addMiscItems(inventory, miscItemChanges);
const inventoryChanges: IInventoryChanges = updateCurrency(inventory, contributions.RegularCredits, false); const inventoryChanges: IInventoryChanges = updateCurrency(inventory, data.RegularCredits, false);
inventoryChanges.MiscItems = miscItemChanges; inventoryChanges.MiscItems = miscItemChanges;
// Check if research is fully funded now. // Check if research is fully funded now.
@ -162,7 +220,9 @@ export const guildTechController: RequestHandler = async (req, res) => {
InventoryChanges: inventoryChanges, InventoryChanges: inventoryChanges,
Vault: getGuildVault(guild) Vault: getGuildVault(guild)
}); });
}
} else if (data.Action.split(",")[0] == "Buy") { } else if (data.Action.split(",")[0] == "Buy") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) { if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) {
res.status(400).send("-1").end(); res.status(400).send("-1").end();
return; return;
@ -190,6 +250,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
} }
}); });
} else if (data.Action == "Fabricate") { } else if (data.Action == "Fabricate") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) { if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) {
res.status(400).send("-1").end(); res.status(400).send("-1").end();
return; return;
@ -206,6 +267,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
// Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`. // Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`.
res.json({ inventoryChanges: inventoryChanges }); res.json({ inventoryChanges: inventoryChanges });
} else if (data.Action == "Pause") { } else if (data.Action == "Pause") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end(); res.status(400).send("-1").end();
return; return;
@ -217,6 +279,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
await removePigmentsFromGuildMembers(guild._id); await removePigmentsFromGuildMembers(guild._id);
res.end(); res.end();
} else if (data.Action == "Unpause") { } else if (data.Action == "Unpause") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end(); res.status(400).send("-1").end();
return; return;
@ -239,7 +302,7 @@ type TGuildTechRequest =
interface IGuildTechBasicRequest { interface IGuildTechBasicRequest {
Action: "Start" | "Fabricate" | "Pause" | "Unpause"; Action: "Start" | "Fabricate" | "Pause" | "Unpause";
Mode: "Guild"; Mode: "Guild" | "Personal";
RecipeType: string; RecipeType: string;
} }
@ -251,7 +314,7 @@ interface IGuildTechBuyRequest {
interface IGuildTechContributeRequest { interface IGuildTechContributeRequest {
Action: "Contribute"; Action: "Contribute";
ResearchId: ""; ResearchId: string;
RecipeType: string; RecipeType: string;
RegularCredits: number; RegularCredits: number;
MiscItems: IMiscItem[]; MiscItems: IMiscItem[];

View File

@ -202,7 +202,8 @@ export const getInventoryResponse = async (
if (config.universalPolarityEverywhere) { if (config.universalPolarityEverywhere) {
const Polarity: IPolarity[] = []; const Polarity: IPolarity[] = [];
for (let i = 0; i != 12; ++i) { // 12 is needed for necramechs. 14 is needed for plexus/crewshipharness.
for (let i = 0; i != 14; ++i) {
Polarity.push({ Polarity.push({
Slot: i, Slot: i,
Value: ArtifactPolarity.Any Value: ArtifactPolarity.Any

View File

@ -1,10 +1,25 @@
import { getInfNodes, getNemesisPasscode } from "@/src/helpers/nemesisHelpers"; import {
consumeModCharge,
encodeNemesisGuess,
getInfNodes,
getNemesisPasscode,
IKnifeResponse
} from "@/src/helpers/nemesisHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
import { freeUpSlot, getInventory } from "@/src/services/inventoryService"; import { freeUpSlot, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { SRng } from "@/src/services/rngService"; import { SRng } from "@/src/services/rngService";
import { IMongoDate, IOid } from "@/src/types/commonTypes"; import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { IInnateDamageFingerprint, InventorySlot, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import {
IInnateDamageFingerprint,
InventorySlot,
IUpgradeClient,
IWeaponSkinClient,
LoadoutIndex,
TEquipmentKey
} from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
@ -49,7 +64,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
} else if ((req.query.mode as string) == "p") { } else if ((req.query.mode as string) == "p") {
const inventory = await getInventory(accountId, "Nemesis"); const inventory = await getInventory(accountId, "Nemesis");
const body = getJSONfromString<INemesisPrespawnCheckRequest>(String(req.body)); const body = getJSONfromString<INemesisPrespawnCheckRequest>(String(req.body));
const passcode = getNemesisPasscode(inventory.Nemesis!.fp, inventory.Nemesis!.Faction); const passcode = getNemesisPasscode(inventory.Nemesis!);
let guessResult = 0; let guessResult = 0;
if (inventory.Nemesis!.Faction == "FC_INFESTATION") { if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
for (let i = 0; i != 3; ++i) { for (let i = 0; i != 3; ++i) {
@ -66,6 +81,88 @@ export const nemesisController: RequestHandler = async (req, res) => {
} }
} }
res.json({ GuessResult: guessResult }); res.json({ GuessResult: guessResult });
} else if (req.query.mode == "r") {
const inventory = await getInventory(
accountId,
"Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades"
);
const body = getJSONfromString<INemesisRequiemRequest>(String(req.body));
if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
const guess: number[] = [body.guess & 0xf, (body.guess >> 4) & 0xf, (body.guess >> 8) & 0xf];
const passcode = getNemesisPasscode(inventory.Nemesis!)[0];
// Add to GuessHistory
const result1 = passcode == guess[0] ? 0 : 1;
const result2 = passcode == guess[1] ? 0 : 1;
const result3 = passcode == guess[2] ? 0 : 1;
inventory.Nemesis!.GuessHistory.push(
encodeNemesisGuess(guess[0], result1, guess[1], result2, guess[2], result3)
);
// Increase antivirus
let antivirusGain = 5;
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid);
const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0;
const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!;
const response: IKnifeResponse = {};
for (const upgrade of body.knife!.AttachedUpgrades) {
switch (upgrade.ItemType) {
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod":
antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod":
antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod": // Instant Secure
antivirusGain += 15;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod": // Immuno Shield
antivirusGain += 15;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod":
antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
}
}
inventory.Nemesis!.HenchmenKilled += antivirusGain;
if (inventory.Nemesis!.HenchmenKilled >= 100) {
inventory.Nemesis!.HenchmenKilled = 100;
inventory.Nemesis!.InfNodes = [
{
Node: "CrewBattleNode559",
Influence: 1
}
];
inventory.Nemesis!.Weakened = true;
} else {
inventory.Nemesis!.InfNodes = getInfNodes("FC_INFESTATION", 0);
}
await inventory.save();
res.json(response);
} else {
const passcode = getNemesisPasscode(inventory.Nemesis!);
if (passcode[body.position] != body.guess) {
res.end();
} else {
inventory.Nemesis!.Rank += 1;
inventory.Nemesis!.InfNodes = getInfNodes(inventory.Nemesis!.Faction, inventory.Nemesis!.Rank);
await inventory.save();
res.json({ RankIncrease: 1 });
}
}
} else if ((req.query.mode as string) == "rs") {
// report spawn; POST but no application data in body
const inventory = await getInventory(accountId, "Nemesis");
inventory.Nemesis!.LastEnc = inventory.Nemesis!.MissionCount;
await inventory.save();
res.json({ LastEnc: inventory.Nemesis!.LastEnc });
} else if ((req.query.mode as string) == "s") { } else if ((req.query.mode as string) == "s") {
const inventory = await getInventory(accountId, "Nemesis"); const inventory = await getInventory(accountId, "Nemesis");
const body = getJSONfromString<INemesisStartRequest>(String(req.body)); const body = getJSONfromString<INemesisStartRequest>(String(req.body));
@ -173,6 +270,20 @@ interface INemesisPrespawnCheckRequest {
potency?: number[]; potency?: number[];
} }
interface INemesisRequiemRequest {
guess: number; // grn/crp: 4 bits | coda: 3x 4 bits
position: number; // grn/crp: 0-2 | coda: 0
// knife field provided for coda only
knife?: {
Item: IEquipmentClient;
Skins: IWeaponSkinClient[];
ModSlot: number;
CustSlot: number;
AttachedUpgrades: IUpgradeClient[];
HiddenWhenHolstered: boolean;
};
}
const kuvaLichVersionSixWeapons = [ const kuvaLichVersionSixWeapons = [
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon", "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak", "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak",

View File

@ -1,4 +1,5 @@
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { config } from "@/src/services/configService";
import { addEmailItem, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { addEmailItem, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { ICompletedDialogue, IDialogueDatabase } from "@/src/types/inventoryTypes/inventoryTypes"; import { ICompletedDialogue, IDialogueDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
@ -24,7 +25,9 @@ export const saveDialogueController: RequestHandler = async (req, res) => {
throw new Error("bad inventory state"); throw new Error("bad inventory state");
} }
const inventoryChanges: IInventoryChanges = {}; const inventoryChanges: IInventoryChanges = {};
const tomorrowAt0Utc = (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000; const tomorrowAt0Utc = config.noKimCooldowns
? Date.now()
: (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000;
inventory.DialogueHistory.Dialogues ??= []; inventory.DialogueHistory.Dialogues ??= [];
const dialogue = getDialogue(inventory, request.DialogueName); const dialogue = getDialogue(inventory, request.DialogueName);
dialogue.Rank = request.Rank; dialogue.Rank = request.Rank;

View File

@ -3,15 +3,9 @@ import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { ExportNightwave, ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus"; import { ExportNightwave, ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService";
addItem,
addMiscItems,
combineInventoryChanges,
getInventory,
updateCurrency
} from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { fromStoreItem, isStoreItem } from "@/src/services/itemDataService"; import { isStoreItem, toStoreItem } from "@/src/services/itemDataService";
export const syndicateSacrificeController: RequestHandler = async (request, response) => { export const syndicateSacrificeController: RequestHandler = async (request, response) => {
const accountId = await getAccountIdForRequest(request); const accountId = await getAccountIdForRequest(request);
@ -77,10 +71,13 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
res.NewEpisodeReward = true; res.NewEpisodeReward = true;
const reward = ExportNightwave.rewards[index]; const reward = ExportNightwave.rewards[index];
let rewardType = reward.uniqueName; let rewardType = reward.uniqueName;
if (isStoreItem(rewardType)) { if (!isStoreItem(rewardType)) {
rewardType = fromStoreItem(rewardType); rewardType = toStoreItem(rewardType);
} }
combineInventoryChanges(res.InventoryChanges, await addItem(inventory, rewardType, reward.itemCount ?? 1)); combineInventoryChanges(
res.InventoryChanges,
(await handleStoreItemAcquisition(rewardType, inventory, reward.itemCount)).InventoryChanges
);
} }
} }

View File

@ -1,6 +1,11 @@
import { ExportRegions } from "warframe-public-export-plus"; import { ExportRegions } from "warframe-public-export-plus";
import { IInfNode } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInfNode } from "@/src/types/inventoryTypes/inventoryTypes";
import { SRng } from "@/src/services/rngService"; import { SRng } from "@/src/services/rngService";
import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel";
import { logger } from "../utils/logger";
import { IOid } from "../types/commonTypes";
import { Types } from "mongoose";
import { addMods } from "../services/inventoryService";
export const getInfNodes = (faction: string, rank: number): IInfNode[] => { export const getInfNodes = (faction: string, rank: number): IInfNode[] => {
const infNodes = []; const infNodes = [];
@ -33,12 +38,93 @@ const systemIndexes: Record<string, number[]> = {
}; };
// Get a parazon 'passcode' based on the nemesis fingerprint so it's always the same for the same nemesis. // Get a parazon 'passcode' based on the nemesis fingerprint so it's always the same for the same nemesis.
export const getNemesisPasscode = (fp: bigint, faction: string): number[] => { export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: string }): number[] => {
const rng = new SRng(fp); const rng = new SRng(nemesis.fp);
const passcode = [rng.randomInt(0, 7)]; const passcode = [rng.randomInt(0, 7)];
if (faction != "FC_INFESTATION") { if (nemesis.Faction != "FC_INFESTATION") {
passcode.push(rng.randomInt(0, 7)); passcode.push(rng.randomInt(0, 7));
passcode.push(rng.randomInt(0, 7)); passcode.push(rng.randomInt(0, 7));
} }
return passcode; return passcode;
}; };
export const encodeNemesisGuess = (
symbol1: number,
result1: number,
symbol2: number,
result2: number,
symbol3: number,
result3: number
): number => {
return (
(symbol1 & 0xf) |
((result1 & 3) << 12) |
((symbol2 << 4) & 0xff) |
((result2 << 14) & 0xffff) |
((symbol3 & 0xf) << 8) |
((result3 & 3) << 16)
);
};
export const decodeNemesisGuess = (val: number): number[] => {
return [val & 0xf, (val >> 12) & 3, (val & 0xff) >> 4, (val & 0xffff) >> 14, (val >> 8) & 0xf, (val >> 16) & 3];
};
export interface IKnifeResponse {
UpgradeIds?: string[];
UpgradeTypes?: string[];
UpgradeFingerprints?: { lvl: number }[];
UpgradeNew?: boolean[];
HasKnife?: boolean;
}
export const consumeModCharge = (
response: IKnifeResponse,
inventory: TInventoryDatabaseDocument,
upgrade: { ItemId: IOid; ItemType: string },
dataknifeUpgrades: string[]
): void => {
response.UpgradeIds ??= [];
response.UpgradeTypes ??= [];
response.UpgradeFingerprints ??= [];
response.UpgradeNew ??= [];
response.HasKnife = true;
if (upgrade.ItemId.$oid != "000000000000000000000000") {
const dbUpgrade = inventory.Upgrades.id(upgrade.ItemId.$oid)!;
const fingerprint = JSON.parse(dbUpgrade.UpgradeFingerprint!) as { lvl: number };
fingerprint.lvl += 1;
dbUpgrade.UpgradeFingerprint = JSON.stringify(fingerprint);
response.UpgradeIds.push(upgrade.ItemId.$oid);
response.UpgradeTypes.push(upgrade.ItemType);
response.UpgradeFingerprints.push(fingerprint);
response.UpgradeNew.push(false);
} else {
const id = new Types.ObjectId();
inventory.Upgrades.push({
_id: id,
ItemType: upgrade.ItemType,
UpgradeFingerprint: `{"lvl":1}`
});
addMods(inventory, [
{
ItemType: upgrade.ItemType,
ItemCount: -1
}
]);
const dataknifeRawUpgradeIndex = dataknifeUpgrades.indexOf(upgrade.ItemType);
if (dataknifeRawUpgradeIndex != -1) {
dataknifeUpgrades[dataknifeRawUpgradeIndex] = id.toString();
} else {
logger.warn(`${upgrade.ItemType} not found in dataknife config`);
}
response.UpgradeIds.push(id.toString());
response.UpgradeTypes.push(upgrade.ItemType);
response.UpgradeFingerprints.push({ lvl: 1 });
response.UpgradeNew.push(true);
}
};

View File

@ -84,7 +84,9 @@ import {
IInfNode, IInfNode,
IDiscoveredMarker, IDiscoveredMarker,
IWeeklyMission, IWeeklyMission,
ILockedWeaponGroupDatabase ILockedWeaponGroupDatabase,
IPersonalTechProjectDatabase,
IPersonalTechProjectClient
} from "../../types/inventoryTypes/inventoryTypes"; } from "../../types/inventoryTypes/inventoryTypes";
import { IOid } from "../../types/commonTypes"; import { IOid } from "../../types/commonTypes";
import { import {
@ -498,7 +500,34 @@ const seasonChallengeHistorySchema = new Schema<ISeasonChallenge>(
{ _id: false } { _id: false }
); );
//TODO: check whether this is complete const personalTechProjectSchema = new Schema<IPersonalTechProjectDatabase>({
State: Number,
ReqCredits: Number,
ItemType: String,
ReqItems: { type: [typeCountSchema], default: undefined },
HasContributions: Boolean,
CompletionDate: Date
});
personalTechProjectSchema.virtual("ItemId").get(function () {
return { $oid: this._id.toString() };
});
personalTechProjectSchema.set("toJSON", {
virtuals: true,
transform(_doc, ret, _options) {
delete ret._id;
delete ret.__v;
const db = ret as IPersonalTechProjectDatabase;
const client = ret as IPersonalTechProjectClient;
if (db.CompletionDate) {
client.CompletionDate = toMongoDate(db.CompletionDate);
}
}
});
const playerSkillsSchema = new Schema<IPlayerSkills>( const playerSkillsSchema = new Schema<IPlayerSkills>(
{ {
LPP_SPACE: { type: Number, default: 0 }, LPP_SPACE: { type: Number, default: 0 },
@ -1442,7 +1471,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//Railjack craft //Railjack craft
//https://warframe.fandom.com/wiki/Rising_Tide //https://warframe.fandom.com/wiki/Rising_Tide
PersonalTechProjects: [Schema.Types.Mixed], PersonalTechProjects: { type: [personalTechProjectSchema], default: [] },
//Modulars lvl and exp(Railjack|Duviri) //Modulars lvl and exp(Railjack|Duviri)
//https://warframe.fandom.com/wiki/Intrinsics //https://warframe.fandom.com/wiki/Intrinsics
@ -1471,7 +1500,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
DuviriInfo: DuviriInfoSchema, DuviriInfo: DuviriInfoSchema,
Mailbox: MailboxSchema, Mailbox: MailboxSchema,
HandlerPoints: Number, HandlerPoints: Number,
ChallengesFixVersion: Number, ChallengesFixVersion: { type: Number, default: 6 },
PlayedParkourTutorial: Boolean, PlayedParkourTutorial: Boolean,
ActiveLandscapeTraps: [Schema.Types.Mixed], ActiveLandscapeTraps: [Schema.Types.Mixed],
RepVotes: [Schema.Types.Mixed], RepVotes: [Schema.Types.Mixed],
@ -1585,6 +1614,7 @@ export type InventoryDocumentProps = {
Drones: Types.DocumentArray<IDroneDatabase>; Drones: Types.DocumentArray<IDroneDatabase>;
CrewShipWeaponSkins: Types.DocumentArray<IUpgradeDatabase>; CrewShipWeaponSkins: Types.DocumentArray<IUpgradeDatabase>;
CrewShipSalvagedWeaponsSkins: Types.DocumentArray<IUpgradeDatabase>; CrewShipSalvagedWeaponsSkins: Types.DocumentArray<IUpgradeDatabase>;
PersonalTechProjects: Types.DocumentArray<IPersonalTechProjectDatabase>;
} & { [K in TEquipmentKey]: Types.DocumentArray<IEquipmentDatabase> }; } & { [K in TEquipmentKey]: Types.DocumentArray<IEquipmentDatabase> };
// eslint-disable-next-line @typescript-eslint/no-empty-object-type // eslint-disable-next-line @typescript-eslint/no-empty-object-type

View File

@ -38,6 +38,7 @@ interface IConfig {
noArgonCrystalDecay?: boolean; noArgonCrystalDecay?: boolean;
noMasteryRankUpCooldown?: boolean; noMasteryRankUpCooldown?: boolean;
noVendorPurchaseLimits?: boolean; noVendorPurchaseLimits?: boolean;
noKimCooldowns?: boolean;
instantResourceExtractorDrones?: boolean; instantResourceExtractorDrones?: boolean;
noDojoRoomBuildStage?: boolean; noDojoRoomBuildStage?: boolean;
noDojoDecoBuildStage?: boolean; noDojoDecoBuildStage?: boolean;

View File

@ -963,7 +963,7 @@ export const addStanding = (
const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0); const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0);
if (syndicate.Standing + gainedStanding > max) gainedStanding = max - syndicate.Standing; if (syndicate.Standing + gainedStanding > max) gainedStanding = max - syndicate.Standing;
if (!isMedallion || (isMedallion && syndicateMeta.medallionsCappedByDailyLimit)) { if (!isMedallion || syndicateMeta.medallionsCappedByDailyLimit) {
if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) { if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) {
gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin); gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin);
} }
@ -996,6 +996,10 @@ export const updateGeneric = async (data: IGenericUpdate, accountId: string): Pr
} }
]; ];
addMiscItems(inventory, inventoryChanges.MiscItems); addMiscItems(inventory, inventoryChanges.MiscItems);
} else if (node == "BeatCaliberChicks") {
await addEmailItem(inventory, "/Lotus/Types/Items/EmailItems/BeatCaliberChicksEmailItem", inventoryChanges);
} else if (node == "ClearedFiveLoops") {
await addEmailItem(inventory, "/Lotus/Types/Items/EmailItems/ClearedFiveLoopsEmailItem", inventoryChanges);
} }
} }

View File

@ -185,13 +185,14 @@ export const getKeyChainMessage = ({ KeyChain, ChainStage }: IKeyChainRequest):
throw new Error(`KeyChain ${KeyChain} does not contain chain stages`); throw new Error(`KeyChain ${KeyChain} does not contain chain stages`);
} }
const keyChainStage = chainStages[ChainStage]; let i = ChainStage;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition let chainStageMessage = chainStages[i].messageToSendWhenTriggered;
if (!keyChainStage) { while (!chainStageMessage) {
throw new Error(`KeyChainStage ${ChainStage} not found`); if (++i >= chainStages.length) {
break;
}
chainStageMessage = chainStages[i].messageToSendWhenTriggered;
} }
const chainStageMessage = keyChainStage.messageToSendWhenTriggered;
if (!chainStageMessage) { if (!chainStageMessage) {
throw new Error( throw new Error(

View File

@ -699,25 +699,10 @@ export const addMissionRewards = async (
} }
if (inventory.Nemesis.Faction == "FC_INFESTATION") { if (inventory.Nemesis.Faction == "FC_INFESTATION") {
inventoryChanges.Nemesis.HenchmenKilled ??= 0;
inventoryChanges.Nemesis.MissionCount ??= 0;
inventory.Nemesis.HenchmenKilled += 5;
inventory.Nemesis.MissionCount += 1; inventory.Nemesis.MissionCount += 1;
inventoryChanges.Nemesis.HenchmenKilled += 5; inventoryChanges.Nemesis.MissionCount ??= 0;
inventoryChanges.Nemesis.MissionCount += 1; inventoryChanges.Nemesis.MissionCount += 1;
if (inventory.Nemesis.HenchmenKilled >= 100) {
inventory.Nemesis.InfNodes = [
{
Node: "CrewBattleNode559",
Influence: 1
}
];
inventory.Nemesis.Weakened = true;
inventoryChanges.Nemesis.Weakened = true;
}
} }
inventoryChanges.Nemesis.InfNodes = inventory.Nemesis.InfNodes; inventoryChanges.Nemesis.InfNodes = inventory.Nemesis.InfNodes;
@ -747,7 +732,7 @@ export const addMissionRewards = async (
const endlessJob = syndicateEntry.Jobs.find(j => j.endless); const endlessJob = syndicateEntry.Jobs.find(j => j.endless);
if (endlessJob) { if (endlessJob) {
const index = rewardInfo.JobStage % endlessJob.xpAmounts.length; const index = rewardInfo.JobStage % endlessJob.xpAmounts.length;
const excess = Math.floor(rewardInfo.JobStage / endlessJob.xpAmounts.length); const excess = Math.floor(rewardInfo.JobStage / (endlessJob.xpAmounts.length - 1));
medallionAmount = Math.floor(endlessJob.xpAmounts[index] * (1 + 0.15000001 * excess)); medallionAmount = Math.floor(endlessJob.xpAmounts[index] * (1 + 0.15000001 * excess));
} }
} }
@ -922,15 +907,140 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u
let rotations: number[] = []; let rotations: number[] = [];
if (RewardInfo.jobId) { if (RewardInfo.jobId) {
if (RewardInfo.JobTier! >= 0) { if (RewardInfo.JobStage! >= 0) {
const id = RewardInfo.jobId.split("_")[3]; // eslint-disable-next-line @typescript-eslint/no-unused-vars
const syndicateInfo = getWorldState().SyndicateMissions.find(x => x._id.$oid == id); const [jobType, tierStr, hubNode, syndicateId, locationTag] = RewardInfo.jobId.split("_");
if (syndicateInfo) { const tier = Number(tierStr);
const jobInfo = syndicateInfo.Jobs![RewardInfo.JobTier!]; let isEndlessJob = false;
rewardManifests = [jobInfo.rewards]; if (syndicateId) {
rotations = [RewardInfo.JobStage!]; const worldState = getWorldState();
let syndicateEntry = worldState.SyndicateMissions.find(m => m._id.$oid === syndicateId);
if (!syndicateEntry) syndicateEntry = worldState.SyndicateMissions.find(m => m.Tag === syndicateId);
if (syndicateEntry && syndicateEntry.Jobs) {
let job = syndicateEntry.Jobs[tier];
if (syndicateEntry.Tag === "EntratiSyndicate") {
const vault = syndicateEntry.Jobs.find(j => j.locationTag === locationTag);
if (vault) job = vault;
// if (
// [
// "DeimosRuinsExterminateBounty",
// "DeimosRuinsEscortBounty",
// "DeimosRuinsMistBounty",
// "DeimosRuinsPurifyBounty",
// "DeimosRuinsSacBounty"
// ].some(ending => jobType.endsWith(ending))
// ) {
// job.rewards = "TODO"; // Droptable for Arcana Isolation Vault
// }
if (
[
"DeimosEndlessAreaDefenseBounty",
"DeimosEndlessExcavateBounty",
"DeimosEndlessPurifyBounty"
].some(ending => jobType.endsWith(ending))
) {
const endlessJob = syndicateEntry.Jobs.find(j => j.endless);
if (endlessJob) {
isEndlessJob = true;
job = endlessJob;
const excess = Math.floor(RewardInfo.JobStage! / (job.xpAmounts.length - 1));
const rotationIndexes = [0, 0, 1, 2];
const rotationIndex = rotationIndexes[excess % rotationIndexes.length];
const dropTable = [
"/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTableARewards",
"/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTableBRewards",
"/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTableCRewards"
];
job.rewards = dropTable[rotationIndex];
} }
} }
} else if (syndicateEntry.Tag === "SolarisSyndicate") {
if (jobType.endsWith("Heists/HeistProfitTakerBountyOne") && RewardInfo.JobStage == 2) {
job = {
rewards:
"/Lotus/Types/Game/MissionDecks/HeistJobMissionRewards/HeistTierATableARewards",
masteryReq: 0,
minEnemyLevel: 40,
maxEnemyLevel: 60,
xpAmounts: [1000]
};
RewardInfo.Q = false; // Just in case
} else {
const tierMap = {
Two: "B",
Three: "C",
Four: "D"
};
for (const [key, tier] of Object.entries(tierMap)) {
if (jobType.endsWith(`Heists/HeistProfitTakerBounty${key}`)) {
job = {
rewards: `/Lotus/Types/Game/MissionDecks/HeistJobMissionRewards/HeistTier${tier}TableARewards`,
masteryReq: 0,
minEnemyLevel: 40,
maxEnemyLevel: 60,
xpAmounts: [1000]
};
RewardInfo.Q = false; // Just in case
break;
}
}
}
}
rewardManifests = [job.rewards];
rotations = [RewardInfo.JobStage! % (job.xpAmounts.length - 1)];
if (
RewardInfo.Q &&
(RewardInfo.JobStage === job.xpAmounts.length - 1 || job.isVault) &&
!isEndlessJob
) {
rewardManifests.push(job.rewards);
rotations.push(ExportRewards[job.rewards].length - 1);
}
}
}
}
} else if (RewardInfo.challengeMissionId) {
const rewardTables: Record<string, string[]> = {
EntratiLabSyndicate: [
"/Lotus/Types/Game/MissionDecks/EntratiLabJobMissionReward/TierATableRewards",
"/Lotus/Types/Game/MissionDecks/EntratiLabJobMissionReward/TierBTableRewards",
"/Lotus/Types/Game/MissionDecks/EntratiLabJobMissionReward/TierCTableRewards",
"/Lotus/Types/Game/MissionDecks/EntratiLabJobMissionReward/TierDTableRewards",
"/Lotus/Types/Game/MissionDecks/EntratiLabJobMissionReward/TierETableRewards"
],
ZarimanSyndicate: [
"/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierATableRewards",
"/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierBTableRewards",
"/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierCTableRewards",
"/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierDTableRewards",
"/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierETableRewards"
],
HexSyndicate: [
"/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierABountyRewards",
"/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierBBountyRewards",
"/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierCBountyRewards",
"/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierDBountyRewards",
"/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierEBountyRewards",
"/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierFBountyRewards",
"/Lotus/Types/Game/MissionDecks/1999MissionRewards/InfestedLichBountyRewards"
]
};
const [syndicateTag, tierStr] = RewardInfo.challengeMissionId.split("_");
const tier = Number(tierStr);
const rewardTable = rewardTables[syndicateTag][tier];
if (rewardTable) {
rewardManifests = [rewardTable];
rotations = [0];
} else {
logger.error(`Unknown syndicate or tier: ${RewardInfo.challengeMissionId}`);
}
} else if (RewardInfo.VaultsCracked) { } else if (RewardInfo.VaultsCracked) {
// For Spy missions, e.g. 3 vaults cracked = A, B, C // For Spy missions, e.g. 3 vaults cracked = A, B, C
for (let i = 0; i != RewardInfo.VaultsCracked; ++i) { for (let i = 0; i != RewardInfo.VaultsCracked; ++i) {

View File

@ -46,6 +46,7 @@ export interface IInventoryDatabase
| "EntratiVaultCountResetDate" | "EntratiVaultCountResetDate"
| "BrandedSuits" | "BrandedSuits"
| "LockedWeaponGroup" | "LockedWeaponGroup"
| "PersonalTechProjects"
| TEquipmentKey | TEquipmentKey
>, >,
InventoryDatabaseEquipment { InventoryDatabaseEquipment {
@ -77,6 +78,7 @@ export interface IInventoryDatabase
EntratiVaultCountResetDate?: Date; EntratiVaultCountResetDate?: Date;
BrandedSuits?: Types.ObjectId[]; BrandedSuits?: Types.ObjectId[];
LockedWeaponGroup?: ILockedWeaponGroupDatabase; LockedWeaponGroup?: ILockedWeaponGroupDatabase;
PersonalTechProjects: IPersonalTechProjectDatabase[];
} }
export interface IQuestKeyDatabase { export interface IQuestKeyDatabase {
@ -157,6 +159,11 @@ export type TSolarMapRegion =
//TODO: perhaps split response and database into their own files //TODO: perhaps split response and database into their own files
export enum LoadoutIndex {
NORMAL = 0,
DATAKNIFE = 7
}
export interface IDailyAffiliations { export interface IDailyAffiliations {
DailyAffiliation: number; DailyAffiliation: number;
DailyAffiliationPvp: number; DailyAffiliationPvp: number;
@ -220,7 +227,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
ActiveQuest: string; ActiveQuest: string;
FlavourItems: IFlavourItem[]; FlavourItems: IFlavourItem[];
LoadOutPresets: ILoadOutPresets; LoadOutPresets: ILoadOutPresets;
CurrentLoadOutIds: IOid[]; //TODO: we store it in the database using this representation as well :/ CurrentLoadOutIds: IOid[]; // we store it in the database using this representation as well :/
Missions: IMission[]; Missions: IMission[];
RandomUpgradesIdentified?: number; RandomUpgradesIdentified?: number;
LastRegionPlayed: TSolarMapRegion; LastRegionPlayed: TSolarMapRegion;
@ -301,7 +308,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
NemesisHistory: INemesisBaseClient[]; NemesisHistory: INemesisBaseClient[];
LastNemesisAllySpawnTime?: IMongoDate; LastNemesisAllySpawnTime?: IMongoDate;
Settings?: ISettings; Settings?: ISettings;
PersonalTechProjects: IPersonalTechProject[]; PersonalTechProjects: IPersonalTechProjectClient[];
PlayerSkills: IPlayerSkills; PlayerSkills: IPlayerSkills;
CrewShipAmmo: ITypeCount[]; CrewShipAmmo: ITypeCount[];
CrewShipWeaponSkins: IUpgradeClient[]; CrewShipWeaponSkins: IUpgradeClient[];
@ -936,16 +943,20 @@ export interface IPersonalGoalProgress {
ReceivedClanReward1?: boolean; ReceivedClanReward1?: boolean;
} }
export interface IPersonalTechProject { export interface IPersonalTechProjectDatabase {
State: number; State: number;
ReqCredits: number; ReqCredits: number;
ItemType: string; ItemType: string;
ReqItems: ITypeCount[]; ReqItems: ITypeCount[];
HasContributions?: boolean;
CompletionDate?: Date;
}
export interface IPersonalTechProjectClient extends Omit<IPersonalTechProjectDatabase, "CompletionDate"> {
CompletionDate?: IMongoDate; CompletionDate?: IMongoDate;
ItemId: IOid;
ProductCategory?: string; ProductCategory?: string;
CategoryItemId?: IOid; CategoryItemId?: IOid;
HasContributions?: boolean; ItemId: IOid;
} }
export interface IPlayerSkills { export interface IPlayerSkills {

View File

@ -406,7 +406,10 @@
<div class="card mb-3"> <div class="card mb-3">
<h5 class="card-header" data-loc="powersuit_archonShardsLabel"></h5> <h5 class="card-header" data-loc="powersuit_archonShardsLabel"></h5>
<div class="card-body"> <div class="card-body">
<p data-loc="powersuit_archonShardsDescription"></p> <p>
<span data-loc="powersuit_archonShardsDescription"></span>
<span data-loc="powersuit_archonShardsDescription2"></span>
</p>
<form class="input-group mb-3" onsubmit="doPushArchonCrystalUpgrade();return false;"> <form class="input-group mb-3" onsubmit="doPushArchonCrystalUpgrade();return false;">
<input type="number" id="archon-crystal-add-count" min="1" max="10000" value="1" class="form-control" style="max-width:100px" /> <input type="number" id="archon-crystal-add-count" min="1" max="10000" value="1" class="form-control" style="max-width:100px" />
<span class="input-group-text">x</span> <span class="input-group-text">x</span>
@ -604,6 +607,10 @@
<input class="form-check-input" type="checkbox" id="noVendorPurchaseLimits" /> <input class="form-check-input" type="checkbox" id="noVendorPurchaseLimits" />
<label class="form-check-label" for="noVendorPurchaseLimits" data-loc="cheats_noVendorPurchaseLimits"></label> <label class="form-check-label" for="noVendorPurchaseLimits" data-loc="cheats_noVendorPurchaseLimits"></label>
</div> </div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="noKimCooldowns" />
<label class="form-check-label" for="noKimCooldowns" data-loc="cheats_noKimCooldowns"></label>
</div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="instantResourceExtractorDrones" /> <input class="form-check-input" type="checkbox" id="instantResourceExtractorDrones" />
<label class="form-check-label" for="instantResourceExtractorDrones" data-loc="cheats_instantResourceExtractorDrones"></label> <label class="form-check-label" for="instantResourceExtractorDrones" data-loc="cheats_instantResourceExtractorDrones"></label>

View File

@ -277,9 +277,7 @@ function fetchItemList() {
} else { } else {
const nameSet = new Set(); const nameSet = new Set();
items.forEach(item => { items.forEach(item => {
if (item.name.includes("<ARCHWING> ")) { item.name = item.name.replace(/<.+>/g, "").trim();
item.name = item.name.replace("<ARCHWING> ", "");
}
if ("badReason" in item) { if ("badReason" in item) {
if (item.badReason == "starter") { if (item.badReason == "starter") {
item.name = loc("code_starter").split("|MOD|").join(item.name); item.name = loc("code_starter").split("|MOD|").join(item.name);

View File

@ -105,6 +105,7 @@ dict = {
currency_owned: `Du hast |COUNT|.`, currency_owned: `Du hast |COUNT|.`,
powersuit_archonShardsLabel: `Archon-Scherben-Slots`, powersuit_archonShardsLabel: `Archon-Scherben-Slots`,
powersuit_archonShardsDescription: `Du kannst diese unbegrenzten Slots nutzen, um eine Vielzahl von Verbesserungen anzuwenden.`, powersuit_archonShardsDescription: `Du kannst diese unbegrenzten Slots nutzen, um eine Vielzahl von Verbesserungen anzuwenden.`,
powersuit_archonShardsDescription2: `Hinweis: Jede Archon-Scherbe benötigt beim Laden etwas Zeit, um angewendet zu werden.`,
mods_addRiven: `Riven hinzufügen`, mods_addRiven: `Riven hinzufügen`,
mods_fingerprint: `Fingerabdruck`, mods_fingerprint: `Fingerabdruck`,
mods_fingerprintHelp: `Benötigst du Hilfe mit dem Fingerabdruck?`, mods_fingerprintHelp: `Benötigst du Hilfe mit dem Fingerabdruck?`,
@ -136,6 +137,7 @@ dict = {
cheats_noArgonCrystalDecay: `Argon-Kristalle verschwinden niemals`, cheats_noArgonCrystalDecay: `Argon-Kristalle verschwinden niemals`,
cheats_noMasteryRankUpCooldown: `Keine Wartezeit beim Meisterschaftsrangaufstieg`, cheats_noMasteryRankUpCooldown: `Keine Wartezeit beim Meisterschaftsrangaufstieg`,
cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`,
cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`,
cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`,
cheats_noDojoRoomBuildStage: `Kein Dojo-Raum-Bauvorgang`, cheats_noDojoRoomBuildStage: `Kein Dojo-Raum-Bauvorgang`,
cheats_noDojoDecoBuildStage: `Kein Dojo-Deko-Bauvorgang`, cheats_noDojoDecoBuildStage: `Kein Dojo-Deko-Bauvorgang`,

View File

@ -103,7 +103,8 @@ dict = {
currency_PrimeTokens: `Regal Aya`, currency_PrimeTokens: `Regal Aya`,
currency_owned: `You have |COUNT|.`, currency_owned: `You have |COUNT|.`,
powersuit_archonShardsLabel: `Archon Shard Slots`, powersuit_archonShardsLabel: `Archon Shard Slots`,
powersuit_archonShardsDescription: `You can use these unlimited slots to apply a wide range of upgrades`, powersuit_archonShardsDescription: `You can use these unlimited slots to apply a wide range of upgrades.`,
powersuit_archonShardsDescription2: `Note that each archon shard takes some time to be applied when loading in.`,
mods_addRiven: `Add Riven`, mods_addRiven: `Add Riven`,
mods_fingerprint: `Fingerprint`, mods_fingerprint: `Fingerprint`,
mods_fingerprintHelp: `Need help with the fingerprint?`, mods_fingerprintHelp: `Need help with the fingerprint?`,
@ -135,6 +136,7 @@ dict = {
cheats_noArgonCrystalDecay: `No Argon Crystal Decay`, cheats_noArgonCrystalDecay: `No Argon Crystal Decay`,
cheats_noMasteryRankUpCooldown: `No Mastery Rank Up Cooldown`, cheats_noMasteryRankUpCooldown: `No Mastery Rank Up Cooldown`,
cheats_noVendorPurchaseLimits: `No Vendor Purchase Limits`, cheats_noVendorPurchaseLimits: `No Vendor Purchase Limits`,
cheats_noKimCooldowns: `No KIM Cooldowns`,
cheats_instantResourceExtractorDrones: `Instant Resource Extractor Drones`, cheats_instantResourceExtractorDrones: `Instant Resource Extractor Drones`,
cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`,
cheats_noDojoDecoBuildStage: `No Dojo Deco Build Stage`, cheats_noDojoDecoBuildStage: `No Dojo Deco Build Stage`,

View File

@ -0,0 +1,160 @@
// Spanish translation by hxedcl
dict = {
general_inventoryUpdateNote: `Nota: Los cambios realizados aquí se reflejarán en el juego cuando este sincronice el inventario. Usar la navegación debería ser la forma más sencilla de activar esto.`,
general_addButton: `Agregar`,
general_bulkActions: `Acciones masivas`,
code_nonValidAuthz: `Tus credenciales no son válidas.`,
code_changeNameConfirm: `¿Qué nombre te gustaría ponerle a tu cuenta?`,
code_deleteAccountConfirm: `¿Estás seguro de que deseas eliminar tu cuenta |DISPLAYNAME| (|EMAIL|)? Esta acción es permanente.`,
code_archgun: `Archcañón`,
code_melee: `Cuerpo a cuerpo`,
code_pistol: `Pistola`,
code_rifle: `Rifle`,
code_shotgun: `Escopeta`,
code_kitgun: `Kitgun`,
code_zaw: `Zaw`,
code_moteAmp: `Amp Mota`,
code_amp: `Amp`,
code_kDrive: `K-Drive`,
code_legendaryCore: `Núcleo legendario`,
code_traumaticPeculiar: `Traumatismo peculiar`,
code_starter: `|MOD| (Defectuoso)`,
code_badItem: `(Impostor)`,
code_maxRank: `Rango máximo`,
code_rename: `Renombrar`,
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_succRankUp: `Ascenso exitoso.`,
code_noEquipmentToRankUp: `No hay equipo para ascender.`,
code_succAdded: `Agregado exitosamente.`,
code_succRemoved: `Eliminado exitosamente.`,
code_buffsNumber: `Cantidad de mejoras`,
code_cursesNumber: `Cantidad de maldiciones`,
code_rerollsNumber: `Cantidad de reintentos`,
code_viewStats: `Ver estadísticas`,
code_rank: `Rango`,
code_count: `Cantidad`,
code_focusAllUnlocked: `Todas las escuelas de enfoque ya están desbloqueadas.`,
code_focusUnlocked: `¡Desbloqueadas |COUNT| nuevas escuelas de enfoque! Se necesita una actualización del inventario para reflejar los cambios en el juego. Visitar la navegación debería ser la forma más sencilla de activarlo.`,
code_addModsConfirm: `¿Estás seguro de que deseas agregar |COUNT| modificadores a tu cuenta?`,
code_succImport: `Importación exitosa.`,
code_gild: `Refinar`,
code_moa: `Moa`,
code_zanuka: `Sabueso`,
code_zanukaA: `Sabueso Dorma`,
code_zanukaB: `Sabueso Bhaira`,
code_zanukaC: `Sabueso Hec`,
code_stage: `Etapa`,
code_complete: `Completa`,
code_nextStage: `Siguiente etapa`,
code_prevStage: `Etapa anterior`,
code_reset: `Reiniciar`,
code_setInactive: `Marcar la misión como inactiva`,
code_completed: `Completada`,
code_active: `Activa`,
code_pigment: `Pigmento`,
login_description: `Inicia sesión con las credenciales de tu cuenta OpenWF (las mismas que usas en el juego al conectarte a este servidor).`,
login_emailLabel: `Dirección de correo electrónico`,
login_passwordLabel: `Contraseña`,
login_loginButton: `Iniciar sesión`,
navbar_logout: `Cerrar sesión`,
navbar_renameAccount: `Renombrar cuenta`,
navbar_deleteAccount: `Eliminar cuenta`,
navbar_inventory: `Inventario`,
navbar_mods: `Mods`,
navbar_quests: `Misiones`,
navbar_cheats: `Trucos`,
navbar_import: `Importar`,
inventory_addItems: `Agregar objetos`,
inventory_suits: `Warframes`,
inventory_longGuns: `Armas primarias`,
inventory_pistols: `Armas secundarias`,
inventory_melee: `Armas cuerpo a cuerpo`,
inventory_spaceSuits: `Archwings`,
inventory_spaceGuns: `Armas primarias Archwing`,
inventory_spaceMelee: `Armas cuerpo a cuerpo Archwing`,
inventory_mechSuits: `Necramechs`,
inventory_sentinels: `Centinelas`,
inventory_sentinelWeapons: `Armas de centinela`,
inventory_operatorAmps: `Amps`,
inventory_hoverboards: `K-Drives`,
inventory_moaPets: `Moa`,
inventory_bulkAddSuits: `Agregar Warframes faltantes`,
inventory_bulkAddWeapons: `Agregar armas faltantes`,
inventory_bulkAddSpaceSuits: `Agregar Archwings faltantes`,
inventory_bulkAddSpaceWeapons: `Agregar armas Archwing faltantes`,
inventory_bulkAddSentinels: `Agregar centinelas faltantes`,
inventory_bulkAddSentinelWeapons: `Agregar armas de centinela faltantes`,
inventory_bulkRankUpSuits: `Maximizar rango de todos los Warframes`,
inventory_bulkRankUpWeapons: `Maximizar rango de todas las armas`,
inventory_bulkRankUpSpaceSuits: `Maximizar rango de todos los Archwings`,
inventory_bulkRankUpSpaceWeapons: `Maximizar rango de todas las armas Archwing`,
inventory_bulkRankUpSentinels: `Maximizar rango de todos los centinelas`,
inventory_bulkRankUpSentinelWeapons: `Maximizar rango de todas las armas de centinela`,
quests_list: `Misiones`,
quests_completeAll: `Completar todas las misiones`,
quests_resetAll: `Reiniciar todas las misiones`,
quests_giveAll: `Otorgar todas las misiones`,
currency_RegularCredits: `Créditos`,
currency_PremiumCredits: `Platino`,
currency_FusionPoints: `Endo`,
currency_PrimeTokens: `Aya Real`,
currency_owned: `Tienes |COUNT|.`,
powersuit_archonShardsLabel: `Ranuras de Fragmento de Archón`,
powersuit_archonShardsDescription: `Puedes usar estas ranuras ilimitadas para aplicar una amplia variedad de mejoras`,
powersuit_archonShardsDescription2: `[UNTRANSLATED] Note that each archon shard takes some time to be applied when loading in.`,
mods_addRiven: `Agregar Agrietado`,
mods_fingerprint: `Huella digital`,
mods_fingerprintHelp: `¿Necesitas ayuda con la huella digital?`,
mods_rivens: `Agrietados`,
mods_mods: `Mods`,
mods_bulkAddMods: `Agregar mods faltantes`,
cheats_administratorRequirement: `Debes ser administrador para usar esta función. Para convertirte en administrador, agrega <code>|DISPLAYNAME|</code> a <code>administratorNames</code> en el archivo config.json.`,
cheats_server: `Servidor`,
cheats_skipTutorial: `Omitir tutorial`,
cheats_skipAllDialogue: `Omitir todos los diálogos`,
cheats_unlockAllScans: `Desbloquear todos los escaneos`,
cheats_unlockAllMissions: `Desbloquear todas las misiones`,
cheats_infiniteCredits: `Créditos infinitos`,
cheats_infinitePlatinum: `Platino infinito`,
cheats_infiniteEndo: `Endo infinito`,
cheats_infiniteRegalAya: `Aya Real infinita`,
cheats_infiniteHelminthMaterials: `Materiales Helminto infinitos`,
cheats_unlockAllShipFeatures: `Desbloquear todas las funciones de nave`,
cheats_unlockAllShipDecorations: `Desbloquear todas las decoraciones de nave`,
cheats_unlockAllFlavourItems: `Desbloquear todos los <abbr title="Conjuntos de animaciones, glifos, paletas, etc.">ítems estéticos</abbr>`,
cheats_unlockAllSkins: `Desbloquear todas las apariencias`,
cheats_unlockAllCapturaScenes: `Desbloquear todas las escenas 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`,
cheats_unlockArcanesEverywhere: `Adaptadores de Arcanos en todas partes`,
cheats_noDailyStandingLimits: `Sin límite diario de reputación`,
cheats_noArgonCrystalDecay: `Sin descomposición de cristal de Argón`,
cheats_noMasteryRankUpCooldown: `Sin tiempo de espera para rango de maestría`,
cheats_noVendorPurchaseLimits: `Sin límite de compras de vendedores`,
cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`,
cheats_instantResourceExtractorDrones: `Drones de extracción de recursos instantáneos`,
cheats_noDojoRoomBuildStage: `Sin etapa de construcción de sala del dojo`,
cheats_noDojoDecoBuildStage: `Sin etapa de construcción de decoraciones del dojo`,
cheats_fastDojoRoomDestruction: `Destrucción rápida de salas del dojo`,
cheats_noDojoResearchCosts: `Sin costo de investigación del dojo`,
cheats_noDojoResearchTime: `Sin tiempo de investigación del dojo`,
cheats_fastClanAscension: `Ascenso rápido del clan`,
cheats_spoofMasteryRank: `Rango de maestría simulado (-1 para desactivar)`,
cheats_saveSettings: `Guardar configuración`,
cheats_account: `Cuenta`,
cheats_unlockAllFocusSchools: `Desbloquear todas las escuelas de enfoque`,
cheats_helminthUnlockAll: `Subir al máximo el Helminto`,
cheats_intrinsicsUnlockAll: `Maximizar todos los intrínsecos`,
cheats_changeSupportedSyndicate: `Sindicatos disponibles`,
cheats_changeButton: `Cambiar`,
cheats_none: `Ninguno`,
import_importNote: `Puedes proporcionar una respuesta de inventario completa o parcial (representación del cliente) aquí. Todos los campos compatibles con el importador <b>serán sobrescritos</b> en tu cuenta.`,
import_submit: `Enviar`,
prettier_sucks_ass: ``
};

View File

@ -105,6 +105,7 @@ dict = {
currency_owned: `|COUNT| possédés.`, currency_owned: `|COUNT| possédés.`,
powersuit_archonShardsLabel: `Emplacements de fragments d'Archonte`, powersuit_archonShardsLabel: `Emplacements de fragments d'Archonte`,
powersuit_archonShardsDescription: `Slots illimités pour appliquer plusieurs améliorations.`, powersuit_archonShardsDescription: `Slots illimités pour appliquer plusieurs améliorations.`,
powersuit_archonShardsDescription2: `[UNTRANSLATED] Note that each archon shard takes some time to be applied when loading in.`,
mods_addRiven: `Ajouter un riven`, mods_addRiven: `Ajouter un riven`,
mods_fingerprint: `Empreinte`, mods_fingerprint: `Empreinte`,
mods_fingerprintHelp: `Besoin d'aide pour l'empreinte ?`, mods_fingerprintHelp: `Besoin d'aide pour l'empreinte ?`,
@ -136,6 +137,7 @@ dict = {
cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`,
cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`,
cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`,
cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`,
cheats_instantResourceExtractorDrones: `Ressources de drone d'extraction instantannées`, cheats_instantResourceExtractorDrones: `Ressources de drone d'extraction instantannées`,
cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`,
cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`, cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`,

View File

@ -105,6 +105,7 @@ dict = {
currency_owned: `У тебя |COUNT|.`, currency_owned: `У тебя |COUNT|.`,
powersuit_archonShardsLabel: `Ячейки осколков архонта`, powersuit_archonShardsLabel: `Ячейки осколков архонта`,
powersuit_archonShardsDescription: `Вы можете использовать эти неограниченные ячейки для установки множества улучшений.`, powersuit_archonShardsDescription: `Вы можете использовать эти неограниченные ячейки для установки множества улучшений.`,
powersuit_archonShardsDescription2: `[UNTRANSLATED] Note that each archon shard takes some time to be applied when loading in.`,
mods_addRiven: `Добавить Мод Разлома`, mods_addRiven: `Добавить Мод Разлома`,
mods_fingerprint: `Отпечаток`, mods_fingerprint: `Отпечаток`,
mods_fingerprintHelp: `Нужна помощь с отпечатком?`, mods_fingerprintHelp: `Нужна помощь с отпечатком?`,
@ -134,11 +135,12 @@ dict = {
cheats_unlockArcanesEverywhere: `Адаптеры для мистификаторов везде`, cheats_unlockArcanesEverywhere: `Адаптеры для мистификаторов везде`,
cheats_noDailyStandingLimits: `Без ежедневных ограничений репутации`, cheats_noDailyStandingLimits: `Без ежедневных ограничений репутации`,
cheats_noArgonCrystalDecay: `Без распада аргоновых кристаллов`, cheats_noArgonCrystalDecay: `Без распада аргоновых кристаллов`,
cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, cheats_noMasteryRankUpCooldown: `Повышение ранга мастерства без кулдауна`,
cheats_noVendorPurchaseLimits: `Отсутствие лимитов на покупки у вендоров`, cheats_noVendorPurchaseLimits: `Отсутствие лимитов на покупки у вендоров`,
cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`,
cheats_instantResourceExtractorDrones: `Мгновенные Экстракторы Ресурсов`, cheats_instantResourceExtractorDrones: `Мгновенные Экстракторы Ресурсов`,
cheats_noDojoRoomBuildStage: `Мгновенное Строительтво Комнат Додзё`, cheats_noDojoRoomBuildStage: `Мгновенное Строительтво Комнат Додзё`,
cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`, cheats_noDojoDecoBuildStage: `Мгновенное Строительтво Декораций Додзё`,
cheats_fastDojoRoomDestruction: `Мгновенные Уничтожение Комнат Додзё`, cheats_fastDojoRoomDestruction: `Мгновенные Уничтожение Комнат Додзё`,
cheats_noDojoResearchCosts: `Бесплатные Исследование Додзё`, cheats_noDojoResearchCosts: `Бесплатные Исследование Додзё`,
cheats_noDojoResearchTime: `Мгновенные Исследование Додзё`, cheats_noDojoResearchTime: `Мгновенные Исследование Додзё`,

View File

@ -105,6 +105,7 @@ dict = {
currency_owned: `当前拥有 |COUNT|。`, currency_owned: `当前拥有 |COUNT|。`,
powersuit_archonShardsLabel: `执刑官源力石槽位`, powersuit_archonShardsLabel: `执刑官源力石槽位`,
powersuit_archonShardsDescription: `您可以使用这些无限插槽应用各种强化效果`, powersuit_archonShardsDescription: `您可以使用这些无限插槽应用各种强化效果`,
powersuit_archonShardsDescription2: `[UNTRANSLATED] Note that each archon shard takes some time to be applied when loading in.`,
mods_addRiven: `添加裂罅MOD`, mods_addRiven: `添加裂罅MOD`,
mods_fingerprint: `印记`, mods_fingerprint: `印记`,
mods_fingerprintHelp: `需要印记相关的帮助?`, mods_fingerprintHelp: `需要印记相关的帮助?`,
@ -136,6 +137,7 @@ dict = {
cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`,
cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`,
cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`,
cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`,
cheats_instantResourceExtractorDrones: `即时资源采集无人机`, cheats_instantResourceExtractorDrones: `即时资源采集无人机`,
cheats_noDojoRoomBuildStage: `无视道场房间建造阶段`, cheats_noDojoRoomBuildStage: `无视道场房间建造阶段`,
cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`, cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`,