chore(webui): update to Spanish translation #1772
@ -10,6 +10,8 @@ To get an idea of what functionality you can expect to be missing [have a look t
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## config.json
 | 
					## config.json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [config.json.example](config.json.example), which has most cheats disabled.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`.
 | 
					- `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`.
 | 
				
			||||||
- `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`.
 | 
					- `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`.
 | 
				
			||||||
- `worldState.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE.
 | 
					- `worldState.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE.
 | 
				
			||||||
 | 
				
			|||||||
@ -29,6 +29,7 @@
 | 
				
			|||||||
  "unlockExilusEverywhere": false,
 | 
					  "unlockExilusEverywhere": false,
 | 
				
			||||||
  "unlockArcanesEverywhere": false,
 | 
					  "unlockArcanesEverywhere": false,
 | 
				
			||||||
  "noDailyStandingLimits": false,
 | 
					  "noDailyStandingLimits": false,
 | 
				
			||||||
 | 
					  "noDailyFocusLimit": false,
 | 
				
			||||||
  "noArgonCrystalDecay": false,
 | 
					  "noArgonCrystalDecay": false,
 | 
				
			||||||
  "noMasteryRankUpCooldown": false,
 | 
					  "noMasteryRankUpCooldown": false,
 | 
				
			||||||
  "noVendorPurchaseLimits": true,
 | 
					  "noVendorPurchaseLimits": true,
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										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.54",
 | 
					        "warframe-public-export-plus": "^0.5.55",
 | 
				
			||||||
        "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.54",
 | 
					      "version": "0.5.55",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.54.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.55.tgz",
 | 
				
			||||||
      "integrity": "sha512-27r6qLErr3P8UVDiEzhDAs/BjdAS3vI2CQ58jSI+LClDlj6QL+y1jQe8va/npl3Ft2K8PywLkZ8Yso0j9YzvOA=="
 | 
					      "integrity": "sha512-Gnd4FCBVuxm2xWGfu8xxxqPIPSnnTqiEWlpP3rsFpVlQs09RNFnW2PEX9rCZt0f3SvHBv5ssDDrFlzgBHS1yrA=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "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.54",
 | 
					    "warframe-public-export-plus": "^0.5.55",
 | 
				
			||||||
    "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"
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,7 @@
 | 
				
			|||||||
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
 | 
					import { toMongoDate } from "@/src/helpers/inventoryHelpers";
 | 
				
			||||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
					import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
				
			||||||
import { Guild, GuildMember } from "@/src/models/guildModel";
 | 
					import { Guild } from "@/src/models/guildModel";
 | 
				
			||||||
import { config } from "@/src/services/configService";
 | 
					import { checkClanAscensionHasRequiredContributors } from "@/src/services/guildService";
 | 
				
			||||||
import { createMessage } from "@/src/services/inboxService";
 | 
					 | 
				
			||||||
import { getInventory } from "@/src/services/inventoryService";
 | 
					import { getInventory } from "@/src/services/inventoryService";
 | 
				
			||||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
					import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
				
			||||||
import { RequestHandler } from "express";
 | 
					import { RequestHandler } from "express";
 | 
				
			||||||
@ -31,43 +30,7 @@ export const contributeGuildClassController: RequestHandler = async (req, res) =
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    guild.CeremonyContributors.push(new Types.ObjectId(accountId));
 | 
					    guild.CeremonyContributors.push(new Types.ObjectId(accountId));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Once required contributor count is hit, the class is committed and there's 72 hours to claim endo.
 | 
					    await checkClanAscensionHasRequiredContributors(guild);
 | 
				
			||||||
    if (guild.CeremonyContributors.length == payload.RequiredContributors) {
 | 
					 | 
				
			||||||
        guild.Class = guild.CeremonyClass!;
 | 
					 | 
				
			||||||
        guild.CeremonyClass = undefined;
 | 
					 | 
				
			||||||
        guild.CeremonyResetDate = new Date(Date.now() + (config.fastClanAscension ? 5_000 : 72 * 3600_000));
 | 
					 | 
				
			||||||
        if (!config.fastClanAscension) {
 | 
					 | 
				
			||||||
            // Send message to all active guild members
 | 
					 | 
				
			||||||
            const members = await GuildMember.find({ guildId: payload.GuildId, status: 0 }, "accountId");
 | 
					 | 
				
			||||||
            for (const member of members) {
 | 
					 | 
				
			||||||
                // somewhat unfaithful as on live the "msg" is not a loctag, but since we don't have the string, we'll let the client fill it in with "arg".
 | 
					 | 
				
			||||||
                await createMessage(member.accountId, [
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        sndr: guild.Name,
 | 
					 | 
				
			||||||
                        msg: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgressDetails",
 | 
					 | 
				
			||||||
                        arg: [
 | 
					 | 
				
			||||||
                            {
 | 
					 | 
				
			||||||
                                Key: "RESETDATE",
 | 
					 | 
				
			||||||
                                Tag:
 | 
					 | 
				
			||||||
                                    guild.CeremonyResetDate.getUTCMonth() +
 | 
					 | 
				
			||||||
                                    "/" +
 | 
					 | 
				
			||||||
                                    guild.CeremonyResetDate.getUTCDate() +
 | 
					 | 
				
			||||||
                                    "/" +
 | 
					 | 
				
			||||||
                                    (guild.CeremonyResetDate.getUTCFullYear() % 100) +
 | 
					 | 
				
			||||||
                                    " " +
 | 
					 | 
				
			||||||
                                    guild.CeremonyResetDate.getUTCHours().toString().padStart(2, "0") +
 | 
					 | 
				
			||||||
                                    ":" +
 | 
					 | 
				
			||||||
                                    guild.CeremonyResetDate.getUTCMinutes().toString().padStart(2, "0")
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        ],
 | 
					 | 
				
			||||||
                        sub: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgress",
 | 
					 | 
				
			||||||
                        icon: "/Lotus/Interface/Graphics/ClanTileImages/ClanEnterDojo.png",
 | 
					 | 
				
			||||||
                        highPriority: true
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                ]);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await guild.save();
 | 
					    await guild.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										41
									
								
								src/controllers/api/crewShipIdentifySalvageController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/controllers/api/crewShipIdentifySalvageController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					import { addCrewShipSalvagedWeaponSkin, addCrewShipRawSalvage, getInventory } from "@/src/services/inventoryService";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
				
			||||||
 | 
					import { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { IInnateDamageFingerprint } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
				
			||||||
 | 
					import { ExportCustoms } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					import { IFingerprintStat } from "@/src/helpers/rivenHelper";
 | 
				
			||||||
 | 
					import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
				
			||||||
 | 
					import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const crewShipIdentifySalvageController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId, "CrewShipSalvagedWeaponSkins CrewShipRawSalvage");
 | 
				
			||||||
 | 
					    const payload = getJSONfromString<ICrewShipIdentifySalvageRequest>(String(req.body));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const buffs: IFingerprintStat[] = [];
 | 
				
			||||||
 | 
					    for (const upgrade of ExportCustoms[payload.ItemType].randomisedUpgrades!) {
 | 
				
			||||||
 | 
					        buffs.push({ Tag: upgrade.tag, Value: Math.trunc(Math.random() * 0x40000000) });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const inventoryChanges: IInventoryChanges = addCrewShipSalvagedWeaponSkin(
 | 
				
			||||||
 | 
					        inventory,
 | 
				
			||||||
 | 
					        payload.ItemType,
 | 
				
			||||||
 | 
					        JSON.stringify({ compat: payload.ItemType, buffs } satisfies IInnateDamageFingerprint)
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    inventoryChanges.CrewShipRawSalvage = [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            ItemType: payload.ItemType,
 | 
				
			||||||
 | 
					            ItemCount: -1
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					    addCrewShipRawSalvage(inventory, inventoryChanges.CrewShipRawSalvage);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        InventoryChanges: inventoryChanges
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ICrewShipIdentifySalvageRequest {
 | 
				
			||||||
 | 
					    ItemType: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,14 +1,12 @@
 | 
				
			|||||||
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
 | 
					import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
 | 
				
			||||||
import { generateRewardSeed } from "@/src/services/inventoryService";
 | 
					import { generateRewardSeed } from "@/src/services/inventoryService";
 | 
				
			||||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
					import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
				
			||||||
import { logger } from "@/src/utils/logger";
 | 
					 | 
				
			||||||
import { RequestHandler } from "express";
 | 
					import { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getNewRewardSeedController: RequestHandler = async (req, res) => {
 | 
					export const getNewRewardSeedController: RequestHandler = async (req, res) => {
 | 
				
			||||||
    const accountId = await getAccountIdForRequest(req);
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const rewardSeed = generateRewardSeed();
 | 
					    const rewardSeed = generateRewardSeed();
 | 
				
			||||||
    logger.debug(`generated new reward seed: ${rewardSeed}`);
 | 
					 | 
				
			||||||
    await Inventory.updateOne(
 | 
					    await Inventory.updateOne(
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            accountOwnerId: accountId
 | 
					            accountOwnerId: accountId
 | 
				
			||||||
 | 
				
			|||||||
@ -16,7 +16,7 @@ export const giveQuestKeyRewardController: RequestHandler = async (req, res) =>
 | 
				
			|||||||
    const inventory = await getInventory(accountId);
 | 
					    const inventory = await getInventory(accountId);
 | 
				
			||||||
    const inventoryChanges = await addItem(inventory, reward.ItemType, reward.Amount);
 | 
					    const inventoryChanges = await addItem(inventory, reward.ItemType, reward.Amount);
 | 
				
			||||||
    await inventory.save();
 | 
					    await inventory.save();
 | 
				
			||||||
    res.json(inventoryChanges.InventoryChanges);
 | 
					    res.json(inventoryChanges);
 | 
				
			||||||
    //TODO: consider whishlist changes
 | 
					    //TODO: consider whishlist changes
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										20
									
								
								src/controllers/api/giveShipDecoAndLoreFragmentController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/controllers/api/giveShipDecoAndLoreFragmentController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
				
			||||||
 | 
					import { addLoreFragmentScans, addShipDecorations, getInventory } from "@/src/services/inventoryService";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
				
			||||||
 | 
					import { ILoreFragmentScan, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
				
			||||||
 | 
					import { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const giveShipDecoAndLoreFragmentController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId, "LoreFragmentScans ShipDecorations");
 | 
				
			||||||
 | 
					    const data = getJSONfromString<IGiveShipDecoAndLoreFragmentRequest>(String(req.body));
 | 
				
			||||||
 | 
					    addLoreFragmentScans(inventory, data.LoreFragmentScans);
 | 
				
			||||||
 | 
					    addShipDecorations(inventory, data.ShipDecorations);
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					    res.end();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IGiveShipDecoAndLoreFragmentRequest {
 | 
				
			||||||
 | 
					    LoreFragmentScans: ILoreFragmentScan[];
 | 
				
			||||||
 | 
					    ShipDecorations: ITypeCount[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -14,20 +14,23 @@ import {
 | 
				
			|||||||
import { ExportDojoRecipes } from "warframe-public-export-plus";
 | 
					import { ExportDojoRecipes } from "warframe-public-export-plus";
 | 
				
			||||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
					import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
 | 
					    addCrewShipWeaponSkin,
 | 
				
			||||||
    addItem,
 | 
					    addItem,
 | 
				
			||||||
    addMiscItems,
 | 
					    addMiscItems,
 | 
				
			||||||
    addRecipes,
 | 
					    addRecipes,
 | 
				
			||||||
    combineInventoryChanges,
 | 
					    combineInventoryChanges,
 | 
				
			||||||
    getInventory,
 | 
					    getInventory,
 | 
				
			||||||
 | 
					    occupySlot,
 | 
				
			||||||
    updateCurrency
 | 
					    updateCurrency
 | 
				
			||||||
} from "@/src/services/inventoryService";
 | 
					} from "@/src/services/inventoryService";
 | 
				
			||||||
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
					import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
				
			||||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
					import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
				
			||||||
import { config } from "@/src/services/configService";
 | 
					import { config } from "@/src/services/configService";
 | 
				
			||||||
import { GuildPermission, ITechProjectClient } from "@/src/types/guildTypes";
 | 
					import { GuildPermission, ITechProjectClient } from "@/src/types/guildTypes";
 | 
				
			||||||
import { GuildMember } from "@/src/models/guildModel";
 | 
					import { GuildMember } from "@/src/models/guildModel";
 | 
				
			||||||
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
 | 
					import { toMongoDate } from "@/src/helpers/inventoryHelpers";
 | 
				
			||||||
import { logger } from "@/src/utils/logger";
 | 
					import { logger } from "@/src/utils/logger";
 | 
				
			||||||
 | 
					import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const guildTechController: RequestHandler = async (req, res) => {
 | 
					export const guildTechController: RequestHandler = async (req, res) => {
 | 
				
			||||||
    const accountId = await getAccountIdForRequest(req);
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
@ -99,6 +102,8 @@ export const guildTechController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
                        State: 0,
 | 
					                        State: 0,
 | 
				
			||||||
                        ReqCredits: recipe.price,
 | 
					                        ReqCredits: recipe.price,
 | 
				
			||||||
                        ItemType: data.RecipeType,
 | 
					                        ItemType: data.RecipeType,
 | 
				
			||||||
 | 
					                        ProductCategory: data.TechProductCategory,
 | 
				
			||||||
 | 
					                        CategoryItemId: data.CategoryItemId,
 | 
				
			||||||
                        ReqItems: recipe.ingredients
 | 
					                        ReqItems: recipe.ingredients
 | 
				
			||||||
                    }) - 1
 | 
					                    }) - 1
 | 
				
			||||||
                ];
 | 
					                ];
 | 
				
			||||||
@ -222,33 +227,44 @@ export const guildTechController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
            });
 | 
					            });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    } 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))) {
 | 
					 | 
				
			||||||
            res.status(400).send("-1").end();
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        const purchase = data as IGuildTechBuyRequest;
 | 
					        const purchase = data as IGuildTechBuyRequest;
 | 
				
			||||||
        const quantity = parseInt(data.Action.split(",")[1]);
 | 
					        if (purchase.Mode == "Guild") {
 | 
				
			||||||
        const recipeChanges = [
 | 
					            const guild = await getGuildForRequestEx(req, inventory);
 | 
				
			||||||
            {
 | 
					            if (
 | 
				
			||||||
                ItemType: purchase.RecipeType,
 | 
					                !hasAccessToDojo(inventory) ||
 | 
				
			||||||
                ItemCount: quantity
 | 
					                !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					                res.status(400).send("-1").end();
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        ];
 | 
					            const quantity = parseInt(data.Action.split(",")[1]);
 | 
				
			||||||
        addRecipes(inventory, recipeChanges);
 | 
					            const recipeChanges = [
 | 
				
			||||||
        const currencyChanges = updateCurrency(
 | 
					                {
 | 
				
			||||||
            inventory,
 | 
					                    ItemType: purchase.RecipeType,
 | 
				
			||||||
            ExportDojoRecipes.research[purchase.RecipeType].replicatePrice,
 | 
					                    ItemCount: quantity
 | 
				
			||||||
            false
 | 
					                }
 | 
				
			||||||
        );
 | 
					            ];
 | 
				
			||||||
        await inventory.save();
 | 
					            addRecipes(inventory, recipeChanges);
 | 
				
			||||||
        // Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`.
 | 
					            const currencyChanges = updateCurrency(
 | 
				
			||||||
        res.json({
 | 
					                inventory,
 | 
				
			||||||
            inventoryChanges: {
 | 
					                ExportDojoRecipes.research[purchase.RecipeType].replicatePrice,
 | 
				
			||||||
                ...currencyChanges,
 | 
					                false
 | 
				
			||||||
                Recipes: recipeChanges
 | 
					            );
 | 
				
			||||||
            }
 | 
					            await inventory.save();
 | 
				
			||||||
        });
 | 
					            // Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`.
 | 
				
			||||||
 | 
					            res.json({
 | 
				
			||||||
 | 
					                inventoryChanges: {
 | 
				
			||||||
 | 
					                    ...currencyChanges,
 | 
				
			||||||
 | 
					                    Recipes: recipeChanges
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            const inventoryChanges = claimSalvagedComponent(inventory, purchase.CategoryItemId!);
 | 
				
			||||||
 | 
					            await inventory.save();
 | 
				
			||||||
 | 
					            res.json({
 | 
				
			||||||
 | 
					                inventoryChanges: inventoryChanges
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    } else if (data.Action == "Fabricate") {
 | 
					    } else if (data.Action == "Fabricate") {
 | 
				
			||||||
        const guild = await getGuildForRequestEx(req, inventory);
 | 
					        const guild = await getGuildForRequestEx(req, inventory);
 | 
				
			||||||
        if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) {
 | 
					        if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) {
 | 
				
			||||||
@ -289,9 +305,18 @@ export const guildTechController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
        guild.ActiveDojoColorResearch = data.RecipeType;
 | 
					        guild.ActiveDojoColorResearch = data.RecipeType;
 | 
				
			||||||
        await guild.save();
 | 
					        await guild.save();
 | 
				
			||||||
        res.end();
 | 
					        res.end();
 | 
				
			||||||
 | 
					    } else if (data.Action == "Rush" && data.CategoryItemId) {
 | 
				
			||||||
 | 
					        const inventoryChanges: IInventoryChanges = {
 | 
				
			||||||
 | 
					            ...updateCurrency(inventory, 20, true),
 | 
				
			||||||
 | 
					            ...claimSalvagedComponent(inventory, data.CategoryItemId)
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        await inventory.save();
 | 
				
			||||||
 | 
					        res.json({
 | 
				
			||||||
 | 
					            inventoryChanges: inventoryChanges
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
 | 
					        logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
 | 
				
			||||||
        throw new Error(`unknown guildTech action: ${data.Action}`);
 | 
					        throw new Error(`unhandled guildTech request`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -301,15 +326,15 @@ type TGuildTechRequest =
 | 
				
			|||||||
    | IGuildTechContributeRequest;
 | 
					    | IGuildTechContributeRequest;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface IGuildTechBasicRequest {
 | 
					interface IGuildTechBasicRequest {
 | 
				
			||||||
    Action: "Start" | "Fabricate" | "Pause" | "Unpause";
 | 
					    Action: "Start" | "Fabricate" | "Pause" | "Unpause" | "Cancel" | "Rush";
 | 
				
			||||||
    Mode: "Guild" | "Personal";
 | 
					    Mode: "Guild" | "Personal";
 | 
				
			||||||
    RecipeType: string;
 | 
					    RecipeType: string;
 | 
				
			||||||
 | 
					    TechProductCategory?: string;
 | 
				
			||||||
 | 
					    CategoryItemId?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface IGuildTechBuyRequest {
 | 
					interface IGuildTechBuyRequest extends Omit<IGuildTechBasicRequest, "Action"> {
 | 
				
			||||||
    Action: string;
 | 
					    Action: string;
 | 
				
			||||||
    Mode: "Guild";
 | 
					 | 
				
			||||||
    RecipeType: string;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface IGuildTechContributeRequest {
 | 
					interface IGuildTechContributeRequest {
 | 
				
			||||||
@ -321,3 +346,30 @@ interface IGuildTechContributeRequest {
 | 
				
			|||||||
    VaultCredits: number;
 | 
					    VaultCredits: number;
 | 
				
			||||||
    VaultMiscItems: IMiscItem[];
 | 
					    VaultMiscItems: IMiscItem[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const claimSalvagedComponent = (inventory: TInventoryDatabaseDocument, itemId: string): IInventoryChanges => {
 | 
				
			||||||
 | 
					    // delete personal tech project
 | 
				
			||||||
 | 
					    const personalTechProjectIndex = inventory.PersonalTechProjects.findIndex(x => x.CategoryItemId?.equals(itemId));
 | 
				
			||||||
 | 
					    if (personalTechProjectIndex != -1) {
 | 
				
			||||||
 | 
					        inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // find salved part & delete it
 | 
				
			||||||
 | 
					    const crewShipSalvagedWeaponSkinsIndex = inventory.CrewShipSalvagedWeaponSkins.findIndex(x => x._id.equals(itemId));
 | 
				
			||||||
 | 
					    const crewShipWeaponSkin = inventory.CrewShipSalvagedWeaponSkins[crewShipSalvagedWeaponSkinsIndex];
 | 
				
			||||||
 | 
					    inventory.CrewShipSalvagedWeaponSkins.splice(crewShipSalvagedWeaponSkinsIndex, 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // add final item
 | 
				
			||||||
 | 
					    const inventoryChanges = {
 | 
				
			||||||
 | 
					        ...addCrewShipWeaponSkin(inventory, crewShipWeaponSkin.ItemType, crewShipWeaponSkin.UpgradeFingerprint),
 | 
				
			||||||
 | 
					        ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false)
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    inventoryChanges.RemovedIdItems = [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            ItemId: { $oid: itemId }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return inventoryChanges;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -149,7 +149,7 @@ export const getInventoryResponse = async (
 | 
				
			|||||||
        inventoryResponse.ShipDecorations = [];
 | 
					        inventoryResponse.ShipDecorations = [];
 | 
				
			||||||
        for (const [uniqueName, item] of Object.entries(ExportResources)) {
 | 
					        for (const [uniqueName, item] of Object.entries(ExportResources)) {
 | 
				
			||||||
            if (item.productCategory == "ShipDecorations") {
 | 
					            if (item.productCategory == "ShipDecorations") {
 | 
				
			||||||
                inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 1 });
 | 
					                inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 999_999 });
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -258,6 +258,10 @@ export const getInventoryResponse = async (
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (config.noDailyFocusLimit) {
 | 
				
			||||||
 | 
					        inventoryResponse.DailyFocus = Math.max(999_999, 250000 + inventoryResponse.PlayerLevel * 5000);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (inventoryResponse.InfestedFoundry) {
 | 
					    if (inventoryResponse.InfestedFoundry) {
 | 
				
			||||||
        applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry);
 | 
					        applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -6,9 +6,12 @@ import {
 | 
				
			|||||||
    addRecipes,
 | 
					    addRecipes,
 | 
				
			||||||
    addMiscItems,
 | 
					    addMiscItems,
 | 
				
			||||||
    addConsumables,
 | 
					    addConsumables,
 | 
				
			||||||
    freeUpSlot
 | 
					    freeUpSlot,
 | 
				
			||||||
 | 
					    combineInventoryChanges
 | 
				
			||||||
} from "@/src/services/inventoryService";
 | 
					} from "@/src/services/inventoryService";
 | 
				
			||||||
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
					import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
				
			||||||
 | 
					import { ExportDojoRecipes } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const sellController: RequestHandler = async (req, res) => {
 | 
					export const sellController: RequestHandler = async (req, res) => {
 | 
				
			||||||
    const payload = JSON.parse(String(req.body)) as ISellRequest;
 | 
					    const payload = JSON.parse(String(req.body)) as ISellRequest;
 | 
				
			||||||
@ -48,6 +51,9 @@ export const sellController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
    if (payload.Items.Hoverboards) {
 | 
					    if (payload.Items.Hoverboards) {
 | 
				
			||||||
        requiredFields.add(InventorySlot.SPACESUITS);
 | 
					        requiredFields.add(InventorySlot.SPACESUITS);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (payload.Items.CrewShipWeapons) {
 | 
				
			||||||
 | 
					        requiredFields.add(InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    const inventory = await getInventory(accountId, Array.from(requiredFields).join(" "));
 | 
					    const inventory = await getInventory(accountId, Array.from(requiredFields).join(" "));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Give currency
 | 
					    // Give currency
 | 
				
			||||||
@ -69,10 +75,14 @@ export const sellController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
                ItemCount: payload.SellPrice
 | 
					                ItemCount: payload.SellPrice
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
 | 
					    } else if (payload.SellCurrency == "SC_Resources") {
 | 
				
			||||||
 | 
					        // Will add appropriate MiscItems from CrewShipWeapons
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        throw new Error("Unknown SellCurrency: " + payload.SellCurrency);
 | 
					        throw new Error("Unknown SellCurrency: " + payload.SellCurrency);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const inventoryChanges: IInventoryChanges = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Remove item(s)
 | 
					    // Remove item(s)
 | 
				
			||||||
    if (payload.Items.Suits) {
 | 
					    if (payload.Items.Suits) {
 | 
				
			||||||
        payload.Items.Suits.forEach(sellItem => {
 | 
					        payload.Items.Suits.forEach(sellItem => {
 | 
				
			||||||
@ -145,6 +155,24 @@ export const sellController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
            inventory.Drones.pull({ _id: sellItem.String });
 | 
					            inventory.Drones.pull({ _id: sellItem.String });
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (payload.Items.CrewShipWeapons) {
 | 
				
			||||||
 | 
					        payload.Items.CrewShipWeapons.forEach(sellItem => {
 | 
				
			||||||
 | 
					            const index = inventory.CrewShipWeapons.findIndex(x => x._id.equals(sellItem.String));
 | 
				
			||||||
 | 
					            if (index != -1) {
 | 
				
			||||||
 | 
					                const itemType = inventory.CrewShipWeapons[index].ItemType;
 | 
				
			||||||
 | 
					                const recipe = Object.values(ExportDojoRecipes.fabrications).find(x => x.resultType == itemType)!;
 | 
				
			||||||
 | 
					                const miscItemChanges = recipe.ingredients.map(x => ({
 | 
				
			||||||
 | 
					                    ItemType: x.ItemType,
 | 
				
			||||||
 | 
					                    ItemCount: Math.trunc(x.ItemCount * 0.8)
 | 
				
			||||||
 | 
					                }));
 | 
				
			||||||
 | 
					                addMiscItems(inventory, miscItemChanges);
 | 
				
			||||||
 | 
					                combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                inventory.CrewShipWeapons.splice(index, 1);
 | 
				
			||||||
 | 
					                freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    if (payload.Items.Consumables) {
 | 
					    if (payload.Items.Consumables) {
 | 
				
			||||||
        const consumablesChanges = [];
 | 
					        const consumablesChanges = [];
 | 
				
			||||||
        for (const sellItem of payload.Items.Consumables) {
 | 
					        for (const sellItem of payload.Items.Consumables) {
 | 
				
			||||||
@ -191,7 +219,9 @@ export const sellController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await inventory.save();
 | 
					    await inventory.save();
 | 
				
			||||||
    res.json({});
 | 
					    res.json({
 | 
				
			||||||
 | 
					        inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges"
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface ISellRequest {
 | 
					interface ISellRequest {
 | 
				
			||||||
@ -212,6 +242,7 @@ interface ISellRequest {
 | 
				
			|||||||
        OperatorAmps?: ISellItem[];
 | 
					        OperatorAmps?: ISellItem[];
 | 
				
			||||||
        Hoverboards?: ISellItem[];
 | 
					        Hoverboards?: ISellItem[];
 | 
				
			||||||
        Drones?: ISellItem[];
 | 
					        Drones?: ISellItem[];
 | 
				
			||||||
 | 
					        CrewShipWeapons?: ISellItem[];
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    SellPrice: number;
 | 
					    SellPrice: number;
 | 
				
			||||||
    SellCurrency:
 | 
					    SellCurrency:
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
					import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
				
			||||||
import { ISetPlacedDecoInfoRequest } from "@/src/types/shipTypes";
 | 
					import { IPictureFrameInfo, ISetPlacedDecoInfoRequest } from "@/src/types/shipTypes";
 | 
				
			||||||
import { RequestHandler } from "express";
 | 
					import { RequestHandler } from "express";
 | 
				
			||||||
import { handleSetPlacedDecoInfo } from "@/src/services/shipCustomizationsService";
 | 
					import { handleSetPlacedDecoInfo } from "@/src/services/shipCustomizationsService";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -7,5 +7,17 @@ export const setPlacedDecoInfoController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
    const accountId = await getAccountIdForRequest(req);
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
    const payload = JSON.parse(req.body as string) as ISetPlacedDecoInfoRequest;
 | 
					    const payload = JSON.parse(req.body as string) as ISetPlacedDecoInfoRequest;
 | 
				
			||||||
    await handleSetPlacedDecoInfo(accountId, payload);
 | 
					    await handleSetPlacedDecoInfo(accountId, payload);
 | 
				
			||||||
    res.end();
 | 
					    res.json({
 | 
				
			||||||
 | 
					        DecoId: payload.DecoId,
 | 
				
			||||||
 | 
					        IsPicture: true,
 | 
				
			||||||
 | 
					        PictureFrameInfo: payload.PictureFrameInfo,
 | 
				
			||||||
 | 
					        BootLocation: payload.BootLocation
 | 
				
			||||||
 | 
					    } satisfies ISetPlacedDecoInfoResponse);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ISetPlacedDecoInfoResponse {
 | 
				
			||||||
 | 
					    DecoId: string;
 | 
				
			||||||
 | 
					    IsPicture: boolean;
 | 
				
			||||||
 | 
					    PictureFrameInfo?: IPictureFrameInfo;
 | 
				
			||||||
 | 
					    BootLocation?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -25,7 +25,13 @@ export const upgradesController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
            operation.UpgradeRequirement == "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker"
 | 
					            operation.UpgradeRequirement == "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker"
 | 
				
			||||||
        ) {
 | 
					        ) {
 | 
				
			||||||
            updateCurrency(inventory, 10, true);
 | 
					            updateCurrency(inventory, 10, true);
 | 
				
			||||||
        } else if (operation.OperationType != "UOT_ABILITY_OVERRIDE") {
 | 
					        } else if (
 | 
				
			||||||
 | 
					            operation.OperationType != "UOT_SWAP_POLARITY" &&
 | 
				
			||||||
 | 
					            operation.OperationType != "UOT_ABILITY_OVERRIDE"
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					            if (!operation.UpgradeRequirement) {
 | 
				
			||||||
 | 
					                throw new Error(`${operation.OperationType} operation should be free?`);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            addMiscItems(inventory, [
 | 
					            addMiscItems(inventory, [
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    ItemType: operation.UpgradeRequirement,
 | 
					                    ItemType: operation.UpgradeRequirement,
 | 
				
			||||||
 | 
				
			|||||||
@ -3,6 +3,7 @@ import { getDict, getItemName, getString } from "@/src/services/itemDataService"
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
    ExportArcanes,
 | 
					    ExportArcanes,
 | 
				
			||||||
    ExportAvionics,
 | 
					    ExportAvionics,
 | 
				
			||||||
 | 
					    ExportCustoms,
 | 
				
			||||||
    ExportDrones,
 | 
					    ExportDrones,
 | 
				
			||||||
    ExportGear,
 | 
					    ExportGear,
 | 
				
			||||||
    ExportKeys,
 | 
					    ExportKeys,
 | 
				
			||||||
@ -171,6 +172,12 @@ const getItemListsController: RequestHandler = (req, response) => {
 | 
				
			|||||||
            name: getString(item.name, lang)
 | 
					            name: getString(item.name, lang)
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    for (const [uniqueName, item] of Object.entries(ExportCustoms)) {
 | 
				
			||||||
 | 
					        res.miscitems.push({
 | 
				
			||||||
 | 
					            uniqueName: uniqueName,
 | 
				
			||||||
 | 
					            name: getString(item.name, lang)
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.mods = [];
 | 
					    res.mods = [];
 | 
				
			||||||
    for (const [uniqueName, upgrade] of Object.entries(ExportUpgrades)) {
 | 
					    for (const [uniqueName, upgrade] of Object.entries(ExportUpgrades)) {
 | 
				
			||||||
 | 
				
			|||||||
@ -56,15 +56,12 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
            break;
 | 
					            break;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        case "deleteKey": {
 | 
					        case "deleteKey": {
 | 
				
			||||||
            if (allQuestKeys.includes(questItemType)) {
 | 
					            const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType);
 | 
				
			||||||
                const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType);
 | 
					            if (!questKey) {
 | 
				
			||||||
                if (!questKey) {
 | 
					                logger.error(`Quest key not found in inventory: ${questItemType}`);
 | 
				
			||||||
                    logger.error(`Quest key not found in inventory: ${questItemType}`);
 | 
					                break;
 | 
				
			||||||
                    break;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                inventory.QuestKeys.pull({ ItemType: questItemType });
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            inventory.QuestKeys.pull({ ItemType: questItemType });
 | 
				
			||||||
            break;
 | 
					            break;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        case "completeKey": {
 | 
					        case "completeKey": {
 | 
				
			||||||
 | 
				
			|||||||
@ -504,6 +504,8 @@ const personalTechProjectSchema = new Schema<IPersonalTechProjectDatabase>({
 | 
				
			|||||||
    State: Number,
 | 
					    State: Number,
 | 
				
			||||||
    ReqCredits: Number,
 | 
					    ReqCredits: Number,
 | 
				
			||||||
    ItemType: String,
 | 
					    ItemType: String,
 | 
				
			||||||
 | 
					    ProductCategory: String,
 | 
				
			||||||
 | 
					    CategoryItemId: Schema.Types.ObjectId,
 | 
				
			||||||
    ReqItems: { type: [typeCountSchema], default: undefined },
 | 
					    ReqItems: { type: [typeCountSchema], default: undefined },
 | 
				
			||||||
    HasContributions: Boolean,
 | 
					    HasContributions: Boolean,
 | 
				
			||||||
    CompletionDate: Date
 | 
					    CompletionDate: Date
 | 
				
			||||||
@ -522,6 +524,9 @@ personalTechProjectSchema.set("toJSON", {
 | 
				
			|||||||
        const db = ret as IPersonalTechProjectDatabase;
 | 
					        const db = ret as IPersonalTechProjectDatabase;
 | 
				
			||||||
        const client = ret as IPersonalTechProjectClient;
 | 
					        const client = ret as IPersonalTechProjectClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (db.CategoryItemId) {
 | 
				
			||||||
 | 
					            client.CategoryItemId = toOid(db.CategoryItemId);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        if (db.CompletionDate) {
 | 
					        if (db.CompletionDate) {
 | 
				
			||||||
            client.CompletionDate = toMongoDate(db.CompletionDate);
 | 
					            client.CompletionDate = toMongoDate(db.CompletionDate);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -1213,7 +1218,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
 | 
				
			|||||||
        accountOwnerId: Schema.Types.ObjectId,
 | 
					        accountOwnerId: Schema.Types.ObjectId,
 | 
				
			||||||
        SubscribedToEmails: { type: Number, default: 0 },
 | 
					        SubscribedToEmails: { type: Number, default: 0 },
 | 
				
			||||||
        SubscribedToEmailsPersonalized: { type: Number, default: 0 },
 | 
					        SubscribedToEmailsPersonalized: { type: Number, default: 0 },
 | 
				
			||||||
        RewardSeed: Number,
 | 
					        RewardSeed: BigInt,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        //Credit
 | 
					        //Credit
 | 
				
			||||||
        RegularCredits: { type: Number, default: 0 },
 | 
					        RegularCredits: { type: Number, default: 0 },
 | 
				
			||||||
@ -1613,7 +1618,7 @@ export type InventoryDocumentProps = {
 | 
				
			|||||||
    QuestKeys: Types.DocumentArray<IQuestKeyDatabase>;
 | 
					    QuestKeys: Types.DocumentArray<IQuestKeyDatabase>;
 | 
				
			||||||
    Drones: Types.DocumentArray<IDroneDatabase>;
 | 
					    Drones: Types.DocumentArray<IDroneDatabase>;
 | 
				
			||||||
    CrewShipWeaponSkins: Types.DocumentArray<IUpgradeDatabase>;
 | 
					    CrewShipWeaponSkins: Types.DocumentArray<IUpgradeDatabase>;
 | 
				
			||||||
    CrewShipSalvagedWeaponsSkins: Types.DocumentArray<IUpgradeDatabase>;
 | 
					    CrewShipSalvagedWeaponSkins: Types.DocumentArray<IUpgradeDatabase>;
 | 
				
			||||||
    PersonalTechProjects: Types.DocumentArray<IPersonalTechProjectDatabase>;
 | 
					    PersonalTechProjects: Types.DocumentArray<IPersonalTechProjectDatabase>;
 | 
				
			||||||
} & { [K in TEquipmentKey]: Types.DocumentArray<IEquipmentDatabase> };
 | 
					} & { [K in TEquipmentKey]: Types.DocumentArray<IEquipmentDatabase> };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -27,6 +27,7 @@ import { contributeToVaultController } from "@/src/controllers/api/contributeToV
 | 
				
			|||||||
import { createAllianceController } from "@/src/controllers/api/createAllianceController";
 | 
					import { createAllianceController } from "@/src/controllers/api/createAllianceController";
 | 
				
			||||||
import { createGuildController } from "@/src/controllers/api/createGuildController";
 | 
					import { createGuildController } from "@/src/controllers/api/createGuildController";
 | 
				
			||||||
import { creditsController } from "@/src/controllers/api/creditsController";
 | 
					import { creditsController } from "@/src/controllers/api/creditsController";
 | 
				
			||||||
 | 
					import { crewShipIdentifySalvageController } from "@/src/controllers/api/crewShipIdentifySalvageController";
 | 
				
			||||||
import { customizeGuildRanksController } from "@/src/controllers/api/customizeGuildRanksController";
 | 
					import { customizeGuildRanksController } from "@/src/controllers/api/customizeGuildRanksController";
 | 
				
			||||||
import { customObstacleCourseLeaderboardController } from "@/src/controllers/api/customObstacleCourseLeaderboardController";
 | 
					import { customObstacleCourseLeaderboardController } from "@/src/controllers/api/customObstacleCourseLeaderboardController";
 | 
				
			||||||
import { declineAllianceInviteController } from "@/src/controllers/api/declineAllianceInviteController";
 | 
					import { declineAllianceInviteController } from "@/src/controllers/api/declineAllianceInviteController";
 | 
				
			||||||
@ -61,7 +62,8 @@ import { giftingController } from "@/src/controllers/api/giftingController";
 | 
				
			|||||||
import { gildWeaponController } from "@/src/controllers/api/gildWeaponController";
 | 
					import { gildWeaponController } from "@/src/controllers/api/gildWeaponController";
 | 
				
			||||||
import { giveKeyChainTriggeredItemsController } from "@/src/controllers/api/giveKeyChainTriggeredItemsController";
 | 
					import { giveKeyChainTriggeredItemsController } from "@/src/controllers/api/giveKeyChainTriggeredItemsController";
 | 
				
			||||||
import { giveKeyChainTriggeredMessageController } from "@/src/controllers/api/giveKeyChainTriggeredMessageController";
 | 
					import { giveKeyChainTriggeredMessageController } from "@/src/controllers/api/giveKeyChainTriggeredMessageController";
 | 
				
			||||||
import { giveQuestKeyRewardController } from "@/src/controllers/api/giveQuestKey";
 | 
					import { giveQuestKeyRewardController } from "@/src/controllers/api/giveQuestKeyRewardController";
 | 
				
			||||||
 | 
					import { giveShipDecoAndLoreFragmentController } from "@/src/controllers/api/giveShipDecoAndLoreFragmentController";
 | 
				
			||||||
import { giveStartingGearController } from "@/src/controllers/api/giveStartingGearController";
 | 
					import { giveStartingGearController } from "@/src/controllers/api/giveStartingGearController";
 | 
				
			||||||
import { guildTechController } from "@/src/controllers/api/guildTechController";
 | 
					import { guildTechController } from "@/src/controllers/api/guildTechController";
 | 
				
			||||||
import { hostSessionController } from "@/src/controllers/api/hostSessionController";
 | 
					import { hostSessionController } from "@/src/controllers/api/hostSessionController";
 | 
				
			||||||
@ -217,6 +219,7 @@ apiRouter.post("/contributeToDojoComponent.php", contributeToDojoComponentContro
 | 
				
			|||||||
apiRouter.post("/contributeToVault.php", contributeToVaultController);
 | 
					apiRouter.post("/contributeToVault.php", contributeToVaultController);
 | 
				
			||||||
apiRouter.post("/createAlliance.php", createAllianceController);
 | 
					apiRouter.post("/createAlliance.php", createAllianceController);
 | 
				
			||||||
apiRouter.post("/createGuild.php", createGuildController);
 | 
					apiRouter.post("/createGuild.php", createGuildController);
 | 
				
			||||||
 | 
					apiRouter.post("/crewShipIdentifySalvage.php", crewShipIdentifySalvageController);
 | 
				
			||||||
apiRouter.post("/customizeGuildRanks.php", customizeGuildRanksController);
 | 
					apiRouter.post("/customizeGuildRanks.php", customizeGuildRanksController);
 | 
				
			||||||
apiRouter.post("/customObstacleCourseLeaderboard.php", customObstacleCourseLeaderboardController);
 | 
					apiRouter.post("/customObstacleCourseLeaderboard.php", customObstacleCourseLeaderboardController);
 | 
				
			||||||
apiRouter.post("/destroyDojoDeco.php", destroyDojoDecoController);
 | 
					apiRouter.post("/destroyDojoDeco.php", destroyDojoDecoController);
 | 
				
			||||||
@ -239,6 +242,7 @@ apiRouter.post("/gildWeapon.php", gildWeaponController);
 | 
				
			|||||||
apiRouter.post("/giveKeyChainTriggeredItems.php", giveKeyChainTriggeredItemsController);
 | 
					apiRouter.post("/giveKeyChainTriggeredItems.php", giveKeyChainTriggeredItemsController);
 | 
				
			||||||
apiRouter.post("/giveKeyChainTriggeredMessage.php", giveKeyChainTriggeredMessageController);
 | 
					apiRouter.post("/giveKeyChainTriggeredMessage.php", giveKeyChainTriggeredMessageController);
 | 
				
			||||||
apiRouter.post("/giveQuestKeyReward.php", giveQuestKeyRewardController);
 | 
					apiRouter.post("/giveQuestKeyReward.php", giveQuestKeyRewardController);
 | 
				
			||||||
 | 
					apiRouter.post("/giveShipDecoAndLoreFragment.php", giveShipDecoAndLoreFragmentController);
 | 
				
			||||||
apiRouter.post("/giveStartingGear.php", giveStartingGearController);
 | 
					apiRouter.post("/giveStartingGear.php", giveStartingGearController);
 | 
				
			||||||
apiRouter.post("/guildTech.php", guildTechController);
 | 
					apiRouter.post("/guildTech.php", guildTechController);
 | 
				
			||||||
apiRouter.post("/hostSession.php", hostSessionController);
 | 
					apiRouter.post("/hostSession.php", hostSessionController);
 | 
				
			||||||
 | 
				
			|||||||
@ -35,6 +35,7 @@ interface IConfig {
 | 
				
			|||||||
    unlockExilusEverywhere?: boolean;
 | 
					    unlockExilusEverywhere?: boolean;
 | 
				
			||||||
    unlockArcanesEverywhere?: boolean;
 | 
					    unlockArcanesEverywhere?: boolean;
 | 
				
			||||||
    noDailyStandingLimits?: boolean;
 | 
					    noDailyStandingLimits?: boolean;
 | 
				
			||||||
 | 
					    noDailyFocusLimit?: boolean;
 | 
				
			||||||
    noArgonCrystalDecay?: boolean;
 | 
					    noArgonCrystalDecay?: boolean;
 | 
				
			||||||
    noMasteryRankUpCooldown?: boolean;
 | 
					    noMasteryRankUpCooldown?: boolean;
 | 
				
			||||||
    noVendorPurchaseLimits?: boolean;
 | 
					    noVendorPurchaseLimits?: boolean;
 | 
				
			||||||
 | 
				
			|||||||
@ -31,6 +31,7 @@ import { IFusionTreasure, ITypeCount } from "../types/inventoryTypes/inventoryTy
 | 
				
			|||||||
import { IInventoryChanges } from "../types/purchaseTypes";
 | 
					import { IInventoryChanges } from "../types/purchaseTypes";
 | 
				
			||||||
import { parallelForeach } from "../utils/async-utils";
 | 
					import { parallelForeach } from "../utils/async-utils";
 | 
				
			||||||
import allDecoRecipes from "@/static/fixed_responses/allDecoRecipes.json";
 | 
					import allDecoRecipes from "@/static/fixed_responses/allDecoRecipes.json";
 | 
				
			||||||
 | 
					import { createMessage } from "./inboxService";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getGuildForRequest = async (req: Request): Promise<TGuildDatabaseDocument> => {
 | 
					export const getGuildForRequest = async (req: Request): Promise<TGuildDatabaseDocument> => {
 | 
				
			||||||
    const accountId = await getAccountIdForRequest(req);
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
@ -601,6 +602,50 @@ const setGuildTier = async (guild: TGuildDatabaseDocument, newTier: number): Pro
 | 
				
			|||||||
            await processGuildTechProjectContributionsUpdate(guild, project);
 | 
					            await processGuildTechProjectContributionsUpdate(guild, project);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (guild.CeremonyContributors) {
 | 
				
			||||||
 | 
					        await checkClanAscensionHasRequiredContributors(guild);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const checkClanAscensionHasRequiredContributors = async (guild: TGuildDatabaseDocument): Promise<void> => {
 | 
				
			||||||
 | 
					    const requiredContributors = [1, 5, 15, 30, 50][guild.Tier - 1];
 | 
				
			||||||
 | 
					    // Once required contributor count is hit, the class is committed and there's 72 hours to claim endo.
 | 
				
			||||||
 | 
					    if (guild.CeremonyContributors!.length >= requiredContributors) {
 | 
				
			||||||
 | 
					        guild.Class = guild.CeremonyClass!;
 | 
				
			||||||
 | 
					        guild.CeremonyClass = undefined;
 | 
				
			||||||
 | 
					        guild.CeremonyResetDate = new Date(Date.now() + (config.fastClanAscension ? 5_000 : 72 * 3600_000));
 | 
				
			||||||
 | 
					        if (!config.fastClanAscension) {
 | 
				
			||||||
 | 
					            // Send message to all active guild members
 | 
				
			||||||
 | 
					            const members = await GuildMember.find({ guildId: guild._id, status: 0 }, "accountId");
 | 
				
			||||||
 | 
					            await parallelForeach(members, async member => {
 | 
				
			||||||
 | 
					                // somewhat unfaithful as on live the "msg" is not a loctag, but since we don't have the string, we'll let the client fill it in with "arg".
 | 
				
			||||||
 | 
					                await createMessage(member.accountId, [
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        sndr: guild.Name,
 | 
				
			||||||
 | 
					                        msg: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgressDetails",
 | 
				
			||||||
 | 
					                        arg: [
 | 
				
			||||||
 | 
					                            {
 | 
				
			||||||
 | 
					                                Key: "RESETDATE",
 | 
				
			||||||
 | 
					                                Tag:
 | 
				
			||||||
 | 
					                                    guild.CeremonyResetDate!.getUTCMonth() +
 | 
				
			||||||
 | 
					                                    "/" +
 | 
				
			||||||
 | 
					                                    guild.CeremonyResetDate!.getUTCDate() +
 | 
				
			||||||
 | 
					                                    "/" +
 | 
				
			||||||
 | 
					                                    (guild.CeremonyResetDate!.getUTCFullYear() % 100) +
 | 
				
			||||||
 | 
					                                    " " +
 | 
				
			||||||
 | 
					                                    guild.CeremonyResetDate!.getUTCHours().toString().padStart(2, "0") +
 | 
				
			||||||
 | 
					                                    ":" +
 | 
				
			||||||
 | 
					                                    guild.CeremonyResetDate!.getUTCMinutes().toString().padStart(2, "0")
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                        sub: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgress",
 | 
				
			||||||
 | 
					                        icon: "/Lotus/Interface/Graphics/ClanTileImages/ClanEnterDojo.png",
 | 
				
			||||||
 | 
					                        highPriority: true
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                ]);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const removeDojoKeyItems = (inventory: TInventoryDatabaseDocument): IInventoryChanges => {
 | 
					export const removeDojoKeyItems = (inventory: TInventoryDatabaseDocument): IInventoryChanges => {
 | 
				
			||||||
 | 
				
			|||||||
@ -21,7 +21,8 @@ import {
 | 
				
			|||||||
    ICalendarProgress,
 | 
					    ICalendarProgress,
 | 
				
			||||||
    IDroneClient,
 | 
					    IDroneClient,
 | 
				
			||||||
    IUpgradeClient,
 | 
					    IUpgradeClient,
 | 
				
			||||||
    TPartialStartingGear
 | 
					    TPartialStartingGear,
 | 
				
			||||||
 | 
					    ILoreFragmentScan
 | 
				
			||||||
} from "@/src/types/inventoryTypes/inventoryTypes";
 | 
					} from "@/src/types/inventoryTypes/inventoryTypes";
 | 
				
			||||||
import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate";
 | 
					import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate";
 | 
				
			||||||
import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes";
 | 
					import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes";
 | 
				
			||||||
@ -407,8 +408,32 @@ export const addItem = async (
 | 
				
			|||||||
        const meta = ExportCustoms[typeName];
 | 
					        const meta = ExportCustoms[typeName];
 | 
				
			||||||
        let inventoryChanges: IInventoryChanges;
 | 
					        let inventoryChanges: IInventoryChanges;
 | 
				
			||||||
        if (meta.productCategory == "CrewShipWeaponSkins") {
 | 
					        if (meta.productCategory == "CrewShipWeaponSkins") {
 | 
				
			||||||
            inventoryChanges = addCrewShipWeaponSkin(inventory, typeName);
 | 
					            if (meta.subroutines || meta.randomisedUpgrades) {
 | 
				
			||||||
 | 
					                // House versions need to be identified to get stats so put them into raw salvage first.
 | 
				
			||||||
 | 
					                const rawSalvageChanges = [
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        ItemType: typeName,
 | 
				
			||||||
 | 
					                        ItemCount: quantity
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                ];
 | 
				
			||||||
 | 
					                addCrewShipRawSalvage(inventory, rawSalvageChanges);
 | 
				
			||||||
 | 
					                inventoryChanges = { CrewShipRawSalvage: rawSalvageChanges };
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                // Sigma versions can be added directly.
 | 
				
			||||||
 | 
					                if (quantity != 1) {
 | 
				
			||||||
 | 
					                    throw new Error(
 | 
				
			||||||
 | 
					                        `unexpected acquisition quantity of CrewShipWeaponSkin: got ${quantity}, expected 1`
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                inventoryChanges = {
 | 
				
			||||||
 | 
					                    ...addCrewShipWeaponSkin(inventory, typeName, undefined),
 | 
				
			||||||
 | 
					                    ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, premiumPurchase)
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
 | 
					            if (quantity != 1) {
 | 
				
			||||||
 | 
					                throw new Error(`unexpected acquisition quantity of WeaponSkins: got ${quantity}, expected 1`);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            inventoryChanges = addSkin(inventory, typeName);
 | 
					            inventoryChanges = addSkin(inventory, typeName);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (meta.additionalItems) {
 | 
					        if (meta.additionalItems) {
 | 
				
			||||||
@ -1082,12 +1107,14 @@ export const addSkin = (
 | 
				
			|||||||
    return inventoryChanges;
 | 
					    return inventoryChanges;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const addCrewShipWeaponSkin = (
 | 
					export const addCrewShipWeaponSkin = (
 | 
				
			||||||
    inventory: TInventoryDatabaseDocument,
 | 
					    inventory: TInventoryDatabaseDocument,
 | 
				
			||||||
    typeName: string,
 | 
					    typeName: string,
 | 
				
			||||||
 | 
					    upgradeFingerprint: string | undefined,
 | 
				
			||||||
    inventoryChanges: IInventoryChanges = {}
 | 
					    inventoryChanges: IInventoryChanges = {}
 | 
				
			||||||
): IInventoryChanges => {
 | 
					): IInventoryChanges => {
 | 
				
			||||||
    const index = inventory.CrewShipWeaponSkins.push({ ItemType: typeName }) - 1;
 | 
					    const index =
 | 
				
			||||||
 | 
					        inventory.CrewShipWeaponSkins.push({ ItemType: typeName, UpgradeFingerprint: upgradeFingerprint }) - 1;
 | 
				
			||||||
    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 | 
					    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 | 
				
			||||||
    inventoryChanges.CrewShipWeaponSkins ??= [];
 | 
					    inventoryChanges.CrewShipWeaponSkins ??= [];
 | 
				
			||||||
    (inventoryChanges.CrewShipWeaponSkins as IUpgradeClient[]).push(
 | 
					    (inventoryChanges.CrewShipWeaponSkins as IUpgradeClient[]).push(
 | 
				
			||||||
@ -1096,6 +1123,22 @@ const addCrewShipWeaponSkin = (
 | 
				
			|||||||
    return inventoryChanges;
 | 
					    return inventoryChanges;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const addCrewShipSalvagedWeaponSkin = (
 | 
				
			||||||
 | 
					    inventory: TInventoryDatabaseDocument,
 | 
				
			||||||
 | 
					    typeName: string,
 | 
				
			||||||
 | 
					    upgradeFingerprint: string | undefined,
 | 
				
			||||||
 | 
					    inventoryChanges: IInventoryChanges = {}
 | 
				
			||||||
 | 
					): IInventoryChanges => {
 | 
				
			||||||
 | 
					    const index =
 | 
				
			||||||
 | 
					        inventory.CrewShipSalvagedWeaponSkins.push({ ItemType: typeName, UpgradeFingerprint: upgradeFingerprint }) - 1;
 | 
				
			||||||
 | 
					    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 | 
				
			||||||
 | 
					    inventoryChanges.CrewShipSalvagedWeaponSkins ??= [];
 | 
				
			||||||
 | 
					    (inventoryChanges.CrewShipSalvagedWeaponSkins as IUpgradeClient[]).push(
 | 
				
			||||||
 | 
					        inventory.CrewShipSalvagedWeaponSkins[index].toJSON<IUpgradeClient>()
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    return inventoryChanges;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const addCrewShip = (
 | 
					const addCrewShip = (
 | 
				
			||||||
    inventory: TInventoryDatabaseDocument,
 | 
					    inventory: TInventoryDatabaseDocument,
 | 
				
			||||||
    typeName: string,
 | 
					    typeName: string,
 | 
				
			||||||
@ -1347,7 +1390,20 @@ export const addFocusXpIncreases = (inventory: TInventoryDatabaseDocument, focus
 | 
				
			|||||||
    inventory.FocusXP.AP_POWER += focusXpPlus[FocusType.AP_POWER];
 | 
					    inventory.FocusXP.AP_POWER += focusXpPlus[FocusType.AP_POWER];
 | 
				
			||||||
    inventory.FocusXP.AP_WARD += focusXpPlus[FocusType.AP_WARD];
 | 
					    inventory.FocusXP.AP_WARD += focusXpPlus[FocusType.AP_WARD];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    inventory.DailyFocus -= focusXpPlus.reduce((a, b) => a + b, 0);
 | 
					    if (!config.noDailyFocusLimit) {
 | 
				
			||||||
 | 
					        inventory.DailyFocus -= focusXpPlus.reduce((a, b) => a + b, 0);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const addLoreFragmentScans = (inventory: TInventoryDatabaseDocument, arr: ILoreFragmentScan[]): void => {
 | 
				
			||||||
 | 
					    arr.forEach(clientFragment => {
 | 
				
			||||||
 | 
					        const fragment = inventory.LoreFragmentScans.find(x => x.ItemType == clientFragment.ItemType);
 | 
				
			||||||
 | 
					        if (fragment) {
 | 
				
			||||||
 | 
					            fragment.Progress += clientFragment.Progress;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            inventory.LoreFragmentScans.push(clientFragment);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const addChallenges = (
 | 
					export const addChallenges = (
 | 
				
			||||||
 | 
				
			|||||||
@ -9,7 +9,7 @@ import {
 | 
				
			|||||||
} from "warframe-public-export-plus";
 | 
					} from "warframe-public-export-plus";
 | 
				
			||||||
import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes";
 | 
					import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes";
 | 
				
			||||||
import { logger } from "@/src/utils/logger";
 | 
					import { logger } from "@/src/utils/logger";
 | 
				
			||||||
import { IRngResult, getRandomElement, getRandomReward } from "@/src/services/rngService";
 | 
					import { IRngResult, SRng, getRandomElement, getRandomReward } from "@/src/services/rngService";
 | 
				
			||||||
import { equipmentKeys, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
					import { equipmentKeys, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
    addBooster,
 | 
					    addBooster,
 | 
				
			||||||
@ -23,6 +23,7 @@ import {
 | 
				
			|||||||
    addGearExpByCategory,
 | 
					    addGearExpByCategory,
 | 
				
			||||||
    addItem,
 | 
					    addItem,
 | 
				
			||||||
    addLevelKeys,
 | 
					    addLevelKeys,
 | 
				
			||||||
 | 
					    addLoreFragmentScans,
 | 
				
			||||||
    addMiscItems,
 | 
					    addMiscItems,
 | 
				
			||||||
    addMissionComplete,
 | 
					    addMissionComplete,
 | 
				
			||||||
    addMods,
 | 
					    addMods,
 | 
				
			||||||
@ -30,6 +31,7 @@ import {
 | 
				
			|||||||
    addShipDecorations,
 | 
					    addShipDecorations,
 | 
				
			||||||
    addStanding,
 | 
					    addStanding,
 | 
				
			||||||
    combineInventoryChanges,
 | 
					    combineInventoryChanges,
 | 
				
			||||||
 | 
					    generateRewardSeed,
 | 
				
			||||||
    updateCurrency,
 | 
					    updateCurrency,
 | 
				
			||||||
    updateSyndicate
 | 
					    updateSyndicate
 | 
				
			||||||
} from "@/src/services/inventoryService";
 | 
					} from "@/src/services/inventoryService";
 | 
				
			||||||
@ -53,7 +55,22 @@ import { Loadout } from "../models/inventoryModels/loadoutModel";
 | 
				
			|||||||
import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes";
 | 
					import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes";
 | 
				
			||||||
import { getWorldState } from "./worldStateService";
 | 
					import { getWorldState } from "./worldStateService";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getRotations = (rotationCount: number, tierOverride: number | undefined): number[] => {
 | 
					const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] => {
 | 
				
			||||||
 | 
					    // For Spy missions, e.g. 3 vaults cracked = A, B, C
 | 
				
			||||||
 | 
					    if (rewardInfo.VaultsCracked) {
 | 
				
			||||||
 | 
					        const rotations: number[] = [];
 | 
				
			||||||
 | 
					        for (let i = 0; i != rewardInfo.VaultsCracked; ++i) {
 | 
				
			||||||
 | 
					            rotations.push(i);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return rotations;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // For Rescue missions
 | 
				
			||||||
 | 
					    if (rewardInfo.node in ExportRegions && ExportRegions[rewardInfo.node].missionIndex == 3 && rewardInfo.rewardTier) {
 | 
				
			||||||
 | 
					        return [rewardInfo.rewardTier];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const rotationCount = rewardInfo.rewardQualifications?.length || 0;
 | 
				
			||||||
    if (rotationCount === 0) return [0];
 | 
					    if (rotationCount === 0) return [0];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const rotationPattern =
 | 
					    const rotationPattern =
 | 
				
			||||||
@ -69,7 +86,12 @@ const getRotations = (rotationCount: number, tierOverride: number | undefined):
 | 
				
			|||||||
    return rotatedValues;
 | 
					    return rotatedValues;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getRandomRewardByChance = (pool: IReward[]): IRngResult | undefined => {
 | 
					const getRandomRewardByChance = (pool: IReward[], rng?: SRng): IRngResult | undefined => {
 | 
				
			||||||
 | 
					    if (rng) {
 | 
				
			||||||
 | 
					        const res = rng.randomReward(pool as IRngResult[]);
 | 
				
			||||||
 | 
					        rng.randomFloat(); // something related to rewards multiplier
 | 
				
			||||||
 | 
					        return res;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    return getRandomReward(pool as IRngResult[]);
 | 
					    return getRandomReward(pool as IRngResult[]);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -291,14 +313,7 @@ export const addMissionInventoryUpdates = async (
 | 
				
			|||||||
                break;
 | 
					                break;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            case "LoreFragmentScans":
 | 
					            case "LoreFragmentScans":
 | 
				
			||||||
                value.forEach(clientFragment => {
 | 
					                addLoreFragmentScans(inventory, value);
 | 
				
			||||||
                    const fragment = inventory.LoreFragmentScans.find(x => x.ItemType == clientFragment.ItemType);
 | 
					 | 
				
			||||||
                    if (fragment) {
 | 
					 | 
				
			||||||
                        fragment.Progress += clientFragment.Progress;
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        inventory.LoreFragmentScans.push(clientFragment);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            case "LibraryScans":
 | 
					            case "LibraryScans":
 | 
				
			||||||
                value.forEach(scan => {
 | 
					                value.forEach(scan => {
 | 
				
			||||||
@ -554,6 +569,11 @@ export const addMissionRewards = async (
 | 
				
			|||||||
        return { MissionRewards: [] };
 | 
					        return { MissionRewards: [] };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (rewardInfo.rewardSeed) {
 | 
				
			||||||
 | 
					        // We're using a reward seed, so give the client a new one in the response. On live, missionInventoryUpdate seems to always provide a fresh one in the response.
 | 
				
			||||||
 | 
					        inventory.RewardSeed = generateRewardSeed();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    //TODO: check double reward merging
 | 
					    //TODO: check double reward merging
 | 
				
			||||||
    const MissionRewards: IMissionReward[] = getRandomMissionDrops(rewardInfo, wagerTier);
 | 
					    const MissionRewards: IMissionReward[] = getRandomMissionDrops(rewardInfo, wagerTier);
 | 
				
			||||||
    logger.debug("random mission drops:", MissionRewards);
 | 
					    logger.debug("random mission drops:", MissionRewards);
 | 
				
			||||||
@ -567,7 +587,7 @@ export const addMissionRewards = async (
 | 
				
			|||||||
        const fixedLevelRewards = getLevelKeyRewards(levelKeyName);
 | 
					        const fixedLevelRewards = getLevelKeyRewards(levelKeyName);
 | 
				
			||||||
        //logger.debug(`fixedLevelRewards ${fixedLevelRewards}`);
 | 
					        //logger.debug(`fixedLevelRewards ${fixedLevelRewards}`);
 | 
				
			||||||
        if (fixedLevelRewards.levelKeyRewards) {
 | 
					        if (fixedLevelRewards.levelKeyRewards) {
 | 
				
			||||||
            addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, inventory, MissionRewards);
 | 
					            addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, inventory, MissionRewards, rewardInfo);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (fixedLevelRewards.levelKeyRewards2) {
 | 
					        if (fixedLevelRewards.levelKeyRewards2) {
 | 
				
			||||||
            for (const reward of fixedLevelRewards.levelKeyRewards2) {
 | 
					            for (const reward of fixedLevelRewards.levelKeyRewards2) {
 | 
				
			||||||
@ -592,7 +612,14 @@ export const addMissionRewards = async (
 | 
				
			|||||||
        const node = ExportRegions[missions.Tag];
 | 
					        const node = ExportRegions[missions.Tag];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        //node based credit rewards for mission completion
 | 
					        //node based credit rewards for mission completion
 | 
				
			||||||
        if (node.missionIndex !== 28) {
 | 
					        if (
 | 
				
			||||||
 | 
					            node.missionIndex != 23 && // junction
 | 
				
			||||||
 | 
					            node.missionIndex != 28 && // open world
 | 
				
			||||||
 | 
					            missions.Tag != "SolNode761" && // the index
 | 
				
			||||||
 | 
					            missions.Tag != "SolNode762" && // the index
 | 
				
			||||||
 | 
					            missions.Tag != "SolNode763" && // the index
 | 
				
			||||||
 | 
					            missions.Tag != "CrewBattleNode556" // free flight
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
            const levelCreditReward = getLevelCreditRewards(node);
 | 
					            const levelCreditReward = getLevelCreditRewards(node);
 | 
				
			||||||
            missionCompletionCredits += levelCreditReward;
 | 
					            missionCompletionCredits += levelCreditReward;
 | 
				
			||||||
            inventory.RegularCredits += levelCreditReward;
 | 
					            inventory.RegularCredits += levelCreditReward;
 | 
				
			||||||
@ -600,7 +627,20 @@ export const addMissionRewards = async (
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (node.missionReward) {
 | 
					        if (node.missionReward) {
 | 
				
			||||||
            missionCompletionCredits += addFixedLevelRewards(node.missionReward, inventory, MissionRewards);
 | 
					            missionCompletionCredits += addFixedLevelRewards(node.missionReward, inventory, MissionRewards, rewardInfo);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (missions.Tag == "PlutoToErisJunction") {
 | 
				
			||||||
 | 
					            await createMessage(inventory.accountOwnerId, [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    sndr: "/Lotus/Language/G1Quests/GolemQuestJordasName",
 | 
				
			||||||
 | 
					                    msg: "/Lotus/Language/G1Quests/GolemQuestIntroBody",
 | 
				
			||||||
 | 
					                    att: ["/Lotus/Types/Keys/GolemQuest/GolemQuestKeyChainItem"],
 | 
				
			||||||
 | 
					                    sub: "/Lotus/Language/G1Quests/GolemQuestIntroTitle",
 | 
				
			||||||
 | 
					                    icon: "/Lotus/Interface/Icons/Npcs/JordasPortrait.png",
 | 
				
			||||||
 | 
					                    highPriority: true
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ]);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -711,14 +751,12 @@ export const addMissionRewards = async (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    if (rewardInfo.JobStage != undefined && rewardInfo.jobId) {
 | 
					    if (rewardInfo.JobStage != undefined && rewardInfo.jobId) {
 | 
				
			||||||
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
					        // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
				
			||||||
        const [jobType, tierStr, hubNode, syndicateId, locationTag] = rewardInfo.jobId.split("_");
 | 
					        const [jobType, unkIndex, hubNode, syndicateId, locationTag] = rewardInfo.jobId.split("_");
 | 
				
			||||||
        const tier = Number(tierStr);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const worldState = getWorldState();
 | 
					        const worldState = getWorldState();
 | 
				
			||||||
        let syndicateEntry = worldState.SyndicateMissions.find(m => m._id.$oid === syndicateId);
 | 
					        let syndicateEntry = worldState.SyndicateMissions.find(m => m._id.$oid === syndicateId);
 | 
				
			||||||
        if (!syndicateEntry) syndicateEntry = worldState.SyndicateMissions.find(m => m.Tag === syndicateId); // Sometimes syndicateId can be tag
 | 
					        if (!syndicateEntry) syndicateEntry = worldState.SyndicateMissions.find(m => m.Tag === syndicateId); // Sometimes syndicateId can be tag
 | 
				
			||||||
        if (syndicateEntry && syndicateEntry.Jobs) {
 | 
					        if (syndicateEntry && syndicateEntry.Jobs) {
 | 
				
			||||||
            let currentJob = syndicateEntry.Jobs[tier];
 | 
					            let currentJob = syndicateEntry.Jobs[rewardInfo.JobTier!];
 | 
				
			||||||
            if (syndicateEntry.Tag === "EntratiSyndicate") {
 | 
					            if (syndicateEntry.Tag === "EntratiSyndicate") {
 | 
				
			||||||
                const vault = syndicateEntry.Jobs.find(j => j.locationTag === locationTag);
 | 
					                const vault = syndicateEntry.Jobs.find(j => j.locationTag === locationTag);
 | 
				
			||||||
                if (vault) currentJob = vault;
 | 
					                if (vault) currentJob = vault;
 | 
				
			||||||
@ -743,7 +781,7 @@ export const addMissionRewards = async (
 | 
				
			|||||||
                });
 | 
					                });
 | 
				
			||||||
                SyndicateXPItemReward = medallionAmount;
 | 
					                SyndicateXPItemReward = medallionAmount;
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                if (tier >= 0) {
 | 
					                if (rewardInfo.JobTier! >= 0) {
 | 
				
			||||||
                    AffiliationMods.push(
 | 
					                    AffiliationMods.push(
 | 
				
			||||||
                        addStanding(inventory, syndicateEntry.Tag, currentJob.xpAmounts[rewardInfo.JobStage])
 | 
					                        addStanding(inventory, syndicateEntry.Tag, currentJob.xpAmounts[rewardInfo.JobStage])
 | 
				
			||||||
                    );
 | 
					                    );
 | 
				
			||||||
@ -843,7 +881,8 @@ export const addCredits = (
 | 
				
			|||||||
export const addFixedLevelRewards = (
 | 
					export const addFixedLevelRewards = (
 | 
				
			||||||
    rewards: IMissionRewardExternal,
 | 
					    rewards: IMissionRewardExternal,
 | 
				
			||||||
    inventory: TInventoryDatabaseDocument,
 | 
					    inventory: TInventoryDatabaseDocument,
 | 
				
			||||||
    MissionRewards: IMissionReward[]
 | 
					    MissionRewards: IMissionReward[],
 | 
				
			||||||
 | 
					    rewardInfo?: IRewardInfo
 | 
				
			||||||
): number => {
 | 
					): number => {
 | 
				
			||||||
    let missionBonusCredits = 0;
 | 
					    let missionBonusCredits = 0;
 | 
				
			||||||
    if (rewards.credits) {
 | 
					    if (rewards.credits) {
 | 
				
			||||||
@ -873,13 +912,16 @@ export const addFixedLevelRewards = (
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    if (rewards.droptable) {
 | 
					    if (rewards.droptable) {
 | 
				
			||||||
        if (rewards.droptable in ExportRewards) {
 | 
					        if (rewards.droptable in ExportRewards) {
 | 
				
			||||||
            logger.debug(`rolling ${rewards.droptable} for level key rewards`);
 | 
					            const rotations: number[] = rewardInfo ? getRotations(rewardInfo) : [0];
 | 
				
			||||||
            const reward = getRandomRewardByChance(ExportRewards[rewards.droptable][0]);
 | 
					            logger.debug(`rolling ${rewards.droptable} for level key rewards`, { rotations });
 | 
				
			||||||
            if (reward) {
 | 
					            for (const tier of rotations) {
 | 
				
			||||||
                MissionRewards.push({
 | 
					                const reward = getRandomRewardByChance(ExportRewards[rewards.droptable][tier]);
 | 
				
			||||||
                    StoreItem: reward.type,
 | 
					                if (reward) {
 | 
				
			||||||
                    ItemCount: reward.itemCount
 | 
					                    MissionRewards.push({
 | 
				
			||||||
                });
 | 
					                        StoreItem: reward.type,
 | 
				
			||||||
 | 
					                        ItemCount: reward.itemCount
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            logger.error(`unknown droptable ${rewards.droptable}`);
 | 
					            logger.error(`unknown droptable ${rewards.droptable}`);
 | 
				
			||||||
@ -898,6 +940,12 @@ function getLevelCreditRewards(node: IRegion): number {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | undefined): IMissionReward[] {
 | 
					function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | undefined): IMissionReward[] {
 | 
				
			||||||
    const drops: IMissionReward[] = [];
 | 
					    const drops: IMissionReward[] = [];
 | 
				
			||||||
 | 
					    if (RewardInfo.periodicMissionTag?.startsWith("HardDaily")) {
 | 
				
			||||||
 | 
					        drops.push({
 | 
				
			||||||
 | 
					            StoreItem: "/Lotus/StoreItems/Types/Items/MiscItems/SteelEssence",
 | 
				
			||||||
 | 
					            ItemCount: 5
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    if (RewardInfo.node in ExportRegions) {
 | 
					    if (RewardInfo.node in ExportRegions) {
 | 
				
			||||||
        const region = ExportRegions[RewardInfo.node];
 | 
					        const region = ExportRegions[RewardInfo.node];
 | 
				
			||||||
        let rewardManifests: string[] =
 | 
					        let rewardManifests: string[] =
 | 
				
			||||||
@ -909,8 +957,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u
 | 
				
			|||||||
        if (RewardInfo.jobId) {
 | 
					        if (RewardInfo.jobId) {
 | 
				
			||||||
            if (RewardInfo.JobStage! >= 0) {
 | 
					            if (RewardInfo.JobStage! >= 0) {
 | 
				
			||||||
                // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
					                // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
				
			||||||
                const [jobType, tierStr, hubNode, syndicateId, locationTag] = RewardInfo.jobId.split("_");
 | 
					                const [jobType, unkIndex, hubNode, syndicateId, locationTag] = RewardInfo.jobId.split("_");
 | 
				
			||||||
                const tier = Number(tierStr);
 | 
					 | 
				
			||||||
                let isEndlessJob = false;
 | 
					                let isEndlessJob = false;
 | 
				
			||||||
                if (syndicateId) {
 | 
					                if (syndicateId) {
 | 
				
			||||||
                    const worldState = getWorldState();
 | 
					                    const worldState = getWorldState();
 | 
				
			||||||
@ -918,11 +965,11 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u
 | 
				
			|||||||
                    if (!syndicateEntry) syndicateEntry = worldState.SyndicateMissions.find(m => m.Tag === syndicateId);
 | 
					                    if (!syndicateEntry) syndicateEntry = worldState.SyndicateMissions.find(m => m.Tag === syndicateId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (syndicateEntry && syndicateEntry.Jobs) {
 | 
					                    if (syndicateEntry && syndicateEntry.Jobs) {
 | 
				
			||||||
                        let job = syndicateEntry.Jobs[tier];
 | 
					                        let job = syndicateEntry.Jobs[RewardInfo.JobTier!];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        if (syndicateEntry.Tag === "EntratiSyndicate") {
 | 
					                        if (syndicateEntry.Tag === "EntratiSyndicate") {
 | 
				
			||||||
                            const vault = syndicateEntry.Jobs.find(j => j.locationTag === locationTag);
 | 
					                            const vault = syndicateEntry.Jobs.find(j => j.locationTag === locationTag);
 | 
				
			||||||
                            if (vault) job = vault;
 | 
					                            if (vault && locationTag) job = vault;
 | 
				
			||||||
                            // if (
 | 
					                            // if (
 | 
				
			||||||
                            //     [
 | 
					                            //     [
 | 
				
			||||||
                            //         "DeimosRuinsExterminateBounty",
 | 
					                            //         "DeimosRuinsExterminateBounty",
 | 
				
			||||||
@ -997,8 +1044,11 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u
 | 
				
			|||||||
                            (RewardInfo.JobStage === job.xpAmounts.length - 1 || job.isVault) &&
 | 
					                            (RewardInfo.JobStage === job.xpAmounts.length - 1 || job.isVault) &&
 | 
				
			||||||
                            !isEndlessJob
 | 
					                            !isEndlessJob
 | 
				
			||||||
                        ) {
 | 
					                        ) {
 | 
				
			||||||
                            rewardManifests.push(job.rewards);
 | 
					                            // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 | 
				
			||||||
                            rotations.push(ExportRewards[job.rewards].length - 1);
 | 
					                            if (ExportRewards[job.rewards]) {
 | 
				
			||||||
 | 
					                                rewardManifests.push(job.rewards);
 | 
				
			||||||
 | 
					                                rotations.push(ExportRewards[job.rewards].length - 1);
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
@ -1041,29 +1091,28 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u
 | 
				
			|||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                logger.error(`Unknown syndicate or tier: ${RewardInfo.challengeMissionId}`);
 | 
					                logger.error(`Unknown syndicate or tier: ${RewardInfo.challengeMissionId}`);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } else if (RewardInfo.VaultsCracked) {
 | 
					 | 
				
			||||||
            // For Spy missions, e.g. 3 vaults cracked = A, B, C
 | 
					 | 
				
			||||||
            for (let i = 0; i != RewardInfo.VaultsCracked; ++i) {
 | 
					 | 
				
			||||||
                rotations.push(i);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            const rotationCount = RewardInfo.rewardQualifications?.length || 0;
 | 
					            rotations = getRotations(RewardInfo, tierOverride);
 | 
				
			||||||
            rotations = getRotations(rotationCount, tierOverride);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (rewardManifests.length != 0) {
 | 
					        if (rewardManifests.length != 0) {
 | 
				
			||||||
            logger.debug(`generating random mission rewards`, { rewardManifests, rotations });
 | 
					            logger.debug(`generating random mission rewards`, { rewardManifests, rotations });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        rewardManifests
 | 
					        const rng = new SRng(BigInt(RewardInfo.rewardSeed ?? generateRewardSeed()) ^ 0xffffffffffffffffn);
 | 
				
			||||||
            .map(name => ExportRewards[name])
 | 
					        rewardManifests.forEach(name => {
 | 
				
			||||||
            .forEach(table => {
 | 
					            const table = ExportRewards[name];
 | 
				
			||||||
                for (const rotation of rotations) {
 | 
					            // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 | 
				
			||||||
                    const rotationRewards = table[rotation];
 | 
					            if (!table) {
 | 
				
			||||||
                    const drop = getRandomRewardByChance(rotationRewards);
 | 
					                logger.error(`unknown droptable: ${name}`);
 | 
				
			||||||
                    if (drop) {
 | 
					                return;
 | 
				
			||||||
                        drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount });
 | 
					            }
 | 
				
			||||||
                    }
 | 
					            for (const rotation of rotations) {
 | 
				
			||||||
 | 
					                const rotationRewards = table[rotation];
 | 
				
			||||||
 | 
					                const drop = getRandomRewardByChance(rotationRewards, rng);
 | 
				
			||||||
 | 
					                if (drop) {
 | 
				
			||||||
 | 
					                    drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount });
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            });
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (region.cacheRewardManifest && RewardInfo.EnemyCachesFound) {
 | 
					        if (region.cacheRewardManifest && RewardInfo.EnemyCachesFound) {
 | 
				
			||||||
            const deck = ExportRewards[region.cacheRewardManifest];
 | 
					            const deck = ExportRewards[region.cacheRewardManifest];
 | 
				
			||||||
@ -1092,6 +1141,32 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u
 | 
				
			|||||||
                drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount });
 | 
					                drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount });
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (RewardInfo.PurgatoryRewardQualifications) {
 | 
				
			||||||
 | 
					            for (const encodedQualification of RewardInfo.PurgatoryRewardQualifications) {
 | 
				
			||||||
 | 
					                const qualification = parseInt(encodedQualification) - 1;
 | 
				
			||||||
 | 
					                if (qualification < 0 || qualification > 8) {
 | 
				
			||||||
 | 
					                    logger.error(`unexpected purgatory reward qualification: ${qualification}`);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    const drop = getRandomRewardByChance(
 | 
				
			||||||
 | 
					                        ExportRewards[
 | 
				
			||||||
 | 
					                            [
 | 
				
			||||||
 | 
					                                "/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryBlackTokenRewards",
 | 
				
			||||||
 | 
					                                "/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryGoldTokenRewards",
 | 
				
			||||||
 | 
					                                "/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryBlueTokenRewards"
 | 
				
			||||||
 | 
					                            ][Math.trunc(qualification / 3)]
 | 
				
			||||||
 | 
					                        ][qualification % 3]
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                    if (drop) {
 | 
				
			||||||
 | 
					                        drops.push({
 | 
				
			||||||
 | 
					                            StoreItem: drop.type,
 | 
				
			||||||
 | 
					                            ItemCount: drop.itemCount,
 | 
				
			||||||
 | 
					                            FromEnemyCache: true // to show "identified"
 | 
				
			||||||
 | 
					                        });
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return drops;
 | 
					    return drops;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -31,7 +31,7 @@ const getRewardAtPercentage = <T extends { probability: number }>(pool: T[], per
 | 
				
			|||||||
            return item;
 | 
					            return item;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    throw new Error("What the fuck?");
 | 
					    return pool[pool.length - 1];
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getRandomReward = <T extends { probability: number }>(pool: T[]): T | undefined => {
 | 
					export const getRandomReward = <T extends { probability: number }>(pool: T[]): T | undefined => {
 | 
				
			||||||
@ -142,4 +142,8 @@ export class SRng {
 | 
				
			|||||||
        this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn;
 | 
					        this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn;
 | 
				
			||||||
        return (Number(this.state >> 38n) & 0xffffff) * 0.000000059604645;
 | 
					        return (Number(this.state >> 38n) & 0xffffff) * 0.000000059604645;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    randomReward<T extends { probability: number }>(pool: T[]): T | undefined {
 | 
				
			||||||
 | 
					        return getRewardAtPercentage(pool, this.randomFloat());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -8,8 +8,7 @@ import { unixTimesInMs } from "@/src/constants/timeConstants";
 | 
				
			|||||||
import { config } from "@/src/services/configService";
 | 
					import { config } from "@/src/services/configService";
 | 
				
			||||||
import { CRng } from "@/src/services/rngService";
 | 
					import { CRng } from "@/src/services/rngService";
 | 
				
			||||||
import { eMissionType, ExportNightwave, ExportRegions } from "warframe-public-export-plus";
 | 
					import { eMissionType, ExportNightwave, ExportRegions } from "warframe-public-export-plus";
 | 
				
			||||||
import { ISeasonChallenge, IWorldState } from "../types/worldStateTypes";
 | 
					import { ISeasonChallenge, ISortie, IWorldState } from "../types/worldStateTypes";
 | 
				
			||||||
import { logger } from "../utils/logger";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const sortieBosses = [
 | 
					const sortieBosses = [
 | 
				
			||||||
    "SORTIE_BOSS_HYENA",
 | 
					    "SORTIE_BOSS_HYENA",
 | 
				
			||||||
@ -80,77 +79,84 @@ const sortieBossNode: Record<string, string> = {
 | 
				
			|||||||
    SORTIE_BOSS_INFALAD: "SolNode705"
 | 
					    SORTIE_BOSS_INFALAD: "SolNode705"
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const jobSets: string[][] = [
 | 
					const eidolonJobs = [
 | 
				
			||||||
    [
 | 
					    "/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyAss",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyAss",
 | 
					    "/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyCap",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyCap",
 | 
					    "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountySab",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountySab",
 | 
					    "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyLib",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyLib",
 | 
					    "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyCap",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyCap",
 | 
					    "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyExt",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyExt",
 | 
					    "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCap",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCap",
 | 
					    "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyTheft",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyTheft",
 | 
					    "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCache",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCache",
 | 
					    "/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapOne",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapOne",
 | 
					    "/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapTwo",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapTwo",
 | 
					    "/Lotus/Types/Gameplay/Eidolon/Jobs/SabotageBountySab",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Eidolon/Jobs/SabotageBountySab",
 | 
					    "/Lotus/Types/Gameplay/Eidolon/Jobs/RescueBountyResc"
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Eidolon/Jobs/RescueBountyResc"
 | 
					];
 | 
				
			||||||
    ],
 | 
					
 | 
				
			||||||
    [
 | 
					const eidolonNarmerJobs = [
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AssassinateBountyAss",
 | 
					    "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AssassinateBountyAss",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AttritionBountyExt",
 | 
					    "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AttritionBountyExt",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/ReclamationBountyTheft",
 | 
					    "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/ReclamationBountyTheft",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AttritionBountyLib"
 | 
					    "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AttritionBountyLib"
 | 
				
			||||||
    ],
 | 
					];
 | 
				
			||||||
    [
 | 
					
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobAmbush",
 | 
					const venusJobs = [
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobExcavation",
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobAmbush",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobRecovery",
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobExcavation",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobAssassinate",
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobRecovery",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobExcavation",
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobAssassinate",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobAssassinate",
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobExcavation",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobExterminate",
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobAssassinate",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobResource",
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobExterminate",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobRecovery",
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobResource",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobResource",
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobRecovery",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobSpy",
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobResource",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/VenusSpyJobSpy",
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobSpy",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobAmbush",
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/VenusSpyJobSpy",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobExcavation",
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobAmbush",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobResource",
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobExcavation",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobCaches",
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobResource",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobResource",
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobCaches",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobSpy",
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobResource",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobDefense",
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobSpy",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobRecovery",
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobDefense",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobResource",
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobRecovery",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/VenusWetworkJobAssassinate",
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobResource",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/VenusWetworkJobSpy"
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/VenusWetworkJobAssassinate",
 | 
				
			||||||
    ],
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/VenusWetworkJobSpy"
 | 
				
			||||||
    [
 | 
					];
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobAssassinate",
 | 
					
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobExterminate",
 | 
					const venusNarmerJobs = [
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusPreservationJobDefense",
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobAssassinate",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusTheftJobExcavation"
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobExterminate",
 | 
				
			||||||
    ],
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusPreservationJobDefense",
 | 
				
			||||||
    [
 | 
					    "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusTheftJobExcavation"
 | 
				
			||||||
        "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAreaDefenseBounty",
 | 
					];
 | 
				
			||||||
        "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAssassinateBounty",
 | 
					
 | 
				
			||||||
        "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosCrpSurvivorBounty",
 | 
					const microplanetJobs = [
 | 
				
			||||||
        "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosGrnSurvivorBounty",
 | 
					    "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAreaDefenseBounty",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosKeyPiecesBounty",
 | 
					    "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAssassinateBounty",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosExcavateBounty",
 | 
					    "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosCrpSurvivorBounty",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosPurifyBounty"
 | 
					    "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosGrnSurvivorBounty",
 | 
				
			||||||
    ],
 | 
					    "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosKeyPiecesBounty",
 | 
				
			||||||
    [
 | 
					    "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosExcavateBounty",
 | 
				
			||||||
        "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessAreaDefenseBounty",
 | 
					    "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosPurifyBounty"
 | 
				
			||||||
        "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessExcavateBounty",
 | 
					];
 | 
				
			||||||
        "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessPurifyBounty"
 | 
					
 | 
				
			||||||
    ]
 | 
					const microplanetEndlessJobs = [
 | 
				
			||||||
 | 
					    "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessAreaDefenseBounty",
 | 
				
			||||||
 | 
					    "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessExcavateBounty",
 | 
				
			||||||
 | 
					    "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessPurifyBounty"
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0
 | 
					const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const isBeforeNextExpectedWorldStateRefresh = (date: number): boolean => {
 | 
				
			||||||
 | 
					    return Date.now() + 300_000 > date;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getSortieTime = (day: number): number => {
 | 
					const getSortieTime = (day: number): number => {
 | 
				
			||||||
    const dayStart = EPOCH + day * 86400000;
 | 
					    const dayStart = EPOCH + day * 86400000;
 | 
				
			||||||
    const date = new Date(dayStart);
 | 
					    const date = new Date(dayStart);
 | 
				
			||||||
@ -165,6 +171,134 @@ const getSortieTime = (day: number): number => {
 | 
				
			|||||||
    return dayStart + (isDst ? 16 : 17) * 3600000;
 | 
					    return dayStart + (isDst ? 16 : 17) * 3600000;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const pushSortieIfRelevant = (out: ISortie[], day: number): void => {
 | 
				
			||||||
 | 
					    const dayStart = getSortieTime(day);
 | 
				
			||||||
 | 
					    if (!isBeforeNextExpectedWorldStateRefresh(dayStart)) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const dayEnd = getSortieTime(day + 1);
 | 
				
			||||||
 | 
					    if (Date.now() >= dayEnd) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const rng = new CRng(day);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const boss = rng.randomElement(sortieBosses);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modifiers = [
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_LOW_ENERGY",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_IMPACT",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_SLASH",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_PUNCTURE",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_EXIMUS",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_MAGNETIC",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_CORROSIVE",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_VIRAL",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_ELECTRICITY",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_RADIATION",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_GAS",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_FIRE",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_EXPLOSION",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_FREEZE",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_TOXIN",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_POISON",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_HAZARD_RADIATION",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_HAZARD_MAGNETIC",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_HAZARD_FOG", // TODO: push this if the mission tileset is Grineer Forest
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_HAZARD_FIRE", // TODO: push this if the mission tileset is Corpus Ship or Grineer Galleon
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_HAZARD_ICE",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_HAZARD_COLD",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_SECONDARY_ONLY",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_SHOTGUN_ONLY",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_SNIPER_ONLY",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_RIFLE_ONLY",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_MELEE_ONLY",
 | 
				
			||||||
 | 
					        "SORTIE_MODIFIER_BOW_ONLY"
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (sortieBossToFaction[boss] == "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_SHIELDS");
 | 
				
			||||||
 | 
					    if (sortieBossToFaction[boss] != "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_ARMOR");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const nodes: string[] = [];
 | 
				
			||||||
 | 
					    const availableMissionIndexes: number[] = [];
 | 
				
			||||||
 | 
					    for (const [key, value] of Object.entries(ExportRegions)) {
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            sortieFactionToSystemIndexes[sortieBossToFaction[boss]].includes(value.systemIndex) &&
 | 
				
			||||||
 | 
					            sortieFactionToFactionIndexes[sortieBossToFaction[boss]].includes(value.factionIndex!) &&
 | 
				
			||||||
 | 
					            value.name.indexOf("Archwing") == -1 &&
 | 
				
			||||||
 | 
					            value.missionIndex != 0 && // Exclude MT_ASSASSINATION
 | 
				
			||||||
 | 
					            value.missionIndex != 5 && // Exclude MT_CAPTURE
 | 
				
			||||||
 | 
					            value.missionIndex != 21 && // Exclude MT_PURIFY
 | 
				
			||||||
 | 
					            value.missionIndex != 23 && // Exclude MT_JUNCTION
 | 
				
			||||||
 | 
					            value.missionIndex <= 28
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					            if (!availableMissionIndexes.includes(value.missionIndex)) {
 | 
				
			||||||
 | 
					                availableMissionIndexes.push(value.missionIndex);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            nodes.push(key);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const selectedNodes: { missionType: string; modifierType: string; node: string }[] = [];
 | 
				
			||||||
 | 
					    const missionTypes = new Set();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (let i = 0; i < 3; i++) {
 | 
				
			||||||
 | 
					        const randomIndex = rng.randomInt(0, nodes.length - 1);
 | 
				
			||||||
 | 
					        const node = nodes[randomIndex];
 | 
				
			||||||
 | 
					        let missionIndex = ExportRegions[node].missionIndex;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            !["SolNode404", "SolNode411"].includes(node) && // for some reason the game doesn't like missionType changes for these missions
 | 
				
			||||||
 | 
					            missionIndex != 28 &&
 | 
				
			||||||
 | 
					            rng.randomInt(0, 2) == 2
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					            missionIndex = rng.randomElement(availableMissionIndexes);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (i == 2 && rng.randomInt(0, 2) == 2) {
 | 
				
			||||||
 | 
					            const filteredModifiers = modifiers.filter(mod => mod !== "SORTIE_MODIFIER_MELEE_ONLY");
 | 
				
			||||||
 | 
					            const modifierType = rng.randomElement(filteredModifiers);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (boss == "SORTIE_BOSS_PHORID") {
 | 
				
			||||||
 | 
					                selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node });
 | 
				
			||||||
 | 
					                nodes.splice(randomIndex, 1);
 | 
				
			||||||
 | 
					                continue;
 | 
				
			||||||
 | 
					            } else if (sortieBossNode[boss]) {
 | 
				
			||||||
 | 
					                selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node: sortieBossNode[boss] });
 | 
				
			||||||
 | 
					                continue;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const missionType = eMissionType[missionIndex].tag;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (missionTypes.has(missionType)) {
 | 
				
			||||||
 | 
					            i--;
 | 
				
			||||||
 | 
					            continue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const filteredModifiers =
 | 
				
			||||||
 | 
					            missionType === "MT_TERRITORY"
 | 
				
			||||||
 | 
					                ? modifiers.filter(mod => mod != "SORTIE_MODIFIER_HAZARD_RADIATION")
 | 
				
			||||||
 | 
					                : modifiers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const modifierType = rng.randomElement(filteredModifiers);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        selectedNodes.push({ missionType, modifierType, node });
 | 
				
			||||||
 | 
					        nodes.splice(randomIndex, 1);
 | 
				
			||||||
 | 
					        missionTypes.add(missionType);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    out.push({
 | 
				
			||||||
 | 
					        _id: { $oid: Math.trunc(dayStart / 1000).toString(16) + "d4d932c97c0a3acd" },
 | 
				
			||||||
 | 
					        Activation: { $date: { $numberLong: dayStart.toString() } },
 | 
				
			||||||
 | 
					        Expiry: { $date: { $numberLong: dayEnd.toString() } },
 | 
				
			||||||
 | 
					        Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards",
 | 
				
			||||||
 | 
					        Seed: day,
 | 
				
			||||||
 | 
					        Boss: boss,
 | 
				
			||||||
 | 
					        Variants: selectedNodes
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const dailyChallenges = Object.keys(ExportNightwave.challenges).filter(x =>
 | 
					const dailyChallenges = Object.keys(ExportNightwave.challenges).filter(x =>
 | 
				
			||||||
    x.startsWith("/Lotus/Types/Challenges/Seasons/Daily/")
 | 
					    x.startsWith("/Lotus/Types/Challenges/Seasons/Daily/")
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
@ -228,9 +362,10 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
				
			|||||||
        BuildLabel: typeof buildLabel == "string" ? buildLabel.split(" ").join("+") : buildConfig.buildLabel,
 | 
					        BuildLabel: typeof buildLabel == "string" ? buildLabel.split(" ").join("+") : buildConfig.buildLabel,
 | 
				
			||||||
        Time: config.worldState?.lockTime || Math.round(Date.now() / 1000),
 | 
					        Time: config.worldState?.lockTime || Math.round(Date.now() / 1000),
 | 
				
			||||||
        Goals: [],
 | 
					        Goals: [],
 | 
				
			||||||
        GlobalUpgrades: [],
 | 
					        Alerts: [],
 | 
				
			||||||
        Sorties: [],
 | 
					        Sorties: [],
 | 
				
			||||||
        LiteSorties: [],
 | 
					        LiteSorties: [],
 | 
				
			||||||
 | 
					        GlobalUpgrades: [],
 | 
				
			||||||
        EndlessXpChoices: [],
 | 
					        EndlessXpChoices: [],
 | 
				
			||||||
        SeasonInfo: {
 | 
					        SeasonInfo: {
 | 
				
			||||||
            Activation: { $date: { $numberLong: "1715796000000" } },
 | 
					            Activation: { $date: { $numberLong: "1715796000000" } },
 | 
				
			||||||
@ -239,36 +374,10 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
				
			|||||||
            Season: 14,
 | 
					            Season: 14,
 | 
				
			||||||
            Phase: 0,
 | 
					            Phase: 0,
 | 
				
			||||||
            Params: "",
 | 
					            Params: "",
 | 
				
			||||||
            ActiveChallenges: [
 | 
					            ActiveChallenges: []
 | 
				
			||||||
                getSeasonDailyChallenge(day - 2),
 | 
					 | 
				
			||||||
                getSeasonDailyChallenge(day - 1),
 | 
					 | 
				
			||||||
                getSeasonDailyChallenge(day - 0),
 | 
					 | 
				
			||||||
                getSeasonWeeklyChallenge(week, 0),
 | 
					 | 
				
			||||||
                getSeasonWeeklyChallenge(week, 1),
 | 
					 | 
				
			||||||
                getSeasonWeeklyHardChallenge(week, 2),
 | 
					 | 
				
			||||||
                getSeasonWeeklyHardChallenge(week, 3),
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") },
 | 
					 | 
				
			||||||
                    Activation: { $date: { $numberLong: weekStart.toString() } },
 | 
					 | 
				
			||||||
                    Expiry: { $date: { $numberLong: weekEnd.toString() } },
 | 
					 | 
				
			||||||
                    Challenge:
 | 
					 | 
				
			||||||
                        "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" + (week - 12)
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") },
 | 
					 | 
				
			||||||
                    Activation: { $date: { $numberLong: weekStart.toString() } },
 | 
					 | 
				
			||||||
                    Expiry: { $date: { $numberLong: weekEnd.toString() } },
 | 
					 | 
				
			||||||
                    Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" + (week - 12)
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") },
 | 
					 | 
				
			||||||
                    Activation: { $date: { $numberLong: weekStart.toString() } },
 | 
					 | 
				
			||||||
                    Expiry: { $date: { $numberLong: weekEnd.toString() } },
 | 
					 | 
				
			||||||
                    Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" + (week - 12)
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            ]
 | 
					 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        ...staticWorldState
 | 
					        ...staticWorldState,
 | 
				
			||||||
 | 
					        SyndicateMissions: [...staticWorldState.SyndicateMissions]
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (config.worldState?.starDays) {
 | 
					    if (config.worldState?.starDays) {
 | 
				
			||||||
@ -288,73 +397,307 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Nightwave Challenges
 | 
				
			||||||
 | 
					    worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 2));
 | 
				
			||||||
 | 
					    worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 1));
 | 
				
			||||||
 | 
					    worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 0));
 | 
				
			||||||
 | 
					    if (isBeforeNextExpectedWorldStateRefresh(EPOCH + (day + 1) * 86400000)) {
 | 
				
			||||||
 | 
					        worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day + 1));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyChallenge(week, 0));
 | 
				
			||||||
 | 
					    worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyChallenge(week, 1));
 | 
				
			||||||
 | 
					    worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyHardChallenge(week, 2));
 | 
				
			||||||
 | 
					    worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyHardChallenge(week, 3));
 | 
				
			||||||
 | 
					    worldState.SeasonInfo.ActiveChallenges.push({
 | 
				
			||||||
 | 
					        _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") },
 | 
				
			||||||
 | 
					        Activation: { $date: { $numberLong: weekStart.toString() } },
 | 
				
			||||||
 | 
					        Expiry: { $date: { $numberLong: weekEnd.toString() } },
 | 
				
			||||||
 | 
					        Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" + (week - 12)
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    worldState.SeasonInfo.ActiveChallenges.push({
 | 
				
			||||||
 | 
					        _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") },
 | 
				
			||||||
 | 
					        Activation: { $date: { $numberLong: weekStart.toString() } },
 | 
				
			||||||
 | 
					        Expiry: { $date: { $numberLong: weekEnd.toString() } },
 | 
				
			||||||
 | 
					        Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" + (week - 12)
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    worldState.SeasonInfo.ActiveChallenges.push({
 | 
				
			||||||
 | 
					        _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") },
 | 
				
			||||||
 | 
					        Activation: { $date: { $numberLong: weekStart.toString() } },
 | 
				
			||||||
 | 
					        Expiry: { $date: { $numberLong: weekEnd.toString() } },
 | 
				
			||||||
 | 
					        Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" + (week - 12)
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    // TODO: Provide upcoming weekly acts if rollover is imminent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Elite Sanctuary Onslaught cycling every week
 | 
					    // Elite Sanctuary Onslaught cycling every week
 | 
				
			||||||
    worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = week; // unfaithful
 | 
					    worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = week; // unfaithful
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation
 | 
					    // Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation
 | 
				
			||||||
    const bountyCycle = Math.trunc(Date.now() / 9000000);
 | 
					    let bountyCycle = Math.trunc(Date.now() / 9000000);
 | 
				
			||||||
    const bountyCycleStart = bountyCycle * 9000000;
 | 
					    let bountyCycleEnd: number | undefined;
 | 
				
			||||||
    const bountyCycleEnd = bountyCycleStart + 9000000;
 | 
					    do {
 | 
				
			||||||
    worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "ZarimanSyndicate")] = {
 | 
					        const bountyCycleStart = bountyCycle * 9000000;
 | 
				
			||||||
        _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000029" },
 | 
					        bountyCycleEnd = bountyCycleStart + 9000000;
 | 
				
			||||||
        Activation: { $date: { $numberLong: bountyCycleStart.toString() } },
 | 
					        worldState.SyndicateMissions.push({
 | 
				
			||||||
        Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } },
 | 
					            _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000029" },
 | 
				
			||||||
        Tag: "ZarimanSyndicate",
 | 
					            Activation: { $date: { $numberLong: bountyCycleStart.toString() } },
 | 
				
			||||||
        Seed: bountyCycle,
 | 
					            Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } },
 | 
				
			||||||
        Nodes: []
 | 
					            Tag: "ZarimanSyndicate",
 | 
				
			||||||
    };
 | 
					            Seed: bountyCycle,
 | 
				
			||||||
    worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "EntratiLabSyndicate")] = {
 | 
					            Nodes: []
 | 
				
			||||||
        _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000004" },
 | 
					        });
 | 
				
			||||||
        Activation: { $date: { $numberLong: bountyCycleStart.toString() } },
 | 
					        worldState.SyndicateMissions.push({
 | 
				
			||||||
        Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } },
 | 
					            _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000004" },
 | 
				
			||||||
        Tag: "EntratiLabSyndicate",
 | 
					            Activation: { $date: { $numberLong: bountyCycleStart.toString() } },
 | 
				
			||||||
        Seed: bountyCycle,
 | 
					            Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } },
 | 
				
			||||||
        Nodes: []
 | 
					            Tag: "EntratiLabSyndicate",
 | 
				
			||||||
    };
 | 
					            Seed: bountyCycle,
 | 
				
			||||||
    worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "HexSyndicate")] = {
 | 
					            Nodes: []
 | 
				
			||||||
        _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000006" },
 | 
					        });
 | 
				
			||||||
        Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } },
 | 
					        worldState.SyndicateMissions.push({
 | 
				
			||||||
        Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } },
 | 
					            _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000006" },
 | 
				
			||||||
        Tag: "HexSyndicate",
 | 
					            Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } },
 | 
				
			||||||
        Seed: bountyCycle,
 | 
					            Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } },
 | 
				
			||||||
        Nodes: []
 | 
					            Tag: "HexSyndicate",
 | 
				
			||||||
    };
 | 
					            Seed: bountyCycle,
 | 
				
			||||||
    for (const syndicateInfo of worldState.SyndicateMissions) {
 | 
					            Nodes: []
 | 
				
			||||||
        if (syndicateInfo.Jobs && syndicateInfo.Seed != bountyCycle) {
 | 
					        });
 | 
				
			||||||
            syndicateInfo.Activation.$date.$numberLong = bountyCycleStart.toString(10);
 | 
					
 | 
				
			||||||
            syndicateInfo.Expiry.$date.$numberLong = bountyCycleEnd.toString(10);
 | 
					        const table = String.fromCharCode(65 + (bountyCycle % 3));
 | 
				
			||||||
            syndicateInfo.Seed = bountyCycle;
 | 
					        const vaultTable = String.fromCharCode(65 + ((bountyCycle + 1) % 3));
 | 
				
			||||||
            logger.debug(`refreshing jobs for ${syndicateInfo.Tag}`);
 | 
					        const deimosDTable = String.fromCharCode(65 + (bountyCycle % 2));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // TODO: xpAmounts need to be calculated based on the jobType somehow?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
            const rng = new CRng(bountyCycle);
 | 
					            const rng = new CRng(bountyCycle);
 | 
				
			||||||
            const table = String.fromCharCode(65 + (bountyCycle % 3));
 | 
					            worldState.SyndicateMissions.push({
 | 
				
			||||||
            const vaultTable = String.fromCharCode(65 + ((bountyCycle + 1) % 3));
 | 
					                _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000008" },
 | 
				
			||||||
            const deimosDTable = String.fromCharCode(65 + (bountyCycle % 2));
 | 
					                Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } },
 | 
				
			||||||
            //console.log({ bountyCycleStart, bountyCycleEnd, table, vaultTable, deimosDTable });
 | 
					                Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } },
 | 
				
			||||||
            for (const jobInfo of syndicateInfo.Jobs) {
 | 
					                Tag: "CetusSyndicate",
 | 
				
			||||||
                if (jobInfo.jobType) {
 | 
					                Seed: bountyCycle,
 | 
				
			||||||
                    let found = false;
 | 
					                Nodes: [],
 | 
				
			||||||
                    for (const jobSet of jobSets) {
 | 
					                Jobs: [
 | 
				
			||||||
                        if (jobSet.indexOf(jobInfo.jobType) != -1) {
 | 
					                    {
 | 
				
			||||||
                            jobInfo.jobType = rng.randomElement(jobSet);
 | 
					                        jobType: rng.randomElement(eidolonJobs),
 | 
				
			||||||
                            // TODO: xpAmounts seems like it might need to differ depending on the job?
 | 
					                        rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierATable${table}Rewards`,
 | 
				
			||||||
                            found = true;
 | 
					                        masteryReq: 0,
 | 
				
			||||||
                            break;
 | 
					                        minEnemyLevel: 5,
 | 
				
			||||||
                        }
 | 
					                        maxEnemyLevel: 15,
 | 
				
			||||||
 | 
					                        xpAmounts: [430, 430, 430]
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        jobType: rng.randomElement(eidolonJobs),
 | 
				
			||||||
 | 
					                        rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierBTable${table}Rewards`,
 | 
				
			||||||
 | 
					                        masteryReq: 1,
 | 
				
			||||||
 | 
					                        minEnemyLevel: 10,
 | 
				
			||||||
 | 
					                        maxEnemyLevel: 30,
 | 
				
			||||||
 | 
					                        xpAmounts: [620, 620, 620]
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        jobType: rng.randomElement(eidolonJobs),
 | 
				
			||||||
 | 
					                        rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierCTable${table}Rewards`,
 | 
				
			||||||
 | 
					                        masteryReq: 2,
 | 
				
			||||||
 | 
					                        minEnemyLevel: 20,
 | 
				
			||||||
 | 
					                        maxEnemyLevel: 40,
 | 
				
			||||||
 | 
					                        xpAmounts: [670, 670, 670, 990]
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        jobType: rng.randomElement(eidolonJobs),
 | 
				
			||||||
 | 
					                        rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierDTable${table}Rewards`,
 | 
				
			||||||
 | 
					                        masteryReq: 3,
 | 
				
			||||||
 | 
					                        minEnemyLevel: 30,
 | 
				
			||||||
 | 
					                        maxEnemyLevel: 50,
 | 
				
			||||||
 | 
					                        xpAmounts: [570, 570, 570, 570, 1110]
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        jobType: rng.randomElement(eidolonJobs),
 | 
				
			||||||
 | 
					                        rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETable${table}Rewards`,
 | 
				
			||||||
 | 
					                        masteryReq: 5,
 | 
				
			||||||
 | 
					                        minEnemyLevel: 40,
 | 
				
			||||||
 | 
					                        maxEnemyLevel: 60,
 | 
				
			||||||
 | 
					                        xpAmounts: [740, 740, 740, 740, 1450]
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        jobType: rng.randomElement(eidolonJobs),
 | 
				
			||||||
 | 
					                        rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETable${table}Rewards`,
 | 
				
			||||||
 | 
					                        masteryReq: 10,
 | 
				
			||||||
 | 
					                        minEnemyLevel: 100,
 | 
				
			||||||
 | 
					                        maxEnemyLevel: 100,
 | 
				
			||||||
 | 
					                        xpAmounts: [840, 840, 840, 840, 1660]
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        jobType: rng.randomElement(eidolonNarmerJobs),
 | 
				
			||||||
 | 
					                        rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTable${table}Rewards`,
 | 
				
			||||||
 | 
					                        masteryReq: 0,
 | 
				
			||||||
 | 
					                        minEnemyLevel: 50,
 | 
				
			||||||
 | 
					                        maxEnemyLevel: 70,
 | 
				
			||||||
 | 
					                        xpAmounts: [840, 840, 840, 840, 1650]
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    if (!found) {
 | 
					                ]
 | 
				
			||||||
                        logger.warn(`no job set found for type ${jobInfo.jobType}`);
 | 
					            });
 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                if (jobInfo.endless || jobInfo.isVault) {
 | 
					 | 
				
			||||||
                    jobInfo.rewards = jobInfo.rewards.replace(/Table.Rewards/, `Table${vaultTable}Rewards`);
 | 
					 | 
				
			||||||
                } else if (jobInfo.rewards.startsWith("/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierD")) {
 | 
					 | 
				
			||||||
                    jobInfo.rewards = jobInfo.rewards.replace(/Table.Rewards/, `Table${deimosDTable}Rewards`);
 | 
					 | 
				
			||||||
                } else if (!jobInfo.rewards.startsWith("/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierE")) {
 | 
					 | 
				
			||||||
                    jobInfo.rewards = jobInfo.rewards.replace(/Table.Rewards/, `Table${table}Rewards`);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            const rng = new CRng(bountyCycle);
 | 
				
			||||||
 | 
					            worldState.SyndicateMissions.push({
 | 
				
			||||||
 | 
					                _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000025" },
 | 
				
			||||||
 | 
					                Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } },
 | 
				
			||||||
 | 
					                Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } },
 | 
				
			||||||
 | 
					                Tag: "SolarisSyndicate",
 | 
				
			||||||
 | 
					                Seed: bountyCycle,
 | 
				
			||||||
 | 
					                Nodes: [],
 | 
				
			||||||
 | 
					                Jobs: [
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        jobType: rng.randomElement(venusJobs),
 | 
				
			||||||
 | 
					                        rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierATable${table}Rewards`,
 | 
				
			||||||
 | 
					                        masteryReq: 0,
 | 
				
			||||||
 | 
					                        minEnemyLevel: 5,
 | 
				
			||||||
 | 
					                        maxEnemyLevel: 15,
 | 
				
			||||||
 | 
					                        xpAmounts: [340, 340, 340]
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        jobType: rng.randomElement(venusJobs),
 | 
				
			||||||
 | 
					                        rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierBTable${table}Rewards`,
 | 
				
			||||||
 | 
					                        masteryReq: 1,
 | 
				
			||||||
 | 
					                        minEnemyLevel: 10,
 | 
				
			||||||
 | 
					                        maxEnemyLevel: 30,
 | 
				
			||||||
 | 
					                        xpAmounts: [660, 660, 660]
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        jobType: rng.randomElement(venusJobs),
 | 
				
			||||||
 | 
					                        rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierCTable${table}Rewards`,
 | 
				
			||||||
 | 
					                        masteryReq: 2,
 | 
				
			||||||
 | 
					                        minEnemyLevel: 20,
 | 
				
			||||||
 | 
					                        maxEnemyLevel: 40,
 | 
				
			||||||
 | 
					                        xpAmounts: [610, 610, 610, 900]
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        jobType: rng.randomElement(venusJobs),
 | 
				
			||||||
 | 
					                        rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierDTable${table}Rewards`,
 | 
				
			||||||
 | 
					                        masteryReq: 3,
 | 
				
			||||||
 | 
					                        minEnemyLevel: 30,
 | 
				
			||||||
 | 
					                        maxEnemyLevel: 50,
 | 
				
			||||||
 | 
					                        xpAmounts: [600, 600, 600, 600, 1170]
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        jobType: rng.randomElement(venusJobs),
 | 
				
			||||||
 | 
					                        rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable${table}Rewards`,
 | 
				
			||||||
 | 
					                        masteryReq: 5,
 | 
				
			||||||
 | 
					                        minEnemyLevel: 40,
 | 
				
			||||||
 | 
					                        maxEnemyLevel: 60,
 | 
				
			||||||
 | 
					                        xpAmounts: [690, 690, 690, 690, 1350]
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        jobType: rng.randomElement(venusJobs),
 | 
				
			||||||
 | 
					                        rewards: `Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable${table}Rewards`,
 | 
				
			||||||
 | 
					                        masteryReq: 10,
 | 
				
			||||||
 | 
					                        minEnemyLevel: 100,
 | 
				
			||||||
 | 
					                        maxEnemyLevel: 100,
 | 
				
			||||||
 | 
					                        xpAmounts: [840, 840, 840, 840, 1660]
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        jobType: rng.randomElement(venusNarmerJobs),
 | 
				
			||||||
 | 
					                        rewards: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTableBRewards",
 | 
				
			||||||
 | 
					                        masteryReq: 0,
 | 
				
			||||||
 | 
					                        minEnemyLevel: 50,
 | 
				
			||||||
 | 
					                        maxEnemyLevel: 70,
 | 
				
			||||||
 | 
					                        xpAmounts: [780, 780, 780, 780, 1540]
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            const rng = new CRng(bountyCycle);
 | 
				
			||||||
 | 
					            worldState.SyndicateMissions.push({
 | 
				
			||||||
 | 
					                _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000002" },
 | 
				
			||||||
 | 
					                Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } },
 | 
				
			||||||
 | 
					                Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } },
 | 
				
			||||||
 | 
					                Tag: "EntratiSyndicate",
 | 
				
			||||||
 | 
					                Seed: bountyCycle,
 | 
				
			||||||
 | 
					                Nodes: [],
 | 
				
			||||||
 | 
					                Jobs: [
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        jobType: rng.randomElement(microplanetJobs),
 | 
				
			||||||
 | 
					                        rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierATable${table}Rewards`,
 | 
				
			||||||
 | 
					                        masteryReq: 0,
 | 
				
			||||||
 | 
					                        minEnemyLevel: 5,
 | 
				
			||||||
 | 
					                        maxEnemyLevel: 15,
 | 
				
			||||||
 | 
					                        xpAmounts: [5, 5, 5]
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        jobType: rng.randomElement(microplanetJobs),
 | 
				
			||||||
 | 
					                        rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierCTable${table}Rewards`,
 | 
				
			||||||
 | 
					                        masteryReq: 1,
 | 
				
			||||||
 | 
					                        minEnemyLevel: 15,
 | 
				
			||||||
 | 
					                        maxEnemyLevel: 25,
 | 
				
			||||||
 | 
					                        xpAmounts: [12, 12, 12]
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        jobType: rng.randomElement(microplanetEndlessJobs),
 | 
				
			||||||
 | 
					                        rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTable${table}Rewards`,
 | 
				
			||||||
 | 
					                        masteryReq: 5,
 | 
				
			||||||
 | 
					                        minEnemyLevel: 25,
 | 
				
			||||||
 | 
					                        maxEnemyLevel: 30,
 | 
				
			||||||
 | 
					                        endless: true,
 | 
				
			||||||
 | 
					                        xpAmounts: [14, 14, 14]
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        jobType: rng.randomElement(microplanetJobs),
 | 
				
			||||||
 | 
					                        rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierDTable${deimosDTable}Rewards`,
 | 
				
			||||||
 | 
					                        masteryReq: 2,
 | 
				
			||||||
 | 
					                        minEnemyLevel: 30,
 | 
				
			||||||
 | 
					                        maxEnemyLevel: 40,
 | 
				
			||||||
 | 
					                        xpAmounts: [17, 17, 17, 25]
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        jobType: rng.randomElement(microplanetJobs),
 | 
				
			||||||
 | 
					                        rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards`,
 | 
				
			||||||
 | 
					                        masteryReq: 3,
 | 
				
			||||||
 | 
					                        minEnemyLevel: 40,
 | 
				
			||||||
 | 
					                        maxEnemyLevel: 60,
 | 
				
			||||||
 | 
					                        xpAmounts: [22, 22, 22, 22, 43]
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        jobType: rng.randomElement(microplanetJobs),
 | 
				
			||||||
 | 
					                        rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards`,
 | 
				
			||||||
 | 
					                        masteryReq: 10,
 | 
				
			||||||
 | 
					                        minEnemyLevel: 100,
 | 
				
			||||||
 | 
					                        maxEnemyLevel: 100,
 | 
				
			||||||
 | 
					                        xpAmounts: [25, 25, 25, 25, 50]
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierATable${vaultTable}Rewards`,
 | 
				
			||||||
 | 
					                        masteryReq: 5,
 | 
				
			||||||
 | 
					                        minEnemyLevel: 30,
 | 
				
			||||||
 | 
					                        maxEnemyLevel: 40,
 | 
				
			||||||
 | 
					                        xpAmounts: [2, 2, 2, 4],
 | 
				
			||||||
 | 
					                        locationTag: "ChamberB",
 | 
				
			||||||
 | 
					                        isVault: true
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierBTable${vaultTable}Rewards`,
 | 
				
			||||||
 | 
					                        masteryReq: 5,
 | 
				
			||||||
 | 
					                        minEnemyLevel: 40,
 | 
				
			||||||
 | 
					                        maxEnemyLevel: 50,
 | 
				
			||||||
 | 
					                        xpAmounts: [4, 4, 4, 5],
 | 
				
			||||||
 | 
					                        locationTag: "ChamberA",
 | 
				
			||||||
 | 
					                        isVault: true
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierCTable${vaultTable}Rewards`,
 | 
				
			||||||
 | 
					                        masteryReq: 5,
 | 
				
			||||||
 | 
					                        minEnemyLevel: 50,
 | 
				
			||||||
 | 
					                        maxEnemyLevel: 60,
 | 
				
			||||||
 | 
					                        xpAmounts: [5, 5, 5, 7],
 | 
				
			||||||
 | 
					                        locationTag: "ChamberC",
 | 
				
			||||||
 | 
					                        isVault: true
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } while (isBeforeNextExpectedWorldStateRefresh(bountyCycleEnd) && ++bountyCycle);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (config.worldState?.creditBoost) {
 | 
					    if (config.worldState?.creditBoost) {
 | 
				
			||||||
        worldState.GlobalUpgrades.push({
 | 
					        worldState.GlobalUpgrades.push({
 | 
				
			||||||
@ -394,142 +737,11 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Sortie cycling every day
 | 
					    // Sortie cycling every day
 | 
				
			||||||
    {
 | 
					    pushSortieIfRelevant(worldState.Sorties, day - 1);
 | 
				
			||||||
        let genDay;
 | 
					    pushSortieIfRelevant(worldState.Sorties, day);
 | 
				
			||||||
        let dayStart;
 | 
					 | 
				
			||||||
        let dayEnd;
 | 
					 | 
				
			||||||
        const sortieRolloverToday = getSortieTime(day);
 | 
					 | 
				
			||||||
        if (Date.now() < sortieRolloverToday) {
 | 
					 | 
				
			||||||
            // Early in the day, generate sortie for `day - 1`, expiring at `sortieRolloverToday`.
 | 
					 | 
				
			||||||
            genDay = day - 1;
 | 
					 | 
				
			||||||
            dayStart = getSortieTime(genDay);
 | 
					 | 
				
			||||||
            dayEnd = sortieRolloverToday;
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            // Late in the day, generate sortie for `day`, expiring at `getSortieTime(day + 1)`.
 | 
					 | 
				
			||||||
            genDay = day;
 | 
					 | 
				
			||||||
            dayStart = sortieRolloverToday;
 | 
					 | 
				
			||||||
            dayEnd = getSortieTime(day + 1);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const rng = new CRng(genDay);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const boss = rng.randomElement(sortieBosses);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const modifiers = [
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_LOW_ENERGY",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_IMPACT",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_SLASH",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_PUNCTURE",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_EXIMUS",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_MAGNETIC",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_CORROSIVE",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_VIRAL",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_ELECTRICITY",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_RADIATION",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_GAS",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_FIRE",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_EXPLOSION",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_FREEZE",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_TOXIN",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_POISON",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_HAZARD_RADIATION",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_HAZARD_MAGNETIC",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_HAZARD_FOG", // TODO: push this if the mission tileset is Grineer Forest
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_HAZARD_FIRE", // TODO: push this if the mission tileset is Corpus Ship or Grineer Galleon
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_HAZARD_ICE",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_HAZARD_COLD",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_SECONDARY_ONLY",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_SHOTGUN_ONLY",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_SNIPER_ONLY",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_RIFLE_ONLY",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_MELEE_ONLY",
 | 
					 | 
				
			||||||
            "SORTIE_MODIFIER_BOW_ONLY"
 | 
					 | 
				
			||||||
        ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (sortieBossToFaction[boss] == "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_SHIELDS");
 | 
					 | 
				
			||||||
        if (sortieBossToFaction[boss] != "FC_CORPUS") modifiers.push("SORTIE_MODIFIER_ARMOR");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const nodes: string[] = [];
 | 
					 | 
				
			||||||
        const availableMissionIndexes: number[] = [];
 | 
					 | 
				
			||||||
        for (const [key, value] of Object.entries(ExportRegions)) {
 | 
					 | 
				
			||||||
            if (
 | 
					 | 
				
			||||||
                sortieFactionToSystemIndexes[sortieBossToFaction[boss]].includes(value.systemIndex) &&
 | 
					 | 
				
			||||||
                sortieFactionToFactionIndexes[sortieBossToFaction[boss]].includes(value.factionIndex!) &&
 | 
					 | 
				
			||||||
                value.name.indexOf("Archwing") == -1 &&
 | 
					 | 
				
			||||||
                value.missionIndex != 0 && // Exclude MT_ASSASSINATION
 | 
					 | 
				
			||||||
                value.missionIndex != 5 && // Exclude MT_CAPTURE
 | 
					 | 
				
			||||||
                value.missionIndex != 21 && // Exclude MT_PURIFY
 | 
					 | 
				
			||||||
                value.missionIndex != 23 && // Exclude MT_JUNCTION
 | 
					 | 
				
			||||||
                value.missionIndex <= 28
 | 
					 | 
				
			||||||
            ) {
 | 
					 | 
				
			||||||
                if (!availableMissionIndexes.includes(value.missionIndex)) {
 | 
					 | 
				
			||||||
                    availableMissionIndexes.push(value.missionIndex);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                nodes.push(key);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const selectedNodes: { missionType: string; modifierType: string; node: string }[] = [];
 | 
					 | 
				
			||||||
        const missionTypes = new Set();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for (let i = 0; i < 3; i++) {
 | 
					 | 
				
			||||||
            const randomIndex = rng.randomInt(0, nodes.length - 1);
 | 
					 | 
				
			||||||
            const node = nodes[randomIndex];
 | 
					 | 
				
			||||||
            let missionIndex = ExportRegions[node].missionIndex;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (
 | 
					 | 
				
			||||||
                !["SolNode404", "SolNode411"].includes(node) && // for some reason the game doesn't like missionType changes for these missions
 | 
					 | 
				
			||||||
                missionIndex != 28 &&
 | 
					 | 
				
			||||||
                rng.randomInt(0, 2) == 2
 | 
					 | 
				
			||||||
            ) {
 | 
					 | 
				
			||||||
                missionIndex = rng.randomElement(availableMissionIndexes);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (i == 2 && rng.randomInt(0, 2) == 2) {
 | 
					 | 
				
			||||||
                const filteredModifiers = modifiers.filter(mod => mod !== "SORTIE_MODIFIER_MELEE_ONLY");
 | 
					 | 
				
			||||||
                const modifierType = rng.randomElement(filteredModifiers);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if (boss == "SORTIE_BOSS_PHORID") {
 | 
					 | 
				
			||||||
                    selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node });
 | 
					 | 
				
			||||||
                    nodes.splice(randomIndex, 1);
 | 
					 | 
				
			||||||
                    continue;
 | 
					 | 
				
			||||||
                } else if (sortieBossNode[boss]) {
 | 
					 | 
				
			||||||
                    selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node: sortieBossNode[boss] });
 | 
					 | 
				
			||||||
                    continue;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const missionType = eMissionType[missionIndex].tag;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (missionTypes.has(missionType)) {
 | 
					 | 
				
			||||||
                i--;
 | 
					 | 
				
			||||||
                continue;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const filteredModifiers =
 | 
					 | 
				
			||||||
                missionType === "MT_TERRITORY"
 | 
					 | 
				
			||||||
                    ? modifiers.filter(mod => mod != "SORTIE_MODIFIER_HAZARD_RADIATION")
 | 
					 | 
				
			||||||
                    : modifiers;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const modifierType = rng.randomElement(filteredModifiers);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            selectedNodes.push({ missionType, modifierType, node });
 | 
					 | 
				
			||||||
            nodes.splice(randomIndex, 1);
 | 
					 | 
				
			||||||
            missionTypes.add(missionType);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        worldState.Sorties.push({
 | 
					 | 
				
			||||||
            _id: { $oid: Math.trunc(dayStart / 1000).toString(16) + "d4d932c97c0a3acd" },
 | 
					 | 
				
			||||||
            Activation: { $date: { $numberLong: dayStart.toString() } },
 | 
					 | 
				
			||||||
            Expiry: { $date: { $numberLong: dayEnd.toString() } },
 | 
					 | 
				
			||||||
            Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards",
 | 
					 | 
				
			||||||
            Seed: genDay,
 | 
					 | 
				
			||||||
            Boss: boss,
 | 
					 | 
				
			||||||
            Variants: selectedNodes
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Archon Hunt cycling every week
 | 
					    // Archon Hunt cycling every week
 | 
				
			||||||
 | 
					    // TODO: Handle imminent rollover
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        const boss = ["SORTIE_BOSS_AMAR", "SORTIE_BOSS_NIRA", "SORTIE_BOSS_BOREAL"][week % 3];
 | 
					        const boss = ["SORTIE_BOSS_AMAR", "SORTIE_BOSS_NIRA", "SORTIE_BOSS_BOREAL"][week % 3];
 | 
				
			||||||
        const showdownNode = ["SolNode99", "SolNode53", "SolNode24"][week % 3];
 | 
					        const showdownNode = ["SolNode99", "SolNode53", "SolNode24"][week % 3];
 | 
				
			||||||
@ -622,6 +834,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 1999 Calendar Season cycling every week + YearIteration every 4 weeks
 | 
					    // 1999 Calendar Season cycling every week + YearIteration every 4 weeks
 | 
				
			||||||
 | 
					    // TODO: Handle imminent rollover
 | 
				
			||||||
    worldState.KnownCalendarSeasons[0].Activation = { $date: { $numberLong: weekStart.toString() } };
 | 
					    worldState.KnownCalendarSeasons[0].Activation = { $date: { $numberLong: weekStart.toString() } };
 | 
				
			||||||
    worldState.KnownCalendarSeasons[0].Expiry = { $date: { $numberLong: weekEnd.toString() } };
 | 
					    worldState.KnownCalendarSeasons[0].Expiry = { $date: { $numberLong: weekEnd.toString() } };
 | 
				
			||||||
    worldState.KnownCalendarSeasons[0].Season = ["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"][week % 4];
 | 
					    worldState.KnownCalendarSeasons[0].Season = ["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"][week % 4];
 | 
				
			||||||
 | 
				
			|||||||
@ -194,7 +194,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
 | 
				
			|||||||
    Mailbox?: IMailboxClient;
 | 
					    Mailbox?: IMailboxClient;
 | 
				
			||||||
    SubscribedToEmails: number;
 | 
					    SubscribedToEmails: number;
 | 
				
			||||||
    Created: IMongoDate;
 | 
					    Created: IMongoDate;
 | 
				
			||||||
    RewardSeed: number;
 | 
					    RewardSeed: number | bigint;
 | 
				
			||||||
    RegularCredits: number;
 | 
					    RegularCredits: number;
 | 
				
			||||||
    PremiumCredits: number;
 | 
					    PremiumCredits: number;
 | 
				
			||||||
    PremiumCreditsFree: number;
 | 
					    PremiumCreditsFree: number;
 | 
				
			||||||
@ -947,15 +947,17 @@ export interface IPersonalTechProjectDatabase {
 | 
				
			|||||||
    State: number;
 | 
					    State: number;
 | 
				
			||||||
    ReqCredits: number;
 | 
					    ReqCredits: number;
 | 
				
			||||||
    ItemType: string;
 | 
					    ItemType: string;
 | 
				
			||||||
 | 
					    ProductCategory?: string;
 | 
				
			||||||
 | 
					    CategoryItemId?: Types.ObjectId;
 | 
				
			||||||
    ReqItems: ITypeCount[];
 | 
					    ReqItems: ITypeCount[];
 | 
				
			||||||
    HasContributions?: boolean;
 | 
					    HasContributions?: boolean;
 | 
				
			||||||
    CompletionDate?: Date;
 | 
					    CompletionDate?: Date;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IPersonalTechProjectClient extends Omit<IPersonalTechProjectDatabase, "CompletionDate"> {
 | 
					export interface IPersonalTechProjectClient
 | 
				
			||||||
    CompletionDate?: IMongoDate;
 | 
					    extends Omit<IPersonalTechProjectDatabase, "CategoryItemId" | "CompletionDate"> {
 | 
				
			||||||
    ProductCategory?: string;
 | 
					 | 
				
			||||||
    CategoryItemId?: IOid;
 | 
					    CategoryItemId?: IOid;
 | 
				
			||||||
 | 
					    CompletionDate?: IMongoDate;
 | 
				
			||||||
    ItemId: IOid;
 | 
					    ItemId: IOid;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -43,6 +43,7 @@ export type IInventoryChanges = {
 | 
				
			|||||||
    Drones?: IDroneClient[];
 | 
					    Drones?: IDroneClient[];
 | 
				
			||||||
    MiscItems?: IMiscItem[];
 | 
					    MiscItems?: IMiscItem[];
 | 
				
			||||||
    EmailItems?: ITypeCount[];
 | 
					    EmailItems?: ITypeCount[];
 | 
				
			||||||
 | 
					    CrewShipRawSalvage?: ITypeCount[];
 | 
				
			||||||
    Nemesis?: Partial<INemesisClient>;
 | 
					    Nemesis?: Partial<INemesisClient>;
 | 
				
			||||||
    NewVendorPurchase?: IRecentVendorPurchaseClient; // >= 38.5.0
 | 
					    NewVendorPurchase?: IRecentVendorPurchaseClient; // >= 38.5.0
 | 
				
			||||||
    RecentVendorPurchases?: IRecentVendorPurchaseClient; // < 38.5.0
 | 
					    RecentVendorPurchases?: IRecentVendorPurchaseClient; // < 38.5.0
 | 
				
			||||||
 | 
				
			|||||||
@ -141,7 +141,7 @@ export interface IRewardInfo {
 | 
				
			|||||||
    EOM_AFK?: number;
 | 
					    EOM_AFK?: number;
 | 
				
			||||||
    rewardQualifications?: string; // did a Survival for 5 minutes and this was "1"
 | 
					    rewardQualifications?: string; // did a Survival for 5 minutes and this was "1"
 | 
				
			||||||
    PurgatoryRewardQualifications?: string;
 | 
					    PurgatoryRewardQualifications?: string;
 | 
				
			||||||
    rewardSeed?: number;
 | 
					    rewardSeed?: number | bigint;
 | 
				
			||||||
    periodicMissionTag?: string;
 | 
					    periodicMissionTag?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // for bounties, only EOM_AFK and node are given from above, plus:
 | 
					    // for bounties, only EOM_AFK and node are given from above, plus:
 | 
				
			||||||
 | 
				
			|||||||
@ -5,10 +5,11 @@ export interface IWorldState {
 | 
				
			|||||||
    BuildLabel: string;
 | 
					    BuildLabel: string;
 | 
				
			||||||
    Time: number;
 | 
					    Time: number;
 | 
				
			||||||
    Goals: IGoal[];
 | 
					    Goals: IGoal[];
 | 
				
			||||||
    SyndicateMissions: ISyndicateMissionInfo[];
 | 
					    Alerts: [];
 | 
				
			||||||
    GlobalUpgrades: IGlobalUpgrade[];
 | 
					 | 
				
			||||||
    Sorties: ISortie[];
 | 
					    Sorties: ISortie[];
 | 
				
			||||||
    LiteSorties: ILiteSortie[];
 | 
					    LiteSorties: ILiteSortie[];
 | 
				
			||||||
 | 
					    SyndicateMissions: ISyndicateMissionInfo[];
 | 
				
			||||||
 | 
					    GlobalUpgrades: IGlobalUpgrade[];
 | 
				
			||||||
    NodeOverrides: INodeOverride[];
 | 
					    NodeOverrides: INodeOverride[];
 | 
				
			||||||
    EndlessXpChoices: IEndlessXpChoice[];
 | 
					    EndlessXpChoices: IEndlessXpChoice[];
 | 
				
			||||||
    SeasonInfo: {
 | 
					    SeasonInfo: {
 | 
				
			||||||
 | 
				
			|||||||
@ -278,248 +278,6 @@
 | 
				
			|||||||
      "Tag": "SteelMeridianSyndicate",
 | 
					      "Tag": "SteelMeridianSyndicate",
 | 
				
			||||||
      "Seed": 42366,
 | 
					      "Seed": 42366,
 | 
				
			||||||
      "Nodes": ["SolNode27", "SolNode107", "SolNode214", "SettlementNode1", "SolNode177", "SolNode141", "SolNode408"]
 | 
					      "Nodes": ["SolNode27", "SolNode107", "SolNode214", "SettlementNode1", "SolNode177", "SolNode141", "SolNode408"]
 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      "_id": { "$oid": "663a71c80000000000000002" },
 | 
					 | 
				
			||||||
      "Activation": { "$date": { "$numberLong": "1715106248403" } },
 | 
					 | 
				
			||||||
      "Expiry": { "$date": { "$numberLong": "2000000000000" } },
 | 
					 | 
				
			||||||
      "Tag": "EntratiSyndicate",
 | 
					 | 
				
			||||||
      "Seed": 99561,
 | 
					 | 
				
			||||||
      "Nodes": [],
 | 
					 | 
				
			||||||
      "Jobs": [
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosGrnSurvivorBounty",
 | 
					 | 
				
			||||||
          "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierATableBRewards",
 | 
					 | 
				
			||||||
          "masteryReq": 0,
 | 
					 | 
				
			||||||
          "minEnemyLevel": 5,
 | 
					 | 
				
			||||||
          "maxEnemyLevel": 15,
 | 
					 | 
				
			||||||
          "xpAmounts": [5, 5, 5]
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAreaDefenseBounty",
 | 
					 | 
				
			||||||
          "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierCTableBRewards",
 | 
					 | 
				
			||||||
          "masteryReq": 1,
 | 
					 | 
				
			||||||
          "minEnemyLevel": 15,
 | 
					 | 
				
			||||||
          "maxEnemyLevel": 25,
 | 
					 | 
				
			||||||
          "xpAmounts": [12, 12, 12]
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessExcavateBounty",
 | 
					 | 
				
			||||||
          "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTableARewards",
 | 
					 | 
				
			||||||
          "masteryReq": 5,
 | 
					 | 
				
			||||||
          "minEnemyLevel": 25,
 | 
					 | 
				
			||||||
          "maxEnemyLevel": 30,
 | 
					 | 
				
			||||||
          "endless": true,
 | 
					 | 
				
			||||||
          "xpAmounts": [14, 14, 14]
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAssassinateBounty",
 | 
					 | 
				
			||||||
          "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierDTableBRewards",
 | 
					 | 
				
			||||||
          "masteryReq": 2,
 | 
					 | 
				
			||||||
          "minEnemyLevel": 30,
 | 
					 | 
				
			||||||
          "maxEnemyLevel": 40,
 | 
					 | 
				
			||||||
          "xpAmounts": [17, 17, 17, 25]
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosKeyPiecesBounty",
 | 
					 | 
				
			||||||
          "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards",
 | 
					 | 
				
			||||||
          "masteryReq": 3,
 | 
					 | 
				
			||||||
          "minEnemyLevel": 40,
 | 
					 | 
				
			||||||
          "maxEnemyLevel": 60,
 | 
					 | 
				
			||||||
          "xpAmounts": [22, 22, 22, 22, 43]
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosExcavateBounty",
 | 
					 | 
				
			||||||
          "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards",
 | 
					 | 
				
			||||||
          "masteryReq": 10,
 | 
					 | 
				
			||||||
          "minEnemyLevel": 100,
 | 
					 | 
				
			||||||
          "maxEnemyLevel": 100,
 | 
					 | 
				
			||||||
          "xpAmounts": [25, 25, 25, 25, 50]
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierATableCRewards",
 | 
					 | 
				
			||||||
          "masteryReq": 5,
 | 
					 | 
				
			||||||
          "minEnemyLevel": 30,
 | 
					 | 
				
			||||||
          "maxEnemyLevel": 40,
 | 
					 | 
				
			||||||
          "xpAmounts": [2, 2, 2, 4],
 | 
					 | 
				
			||||||
          "locationTag": "ChamberB",
 | 
					 | 
				
			||||||
          "isVault": true
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierBTableCRewards",
 | 
					 | 
				
			||||||
          "masteryReq": 5,
 | 
					 | 
				
			||||||
          "minEnemyLevel": 40,
 | 
					 | 
				
			||||||
          "maxEnemyLevel": 50,
 | 
					 | 
				
			||||||
          "xpAmounts": [4, 4, 4, 5],
 | 
					 | 
				
			||||||
          "locationTag": "ChamberA",
 | 
					 | 
				
			||||||
          "isVault": true
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierCTableCRewards",
 | 
					 | 
				
			||||||
          "masteryReq": 5,
 | 
					 | 
				
			||||||
          "minEnemyLevel": 50,
 | 
					 | 
				
			||||||
          "maxEnemyLevel": 60,
 | 
					 | 
				
			||||||
          "xpAmounts": [5, 5, 5, 7],
 | 
					 | 
				
			||||||
          "locationTag": "ChamberC",
 | 
					 | 
				
			||||||
          "isVault": true
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      ]
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      "_id": { "$oid": "663a71c80000000000000004" },
 | 
					 | 
				
			||||||
      "Activation": { "$date": { "$numberLong": "1715106248403" } },
 | 
					 | 
				
			||||||
      "Expiry": { "$date": { "$numberLong": "2000000000000" } },
 | 
					 | 
				
			||||||
      "Tag": "EntratiLabSyndicate",
 | 
					 | 
				
			||||||
      "Seed": 99562,
 | 
					 | 
				
			||||||
      "Nodes": []
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      "_id": { "$oid": "663a71c80000000000000008" },
 | 
					 | 
				
			||||||
      "Activation": { "$date": { "$numberLong": "1715106248403" } },
 | 
					 | 
				
			||||||
      "Expiry": { "$date": { "$numberLong": "2000000000000" } },
 | 
					 | 
				
			||||||
      "Tag": "CetusSyndicate",
 | 
					 | 
				
			||||||
      "Seed": 99561,
 | 
					 | 
				
			||||||
      "Nodes": [],
 | 
					 | 
				
			||||||
      "Jobs": [
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyCap",
 | 
					 | 
				
			||||||
          "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierATableBRewards",
 | 
					 | 
				
			||||||
          "masteryReq": 0,
 | 
					 | 
				
			||||||
          "minEnemyLevel": 5,
 | 
					 | 
				
			||||||
          "maxEnemyLevel": 15,
 | 
					 | 
				
			||||||
          "xpAmounts": [430, 430, 430]
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyLib",
 | 
					 | 
				
			||||||
          "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierBTableBRewards",
 | 
					 | 
				
			||||||
          "masteryReq": 1,
 | 
					 | 
				
			||||||
          "minEnemyLevel": 10,
 | 
					 | 
				
			||||||
          "maxEnemyLevel": 30,
 | 
					 | 
				
			||||||
          "xpAmounts": [620, 620, 620]
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/RescueBountyResc",
 | 
					 | 
				
			||||||
          "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierCTableBRewards",
 | 
					 | 
				
			||||||
          "masteryReq": 2,
 | 
					 | 
				
			||||||
          "minEnemyLevel": 20,
 | 
					 | 
				
			||||||
          "maxEnemyLevel": 40,
 | 
					 | 
				
			||||||
          "xpAmounts": [670, 670, 670, 990]
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapTwo",
 | 
					 | 
				
			||||||
          "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierDTableBRewards",
 | 
					 | 
				
			||||||
          "masteryReq": 3,
 | 
					 | 
				
			||||||
          "minEnemyLevel": 30,
 | 
					 | 
				
			||||||
          "maxEnemyLevel": 50,
 | 
					 | 
				
			||||||
          "xpAmounts": [570, 570, 570, 570, 1110]
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCache",
 | 
					 | 
				
			||||||
          "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETableBRewards",
 | 
					 | 
				
			||||||
          "masteryReq": 5,
 | 
					 | 
				
			||||||
          "minEnemyLevel": 40,
 | 
					 | 
				
			||||||
          "maxEnemyLevel": 60,
 | 
					 | 
				
			||||||
          "xpAmounts": [740, 740, 740, 740, 1450]
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyCap",
 | 
					 | 
				
			||||||
          "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETableBRewards",
 | 
					 | 
				
			||||||
          "masteryReq": 10,
 | 
					 | 
				
			||||||
          "minEnemyLevel": 100,
 | 
					 | 
				
			||||||
          "maxEnemyLevel": 100,
 | 
					 | 
				
			||||||
          "xpAmounts": [840, 840, 840, 840, 1660]
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AssassinateBountyAss",
 | 
					 | 
				
			||||||
          "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTableBRewards",
 | 
					 | 
				
			||||||
          "masteryReq": 0,
 | 
					 | 
				
			||||||
          "minEnemyLevel": 50,
 | 
					 | 
				
			||||||
          "maxEnemyLevel": 70,
 | 
					 | 
				
			||||||
          "xpAmounts": [840, 840, 840, 840, 1650]
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      ]
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      "_id": { "$oid": "663a71c80000000000000025" },
 | 
					 | 
				
			||||||
      "Activation": { "$date": { "$numberLong": "1715106248403" } },
 | 
					 | 
				
			||||||
      "Expiry": { "$date": { "$numberLong": "2000000000000" } },
 | 
					 | 
				
			||||||
      "Tag": "SolarisSyndicate",
 | 
					 | 
				
			||||||
      "Seed": 99561,
 | 
					 | 
				
			||||||
      "Nodes": [],
 | 
					 | 
				
			||||||
      "Jobs": [
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobSpy",
 | 
					 | 
				
			||||||
          "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierATableBRewards",
 | 
					 | 
				
			||||||
          "masteryReq": 0,
 | 
					 | 
				
			||||||
          "minEnemyLevel": 5,
 | 
					 | 
				
			||||||
          "maxEnemyLevel": 15,
 | 
					 | 
				
			||||||
          "xpAmounts": [340, 340, 340]
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobResource",
 | 
					 | 
				
			||||||
          "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierBTableBRewards",
 | 
					 | 
				
			||||||
          "masteryReq": 1,
 | 
					 | 
				
			||||||
          "minEnemyLevel": 10,
 | 
					 | 
				
			||||||
          "maxEnemyLevel": 30,
 | 
					 | 
				
			||||||
          "xpAmounts": [660, 660, 660]
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobRecovery",
 | 
					 | 
				
			||||||
          "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierCTableBRewards",
 | 
					 | 
				
			||||||
          "masteryReq": 2,
 | 
					 | 
				
			||||||
          "minEnemyLevel": 20,
 | 
					 | 
				
			||||||
          "maxEnemyLevel": 40,
 | 
					 | 
				
			||||||
          "xpAmounts": [610, 610, 610, 900]
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobCaches",
 | 
					 | 
				
			||||||
          "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierDTableBRewards",
 | 
					 | 
				
			||||||
          "masteryReq": 3,
 | 
					 | 
				
			||||||
          "minEnemyLevel": 30,
 | 
					 | 
				
			||||||
          "maxEnemyLevel": 50,
 | 
					 | 
				
			||||||
          "xpAmounts": [600, 600, 600, 600, 1170]
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobAmbush",
 | 
					 | 
				
			||||||
          "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETableBRewards",
 | 
					 | 
				
			||||||
          "masteryReq": 5,
 | 
					 | 
				
			||||||
          "minEnemyLevel": 40,
 | 
					 | 
				
			||||||
          "maxEnemyLevel": 60,
 | 
					 | 
				
			||||||
          "xpAmounts": [690, 690, 690, 690, 1350]
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobExcavation",
 | 
					 | 
				
			||||||
          "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETableBRewards",
 | 
					 | 
				
			||||||
          "masteryReq": 10,
 | 
					 | 
				
			||||||
          "minEnemyLevel": 100,
 | 
					 | 
				
			||||||
          "maxEnemyLevel": 100,
 | 
					 | 
				
			||||||
          "xpAmounts": [840, 840, 840, 840, 1660]
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobExterminate",
 | 
					 | 
				
			||||||
          "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTableBRewards",
 | 
					 | 
				
			||||||
          "masteryReq": 0,
 | 
					 | 
				
			||||||
          "minEnemyLevel": 50,
 | 
					 | 
				
			||||||
          "maxEnemyLevel": 70,
 | 
					 | 
				
			||||||
          "xpAmounts": [780, 780, 780, 780, 1540]
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      ]
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      "_id": { "$oid": "663a71c80000000000000029" },
 | 
					 | 
				
			||||||
      "Activation": { "$date": { "$numberLong": "1715106248403" } },
 | 
					 | 
				
			||||||
      "Expiry": { "$date": { "$numberLong": "2000000000000" } },
 | 
					 | 
				
			||||||
      "Tag": "ZarimanSyndicate",
 | 
					 | 
				
			||||||
      "Seed": 99562,
 | 
					 | 
				
			||||||
      "Nodes": []
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      "_id": { "$oid": "676b8d340000000000000006" },
 | 
					 | 
				
			||||||
      "Activation": { "$date": { "$numberLong": "1735101748215" } },
 | 
					 | 
				
			||||||
      "Expiry": { "$date": { "$numberLong": "2000000000000" } },
 | 
					 | 
				
			||||||
      "Tag": "HexSyndicate",
 | 
					 | 
				
			||||||
      "Seed": 33872,
 | 
					 | 
				
			||||||
      "Nodes": []
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  "ActiveMissions": [
 | 
					  "ActiveMissions": [
 | 
				
			||||||
 | 
				
			|||||||
@ -595,6 +595,10 @@
 | 
				
			|||||||
                                        <input class="form-check-input" type="checkbox" id="noDailyStandingLimits" />
 | 
					                                        <input class="form-check-input" type="checkbox" id="noDailyStandingLimits" />
 | 
				
			||||||
                                        <label class="form-check-label" for="noDailyStandingLimits" data-loc="cheats_noDailyStandingLimits"></label>
 | 
					                                        <label class="form-check-label" for="noDailyStandingLimits" data-loc="cheats_noDailyStandingLimits"></label>
 | 
				
			||||||
                                    </div>
 | 
					                                    </div>
 | 
				
			||||||
 | 
					                                    <div class="form-check">
 | 
				
			||||||
 | 
					                                        <input class="form-check-input" type="checkbox" id="noDailyFocusLimit" />
 | 
				
			||||||
 | 
					                                        <label class="form-check-label" for="noDailyFocusLimit" data-loc="cheats_noDailyFocusLimit"></label>
 | 
				
			||||||
 | 
					                                    </div>
 | 
				
			||||||
                                    <div class="form-check">
 | 
					                                    <div class="form-check">
 | 
				
			||||||
                                        <input class="form-check-input" type="checkbox" id="noArgonCrystalDecay" />
 | 
					                                        <input class="form-check-input" type="checkbox" id="noArgonCrystalDecay" />
 | 
				
			||||||
                                        <label class="form-check-label" for="noArgonCrystalDecay" data-loc="cheats_noArgonCrystalDecay"></label>
 | 
					                                        <label class="form-check-label" for="noArgonCrystalDecay" data-loc="cheats_noArgonCrystalDecay"></label>
 | 
				
			||||||
 | 
				
			|||||||
@ -133,13 +133,14 @@ dict = {
 | 
				
			|||||||
    cheats_unlockDoubleCapacityPotatoesEverywhere: `Orokin Reaktor & Beschleuniger überall`,
 | 
					    cheats_unlockDoubleCapacityPotatoesEverywhere: `Orokin Reaktor & Beschleuniger überall`,
 | 
				
			||||||
    cheats_unlockExilusEverywhere: `Exilus-Adapter überall`,
 | 
					    cheats_unlockExilusEverywhere: `Exilus-Adapter überall`,
 | 
				
			||||||
    cheats_unlockArcanesEverywhere: `Arkana-Adapter überall`,
 | 
					    cheats_unlockArcanesEverywhere: `Arkana-Adapter überall`,
 | 
				
			||||||
    cheats_noDailyStandingLimits: `Kein tägliches Ansehenslimit`,
 | 
					    cheats_noDailyStandingLimits: `Kein tägliches Ansehen Limit`,
 | 
				
			||||||
 | 
					    cheats_noDailyFocusLimit: `Kein tägliches Fokus-Limit`,
 | 
				
			||||||
    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: `Keine Wartezeit bei KIM`,
 | 
					    cheats_noKimCooldowns: `Keine Wartezeit bei KIM`,
 | 
				
			||||||
    cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`,
 | 
					    cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`,
 | 
				
			||||||
    cheats_noResourceExtractorDronesDamage: `[UNTRANSLATED] No Resource Extractor Drones Damage`,
 | 
					    cheats_noResourceExtractorDronesDamage: `Kein Schaden für 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`,
 | 
				
			||||||
    cheats_fastDojoRoomDestruction: `Schnelle Dojo-Raum-Zerstörung`,
 | 
					    cheats_fastDojoRoomDestruction: `Schnelle Dojo-Raum-Zerstörung`,
 | 
				
			||||||
 | 
				
			|||||||
@ -133,6 +133,7 @@ dict = {
 | 
				
			|||||||
    cheats_unlockExilusEverywhere: `Exilus Adapters Everywhere`,
 | 
					    cheats_unlockExilusEverywhere: `Exilus Adapters Everywhere`,
 | 
				
			||||||
    cheats_unlockArcanesEverywhere: `Arcane Adapters Everywhere`,
 | 
					    cheats_unlockArcanesEverywhere: `Arcane Adapters Everywhere`,
 | 
				
			||||||
    cheats_noDailyStandingLimits: `No Daily Standing Limits`,
 | 
					    cheats_noDailyStandingLimits: `No Daily Standing Limits`,
 | 
				
			||||||
 | 
					    cheats_noDailyFocusLimit: `No Daily Focus Limit`,
 | 
				
			||||||
    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`,
 | 
				
			||||||
 | 
				
			|||||||
@ -134,6 +134,7 @@ dict = {
 | 
				
			|||||||
    cheats_unlockExilusEverywhere: `Adaptadores Exilus en todas partes`,
 | 
					    cheats_unlockExilusEverywhere: `Adaptadores Exilus en todas partes`,
 | 
				
			||||||
    cheats_unlockArcanesEverywhere: `Adaptadores de Arcanos en todas partes`,
 | 
					    cheats_unlockArcanesEverywhere: `Adaptadores de Arcanos en todas partes`,
 | 
				
			||||||
    cheats_noDailyStandingLimits: `Sin límite diario de reputación`,
 | 
					    cheats_noDailyStandingLimits: `Sin límite diario de reputación`,
 | 
				
			||||||
 | 
					    cheats_noDailyFocusLimit: `[UNTRANSLATED] No Daily Focus Limits`,
 | 
				
			||||||
    cheats_noArgonCrystalDecay: `Sin descomposición de cristal de Argón`,
 | 
					    cheats_noArgonCrystalDecay: `Sin descomposición de cristal de Argón`,
 | 
				
			||||||
    cheats_noMasteryRankUpCooldown: `Sin tiempo de espera para rango de maestría`,
 | 
					    cheats_noMasteryRankUpCooldown: `Sin tiempo de espera para rango de maestría`,
 | 
				
			||||||
    cheats_noVendorPurchaseLimits: `Sin límite de compras de vendedores`,
 | 
					    cheats_noVendorPurchaseLimits: `Sin límite de compras de vendedores`,
 | 
				
			||||||
 | 
				
			|||||||
@ -134,6 +134,7 @@ dict = {
 | 
				
			|||||||
    cheats_unlockExilusEverywhere: `Adaptateurs Exilus partout`,
 | 
					    cheats_unlockExilusEverywhere: `Adaptateurs Exilus partout`,
 | 
				
			||||||
    cheats_unlockArcanesEverywhere: `Adaptateur d'Arcanes partout`,
 | 
					    cheats_unlockArcanesEverywhere: `Adaptateur d'Arcanes partout`,
 | 
				
			||||||
    cheats_noDailyStandingLimits: `Pas de limite de réputation journalière`,
 | 
					    cheats_noDailyStandingLimits: `Pas de limite de réputation journalière`,
 | 
				
			||||||
 | 
					    cheats_noDailyFocusLimit: `[UNTRANSLATED] No Daily Focus Limits`,
 | 
				
			||||||
    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`,
 | 
				
			||||||
 | 
				
			|||||||
@ -134,6 +134,7 @@ dict = {
 | 
				
			|||||||
    cheats_unlockExilusEverywhere: `Адаптеры Эксилус везде`,
 | 
					    cheats_unlockExilusEverywhere: `Адаптеры Эксилус везде`,
 | 
				
			||||||
    cheats_unlockArcanesEverywhere: `Адаптеры для мистификаторов везде`,
 | 
					    cheats_unlockArcanesEverywhere: `Адаптеры для мистификаторов везде`,
 | 
				
			||||||
    cheats_noDailyStandingLimits: `Без ежедневных ограничений репутации`,
 | 
					    cheats_noDailyStandingLimits: `Без ежедневных ограничений репутации`,
 | 
				
			||||||
 | 
					    cheats_noDailyFocusLimit: `[UNTRANSLATED] No Daily Focus Limits`,
 | 
				
			||||||
    cheats_noArgonCrystalDecay: `Без распада аргоновых кристаллов`,
 | 
					    cheats_noArgonCrystalDecay: `Без распада аргоновых кристаллов`,
 | 
				
			||||||
    cheats_noMasteryRankUpCooldown: `Повышение ранга мастерства без кулдауна`,
 | 
					    cheats_noMasteryRankUpCooldown: `Повышение ранга мастерства без кулдауна`,
 | 
				
			||||||
    cheats_noVendorPurchaseLimits: `Отсутствие лимитов на покупки у вендоров`,
 | 
					    cheats_noVendorPurchaseLimits: `Отсутствие лимитов на покупки у вендоров`,
 | 
				
			||||||
 | 
				
			|||||||
@ -134,6 +134,7 @@ dict = {
 | 
				
			|||||||
    cheats_unlockExilusEverywhere: `全物品自带适配器`,
 | 
					    cheats_unlockExilusEverywhere: `全物品自带适配器`,
 | 
				
			||||||
    cheats_unlockArcanesEverywhere: `全物品自带赋能适配器`,
 | 
					    cheats_unlockArcanesEverywhere: `全物品自带赋能适配器`,
 | 
				
			||||||
    cheats_noDailyStandingLimits: `无每日声望限制`,
 | 
					    cheats_noDailyStandingLimits: `无每日声望限制`,
 | 
				
			||||||
 | 
					    cheats_noDailyFocusLimit: `[UNTRANSLATED] No Daily Focus Limits`,
 | 
				
			||||||
    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`,
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user