diff --git a/.gitattributes b/.gitattributes index 0936ecf2..b3bfce24 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ # Auto detect text files and perform LF normalization -* text=auto +* text=auto eol=lf static/webui/libs/ linguist-vendored diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 24a2f307..a59167d1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,11 +15,12 @@ jobs: - run: npm run verify - run: npm run lint:ci - run: npm run prettier + - run: npm run update-translations - name: Fail if there are uncommitted changes run: | if [[ -n "$(git status --porcelain)" ]]; then echo "Uncommitted changes detected:" git status - git diff + git --no-pager diff exit 1 fi diff --git a/config.json.example b/config.json.example index 626d2d7b..a813d093 100644 --- a/config.json.example +++ b/config.json.example @@ -32,6 +32,7 @@ "noArgonCrystalDecay": false, "noMasteryRankUpCooldown": false, "noVendorPurchaseLimits": true, + "noKimCooldowns": false, "instantResourceExtractorDrones": false, "noDojoRoomBuildStage": false, "noDecoBuildStage": false, diff --git a/package-lock.json b/package-lock.json index 8a09d9ca..a858a5d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.53", + "warframe-public-export-plus": "^0.5.54", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -3789,9 +3789,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.53", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.53.tgz", - "integrity": "sha512-FjYeCJ5OxvPWyETnV33YOeX7weVVeMy451RY7uewwSvRbSNFTDhmhvbrLhfwykulUX4RPakfZr8nO0S0a6lGCA==" + "version": "0.5.54", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.54.tgz", + "integrity": "sha512-27r6qLErr3P8UVDiEzhDAs/BjdAS3vI2CQ58jSI+LClDlj6QL+y1jQe8va/npl3Ft2K8PywLkZ8Yso0j9YzvOA==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index 77358348..008d5a18 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.53", + "warframe-public-export-plus": "^0.5.54", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/scripts/update-translations.js b/scripts/update-translations.js index 568885e6..5351afaa 100644 --- a/scripts/update-translations.js +++ b/scripts/update-translations.js @@ -4,7 +4,7 @@ const fs = require("fs"); function extractStrings(content) { - const regex = /([a-zA-Z_]+): `([^`]*)`,/g; + const regex = /([a-zA-Z0-9_]+): `([^`]*)`,/g; let matches; const strings = {}; while ((matches = regex.exec(content)) !== null) { @@ -15,7 +15,7 @@ function extractStrings(content) { const source = fs.readFileSync("../static/webui/translations/en.js", "utf8"); 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 => { 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`); } }); - } else if (line.length) { + } else { fs.writeSync(fileHandle, line + "\n"); } }); diff --git a/src/app.ts b/src/app.ts index 160a93c8..c8b19583 100644 --- a/src/app.ts +++ b/src/app.ts @@ -16,9 +16,9 @@ import { webuiRouter } from "@/src/routes/webui"; const app = express(); 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. - if (req.headers["content-encoding"] == "ezip") { + if (req.headers["content-encoding"] == "ezip" || req.headers["content-encoding"] == "e") { req.headers["content-encoding"] = undefined; } next(); diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index 5b0b5374..55083d5a 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -32,11 +32,11 @@ import { logger } from "@/src/utils/logger"; export const guildTechController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId); - const guild = await getGuildForRequestEx(req, inventory); const data = JSON.parse(String(req.body)) as TGuildTechRequest; if (data.Action == "Sync") { let needSave = false; const techProjects: ITechProjectClient[] = []; + const guild = await getGuildForRequestEx(req, inventory); if (guild.TechProjects) { for (const project of guild.TechProjects) { const techProject: ITechProjectClient = { @@ -59,110 +59,170 @@ export const guildTechController: RequestHandler = async (req, res) => { } res.json({ TechProjects: techProjects }); } else if (data.Action == "Start") { - if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { - res.status(400).send("-1").end(); - return; - } - const recipe = ExportDojoRecipes.research[data.RecipeType]; - guild.TechProjects ??= []; - if (!guild.TechProjects.find(x => x.ItemType == data.RecipeType)) { - const techProject = - guild.TechProjects[ - guild.TechProjects.push({ - ItemType: data.RecipeType, - ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price), - ReqItems: recipe.ingredients.map(x => ({ - ItemType: x.ItemType, - ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount) - })), - State: 0 - }) - 1 - ]; - setGuildTechLogState(guild, techProject.ItemType, 5); - if (config.noDojoResearchCosts) { - processFundedGuildTechProject(guild, techProject, recipe); - } else { - if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") { - guild.ActiveDojoColorResearch = data.RecipeType; + if (data.Mode == "Guild") { + const guild = await getGuildForRequestEx(req, inventory); + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { + res.status(400).send("-1").end(); + return; + } + const recipe = ExportDojoRecipes.research[data.RecipeType]; + guild.TechProjects ??= []; + if (!guild.TechProjects.find(x => x.ItemType == data.RecipeType)) { + const techProject = + guild.TechProjects[ + guild.TechProjects.push({ + ItemType: data.RecipeType, + ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price), + ReqItems: recipe.ingredients.map(x => ({ + ItemType: x.ItemType, + ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount) + })), + State: 0 + }) - 1 + ]; + setGuildTechLogState(guild, techProject.ItemType, 5); + if (config.noDojoResearchCosts) { + processFundedGuildTechProject(guild, techProject, recipe); + } else { + if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") { + guild.ActiveDojoColorResearch = data.RecipeType; + } } } + await guild.save(); + 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() + }); } - await guild.save(); - res.end(); } else if (data.Action == "Contribute") { - if (!hasAccessToDojo(inventory)) { - res.status(400).send("-1").end(); - return; - } + if ((req.query.guildId as string) == "000000000000000000000000") { + const techProject = inventory.PersonalTechProjects.id(data.ResearchId)!; - const guildMember = (await GuildMember.findOne( - { accountId, guildId: guild._id }, - "RegularCreditsContributed MiscItemsContributed" - ))!; + techProject.ReqCredits -= data.RegularCredits; + const inventoryChanges: IInventoryChanges = updateCurrency(inventory, data.RegularCredits, false); - const contributions = data; - const techProject = guild.TechProjects!.find(x => x.ItemType == contributions.RecipeType)!; - - if (contributions.VaultCredits) { - if (contributions.VaultCredits > techProject.ReqCredits) { - contributions.VaultCredits = techProject.ReqCredits; - } - techProject.ReqCredits -= contributions.VaultCredits; - guild.VaultRegularCredits! -= contributions.VaultCredits; - } - - if (contributions.RegularCredits > techProject.ReqCredits) { - contributions.RegularCredits = techProject.ReqCredits; - } - techProject.ReqCredits -= contributions.RegularCredits; - - guildMember.RegularCreditsContributed ??= 0; - guildMember.RegularCreditsContributed += contributions.RegularCredits; - - if (contributions.VaultMiscItems.length) { - for (const miscItem of contributions.VaultMiscItems) { + 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; - - const vaultMiscItem = guild.VaultMiscItems!.find(x => x.ItemType == miscItem.ItemType)!; - vaultMiscItem.ItemCount -= miscItem.ItemCount; + miscItemChanges.push({ + ItemType: miscItem.ItemType, + ItemCount: miscItem.ItemCount * -1 + }); } } - } + addMiscItems(inventory, miscItemChanges); + inventoryChanges.MiscItems = miscItemChanges; - const miscItemChanges = []; - for (const miscItem of contributions.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 - }); + techProject.HasContributions = true; - addGuildMemberMiscItemContribution(guildMember, miscItem); + 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)) { + res.status(400).send("-1").end(); + return; + } + + const guild = await getGuildForRequestEx(req, inventory); + const guildMember = (await GuildMember.findOne( + { accountId, guildId: guild._id }, + "RegularCreditsContributed MiscItemsContributed" + ))!; + + const techProject = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!; + + if (data.VaultCredits) { + if (data.VaultCredits > techProject.ReqCredits) { + data.VaultCredits = techProject.ReqCredits; + } + techProject.ReqCredits -= data.VaultCredits; + guild.VaultRegularCredits! -= data.VaultCredits; + } + + if (data.RegularCredits > techProject.ReqCredits) { + data.RegularCredits = techProject.ReqCredits; + } + techProject.ReqCredits -= data.RegularCredits; + + guildMember.RegularCreditsContributed ??= 0; + guildMember.RegularCreditsContributed += data.RegularCredits; + + if (data.VaultMiscItems.length) { + for (const miscItem of data.VaultMiscItems) { + 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; + + const vaultMiscItem = guild.VaultMiscItems!.find(x => x.ItemType == miscItem.ItemType)!; + vaultMiscItem.ItemCount -= miscItem.ItemCount; + } + } + } + + 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 + }); + + addGuildMemberMiscItemContribution(guildMember, miscItem); + } + } + addMiscItems(inventory, miscItemChanges); + const inventoryChanges: IInventoryChanges = updateCurrency(inventory, data.RegularCredits, false); + inventoryChanges.MiscItems = miscItemChanges; + + // Check if research is fully funded now. + await processGuildTechProjectContributionsUpdate(guild, techProject); + + await Promise.all([guild.save(), inventory.save(), guildMember.save()]); + res.json({ + InventoryChanges: inventoryChanges, + Vault: getGuildVault(guild) + }); } - addMiscItems(inventory, miscItemChanges); - const inventoryChanges: IInventoryChanges = updateCurrency(inventory, contributions.RegularCredits, false); - inventoryChanges.MiscItems = miscItemChanges; - - // Check if research is fully funded now. - await processGuildTechProjectContributionsUpdate(guild, techProject); - - await Promise.all([guild.save(), inventory.save(), guildMember.save()]); - res.json({ - InventoryChanges: inventoryChanges, - Vault: getGuildVault(guild) - }); } else if (data.Action.split(",")[0] == "Buy") { + const guild = await getGuildForRequestEx(req, inventory); if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) { res.status(400).send("-1").end(); return; @@ -190,6 +250,7 @@ export const guildTechController: RequestHandler = async (req, res) => { } }); } else if (data.Action == "Fabricate") { + const guild = await getGuildForRequestEx(req, inventory); if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) { res.status(400).send("-1").end(); return; @@ -206,6 +267,7 @@ export const guildTechController: RequestHandler = async (req, res) => { // Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`. res.json({ inventoryChanges: inventoryChanges }); } else if (data.Action == "Pause") { + const guild = await getGuildForRequestEx(req, inventory); if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { res.status(400).send("-1").end(); return; @@ -217,6 +279,7 @@ export const guildTechController: RequestHandler = async (req, res) => { await removePigmentsFromGuildMembers(guild._id); res.end(); } else if (data.Action == "Unpause") { + const guild = await getGuildForRequestEx(req, inventory); if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { res.status(400).send("-1").end(); return; @@ -239,7 +302,7 @@ type TGuildTechRequest = interface IGuildTechBasicRequest { Action: "Start" | "Fabricate" | "Pause" | "Unpause"; - Mode: "Guild"; + Mode: "Guild" | "Personal"; RecipeType: string; } @@ -251,7 +314,7 @@ interface IGuildTechBuyRequest { interface IGuildTechContributeRequest { Action: "Contribute"; - ResearchId: ""; + ResearchId: string; RecipeType: string; RegularCredits: number; MiscItems: IMiscItem[]; diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 2c659717..805df53d 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -202,7 +202,8 @@ export const getInventoryResponse = async ( if (config.universalPolarityEverywhere) { 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({ Slot: i, Value: ArtifactPolarity.Any diff --git a/src/controllers/api/nemesisController.ts b/src/controllers/api/nemesisController.ts index 34e6bd4a..6f789e98 100644 --- a/src/controllers/api/nemesisController.ts +++ b/src/controllers/api/nemesisController.ts @@ -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 { Loadout } from "@/src/models/inventoryModels/loadoutModel"; import { freeUpSlot, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { SRng } from "@/src/services/rngService"; 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 { RequestHandler } from "express"; @@ -49,7 +64,7 @@ export const nemesisController: RequestHandler = async (req, res) => { } else if ((req.query.mode as string) == "p") { const inventory = await getInventory(accountId, "Nemesis"); const body = getJSONfromString(String(req.body)); - const passcode = getNemesisPasscode(inventory.Nemesis!.fp, inventory.Nemesis!.Faction); + const passcode = getNemesisPasscode(inventory.Nemesis!); let guessResult = 0; if (inventory.Nemesis!.Faction == "FC_INFESTATION") { for (let i = 0; i != 3; ++i) { @@ -66,6 +81,88 @@ export const nemesisController: RequestHandler = async (req, res) => { } } res.json({ GuessResult: guessResult }); + } else if (req.query.mode == "r") { + const inventory = await getInventory( + accountId, + "Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades" + ); + const body = getJSONfromString(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") { const inventory = await getInventory(accountId, "Nemesis"); const body = getJSONfromString(String(req.body)); @@ -173,6 +270,20 @@ interface INemesisPrespawnCheckRequest { 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 = [ "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon", "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak", diff --git a/src/controllers/api/saveDialogueController.ts b/src/controllers/api/saveDialogueController.ts index 999e0293..7d7d6380 100644 --- a/src/controllers/api/saveDialogueController.ts +++ b/src/controllers/api/saveDialogueController.ts @@ -1,4 +1,5 @@ import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; +import { config } from "@/src/services/configService"; import { addEmailItem, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; 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"); } 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 ??= []; const dialogue = getDialogue(inventory, request.DialogueName); dialogue.Rank = request.Rank; diff --git a/src/controllers/api/syndicateSacrificeController.ts b/src/controllers/api/syndicateSacrificeController.ts index b20df3bf..36ad07cb 100644 --- a/src/controllers/api/syndicateSacrificeController.ts +++ b/src/controllers/api/syndicateSacrificeController.ts @@ -3,15 +3,9 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { ExportNightwave, ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; -import { - addItem, - addMiscItems, - combineInventoryChanges, - getInventory, - updateCurrency -} from "@/src/services/inventoryService"; +import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService"; 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) => { const accountId = await getAccountIdForRequest(request); @@ -77,10 +71,13 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp res.NewEpisodeReward = true; const reward = ExportNightwave.rewards[index]; let rewardType = reward.uniqueName; - if (isStoreItem(rewardType)) { - rewardType = fromStoreItem(rewardType); + if (!isStoreItem(rewardType)) { + rewardType = toStoreItem(rewardType); } - combineInventoryChanges(res.InventoryChanges, await addItem(inventory, rewardType, reward.itemCount ?? 1)); + combineInventoryChanges( + res.InventoryChanges, + (await handleStoreItemAcquisition(rewardType, inventory, reward.itemCount)).InventoryChanges + ); } } diff --git a/src/helpers/nemesisHelpers.ts b/src/helpers/nemesisHelpers.ts index 839e0675..fff5e94e 100644 --- a/src/helpers/nemesisHelpers.ts +++ b/src/helpers/nemesisHelpers.ts @@ -1,6 +1,11 @@ import { ExportRegions } from "warframe-public-export-plus"; import { IInfNode } from "@/src/types/inventoryTypes/inventoryTypes"; 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[] => { const infNodes = []; @@ -33,12 +38,93 @@ const systemIndexes: Record = { }; // 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[] => { - const rng = new SRng(fp); +export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: string }): number[] => { + const rng = new SRng(nemesis.fp); 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)); } 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); + } +}; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 0cb698cc..a0eddb10 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -84,7 +84,9 @@ import { IInfNode, IDiscoveredMarker, IWeeklyMission, - ILockedWeaponGroupDatabase + ILockedWeaponGroupDatabase, + IPersonalTechProjectDatabase, + IPersonalTechProjectClient } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -498,7 +500,34 @@ const seasonChallengeHistorySchema = new Schema( { _id: false } ); -//TODO: check whether this is complete +const personalTechProjectSchema = new Schema({ + 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( { LPP_SPACE: { type: Number, default: 0 }, @@ -1442,7 +1471,7 @@ const inventorySchema = new Schema( //Railjack craft //https://warframe.fandom.com/wiki/Rising_Tide - PersonalTechProjects: [Schema.Types.Mixed], + PersonalTechProjects: { type: [personalTechProjectSchema], default: [] }, //Modulars lvl and exp(Railjack|Duviri) //https://warframe.fandom.com/wiki/Intrinsics @@ -1471,7 +1500,7 @@ const inventorySchema = new Schema( DuviriInfo: DuviriInfoSchema, Mailbox: MailboxSchema, HandlerPoints: Number, - ChallengesFixVersion: Number, + ChallengesFixVersion: { type: Number, default: 6 }, PlayedParkourTutorial: Boolean, ActiveLandscapeTraps: [Schema.Types.Mixed], RepVotes: [Schema.Types.Mixed], @@ -1585,6 +1614,7 @@ export type InventoryDocumentProps = { Drones: Types.DocumentArray; CrewShipWeaponSkins: Types.DocumentArray; CrewShipSalvagedWeaponsSkins: Types.DocumentArray; + PersonalTechProjects: Types.DocumentArray; } & { [K in TEquipmentKey]: Types.DocumentArray }; // eslint-disable-next-line @typescript-eslint/no-empty-object-type diff --git a/src/services/configService.ts b/src/services/configService.ts index 7a3f0b1d..c584a1fb 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -38,6 +38,7 @@ interface IConfig { noArgonCrystalDecay?: boolean; noMasteryRankUpCooldown?: boolean; noVendorPurchaseLimits?: boolean; + noKimCooldowns?: boolean; instantResourceExtractorDrones?: boolean; noDojoRoomBuildStage?: boolean; noDojoDecoBuildStage?: boolean; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 90b26303..d74bd9af 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -963,7 +963,7 @@ export const addStanding = ( const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0); if (syndicate.Standing + gainedStanding > max) gainedStanding = max - syndicate.Standing; - if (!isMedallion || (isMedallion && syndicateMeta.medallionsCappedByDailyLimit)) { + if (!isMedallion || syndicateMeta.medallionsCappedByDailyLimit) { if (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); + } 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); } } diff --git a/src/services/itemDataService.ts b/src/services/itemDataService.ts index e071111f..6e0edd23 100644 --- a/src/services/itemDataService.ts +++ b/src/services/itemDataService.ts @@ -185,14 +185,15 @@ export const getKeyChainMessage = ({ KeyChain, ChainStage }: IKeyChainRequest): throw new Error(`KeyChain ${KeyChain} does not contain chain stages`); } - const keyChainStage = chainStages[ChainStage]; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (!keyChainStage) { - throw new Error(`KeyChainStage ${ChainStage} not found`); + let i = ChainStage; + let chainStageMessage = chainStages[i].messageToSendWhenTriggered; + while (!chainStageMessage) { + if (++i >= chainStages.length) { + break; + } + chainStageMessage = chainStages[i].messageToSendWhenTriggered; } - const chainStageMessage = keyChainStage.messageToSendWhenTriggered; - if (!chainStageMessage) { throw new Error( `client requested key chain message in keychain ${KeyChain} at stage ${ChainStage} but they did not exist` diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index e6492a6e..b506b37b 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -699,25 +699,10 @@ export const addMissionRewards = async ( } if (inventory.Nemesis.Faction == "FC_INFESTATION") { - inventoryChanges.Nemesis.HenchmenKilled ??= 0; - inventoryChanges.Nemesis.MissionCount ??= 0; - - inventory.Nemesis.HenchmenKilled += 5; inventory.Nemesis.MissionCount += 1; - inventoryChanges.Nemesis.HenchmenKilled += 5; + inventoryChanges.Nemesis.MissionCount ??= 0; 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; @@ -747,7 +732,7 @@ export const addMissionRewards = async ( const endlessJob = syndicateEntry.Jobs.find(j => j.endless); if (endlessJob) { 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)); } } @@ -922,15 +907,140 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u let rotations: number[] = []; if (RewardInfo.jobId) { - if (RewardInfo.JobTier! >= 0) { - const id = RewardInfo.jobId.split("_")[3]; - const syndicateInfo = getWorldState().SyndicateMissions.find(x => x._id.$oid == id); - if (syndicateInfo) { - const jobInfo = syndicateInfo.Jobs![RewardInfo.JobTier!]; - rewardManifests = [jobInfo.rewards]; - rotations = [RewardInfo.JobStage!]; + if (RewardInfo.JobStage! >= 0) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [jobType, tierStr, hubNode, syndicateId, locationTag] = RewardInfo.jobId.split("_"); + const tier = Number(tierStr); + let isEndlessJob = false; + if (syndicateId) { + 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 = { + 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) { // For Spy missions, e.g. 3 vaults cracked = A, B, C for (let i = 0; i != RewardInfo.VaultsCracked; ++i) { diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 63d1b315..1146bef6 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -46,6 +46,7 @@ export interface IInventoryDatabase | "EntratiVaultCountResetDate" | "BrandedSuits" | "LockedWeaponGroup" + | "PersonalTechProjects" | TEquipmentKey >, InventoryDatabaseEquipment { @@ -77,6 +78,7 @@ export interface IInventoryDatabase EntratiVaultCountResetDate?: Date; BrandedSuits?: Types.ObjectId[]; LockedWeaponGroup?: ILockedWeaponGroupDatabase; + PersonalTechProjects: IPersonalTechProjectDatabase[]; } export interface IQuestKeyDatabase { @@ -157,6 +159,11 @@ export type TSolarMapRegion = //TODO: perhaps split response and database into their own files +export enum LoadoutIndex { + NORMAL = 0, + DATAKNIFE = 7 +} + export interface IDailyAffiliations { DailyAffiliation: number; DailyAffiliationPvp: number; @@ -220,7 +227,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu ActiveQuest: string; FlavourItems: IFlavourItem[]; 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[]; RandomUpgradesIdentified?: number; LastRegionPlayed: TSolarMapRegion; @@ -301,7 +308,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu NemesisHistory: INemesisBaseClient[]; LastNemesisAllySpawnTime?: IMongoDate; Settings?: ISettings; - PersonalTechProjects: IPersonalTechProject[]; + PersonalTechProjects: IPersonalTechProjectClient[]; PlayerSkills: IPlayerSkills; CrewShipAmmo: ITypeCount[]; CrewShipWeaponSkins: IUpgradeClient[]; @@ -936,16 +943,20 @@ export interface IPersonalGoalProgress { ReceivedClanReward1?: boolean; } -export interface IPersonalTechProject { +export interface IPersonalTechProjectDatabase { State: number; ReqCredits: number; ItemType: string; ReqItems: ITypeCount[]; + HasContributions?: boolean; + CompletionDate?: Date; +} + +export interface IPersonalTechProjectClient extends Omit { CompletionDate?: IMongoDate; - ItemId: IOid; ProductCategory?: string; CategoryItemId?: IOid; - HasContributions?: boolean; + ItemId: IOid; } export interface IPlayerSkills { diff --git a/static/webui/index.html b/static/webui/index.html index afd841cd..38eb3e54 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -406,7 +406,10 @@
-

+

+ + +

x @@ -604,6 +607,10 @@
+
+ + +
diff --git a/static/webui/script.js b/static/webui/script.js index fd997bbb..b56e2f6e 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -277,9 +277,7 @@ function fetchItemList() { } else { const nameSet = new Set(); items.forEach(item => { - if (item.name.includes(" ")) { - item.name = item.name.replace(" ", ""); - } + item.name = item.name.replace(/<.+>/g, "").trim(); if ("badReason" in item) { if (item.badReason == "starter") { item.name = loc("code_starter").split("|MOD|").join(item.name); diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index ea662e37..95870e31 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -105,6 +105,7 @@ dict = { currency_owned: `Du hast |COUNT|.`, powersuit_archonShardsLabel: `Archon-Scherben-Slots`, 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_fingerprint: `Fingerabdruck`, mods_fingerprintHelp: `Benötigst du Hilfe mit dem Fingerabdruck?`, @@ -136,6 +137,7 @@ dict = { cheats_noArgonCrystalDecay: `Argon-Kristalle verschwinden niemals`, cheats_noMasteryRankUpCooldown: `Keine Wartezeit beim Meisterschaftsrangaufstieg`, cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, + cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`, cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, cheats_noDojoRoomBuildStage: `Kein Dojo-Raum-Bauvorgang`, cheats_noDojoDecoBuildStage: `Kein Dojo-Deko-Bauvorgang`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 700e8566..3c9db66f 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -103,7 +103,8 @@ dict = { currency_PrimeTokens: `Regal Aya`, currency_owned: `You have |COUNT|.`, 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_fingerprint: `Fingerprint`, mods_fingerprintHelp: `Need help with the fingerprint?`, @@ -135,6 +136,7 @@ dict = { cheats_noArgonCrystalDecay: `No Argon Crystal Decay`, cheats_noMasteryRankUpCooldown: `No Mastery Rank Up Cooldown`, cheats_noVendorPurchaseLimits: `No Vendor Purchase Limits`, + cheats_noKimCooldowns: `No KIM Cooldowns`, cheats_instantResourceExtractorDrones: `Instant Resource Extractor Drones`, cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, cheats_noDojoDecoBuildStage: `No Dojo Deco Build Stage`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js new file mode 100644 index 00000000..4656a328 --- /dev/null +++ b/static/webui/translations/es.js @@ -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 |DISPLAYNAME| a administratorNames 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 ítems estéticos`, + 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 serán sobrescritos en tu cuenta.`, + import_submit: `Enviar`, + prettier_sucks_ass: `` +}; diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 03807992..90188be4 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -105,6 +105,7 @@ dict = { currency_owned: `|COUNT| possédés.`, powersuit_archonShardsLabel: `Emplacements de fragments d'Archonte`, 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_fingerprint: `Empreinte`, mods_fingerprintHelp: `Besoin d'aide pour l'empreinte ?`, @@ -136,6 +137,7 @@ dict = { cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, + cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`, cheats_instantResourceExtractorDrones: `Ressources de drone d'extraction instantannées`, cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 2299f2c1..8682cca4 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -105,6 +105,7 @@ dict = { currency_owned: `У тебя |COUNT|.`, powersuit_archonShardsLabel: `Ячейки осколков архонта`, powersuit_archonShardsDescription: `Вы можете использовать эти неограниченные ячейки для установки множества улучшений.`, + powersuit_archonShardsDescription2: `[UNTRANSLATED] Note that each archon shard takes some time to be applied when loading in.`, mods_addRiven: `Добавить Мод Разлома`, mods_fingerprint: `Отпечаток`, mods_fingerprintHelp: `Нужна помощь с отпечатком?`, @@ -134,11 +135,12 @@ dict = { cheats_unlockArcanesEverywhere: `Адаптеры для мистификаторов везде`, cheats_noDailyStandingLimits: `Без ежедневных ограничений репутации`, cheats_noArgonCrystalDecay: `Без распада аргоновых кристаллов`, - cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, + cheats_noMasteryRankUpCooldown: `Повышение ранга мастерства без кулдауна`, cheats_noVendorPurchaseLimits: `Отсутствие лимитов на покупки у вендоров`, + cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`, cheats_instantResourceExtractorDrones: `Мгновенные Экстракторы Ресурсов`, cheats_noDojoRoomBuildStage: `Мгновенное Строительтво Комнат Додзё`, - cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`, + cheats_noDojoDecoBuildStage: `Мгновенное Строительтво Декораций Додзё`, cheats_fastDojoRoomDestruction: `Мгновенные Уничтожение Комнат Додзё`, cheats_noDojoResearchCosts: `Бесплатные Исследование Додзё`, cheats_noDojoResearchTime: `Мгновенные Исследование Додзё`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 7598b005..2f720067 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -105,6 +105,7 @@ dict = { currency_owned: `当前拥有 |COUNT|。`, powersuit_archonShardsLabel: `执刑官源力石槽位`, powersuit_archonShardsDescription: `您可以使用这些无限插槽应用各种强化效果`, + powersuit_archonShardsDescription2: `[UNTRANSLATED] Note that each archon shard takes some time to be applied when loading in.`, mods_addRiven: `添加裂罅MOD`, mods_fingerprint: `印记`, mods_fingerprintHelp: `需要印记相关的帮助?`, @@ -136,6 +137,7 @@ dict = { cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, + cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`, cheats_instantResourceExtractorDrones: `即时资源采集无人机`, cheats_noDojoRoomBuildStage: `无视道场房间建造阶段`, cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`,