merge upstream
This commit is contained in:
		
						commit
						0acca4fe31
					
				
							
								
								
									
										2
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							@ -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
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										3
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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
									
									
									
								
							
							
						
						
									
										8
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -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",
 | 
				
			||||||
 | 
				
			|||||||
@ -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"
 | 
				
			||||||
 | 
				
			|||||||
@ -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");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
				
			|||||||
@ -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();
 | 
				
			||||||
 | 
				
			|||||||
@ -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,110 +59,170 @@ 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 (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
 | 
					        if (data.Mode == "Guild") {
 | 
				
			||||||
            res.status(400).send("-1").end();
 | 
					            const guild = await getGuildForRequestEx(req, inventory);
 | 
				
			||||||
            return;
 | 
					            if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
 | 
				
			||||||
        }
 | 
					                res.status(400).send("-1").end();
 | 
				
			||||||
        const recipe = ExportDojoRecipes.research[data.RecipeType];
 | 
					                return;
 | 
				
			||||||
        guild.TechProjects ??= [];
 | 
					            }
 | 
				
			||||||
        if (!guild.TechProjects.find(x => x.ItemType == data.RecipeType)) {
 | 
					            const recipe = ExportDojoRecipes.research[data.RecipeType];
 | 
				
			||||||
            const techProject =
 | 
					            guild.TechProjects ??= [];
 | 
				
			||||||
                guild.TechProjects[
 | 
					            if (!guild.TechProjects.find(x => x.ItemType == data.RecipeType)) {
 | 
				
			||||||
                    guild.TechProjects.push({
 | 
					                const techProject =
 | 
				
			||||||
                        ItemType: data.RecipeType,
 | 
					                    guild.TechProjects[
 | 
				
			||||||
                        ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price),
 | 
					                        guild.TechProjects.push({
 | 
				
			||||||
                        ReqItems: recipe.ingredients.map(x => ({
 | 
					                            ItemType: data.RecipeType,
 | 
				
			||||||
                            ItemType: x.ItemType,
 | 
					                            ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price),
 | 
				
			||||||
                            ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount)
 | 
					                            ReqItems: recipe.ingredients.map(x => ({
 | 
				
			||||||
                        })),
 | 
					                                ItemType: x.ItemType,
 | 
				
			||||||
                        State: 0
 | 
					                                ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount)
 | 
				
			||||||
                    }) - 1
 | 
					                            })),
 | 
				
			||||||
                ];
 | 
					                            State: 0
 | 
				
			||||||
            setGuildTechLogState(guild, techProject.ItemType, 5);
 | 
					                        }) - 1
 | 
				
			||||||
            if (config.noDojoResearchCosts) {
 | 
					                    ];
 | 
				
			||||||
                processFundedGuildTechProject(guild, techProject, recipe);
 | 
					                setGuildTechLogState(guild, techProject.ItemType, 5);
 | 
				
			||||||
            } else {
 | 
					                if (config.noDojoResearchCosts) {
 | 
				
			||||||
                if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") {
 | 
					                    processFundedGuildTechProject(guild, techProject, recipe);
 | 
				
			||||||
                    guild.ActiveDojoColorResearch = data.RecipeType;
 | 
					                } 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") {
 | 
					    } else if (data.Action == "Contribute") {
 | 
				
			||||||
        if (!hasAccessToDojo(inventory)) {
 | 
					        if ((req.query.guildId as string) == "000000000000000000000000") {
 | 
				
			||||||
            res.status(400).send("-1").end();
 | 
					            const techProject = inventory.PersonalTechProjects.id(data.ResearchId)!;
 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const guildMember = (await GuildMember.findOne(
 | 
					            techProject.ReqCredits -= data.RegularCredits;
 | 
				
			||||||
            { accountId, guildId: guild._id },
 | 
					            const inventoryChanges: IInventoryChanges = updateCurrency(inventory, data.RegularCredits, false);
 | 
				
			||||||
            "RegularCreditsContributed MiscItemsContributed"
 | 
					 | 
				
			||||||
        ))!;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const contributions = data;
 | 
					            const miscItemChanges = [];
 | 
				
			||||||
        const techProject = guild.TechProjects!.find(x => x.ItemType == contributions.RecipeType)!;
 | 
					            for (const miscItem of data.MiscItems) {
 | 
				
			||||||
 | 
					 | 
				
			||||||
        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 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) {
 | 
				
			||||||
                        miscItem.ItemCount = reqItem.ItemCount;
 | 
					                        miscItem.ItemCount = reqItem.ItemCount;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    reqItem.ItemCount -= miscItem.ItemCount;
 | 
					                    reqItem.ItemCount -= miscItem.ItemCount;
 | 
				
			||||||
 | 
					                    miscItemChanges.push({
 | 
				
			||||||
                    const vaultMiscItem = guild.VaultMiscItems!.find(x => x.ItemType == miscItem.ItemType)!;
 | 
					                        ItemType: miscItem.ItemType,
 | 
				
			||||||
                    vaultMiscItem.ItemCount -= miscItem.ItemCount;
 | 
					                        ItemCount: miscItem.ItemCount * -1
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					            addMiscItems(inventory, miscItemChanges);
 | 
				
			||||||
 | 
					            inventoryChanges.MiscItems = miscItemChanges;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const miscItemChanges = [];
 | 
					            techProject.HasContributions = true;
 | 
				
			||||||
        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
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                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") {
 | 
					    } 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[];
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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",
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -185,14 +185,15 @@ 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(
 | 
				
			||||||
            `client requested key chain message in keychain ${KeyChain} at stage ${ChainStage} but they did not exist`
 | 
					            `client requested key chain message in keychain ${KeyChain} at stage ${ChainStage} but they did not exist`
 | 
				
			||||||
 | 
				
			|||||||
@ -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) {
 | 
				
			||||||
 | 
				
			|||||||
@ -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 {
 | 
				
			||||||
 | 
				
			|||||||
@ -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>
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
 | 
				
			|||||||
@ -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`,
 | 
				
			||||||
 | 
				
			|||||||
@ -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`,
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										160
									
								
								static/webui/translations/es.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								static/webui/translations/es.js
									
									
									
									
									
										Normal 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: ``
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -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`,
 | 
				
			||||||
 | 
				
			|||||||
@ -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: `Мгновенные Исследование Додзё`,
 | 
				
			||||||
 | 
				
			|||||||
@ -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`,
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user