Updated french translations (webui) #2282
							
								
								
									
										20
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								README.md
									
									
									
									
									
								
							@ -14,4 +14,22 @@ SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [confi
 | 
			
		||||
 | 
			
		||||
- `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 ]`.
 | 
			
		||||
- `worldState.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE.
 | 
			
		||||
- `worldState.eidolonOverride` can be set to `day` or `night` to lock the time to day/fass and night/vome on Plains of Eidolon/Cambion Drift.
 | 
			
		||||
- `worldState.vallisOverride` can be set to `warm` or `cold` to lock the temperature on Orb Vallis.
 | 
			
		||||
- `worldState.nightwaveOverride` will lock the nightwave season, assuming the client is new enough for it. Valid values:
 | 
			
		||||
  - `RadioLegionIntermission13Syndicate` for Nora's Mix Vol. 9
 | 
			
		||||
  - `RadioLegionIntermission12Syndicate` for Nora's Mix Vol. 8
 | 
			
		||||
  - `RadioLegionIntermission11Syndicate` for Nora's Mix Vol. 7
 | 
			
		||||
  - `RadioLegionIntermission10Syndicate` for Nora's Mix Vol. 6
 | 
			
		||||
  - `RadioLegionIntermission9Syndicate` for Nora's Mix Vol. 5
 | 
			
		||||
  - `RadioLegionIntermission8Syndicate` for Nora's Mix Vol. 4
 | 
			
		||||
  - `RadioLegionIntermission7Syndicate` for Nora's Mix Vol. 3
 | 
			
		||||
  - `RadioLegionIntermission6Syndicate` for Nora's Mix Vol. 2
 | 
			
		||||
  - `RadioLegionIntermission5Syndicate` for Nora's Mix Vol. 1
 | 
			
		||||
  - `RadioLegionIntermission4Syndicate` for Nora's Choice
 | 
			
		||||
  - `RadioLegionIntermission3Syndicate` for Intermission III
 | 
			
		||||
  - `RadioLegion3Syndicate` for Glassmaker
 | 
			
		||||
  - `RadioLegionIntermission2Syndicate` for Intermission II
 | 
			
		||||
  - `RadioLegion2Syndicate` for The Emissary
 | 
			
		||||
  - `RadioLegionIntermissionSyndicate` for Intermission I
 | 
			
		||||
  - `RadioLegionSyndicate` for The Wolf of Saturn Six
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@
 | 
			
		||||
echo Updating SpaceNinjaServer...
 | 
			
		||||
git fetch --prune
 | 
			
		||||
git stash
 | 
			
		||||
git reset --hard origin/main
 | 
			
		||||
git checkout -f origin/main
 | 
			
		||||
 | 
			
		||||
if exist static\data\0\ (
 | 
			
		||||
	echo Updating stripped assets...
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										23
									
								
								UPDATE AND START SERVER.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										23
									
								
								UPDATE AND START SERVER.sh
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
echo "Updating SpaceNinjaServer..."
 | 
			
		||||
git fetch --prune
 | 
			
		||||
git stash
 | 
			
		||||
git checkout -f origin/main
 | 
			
		||||
 | 
			
		||||
if [ -d "static/data/0/" ]; then
 | 
			
		||||
    echo "Updating stripped assets..."
 | 
			
		||||
    cd static/data/0/
 | 
			
		||||
    git pull
 | 
			
		||||
    cd ../../../
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
echo "Updating dependencies..."
 | 
			
		||||
npm i --omit=dev
 | 
			
		||||
 | 
			
		||||
npm run build
 | 
			
		||||
if [ $? -eq 0 ]; then
 | 
			
		||||
    npm run start
 | 
			
		||||
    echo "SpaceNinjaServer seems to have crashed."
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
@ -55,6 +55,8 @@
 | 
			
		||||
    "affinityBoost": false,
 | 
			
		||||
    "resourceBoost": false,
 | 
			
		||||
    "starDays": true,
 | 
			
		||||
    "lockTime": 0
 | 
			
		||||
    "eidolonOverride": "",
 | 
			
		||||
    "vallisOverride": "",
 | 
			
		||||
    "nightwaveOverride": ""
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										8
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -18,7 +18,7 @@
 | 
			
		||||
        "morgan": "^1.10.0",
 | 
			
		||||
        "ncp": "^2.0.0",
 | 
			
		||||
        "typescript": "^5.5",
 | 
			
		||||
        "warframe-public-export-plus": "^0.5.64",
 | 
			
		||||
        "warframe-public-export-plus": "^0.5.66",
 | 
			
		||||
        "warframe-riven-info": "^0.1.2",
 | 
			
		||||
        "winston": "^3.17.0",
 | 
			
		||||
        "winston-daily-rotate-file": "^5.0.0"
 | 
			
		||||
@ -3814,9 +3814,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/warframe-public-export-plus": {
 | 
			
		||||
      "version": "0.5.64",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.64.tgz",
 | 
			
		||||
      "integrity": "sha512-JyHRtYumfwQ1Iog2unzlBWfQHJlZER+iUISquyFFv0Qqtv2QsNzFv2AbV7sCaqgDcE8tw6e5/YqGgfI0m403/g=="
 | 
			
		||||
      "version": "0.5.66",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.66.tgz",
 | 
			
		||||
      "integrity": "sha512-AU7XQA96OfYrLm2RioCwDjjdI3IrsmUiqebXyE+bpM0iST+4x/NHu8LTRT4Oygfo/2OBtDYhib7G6re0EeAe5g=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/warframe-riven-info": {
 | 
			
		||||
      "version": "0.1.2",
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,7 @@
 | 
			
		||||
    "morgan": "^1.10.0",
 | 
			
		||||
    "ncp": "^2.0.0",
 | 
			
		||||
    "typescript": "^5.5",
 | 
			
		||||
    "warframe-public-export-plus": "^0.5.64",
 | 
			
		||||
    "warframe-public-export-plus": "^0.5.66",
 | 
			
		||||
    "warframe-riven-info": "^0.1.2",
 | 
			
		||||
    "winston": "^3.17.0",
 | 
			
		||||
    "winston-daily-rotate-file": "^5.0.0"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										27
									
								
								src/controllers/api/adoptPetController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/controllers/api/adoptPetController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const adoptPetController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "KubrowPets");
 | 
			
		||||
    const data = getJSONfromString<IAdoptPetRequest>(String(req.body));
 | 
			
		||||
    const details = inventory.KubrowPets.id(data.petId)!.Details!;
 | 
			
		||||
    details.Name = data.name;
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json({
 | 
			
		||||
        petId: data.petId,
 | 
			
		||||
        newName: data.name
 | 
			
		||||
    } satisfies IAdoptPetResponse);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IAdoptPetRequest {
 | 
			
		||||
    petId: string;
 | 
			
		||||
    name: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IAdoptPetResponse {
 | 
			
		||||
    petId: string;
 | 
			
		||||
    newName: string;
 | 
			
		||||
}
 | 
			
		||||
@ -17,7 +17,7 @@ import {
 | 
			
		||||
} from "@/src/services/inventoryService";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
 | 
			
		||||
import { InventorySlot, IPendingRecipeDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { InventorySlot, IPendingRecipeDatabase, Status } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { toOid2 } from "@/src/helpers/inventoryHelpers";
 | 
			
		||||
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
 | 
			
		||||
import { IRecipe } from "warframe-public-export-plus";
 | 
			
		||||
@ -105,7 +105,21 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
 | 
			
		||||
                ...updateCurrency(inventory, cost, true)
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        if (recipe.secretIngredientAction != "SIA_UNBRAND") {
 | 
			
		||||
 | 
			
		||||
        if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") {
 | 
			
		||||
            const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!;
 | 
			
		||||
            if (pet.Details!.HatchDate!.getTime() > Date.now()) {
 | 
			
		||||
                pet.Details!.HatchDate = new Date();
 | 
			
		||||
            }
 | 
			
		||||
            let canSetActive = true;
 | 
			
		||||
            for (const pet of inventory.KubrowPets) {
 | 
			
		||||
                if (pet.Details!.Status == Status.StatusAvailable) {
 | 
			
		||||
                    canSetActive = false;
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            pet.Details!.Status = canSetActive ? Status.StatusAvailable : Status.StatusStasis;
 | 
			
		||||
        } else if (recipe.secretIngredientAction != "SIA_UNBRAND") {
 | 
			
		||||
            InventoryChanges = {
 | 
			
		||||
                ...InventoryChanges,
 | 
			
		||||
                ...(await addItem(
 | 
			
		||||
@ -118,7 +132,10 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
 | 
			
		||||
                ))
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        if (config.claimingBlueprintRefundsIngredients) {
 | 
			
		||||
        if (
 | 
			
		||||
            config.claimingBlueprintRefundsIngredients &&
 | 
			
		||||
            recipe.secretIngredientAction != "SIA_CREATE_KUBROW" // Can't refund the egg
 | 
			
		||||
        ) {
 | 
			
		||||
            await refundRecipeIngredients(inventory, InventoryChanges, recipe, pendingRecipe);
 | 
			
		||||
        }
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
 | 
			
		||||
@ -310,7 +310,7 @@ export const getInventoryResponse = async (
 | 
			
		||||
        // Fix nemesis for older versions
 | 
			
		||||
        if (
 | 
			
		||||
            inventoryResponse.Nemesis &&
 | 
			
		||||
            version_compare(getNemesisManifest(inventoryResponse.Nemesis.manifest).minBuild, buildLabel) < 0
 | 
			
		||||
            version_compare(buildLabel, getNemesisManifest(inventoryResponse.Nemesis.manifest).minBuild) < 0
 | 
			
		||||
        ) {
 | 
			
		||||
            inventoryResponse.Nemesis = undefined;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,18 @@
 | 
			
		||||
import { version_compare } from "@/src/helpers/inventoryHelpers";
 | 
			
		||||
import {
 | 
			
		||||
    consumeModCharge,
 | 
			
		||||
    decodeNemesisGuess,
 | 
			
		||||
    encodeNemesisGuess,
 | 
			
		||||
    getInfNodes,
 | 
			
		||||
    getKnifeUpgrade,
 | 
			
		||||
    getNemesisManifest,
 | 
			
		||||
    getNemesisPasscode,
 | 
			
		||||
    getNemesisPasscodeModTypes,
 | 
			
		||||
    GUESS_CORRECT,
 | 
			
		||||
    GUESS_INCORRECT,
 | 
			
		||||
    GUESS_NEUTRAL,
 | 
			
		||||
    GUESS_NONE,
 | 
			
		||||
    GUESS_WILDCARD,
 | 
			
		||||
    IKnifeResponse
 | 
			
		||||
} from "@/src/helpers/nemesisHelpers";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
@ -82,7 +88,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            for (let i = 0; i != 3; ++i) {
 | 
			
		||||
                if (body.guess[i] == passcode[i]) {
 | 
			
		||||
                if (body.guess[i] == passcode[i] || body.guess[i] == GUESS_WILDCARD) {
 | 
			
		||||
                    ++guessResult;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
@ -97,18 +103,29 @@ export const nemesisController: RequestHandler = async (req, res) => {
 | 
			
		||||
        if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
 | 
			
		||||
            const guess: number[] = [body.guess & 0xf, (body.guess >> 4) & 0xf, (body.guess >> 8) & 0xf];
 | 
			
		||||
            const passcode = getNemesisPasscode(inventory.Nemesis!)[0];
 | 
			
		||||
 | 
			
		||||
            // Add to GuessHistory
 | 
			
		||||
            const result1 = passcode == guess[0] ? 0 : 1;
 | 
			
		||||
            const result2 = passcode == guess[1] ? 0 : 1;
 | 
			
		||||
            const result3 = passcode == guess[2] ? 0 : 1;
 | 
			
		||||
            const result1 = passcode == guess[0] ? GUESS_CORRECT : GUESS_INCORRECT;
 | 
			
		||||
            const result2 = passcode == guess[1] ? GUESS_CORRECT : GUESS_INCORRECT;
 | 
			
		||||
            const result3 = passcode == guess[2] ? GUESS_CORRECT : GUESS_INCORRECT;
 | 
			
		||||
            inventory.Nemesis!.GuessHistory.push(
 | 
			
		||||
                encodeNemesisGuess(guess[0], result1, guess[1], result2, guess[2], result3)
 | 
			
		||||
                encodeNemesisGuess([
 | 
			
		||||
                    {
 | 
			
		||||
                        symbol: guess[0],
 | 
			
		||||
                        result: result1
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        symbol: guess[1],
 | 
			
		||||
                        result: result2
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        symbol: guess[2],
 | 
			
		||||
                        result: result3
 | 
			
		||||
                    }
 | 
			
		||||
                ])
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            // Increase antivirus if correct antivirus mod is installed
 | 
			
		||||
            const response: IKnifeResponse = {};
 | 
			
		||||
            if (result1 == 0 || result2 == 0 || result3 == 0) {
 | 
			
		||||
            if (result1 == GUESS_CORRECT || result2 == GUESS_CORRECT || result3 == GUESS_CORRECT) {
 | 
			
		||||
                let antivirusGain = 5;
 | 
			
		||||
                const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
 | 
			
		||||
                const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid);
 | 
			
		||||
@ -149,18 +166,48 @@ export const nemesisController: RequestHandler = async (req, res) => {
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            res.json(response);
 | 
			
		||||
        } else {
 | 
			
		||||
            const passcode = getNemesisPasscode(inventory.Nemesis!);
 | 
			
		||||
            if (passcode[body.position] != body.guess) {
 | 
			
		||||
                res.end();
 | 
			
		||||
            } else {
 | 
			
		||||
                inventory.Nemesis!.Rank += 1;
 | 
			
		||||
                inventory.Nemesis!.InfNodes = getInfNodes(
 | 
			
		||||
                    getNemesisManifest(inventory.Nemesis!.manifest),
 | 
			
		||||
                    inventory.Nemesis!.Rank
 | 
			
		||||
            // For first guess, create a new entry.
 | 
			
		||||
            if (body.position == 0) {
 | 
			
		||||
                inventory.Nemesis!.GuessHistory.push(
 | 
			
		||||
                    encodeNemesisGuess([
 | 
			
		||||
                        {
 | 
			
		||||
                            symbol: GUESS_NONE,
 | 
			
		||||
                            result: GUESS_NEUTRAL
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            symbol: GUESS_NONE,
 | 
			
		||||
                            result: GUESS_NEUTRAL
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            symbol: GUESS_NONE,
 | 
			
		||||
                            result: GUESS_NEUTRAL
 | 
			
		||||
                        }
 | 
			
		||||
                    ])
 | 
			
		||||
                );
 | 
			
		||||
                await inventory.save();
 | 
			
		||||
                res.json({ RankIncrease: 1 });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Evaluate guess
 | 
			
		||||
            const correct =
 | 
			
		||||
                body.guess == GUESS_WILDCARD || getNemesisPasscode(inventory.Nemesis!)[body.position] == body.guess;
 | 
			
		||||
 | 
			
		||||
            // Update entry
 | 
			
		||||
            const guess = decodeNemesisGuess(
 | 
			
		||||
                inventory.Nemesis!.GuessHistory[inventory.Nemesis!.GuessHistory.length - 1]
 | 
			
		||||
            );
 | 
			
		||||
            guess[body.position].symbol = body.guess;
 | 
			
		||||
            guess[body.position].result = correct ? GUESS_CORRECT : GUESS_INCORRECT;
 | 
			
		||||
            inventory.Nemesis!.GuessHistory[inventory.Nemesis!.GuessHistory.length - 1] = encodeNemesisGuess(guess);
 | 
			
		||||
 | 
			
		||||
            // Increase rank if incorrect
 | 
			
		||||
            let RankIncrease: number | undefined;
 | 
			
		||||
            if (!correct) {
 | 
			
		||||
                RankIncrease = 1;
 | 
			
		||||
                const manifest = getNemesisManifest(inventory.Nemesis!.manifest);
 | 
			
		||||
                inventory.Nemesis!.Rank = Math.min(inventory.Nemesis!.Rank + 1, manifest.systemIndexes.length - 1);
 | 
			
		||||
                inventory.Nemesis!.InfNodes = getInfNodes(manifest, inventory.Nemesis!.Rank);
 | 
			
		||||
            }
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            res.json({ RankIncrease });
 | 
			
		||||
        }
 | 
			
		||||
    } else if ((req.query.mode as string) == "rs") {
 | 
			
		||||
        // report spawn; POST but no application data in body
 | 
			
		||||
@ -170,11 +217,14 @@ export const nemesisController: RequestHandler = async (req, res) => {
 | 
			
		||||
        res.json({ LastEnc: inventory.Nemesis!.LastEnc });
 | 
			
		||||
    } else if ((req.query.mode as string) == "s") {
 | 
			
		||||
        const inventory = await getInventory(account._id.toString(), "Nemesis");
 | 
			
		||||
        if (inventory.Nemesis) {
 | 
			
		||||
            logger.warn(`overwriting an existing nemesis as a new one is being requested`);
 | 
			
		||||
        }
 | 
			
		||||
        const body = getJSONfromString<INemesisStartRequest>(String(req.body));
 | 
			
		||||
        body.target.fp = BigInt(body.target.fp);
 | 
			
		||||
 | 
			
		||||
        const manifest = getNemesisManifest(body.target.manifest);
 | 
			
		||||
        if (account.BuildLabel && version_compare(manifest.minBuild, account.BuildLabel) < 0) {
 | 
			
		||||
        if (account.BuildLabel && version_compare(account.BuildLabel, manifest.minBuild) < 0) {
 | 
			
		||||
            logger.warn(
 | 
			
		||||
                `client on version ${account.BuildLabel} provided nemesis manifest ${body.target.manifest} which was expected to require ${manifest.minBuild} or above. please file a bug report.`
 | 
			
		||||
            );
 | 
			
		||||
@ -185,13 +235,15 @@ export const nemesisController: RequestHandler = async (req, res) => {
 | 
			
		||||
            const weapons: readonly string[] = manifest.weapons;
 | 
			
		||||
            const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1);
 | 
			
		||||
            weaponIdx = initialWeaponIdx;
 | 
			
		||||
            do {
 | 
			
		||||
                const weapon = weapons[weaponIdx];
 | 
			
		||||
                if (body.target.DisallowedWeapons.indexOf(weapon) == -1) {
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
                weaponIdx = (weaponIdx + 1) % weapons.length;
 | 
			
		||||
            } while (weaponIdx != initialWeaponIdx);
 | 
			
		||||
            if (body.target.DisallowedWeapons) {
 | 
			
		||||
                do {
 | 
			
		||||
                    const weapon = weapons[weaponIdx];
 | 
			
		||||
                    if (body.target.DisallowedWeapons.indexOf(weapon) == -1) {
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
                    weaponIdx = (weaponIdx + 1) % weapons.length;
 | 
			
		||||
                } while (weaponIdx != initialWeaponIdx);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        inventory.Nemesis = {
 | 
			
		||||
@ -212,10 +264,10 @@ export const nemesisController: RequestHandler = async (req, res) => {
 | 
			
		||||
            GuessHistory: [],
 | 
			
		||||
            Hints: [],
 | 
			
		||||
            HintProgress: 0,
 | 
			
		||||
            Weakened: body.target.Weakened,
 | 
			
		||||
            Weakened: false,
 | 
			
		||||
            PrevOwners: 0,
 | 
			
		||||
            HenchmenKilled: 0,
 | 
			
		||||
            SecondInCommand: body.target.SecondInCommand,
 | 
			
		||||
            SecondInCommand: false,
 | 
			
		||||
            MissionCount: 0,
 | 
			
		||||
            LastEnc: 0
 | 
			
		||||
        };
 | 
			
		||||
@ -276,7 +328,7 @@ interface INemesisStartRequest {
 | 
			
		||||
        KillingSuit: string;
 | 
			
		||||
        killingDamageType: number;
 | 
			
		||||
        ShoulderHelmet: string;
 | 
			
		||||
        DisallowedWeapons: string[];
 | 
			
		||||
        DisallowedWeapons?: string[];
 | 
			
		||||
        WeaponIdx: number;
 | 
			
		||||
        AgentIdx: number;
 | 
			
		||||
        BirthNode: string;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										23
									
								
								src/controllers/api/renamePetController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/controllers/api/renamePetController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { getInventory, updateCurrency } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const renamePetController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "KubrowPets PremiumCredits PremiumCreditsFree");
 | 
			
		||||
    const data = getJSONfromString<IRenamePetRequest>(String(req.body));
 | 
			
		||||
    const details = inventory.KubrowPets.id(data.petId)!.Details!;
 | 
			
		||||
    details.Name = data.name;
 | 
			
		||||
    const currencyChanges = updateCurrency(inventory, 15, true);
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json({
 | 
			
		||||
        ...data,
 | 
			
		||||
        inventoryChanges: currencyChanges
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IRenamePetRequest {
 | 
			
		||||
    petId: string;
 | 
			
		||||
    name: string;
 | 
			
		||||
}
 | 
			
		||||
@ -3,12 +3,14 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { getRecipe } from "@/src/services/itemDataService";
 | 
			
		||||
import { addItem, freeUpSlot, getInventory, updateCurrency } from "@/src/services/inventoryService";
 | 
			
		||||
import { addItem, addKubrowPet, freeUpSlot, getInventory, updateCurrency } from "@/src/services/inventoryService";
 | 
			
		||||
import { unixTimesInMs } from "@/src/constants/timeConstants";
 | 
			
		||||
import { Types } from "mongoose";
 | 
			
		||||
import { InventorySlot, ISpectreLoadout } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { toOid } from "@/src/helpers/inventoryHelpers";
 | 
			
		||||
import { fromOid, toOid } from "@/src/helpers/inventoryHelpers";
 | 
			
		||||
import { ExportWeapons } from "warframe-public-export-plus";
 | 
			
		||||
import { getRandomElement } from "@/src/services/rngService";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
 | 
			
		||||
interface IStartRecipeRequest {
 | 
			
		||||
    RecipeName: string;
 | 
			
		||||
@ -42,24 +44,35 @@ export const startRecipeController: RequestHandler = async (req, res) => {
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i != recipe.ingredients.length; ++i) {
 | 
			
		||||
        if (startRecipeRequest.Ids[i] && startRecipeRequest.Ids[i][0] != "/") {
 | 
			
		||||
            const category = ExportWeapons[recipe.ingredients[i].ItemType].productCategory;
 | 
			
		||||
            if (category != "LongGuns" && category != "Pistols" && category != "Melee") {
 | 
			
		||||
                throw new Error(`unexpected equipment ingredient type: ${category}`);
 | 
			
		||||
            if (recipe.ingredients[i].ItemType == "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem") {
 | 
			
		||||
                const index = inventory.KubrowPetEggs!.findIndex(x => x._id.equals(startRecipeRequest.Ids[i]));
 | 
			
		||||
                if (index != -1) {
 | 
			
		||||
                    inventory.KubrowPetEggs!.splice(index, 1);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                const category = ExportWeapons[recipe.ingredients[i].ItemType].productCategory;
 | 
			
		||||
                if (category != "LongGuns" && category != "Pistols" && category != "Melee") {
 | 
			
		||||
                    throw new Error(`unexpected equipment ingredient type: ${category}`);
 | 
			
		||||
                }
 | 
			
		||||
                const equipmentIndex = inventory[category].findIndex(x => x._id.equals(startRecipeRequest.Ids[i]));
 | 
			
		||||
                if (equipmentIndex == -1) {
 | 
			
		||||
                    throw new Error(`could not find equipment item to use for recipe`);
 | 
			
		||||
                }
 | 
			
		||||
                pr[category] ??= [];
 | 
			
		||||
                pr[category].push(inventory[category][equipmentIndex]);
 | 
			
		||||
                inventory[category].splice(equipmentIndex, 1);
 | 
			
		||||
                freeUpSlot(inventory, InventorySlot.WEAPONS);
 | 
			
		||||
            }
 | 
			
		||||
            const equipmentIndex = inventory[category].findIndex(x => x._id.equals(startRecipeRequest.Ids[i]));
 | 
			
		||||
            if (equipmentIndex == -1) {
 | 
			
		||||
                throw new Error(`could not find equipment item to use for recipe`);
 | 
			
		||||
            }
 | 
			
		||||
            pr[category] ??= [];
 | 
			
		||||
            pr[category].push(inventory[category][equipmentIndex]);
 | 
			
		||||
            inventory[category].splice(equipmentIndex, 1);
 | 
			
		||||
            freeUpSlot(inventory, InventorySlot.WEAPONS);
 | 
			
		||||
        } else {
 | 
			
		||||
            await addItem(inventory, recipe.ingredients[i].ItemType, recipe.ingredients[i].ItemCount * -1);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
 | 
			
		||||
    let inventoryChanges: IInventoryChanges | undefined;
 | 
			
		||||
    if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") {
 | 
			
		||||
        inventoryChanges = addKubrowPet(inventory, getRandomElement(recipe.secretIngredients!)!.ItemType);
 | 
			
		||||
        pr.KubrowPet = new Types.ObjectId(fromOid(inventoryChanges.KubrowPets![0].ItemId));
 | 
			
		||||
    } else if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
 | 
			
		||||
        const spectreLoadout: ISpectreLoadout = {
 | 
			
		||||
            ItemType: recipe.resultType,
 | 
			
		||||
            Suits: "",
 | 
			
		||||
@ -116,5 +129,5 @@ export const startRecipeController: RequestHandler = async (req, res) => {
 | 
			
		||||
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
 | 
			
		||||
    res.json({ RecipeId: toOid(pr._id) });
 | 
			
		||||
    res.json({ RecipeId: toOid(pr._id), InventoryChanges: inventoryChanges });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ import { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus";
 | 
			
		||||
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
 | 
			
		||||
import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService";
 | 
			
		||||
import { addMiscItem, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
import { toStoreItem } from "@/src/services/itemDataService";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
@ -18,80 +18,83 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
 | 
			
		||||
        syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: data.AffiliationTag, Standing: 0 }) - 1];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const level = data.SacrificeLevel - (syndicate.Title ?? 0);
 | 
			
		||||
    const oldLevel = syndicate.Title ?? 0;
 | 
			
		||||
    const levelIncrease = data.SacrificeLevel - oldLevel;
 | 
			
		||||
    if (levelIncrease < 1) {
 | 
			
		||||
        throw new Error(`syndicate sacrifice needs an increase of at least 1`);
 | 
			
		||||
    }
 | 
			
		||||
    if (levelIncrease > 1 && !data.AllowMultiple) {
 | 
			
		||||
        throw new Error(`desired syndicate level is an increase of ${levelIncrease}, max. allowed increase is 1`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const res: ISyndicateSacrificeResponse = {
 | 
			
		||||
        AffiliationTag: data.AffiliationTag,
 | 
			
		||||
        InventoryChanges: {},
 | 
			
		||||
        Level: data.SacrificeLevel,
 | 
			
		||||
        LevelIncrease: level <= 0 ? 1 : level,
 | 
			
		||||
        LevelIncrease: levelIncrease,
 | 
			
		||||
        NewEpisodeReward: false
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Process sacrifices and rewards for every level we're reaching
 | 
			
		||||
    const manifest = ExportSyndicates[data.AffiliationTag];
 | 
			
		||||
    let sacrifice: ISyndicateSacrifice | undefined;
 | 
			
		||||
    let reward: string | undefined;
 | 
			
		||||
    if (data.SacrificeLevel == 0) {
 | 
			
		||||
        sacrifice = manifest.initiationSacrifice;
 | 
			
		||||
        reward = manifest.initiationReward;
 | 
			
		||||
        syndicate.Initiated = true;
 | 
			
		||||
    } else {
 | 
			
		||||
        sacrifice = manifest.titles?.find(x => x.level == data.SacrificeLevel)?.sacrifice;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (sacrifice) {
 | 
			
		||||
        res.InventoryChanges = { ...updateCurrency(inventory, sacrifice.credits, false) };
 | 
			
		||||
 | 
			
		||||
        const miscItemChanges = sacrifice.items.map(x => ({
 | 
			
		||||
            ItemType: x.ItemType,
 | 
			
		||||
            ItemCount: x.ItemCount * -1
 | 
			
		||||
        }));
 | 
			
		||||
        addMiscItems(inventory, miscItemChanges);
 | 
			
		||||
        res.InventoryChanges.MiscItems = miscItemChanges;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    syndicate.Title ??= 0;
 | 
			
		||||
    syndicate.Title += 1;
 | 
			
		||||
 | 
			
		||||
    if (reward) {
 | 
			
		||||
        combineInventoryChanges(
 | 
			
		||||
            res.InventoryChanges,
 | 
			
		||||
            (await handleStoreItemAcquisition(reward, inventory)).InventoryChanges
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Quacks like a nightwave syndicate?
 | 
			
		||||
    if (manifest.dailyChallenges) {
 | 
			
		||||
        const title = manifest.titles!.find(x => x.level == syndicate.Title);
 | 
			
		||||
        if (title) {
 | 
			
		||||
            res.NewEpisodeReward = true;
 | 
			
		||||
            let rewardType: string;
 | 
			
		||||
            let rewardCount: number;
 | 
			
		||||
            if (title.storeItemReward) {
 | 
			
		||||
                rewardType = title.storeItemReward;
 | 
			
		||||
                rewardCount = 1;
 | 
			
		||||
            } else {
 | 
			
		||||
                rewardType = toStoreItem(title.reward!.ItemType);
 | 
			
		||||
                rewardCount = title.reward!.ItemCount;
 | 
			
		||||
    for (let level = oldLevel + 1; level <= data.SacrificeLevel; ++level) {
 | 
			
		||||
        let sacrifice: ISyndicateSacrifice | undefined;
 | 
			
		||||
        if (level == 0) {
 | 
			
		||||
            sacrifice = manifest.initiationSacrifice;
 | 
			
		||||
            if (manifest.initiationReward) {
 | 
			
		||||
                combineInventoryChanges(
 | 
			
		||||
                    res.InventoryChanges,
 | 
			
		||||
                    (await handleStoreItemAcquisition(manifest.initiationReward, inventory)).InventoryChanges
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
            const rewardInventoryChanges = (await handleStoreItemAcquisition(rewardType, inventory, rewardCount))
 | 
			
		||||
                .InventoryChanges;
 | 
			
		||||
            if (Object.keys(rewardInventoryChanges).length == 0) {
 | 
			
		||||
                logger.debug(`nightwave rank up reward did not seem to get added, giving 50 creds instead`);
 | 
			
		||||
                const nightwaveCredsItemType = manifest.titles![0].reward!.ItemType;
 | 
			
		||||
                rewardInventoryChanges.MiscItems = [{ ItemType: nightwaveCredsItemType, ItemCount: 50 }];
 | 
			
		||||
                addMiscItems(inventory, rewardInventoryChanges.MiscItems);
 | 
			
		||||
            }
 | 
			
		||||
            combineInventoryChanges(res.InventoryChanges, rewardInventoryChanges);
 | 
			
		||||
            syndicate.Initiated = true;
 | 
			
		||||
        } else {
 | 
			
		||||
            sacrifice = manifest.titles?.find(x => x.level == level)?.sacrifice;
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        if (syndicate.Title > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == syndicate.Title)) {
 | 
			
		||||
            syndicate.FreeFavorsEarned ??= [];
 | 
			
		||||
            if (!syndicate.FreeFavorsEarned.includes(syndicate.Title)) {
 | 
			
		||||
                syndicate.FreeFavorsEarned.push(syndicate.Title);
 | 
			
		||||
 | 
			
		||||
        if (sacrifice) {
 | 
			
		||||
            updateCurrency(inventory, sacrifice.credits, false, res.InventoryChanges);
 | 
			
		||||
 | 
			
		||||
            for (const item of sacrifice.items) {
 | 
			
		||||
                addMiscItem(inventory, item.ItemType, item.ItemCount * -1, res.InventoryChanges);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Quacks like a nightwave syndicate?
 | 
			
		||||
        if (manifest.dailyChallenges) {
 | 
			
		||||
            const title = manifest.titles!.find(x => x.level == level);
 | 
			
		||||
            if (title) {
 | 
			
		||||
                res.NewEpisodeReward = true;
 | 
			
		||||
                let rewardType: string;
 | 
			
		||||
                let rewardCount: number;
 | 
			
		||||
                if (title.storeItemReward) {
 | 
			
		||||
                    rewardType = title.storeItemReward;
 | 
			
		||||
                    rewardCount = 1;
 | 
			
		||||
                } else {
 | 
			
		||||
                    rewardType = toStoreItem(title.reward!.ItemType);
 | 
			
		||||
                    rewardCount = title.reward!.ItemCount;
 | 
			
		||||
                }
 | 
			
		||||
                const rewardInventoryChanges = (await handleStoreItemAcquisition(rewardType, inventory, rewardCount))
 | 
			
		||||
                    .InventoryChanges;
 | 
			
		||||
                if (Object.keys(rewardInventoryChanges).length == 0) {
 | 
			
		||||
                    logger.debug(`nightwave rank up reward did not seem to get added, giving 50 creds instead`);
 | 
			
		||||
                    const nightwaveCredsItemType = manifest.titles![0].reward!.ItemType;
 | 
			
		||||
                    addMiscItem(inventory, nightwaveCredsItemType, 50, rewardInventoryChanges);
 | 
			
		||||
                }
 | 
			
		||||
                combineInventoryChanges(res.InventoryChanges, rewardInventoryChanges);
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            if (level > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == level)) {
 | 
			
		||||
                syndicate.FreeFavorsEarned ??= [];
 | 
			
		||||
                if (!syndicate.FreeFavorsEarned.includes(level)) {
 | 
			
		||||
                    syndicate.FreeFavorsEarned.push(level);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Commit
 | 
			
		||||
    syndicate.Title = data.SacrificeLevel;
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
 | 
			
		||||
    response.json(res);
 | 
			
		||||
 | 
			
		||||
@ -35,6 +35,17 @@ const trainingResultController: RequestHandler = async (req, res): Promise<void>
 | 
			
		||||
        inventory.PlayerLevel += 1;
 | 
			
		||||
        inventory.TradesRemaining += 1;
 | 
			
		||||
 | 
			
		||||
        if (inventory.PlayerLevel == 2) {
 | 
			
		||||
            await createMessage(accountId, [
 | 
			
		||||
                {
 | 
			
		||||
                    sndr: "/Lotus/Language/Game/Maroo",
 | 
			
		||||
                    msg: "/Lotus/Language/Clan/MarooClanSearchDesc",
 | 
			
		||||
                    sub: "/Lotus/Language/Clan/MarooClanSearchTitle",
 | 
			
		||||
                    icon: "/Lotus/Interface/Icons/Npcs/Maroo.png"
 | 
			
		||||
                }
 | 
			
		||||
            ]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await createMessage(accountId, [
 | 
			
		||||
            {
 | 
			
		||||
                sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
			
		||||
 | 
			
		||||
@ -25,6 +25,7 @@ import allIncarnons from "@/static/fixed_responses/allIncarnonList.json";
 | 
			
		||||
interface ListedItem {
 | 
			
		||||
    uniqueName: string;
 | 
			
		||||
    name: string;
 | 
			
		||||
    subtype?: string;
 | 
			
		||||
    fusionLimit?: number;
 | 
			
		||||
    exalted?: string[];
 | 
			
		||||
    badReason?: "starter" | "frivolous" | "notraw";
 | 
			
		||||
@ -175,7 +176,8 @@ const getItemListsController: RequestHandler = (req, response) => {
 | 
			
		||||
        ) {
 | 
			
		||||
            res.miscitems.push({
 | 
			
		||||
                uniqueName: uniqueName,
 | 
			
		||||
                name: name
 | 
			
		||||
                name: name,
 | 
			
		||||
                subtype: "Resource"
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -193,7 +195,8 @@ const getItemListsController: RequestHandler = (req, response) => {
 | 
			
		||||
    for (const [uniqueName, item] of Object.entries(ExportGear)) {
 | 
			
		||||
        res.miscitems.push({
 | 
			
		||||
            uniqueName: uniqueName,
 | 
			
		||||
            name: getString(item.name, lang)
 | 
			
		||||
            name: getString(item.name, lang),
 | 
			
		||||
            subtype: "Gear"
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    const recipeNameTemplate = getString("/Lotus/Language/Items/BlueprintAndItem", lang);
 | 
			
		||||
 | 
			
		||||
@ -237,7 +237,7 @@ export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: TNemesisFacti
 | 
			
		||||
    return passcode;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const reqiuemMods: readonly string[] = [
 | 
			
		||||
const requiemMods: readonly string[] = [
 | 
			
		||||
    "/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod",
 | 
			
		||||
    "/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod",
 | 
			
		||||
    "/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod",
 | 
			
		||||
@ -263,29 +263,51 @@ export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: TNeme
 | 
			
		||||
    const passcode = getNemesisPasscode(nemesis);
 | 
			
		||||
    return nemesis.Faction == "FC_INFESTATION"
 | 
			
		||||
        ? passcode.map(i => antivirusMods[i])
 | 
			
		||||
        : passcode.map(i => reqiuemMods[i]);
 | 
			
		||||
        : passcode.map(i => requiemMods[i]);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const encodeNemesisGuess = (
 | 
			
		||||
    symbol1: number,
 | 
			
		||||
    result1: number,
 | 
			
		||||
    symbol2: number,
 | 
			
		||||
    result2: number,
 | 
			
		||||
    symbol3: number,
 | 
			
		||||
    result3: number
 | 
			
		||||
): number => {
 | 
			
		||||
// Symbols; 0-7 are the normal requiem mods.
 | 
			
		||||
export const GUESS_NONE = 8;
 | 
			
		||||
export const GUESS_WILDCARD = 9;
 | 
			
		||||
 | 
			
		||||
// Results; there are 3, 4, 5 as well which are more muted versions but unused afaik.
 | 
			
		||||
export const GUESS_NEUTRAL = 0;
 | 
			
		||||
export const GUESS_INCORRECT = 1;
 | 
			
		||||
export const GUESS_CORRECT = 2;
 | 
			
		||||
 | 
			
		||||
interface NemesisPositionGuess {
 | 
			
		||||
    symbol: number;
 | 
			
		||||
    result: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type NemesisGuess = [NemesisPositionGuess, NemesisPositionGuess, NemesisPositionGuess];
 | 
			
		||||
 | 
			
		||||
export const encodeNemesisGuess = (guess: NemesisGuess): number => {
 | 
			
		||||
    return (
 | 
			
		||||
        (symbol1 & 0xf) |
 | 
			
		||||
        ((result1 & 3) << 12) |
 | 
			
		||||
        ((symbol2 << 4) & 0xff) |
 | 
			
		||||
        ((result2 << 14) & 0xffff) |
 | 
			
		||||
        ((symbol3 & 0xf) << 8) |
 | 
			
		||||
        ((result3 & 3) << 16)
 | 
			
		||||
        (guess[0].symbol & 0xf) |
 | 
			
		||||
        ((guess[0].result & 3) << 12) |
 | 
			
		||||
        ((guess[1].symbol << 4) & 0xff) |
 | 
			
		||||
        ((guess[1].result << 14) & 0xffff) |
 | 
			
		||||
        ((guess[2].symbol & 0xf) << 8) |
 | 
			
		||||
        ((guess[2].result & 3) << 16)
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const decodeNemesisGuess = (val: number): number[] => {
 | 
			
		||||
    return [val & 0xf, (val >> 12) & 3, (val & 0xff) >> 4, (val & 0xffff) >> 14, (val >> 8) & 0xf, (val >> 16) & 3];
 | 
			
		||||
export const decodeNemesisGuess = (val: number): NemesisGuess => {
 | 
			
		||||
    return [
 | 
			
		||||
        {
 | 
			
		||||
            symbol: val & 0xf,
 | 
			
		||||
            result: (val >> 12) & 3
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            symbol: (val & 0xff) >> 4,
 | 
			
		||||
            result: (val & 0xffff) >> 14
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            symbol: (val >> 8) & 0xf,
 | 
			
		||||
            result: (val >> 16) & 3
 | 
			
		||||
        }
 | 
			
		||||
    ];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface IKnifeResponse {
 | 
			
		||||
 | 
			
		||||
@ -1097,7 +1097,8 @@ const pendingRecipeSchema = new Schema<IPendingRecipeDatabase>(
 | 
			
		||||
        LongGuns: { type: [EquipmentSchema], default: undefined },
 | 
			
		||||
        Pistols: { type: [EquipmentSchema], default: undefined },
 | 
			
		||||
        Melee: { type: [EquipmentSchema], default: undefined },
 | 
			
		||||
        SuitToUnbrand: { type: Schema.Types.ObjectId, default: undefined }
 | 
			
		||||
        SuitToUnbrand: { type: Schema.Types.ObjectId, default: undefined },
 | 
			
		||||
        KubrowPet: { type: Schema.Types.ObjectId, default: undefined }
 | 
			
		||||
    },
 | 
			
		||||
    { id: false }
 | 
			
		||||
);
 | 
			
		||||
@ -1115,6 +1116,7 @@ pendingRecipeSchema.set("toJSON", {
 | 
			
		||||
        delete returnedObject.Pistols;
 | 
			
		||||
        delete returnedObject.Melees;
 | 
			
		||||
        delete returnedObject.SuitToUnbrand;
 | 
			
		||||
        delete returnedObject.KubrowPet;
 | 
			
		||||
        (returnedObject as IPendingRecipeClient).CompletionDate = {
 | 
			
		||||
            $date: { $numberLong: (returnedObject as IPendingRecipeDatabase).CompletionDate.getTime().toString() }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@ import { addIgnoredUserController } from "@/src/controllers/api/addIgnoredUserCo
 | 
			
		||||
import { addPendingFriendController } from "@/src/controllers/api/addPendingFriendController";
 | 
			
		||||
import { addToAllianceController } from "@/src/controllers/api/addToAllianceController";
 | 
			
		||||
import { addToGuildController } from "@/src/controllers/api/addToGuildController";
 | 
			
		||||
import { adoptPetController } from "@/src/controllers/api/adoptPetController";
 | 
			
		||||
import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController";
 | 
			
		||||
import { archonFusionController } from "@/src/controllers/api/archonFusionController";
 | 
			
		||||
import { artifactsController } from "@/src/controllers/api/artifactsController";
 | 
			
		||||
@ -107,6 +108,7 @@ import { removeFriendGetController, removeFriendPostController } from "@/src/con
 | 
			
		||||
import { removeFromAllianceController } from "@/src/controllers/api/removeFromAllianceController";
 | 
			
		||||
import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController";
 | 
			
		||||
import { removeIgnoredUserController } from "@/src/controllers/api/removeIgnoredUserController";
 | 
			
		||||
import { renamePetController } from "@/src/controllers/api/renamePetController";
 | 
			
		||||
import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController";
 | 
			
		||||
import { retrievePetFromStasisController } from "@/src/controllers/api/retrievePetFromStasisController";
 | 
			
		||||
import { saveDialogueController } from "@/src/controllers/api/saveDialogueController";
 | 
			
		||||
@ -225,6 +227,7 @@ apiRouter.post("/addIgnoredUser.php", addIgnoredUserController);
 | 
			
		||||
apiRouter.post("/addPendingFriend.php", addPendingFriendController);
 | 
			
		||||
apiRouter.post("/addToAlliance.php", addToAllianceController);
 | 
			
		||||
apiRouter.post("/addToGuild.php", addToGuildController);
 | 
			
		||||
apiRouter.post("/adoptPet.php", adoptPetController);
 | 
			
		||||
apiRouter.post("/arcaneCommon.php", arcaneCommonController);
 | 
			
		||||
apiRouter.post("/archonFusion.php", archonFusionController);
 | 
			
		||||
apiRouter.post("/artifacts.php", artifactsController);
 | 
			
		||||
@ -294,6 +297,7 @@ apiRouter.post("/releasePet.php", releasePetController);
 | 
			
		||||
apiRouter.post("/removeFriend.php", removeFriendPostController);
 | 
			
		||||
apiRouter.post("/removeFromGuild.php", removeFromGuildController);
 | 
			
		||||
apiRouter.post("/removeIgnoredUser.php", removeIgnoredUserController);
 | 
			
		||||
apiRouter.post("/renamePet.php", renamePetController);
 | 
			
		||||
apiRouter.post("/rerollRandomMod.php", rerollRandomModController);
 | 
			
		||||
apiRouter.post("/retrievePetFromStasis.php", retrievePetFromStasisController);
 | 
			
		||||
apiRouter.post("/saveDialogue.php", saveDialogueController);
 | 
			
		||||
 | 
			
		||||
@ -61,7 +61,9 @@ interface IConfig {
 | 
			
		||||
        affinityBoost?: boolean;
 | 
			
		||||
        resourceBoost?: boolean;
 | 
			
		||||
        starDays?: boolean;
 | 
			
		||||
        lockTime?: number;
 | 
			
		||||
        eidolonOverride?: string;
 | 
			
		||||
        vallisOverride?: string;
 | 
			
		||||
        nightwaveOverride?: string;
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -27,9 +27,21 @@ fs.watchFile(configPath, () => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const validateConfig = (): void => {
 | 
			
		||||
    if (typeof config.administratorNames == "string") {
 | 
			
		||||
        logger.info(`Updating config.json to make administratorNames an array.`);
 | 
			
		||||
        config.administratorNames = [config.administratorNames];
 | 
			
		||||
    let modified = false;
 | 
			
		||||
    if (config.administratorNames) {
 | 
			
		||||
        if (!Array.isArray(config.administratorNames)) {
 | 
			
		||||
            config.administratorNames = [config.administratorNames];
 | 
			
		||||
            modified = true;
 | 
			
		||||
        }
 | 
			
		||||
        for (let i = 0; i != config.administratorNames.length; ++i) {
 | 
			
		||||
            if (typeof config.administratorNames[i] != "string") {
 | 
			
		||||
                config.administratorNames[i] = String(config.administratorNames[i]);
 | 
			
		||||
                modified = true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if (modified) {
 | 
			
		||||
        logger.info(`Updating config.json to fix some issues with it.`);
 | 
			
		||||
        void saveConfig();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -86,6 +86,7 @@ import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
 | 
			
		||||
import { getNightwaveSyndicateTag, getWorldState } from "./worldStateService";
 | 
			
		||||
import { generateNemesisProfile, INemesisProfile } from "../helpers/nemesisHelpers";
 | 
			
		||||
import { TAccountDocument } from "./loginService";
 | 
			
		||||
import { unixTimesInMs } from "../constants/timeConstants";
 | 
			
		||||
 | 
			
		||||
export const createInventory = async (
 | 
			
		||||
    accountOwnerId: Types.ObjectId,
 | 
			
		||||
@ -722,6 +723,10 @@ export const addItem = async (
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
 | 
			
		||||
                case "Boons":
 | 
			
		||||
                    // Can purchase /Lotus/Upgrades/Boons/DuviriVendorBoonItem from Acrithis, doesn't need to be added to inventory.
 | 
			
		||||
                    return {};
 | 
			
		||||
 | 
			
		||||
                case "Stickers":
 | 
			
		||||
                    {
 | 
			
		||||
                        const entry = inventory.RawUpgrades.find(x => x.ItemType == typeName);
 | 
			
		||||
@ -776,7 +781,9 @@ export const addItem = async (
 | 
			
		||||
                        typeName.substr(1).split("/")[3] == "CatbrowPet" ||
 | 
			
		||||
                        typeName.substr(1).split("/")[3] == "KubrowPet"
 | 
			
		||||
                    ) {
 | 
			
		||||
                        return addKubrowPet(inventory, typeName, undefined, premiumPurchase);
 | 
			
		||||
                        if (typeName != "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem") {
 | 
			
		||||
                            return addKubrowPet(inventory, typeName, undefined, premiumPurchase);
 | 
			
		||||
                        }
 | 
			
		||||
                    } else if (typeName.startsWith("/Lotus/Types/Game/CrewShip/CrewMember/")) {
 | 
			
		||||
                        if (!seed) {
 | 
			
		||||
                            throw new Error(`Expected crew member to have a seed`);
 | 
			
		||||
@ -791,6 +798,12 @@ export const addItem = async (
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
                case "Items": {
 | 
			
		||||
                    if (typeName.substr(1).split("/")[3] == "Emotes") {
 | 
			
		||||
                        return addCustomization(inventory, typeName);
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
                case "NeutralCreatures": {
 | 
			
		||||
                    if (inventory.Horses.length != 0) {
 | 
			
		||||
                        logger.warn("refusing to add Horse because account already has one");
 | 
			
		||||
@ -1015,12 +1028,13 @@ export const addSpaceSuit = (
 | 
			
		||||
export const addKubrowPet = (
 | 
			
		||||
    inventory: TInventoryDatabaseDocument,
 | 
			
		||||
    kubrowPetName: string,
 | 
			
		||||
    details: IKubrowPetDetailsDatabase | undefined,
 | 
			
		||||
    premiumPurchase: boolean,
 | 
			
		||||
    details?: IKubrowPetDetailsDatabase,
 | 
			
		||||
    premiumPurchase: boolean = false,
 | 
			
		||||
    inventoryChanges: IInventoryChanges = {}
 | 
			
		||||
): IInventoryChanges => {
 | 
			
		||||
    combineInventoryChanges(inventoryChanges, occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase));
 | 
			
		||||
 | 
			
		||||
    // TODO: When incubating, this should only be given when claiming the recipe.
 | 
			
		||||
    const kubrowPet = ExportSentinels[kubrowPetName] as ISentinel | undefined;
 | 
			
		||||
    const exalted = kubrowPet?.exalted ?? [];
 | 
			
		||||
    for (const specialItem of exalted) {
 | 
			
		||||
@ -1069,11 +1083,11 @@ export const addKubrowPet = (
 | 
			
		||||
 | 
			
		||||
        details = {
 | 
			
		||||
            Name: "",
 | 
			
		||||
            IsPuppy: false,
 | 
			
		||||
            IsPuppy: !premiumPurchase,
 | 
			
		||||
            HasCollar: true,
 | 
			
		||||
            PrintsRemaining: 2,
 | 
			
		||||
            Status: Status.StatusStasis,
 | 
			
		||||
            HatchDate: new Date(Math.trunc(Date.now() / 86400000) * 86400000),
 | 
			
		||||
            PrintsRemaining: 3,
 | 
			
		||||
            Status: premiumPurchase ? Status.StatusStasis : Status.StatusIncubating,
 | 
			
		||||
            HatchDate: premiumPurchase ? new Date() : new Date(Date.now() + 10 * unixTimesInMs.hour), // On live, this seems to be somewhat randomised so that the pet hatches 9~11 hours after start.
 | 
			
		||||
            IsMale: !!getRandomInt(0, 1),
 | 
			
		||||
            Size: getRandomInt(70, 100) / 100,
 | 
			
		||||
            DominantTraits: traits,
 | 
			
		||||
@ -1511,7 +1525,8 @@ export const applyClientEquipmentUpdates = (
 | 
			
		||||
    gearArray.forEach(({ ItemId, XP, InfestationDate }) => {
 | 
			
		||||
        const item = category.id(fromOid(ItemId));
 | 
			
		||||
        if (!item) {
 | 
			
		||||
            throw new Error(`No item with id ${fromOid(ItemId)} in ${categoryName}`);
 | 
			
		||||
            logger.warn(`Skipping unknown ${categoryName} item: id ${fromOid(ItemId)} not found`);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (XP) {
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,4 @@
 | 
			
		||||
import { IKeyChainRequest } from "@/src/types/requestTypes";
 | 
			
		||||
import { getIndexAfter } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import {
 | 
			
		||||
    dict_de,
 | 
			
		||||
    dict_en,
 | 
			
		||||
@ -53,20 +52,32 @@ export const getRecipeByResult = (resultType: string): IRecipe | undefined => {
 | 
			
		||||
    return Object.values(ExportRecipes).find(x => x.resultType == resultType);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getItemCategoryByUniqueName = (uniqueName: string): string => {
 | 
			
		||||
    //Lotus/Types/Items/MiscItems/PolymerBundle
 | 
			
		||||
 | 
			
		||||
    let splitWord = "Items/";
 | 
			
		||||
    if (!uniqueName.includes("/Items/")) {
 | 
			
		||||
        splitWord = "/Types/";
 | 
			
		||||
export const getItemCategoryByUniqueName = (uniqueName: string): string | undefined => {
 | 
			
		||||
    if (uniqueName in ExportCustoms) {
 | 
			
		||||
        return ExportCustoms[uniqueName].productCategory;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const index = getIndexAfter(uniqueName, splitWord);
 | 
			
		||||
    if (index === -1) {
 | 
			
		||||
        throw new Error(`error parsing item category ${uniqueName}`);
 | 
			
		||||
    if (uniqueName in ExportDrones) {
 | 
			
		||||
        return "Drones";
 | 
			
		||||
    }
 | 
			
		||||
    const category = uniqueName.substring(index).split("/")[0];
 | 
			
		||||
    return category;
 | 
			
		||||
    if (uniqueName in ExportKeys) {
 | 
			
		||||
        return "LevelKeys";
 | 
			
		||||
    }
 | 
			
		||||
    if (uniqueName in ExportGear) {
 | 
			
		||||
        return "Consumables";
 | 
			
		||||
    }
 | 
			
		||||
    if (uniqueName in ExportResources) {
 | 
			
		||||
        return ExportResources[uniqueName].productCategory;
 | 
			
		||||
    }
 | 
			
		||||
    if (uniqueName in ExportSentinels) {
 | 
			
		||||
        return ExportSentinels[uniqueName].productCategory;
 | 
			
		||||
    }
 | 
			
		||||
    if (uniqueName in ExportWarframes) {
 | 
			
		||||
        return ExportWarframes[uniqueName].productCategory;
 | 
			
		||||
    }
 | 
			
		||||
    if (uniqueName in ExportWeapons) {
 | 
			
		||||
        return ExportWeapons[uniqueName].productCategory;
 | 
			
		||||
    }
 | 
			
		||||
    return undefined;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getItemName = (uniqueName: string): string | undefined => {
 | 
			
		||||
@ -222,7 +233,7 @@ export const isStoreItem = (type: string): boolean => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const toStoreItem = (type: string): string => {
 | 
			
		||||
    if (type.startsWith("/Lotus/Types/StoreItems/Boosters/")) {
 | 
			
		||||
    if (type.startsWith("/Lotus/Types/Boosters/")) {
 | 
			
		||||
        const boosterEntry = Object.entries(ExportBoosters).find(arr => arr[1].typeName == type);
 | 
			
		||||
        if (boosterEntry) {
 | 
			
		||||
            return boosterEntry[0];
 | 
			
		||||
 | 
			
		||||
@ -326,8 +326,8 @@ export const addMissionInventoryUpdates = async (
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
            case "PlayerSkillGains": {
 | 
			
		||||
                inventory.PlayerSkills.LPP_SPACE += value.LPP_SPACE;
 | 
			
		||||
                inventory.PlayerSkills.LPP_DRIFTER += value.LPP_DRIFTER;
 | 
			
		||||
                inventory.PlayerSkills.LPP_SPACE += value.LPP_SPACE ?? 0;
 | 
			
		||||
                inventory.PlayerSkills.LPP_DRIFTER += value.LPP_DRIFTER ?? 0;
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
            case "CustomMarkers": {
 | 
			
		||||
@ -1182,14 +1182,12 @@ export const addMissionRewards = async (
 | 
			
		||||
            if (nodeIndex !== -1) inventory.Nemesis.InfNodes.splice(nodeIndex, 1);
 | 
			
		||||
 | 
			
		||||
            if (inventory.Nemesis.InfNodes.length <= 0) {
 | 
			
		||||
                const manifest = getNemesisManifest(inventory.Nemesis.manifest);
 | 
			
		||||
                if (inventory.Nemesis.Faction != "FC_INFESTATION") {
 | 
			
		||||
                    inventory.Nemesis.Rank = Math.min(inventory.Nemesis.Rank + 1, 4);
 | 
			
		||||
                    inventory.Nemesis.Rank = Math.min(inventory.Nemesis.Rank + 1, manifest.systemIndexes.length - 1);
 | 
			
		||||
                    inventoryChanges.Nemesis.Rank = inventory.Nemesis.Rank;
 | 
			
		||||
                }
 | 
			
		||||
                inventory.Nemesis.InfNodes = getInfNodes(
 | 
			
		||||
                    getNemesisManifest(inventory.Nemesis.manifest),
 | 
			
		||||
                    inventory.Nemesis.Rank
 | 
			
		||||
                );
 | 
			
		||||
                inventory.Nemesis.InfNodes = getInfNodes(manifest, inventory.Nemesis.Rank);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (inventory.Nemesis.Faction == "FC_INFESTATION") {
 | 
			
		||||
@ -1207,7 +1205,9 @@ export const addMissionRewards = async (
 | 
			
		||||
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
        const [jobType, unkIndex, hubNode, syndicateMissionId, locationTag] = rewardInfo.jobId.split("_");
 | 
			
		||||
        const syndicateMissions: ISyndicateMissionInfo[] = [];
 | 
			
		||||
        pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId));
 | 
			
		||||
        if (syndicateMissionId) {
 | 
			
		||||
            pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId));
 | 
			
		||||
        }
 | 
			
		||||
        const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId);
 | 
			
		||||
        if (syndicateEntry && syndicateEntry.Jobs) {
 | 
			
		||||
            let currentJob = syndicateEntry.Jobs[rewardInfo.JobTier!];
 | 
			
		||||
@ -1556,7 +1556,9 @@ function getRandomMissionDrops(
 | 
			
		||||
                let isEndlessJob = false;
 | 
			
		||||
                if (syndicateMissionId) {
 | 
			
		||||
                    const syndicateMissions: ISyndicateMissionInfo[] = [];
 | 
			
		||||
                    pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId));
 | 
			
		||||
                    if (syndicateMissionId) {
 | 
			
		||||
                        pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId));
 | 
			
		||||
                    }
 | 
			
		||||
                    const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId);
 | 
			
		||||
                    if (syndicateEntry && syndicateEntry.Jobs) {
 | 
			
		||||
                        let job = syndicateEntry.Jobs[RewardInfo.JobTier!];
 | 
			
		||||
 | 
			
		||||
@ -303,7 +303,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
 | 
			
		||||
        }
 | 
			
		||||
        const cycleStart = cycleOffset + cycleIndex * cycleDuration;
 | 
			
		||||
        for (const rawItem of offersToAdd) {
 | 
			
		||||
            const durationHoursRange = toRange(rawItem.durationHours);
 | 
			
		||||
            const durationHoursRange = toRange(rawItem.durationHours ?? cycleDuration);
 | 
			
		||||
            const expiry =
 | 
			
		||||
                cycleStart +
 | 
			
		||||
                rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour;
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,7 @@ import {
 | 
			
		||||
    IWorldState
 | 
			
		||||
} from "../types/worldStateTypes";
 | 
			
		||||
import { version_compare } from "../helpers/inventoryHelpers";
 | 
			
		||||
import { logger } from "../utils/logger";
 | 
			
		||||
 | 
			
		||||
const sortieBosses = [
 | 
			
		||||
    "SORTIE_BOSS_HYENA",
 | 
			
		||||
@ -166,8 +167,8 @@ const microplanetEndlessJobs = [
 | 
			
		||||
 | 
			
		||||
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 isBeforeNextExpectedWorldStateRefresh = (nowMs: number, thenMs: number): boolean => {
 | 
			
		||||
    return nowMs + 300_000 > thenMs;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getSortieTime = (day: number): number => {
 | 
			
		||||
@ -348,6 +349,7 @@ interface IRotatingSeasonChallengePools {
 | 
			
		||||
    daily: string[];
 | 
			
		||||
    weekly: string[];
 | 
			
		||||
    hardWeekly: string[];
 | 
			
		||||
    hasWeeklyPermanent: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const getSeasonChallengePools = (syndicateTag: string): IRotatingSeasonChallengePools => {
 | 
			
		||||
@ -359,7 +361,12 @@ const getSeasonChallengePools = (syndicateTag: string): IRotatingSeasonChallenge
 | 
			
		||||
                x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/") &&
 | 
			
		||||
                !x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")
 | 
			
		||||
        ),
 | 
			
		||||
        hardWeekly: syndicate.weeklyChallenges!.filter(x => x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/"))
 | 
			
		||||
        hardWeekly: syndicate.weeklyChallenges!.filter(x =>
 | 
			
		||||
            x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/")
 | 
			
		||||
        ),
 | 
			
		||||
        hasWeeklyPermanent: !!syndicate.weeklyChallenges!.find(x =>
 | 
			
		||||
            x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")
 | 
			
		||||
        )
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -416,26 +423,34 @@ const pushWeeklyActs = (
 | 
			
		||||
 | 
			
		||||
    activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 0));
 | 
			
		||||
    activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 1));
 | 
			
		||||
    activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 2));
 | 
			
		||||
    activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 3));
 | 
			
		||||
    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"
 | 
			
		||||
    });
 | 
			
		||||
    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"
 | 
			
		||||
    });
 | 
			
		||||
    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"
 | 
			
		||||
    });
 | 
			
		||||
    if (pools.hasWeeklyPermanent) {
 | 
			
		||||
        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"
 | 
			
		||||
        });
 | 
			
		||||
        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"
 | 
			
		||||
        });
 | 
			
		||||
        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"
 | 
			
		||||
        });
 | 
			
		||||
        activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 2));
 | 
			
		||||
        activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 3));
 | 
			
		||||
    } else {
 | 
			
		||||
        activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 2));
 | 
			
		||||
        activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 3));
 | 
			
		||||
        activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 4));
 | 
			
		||||
        activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 5));
 | 
			
		||||
        activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 6));
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[], bountyCycle: number): void => {
 | 
			
		||||
@ -924,15 +939,70 @@ const getCalendarSeason = (week: number): ICalendarSeason => {
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const doesTimeSatsifyConstraints = (timeSecs: number): boolean => {
 | 
			
		||||
    if (config.worldState?.eidolonOverride) {
 | 
			
		||||
        const eidolonEpoch = 1391992660;
 | 
			
		||||
        const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / 9000);
 | 
			
		||||
        const eidolonCycleStart = eidolonEpoch + eidolonCycle * 9000;
 | 
			
		||||
        const eidolonCycleEnd = eidolonCycleStart + 9000;
 | 
			
		||||
        const eidolonCycleNightStart = eidolonCycleEnd - 3000;
 | 
			
		||||
        if (config.worldState.eidolonOverride == "day") {
 | 
			
		||||
            if (
 | 
			
		||||
                //timeSecs < eidolonCycleStart ||
 | 
			
		||||
                isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, eidolonCycleNightStart * 1000)
 | 
			
		||||
            ) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            if (
 | 
			
		||||
                timeSecs < eidolonCycleNightStart ||
 | 
			
		||||
                isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, eidolonCycleEnd * 1000)
 | 
			
		||||
            ) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (config.worldState?.vallisOverride) {
 | 
			
		||||
        const vallisEpoch = 1541837628;
 | 
			
		||||
        const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600);
 | 
			
		||||
        const vallisCycleStart = vallisEpoch + vallisCycle * 1600;
 | 
			
		||||
        const vallisCycleEnd = vallisCycleStart + 1600;
 | 
			
		||||
        const vallisCycleColdStart = vallisCycleStart + 400;
 | 
			
		||||
        if (config.worldState.vallisOverride == "cold") {
 | 
			
		||||
            if (
 | 
			
		||||
                timeSecs < vallisCycleColdStart ||
 | 
			
		||||
                isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, vallisCycleEnd * 1000)
 | 
			
		||||
            ) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            if (
 | 
			
		||||
                //timeSecs < vallisCycleStart ||
 | 
			
		||||
                isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, vallisCycleColdStart * 1000)
 | 
			
		||||
            ) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
			
		||||
    const day = Math.trunc((Date.now() - EPOCH) / 86400000);
 | 
			
		||||
    let timeSecs = Math.round(Date.now() / 1000);
 | 
			
		||||
    while (!doesTimeSatsifyConstraints(timeSecs)) {
 | 
			
		||||
        timeSecs -= 60;
 | 
			
		||||
    }
 | 
			
		||||
    const timeMs = timeSecs * 1000;
 | 
			
		||||
    const day = Math.trunc((timeMs - EPOCH) / 86400000);
 | 
			
		||||
    const week = Math.trunc(day / 7);
 | 
			
		||||
    const weekStart = EPOCH + week * 604800000;
 | 
			
		||||
    const weekEnd = weekStart + 604800000;
 | 
			
		||||
 | 
			
		||||
    const worldState: IWorldState = {
 | 
			
		||||
        BuildLabel: typeof buildLabel == "string" ? buildLabel.split(" ").join("+") : buildConfig.buildLabel,
 | 
			
		||||
        Time: config.worldState?.lockTime || Math.round(Date.now() / 1000),
 | 
			
		||||
        Time: timeSecs,
 | 
			
		||||
        Goals: [],
 | 
			
		||||
        Alerts: [],
 | 
			
		||||
        Sorties: [],
 | 
			
		||||
@ -986,11 +1056,11 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
			
		||||
        worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 2));
 | 
			
		||||
        worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 1));
 | 
			
		||||
        worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 0));
 | 
			
		||||
        if (isBeforeNextExpectedWorldStateRefresh(EPOCH + (day + 1) * 86400000)) {
 | 
			
		||||
        if (isBeforeNextExpectedWorldStateRefresh(timeMs, EPOCH + (day + 1) * 86400000)) {
 | 
			
		||||
            worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day + 1));
 | 
			
		||||
        }
 | 
			
		||||
        pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week);
 | 
			
		||||
        if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) {
 | 
			
		||||
        if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
 | 
			
		||||
            pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week + 1);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -999,7 +1069,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
			
		||||
    worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = new SRng(week).randomInt(0, 0xff_ffff);
 | 
			
		||||
 | 
			
		||||
    // Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation
 | 
			
		||||
    let bountyCycle = Math.trunc(Date.now() / 9000000);
 | 
			
		||||
    let bountyCycle = Math.trunc(timeSecs / 9000);
 | 
			
		||||
    let bountyCycleEnd: number | undefined;
 | 
			
		||||
    do {
 | 
			
		||||
        const bountyCycleStart = bountyCycle * 9000000;
 | 
			
		||||
@ -1030,7 +1100,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        pushClassicBounties(worldState.SyndicateMissions, bountyCycle);
 | 
			
		||||
    } while (isBeforeNextExpectedWorldStateRefresh(bountyCycleEnd) && ++bountyCycle);
 | 
			
		||||
    } while (isBeforeNextExpectedWorldStateRefresh(timeMs, bountyCycleEnd) && ++bountyCycle);
 | 
			
		||||
 | 
			
		||||
    if (config.worldState?.creditBoost) {
 | 
			
		||||
        worldState.GlobalUpgrades.push({
 | 
			
		||||
@ -1073,15 +1143,15 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
			
		||||
    {
 | 
			
		||||
        const rollover = getSortieTime(day);
 | 
			
		||||
 | 
			
		||||
        if (Date.now() < rollover) {
 | 
			
		||||
        if (timeMs < rollover) {
 | 
			
		||||
            worldState.Sorties.push(getSortie(day - 1));
 | 
			
		||||
        }
 | 
			
		||||
        if (isBeforeNextExpectedWorldStateRefresh(rollover)) {
 | 
			
		||||
        if (isBeforeNextExpectedWorldStateRefresh(timeMs, rollover)) {
 | 
			
		||||
            worldState.Sorties.push(getSortie(day));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // The client does not seem to respect activation for classic syndicate missions, so only pushing current ones.
 | 
			
		||||
        const sdy = Date.now() >= rollover ? day : day - 1;
 | 
			
		||||
        const sdy = timeMs >= rollover ? day : day - 1;
 | 
			
		||||
        const rng = new SRng(sdy);
 | 
			
		||||
        pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48049", "ArbitersSyndicate");
 | 
			
		||||
        pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4804a", "CephalonSudaSyndicate");
 | 
			
		||||
@ -1093,7 +1163,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
			
		||||
 | 
			
		||||
    // Archon Hunt cycling every week
 | 
			
		||||
    worldState.LiteSorties.push(getLiteSortie(week));
 | 
			
		||||
    if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) {
 | 
			
		||||
    if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
 | 
			
		||||
        worldState.LiteSorties.push(getLiteSortie(week + 1));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1130,12 +1200,12 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
			
		||||
 | 
			
		||||
    // 1999 Calendar Season cycling every week + YearIteration every 4 weeks
 | 
			
		||||
    worldState.KnownCalendarSeasons.push(getCalendarSeason(week));
 | 
			
		||||
    if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) {
 | 
			
		||||
    if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
 | 
			
		||||
        worldState.KnownCalendarSeasons.push(getCalendarSeason(week + 1));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Sentient Anomaly cycling every 30 minutes
 | 
			
		||||
    const halfHour = Math.trunc(Date.now() / (unixTimesInMs.hour / 2));
 | 
			
		||||
    const halfHour = Math.trunc(timeMs / (unixTimesInMs.hour / 2));
 | 
			
		||||
    const tmp = {
 | 
			
		||||
        cavabegin: "1690761600",
 | 
			
		||||
        PurchasePlatformLockEnabled: true,
 | 
			
		||||
@ -1258,6 +1328,15 @@ export const isArchwingMission = (node: IRegion): boolean => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getNightwaveSyndicateTag = (buildLabel: string | undefined): string | undefined => {
 | 
			
		||||
    if (config.worldState?.nightwaveOverride) {
 | 
			
		||||
        if (config.worldState.nightwaveOverride in nightwaveTagToSeason) {
 | 
			
		||||
            return config.worldState.nightwaveOverride;
 | 
			
		||||
        }
 | 
			
		||||
        logger.warn(`ignoring invalid config value for worldState.nightwaveOverride`, {
 | 
			
		||||
            value: config.worldState.nightwaveOverride,
 | 
			
		||||
            valid_values: Object.keys(nightwaveTagToSeason)
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    if (!buildLabel || version_compare(buildLabel, "2025.05.20.10.18") >= 0) {
 | 
			
		||||
        return "RadioLegionIntermission13Syndicate";
 | 
			
		||||
    }
 | 
			
		||||
@ -1268,6 +1347,20 @@ export const getNightwaveSyndicateTag = (buildLabel: string | undefined): string
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const nightwaveTagToSeason: Record<string, number> = {
 | 
			
		||||
    RadioLegionIntermission13Syndicate: 15,
 | 
			
		||||
    RadioLegionIntermission12Syndicate: 14
 | 
			
		||||
    RadioLegionIntermission13Syndicate: 15, // Nora's Mix Vol. 9
 | 
			
		||||
    RadioLegionIntermission12Syndicate: 14, // Nora's Mix Vol. 8
 | 
			
		||||
    RadioLegionIntermission11Syndicate: 13, // Nora's Mix Vol. 7
 | 
			
		||||
    RadioLegionIntermission10Syndicate: 12, // Nora's Mix Vol. 6
 | 
			
		||||
    RadioLegionIntermission9Syndicate: 11, // Nora's Mix Vol. 5
 | 
			
		||||
    RadioLegionIntermission8Syndicate: 10, // Nora's Mix Vol. 4
 | 
			
		||||
    RadioLegionIntermission7Syndicate: 9, // Nora's Mix Vol. 3
 | 
			
		||||
    RadioLegionIntermission6Syndicate: 8, // Nora's Mix Vol. 2
 | 
			
		||||
    RadioLegionIntermission5Syndicate: 7, // Nora's Mix Vol. 1
 | 
			
		||||
    RadioLegionIntermission4Syndicate: 6, // Nora's Choice
 | 
			
		||||
    RadioLegionIntermission3Syndicate: 5, // Intermission III
 | 
			
		||||
    RadioLegion3Syndicate: 4, // Glassmaker
 | 
			
		||||
    RadioLegionIntermission2Syndicate: 3, // Intermission II
 | 
			
		||||
    RadioLegion2Syndicate: 2, // The Emissary
 | 
			
		||||
    RadioLegionIntermissionSyndicate: 1, // Intermission I
 | 
			
		||||
    RadioLegionSyndicate: 0 // The Wolf of Saturn Six
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -765,7 +765,8 @@ export interface IKubrowPetDetailsClient extends Omit<IKubrowPetDetailsDatabase,
 | 
			
		||||
 | 
			
		||||
export enum Status {
 | 
			
		||||
    StatusAvailable = "STATUS_AVAILABLE",
 | 
			
		||||
    StatusStasis = "STATUS_STASIS"
 | 
			
		||||
    StatusStasis = "STATUS_STASIS",
 | 
			
		||||
    StatusIncubating = "STATUS_INCUBATING"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ILastSortieRewardClient {
 | 
			
		||||
@ -929,10 +930,14 @@ export interface IPendingRecipeDatabase {
 | 
			
		||||
    Pistols?: IEquipmentDatabase[];
 | 
			
		||||
    Melee?: IEquipmentDatabase[];
 | 
			
		||||
    SuitToUnbrand?: Types.ObjectId;
 | 
			
		||||
    KubrowPet?: Types.ObjectId;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IPendingRecipeClient
 | 
			
		||||
    extends Omit<IPendingRecipeDatabase, "CompletionDate" | "LongGuns" | "Pistols" | "Melee" | "SuitToUnbrand"> {
 | 
			
		||||
    extends Omit<
 | 
			
		||||
        IPendingRecipeDatabase,
 | 
			
		||||
        "CompletionDate" | "LongGuns" | "Pistols" | "Melee" | "SuitToUnbrand" | "KubrowPet"
 | 
			
		||||
    > {
 | 
			
		||||
    CompletionDate: IMongoDate;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -96,7 +96,7 @@ export type IMissionInventoryUpdateRequest = {
 | 
			
		||||
    FpsSamples: number;
 | 
			
		||||
    EvolutionProgress?: IEvolutionProgress[];
 | 
			
		||||
    FocusXpIncreases?: number[];
 | 
			
		||||
    PlayerSkillGains: IPlayerSkills;
 | 
			
		||||
    PlayerSkillGains: Partial<IPlayerSkills>;
 | 
			
		||||
    CustomMarkers?: ICustomMarkers[];
 | 
			
		||||
    LoreFragmentScans?: ILoreFragmentScan[];
 | 
			
		||||
    VoidTearParticipantsCurrWave?: {
 | 
			
		||||
 | 
			
		||||
@ -312,7 +312,7 @@ function fetchItemList() {
 | 
			
		||||
                        document.getElementById("changeSyndicate").appendChild(option);
 | 
			
		||||
                    });
 | 
			
		||||
                } else {
 | 
			
		||||
                    const nameSet = new Set();
 | 
			
		||||
                    const nameToItems = {};
 | 
			
		||||
                    items.forEach(item => {
 | 
			
		||||
                        item.name = item.name.replace(/<.+>/g, "").trim();
 | 
			
		||||
                        if ("badReason" in item) {
 | 
			
		||||
@ -322,6 +322,11 @@ function fetchItemList() {
 | 
			
		||||
                                item.name += " " + loc("code_badItem");
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        nameToItems[item.name] ??= [];
 | 
			
		||||
                        nameToItems[item.name].push(item);
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    items.forEach(item => {
 | 
			
		||||
                        if (type == "ModularParts") {
 | 
			
		||||
                            const supportedModularParts = [
 | 
			
		||||
                                "LWPT_HB_DECK",
 | 
			
		||||
@ -360,15 +365,26 @@ function fetchItemList() {
 | 
			
		||||
                                    .appendChild(option);
 | 
			
		||||
                            }
 | 
			
		||||
                        } else if (item.badReason != "notraw") {
 | 
			
		||||
                            if (nameSet.has(item.name)) {
 | 
			
		||||
                                //console.log(`Not adding ${item.uniqueName} to datalist for ${type} due to duplicate display name: ${item.name}`);
 | 
			
		||||
                            } else {
 | 
			
		||||
                                nameSet.add(item.name);
 | 
			
		||||
 | 
			
		||||
                            const ambiguous = nameToItems[item.name].length > 1;
 | 
			
		||||
                            let canDisambiguate = true;
 | 
			
		||||
                            if (ambiguous) {
 | 
			
		||||
                                for (const i2 of nameToItems[item.name]) {
 | 
			
		||||
                                    if (!i2.subtype) {
 | 
			
		||||
                                        canDisambiguate = false;
 | 
			
		||||
                                        break;
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            if (!ambiguous || canDisambiguate || nameToItems[item.name][0] == item) {
 | 
			
		||||
                                const option = document.createElement("option");
 | 
			
		||||
                                option.setAttribute("data-key", item.uniqueName);
 | 
			
		||||
                                option.value = item.name;
 | 
			
		||||
                                if (ambiguous && canDisambiguate) {
 | 
			
		||||
                                    option.value += " (" + item.subtype + ")";
 | 
			
		||||
                                }
 | 
			
		||||
                                document.getElementById("datalist-" + type).appendChild(option);
 | 
			
		||||
                            } else {
 | 
			
		||||
                                //console.log(`Not adding ${item.uniqueName} to datalist for ${type} due to duplicate display name: ${item.name}`);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        itemMap[item.uniqueName] = { ...item, type };
 | 
			
		||||
@ -476,7 +492,7 @@ function updateInventory() {
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        let anyExaltedMissingXP = false;
 | 
			
		||||
                        if (item.XP >= maxXP && "exalted" in itemMap[item.ItemType]) {
 | 
			
		||||
                        if (item.XP >= maxXP && item.ItemType in itemMap && "exalted" in itemMap[item.ItemType]) {
 | 
			
		||||
                            for (const exaltedType of itemMap[item.ItemType].exalted) {
 | 
			
		||||
                                const exaltedItem = data.SpecialItems.find(x => x.ItemType == exaltedType);
 | 
			
		||||
                                if (exaltedItem) {
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
// Chinese translation by meb154
 | 
			
		||||
// Chinese translation by meb154 & bishan178
 | 
			
		||||
dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `注意:此处所做的更改只有在游戏同步仓库后才会生效。您可以通过访问星图来触发仓库更新。`,
 | 
			
		||||
    general_addButton: `添加`,
 | 
			
		||||
@ -18,7 +18,7 @@ dict = {
 | 
			
		||||
    code_kDrive: `K式悬浮板`,
 | 
			
		||||
    code_legendaryCore: `传奇核心`,
 | 
			
		||||
    code_traumaticPeculiar: `创伤怪奇`,
 | 
			
		||||
    code_starter: `|MOD| (有瑕疵的)`,
 | 
			
		||||
    code_starter: `|MOD|(有瑕疵的)`,
 | 
			
		||||
    code_badItem: `(Imposter)`,
 | 
			
		||||
    code_maxRank: `满级`,
 | 
			
		||||
    code_rename: `重命名`,
 | 
			
		||||
@ -28,7 +28,7 @@ dict = {
 | 
			
		||||
    code_succRankUp: `[UNTRANSLATED] Successfully ranked up.`,
 | 
			
		||||
    code_noEquipmentToRankUp: `没有可升级的装备。`,
 | 
			
		||||
    code_succAdded: `已成功添加。`,
 | 
			
		||||
    code_succRemoved: `[UNTRANSLATED] Successfully removed.`,
 | 
			
		||||
    code_succRemoved: `已成功移除。`,
 | 
			
		||||
    code_buffsNumber: `增益数量`,
 | 
			
		||||
    code_cursesNumber: `负面数量`,
 | 
			
		||||
    code_rerollsNumber: `洗卡次数`,
 | 
			
		||||
@ -39,27 +39,27 @@ dict = {
 | 
			
		||||
    code_count: `数量`,
 | 
			
		||||
    code_focusAllUnlocked: `所有专精学派均已解锁。`,
 | 
			
		||||
    code_focusUnlocked: `已解锁 |COUNT| 个新专精学派!需要游戏内仓库更新才能生效,您可以通过访问星图来触发仓库更新。`,
 | 
			
		||||
    code_addModsConfirm: `确定要向账户添加 |COUNT| 张MOD吗?`,
 | 
			
		||||
    code_addModsConfirm: `确定要向账户添加 |COUNT| 张MOD吗?`,
 | 
			
		||||
    code_succImport: `导入成功。`,
 | 
			
		||||
    code_gild: `镀金`,
 | 
			
		||||
    code_moa: `恐鸟`,
 | 
			
		||||
    code_zanuka: `猎犬`,
 | 
			
		||||
    code_stage: `[UNTRANSLATED] Stage`,
 | 
			
		||||
    code_complete: `[UNTRANSLATED] Complete`,
 | 
			
		||||
    code_nextStage: `[UNTRANSLATED] Next stage`,
 | 
			
		||||
    code_prevStage: `[UNTRANSLATED] Previous stage`,
 | 
			
		||||
    code_reset: `[UNTRANSLATED] Reset`,
 | 
			
		||||
    code_setInactive: `[UNTRANSLATED] Make the quest inactive`,
 | 
			
		||||
    code_completed: `[UNTRANSLATED] Completed`,
 | 
			
		||||
    code_active: `[UNTRANSLATED] Active`,
 | 
			
		||||
    code_stage: `阶段`,
 | 
			
		||||
    code_complete: `完成`,
 | 
			
		||||
    code_nextStage: `下一阶段`,
 | 
			
		||||
    code_prevStage: `上一阶段`,
 | 
			
		||||
    code_reset: `重置`,
 | 
			
		||||
    code_setInactive: `使任务处于未激活状态`,
 | 
			
		||||
    code_completed: `已完成`,
 | 
			
		||||
    code_active: `正在执行`,
 | 
			
		||||
    code_pigment: `颜料`,
 | 
			
		||||
    code_mature: `[UNTRANSLATED] Mature for combat`,
 | 
			
		||||
    code_unmature: `[UNTRANSLATED] Regress genetic aging`,
 | 
			
		||||
    code_mature: `成长并战备`,
 | 
			
		||||
    code_unmature: `逆转衰老基因`,
 | 
			
		||||
    login_description: `使用您的 OpenWF 账户凭证登录(与游戏内连接本服务器时使用的昵称相同)。`,
 | 
			
		||||
    login_emailLabel: `电子邮箱`,
 | 
			
		||||
    login_passwordLabel: `密码`,
 | 
			
		||||
    login_loginButton: `登录`,
 | 
			
		||||
    login_registerButton: `[UNTRANSLATED] Register`,
 | 
			
		||||
    login_registerButton: `注册账号`,
 | 
			
		||||
    navbar_logout: `退出登录`,
 | 
			
		||||
    navbar_renameAccount: `重命名账户`,
 | 
			
		||||
    navbar_deleteAccount: `删除账户`,
 | 
			
		||||
@ -82,22 +82,22 @@ dict = {
 | 
			
		||||
    inventory_operatorAmps: `增幅器`,
 | 
			
		||||
    inventory_hoverboards: `K式悬浮板`,
 | 
			
		||||
    inventory_moaPets: `恐鸟`,
 | 
			
		||||
    inventory_kubrowPets: `[UNTRANSLATED] Beasts`,
 | 
			
		||||
    inventory_evolutionProgress: `[UNTRANSLATED] Incarnon Evolution Progress`,
 | 
			
		||||
    inventory_kubrowPets: `动物同伴`,
 | 
			
		||||
    inventory_evolutionProgress: `灵化之源进度`,
 | 
			
		||||
    inventory_bulkAddSuits: `添加缺失战甲`,
 | 
			
		||||
    inventory_bulkAddWeapons: `添加缺失武器`,
 | 
			
		||||
    inventory_bulkAddSpaceSuits: `添加缺失Archwing`,
 | 
			
		||||
    inventory_bulkAddSpaceWeapons: `添加缺失Archwing武器`,
 | 
			
		||||
    inventory_bulkAddSentinels: `添加缺失守护`,
 | 
			
		||||
    inventory_bulkAddSentinelWeapons: `添加缺失守护武器`,
 | 
			
		||||
    inventory_bulkAddEvolutionProgress: `[UNTRANSLATED] Add Missing Incarnon Evolution Progress`,
 | 
			
		||||
    inventory_bulkAddEvolutionProgress: `添加缺失的灵化之源`,
 | 
			
		||||
    inventory_bulkRankUpSuits: `所有战甲升满级`,
 | 
			
		||||
    inventory_bulkRankUpWeapons: `所有武器升满级`,
 | 
			
		||||
    inventory_bulkRankUpSpaceSuits: `所有Archwing升满级`,
 | 
			
		||||
    inventory_bulkRankUpSpaceWeapons: `所有Archwing武器升满级`,
 | 
			
		||||
    inventory_bulkRankUpSentinels: `所有守护升满级`,
 | 
			
		||||
    inventory_bulkRankUpSentinelWeapons: `所有守护武器升满级`,
 | 
			
		||||
    inventory_bulkRankUpEvolutionProgress: `[UNTRANSLATED] Max Rank All Incarnon Evolution Progress`,
 | 
			
		||||
    inventory_bulkRankUpEvolutionProgress: `所有灵化之源最大等级`,
 | 
			
		||||
 | 
			
		||||
    quests_list: `任务`,
 | 
			
		||||
    quests_completeAll: `完成所有任务`,
 | 
			
		||||
@ -111,15 +111,15 @@ dict = {
 | 
			
		||||
    currency_owned: `当前拥有 |COUNT|。`,
 | 
			
		||||
    powersuit_archonShardsLabel: `执刑官源力石槽位`,
 | 
			
		||||
    powersuit_archonShardsDescription: `您可以使用这些无限插槽应用各种强化效果`,
 | 
			
		||||
    powersuit_archonShardsDescription2: `[UNTRANSLATED] Note that each archon shard takes some time to be applied when loading in.`,
 | 
			
		||||
    powersuit_archonShardsDescription2: `请注意, 在加载时, 每个执政官源力石都需要一定的时间来生效。`,
 | 
			
		||||
    mods_addRiven: `添加裂罅MOD`,
 | 
			
		||||
    mods_fingerprint: `印记`,
 | 
			
		||||
    mods_fingerprintHelp: `需要印记相关的帮助?`,
 | 
			
		||||
    mods_rivens: `裂罅MOD`,
 | 
			
		||||
    mods_mods: `Mods`,
 | 
			
		||||
    mods_addMissingUnrankedMods: `[UNTRANSLATED] Add Missing Unranked Mods`,
 | 
			
		||||
    mods_removeUnranked: `[UNTRANSLATED] Remove Unranked Mods`,
 | 
			
		||||
    mods_addMissingMaxRankMods: `[UNTRANSLATED] Add Missing Max Rank Mods`,
 | 
			
		||||
    mods_addMissingUnrankedMods: `添加所有缺失的Mods`,
 | 
			
		||||
    mods_removeUnranked: `删除所有未升级的Mods`,
 | 
			
		||||
    mods_addMissingMaxRankMods: `添加所有缺失的满级Mods`,
 | 
			
		||||
    cheats_administratorRequirement: `您必须是管理员才能使用此功能。要成为管理员,请将 <code>|DISPLAYNAME|</code> 添加到 config.json 的 <code>administratorNames</code> 中。`,
 | 
			
		||||
    cheats_server: `服务器`,
 | 
			
		||||
    cheats_skipTutorial: `跳过教程`,
 | 
			
		||||
@ -131,43 +131,43 @@ dict = {
 | 
			
		||||
    cheats_infiniteEndo: `无限内融核心`,
 | 
			
		||||
    cheats_infiniteRegalAya: `无限御品阿耶`,
 | 
			
		||||
    cheats_infiniteHelminthMaterials: `无限Helminth材料`,
 | 
			
		||||
    cheats_claimingBlueprintRefundsIngredients: `[UNTRANSLATED] Claiming Blueprint Refunds Ingredients`,
 | 
			
		||||
    cheats_dontSubtractVoidTraces: `[UNTRANSLATED] Don't Subtract Void Traces`,
 | 
			
		||||
    cheats_dontSubtractConsumables: `[UNTRANSLATED] Don't Subtract Consumables`,
 | 
			
		||||
    cheats_claimingBlueprintRefundsIngredients: `取消蓝图制造时返还材料`,
 | 
			
		||||
    cheats_dontSubtractVoidTraces: `虚空光体无消耗`,
 | 
			
		||||
    cheats_dontSubtractConsumables: `消耗物品使用时无损耗`,
 | 
			
		||||
    cheats_unlockAllShipFeatures: `解锁所有飞船功能`,
 | 
			
		||||
    cheats_unlockAllShipDecorations: `解锁所有飞船装饰`,
 | 
			
		||||
    cheats_unlockAllFlavourItems: `解锁所有<abbr title=\"动画组合、图标、调色板等\">装饰物品</abbr>`,
 | 
			
		||||
    cheats_unlockAllSkins: `解锁所有外观`,
 | 
			
		||||
    cheats_unlockAllCapturaScenes: `解锁所有Captura场景`,
 | 
			
		||||
    cheats_unlockAllDecoRecipes: `[UNTRANSLATED] Unlock All Dojo Deco Recipes`,
 | 
			
		||||
    cheats_unlockAllDecoRecipes: `解锁所有道场配方`,
 | 
			
		||||
    cheats_universalPolarityEverywhere: `全局万用极性`,
 | 
			
		||||
    cheats_unlockDoubleCapacityPotatoesEverywhere: `全物品自带Orokin反应堆`,
 | 
			
		||||
    cheats_unlockExilusEverywhere: `全物品自带适配器`,
 | 
			
		||||
    cheats_unlockArcanesEverywhere: `全物品自带赋能适配器`,
 | 
			
		||||
    cheats_noDailyStandingLimits: `无每日声望限制`,
 | 
			
		||||
    cheats_noDailyFocusLimit: `[UNTRANSLATED] No Daily Focus Limits`,
 | 
			
		||||
    cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`,
 | 
			
		||||
    cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`,
 | 
			
		||||
    cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`,
 | 
			
		||||
    cheats_noDeathMarks: `[UNTRANSLATED] No Death Marks`,
 | 
			
		||||
    cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`,
 | 
			
		||||
    cheats_syndicateMissionsRepeatable: `[UNTRANSLATED] Syndicate Missions Repeatable`,
 | 
			
		||||
    cheats_instantFinishRivenChallenge: `[UNTRANSLATED] Instant Finish Riven Challenge`,
 | 
			
		||||
    cheats_noDailyFocusLimit: `指挥官专精无每日获取上限`,
 | 
			
		||||
    cheats_noArgonCrystalDecay: `氩结晶无衰变`,
 | 
			
		||||
    cheats_noMasteryRankUpCooldown: `段位考核无冷却时间`,
 | 
			
		||||
    cheats_noVendorPurchaseLimits: `商城或商人无购买限制`,
 | 
			
		||||
    cheats_noDeathMarks: `无死亡标记(不会被 Stalker/Grustrag 三霸/Zanuka 猎人等标记)`,
 | 
			
		||||
    cheats_noKimCooldowns: `无 KIM 冷却时间`,
 | 
			
		||||
    cheats_syndicateMissionsRepeatable: `集团任务可重复`,
 | 
			
		||||
    cheats_instantFinishRivenChallenge: `立即完成裂罅挑战`,
 | 
			
		||||
    cheats_instantResourceExtractorDrones: `即时资源采集无人机`,
 | 
			
		||||
    cheats_noResourceExtractorDronesDamage: `[UNTRANSLATED] No Resource Extractor Drones Damage`,
 | 
			
		||||
    cheats_skipClanKeyCrafting: `[UNTRANSLATED] Skip Clan Key Crafting`,
 | 
			
		||||
    cheats_noResourceExtractorDronesDamage: `资源提取器不会损毁`,
 | 
			
		||||
    cheats_skipClanKeyCrafting: `跳过氏族钥匙制作, 进入道场无需氏族钥匙`,
 | 
			
		||||
    cheats_noDojoRoomBuildStage: `无视道场房间建造阶段`,
 | 
			
		||||
    cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`,
 | 
			
		||||
    cheats_noDojoDecoBuildStage: `道场装饰建造立即完成`,
 | 
			
		||||
    cheats_fastDojoRoomDestruction: `快速拆除道场房间`,
 | 
			
		||||
    cheats_noDojoResearchCosts: `无视道场研究消耗`,
 | 
			
		||||
    cheats_noDojoResearchTime: `无视道场研究时间`,
 | 
			
		||||
    cheats_fastClanAscension: `快速升级氏族`,
 | 
			
		||||
    cheats_spoofMasteryRank: `伪造精通段位(-1为禁用)`,
 | 
			
		||||
    cheats_spoofMasteryRank: `伪造精通段位(-1为禁用)`,
 | 
			
		||||
    cheats_saveSettings: `保存设置`,
 | 
			
		||||
    cheats_account: `账户`,
 | 
			
		||||
    cheats_unlockAllFocusSchools: `解锁所有专精学派`,
 | 
			
		||||
    cheats_helminthUnlockAll: `完全升级Helminth`,
 | 
			
		||||
    cheats_intrinsicsUnlockAll: `[UNTRANSLATED] Max Rank All Intrinsics`,
 | 
			
		||||
    cheats_intrinsicsUnlockAll: `所有内源之力最大等级`,
 | 
			
		||||
    cheats_changeSupportedSyndicate: `支持的集团`,
 | 
			
		||||
    cheats_changeButton: `更改`,
 | 
			
		||||
    cheats_none: `无`,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user