chore(webui): improving inconsistent, long string #2344
							
								
								
									
										2
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							@ -10,6 +10,8 @@ jobs:
 | 
				
			|||||||
              uses: actions/checkout@v4.1.2
 | 
					              uses: actions/checkout@v4.1.2
 | 
				
			||||||
            - name: Setup Node.js environment
 | 
					            - name: Setup Node.js environment
 | 
				
			||||||
              uses: actions/setup-node@v4.0.2
 | 
					              uses: actions/setup-node@v4.0.2
 | 
				
			||||||
 | 
					              with:
 | 
				
			||||||
 | 
					                  node-version: ">=20.6.0"
 | 
				
			||||||
            - run: npm ci
 | 
					            - run: npm ci
 | 
				
			||||||
            - run: cp config.json.example config.json
 | 
					            - run: cp config.json.example config.json
 | 
				
			||||||
            - run: npm run verify
 | 
					            - run: npm run verify
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										17
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								README.md
									
									
									
									
									
								
							@ -15,3 +15,20 @@ 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`.
 | 
					- `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`.
 | 
				
			||||||
- `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`.
 | 
					- `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`.
 | 
				
			||||||
- `worldState.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE.
 | 
					- `worldState.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE.
 | 
				
			||||||
 | 
					- `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...
 | 
					echo Updating SpaceNinjaServer...
 | 
				
			||||||
git fetch --prune
 | 
					git fetch --prune
 | 
				
			||||||
git stash
 | 
					git stash
 | 
				
			||||||
git reset --hard origin/main
 | 
					git checkout -f origin/main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if exist static\data\0\ (
 | 
					if exist static\data\0\ (
 | 
				
			||||||
	echo Updating stripped assets...
 | 
						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,7 @@
 | 
				
			|||||||
    "affinityBoost": false,
 | 
					    "affinityBoost": false,
 | 
				
			||||||
    "resourceBoost": false,
 | 
					    "resourceBoost": false,
 | 
				
			||||||
    "starDays": true,
 | 
					    "starDays": true,
 | 
				
			||||||
    "lockTime": 0
 | 
					    "lockTime": 0,
 | 
				
			||||||
 | 
					    "nightwaveOverride": ""
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										181
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										181
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -18,24 +18,20 @@
 | 
				
			|||||||
        "morgan": "^1.10.0",
 | 
					        "morgan": "^1.10.0",
 | 
				
			||||||
        "ncp": "^2.0.0",
 | 
					        "ncp": "^2.0.0",
 | 
				
			||||||
        "typescript": "^5.5",
 | 
					        "typescript": "^5.5",
 | 
				
			||||||
        "warframe-public-export-plus": "^0.5.62",
 | 
					        "warframe-public-export-plus": "^0.5.65",
 | 
				
			||||||
        "warframe-riven-info": "^0.1.2",
 | 
					        "warframe-riven-info": "^0.1.2",
 | 
				
			||||||
        "winston": "^3.17.0",
 | 
					        "winston": "^3.17.0",
 | 
				
			||||||
        "winston-daily-rotate-file": "^5.0.0"
 | 
					        "winston-daily-rotate-file": "^5.0.0"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "devDependencies": {
 | 
					      "devDependencies": {
 | 
				
			||||||
        "@rxliuli/tsgo": "^2025.3.31",
 | 
					 | 
				
			||||||
        "@typescript-eslint/eslint-plugin": "^8.28.0",
 | 
					        "@typescript-eslint/eslint-plugin": "^8.28.0",
 | 
				
			||||||
        "@typescript-eslint/parser": "^8.28.0",
 | 
					        "@typescript-eslint/parser": "^8.28.0",
 | 
				
			||||||
 | 
					        "@typescript/native-preview": "^7.0.0-dev.20250523.1",
 | 
				
			||||||
        "eslint": "^8",
 | 
					        "eslint": "^8",
 | 
				
			||||||
        "eslint-plugin-prettier": "^5.2.5",
 | 
					        "eslint-plugin-prettier": "^5.2.5",
 | 
				
			||||||
        "prettier": "^3.5.3",
 | 
					        "prettier": "^3.5.3",
 | 
				
			||||||
        "ts-node-dev": "^2.0.0",
 | 
					        "ts-node-dev": "^2.0.0",
 | 
				
			||||||
        "tsconfig-paths": "^4.2.0"
 | 
					        "tsconfig-paths": "^4.2.0"
 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">=18.15.0",
 | 
					 | 
				
			||||||
        "npm": ">=9.5.0"
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@colors/colors": {
 | 
					    "node_modules/@colors/colors": {
 | 
				
			||||||
@ -308,32 +304,6 @@
 | 
				
			|||||||
        "url": "https://opencollective.com/pkgr"
 | 
					        "url": "https://opencollective.com/pkgr"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@rxliuli/tsgo": {
 | 
					 | 
				
			||||||
      "version": "2025.5.8",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@rxliuli/tsgo/-/tsgo-2025.5.8.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-P3/qxcUgiWz6nSJslJ5mMeAEqacK8LQSoOhdvHxI1/d0Xqxt2Qp6/nmhWuOlyqnCyAaIoXgoiUshiXWBGr2jaw==",
 | 
					 | 
				
			||||||
      "cpu": [
 | 
					 | 
				
			||||||
        "x64",
 | 
					 | 
				
			||||||
        "ia32",
 | 
					 | 
				
			||||||
        "arm",
 | 
					 | 
				
			||||||
        "arm64"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "dev": true,
 | 
					 | 
				
			||||||
      "hasInstallScript": true,
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "os": [
 | 
					 | 
				
			||||||
        "darwin",
 | 
					 | 
				
			||||||
        "linux",
 | 
					 | 
				
			||||||
        "win32",
 | 
					 | 
				
			||||||
        "freebsd"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "bin": {
 | 
					 | 
				
			||||||
        "tsgo": "bin.js"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">=12"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/@tsconfig/node10": {
 | 
					    "node_modules/@tsconfig/node10": {
 | 
				
			||||||
      "version": "1.0.11",
 | 
					      "version": "1.0.11",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
 | 
				
			||||||
@ -695,6 +665,147 @@
 | 
				
			|||||||
        "url": "https://opencollective.com/eslint"
 | 
					        "url": "https://opencollective.com/eslint"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@typescript/native-preview": {
 | 
				
			||||||
 | 
					      "version": "7.0.0-dev.20250523.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20250523.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-CgdgP/gmyaMThY7Fho19nDaTVryn9QV/zD/6w1KfDCn3M4Rq4WvkSc7Ob1ohc4V1XjCSIzg6Ul+HbLEc7xvV4Q==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "Apache-2.0",
 | 
				
			||||||
 | 
					      "bin": {
 | 
				
			||||||
 | 
					        "tsgo": "bin/tsgo.js"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=20.6.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "optionalDependencies": {
 | 
				
			||||||
 | 
					        "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20250523.1",
 | 
				
			||||||
 | 
					        "@typescript/native-preview-darwin-x64": "7.0.0-dev.20250523.1",
 | 
				
			||||||
 | 
					        "@typescript/native-preview-linux-arm": "7.0.0-dev.20250523.1",
 | 
				
			||||||
 | 
					        "@typescript/native-preview-linux-arm64": "7.0.0-dev.20250523.1",
 | 
				
			||||||
 | 
					        "@typescript/native-preview-linux-x64": "7.0.0-dev.20250523.1",
 | 
				
			||||||
 | 
					        "@typescript/native-preview-win32-arm64": "7.0.0-dev.20250523.1",
 | 
				
			||||||
 | 
					        "@typescript/native-preview-win32-x64": "7.0.0-dev.20250523.1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@typescript/native-preview-darwin-arm64": {
 | 
				
			||||||
 | 
					      "version": "7.0.0-dev.20250523.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20250523.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-oWJMPD+lfH9/dvHhPSZdTv43lfyZGrn7crytefhkiQPSwP0MIUCpnDkofGP/ML1nv0xx0pwWhH+Ein88NW3LuA==",
 | 
				
			||||||
 | 
					      "cpu": [
 | 
				
			||||||
 | 
					        "arm64"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "Apache-2.0",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "os": [
 | 
				
			||||||
 | 
					        "darwin"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=20.6.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@typescript/native-preview-darwin-x64": {
 | 
				
			||||||
 | 
					      "version": "7.0.0-dev.20250523.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20250523.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-Yk8bJEsYsRKgRqYlwPvh7DPdgBMC/oPN60X0LWeuMLci65+4kyqF8Cv6K/W3ABc005cB4tYn4iR+9T6zipvrKw==",
 | 
				
			||||||
 | 
					      "cpu": [
 | 
				
			||||||
 | 
					        "x64"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "Apache-2.0",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "os": [
 | 
				
			||||||
 | 
					        "darwin"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=20.6.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@typescript/native-preview-linux-arm": {
 | 
				
			||||||
 | 
					      "version": "7.0.0-dev.20250523.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20250523.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-B+8CRIv6ebL8gzAagnJP8wml3baFV2FtFWuXYl6jlAcLGoQOh/yGdcAueZoJjJKNod4gAOl8OJoTicuC0BVIxw==",
 | 
				
			||||||
 | 
					      "cpu": [
 | 
				
			||||||
 | 
					        "arm"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "Apache-2.0",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "os": [
 | 
				
			||||||
 | 
					        "linux"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=20.6.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@typescript/native-preview-linux-arm64": {
 | 
				
			||||||
 | 
					      "version": "7.0.0-dev.20250523.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20250523.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-IErNI08z9qE6mHaJaT6tM7il8j21ryH3DNVyFP4yz5FTKnkXFj1Kb4NcI41Q8w226LTQgBR8kNErVlbUWr7ywA==",
 | 
				
			||||||
 | 
					      "cpu": [
 | 
				
			||||||
 | 
					        "arm64"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "Apache-2.0",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "os": [
 | 
				
			||||||
 | 
					        "linux"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=20.6.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@typescript/native-preview-linux-x64": {
 | 
				
			||||||
 | 
					      "version": "7.0.0-dev.20250523.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20250523.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-TCZtknsLUgPRaEfX9CvBZNgrHhMRZPYYZgF1Aasdv0PONv9mB8w0Xforgxoo4UFjdF5ZzOu2icgc7sKJJeu5vw==",
 | 
				
			||||||
 | 
					      "cpu": [
 | 
				
			||||||
 | 
					        "x64"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "Apache-2.0",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "os": [
 | 
				
			||||||
 | 
					        "linux"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=20.6.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@typescript/native-preview-win32-arm64": {
 | 
				
			||||||
 | 
					      "version": "7.0.0-dev.20250523.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20250523.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-bulwrkLEkoY4Jqeuvfz24RiVOiZZ7Rr9TblFqZAgZFZOnyXuhjM1jE8F1hnJFC5AghJe2HdLD3EKfabqlffrIw==",
 | 
				
			||||||
 | 
					      "cpu": [
 | 
				
			||||||
 | 
					        "arm64"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "Apache-2.0",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "os": [
 | 
				
			||||||
 | 
					        "win32"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=20.6.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@typescript/native-preview-win32-x64": {
 | 
				
			||||||
 | 
					      "version": "7.0.0-dev.20250523.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20250523.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-ztzfO0oF/rj8xO5y3SyAcigmgvgczrqobCugEWFqiYumteWZPN2MYWcNYk2k8Y5LAgg1fN1xHIg8RRSPoo6XUg==",
 | 
				
			||||||
 | 
					      "cpu": [
 | 
				
			||||||
 | 
					        "x64"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "Apache-2.0",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "os": [
 | 
				
			||||||
 | 
					        "win32"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=20.6.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@ungap/structured-clone": {
 | 
					    "node_modules/@ungap/structured-clone": {
 | 
				
			||||||
      "version": "1.3.0",
 | 
					      "version": "1.3.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
 | 
				
			||||||
@ -3703,9 +3814,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/warframe-public-export-plus": {
 | 
					    "node_modules/warframe-public-export-plus": {
 | 
				
			||||||
      "version": "0.5.62",
 | 
					      "version": "0.5.65",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.62.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.65.tgz",
 | 
				
			||||||
      "integrity": "sha512-D8ZzjkU9rrK/59VqCfpMoV31HVmwHZV1dNZxPO85AOlcjg/G81Fu3kgITQTaw9sdNagLPLQnFaiXY58pxxRwgA=="
 | 
					      "integrity": "sha512-y/HN61lE5g8gx0Giutdl/jzQnQmw1u2uI0BiwKVW341nf42sKWQPsKsCVTL5x9MIDYyRCbFsMU+PazKC7byMdg=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/warframe-riven-info": {
 | 
					    "node_modules/warframe-riven-info": {
 | 
				
			||||||
      "version": "0.1.2",
 | 
					      "version": "0.1.2",
 | 
				
			||||||
 | 
				
			|||||||
@ -25,23 +25,19 @@
 | 
				
			|||||||
    "morgan": "^1.10.0",
 | 
					    "morgan": "^1.10.0",
 | 
				
			||||||
    "ncp": "^2.0.0",
 | 
					    "ncp": "^2.0.0",
 | 
				
			||||||
    "typescript": "^5.5",
 | 
					    "typescript": "^5.5",
 | 
				
			||||||
    "warframe-public-export-plus": "^0.5.62",
 | 
					    "warframe-public-export-plus": "^0.5.65",
 | 
				
			||||||
    "warframe-riven-info": "^0.1.2",
 | 
					    "warframe-riven-info": "^0.1.2",
 | 
				
			||||||
    "winston": "^3.17.0",
 | 
					    "winston": "^3.17.0",
 | 
				
			||||||
    "winston-daily-rotate-file": "^5.0.0"
 | 
					    "winston-daily-rotate-file": "^5.0.0"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@rxliuli/tsgo": "^2025.3.31",
 | 
					 | 
				
			||||||
    "@typescript-eslint/eslint-plugin": "^8.28.0",
 | 
					    "@typescript-eslint/eslint-plugin": "^8.28.0",
 | 
				
			||||||
    "@typescript-eslint/parser": "^8.28.0",
 | 
					    "@typescript-eslint/parser": "^8.28.0",
 | 
				
			||||||
 | 
					    "@typescript/native-preview": "^7.0.0-dev.20250523.1",
 | 
				
			||||||
    "eslint": "^8",
 | 
					    "eslint": "^8",
 | 
				
			||||||
    "eslint-plugin-prettier": "^5.2.5",
 | 
					    "eslint-plugin-prettier": "^5.2.5",
 | 
				
			||||||
    "prettier": "^3.5.3",
 | 
					    "prettier": "^3.5.3",
 | 
				
			||||||
    "ts-node-dev": "^2.0.0",
 | 
					    "ts-node-dev": "^2.0.0",
 | 
				
			||||||
    "tsconfig-paths": "^4.2.0"
 | 
					    "tsconfig-paths": "^4.2.0"
 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "engines": {
 | 
					 | 
				
			||||||
    "node": ">=18.15.0",
 | 
					 | 
				
			||||||
    "npm": ">=9.5.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";
 | 
					} from "@/src/services/inventoryService";
 | 
				
			||||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
					import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
				
			||||||
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
 | 
					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 { toOid2 } from "@/src/helpers/inventoryHelpers";
 | 
				
			||||||
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
 | 
					import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
 | 
				
			||||||
import { IRecipe } from "warframe-public-export-plus";
 | 
					import { IRecipe } from "warframe-public-export-plus";
 | 
				
			||||||
@ -105,7 +105,21 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
 | 
				
			|||||||
                ...updateCurrency(inventory, cost, true)
 | 
					                ...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.StatusIncubating;
 | 
				
			||||||
 | 
					        } else if (recipe.secretIngredientAction != "SIA_UNBRAND") {
 | 
				
			||||||
            InventoryChanges = {
 | 
					            InventoryChanges = {
 | 
				
			||||||
                ...InventoryChanges,
 | 
					                ...InventoryChanges,
 | 
				
			||||||
                ...(await addItem(
 | 
					                ...(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 refundRecipeIngredients(inventory, InventoryChanges, recipe, pendingRecipe);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        await inventory.save();
 | 
					        await inventory.save();
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
import { RequestHandler } from "express";
 | 
					import { RequestHandler } from "express";
 | 
				
			||||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
					import { getAccountForRequest } from "@/src/services/loginService";
 | 
				
			||||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
					import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
				
			||||||
import { getInventory, addMiscItems, updateCurrency, addRecipes, freeUpSlot } from "@/src/services/inventoryService";
 | 
					import { getInventory, addMiscItems, updateCurrency, addRecipes, freeUpSlot } from "@/src/services/inventoryService";
 | 
				
			||||||
import { IOid } from "@/src/types/commonTypes";
 | 
					import { IOid } from "@/src/types/commonTypes";
 | 
				
			||||||
@ -12,7 +12,7 @@ import {
 | 
				
			|||||||
} from "@/src/types/inventoryTypes/inventoryTypes";
 | 
					} from "@/src/types/inventoryTypes/inventoryTypes";
 | 
				
			||||||
import { ExportMisc } from "warframe-public-export-plus";
 | 
					import { ExportMisc } from "warframe-public-export-plus";
 | 
				
			||||||
import { getRecipe } from "@/src/services/itemDataService";
 | 
					import { getRecipe } from "@/src/services/itemDataService";
 | 
				
			||||||
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
 | 
					import { toMongoDate, version_compare } from "@/src/helpers/inventoryHelpers";
 | 
				
			||||||
import { logger } from "@/src/utils/logger";
 | 
					import { logger } from "@/src/utils/logger";
 | 
				
			||||||
import { colorToShard } from "@/src/helpers/shardHelper";
 | 
					import { colorToShard } from "@/src/helpers/shardHelper";
 | 
				
			||||||
import { config } from "@/src/services/configService";
 | 
					import { config } from "@/src/services/configService";
 | 
				
			||||||
@ -23,12 +23,12 @@ import {
 | 
				
			|||||||
} from "@/src/services/infestedFoundryService";
 | 
					} from "@/src/services/infestedFoundryService";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const infestedFoundryController: RequestHandler = async (req, res) => {
 | 
					export const infestedFoundryController: RequestHandler = async (req, res) => {
 | 
				
			||||||
    const accountId = await getAccountIdForRequest(req);
 | 
					    const account = await getAccountForRequest(req);
 | 
				
			||||||
    switch (req.query.mode) {
 | 
					    switch (req.query.mode) {
 | 
				
			||||||
        case "s": {
 | 
					        case "s": {
 | 
				
			||||||
            // shard installation
 | 
					            // shard installation
 | 
				
			||||||
            const request = getJSONfromString<IShardInstallRequest>(String(req.body));
 | 
					            const request = getJSONfromString<IShardInstallRequest>(String(req.body));
 | 
				
			||||||
            const inventory = await getInventory(accountId);
 | 
					            const inventory = await getInventory(account._id.toString());
 | 
				
			||||||
            const suit = inventory.Suits.id(request.SuitId.$oid)!;
 | 
					            const suit = inventory.Suits.id(request.SuitId.$oid)!;
 | 
				
			||||||
            if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) {
 | 
					            if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) {
 | 
				
			||||||
                suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}];
 | 
					                suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}];
 | 
				
			||||||
@ -56,7 +56,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
        case "x": {
 | 
					        case "x": {
 | 
				
			||||||
            // shard removal
 | 
					            // shard removal
 | 
				
			||||||
            const request = getJSONfromString<IShardUninstallRequest>(String(req.body));
 | 
					            const request = getJSONfromString<IShardUninstallRequest>(String(req.body));
 | 
				
			||||||
            const inventory = await getInventory(accountId);
 | 
					            const inventory = await getInventory(account._id.toString());
 | 
				
			||||||
            const suit = inventory.Suits.id(request.SuitId.$oid)!;
 | 
					            const suit = inventory.Suits.id(request.SuitId.$oid)!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const miscItemChanges: IMiscItem[] = [];
 | 
					            const miscItemChanges: IMiscItem[] = [];
 | 
				
			||||||
@ -70,19 +70,30 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
                    ItemCount: 1
 | 
					                    ItemCount: 1
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
                addMiscItems(inventory, miscItemChanges);
 | 
					                addMiscItems(inventory, miscItemChanges);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // consume resources
 | 
				
			||||||
 | 
					                if (!config.infiniteHelminthMaterials) {
 | 
				
			||||||
 | 
					                    let type: string;
 | 
				
			||||||
 | 
					                    let count: number;
 | 
				
			||||||
 | 
					                    if (account.BuildLabel && version_compare(account.BuildLabel, "2025.05.20.10.18") < 0) {
 | 
				
			||||||
 | 
					                        // < 38.6.0
 | 
				
			||||||
 | 
					                        type = "/Lotus/Types/Items/InfestedFoundry/HelminthBile";
 | 
				
			||||||
 | 
					                        count = 300;
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        // >= 38.6.0
 | 
				
			||||||
 | 
					                        type =
 | 
				
			||||||
 | 
					                            archonCrystalRemovalResource[
 | 
				
			||||||
 | 
					                                suit.ArchonCrystalUpgrades![request.Slot].Color!.replace("_MYTHIC", "")
 | 
				
			||||||
 | 
					                            ];
 | 
				
			||||||
 | 
					                        count = suit.ArchonCrystalUpgrades![request.Slot].Color!.indexOf("_MYTHIC") != -1 ? 300 : 150;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == type)!.Count -= count;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // remove from suit
 | 
					            // remove from suit
 | 
				
			||||||
            suit.ArchonCrystalUpgrades![request.Slot] = {};
 | 
					            suit.ArchonCrystalUpgrades![request.Slot] = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!config.infiniteHelminthMaterials) {
 | 
					 | 
				
			||||||
                // remove bile
 | 
					 | 
				
			||||||
                const bile = inventory.InfestedFoundry!.Resources!.find(
 | 
					 | 
				
			||||||
                    x => x.ItemType == "/Lotus/Types/Items/InfestedFoundry/HelminthBile"
 | 
					 | 
				
			||||||
                )!;
 | 
					 | 
				
			||||||
                bile.Count -= 300;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            await inventory.save();
 | 
					            await inventory.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
 | 
					            const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
 | 
				
			||||||
@ -99,7 +110,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
        case "n": {
 | 
					        case "n": {
 | 
				
			||||||
            // name the beast
 | 
					            // name the beast
 | 
				
			||||||
            const request = getJSONfromString<IHelminthNameRequest>(String(req.body));
 | 
					            const request = getJSONfromString<IHelminthNameRequest>(String(req.body));
 | 
				
			||||||
            const inventory = await getInventory(accountId);
 | 
					            const inventory = await getInventory(account._id.toString());
 | 
				
			||||||
            inventory.InfestedFoundry ??= {};
 | 
					            inventory.InfestedFoundry ??= {};
 | 
				
			||||||
            inventory.InfestedFoundry.Name = request.newName;
 | 
					            inventory.InfestedFoundry.Name = request.newName;
 | 
				
			||||||
            await inventory.save();
 | 
					            await inventory.save();
 | 
				
			||||||
@ -122,7 +133,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const request = getJSONfromString<IHelminthFeedRequest>(String(req.body));
 | 
					            const request = getJSONfromString<IHelminthFeedRequest>(String(req.body));
 | 
				
			||||||
            const inventory = await getInventory(accountId);
 | 
					            const inventory = await getInventory(account._id.toString());
 | 
				
			||||||
            inventory.InfestedFoundry ??= {};
 | 
					            inventory.InfestedFoundry ??= {};
 | 
				
			||||||
            inventory.InfestedFoundry.Resources ??= [];
 | 
					            inventory.InfestedFoundry.Resources ??= [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -218,7 +229,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
        case "o": {
 | 
					        case "o": {
 | 
				
			||||||
            // offerings update
 | 
					            // offerings update
 | 
				
			||||||
            const request = getJSONfromString<IHelminthOfferingsUpdate>(String(req.body));
 | 
					            const request = getJSONfromString<IHelminthOfferingsUpdate>(String(req.body));
 | 
				
			||||||
            const inventory = await getInventory(accountId);
 | 
					            const inventory = await getInventory(account._id.toString());
 | 
				
			||||||
            inventory.InfestedFoundry ??= {};
 | 
					            inventory.InfestedFoundry ??= {};
 | 
				
			||||||
            inventory.InfestedFoundry.InvigorationIndex = request.OfferingsIndex;
 | 
					            inventory.InfestedFoundry.InvigorationIndex = request.OfferingsIndex;
 | 
				
			||||||
            inventory.InfestedFoundry.InvigorationSuitOfferings = request.SuitTypes;
 | 
					            inventory.InfestedFoundry.InvigorationSuitOfferings = request.SuitTypes;
 | 
				
			||||||
@ -239,7 +250,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
        case "a": {
 | 
					        case "a": {
 | 
				
			||||||
            // subsume warframe
 | 
					            // subsume warframe
 | 
				
			||||||
            const request = getJSONfromString<IHelminthSubsumeRequest>(String(req.body));
 | 
					            const request = getJSONfromString<IHelminthSubsumeRequest>(String(req.body));
 | 
				
			||||||
            const inventory = await getInventory(accountId);
 | 
					            const inventory = await getInventory(account._id.toString());
 | 
				
			||||||
            const recipe = getRecipe(request.Recipe)!;
 | 
					            const recipe = getRecipe(request.Recipe)!;
 | 
				
			||||||
            if (!config.infiniteHelminthMaterials) {
 | 
					            if (!config.infiniteHelminthMaterials) {
 | 
				
			||||||
                for (const ingredient of recipe.secretIngredients!) {
 | 
					                for (const ingredient of recipe.secretIngredients!) {
 | 
				
			||||||
@ -289,7 +300,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        case "r": {
 | 
					        case "r": {
 | 
				
			||||||
            // rush subsume
 | 
					            // rush subsume
 | 
				
			||||||
            const inventory = await getInventory(accountId);
 | 
					            const inventory = await getInventory(account._id.toString());
 | 
				
			||||||
            const currencyChanges = updateCurrency(inventory, 50, true);
 | 
					            const currencyChanges = updateCurrency(inventory, 50, true);
 | 
				
			||||||
            const recipeChanges = handleSubsumeCompletion(inventory);
 | 
					            const recipeChanges = handleSubsumeCompletion(inventory);
 | 
				
			||||||
            await inventory.save();
 | 
					            await inventory.save();
 | 
				
			||||||
@ -307,7 +318,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        case "u": {
 | 
					        case "u": {
 | 
				
			||||||
            const request = getJSONfromString<IHelminthInvigorationRequest>(String(req.body));
 | 
					            const request = getJSONfromString<IHelminthInvigorationRequest>(String(req.body));
 | 
				
			||||||
            const inventory = await getInventory(accountId);
 | 
					            const inventory = await getInventory(account._id.toString());
 | 
				
			||||||
            const suit = inventory.Suits.id(request.SuitId.$oid)!;
 | 
					            const suit = inventory.Suits.id(request.SuitId.$oid)!;
 | 
				
			||||||
            const upgradesExpiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
 | 
					            const upgradesExpiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
 | 
				
			||||||
            suit.OffensiveUpgrade = request.OffensiveUpgradeType;
 | 
					            suit.OffensiveUpgrade = request.OffensiveUpgradeType;
 | 
				
			||||||
@ -340,7 +351,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        case "custom_unlockall": {
 | 
					        case "custom_unlockall": {
 | 
				
			||||||
            const inventory = await getInventory(accountId);
 | 
					            const inventory = await getInventory(account._id.toString());
 | 
				
			||||||
            inventory.InfestedFoundry ??= {};
 | 
					            inventory.InfestedFoundry ??= {};
 | 
				
			||||||
            inventory.InfestedFoundry.XP ??= 0;
 | 
					            inventory.InfestedFoundry.XP ??= 0;
 | 
				
			||||||
            if (151875_00 > inventory.InfestedFoundry.XP) {
 | 
					            if (151875_00 > inventory.InfestedFoundry.XP) {
 | 
				
			||||||
@ -439,3 +450,12 @@ const apetiteModel = (x: number): number => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    return 3;
 | 
					    return 3;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const archonCrystalRemovalResource: Record<string, string> = {
 | 
				
			||||||
 | 
					    ACC_RED: "/Lotus/Types/Items/InfestedFoundry/HelminthOxides",
 | 
				
			||||||
 | 
					    ACC_YELLOW: "/Lotus/Types/Items/InfestedFoundry/HelminthBile",
 | 
				
			||||||
 | 
					    ACC_BLUE: "/Lotus/Types/Items/InfestedFoundry/HelminthSynthetics",
 | 
				
			||||||
 | 
					    ACC_GREEN: "/Lotus/Types/Items/InfestedFoundry/HelminthBiotics",
 | 
				
			||||||
 | 
					    ACC_ORANGE: "/Lotus/Types/Items/InfestedFoundry/HelminthPheromones",
 | 
				
			||||||
 | 
					    ACC_PURPLE: "/Lotus/Types/Items/InfestedFoundry/HelminthCalx"
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -310,7 +310,7 @@ export const getInventoryResponse = async (
 | 
				
			|||||||
        // Fix nemesis for older versions
 | 
					        // Fix nemesis for older versions
 | 
				
			||||||
        if (
 | 
					        if (
 | 
				
			||||||
            inventoryResponse.Nemesis &&
 | 
					            inventoryResponse.Nemesis &&
 | 
				
			||||||
            version_compare(getNemesisManifest(inventoryResponse.Nemesis.manifest).minBuild, buildLabel) < 0
 | 
					            version_compare(buildLabel, getNemesisManifest(inventoryResponse.Nemesis.manifest).minBuild) < 0
 | 
				
			||||||
        ) {
 | 
					        ) {
 | 
				
			||||||
            inventoryResponse.Nemesis = undefined;
 | 
					            inventoryResponse.Nemesis = undefined;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -20,7 +20,21 @@ export const loginController: RequestHandler = async (request, response) => {
 | 
				
			|||||||
            ? request.query.buildLabel.split(" ").join("+")
 | 
					            ? request.query.buildLabel.split(" ").join("+")
 | 
				
			||||||
            : buildConfig.buildLabel;
 | 
					            : buildConfig.buildLabel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const myAddress = request.host.indexOf("warframe.com") == -1 ? request.host : config.myAddress;
 | 
					    let myAddress: string;
 | 
				
			||||||
 | 
					    let myUrlBase: string = request.protocol + "://";
 | 
				
			||||||
 | 
					    if (request.host.indexOf("warframe.com") == -1) {
 | 
				
			||||||
 | 
					        // Client request was redirected cleanly, so we know it can reach us how it's reaching us now.
 | 
				
			||||||
 | 
					        myAddress = request.hostname;
 | 
				
			||||||
 | 
					        myUrlBase += request.host;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        // Don't know how the client reached us, hoping the config does.
 | 
				
			||||||
 | 
					        myAddress = config.myAddress;
 | 
				
			||||||
 | 
					        myUrlBase += myAddress;
 | 
				
			||||||
 | 
					        const port: number = request.protocol == "http" ? config.httpPort || 80 : config.httpsPort || 443;
 | 
				
			||||||
 | 
					        if (port != (request.protocol == "http" ? 80 : 443)) {
 | 
				
			||||||
 | 
					            myUrlBase += ":" + port;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (
 | 
					    if (
 | 
				
			||||||
        !account &&
 | 
					        !account &&
 | 
				
			||||||
@ -52,7 +66,7 @@ export const loginController: RequestHandler = async (request, response) => {
 | 
				
			|||||||
                LastLogin: new Date()
 | 
					                LastLogin: new Date()
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
            logger.debug("created new account");
 | 
					            logger.debug("created new account");
 | 
				
			||||||
            response.json(createLoginResponse(myAddress, newAccount, buildLabel));
 | 
					            response.json(createLoginResponse(myAddress, myUrlBase, newAccount, buildLabel));
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        } catch (error: unknown) {
 | 
					        } catch (error: unknown) {
 | 
				
			||||||
            if (error instanceof Error) {
 | 
					            if (error instanceof Error) {
 | 
				
			||||||
@ -98,10 +112,15 @@ export const loginController: RequestHandler = async (request, response) => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    await account.save();
 | 
					    await account.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    response.json(createLoginResponse(myAddress, account.toJSON(), buildLabel));
 | 
					    response.json(createLoginResponse(myAddress, myUrlBase, account.toJSON(), buildLabel));
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, buildLabel: string): ILoginResponse => {
 | 
					const createLoginResponse = (
 | 
				
			||||||
 | 
					    myAddress: string,
 | 
				
			||||||
 | 
					    myUrlBase: string,
 | 
				
			||||||
 | 
					    account: IDatabaseAccountJson,
 | 
				
			||||||
 | 
					    buildLabel: string
 | 
				
			||||||
 | 
					): ILoginResponse => {
 | 
				
			||||||
    const resp: ILoginResponse = {
 | 
					    const resp: ILoginResponse = {
 | 
				
			||||||
        id: account.id,
 | 
					        id: account.id,
 | 
				
			||||||
        DisplayName: account.DisplayName,
 | 
					        DisplayName: account.DisplayName,
 | 
				
			||||||
@ -139,11 +158,11 @@ const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, b
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    if (version_compare(buildLabel, "2022.09.06.19.24") >= 0) {
 | 
					    if (version_compare(buildLabel, "2022.09.06.19.24") >= 0) {
 | 
				
			||||||
        resp.CrossPlatformAllowed = account.CrossPlatformAllowed;
 | 
					        resp.CrossPlatformAllowed = account.CrossPlatformAllowed;
 | 
				
			||||||
        resp.HUB = `https://${myAddress}/api/`;
 | 
					        resp.HUB = `${myUrlBase}/api/`;
 | 
				
			||||||
        resp.MatchmakingBuildId = buildConfig.matchmakingBuildId;
 | 
					        resp.MatchmakingBuildId = buildConfig.matchmakingBuildId;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (version_compare(buildLabel, "2023.04.25.23.40") >= 0) {
 | 
					    if (version_compare(buildLabel, "2023.04.25.23.40") >= 0) {
 | 
				
			||||||
        resp.platformCDNs = [`https://${myAddress}/`];
 | 
					        resp.platformCDNs = [`${myUrlBase}/`];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return resp;
 | 
					    return resp;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -57,7 +57,7 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
 | 
				
			|||||||
    const firstCompletion = missionReport.SortieId
 | 
					    const firstCompletion = missionReport.SortieId
 | 
				
			||||||
        ? inventory.CompletedSorties.indexOf(missionReport.SortieId) == -1
 | 
					        ? inventory.CompletedSorties.indexOf(missionReport.SortieId) == -1
 | 
				
			||||||
        : false;
 | 
					        : false;
 | 
				
			||||||
    const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport);
 | 
					    const inventoryUpdates = await addMissionInventoryUpdates(account, inventory, missionReport);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (
 | 
					    if (
 | 
				
			||||||
        missionReport.MissionStatus !== "GS_SUCCESS" &&
 | 
					        missionReport.MissionStatus !== "GS_SUCCESS" &&
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,18 @@
 | 
				
			|||||||
import { version_compare } from "@/src/helpers/inventoryHelpers";
 | 
					import { version_compare } from "@/src/helpers/inventoryHelpers";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
    consumeModCharge,
 | 
					    consumeModCharge,
 | 
				
			||||||
 | 
					    decodeNemesisGuess,
 | 
				
			||||||
    encodeNemesisGuess,
 | 
					    encodeNemesisGuess,
 | 
				
			||||||
    getInfNodes,
 | 
					    getInfNodes,
 | 
				
			||||||
    getKnifeUpgrade,
 | 
					    getKnifeUpgrade,
 | 
				
			||||||
    getNemesisManifest,
 | 
					    getNemesisManifest,
 | 
				
			||||||
    getNemesisPasscode,
 | 
					    getNemesisPasscode,
 | 
				
			||||||
    getNemesisPasscodeModTypes,
 | 
					    getNemesisPasscodeModTypes,
 | 
				
			||||||
 | 
					    GUESS_CORRECT,
 | 
				
			||||||
 | 
					    GUESS_INCORRECT,
 | 
				
			||||||
 | 
					    GUESS_NEUTRAL,
 | 
				
			||||||
 | 
					    GUESS_NONE,
 | 
				
			||||||
 | 
					    GUESS_WILDCARD,
 | 
				
			||||||
    IKnifeResponse
 | 
					    IKnifeResponse
 | 
				
			||||||
} from "@/src/helpers/nemesisHelpers";
 | 
					} from "@/src/helpers/nemesisHelpers";
 | 
				
			||||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
					import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
				
			||||||
@ -82,7 +88,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            for (let i = 0; i != 3; ++i) {
 | 
					            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;
 | 
					                    ++guessResult;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -97,18 +103,29 @@ export const nemesisController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
        if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
 | 
					        if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
 | 
				
			||||||
            const guess: number[] = [body.guess & 0xf, (body.guess >> 4) & 0xf, (body.guess >> 8) & 0xf];
 | 
					            const guess: number[] = [body.guess & 0xf, (body.guess >> 4) & 0xf, (body.guess >> 8) & 0xf];
 | 
				
			||||||
            const passcode = getNemesisPasscode(inventory.Nemesis!)[0];
 | 
					            const passcode = getNemesisPasscode(inventory.Nemesis!)[0];
 | 
				
			||||||
 | 
					            const result1 = passcode == guess[0] ? GUESS_CORRECT : GUESS_INCORRECT;
 | 
				
			||||||
            // Add to GuessHistory
 | 
					            const result2 = passcode == guess[1] ? GUESS_CORRECT : GUESS_INCORRECT;
 | 
				
			||||||
            const result1 = passcode == guess[0] ? 0 : 1;
 | 
					            const result3 = passcode == guess[2] ? GUESS_CORRECT : GUESS_INCORRECT;
 | 
				
			||||||
            const result2 = passcode == guess[1] ? 0 : 1;
 | 
					 | 
				
			||||||
            const result3 = passcode == guess[2] ? 0 : 1;
 | 
					 | 
				
			||||||
            inventory.Nemesis!.GuessHistory.push(
 | 
					            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
 | 
					            // Increase antivirus if correct antivirus mod is installed
 | 
				
			||||||
            const response: IKnifeResponse = {};
 | 
					            const response: IKnifeResponse = {};
 | 
				
			||||||
            if (result1 == 0 || result2 == 0 || result3 == 0) {
 | 
					            if (result1 == GUESS_CORRECT || result2 == GUESS_CORRECT || result3 == GUESS_CORRECT) {
 | 
				
			||||||
                let antivirusGain = 5;
 | 
					                let antivirusGain = 5;
 | 
				
			||||||
                const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
 | 
					                const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
 | 
				
			||||||
                const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid);
 | 
					                const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid);
 | 
				
			||||||
@ -149,18 +166,48 @@ export const nemesisController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
            await inventory.save();
 | 
					            await inventory.save();
 | 
				
			||||||
            res.json(response);
 | 
					            res.json(response);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            const passcode = getNemesisPasscode(inventory.Nemesis!);
 | 
					            // For first guess, create a new entry.
 | 
				
			||||||
            if (passcode[body.position] != body.guess) {
 | 
					            if (body.position == 0) {
 | 
				
			||||||
                res.end();
 | 
					                inventory.Nemesis!.GuessHistory.push(
 | 
				
			||||||
            } else {
 | 
					                    encodeNemesisGuess([
 | 
				
			||||||
                inventory.Nemesis!.Rank += 1;
 | 
					                        {
 | 
				
			||||||
                inventory.Nemesis!.InfNodes = getInfNodes(
 | 
					                            symbol: GUESS_NONE,
 | 
				
			||||||
                    getNemesisManifest(inventory.Nemesis!.manifest),
 | 
					                            result: GUESS_NEUTRAL
 | 
				
			||||||
                    inventory.Nemesis!.Rank
 | 
					                        },
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            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") {
 | 
					    } else if ((req.query.mode as string) == "rs") {
 | 
				
			||||||
        // report spawn; POST but no application data in body
 | 
					        // 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 });
 | 
					        res.json({ LastEnc: inventory.Nemesis!.LastEnc });
 | 
				
			||||||
    } else if ((req.query.mode as string) == "s") {
 | 
					    } else if ((req.query.mode as string) == "s") {
 | 
				
			||||||
        const inventory = await getInventory(account._id.toString(), "Nemesis");
 | 
					        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));
 | 
					        const body = getJSONfromString<INemesisStartRequest>(String(req.body));
 | 
				
			||||||
        body.target.fp = BigInt(body.target.fp);
 | 
					        body.target.fp = BigInt(body.target.fp);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const manifest = getNemesisManifest(body.target.manifest);
 | 
					        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(
 | 
					            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.`
 | 
					                `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 weapons: readonly string[] = manifest.weapons;
 | 
				
			||||||
            const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1);
 | 
					            const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1);
 | 
				
			||||||
            weaponIdx = initialWeaponIdx;
 | 
					            weaponIdx = initialWeaponIdx;
 | 
				
			||||||
            do {
 | 
					            if (body.target.DisallowedWeapons) {
 | 
				
			||||||
                const weapon = weapons[weaponIdx];
 | 
					                do {
 | 
				
			||||||
                if (body.target.DisallowedWeapons.indexOf(weapon) == -1) {
 | 
					                    const weapon = weapons[weaponIdx];
 | 
				
			||||||
                    break;
 | 
					                    if (body.target.DisallowedWeapons.indexOf(weapon) == -1) {
 | 
				
			||||||
                }
 | 
					                        break;
 | 
				
			||||||
                weaponIdx = (weaponIdx + 1) % weapons.length;
 | 
					                    }
 | 
				
			||||||
            } while (weaponIdx != initialWeaponIdx);
 | 
					                    weaponIdx = (weaponIdx + 1) % weapons.length;
 | 
				
			||||||
 | 
					                } while (weaponIdx != initialWeaponIdx);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        inventory.Nemesis = {
 | 
					        inventory.Nemesis = {
 | 
				
			||||||
@ -212,10 +264,10 @@ export const nemesisController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
            GuessHistory: [],
 | 
					            GuessHistory: [],
 | 
				
			||||||
            Hints: [],
 | 
					            Hints: [],
 | 
				
			||||||
            HintProgress: 0,
 | 
					            HintProgress: 0,
 | 
				
			||||||
            Weakened: body.target.Weakened,
 | 
					            Weakened: false,
 | 
				
			||||||
            PrevOwners: 0,
 | 
					            PrevOwners: 0,
 | 
				
			||||||
            HenchmenKilled: 0,
 | 
					            HenchmenKilled: 0,
 | 
				
			||||||
            SecondInCommand: body.target.SecondInCommand,
 | 
					            SecondInCommand: false,
 | 
				
			||||||
            MissionCount: 0,
 | 
					            MissionCount: 0,
 | 
				
			||||||
            LastEnc: 0
 | 
					            LastEnc: 0
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
@ -276,7 +328,7 @@ interface INemesisStartRequest {
 | 
				
			|||||||
        KillingSuit: string;
 | 
					        KillingSuit: string;
 | 
				
			||||||
        killingDamageType: number;
 | 
					        killingDamageType: number;
 | 
				
			||||||
        ShoulderHelmet: string;
 | 
					        ShoulderHelmet: string;
 | 
				
			||||||
        DisallowedWeapons: string[];
 | 
					        DisallowedWeapons?: string[];
 | 
				
			||||||
        WeaponIdx: number;
 | 
					        WeaponIdx: number;
 | 
				
			||||||
        AgentIdx: number;
 | 
					        AgentIdx: number;
 | 
				
			||||||
        BirthNode: string;
 | 
					        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 { logger } from "@/src/utils/logger";
 | 
				
			||||||
import { RequestHandler } from "express";
 | 
					import { RequestHandler } from "express";
 | 
				
			||||||
import { getRecipe } from "@/src/services/itemDataService";
 | 
					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 { unixTimesInMs } from "@/src/constants/timeConstants";
 | 
				
			||||||
import { Types } from "mongoose";
 | 
					import { Types } from "mongoose";
 | 
				
			||||||
import { InventorySlot, ISpectreLoadout } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
					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 { ExportWeapons } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					import { getRandomElement } from "@/src/services/rngService";
 | 
				
			||||||
 | 
					import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface IStartRecipeRequest {
 | 
					interface IStartRecipeRequest {
 | 
				
			||||||
    RecipeName: string;
 | 
					    RecipeName: string;
 | 
				
			||||||
@ -42,24 +44,35 @@ export const startRecipeController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    for (let i = 0; i != recipe.ingredients.length; ++i) {
 | 
					    for (let i = 0; i != recipe.ingredients.length; ++i) {
 | 
				
			||||||
        if (startRecipeRequest.Ids[i] && startRecipeRequest.Ids[i][0] != "/") {
 | 
					        if (startRecipeRequest.Ids[i] && startRecipeRequest.Ids[i][0] != "/") {
 | 
				
			||||||
            const category = ExportWeapons[recipe.ingredients[i].ItemType].productCategory;
 | 
					            if (recipe.ingredients[i].ItemType == "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem") {
 | 
				
			||||||
            if (category != "LongGuns" && category != "Pistols" && category != "Melee") {
 | 
					                const index = inventory.KubrowPetEggs!.findIndex(x => x._id.equals(startRecipeRequest.Ids[i]));
 | 
				
			||||||
                throw new Error(`unexpected equipment ingredient type: ${category}`);
 | 
					                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 {
 | 
					        } else {
 | 
				
			||||||
            await addItem(inventory, recipe.ingredients[i].ItemType, recipe.ingredients[i].ItemCount * -1);
 | 
					            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 = {
 | 
					        const spectreLoadout: ISpectreLoadout = {
 | 
				
			||||||
            ItemType: recipe.resultType,
 | 
					            ItemType: recipe.resultType,
 | 
				
			||||||
            Suits: "",
 | 
					            Suits: "",
 | 
				
			||||||
@ -116,5 +129,5 @@ export const startRecipeController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    await inventory.save();
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.json({ RecipeId: toOid(pr._id) });
 | 
					    res.json({ RecipeId: toOid(pr._id), InventoryChanges: inventoryChanges });
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -1,15 +1,13 @@
 | 
				
			|||||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
					import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
				
			||||||
import { RequestHandler } from "express";
 | 
					import { RequestHandler } from "express";
 | 
				
			||||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
					import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
				
			||||||
import { ExportNightwave, ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus";
 | 
					import { ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus";
 | 
				
			||||||
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
 | 
					import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
 | 
				
			||||||
import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService";
 | 
					import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService";
 | 
				
			||||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
					import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
				
			||||||
import { isStoreItem, toStoreItem } from "@/src/services/itemDataService";
 | 
					import { toStoreItem } from "@/src/services/itemDataService";
 | 
				
			||||||
import { logger } from "@/src/utils/logger";
 | 
					import { logger } from "@/src/utils/logger";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const nightwaveCredsItemType = ExportNightwave.rewards[ExportNightwave.rewards.length - 1].uniqueName;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const syndicateSacrificeController: RequestHandler = async (request, response) => {
 | 
					export const syndicateSacrificeController: RequestHandler = async (request, response) => {
 | 
				
			||||||
    const accountId = await getAccountIdForRequest(request);
 | 
					    const accountId = await getAccountIdForRequest(request);
 | 
				
			||||||
    const inventory = await getInventory(accountId);
 | 
					    const inventory = await getInventory(accountId);
 | 
				
			||||||
@ -54,13 +52,6 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
 | 
				
			|||||||
    syndicate.Title ??= 0;
 | 
					    syndicate.Title ??= 0;
 | 
				
			||||||
    syndicate.Title += 1;
 | 
					    syndicate.Title += 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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 (reward) {
 | 
					    if (reward) {
 | 
				
			||||||
        combineInventoryChanges(
 | 
					        combineInventoryChanges(
 | 
				
			||||||
            res.InventoryChanges,
 | 
					            res.InventoryChanges,
 | 
				
			||||||
@ -68,24 +59,37 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
 | 
				
			|||||||
        );
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (data.AffiliationTag == ExportNightwave.affiliationTag) {
 | 
					    // Quacks like a nightwave syndicate?
 | 
				
			||||||
        const index = syndicate.Title - 1;
 | 
					    if (manifest.dailyChallenges) {
 | 
				
			||||||
        if (index < ExportNightwave.rewards.length) {
 | 
					        const title = manifest.titles!.find(x => x.level == syndicate.Title);
 | 
				
			||||||
 | 
					        if (title) {
 | 
				
			||||||
            res.NewEpisodeReward = true;
 | 
					            res.NewEpisodeReward = true;
 | 
				
			||||||
            const reward = ExportNightwave.rewards[index];
 | 
					            let rewardType: string;
 | 
				
			||||||
            let rewardType = reward.uniqueName;
 | 
					            let rewardCount: number;
 | 
				
			||||||
            if (!isStoreItem(rewardType)) {
 | 
					            if (title.storeItemReward) {
 | 
				
			||||||
                rewardType = toStoreItem(rewardType);
 | 
					                rewardType = title.storeItemReward;
 | 
				
			||||||
 | 
					                rewardCount = 1;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                rewardType = toStoreItem(title.reward!.ItemType);
 | 
				
			||||||
 | 
					                rewardCount = title.reward!.ItemCount;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            const rewardInventoryChanges = (await handleStoreItemAcquisition(rewardType, inventory, reward.itemCount))
 | 
					            const rewardInventoryChanges = (await handleStoreItemAcquisition(rewardType, inventory, rewardCount))
 | 
				
			||||||
                .InventoryChanges;
 | 
					                .InventoryChanges;
 | 
				
			||||||
            if (Object.keys(rewardInventoryChanges).length == 0) {
 | 
					            if (Object.keys(rewardInventoryChanges).length == 0) {
 | 
				
			||||||
                logger.debug(`nightwave rank up reward did not seem to get added, giving 50 creds instead`);
 | 
					                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 }];
 | 
					                rewardInventoryChanges.MiscItems = [{ ItemType: nightwaveCredsItemType, ItemCount: 50 }];
 | 
				
			||||||
                addMiscItems(inventory, rewardInventoryChanges.MiscItems);
 | 
					                addMiscItems(inventory, rewardInventoryChanges.MiscItems);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            combineInventoryChanges(res.InventoryChanges, rewardInventoryChanges);
 | 
					            combineInventoryChanges(res.InventoryChanges, rewardInventoryChanges);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					    } 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);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await inventory.save();
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
				
			|||||||
@ -1,18 +1,26 @@
 | 
				
			|||||||
import { RequestHandler } from "express";
 | 
					import { RequestHandler } from "express";
 | 
				
			||||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
					import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
				
			||||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
					import { getAccountForRequest } from "@/src/services/loginService";
 | 
				
			||||||
import { addChallenges, getInventory } from "@/src/services/inventoryService";
 | 
					import { addChallenges, getInventory } from "@/src/services/inventoryService";
 | 
				
			||||||
import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
					import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
				
			||||||
import { IAffiliationMods } from "@/src/types/purchaseTypes";
 | 
					import { IAffiliationMods } from "@/src/types/purchaseTypes";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const updateChallengeProgressController: RequestHandler = async (req, res) => {
 | 
					export const updateChallengeProgressController: RequestHandler = async (req, res) => {
 | 
				
			||||||
    const challenges = getJSONfromString<IUpdateChallengeProgressRequest>(String(req.body));
 | 
					    const challenges = getJSONfromString<IUpdateChallengeProgressRequest>(String(req.body));
 | 
				
			||||||
    const accountId = await getAccountIdForRequest(req);
 | 
					    const account = await getAccountForRequest(req);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const inventory = await getInventory(accountId, "ChallengeProgress SeasonChallengeHistory Affiliations");
 | 
					    const inventory = await getInventory(
 | 
				
			||||||
 | 
					        account._id.toString(),
 | 
				
			||||||
 | 
					        "ChallengeProgress SeasonChallengeHistory Affiliations"
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
    let affiliationMods: IAffiliationMods[] = [];
 | 
					    let affiliationMods: IAffiliationMods[] = [];
 | 
				
			||||||
    if (challenges.ChallengeProgress) {
 | 
					    if (challenges.ChallengeProgress) {
 | 
				
			||||||
        affiliationMods = addChallenges(inventory, challenges.ChallengeProgress, challenges.SeasonChallengeCompletions);
 | 
					        affiliationMods = addChallenges(
 | 
				
			||||||
 | 
					            account,
 | 
				
			||||||
 | 
					            inventory,
 | 
				
			||||||
 | 
					            challenges.ChallengeProgress,
 | 
				
			||||||
 | 
					            challenges.SeasonChallengeCompletions
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (challenges.SeasonChallengeHistory) {
 | 
					    if (challenges.SeasonChallengeHistory) {
 | 
				
			||||||
        challenges.SeasonChallengeHistory.forEach(({ challenge, id }) => {
 | 
					        challenges.SeasonChallengeHistory.forEach(({ challenge, id }) => {
 | 
				
			||||||
 | 
				
			|||||||
@ -25,6 +25,7 @@ import allIncarnons from "@/static/fixed_responses/allIncarnonList.json";
 | 
				
			|||||||
interface ListedItem {
 | 
					interface ListedItem {
 | 
				
			||||||
    uniqueName: string;
 | 
					    uniqueName: string;
 | 
				
			||||||
    name: string;
 | 
					    name: string;
 | 
				
			||||||
 | 
					    subtype?: string;
 | 
				
			||||||
    fusionLimit?: number;
 | 
					    fusionLimit?: number;
 | 
				
			||||||
    exalted?: string[];
 | 
					    exalted?: string[];
 | 
				
			||||||
    badReason?: "starter" | "frivolous" | "notraw";
 | 
					    badReason?: "starter" | "frivolous" | "notraw";
 | 
				
			||||||
@ -175,7 +176,8 @@ const getItemListsController: RequestHandler = (req, response) => {
 | 
				
			|||||||
        ) {
 | 
					        ) {
 | 
				
			||||||
            res.miscitems.push({
 | 
					            res.miscitems.push({
 | 
				
			||||||
                uniqueName: uniqueName,
 | 
					                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)) {
 | 
					    for (const [uniqueName, item] of Object.entries(ExportGear)) {
 | 
				
			||||||
        res.miscitems.push({
 | 
					        res.miscitems.push({
 | 
				
			||||||
            uniqueName: uniqueName,
 | 
					            uniqueName: uniqueName,
 | 
				
			||||||
            name: getString(item.name, lang)
 | 
					            name: getString(item.name, lang),
 | 
				
			||||||
 | 
					            subtype: "Gear"
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const recipeNameTemplate = getString("/Lotus/Language/Items/BlueprintAndItem", lang);
 | 
					    const recipeNameTemplate = getString("/Lotus/Language/Items/BlueprintAndItem", lang);
 | 
				
			||||||
 | 
				
			|||||||
@ -237,7 +237,7 @@ export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: TNemesisFacti
 | 
				
			|||||||
    return passcode;
 | 
					    return passcode;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const reqiuemMods: readonly string[] = [
 | 
					const requiemMods: readonly string[] = [
 | 
				
			||||||
    "/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod",
 | 
					    "/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod",
 | 
				
			||||||
    "/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod",
 | 
					    "/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod",
 | 
				
			||||||
    "/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod",
 | 
					    "/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod",
 | 
				
			||||||
@ -263,29 +263,51 @@ export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: TNeme
 | 
				
			|||||||
    const passcode = getNemesisPasscode(nemesis);
 | 
					    const passcode = getNemesisPasscode(nemesis);
 | 
				
			||||||
    return nemesis.Faction == "FC_INFESTATION"
 | 
					    return nemesis.Faction == "FC_INFESTATION"
 | 
				
			||||||
        ? passcode.map(i => antivirusMods[i])
 | 
					        ? passcode.map(i => antivirusMods[i])
 | 
				
			||||||
        : passcode.map(i => reqiuemMods[i]);
 | 
					        : passcode.map(i => requiemMods[i]);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const encodeNemesisGuess = (
 | 
					// Symbols; 0-7 are the normal requiem mods.
 | 
				
			||||||
    symbol1: number,
 | 
					export const GUESS_NONE = 8;
 | 
				
			||||||
    result1: number,
 | 
					export const GUESS_WILDCARD = 9;
 | 
				
			||||||
    symbol2: number,
 | 
					
 | 
				
			||||||
    result2: number,
 | 
					// Results; there are 3, 4, 5 as well which are more muted versions but unused afaik.
 | 
				
			||||||
    symbol3: number,
 | 
					export const GUESS_NEUTRAL = 0;
 | 
				
			||||||
    result3: number
 | 
					export const GUESS_INCORRECT = 1;
 | 
				
			||||||
): number => {
 | 
					export const GUESS_CORRECT = 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface NemesisPositionGuess {
 | 
				
			||||||
 | 
					    symbol: number;
 | 
				
			||||||
 | 
					    result: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type NemesisGuess = [NemesisPositionGuess, NemesisPositionGuess, NemesisPositionGuess];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const encodeNemesisGuess = (guess: NemesisGuess): number => {
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        (symbol1 & 0xf) |
 | 
					        (guess[0].symbol & 0xf) |
 | 
				
			||||||
        ((result1 & 3) << 12) |
 | 
					        ((guess[0].result & 3) << 12) |
 | 
				
			||||||
        ((symbol2 << 4) & 0xff) |
 | 
					        ((guess[1].symbol << 4) & 0xff) |
 | 
				
			||||||
        ((result2 << 14) & 0xffff) |
 | 
					        ((guess[1].result << 14) & 0xffff) |
 | 
				
			||||||
        ((symbol3 & 0xf) << 8) |
 | 
					        ((guess[2].symbol & 0xf) << 8) |
 | 
				
			||||||
        ((result3 & 3) << 16)
 | 
					        ((guess[2].result & 3) << 16)
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const decodeNemesisGuess = (val: number): number[] => {
 | 
					export const decodeNemesisGuess = (val: number): NemesisGuess => {
 | 
				
			||||||
    return [val & 0xf, (val >> 12) & 3, (val & 0xff) >> 4, (val & 0xffff) >> 14, (val >> 8) & 0xf, (val >> 16) & 3];
 | 
					    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 {
 | 
					export interface IKnifeResponse {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										27
									
								
								src/index.ts
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								src/index.ts
									
									
									
									
									
								
							@ -12,12 +12,10 @@ import { logger } from "@/src/utils/logger";
 | 
				
			|||||||
logger.info("Starting up...");
 | 
					logger.info("Starting up...");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Proceed with normal startup: bring up config watcher service, validate config, connect to MongoDB, and finally start listening for HTTP.
 | 
					// Proceed with normal startup: bring up config watcher service, validate config, connect to MongoDB, and finally start listening for HTTP.
 | 
				
			||||||
import http from "http";
 | 
					 | 
				
			||||||
import https from "https";
 | 
					 | 
				
			||||||
import fs from "node:fs";
 | 
					 | 
				
			||||||
import { app } from "./app";
 | 
					 | 
				
			||||||
import mongoose from "mongoose";
 | 
					import mongoose from "mongoose";
 | 
				
			||||||
import { JSONStringify } from "json-with-bigint";
 | 
					import { JSONStringify } from "json-with-bigint";
 | 
				
			||||||
 | 
					import { startWebServer } from "./services/webService";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { validateConfig } from "@/src/services/configWatcherService";
 | 
					import { validateConfig } from "@/src/services/configWatcherService";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Patch JSON.stringify to work flawlessly with Bigints.
 | 
					// Patch JSON.stringify to work flawlessly with Bigints.
 | 
				
			||||||
@ -29,26 +27,7 @@ mongoose
 | 
				
			|||||||
    .connect(config.mongodbUrl)
 | 
					    .connect(config.mongodbUrl)
 | 
				
			||||||
    .then(() => {
 | 
					    .then(() => {
 | 
				
			||||||
        logger.info("Connected to MongoDB");
 | 
					        logger.info("Connected to MongoDB");
 | 
				
			||||||
 | 
					        startWebServer();
 | 
				
			||||||
        const httpPort = config.httpPort || 80;
 | 
					 | 
				
			||||||
        const httpsPort = config.httpsPort || 443;
 | 
					 | 
				
			||||||
        const options = {
 | 
					 | 
				
			||||||
            key: fs.readFileSync("static/certs/key.pem"),
 | 
					 | 
				
			||||||
            cert: fs.readFileSync("static/certs/cert.pem")
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
					 | 
				
			||||||
        http.createServer(app).listen(httpPort, () => {
 | 
					 | 
				
			||||||
            logger.info("HTTP server started on port " + httpPort);
 | 
					 | 
				
			||||||
            // eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
					 | 
				
			||||||
            https.createServer(options, app).listen(httpsPort, () => {
 | 
					 | 
				
			||||||
                logger.info("HTTPS server started on port " + httpsPort);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                logger.info(
 | 
					 | 
				
			||||||
                    "Access the WebUI in your browser at http://localhost" + (httpPort == 80 ? "" : ":" + httpPort)
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    .catch(error => {
 | 
					    .catch(error => {
 | 
				
			||||||
        if (error instanceof Error) {
 | 
					        if (error instanceof Error) {
 | 
				
			||||||
 | 
				
			|||||||
@ -1097,7 +1097,8 @@ const pendingRecipeSchema = new Schema<IPendingRecipeDatabase>(
 | 
				
			|||||||
        LongGuns: { type: [EquipmentSchema], default: undefined },
 | 
					        LongGuns: { type: [EquipmentSchema], default: undefined },
 | 
				
			||||||
        Pistols: { type: [EquipmentSchema], default: undefined },
 | 
					        Pistols: { type: [EquipmentSchema], default: undefined },
 | 
				
			||||||
        Melee: { 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 }
 | 
					    { id: false }
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
@ -1115,6 +1116,7 @@ pendingRecipeSchema.set("toJSON", {
 | 
				
			|||||||
        delete returnedObject.Pistols;
 | 
					        delete returnedObject.Pistols;
 | 
				
			||||||
        delete returnedObject.Melees;
 | 
					        delete returnedObject.Melees;
 | 
				
			||||||
        delete returnedObject.SuitToUnbrand;
 | 
					        delete returnedObject.SuitToUnbrand;
 | 
				
			||||||
 | 
					        delete returnedObject.KubrowPet;
 | 
				
			||||||
        (returnedObject as IPendingRecipeClient).CompletionDate = {
 | 
					        (returnedObject as IPendingRecipeClient).CompletionDate = {
 | 
				
			||||||
            $date: { $numberLong: (returnedObject as IPendingRecipeDatabase).CompletionDate.getTime().toString() }
 | 
					            $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 { addPendingFriendController } from "@/src/controllers/api/addPendingFriendController";
 | 
				
			||||||
import { addToAllianceController } from "@/src/controllers/api/addToAllianceController";
 | 
					import { addToAllianceController } from "@/src/controllers/api/addToAllianceController";
 | 
				
			||||||
import { addToGuildController } from "@/src/controllers/api/addToGuildController";
 | 
					import { addToGuildController } from "@/src/controllers/api/addToGuildController";
 | 
				
			||||||
 | 
					import { adoptPetController } from "@/src/controllers/api/adoptPetController";
 | 
				
			||||||
import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController";
 | 
					import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController";
 | 
				
			||||||
import { archonFusionController } from "@/src/controllers/api/archonFusionController";
 | 
					import { archonFusionController } from "@/src/controllers/api/archonFusionController";
 | 
				
			||||||
import { artifactsController } from "@/src/controllers/api/artifactsController";
 | 
					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 { removeFromAllianceController } from "@/src/controllers/api/removeFromAllianceController";
 | 
				
			||||||
import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController";
 | 
					import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController";
 | 
				
			||||||
import { removeIgnoredUserController } from "@/src/controllers/api/removeIgnoredUserController";
 | 
					import { removeIgnoredUserController } from "@/src/controllers/api/removeIgnoredUserController";
 | 
				
			||||||
 | 
					import { renamePetController } from "@/src/controllers/api/renamePetController";
 | 
				
			||||||
import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController";
 | 
					import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController";
 | 
				
			||||||
import { retrievePetFromStasisController } from "@/src/controllers/api/retrievePetFromStasisController";
 | 
					import { retrievePetFromStasisController } from "@/src/controllers/api/retrievePetFromStasisController";
 | 
				
			||||||
import { saveDialogueController } from "@/src/controllers/api/saveDialogueController";
 | 
					import { saveDialogueController } from "@/src/controllers/api/saveDialogueController";
 | 
				
			||||||
@ -225,6 +227,7 @@ apiRouter.post("/addIgnoredUser.php", addIgnoredUserController);
 | 
				
			|||||||
apiRouter.post("/addPendingFriend.php", addPendingFriendController);
 | 
					apiRouter.post("/addPendingFriend.php", addPendingFriendController);
 | 
				
			||||||
apiRouter.post("/addToAlliance.php", addToAllianceController);
 | 
					apiRouter.post("/addToAlliance.php", addToAllianceController);
 | 
				
			||||||
apiRouter.post("/addToGuild.php", addToGuildController);
 | 
					apiRouter.post("/addToGuild.php", addToGuildController);
 | 
				
			||||||
 | 
					apiRouter.post("/adoptPet.php", adoptPetController);
 | 
				
			||||||
apiRouter.post("/arcaneCommon.php", arcaneCommonController);
 | 
					apiRouter.post("/arcaneCommon.php", arcaneCommonController);
 | 
				
			||||||
apiRouter.post("/archonFusion.php", archonFusionController);
 | 
					apiRouter.post("/archonFusion.php", archonFusionController);
 | 
				
			||||||
apiRouter.post("/artifacts.php", artifactsController);
 | 
					apiRouter.post("/artifacts.php", artifactsController);
 | 
				
			||||||
@ -294,6 +297,7 @@ apiRouter.post("/releasePet.php", releasePetController);
 | 
				
			|||||||
apiRouter.post("/removeFriend.php", removeFriendPostController);
 | 
					apiRouter.post("/removeFriend.php", removeFriendPostController);
 | 
				
			||||||
apiRouter.post("/removeFromGuild.php", removeFromGuildController);
 | 
					apiRouter.post("/removeFromGuild.php", removeFromGuildController);
 | 
				
			||||||
apiRouter.post("/removeIgnoredUser.php", removeIgnoredUserController);
 | 
					apiRouter.post("/removeIgnoredUser.php", removeIgnoredUserController);
 | 
				
			||||||
 | 
					apiRouter.post("/renamePet.php", renamePetController);
 | 
				
			||||||
apiRouter.post("/rerollRandomMod.php", rerollRandomModController);
 | 
					apiRouter.post("/rerollRandomMod.php", rerollRandomModController);
 | 
				
			||||||
apiRouter.post("/retrievePetFromStasis.php", retrievePetFromStasisController);
 | 
					apiRouter.post("/retrievePetFromStasis.php", retrievePetFromStasisController);
 | 
				
			||||||
apiRouter.post("/saveDialogue.php", saveDialogueController);
 | 
					apiRouter.post("/saveDialogue.php", saveDialogueController);
 | 
				
			||||||
 | 
				
			|||||||
@ -62,6 +62,7 @@ interface IConfig {
 | 
				
			|||||||
        resourceBoost?: boolean;
 | 
					        resourceBoost?: boolean;
 | 
				
			||||||
        starDays?: boolean;
 | 
					        starDays?: boolean;
 | 
				
			||||||
        lockTime?: number;
 | 
					        lockTime?: number;
 | 
				
			||||||
 | 
					        nightwaveOverride?: string;
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@ import fs from "fs";
 | 
				
			|||||||
import fsPromises from "fs/promises";
 | 
					import fsPromises from "fs/promises";
 | 
				
			||||||
import { logger } from "../utils/logger";
 | 
					import { logger } from "../utils/logger";
 | 
				
			||||||
import { config, configPath, loadConfig } from "./configService";
 | 
					import { config, configPath, loadConfig } from "./configService";
 | 
				
			||||||
 | 
					import { getWebPorts, startWebServer, stopWebServer } from "./webService";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let amnesia = false;
 | 
					let amnesia = false;
 | 
				
			||||||
fs.watchFile(configPath, () => {
 | 
					fs.watchFile(configPath, () => {
 | 
				
			||||||
@ -16,13 +17,31 @@ fs.watchFile(configPath, () => {
 | 
				
			|||||||
            process.exit(1);
 | 
					            process.exit(1);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        validateConfig();
 | 
					        validateConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const webPorts = getWebPorts();
 | 
				
			||||||
 | 
					        if (config.httpPort != webPorts.http || config.httpsPort != webPorts.https) {
 | 
				
			||||||
 | 
					            logger.info(`Restarting web server to apply port changes.`);
 | 
				
			||||||
 | 
					            void stopWebServer().then(startWebServer);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const validateConfig = (): void => {
 | 
					export const validateConfig = (): void => {
 | 
				
			||||||
    if (typeof config.administratorNames == "string") {
 | 
					    let modified = false;
 | 
				
			||||||
        logger.info(`Updating config.json to make administratorNames an array.`);
 | 
					    if (config.administratorNames) {
 | 
				
			||||||
        config.administratorNames = [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();
 | 
					        void saveConfig();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -44,6 +44,7 @@ import {
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
    ExportArcanes,
 | 
					    ExportArcanes,
 | 
				
			||||||
    ExportBundles,
 | 
					    ExportBundles,
 | 
				
			||||||
 | 
					    ExportChallenges,
 | 
				
			||||||
    ExportCustoms,
 | 
					    ExportCustoms,
 | 
				
			||||||
    ExportDrones,
 | 
					    ExportDrones,
 | 
				
			||||||
    ExportEmailItems,
 | 
					    ExportEmailItems,
 | 
				
			||||||
@ -53,7 +54,6 @@ import {
 | 
				
			|||||||
    ExportGear,
 | 
					    ExportGear,
 | 
				
			||||||
    ExportKeys,
 | 
					    ExportKeys,
 | 
				
			||||||
    ExportMisc,
 | 
					    ExportMisc,
 | 
				
			||||||
    ExportNightwave,
 | 
					 | 
				
			||||||
    ExportRailjackWeapons,
 | 
					    ExportRailjackWeapons,
 | 
				
			||||||
    ExportRecipes,
 | 
					    ExportRecipes,
 | 
				
			||||||
    ExportResources,
 | 
					    ExportResources,
 | 
				
			||||||
@ -83,8 +83,10 @@ import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json";
 | 
				
			|||||||
import { getRandomElement, getRandomInt, getRandomWeightedReward, SRng } from "./rngService";
 | 
					import { getRandomElement, getRandomInt, getRandomWeightedReward, SRng } from "./rngService";
 | 
				
			||||||
import { createMessage } from "./inboxService";
 | 
					import { createMessage } from "./inboxService";
 | 
				
			||||||
import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
 | 
					import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
 | 
				
			||||||
import { getWorldState } from "./worldStateService";
 | 
					import { getNightwaveSyndicateTag, getWorldState } from "./worldStateService";
 | 
				
			||||||
import { generateNemesisProfile, INemesisProfile } from "../helpers/nemesisHelpers";
 | 
					import { generateNemesisProfile, INemesisProfile } from "../helpers/nemesisHelpers";
 | 
				
			||||||
 | 
					import { TAccountDocument } from "./loginService";
 | 
				
			||||||
 | 
					import { unixTimesInMs } from "../constants/timeConstants";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const createInventory = async (
 | 
					export const createInventory = async (
 | 
				
			||||||
    accountOwnerId: Types.ObjectId,
 | 
					    accountOwnerId: Types.ObjectId,
 | 
				
			||||||
@ -721,6 +723,10 @@ export const addItem = async (
 | 
				
			|||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    break;
 | 
					                    break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                case "Boons":
 | 
				
			||||||
 | 
					                    // Can purchase /Lotus/Upgrades/Boons/DuviriVendorBoonItem from Acrithis, doesn't need to be added to inventory.
 | 
				
			||||||
 | 
					                    return {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                case "Stickers":
 | 
					                case "Stickers":
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        const entry = inventory.RawUpgrades.find(x => x.ItemType == typeName);
 | 
					                        const entry = inventory.RawUpgrades.find(x => x.ItemType == typeName);
 | 
				
			||||||
@ -775,7 +781,9 @@ export const addItem = async (
 | 
				
			|||||||
                        typeName.substr(1).split("/")[3] == "CatbrowPet" ||
 | 
					                        typeName.substr(1).split("/")[3] == "CatbrowPet" ||
 | 
				
			||||||
                        typeName.substr(1).split("/")[3] == "KubrowPet"
 | 
					                        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/")) {
 | 
					                    } else if (typeName.startsWith("/Lotus/Types/Game/CrewShip/CrewMember/")) {
 | 
				
			||||||
                        if (!seed) {
 | 
					                        if (!seed) {
 | 
				
			||||||
                            throw new Error(`Expected crew member to have a seed`);
 | 
					                            throw new Error(`Expected crew member to have a seed`);
 | 
				
			||||||
@ -790,6 +798,12 @@ export const addItem = async (
 | 
				
			|||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    break;
 | 
					                    break;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					                case "Items": {
 | 
				
			||||||
 | 
					                    if (typeName.substr(1).split("/")[3] == "Emotes") {
 | 
				
			||||||
 | 
					                        return addCustomization(inventory, typeName);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
                case "NeutralCreatures": {
 | 
					                case "NeutralCreatures": {
 | 
				
			||||||
                    if (inventory.Horses.length != 0) {
 | 
					                    if (inventory.Horses.length != 0) {
 | 
				
			||||||
                        logger.warn("refusing to add Horse because account already has one");
 | 
					                        logger.warn("refusing to add Horse because account already has one");
 | 
				
			||||||
@ -1014,12 +1028,13 @@ export const addSpaceSuit = (
 | 
				
			|||||||
export const addKubrowPet = (
 | 
					export const addKubrowPet = (
 | 
				
			||||||
    inventory: TInventoryDatabaseDocument,
 | 
					    inventory: TInventoryDatabaseDocument,
 | 
				
			||||||
    kubrowPetName: string,
 | 
					    kubrowPetName: string,
 | 
				
			||||||
    details: IKubrowPetDetailsDatabase | undefined,
 | 
					    details?: IKubrowPetDetailsDatabase,
 | 
				
			||||||
    premiumPurchase: boolean,
 | 
					    premiumPurchase: boolean = false,
 | 
				
			||||||
    inventoryChanges: IInventoryChanges = {}
 | 
					    inventoryChanges: IInventoryChanges = {}
 | 
				
			||||||
): IInventoryChanges => {
 | 
					): IInventoryChanges => {
 | 
				
			||||||
    combineInventoryChanges(inventoryChanges, occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase));
 | 
					    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 kubrowPet = ExportSentinels[kubrowPetName] as ISentinel | undefined;
 | 
				
			||||||
    const exalted = kubrowPet?.exalted ?? [];
 | 
					    const exalted = kubrowPet?.exalted ?? [];
 | 
				
			||||||
    for (const specialItem of exalted) {
 | 
					    for (const specialItem of exalted) {
 | 
				
			||||||
@ -1068,11 +1083,11 @@ export const addKubrowPet = (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        details = {
 | 
					        details = {
 | 
				
			||||||
            Name: "",
 | 
					            Name: "",
 | 
				
			||||||
            IsPuppy: false,
 | 
					            IsPuppy: !premiumPurchase,
 | 
				
			||||||
            HasCollar: true,
 | 
					            HasCollar: true,
 | 
				
			||||||
            PrintsRemaining: 2,
 | 
					            PrintsRemaining: 3,
 | 
				
			||||||
            Status: Status.StatusStasis,
 | 
					            Status: premiumPurchase ? Status.StatusStasis : Status.StatusIncubating,
 | 
				
			||||||
            HatchDate: new Date(Math.trunc(Date.now() / 86400000) * 86400000),
 | 
					            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),
 | 
					            IsMale: !!getRandomInt(0, 1),
 | 
				
			||||||
            Size: getRandomInt(70, 100) / 100,
 | 
					            Size: getRandomInt(70, 100) / 100,
 | 
				
			||||||
            DominantTraits: traits,
 | 
					            DominantTraits: traits,
 | 
				
			||||||
@ -1716,6 +1731,7 @@ export const addLoreFragmentScans = (inventory: TInventoryDatabaseDocument, arr:
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const addChallenges = (
 | 
					export const addChallenges = (
 | 
				
			||||||
 | 
					    account: TAccountDocument,
 | 
				
			||||||
    inventory: TInventoryDatabaseDocument,
 | 
					    inventory: TInventoryDatabaseDocument,
 | 
				
			||||||
    ChallengeProgress: IChallengeProgress[],
 | 
					    ChallengeProgress: IChallengeProgress[],
 | 
				
			||||||
    SeasonChallengeCompletions: ISeasonChallenge[] | undefined
 | 
					    SeasonChallengeCompletions: ISeasonChallenge[] | undefined
 | 
				
			||||||
@ -1738,26 +1754,32 @@ export const addChallenges = (
 | 
				
			|||||||
                continue;
 | 
					                continue;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const meta = ExportNightwave.challenges[challenge.challenge];
 | 
					            const meta = ExportChallenges[challenge.challenge];
 | 
				
			||||||
            logger.debug("Completed challenge", meta);
 | 
					            const nightwaveSyndicateTag = getNightwaveSyndicateTag(account.BuildLabel);
 | 
				
			||||||
 | 
					            logger.debug("Completed season challenge", {
 | 
				
			||||||
 | 
					                uniqueName: challenge.challenge,
 | 
				
			||||||
 | 
					                syndicateTag: nightwaveSyndicateTag,
 | 
				
			||||||
 | 
					                ...meta
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            if (nightwaveSyndicateTag) {
 | 
				
			||||||
 | 
					                let affiliation = inventory.Affiliations.find(x => x.Tag == nightwaveSyndicateTag);
 | 
				
			||||||
 | 
					                if (!affiliation) {
 | 
				
			||||||
 | 
					                    affiliation =
 | 
				
			||||||
 | 
					                        inventory.Affiliations[
 | 
				
			||||||
 | 
					                            inventory.Affiliations.push({
 | 
				
			||||||
 | 
					                                Tag: nightwaveSyndicateTag,
 | 
				
			||||||
 | 
					                                Standing: 0
 | 
				
			||||||
 | 
					                            }) - 1
 | 
				
			||||||
 | 
					                        ];
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                affiliation.Standing += meta.standing!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            let affiliation = inventory.Affiliations.find(x => x.Tag == ExportNightwave.affiliationTag);
 | 
					                if (affiliationMods.length == 0) {
 | 
				
			||||||
            if (!affiliation) {
 | 
					                    affiliationMods.push({ Tag: nightwaveSyndicateTag });
 | 
				
			||||||
                affiliation =
 | 
					                }
 | 
				
			||||||
                    inventory.Affiliations[
 | 
					                affiliationMods[0].Standing ??= 0;
 | 
				
			||||||
                        inventory.Affiliations.push({
 | 
					                affiliationMods[0].Standing += meta.standing!;
 | 
				
			||||||
                            Tag: ExportNightwave.affiliationTag,
 | 
					 | 
				
			||||||
                            Standing: 0
 | 
					 | 
				
			||||||
                        }) - 1
 | 
					 | 
				
			||||||
                    ];
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            affiliation.Standing += meta.standing;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (affiliationMods.length == 0) {
 | 
					 | 
				
			||||||
                affiliationMods.push({ Tag: ExportNightwave.affiliationTag });
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            affiliationMods[0].Standing ??= 0;
 | 
					 | 
				
			||||||
            affiliationMods[0].Standing += meta.standing;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return affiliationMods;
 | 
					    return affiliationMods;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,4 @@
 | 
				
			|||||||
import { IKeyChainRequest } from "@/src/types/requestTypes";
 | 
					import { IKeyChainRequest } from "@/src/types/requestTypes";
 | 
				
			||||||
import { getIndexAfter } from "@/src/helpers/stringHelpers";
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
    dict_de,
 | 
					    dict_de,
 | 
				
			||||||
    dict_en,
 | 
					    dict_en,
 | 
				
			||||||
@ -53,20 +52,32 @@ export const getRecipeByResult = (resultType: string): IRecipe | undefined => {
 | 
				
			|||||||
    return Object.values(ExportRecipes).find(x => x.resultType == resultType);
 | 
					    return Object.values(ExportRecipes).find(x => x.resultType == resultType);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getItemCategoryByUniqueName = (uniqueName: string): string => {
 | 
					export const getItemCategoryByUniqueName = (uniqueName: string): string | undefined => {
 | 
				
			||||||
    //Lotus/Types/Items/MiscItems/PolymerBundle
 | 
					    if (uniqueName in ExportCustoms) {
 | 
				
			||||||
 | 
					        return ExportCustoms[uniqueName].productCategory;
 | 
				
			||||||
    let splitWord = "Items/";
 | 
					 | 
				
			||||||
    if (!uniqueName.includes("/Items/")) {
 | 
					 | 
				
			||||||
        splitWord = "/Types/";
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (uniqueName in ExportDrones) {
 | 
				
			||||||
    const index = getIndexAfter(uniqueName, splitWord);
 | 
					        return "Drones";
 | 
				
			||||||
    if (index === -1) {
 | 
					 | 
				
			||||||
        throw new Error(`error parsing item category ${uniqueName}`);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const category = uniqueName.substring(index).split("/")[0];
 | 
					    if (uniqueName in ExportKeys) {
 | 
				
			||||||
    return category;
 | 
					        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 => {
 | 
					export const getItemName = (uniqueName: string): string | undefined => {
 | 
				
			||||||
 | 
				
			|||||||
@ -5,7 +5,13 @@ import { mixSeeds, SRng } from "./rngService";
 | 
				
			|||||||
import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel";
 | 
					import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel";
 | 
				
			||||||
import { addBooster, updateCurrency } from "./inventoryService";
 | 
					import { addBooster, updateCurrency } from "./inventoryService";
 | 
				
			||||||
import { handleStoreItemAcquisition } from "./purchaseService";
 | 
					import { handleStoreItemAcquisition } from "./purchaseService";
 | 
				
			||||||
import { ExportBoosters, ExportRecipes, ExportWarframes, ExportWeapons } from "warframe-public-export-plus";
 | 
					import {
 | 
				
			||||||
 | 
					    ExportBoosterPacks,
 | 
				
			||||||
 | 
					    ExportBoosters,
 | 
				
			||||||
 | 
					    ExportRecipes,
 | 
				
			||||||
 | 
					    ExportWarframes,
 | 
				
			||||||
 | 
					    ExportWeapons
 | 
				
			||||||
 | 
					} from "warframe-public-export-plus";
 | 
				
			||||||
import { toStoreItem } from "./itemDataService";
 | 
					import { toStoreItem } from "./itemDataService";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ILoginRewardsReponse {
 | 
					export interface ILoginRewardsReponse {
 | 
				
			||||||
@ -76,6 +82,7 @@ export const getRandomLoginRewards = (
 | 
				
			|||||||
const getRandomLoginReward = (rng: SRng, day: number, inventory: TInventoryDatabaseDocument): ILoginReward => {
 | 
					const getRandomLoginReward = (rng: SRng, day: number, inventory: TInventoryDatabaseDocument): ILoginReward => {
 | 
				
			||||||
    const reward = rng.randomReward(randomRewards)!;
 | 
					    const reward = rng.randomReward(randomRewards)!;
 | 
				
			||||||
    //const reward = randomRewards.find(x => x.RewardType == "RT_BOOSTER")!;
 | 
					    //const reward = randomRewards.find(x => x.RewardType == "RT_BOOSTER")!;
 | 
				
			||||||
 | 
					    let storeItemType: string = reward.StoreItemType;
 | 
				
			||||||
    if (reward.RewardType == "RT_RANDOM_RECIPE") {
 | 
					    if (reward.RewardType == "RT_RANDOM_RECIPE") {
 | 
				
			||||||
        const masteredItems = new Set();
 | 
					        const masteredItems = new Set();
 | 
				
			||||||
        for (const entry of inventory.XPInfo) {
 | 
					        for (const entry of inventory.XPInfo) {
 | 
				
			||||||
@ -102,7 +109,12 @@ const getRandomLoginReward = (rng: SRng, day: number, inventory: TInventoryDatab
 | 
				
			|||||||
            // This account has all applicable warframes and weapons already mastered (filthy cheater), need a different reward.
 | 
					            // This account has all applicable warframes and weapons already mastered (filthy cheater), need a different reward.
 | 
				
			||||||
            return getRandomLoginReward(rng, day, inventory);
 | 
					            return getRandomLoginReward(rng, day, inventory);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        reward.StoreItemType = toStoreItem(rng.randomElement(eligibleRecipes)!);
 | 
					        storeItemType = toStoreItem(rng.randomElement(eligibleRecipes)!);
 | 
				
			||||||
 | 
					    } else if (reward.StoreItemType == "/Lotus/StoreItems/Types/BoosterPacks/LoginRewardRandomProjection") {
 | 
				
			||||||
 | 
					        storeItemType = toStoreItem(
 | 
				
			||||||
 | 
					            rng.randomElement(ExportBoosterPacks["/Lotus/Types/BoosterPacks/LoginRewardRandomProjection"].components)!
 | 
				
			||||||
 | 
					                .Item
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
        //_id: toOid(new Types.ObjectId()),
 | 
					        //_id: toOid(new Types.ObjectId()),
 | 
				
			||||||
@ -110,7 +122,7 @@ const getRandomLoginReward = (rng: SRng, day: number, inventory: TInventoryDatab
 | 
				
			|||||||
        //CouponType: "CPT_PLATINUM",
 | 
					        //CouponType: "CPT_PLATINUM",
 | 
				
			||||||
        Icon: reward.Icon ?? "",
 | 
					        Icon: reward.Icon ?? "",
 | 
				
			||||||
        //ItemType: "",
 | 
					        //ItemType: "",
 | 
				
			||||||
        StoreItemType: reward.StoreItemType,
 | 
					        StoreItemType: storeItemType,
 | 
				
			||||||
        //ProductCategory: "Pistols",
 | 
					        //ProductCategory: "Pistols",
 | 
				
			||||||
        Amount: reward.Duration ? 1 : Math.round(scaleAmount(day, reward.Amount, reward.ScalingMultiplier)),
 | 
					        Amount: reward.Duration ? 1 : Math.round(scaleAmount(day, reward.Amount, reward.ScalingMultiplier)),
 | 
				
			||||||
        ScalingMultiplier: reward.ScalingMultiplier,
 | 
					        ScalingMultiplier: reward.ScalingMultiplier,
 | 
				
			||||||
 | 
				
			|||||||
@ -79,6 +79,7 @@ import { config } from "./configService";
 | 
				
			|||||||
import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json";
 | 
					import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json";
 | 
				
			||||||
import { ISyndicateMissionInfo } from "../types/worldStateTypes";
 | 
					import { ISyndicateMissionInfo } from "../types/worldStateTypes";
 | 
				
			||||||
import { fromOid } from "../helpers/inventoryHelpers";
 | 
					import { fromOid } from "../helpers/inventoryHelpers";
 | 
				
			||||||
 | 
					import { TAccountDocument } from "./loginService";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] => {
 | 
					const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] => {
 | 
				
			||||||
    // For Spy missions, e.g. 3 vaults cracked = A, B, C
 | 
					    // For Spy missions, e.g. 3 vaults cracked = A, B, C
 | 
				
			||||||
@ -135,6 +136,7 @@ const getRandomRewardByChance = (pool: IReward[], rng?: SRng): IRngResult | unde
 | 
				
			|||||||
//const knownUnhandledKeys: readonly string[] = ["test"] as const; // for unimplemented but important keys
 | 
					//const knownUnhandledKeys: readonly string[] = ["test"] as const; // for unimplemented but important keys
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const addMissionInventoryUpdates = async (
 | 
					export const addMissionInventoryUpdates = async (
 | 
				
			||||||
 | 
					    account: TAccountDocument,
 | 
				
			||||||
    inventory: TInventoryDatabaseDocument,
 | 
					    inventory: TInventoryDatabaseDocument,
 | 
				
			||||||
    inventoryUpdates: IMissionInventoryUpdateRequest
 | 
					    inventoryUpdates: IMissionInventoryUpdateRequest
 | 
				
			||||||
): Promise<IInventoryChanges> => {
 | 
					): Promise<IInventoryChanges> => {
 | 
				
			||||||
@ -287,7 +289,7 @@ export const addMissionInventoryUpdates = async (
 | 
				
			|||||||
                addRecipes(inventory, value);
 | 
					                addRecipes(inventory, value);
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            case "ChallengeProgress":
 | 
					            case "ChallengeProgress":
 | 
				
			||||||
                addChallenges(inventory, value, inventoryUpdates.SeasonChallengeCompletions);
 | 
					                addChallenges(account, inventory, value, inventoryUpdates.SeasonChallengeCompletions);
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            case "FusionTreasures":
 | 
					            case "FusionTreasures":
 | 
				
			||||||
                addFusionTreasures(inventory, value);
 | 
					                addFusionTreasures(inventory, value);
 | 
				
			||||||
@ -324,8 +326,8 @@ export const addMissionInventoryUpdates = async (
 | 
				
			|||||||
                break;
 | 
					                break;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            case "PlayerSkillGains": {
 | 
					            case "PlayerSkillGains": {
 | 
				
			||||||
                inventory.PlayerSkills.LPP_SPACE += value.LPP_SPACE;
 | 
					                inventory.PlayerSkills.LPP_SPACE += value.LPP_SPACE ?? 0;
 | 
				
			||||||
                inventory.PlayerSkills.LPP_DRIFTER += value.LPP_DRIFTER;
 | 
					                inventory.PlayerSkills.LPP_DRIFTER += value.LPP_DRIFTER ?? 0;
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            case "CustomMarkers": {
 | 
					            case "CustomMarkers": {
 | 
				
			||||||
@ -1180,14 +1182,12 @@ export const addMissionRewards = async (
 | 
				
			|||||||
            if (nodeIndex !== -1) inventory.Nemesis.InfNodes.splice(nodeIndex, 1);
 | 
					            if (nodeIndex !== -1) inventory.Nemesis.InfNodes.splice(nodeIndex, 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (inventory.Nemesis.InfNodes.length <= 0) {
 | 
					            if (inventory.Nemesis.InfNodes.length <= 0) {
 | 
				
			||||||
 | 
					                const manifest = getNemesisManifest(inventory.Nemesis.manifest);
 | 
				
			||||||
                if (inventory.Nemesis.Faction != "FC_INFESTATION") {
 | 
					                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;
 | 
					                    inventoryChanges.Nemesis.Rank = inventory.Nemesis.Rank;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                inventory.Nemesis.InfNodes = getInfNodes(
 | 
					                inventory.Nemesis.InfNodes = getInfNodes(manifest, inventory.Nemesis.Rank);
 | 
				
			||||||
                    getNemesisManifest(inventory.Nemesis.manifest),
 | 
					 | 
				
			||||||
                    inventory.Nemesis.Rank
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (inventory.Nemesis.Faction == "FC_INFESTATION") {
 | 
					            if (inventory.Nemesis.Faction == "FC_INFESTATION") {
 | 
				
			||||||
@ -1205,7 +1205,9 @@ export const addMissionRewards = async (
 | 
				
			|||||||
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
					        // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
				
			||||||
        const [jobType, unkIndex, hubNode, syndicateMissionId, locationTag] = rewardInfo.jobId.split("_");
 | 
					        const [jobType, unkIndex, hubNode, syndicateMissionId, locationTag] = rewardInfo.jobId.split("_");
 | 
				
			||||||
        const syndicateMissions: ISyndicateMissionInfo[] = [];
 | 
					        const syndicateMissions: ISyndicateMissionInfo[] = [];
 | 
				
			||||||
        pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId));
 | 
					        if (syndicateMissionId) {
 | 
				
			||||||
 | 
					            pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId);
 | 
					        const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId);
 | 
				
			||||||
        if (syndicateEntry && syndicateEntry.Jobs) {
 | 
					        if (syndicateEntry && syndicateEntry.Jobs) {
 | 
				
			||||||
            let currentJob = syndicateEntry.Jobs[rewardInfo.JobTier!];
 | 
					            let currentJob = syndicateEntry.Jobs[rewardInfo.JobTier!];
 | 
				
			||||||
@ -1554,7 +1556,9 @@ function getRandomMissionDrops(
 | 
				
			|||||||
                let isEndlessJob = false;
 | 
					                let isEndlessJob = false;
 | 
				
			||||||
                if (syndicateMissionId) {
 | 
					                if (syndicateMissionId) {
 | 
				
			||||||
                    const syndicateMissions: ISyndicateMissionInfo[] = [];
 | 
					                    const syndicateMissions: ISyndicateMissionInfo[] = [];
 | 
				
			||||||
                    pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId));
 | 
					                    if (syndicateMissionId) {
 | 
				
			||||||
 | 
					                        pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId));
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                    const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId);
 | 
					                    const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId);
 | 
				
			||||||
                    if (syndicateEntry && syndicateEntry.Jobs) {
 | 
					                    if (syndicateEntry && syndicateEntry.Jobs) {
 | 
				
			||||||
                        let job = syndicateEntry.Jobs[RewardInfo.JobTier!];
 | 
					                        let job = syndicateEntry.Jobs[RewardInfo.JobTier!];
 | 
				
			||||||
 | 
				
			|||||||
@ -27,7 +27,6 @@ import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSal
 | 
				
			|||||||
import Nova1999ConquestShopManifest from "@/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json";
 | 
					import Nova1999ConquestShopManifest from "@/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json";
 | 
				
			||||||
import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json";
 | 
					import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json";
 | 
				
			||||||
import OstronProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronProspectorVendorManifest.json";
 | 
					import OstronProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronProspectorVendorManifest.json";
 | 
				
			||||||
import RadioLegionIntermission12VendorManifest from "@/static/fixed_responses/getVendorInfo/RadioLegionIntermission12VendorManifest.json";
 | 
					 | 
				
			||||||
import SolarisDebtTokenVendorRepossessionsManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorRepossessionsManifest.json";
 | 
					import SolarisDebtTokenVendorRepossessionsManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorRepossessionsManifest.json";
 | 
				
			||||||
import SolarisProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisProspectorVendorManifest.json";
 | 
					import SolarisProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisProspectorVendorManifest.json";
 | 
				
			||||||
import Temple1999VendorManifest from "@/static/fixed_responses/getVendorInfo/Temple1999VendorManifest.json";
 | 
					import Temple1999VendorManifest from "@/static/fixed_responses/getVendorInfo/Temple1999VendorManifest.json";
 | 
				
			||||||
@ -54,7 +53,6 @@ const rawVendorManifests: IVendorManifest[] = [
 | 
				
			|||||||
    Nova1999ConquestShopManifest,
 | 
					    Nova1999ConquestShopManifest,
 | 
				
			||||||
    OstronPetVendorManifest,
 | 
					    OstronPetVendorManifest,
 | 
				
			||||||
    OstronProspectorVendorManifest,
 | 
					    OstronProspectorVendorManifest,
 | 
				
			||||||
    RadioLegionIntermission12VendorManifest,
 | 
					 | 
				
			||||||
    SolarisDebtTokenVendorRepossessionsManifest,
 | 
					    SolarisDebtTokenVendorRepossessionsManifest,
 | 
				
			||||||
    SolarisProspectorVendorManifest,
 | 
					    SolarisProspectorVendorManifest,
 | 
				
			||||||
    Temple1999VendorManifest,
 | 
					    Temple1999VendorManifest,
 | 
				
			||||||
@ -305,7 +303,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        const cycleStart = cycleOffset + cycleIndex * cycleDuration;
 | 
					        const cycleStart = cycleOffset + cycleIndex * cycleDuration;
 | 
				
			||||||
        for (const rawItem of offersToAdd) {
 | 
					        for (const rawItem of offersToAdd) {
 | 
				
			||||||
            const durationHoursRange = toRange(rawItem.durationHours);
 | 
					            const durationHoursRange = toRange(rawItem.durationHours ?? cycleDuration);
 | 
				
			||||||
            const expiry =
 | 
					            const expiry =
 | 
				
			||||||
                cycleStart +
 | 
					                cycleStart +
 | 
				
			||||||
                rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour;
 | 
					                rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour;
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										65
									
								
								src/services/webService.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/services/webService.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,65 @@
 | 
				
			|||||||
 | 
					import http from "http";
 | 
				
			||||||
 | 
					import https from "https";
 | 
				
			||||||
 | 
					import fs from "node:fs";
 | 
				
			||||||
 | 
					import { config } from "./configService";
 | 
				
			||||||
 | 
					import { logger } from "../utils/logger";
 | 
				
			||||||
 | 
					import { app } from "../app";
 | 
				
			||||||
 | 
					import { AddressInfo } from "node:net";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let httpServer: http.Server | undefined;
 | 
				
			||||||
 | 
					let httpsServer: https.Server | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const tlsOptions = {
 | 
				
			||||||
 | 
					    key: fs.readFileSync("static/certs/key.pem"),
 | 
				
			||||||
 | 
					    cert: fs.readFileSync("static/certs/cert.pem")
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const startWebServer = (): void => {
 | 
				
			||||||
 | 
					    const httpPort = config.httpPort || 80;
 | 
				
			||||||
 | 
					    const httpsPort = config.httpsPort || 443;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
				
			||||||
 | 
					    httpServer = http.createServer(app);
 | 
				
			||||||
 | 
					    httpServer.listen(httpPort, () => {
 | 
				
			||||||
 | 
					        logger.info("HTTP server started on port " + httpPort);
 | 
				
			||||||
 | 
					        // eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
				
			||||||
 | 
					        httpsServer = https.createServer(tlsOptions, app);
 | 
				
			||||||
 | 
					        httpsServer.listen(httpsPort, () => {
 | 
				
			||||||
 | 
					            logger.info("HTTPS server started on port " + httpsPort);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            logger.info(
 | 
				
			||||||
 | 
					                "Access the WebUI in your browser at http://localhost" + (httpPort == 80 ? "" : ":" + httpPort)
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getWebPorts = (): Record<"http" | "https", number | undefined> => {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        http: (httpServer?.address() as AddressInfo | undefined)?.port,
 | 
				
			||||||
 | 
					        https: (httpsServer?.address() as AddressInfo | undefined)?.port
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const stopWebServer = async (): Promise<void> => {
 | 
				
			||||||
 | 
					    const promises: Promise<void>[] = [];
 | 
				
			||||||
 | 
					    if (httpServer) {
 | 
				
			||||||
 | 
					        promises.push(
 | 
				
			||||||
 | 
					            new Promise(resolve => {
 | 
				
			||||||
 | 
					                httpServer!.close(() => {
 | 
				
			||||||
 | 
					                    resolve();
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (httpsServer) {
 | 
				
			||||||
 | 
					        promises.push(
 | 
				
			||||||
 | 
					            new Promise(resolve => {
 | 
				
			||||||
 | 
					                httpsServer!.close(() => {
 | 
				
			||||||
 | 
					                    resolve();
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    await Promise.all(promises);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -6,7 +6,7 @@ import { buildConfig } from "@/src/services/buildConfigService";
 | 
				
			|||||||
import { unixTimesInMs } from "@/src/constants/timeConstants";
 | 
					import { unixTimesInMs } from "@/src/constants/timeConstants";
 | 
				
			||||||
import { config } from "@/src/services/configService";
 | 
					import { config } from "@/src/services/configService";
 | 
				
			||||||
import { SRng } from "@/src/services/rngService";
 | 
					import { SRng } from "@/src/services/rngService";
 | 
				
			||||||
import { ExportNightwave, ExportRegions, IRegion } from "warframe-public-export-plus";
 | 
					import { ExportRegions, ExportSyndicates, IRegion } from "warframe-public-export-plus";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
    ICalendarDay,
 | 
					    ICalendarDay,
 | 
				
			||||||
    ICalendarEvent,
 | 
					    ICalendarEvent,
 | 
				
			||||||
@ -166,8 +166,8 @@ const microplanetEndlessJobs = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0
 | 
					const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const isBeforeNextExpectedWorldStateRefresh = (date: number): boolean => {
 | 
					const isBeforeNextExpectedWorldStateRefresh = (nowMs: number, thenMs: number): boolean => {
 | 
				
			||||||
    return Date.now() + 300_000 > date;
 | 
					    return nowMs + 300_000 > thenMs;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getSortieTime = (day: number): number => {
 | 
					const getSortieTime = (day: number): number => {
 | 
				
			||||||
@ -344,11 +344,32 @@ export const getSortie = (day: number): ISortie => {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const dailyChallenges = Object.keys(ExportNightwave.challenges).filter(x =>
 | 
					interface IRotatingSeasonChallengePools {
 | 
				
			||||||
    x.startsWith("/Lotus/Types/Challenges/Seasons/Daily/")
 | 
					    daily: string[];
 | 
				
			||||||
);
 | 
					    weekly: string[];
 | 
				
			||||||
 | 
					    hardWeekly: string[];
 | 
				
			||||||
 | 
					    hasWeeklyPermanent: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getSeasonDailyChallenge = (day: number): ISeasonChallenge => {
 | 
					const getSeasonChallengePools = (syndicateTag: string): IRotatingSeasonChallengePools => {
 | 
				
			||||||
 | 
					    const syndicate = ExportSyndicates[syndicateTag];
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        daily: syndicate.dailyChallenges!,
 | 
				
			||||||
 | 
					        weekly: syndicate.weeklyChallenges!.filter(
 | 
				
			||||||
 | 
					            x =>
 | 
				
			||||||
 | 
					                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/")
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        hasWeeklyPermanent: !!syndicate.weeklyChallenges!.find(x =>
 | 
				
			||||||
 | 
					            x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getSeasonDailyChallenge = (pools: IRotatingSeasonChallengePools, day: number): ISeasonChallenge => {
 | 
				
			||||||
    const dayStart = EPOCH + day * 86400000;
 | 
					    const dayStart = EPOCH + day * 86400000;
 | 
				
			||||||
    const dayEnd = EPOCH + (day + 3) * 86400000;
 | 
					    const dayEnd = EPOCH + (day + 3) * 86400000;
 | 
				
			||||||
    const rng = new SRng(new SRng(day).randomInt(0, 100_000));
 | 
					    const rng = new SRng(new SRng(day).randomInt(0, 100_000));
 | 
				
			||||||
@ -357,17 +378,11 @@ const getSeasonDailyChallenge = (day: number): ISeasonChallenge => {
 | 
				
			|||||||
        Daily: true,
 | 
					        Daily: true,
 | 
				
			||||||
        Activation: { $date: { $numberLong: dayStart.toString() } },
 | 
					        Activation: { $date: { $numberLong: dayStart.toString() } },
 | 
				
			||||||
        Expiry: { $date: { $numberLong: dayEnd.toString() } },
 | 
					        Expiry: { $date: { $numberLong: dayEnd.toString() } },
 | 
				
			||||||
        Challenge: rng.randomElement(dailyChallenges)!
 | 
					        Challenge: rng.randomElement(pools.daily)!
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const weeklyChallenges = Object.keys(ExportNightwave.challenges).filter(
 | 
					const getSeasonWeeklyChallenge = (pools: IRotatingSeasonChallengePools, week: number, id: number): ISeasonChallenge => {
 | 
				
			||||||
    x =>
 | 
					 | 
				
			||||||
        x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/") &&
 | 
					 | 
				
			||||||
        !x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const getSeasonWeeklyChallenge = (week: number, id: number): ISeasonChallenge => {
 | 
					 | 
				
			||||||
    const weekStart = EPOCH + week * 604800000;
 | 
					    const weekStart = EPOCH + week * 604800000;
 | 
				
			||||||
    const weekEnd = weekStart + 604800000;
 | 
					    const weekEnd = weekStart + 604800000;
 | 
				
			||||||
    const challengeId = week * 7 + id;
 | 
					    const challengeId = week * 7 + id;
 | 
				
			||||||
@ -376,15 +391,15 @@ const getSeasonWeeklyChallenge = (week: number, id: number): ISeasonChallenge =>
 | 
				
			|||||||
        _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") },
 | 
					        _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") },
 | 
				
			||||||
        Activation: { $date: { $numberLong: weekStart.toString() } },
 | 
					        Activation: { $date: { $numberLong: weekStart.toString() } },
 | 
				
			||||||
        Expiry: { $date: { $numberLong: weekEnd.toString() } },
 | 
					        Expiry: { $date: { $numberLong: weekEnd.toString() } },
 | 
				
			||||||
        Challenge: rng.randomElement(weeklyChallenges)!
 | 
					        Challenge: rng.randomElement(pools.weekly)!
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const weeklyHardChallenges = Object.keys(ExportNightwave.challenges).filter(x =>
 | 
					const getSeasonWeeklyHardChallenge = (
 | 
				
			||||||
    x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/")
 | 
					    pools: IRotatingSeasonChallengePools,
 | 
				
			||||||
);
 | 
					    week: number,
 | 
				
			||||||
 | 
					    id: number
 | 
				
			||||||
const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChallenge => {
 | 
					): ISeasonChallenge => {
 | 
				
			||||||
    const weekStart = EPOCH + week * 604800000;
 | 
					    const weekStart = EPOCH + week * 604800000;
 | 
				
			||||||
    const weekEnd = weekStart + 604800000;
 | 
					    const weekEnd = weekStart + 604800000;
 | 
				
			||||||
    const challengeId = week * 7 + id;
 | 
					    const challengeId = week * 7 + id;
 | 
				
			||||||
@ -393,36 +408,48 @@ const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChalleng
 | 
				
			|||||||
        _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") },
 | 
					        _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") },
 | 
				
			||||||
        Activation: { $date: { $numberLong: weekStart.toString() } },
 | 
					        Activation: { $date: { $numberLong: weekStart.toString() } },
 | 
				
			||||||
        Expiry: { $date: { $numberLong: weekEnd.toString() } },
 | 
					        Expiry: { $date: { $numberLong: weekEnd.toString() } },
 | 
				
			||||||
        Challenge: rng.randomElement(weeklyHardChallenges)!
 | 
					        Challenge: rng.randomElement(pools.hardWeekly)!
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const pushWeeklyActs = (worldState: IWorldState, week: number): void => {
 | 
					const pushWeeklyActs = (
 | 
				
			||||||
 | 
					    activeChallenges: ISeasonChallenge[],
 | 
				
			||||||
 | 
					    pools: IRotatingSeasonChallengePools,
 | 
				
			||||||
 | 
					    week: number
 | 
				
			||||||
 | 
					): void => {
 | 
				
			||||||
    const weekStart = EPOCH + week * 604800000;
 | 
					    const weekStart = EPOCH + week * 604800000;
 | 
				
			||||||
    const weekEnd = weekStart + 604800000;
 | 
					    const weekEnd = weekStart + 604800000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyChallenge(week, 0));
 | 
					    activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 0));
 | 
				
			||||||
    worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyChallenge(week, 1));
 | 
					    activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 1));
 | 
				
			||||||
    worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyHardChallenge(week, 2));
 | 
					    if (pools.hasWeeklyPermanent) {
 | 
				
			||||||
    worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyHardChallenge(week, 3));
 | 
					        activeChallenges.push({
 | 
				
			||||||
    worldState.SeasonInfo.ActiveChallenges.push({
 | 
					            _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") },
 | 
				
			||||||
        _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") },
 | 
					            Activation: { $date: { $numberLong: weekStart.toString() } },
 | 
				
			||||||
        Activation: { $date: { $numberLong: weekStart.toString() } },
 | 
					            Expiry: { $date: { $numberLong: weekEnd.toString() } },
 | 
				
			||||||
        Expiry: { $date: { $numberLong: weekEnd.toString() } },
 | 
					            Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions"
 | 
				
			||||||
        Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" + (week - 12)
 | 
					        });
 | 
				
			||||||
    });
 | 
					        activeChallenges.push({
 | 
				
			||||||
    worldState.SeasonInfo.ActiveChallenges.push({
 | 
					            _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") },
 | 
				
			||||||
        _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") },
 | 
					            Activation: { $date: { $numberLong: weekStart.toString() } },
 | 
				
			||||||
        Activation: { $date: { $numberLong: weekStart.toString() } },
 | 
					            Expiry: { $date: { $numberLong: weekEnd.toString() } },
 | 
				
			||||||
        Expiry: { $date: { $numberLong: weekEnd.toString() } },
 | 
					            Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus"
 | 
				
			||||||
        Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" + (week - 12)
 | 
					        });
 | 
				
			||||||
    });
 | 
					        activeChallenges.push({
 | 
				
			||||||
    worldState.SeasonInfo.ActiveChallenges.push({
 | 
					            _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") },
 | 
				
			||||||
        _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") },
 | 
					            Activation: { $date: { $numberLong: weekStart.toString() } },
 | 
				
			||||||
        Activation: { $date: { $numberLong: weekStart.toString() } },
 | 
					            Expiry: { $date: { $numberLong: weekEnd.toString() } },
 | 
				
			||||||
        Expiry: { $date: { $numberLong: weekEnd.toString() } },
 | 
					            Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies"
 | 
				
			||||||
        Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" + (week - 12)
 | 
					        });
 | 
				
			||||||
    });
 | 
					        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 => {
 | 
					export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[], bountyCycle: number): void => {
 | 
				
			||||||
@ -912,29 +939,22 @@ const getCalendarSeason = (week: number): ICalendarSeason => {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
					export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
				
			||||||
    const day = Math.trunc((Date.now() - EPOCH) / 86400000);
 | 
					    const timeSecs = config.worldState?.lockTime || Math.round(Date.now() / 1000);
 | 
				
			||||||
 | 
					    const timeMs = timeSecs * 1000;
 | 
				
			||||||
 | 
					    const day = Math.trunc((timeMs - EPOCH) / 86400000);
 | 
				
			||||||
    const week = Math.trunc(day / 7);
 | 
					    const week = Math.trunc(day / 7);
 | 
				
			||||||
    const weekStart = EPOCH + week * 604800000;
 | 
					    const weekStart = EPOCH + week * 604800000;
 | 
				
			||||||
    const weekEnd = weekStart + 604800000;
 | 
					    const weekEnd = weekStart + 604800000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const worldState: IWorldState = {
 | 
					    const worldState: IWorldState = {
 | 
				
			||||||
        BuildLabel: typeof buildLabel == "string" ? buildLabel.split(" ").join("+") : buildConfig.buildLabel,
 | 
					        BuildLabel: typeof buildLabel == "string" ? buildLabel.split(" ").join("+") : buildConfig.buildLabel,
 | 
				
			||||||
        Time: config.worldState?.lockTime || Math.round(Date.now() / 1000),
 | 
					        Time: timeSecs,
 | 
				
			||||||
        Goals: [],
 | 
					        Goals: [],
 | 
				
			||||||
        Alerts: [],
 | 
					        Alerts: [],
 | 
				
			||||||
        Sorties: [],
 | 
					        Sorties: [],
 | 
				
			||||||
        LiteSorties: [],
 | 
					        LiteSorties: [],
 | 
				
			||||||
        GlobalUpgrades: [],
 | 
					        GlobalUpgrades: [],
 | 
				
			||||||
        EndlessXpChoices: [],
 | 
					        EndlessXpChoices: [],
 | 
				
			||||||
        SeasonInfo: {
 | 
					 | 
				
			||||||
            Activation: { $date: { $numberLong: "1715796000000" } },
 | 
					 | 
				
			||||||
            Expiry: { $date: { $numberLong: "2000000000000" } },
 | 
					 | 
				
			||||||
            AffiliationTag: "RadioLegionIntermission12Syndicate",
 | 
					 | 
				
			||||||
            Season: 14,
 | 
					 | 
				
			||||||
            Phase: 0,
 | 
					 | 
				
			||||||
            Params: "",
 | 
					 | 
				
			||||||
            ActiveChallenges: []
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        KnownCalendarSeasons: [],
 | 
					        KnownCalendarSeasons: [],
 | 
				
			||||||
        ...staticWorldState,
 | 
					        ...staticWorldState,
 | 
				
			||||||
        SyndicateMissions: [...staticWorldState.SyndicateMissions]
 | 
					        SyndicateMissions: [...staticWorldState.SyndicateMissions]
 | 
				
			||||||
@ -967,17 +987,27 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Nightwave Challenges
 | 
					    // Nightwave Challenges
 | 
				
			||||||
    // Current nightwave season was introduced in 38.0.8 so omitting challenges before that to avoid UI bugs and even crashes on really old versions.
 | 
					    const nightwaveSyndicateTag = getNightwaveSyndicateTag(buildLabel);
 | 
				
			||||||
    if (!buildLabel || version_compare(buildLabel, "2025.02.05.11.19") >= 0) {
 | 
					    if (nightwaveSyndicateTag) {
 | 
				
			||||||
        worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 2));
 | 
					        worldState.SeasonInfo = {
 | 
				
			||||||
        worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 1));
 | 
					            Activation: { $date: { $numberLong: "1715796000000" } },
 | 
				
			||||||
        worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 0));
 | 
					            Expiry: { $date: { $numberLong: "2000000000000" } },
 | 
				
			||||||
        if (isBeforeNextExpectedWorldStateRefresh(EPOCH + (day + 1) * 86400000)) {
 | 
					            AffiliationTag: nightwaveSyndicateTag,
 | 
				
			||||||
            worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day + 1));
 | 
					            Season: nightwaveTagToSeason[nightwaveSyndicateTag],
 | 
				
			||||||
 | 
					            Phase: 0,
 | 
				
			||||||
 | 
					            Params: "",
 | 
				
			||||||
 | 
					            ActiveChallenges: []
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        const pools = getSeasonChallengePools(nightwaveSyndicateTag);
 | 
				
			||||||
 | 
					        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(timeMs, EPOCH + (day + 1) * 86400000)) {
 | 
				
			||||||
 | 
					            worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day + 1));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        pushWeeklyActs(worldState, week);
 | 
					        pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week);
 | 
				
			||||||
        if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) {
 | 
					        if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
 | 
				
			||||||
            pushWeeklyActs(worldState, week + 1);
 | 
					            pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week + 1);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -985,7 +1015,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
				
			|||||||
    worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = new SRng(week).randomInt(0, 0xff_ffff);
 | 
					    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
 | 
					    // 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;
 | 
					    let bountyCycleEnd: number | undefined;
 | 
				
			||||||
    do {
 | 
					    do {
 | 
				
			||||||
        const bountyCycleStart = bountyCycle * 9000000;
 | 
					        const bountyCycleStart = bountyCycle * 9000000;
 | 
				
			||||||
@ -1016,7 +1046,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        pushClassicBounties(worldState.SyndicateMissions, bountyCycle);
 | 
					        pushClassicBounties(worldState.SyndicateMissions, bountyCycle);
 | 
				
			||||||
    } while (isBeforeNextExpectedWorldStateRefresh(bountyCycleEnd) && ++bountyCycle);
 | 
					    } while (isBeforeNextExpectedWorldStateRefresh(timeMs, bountyCycleEnd) && ++bountyCycle);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (config.worldState?.creditBoost) {
 | 
					    if (config.worldState?.creditBoost) {
 | 
				
			||||||
        worldState.GlobalUpgrades.push({
 | 
					        worldState.GlobalUpgrades.push({
 | 
				
			||||||
@ -1059,15 +1089,15 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        const rollover = getSortieTime(day);
 | 
					        const rollover = getSortieTime(day);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (Date.now() < rollover) {
 | 
					        if (timeMs < rollover) {
 | 
				
			||||||
            worldState.Sorties.push(getSortie(day - 1));
 | 
					            worldState.Sorties.push(getSortie(day - 1));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (isBeforeNextExpectedWorldStateRefresh(rollover)) {
 | 
					        if (isBeforeNextExpectedWorldStateRefresh(timeMs, rollover)) {
 | 
				
			||||||
            worldState.Sorties.push(getSortie(day));
 | 
					            worldState.Sorties.push(getSortie(day));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // The client does not seem to respect activation for classic syndicate missions, so only pushing current ones.
 | 
					        // 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);
 | 
					        const rng = new SRng(sdy);
 | 
				
			||||||
        pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48049", "ArbitersSyndicate");
 | 
					        pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48049", "ArbitersSyndicate");
 | 
				
			||||||
        pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4804a", "CephalonSudaSyndicate");
 | 
					        pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4804a", "CephalonSudaSyndicate");
 | 
				
			||||||
@ -1079,7 +1109,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Archon Hunt cycling every week
 | 
					    // Archon Hunt cycling every week
 | 
				
			||||||
    worldState.LiteSorties.push(getLiteSortie(week));
 | 
					    worldState.LiteSorties.push(getLiteSortie(week));
 | 
				
			||||||
    if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) {
 | 
					    if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
 | 
				
			||||||
        worldState.LiteSorties.push(getLiteSortie(week + 1));
 | 
					        worldState.LiteSorties.push(getLiteSortie(week + 1));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1116,12 +1146,12 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // 1999 Calendar Season cycling every week + YearIteration every 4 weeks
 | 
					    // 1999 Calendar Season cycling every week + YearIteration every 4 weeks
 | 
				
			||||||
    worldState.KnownCalendarSeasons.push(getCalendarSeason(week));
 | 
					    worldState.KnownCalendarSeasons.push(getCalendarSeason(week));
 | 
				
			||||||
    if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) {
 | 
					    if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
 | 
				
			||||||
        worldState.KnownCalendarSeasons.push(getCalendarSeason(week + 1));
 | 
					        worldState.KnownCalendarSeasons.push(getCalendarSeason(week + 1));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Sentient Anomaly cycling every 30 minutes
 | 
					    // 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 = {
 | 
					    const tmp = {
 | 
				
			||||||
        cavabegin: "1690761600",
 | 
					        cavabegin: "1690761600",
 | 
				
			||||||
        PurchasePlatformLockEnabled: true,
 | 
					        PurchasePlatformLockEnabled: true,
 | 
				
			||||||
@ -1242,3 +1272,35 @@ export const isArchwingMission = (node: IRegion): boolean => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getNightwaveSyndicateTag = (buildLabel: string | undefined): string | undefined => {
 | 
				
			||||||
 | 
					    if (config.worldState?.nightwaveOverride) {
 | 
				
			||||||
 | 
					        return config.worldState.nightwaveOverride;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!buildLabel || version_compare(buildLabel, "2025.05.20.10.18") >= 0) {
 | 
				
			||||||
 | 
					        return "RadioLegionIntermission13Syndicate";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (version_compare(buildLabel, "2025.02.05.11.19") >= 0) {
 | 
				
			||||||
 | 
					        return "RadioLegionIntermission12Syndicate";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return undefined;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const nightwaveTagToSeason: Record<string, number> = {
 | 
				
			||||||
 | 
					    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 {
 | 
					export enum Status {
 | 
				
			||||||
    StatusAvailable = "STATUS_AVAILABLE",
 | 
					    StatusAvailable = "STATUS_AVAILABLE",
 | 
				
			||||||
    StatusStasis = "STATUS_STASIS"
 | 
					    StatusStasis = "STATUS_STASIS",
 | 
				
			||||||
 | 
					    StatusIncubating = "STATUS_INCUBATING"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ILastSortieRewardClient {
 | 
					export interface ILastSortieRewardClient {
 | 
				
			||||||
@ -929,10 +930,14 @@ export interface IPendingRecipeDatabase {
 | 
				
			|||||||
    Pistols?: IEquipmentDatabase[];
 | 
					    Pistols?: IEquipmentDatabase[];
 | 
				
			||||||
    Melee?: IEquipmentDatabase[];
 | 
					    Melee?: IEquipmentDatabase[];
 | 
				
			||||||
    SuitToUnbrand?: Types.ObjectId;
 | 
					    SuitToUnbrand?: Types.ObjectId;
 | 
				
			||||||
 | 
					    KubrowPet?: Types.ObjectId;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IPendingRecipeClient
 | 
					export interface IPendingRecipeClient
 | 
				
			||||||
    extends Omit<IPendingRecipeDatabase, "CompletionDate" | "LongGuns" | "Pistols" | "Melee" | "SuitToUnbrand"> {
 | 
					    extends Omit<
 | 
				
			||||||
 | 
					        IPendingRecipeDatabase,
 | 
				
			||||||
 | 
					        "CompletionDate" | "LongGuns" | "Pistols" | "Melee" | "SuitToUnbrand" | "KubrowPet"
 | 
				
			||||||
 | 
					    > {
 | 
				
			||||||
    CompletionDate: IMongoDate;
 | 
					    CompletionDate: IMongoDate;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -96,7 +96,7 @@ export type IMissionInventoryUpdateRequest = {
 | 
				
			|||||||
    FpsSamples: number;
 | 
					    FpsSamples: number;
 | 
				
			||||||
    EvolutionProgress?: IEvolutionProgress[];
 | 
					    EvolutionProgress?: IEvolutionProgress[];
 | 
				
			||||||
    FocusXpIncreases?: number[];
 | 
					    FocusXpIncreases?: number[];
 | 
				
			||||||
    PlayerSkillGains: IPlayerSkills;
 | 
					    PlayerSkillGains: Partial<IPlayerSkills>;
 | 
				
			||||||
    CustomMarkers?: ICustomMarkers[];
 | 
					    CustomMarkers?: ICustomMarkers[];
 | 
				
			||||||
    LoreFragmentScans?: ILoreFragmentScan[];
 | 
					    LoreFragmentScans?: ILoreFragmentScan[];
 | 
				
			||||||
    VoidTearParticipantsCurrWave?: {
 | 
					    VoidTearParticipantsCurrWave?: {
 | 
				
			||||||
 | 
				
			|||||||
@ -14,7 +14,7 @@ export interface IWorldState {
 | 
				
			|||||||
    NodeOverrides: INodeOverride[];
 | 
					    NodeOverrides: INodeOverride[];
 | 
				
			||||||
    PVPChallengeInstances: IPVPChallengeInstance[];
 | 
					    PVPChallengeInstances: IPVPChallengeInstance[];
 | 
				
			||||||
    EndlessXpChoices: IEndlessXpChoice[];
 | 
					    EndlessXpChoices: IEndlessXpChoice[];
 | 
				
			||||||
    SeasonInfo: {
 | 
					    SeasonInfo?: {
 | 
				
			||||||
        Activation: IMongoDate;
 | 
					        Activation: IMongoDate;
 | 
				
			||||||
        Expiry: IMongoDate;
 | 
					        Expiry: IMongoDate;
 | 
				
			||||||
        AffiliationTag: string;
 | 
					        AffiliationTag: string;
 | 
				
			||||||
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -312,7 +312,7 @@ function fetchItemList() {
 | 
				
			|||||||
                        document.getElementById("changeSyndicate").appendChild(option);
 | 
					                        document.getElementById("changeSyndicate").appendChild(option);
 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    const nameSet = new Set();
 | 
					                    const nameToItems = {};
 | 
				
			||||||
                    items.forEach(item => {
 | 
					                    items.forEach(item => {
 | 
				
			||||||
                        item.name = item.name.replace(/<.+>/g, "").trim();
 | 
					                        item.name = item.name.replace(/<.+>/g, "").trim();
 | 
				
			||||||
                        if ("badReason" in item) {
 | 
					                        if ("badReason" in item) {
 | 
				
			||||||
@ -322,6 +322,11 @@ function fetchItemList() {
 | 
				
			|||||||
                                item.name += " " + loc("code_badItem");
 | 
					                                item.name += " " + loc("code_badItem");
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					                        nameToItems[item.name] ??= [];
 | 
				
			||||||
 | 
					                        nameToItems[item.name].push(item);
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    items.forEach(item => {
 | 
				
			||||||
                        if (type == "ModularParts") {
 | 
					                        if (type == "ModularParts") {
 | 
				
			||||||
                            const supportedModularParts = [
 | 
					                            const supportedModularParts = [
 | 
				
			||||||
                                "LWPT_HB_DECK",
 | 
					                                "LWPT_HB_DECK",
 | 
				
			||||||
@ -360,15 +365,26 @@ function fetchItemList() {
 | 
				
			|||||||
                                    .appendChild(option);
 | 
					                                    .appendChild(option);
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        } else if (item.badReason != "notraw") {
 | 
					                        } else if (item.badReason != "notraw") {
 | 
				
			||||||
                            if (nameSet.has(item.name)) {
 | 
					                            const ambiguous = nameToItems[item.name].length > 1;
 | 
				
			||||||
                                //console.log(`Not adding ${item.uniqueName} to datalist for ${type} due to duplicate display name: ${item.name}`);
 | 
					                            let canDisambiguate = true;
 | 
				
			||||||
                            } else {
 | 
					                            if (ambiguous) {
 | 
				
			||||||
                                nameSet.add(item.name);
 | 
					                                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");
 | 
					                                const option = document.createElement("option");
 | 
				
			||||||
                                option.setAttribute("data-key", item.uniqueName);
 | 
					                                option.setAttribute("data-key", item.uniqueName);
 | 
				
			||||||
                                option.value = item.name;
 | 
					                                option.value = item.name;
 | 
				
			||||||
 | 
					                                if (ambiguous && canDisambiguate) {
 | 
				
			||||||
 | 
					                                    option.value += " (" + item.subtype + ")";
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
                                document.getElementById("datalist-" + type).appendChild(option);
 | 
					                                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 };
 | 
					                        itemMap[item.uniqueName] = { ...item, type };
 | 
				
			||||||
@ -476,7 +492,7 @@ function updateInventory() {
 | 
				
			|||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        let anyExaltedMissingXP = false;
 | 
					                        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) {
 | 
					                            for (const exaltedType of itemMap[item.ItemType].exalted) {
 | 
				
			||||||
                                const exaltedItem = data.SpecialItems.find(x => x.ItemType == exaltedType);
 | 
					                                const exaltedItem = data.SpecialItems.find(x => x.ItemType == exaltedType);
 | 
				
			||||||
                                if (exaltedItem) {
 | 
					                                if (exaltedItem) {
 | 
				
			||||||
 | 
				
			|||||||
@ -151,7 +151,7 @@ dict = {
 | 
				
			|||||||
    cheats_noVendorPurchaseLimits: `Sin límite de compras de vendedores`,
 | 
					    cheats_noVendorPurchaseLimits: `Sin límite de compras de vendedores`,
 | 
				
			||||||
    cheats_noDeathMarks: `Sin marcas de muerte`,
 | 
					    cheats_noDeathMarks: `Sin marcas de muerte`,
 | 
				
			||||||
    cheats_noKimCooldowns: `Sin tiempo de espera para conversaciones KIM`,
 | 
					    cheats_noKimCooldowns: `Sin tiempo de espera para conversaciones KIM`,
 | 
				
			||||||
    cheats_syndicateMissionsRepeatable: `[UNTRANSLATED] Syndicate Missions Repeatable`,
 | 
					    cheats_syndicateMissionsRepeatable: `Misiones de sindicato rejugables`,
 | 
				
			||||||
    cheats_instantFinishRivenChallenge: `Terminar desafío de agrietado inmediatamente`,
 | 
					    cheats_instantFinishRivenChallenge: `Terminar desafío de agrietado inmediatamente`,
 | 
				
			||||||
    cheats_instantResourceExtractorDrones: `Drones de extracción de recursos instantáneos`,
 | 
					    cheats_instantResourceExtractorDrones: `Drones de extracción de recursos instantáneos`,
 | 
				
			||||||
    cheats_noResourceExtractorDronesDamage: `Sin daño a los drones extractores de recursos`,
 | 
					    cheats_noResourceExtractorDronesDamage: `Sin daño a los drones extractores de recursos`,
 | 
				
			||||||
 | 
				
			|||||||
@ -34,8 +34,8 @@ dict = {
 | 
				
			|||||||
    code_rerollsNumber: `Nombre de rerolls`,
 | 
					    code_rerollsNumber: `Nombre de rerolls`,
 | 
				
			||||||
    code_viewStats: `Voir les stats`,
 | 
					    code_viewStats: `Voir les stats`,
 | 
				
			||||||
    code_rank: `Rang`,
 | 
					    code_rank: `Rang`,
 | 
				
			||||||
    code_rankUp: `[UNTRANSLATED] Rank up`,
 | 
					    code_rankUp: `Monter de rang`,
 | 
				
			||||||
    code_rankDown: `[UNTRANSLATED] Rank down`,
 | 
					    code_rankDown: `Baisser de rang`,
 | 
				
			||||||
    code_count: `Quantité`,
 | 
					    code_count: `Quantité`,
 | 
				
			||||||
    code_focusAllUnlocked: `Les écoles de Focus sont déjà déverrouillées.`,
 | 
					    code_focusAllUnlocked: `Les écoles de Focus sont déjà déverrouillées.`,
 | 
				
			||||||
    code_focusUnlocked: `|COUNT| écoles de Focus déverrouillées ! Synchronisation de l'inventaire nécessaire.`,
 | 
					    code_focusUnlocked: `|COUNT| écoles de Focus déverrouillées ! Synchronisation de l'inventaire nécessaire.`,
 | 
				
			||||||
@ -59,7 +59,7 @@ dict = {
 | 
				
			|||||||
    login_emailLabel: `Email`,
 | 
					    login_emailLabel: `Email`,
 | 
				
			||||||
    login_passwordLabel: `Mot de passe`,
 | 
					    login_passwordLabel: `Mot de passe`,
 | 
				
			||||||
    login_loginButton: `Connexion`,
 | 
					    login_loginButton: `Connexion`,
 | 
				
			||||||
    login_registerButton: `[UNTRANSLATED] Register`,
 | 
					    login_registerButton: `S'enregistrer`,
 | 
				
			||||||
    navbar_logout: `Déconnexion`,
 | 
					    navbar_logout: `Déconnexion`,
 | 
				
			||||||
    navbar_renameAccount: `Renommer le compte`,
 | 
					    navbar_renameAccount: `Renommer le compte`,
 | 
				
			||||||
    navbar_deleteAccount: `Supprimer le compte`,
 | 
					    navbar_deleteAccount: `Supprimer le compte`,
 | 
				
			||||||
@ -83,21 +83,21 @@ dict = {
 | 
				
			|||||||
    inventory_hoverboards: `K-Drives`,
 | 
					    inventory_hoverboards: `K-Drives`,
 | 
				
			||||||
    inventory_moaPets: `Moas`,
 | 
					    inventory_moaPets: `Moas`,
 | 
				
			||||||
    inventory_kubrowPets: `Bêtes`,
 | 
					    inventory_kubrowPets: `Bêtes`,
 | 
				
			||||||
    inventory_evolutionProgress: `[UNTRANSLATED] Incarnon Evolution Progress`,
 | 
					    inventory_evolutionProgress: `Progrès de l'évolution Incarnon`,
 | 
				
			||||||
    inventory_bulkAddSuits: `Ajouter les Warframes manquantes`,
 | 
					    inventory_bulkAddSuits: `Ajouter les Warframes manquantes`,
 | 
				
			||||||
    inventory_bulkAddWeapons: `Ajouter les armes manquantes`,
 | 
					    inventory_bulkAddWeapons: `Ajouter les armes manquantes`,
 | 
				
			||||||
    inventory_bulkAddSpaceSuits: `Ajouter les Archwings manquants`,
 | 
					    inventory_bulkAddSpaceSuits: `Ajouter les Archwings manquants`,
 | 
				
			||||||
    inventory_bulkAddSpaceWeapons: `Ajouter les armes d'Archwing manquantes`,
 | 
					    inventory_bulkAddSpaceWeapons: `Ajouter les armes d'Archwing manquantes`,
 | 
				
			||||||
    inventory_bulkAddSentinels: `Ajouter les Sentinelles manquantes`,
 | 
					    inventory_bulkAddSentinels: `Ajouter les Sentinelles manquantes`,
 | 
				
			||||||
    inventory_bulkAddSentinelWeapons: `Ajouter les armes de Sentinelles manquantes`,
 | 
					    inventory_bulkAddSentinelWeapons: `Ajouter les armes de Sentinelles manquantes`,
 | 
				
			||||||
    inventory_bulkAddEvolutionProgress: `[UNTRANSLATED] Add Missing Incarnon Evolution Progress`,
 | 
					    inventory_bulkAddEvolutionProgress: `Ajouter les évolutions Incarnon manquantes`,
 | 
				
			||||||
    inventory_bulkRankUpSuits: `Toutes les Warframes rang max`,
 | 
					    inventory_bulkRankUpSuits: `Toutes les Warframes au rang max`,
 | 
				
			||||||
    inventory_bulkRankUpWeapons: `Toutes les armes rang max`,
 | 
					    inventory_bulkRankUpWeapons: `Toutes les armes au rang max`,
 | 
				
			||||||
    inventory_bulkRankUpSpaceSuits: `Tous les Archwings rang max`,
 | 
					    inventory_bulkRankUpSpaceSuits: `Tous les Archwings au rang max`,
 | 
				
			||||||
    inventory_bulkRankUpSpaceWeapons: `Toutes les armes d'Archwing rang max`,
 | 
					    inventory_bulkRankUpSpaceWeapons: `Toutes les armes d'Archwing au rang max`,
 | 
				
			||||||
    inventory_bulkRankUpSentinels: `Toutes les Sentinelles rang max`,
 | 
					    inventory_bulkRankUpSentinels: `Toutes les Sentinelles au rang max`,
 | 
				
			||||||
    inventory_bulkRankUpSentinelWeapons: `Toutes les armes de Sentinelles rang max`,
 | 
					    inventory_bulkRankUpSentinelWeapons: `Toutes les armes de Sentinelles au rang max`,
 | 
				
			||||||
    inventory_bulkRankUpEvolutionProgress: `[UNTRANSLATED] Max Rank All Incarnon Evolution Progress`,
 | 
					    inventory_bulkRankUpEvolutionProgress: `Toutes les évolutions Incarnon au rang max`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    quests_list: `Quêtes`,
 | 
					    quests_list: `Quêtes`,
 | 
				
			||||||
    quests_completeAll: `Compléter toutes les quêtes`,
 | 
					    quests_completeAll: `Compléter toutes les quêtes`,
 | 
				
			||||||
@ -117,9 +117,9 @@ dict = {
 | 
				
			|||||||
    mods_fingerprintHelp: `Besoin d'aide pour l'empreinte ?`,
 | 
					    mods_fingerprintHelp: `Besoin d'aide pour l'empreinte ?`,
 | 
				
			||||||
    mods_rivens: `Rivens`,
 | 
					    mods_rivens: `Rivens`,
 | 
				
			||||||
    mods_mods: `Mods`,
 | 
					    mods_mods: `Mods`,
 | 
				
			||||||
    mods_addMissingUnrankedMods: `[UNTRANSLATED] Add Missing Unranked Mods`,
 | 
					    mods_addMissingUnrankedMods: `Ajouter les mods sans rang manquants`,
 | 
				
			||||||
    mods_removeUnranked: `[UNTRANSLATED] Remove Unranked Mods`,
 | 
					    mods_removeUnranked: `Retirer les mods sans rang`,
 | 
				
			||||||
    mods_addMissingMaxRankMods: `[UNTRANSLATED] Add Missing Max Rank Mods`,
 | 
					    mods_addMissingMaxRankMods: `Ajouter les mods niveau max manquants`,
 | 
				
			||||||
    cheats_administratorRequirement: `Rôle d'administrateur requis pour cette fonctionnalité. Ajoutez <code>|DISPLAYNAME|</code> à la ligne <code>administratorNames</code> dans le fichier config.json.`,
 | 
					    cheats_administratorRequirement: `Rôle d'administrateur requis pour cette fonctionnalité. Ajoutez <code>|DISPLAYNAME|</code> à la ligne <code>administratorNames</code> dans le fichier config.json.`,
 | 
				
			||||||
    cheats_server: `Serveur`,
 | 
					    cheats_server: `Serveur`,
 | 
				
			||||||
    cheats_skipTutorial: `Passer le tutoriel`,
 | 
					    cheats_skipTutorial: `Passer le tutoriel`,
 | 
				
			||||||
@ -131,9 +131,9 @@ dict = {
 | 
				
			|||||||
    cheats_infiniteEndo: `Endo infini`,
 | 
					    cheats_infiniteEndo: `Endo infini`,
 | 
				
			||||||
    cheats_infiniteRegalAya: `Aya Raffiné infini`,
 | 
					    cheats_infiniteRegalAya: `Aya Raffiné infini`,
 | 
				
			||||||
    cheats_infiniteHelminthMaterials: `Ressources d'Helminth infinies`,
 | 
					    cheats_infiniteHelminthMaterials: `Ressources d'Helminth infinies`,
 | 
				
			||||||
    cheats_claimingBlueprintRefundsIngredients: `[UNTRANSLATED] Claiming Blueprint Refunds Ingredients`,
 | 
					    cheats_claimingBlueprintRefundsIngredients: `Récupérer les items rend les ressources`,
 | 
				
			||||||
    cheats_dontSubtractVoidTraces: `[UNTRANSLATED] Don't Subtract Void Traces`,
 | 
					    cheats_dontSubtractVoidTraces: `Ne pas consommer de Void Traces`,
 | 
				
			||||||
    cheats_dontSubtractConsumables: `[UNTRANSLATED] Don't Subtract Consumables`,
 | 
					    cheats_dontSubtractConsumables: `Ne pas retirer de consommables`,
 | 
				
			||||||
    cheats_unlockAllShipFeatures: `Débloquer tous les segments du vaisseau`,
 | 
					    cheats_unlockAllShipFeatures: `Débloquer tous les segments du vaisseau`,
 | 
				
			||||||
    cheats_unlockAllShipDecorations: `Débloquer toutes les décorations du vaisseau`,
 | 
					    cheats_unlockAllShipDecorations: `Débloquer toutes les décorations du vaisseau`,
 | 
				
			||||||
    cheats_unlockAllFlavourItems: `Débloquer tous les <abbr title=\"Animations, Glyphes, Palettes, etc.\">Flavor Items</abbr>`,
 | 
					    cheats_unlockAllFlavourItems: `Débloquer tous les <abbr title=\"Animations, Glyphes, Palettes, etc.\">Flavor Items</abbr>`,
 | 
				
			||||||
@ -151,11 +151,11 @@ dict = {
 | 
				
			|||||||
    cheats_noVendorPurchaseLimits: `Aucune limite d'achat chez les PNJ`,
 | 
					    cheats_noVendorPurchaseLimits: `Aucune limite d'achat chez les PNJ`,
 | 
				
			||||||
    cheats_noDeathMarks: `Aucune marque d'assassin`,
 | 
					    cheats_noDeathMarks: `Aucune marque d'assassin`,
 | 
				
			||||||
    cheats_noKimCooldowns: `Aucun cooldown sur le KIM`,
 | 
					    cheats_noKimCooldowns: `Aucun cooldown sur le KIM`,
 | 
				
			||||||
    cheats_syndicateMissionsRepeatable: `[UNTRANSLATED] Syndicate Missions Repeatable`,
 | 
					    cheats_syndicateMissionsRepeatable: `Mission syndicat répétables`,
 | 
				
			||||||
    cheats_instantFinishRivenChallenge: `[UNTRANSLATED] Instant Finish Riven Challenge`,
 | 
					    cheats_instantFinishRivenChallenge: `Débloquer le challenge Riven instantanément`,
 | 
				
			||||||
    cheats_instantResourceExtractorDrones: `Ressources de drones d'extraction instantannées`,
 | 
					    cheats_instantResourceExtractorDrones: `Ressources de drones d'extraction instantannées`,
 | 
				
			||||||
    cheats_noResourceExtractorDronesDamage: `Aucun dégâts aux drones d'extraction de resources`,
 | 
					    cheats_noResourceExtractorDronesDamage: `Aucun dégâts aux drones d'extraction de resources`,
 | 
				
			||||||
    cheats_skipClanKeyCrafting: `[UNTRANSLATED] Skip Clan Key Crafting`,
 | 
					    cheats_skipClanKeyCrafting: `Passer le craft de la clé de clan`,
 | 
				
			||||||
    cheats_noDojoRoomBuildStage: `Aucune attente (construction des salles)`,
 | 
					    cheats_noDojoRoomBuildStage: `Aucune attente (construction des salles)`,
 | 
				
			||||||
    cheats_noDojoDecoBuildStage: `Aucune attente (construction des décorations)`,
 | 
					    cheats_noDojoDecoBuildStage: `Aucune attente (construction des décorations)`,
 | 
				
			||||||
    cheats_fastDojoRoomDestruction: `Destruction de salle instantanée (Dojo)`,
 | 
					    cheats_fastDojoRoomDestruction: `Destruction de salle instantanée (Dojo)`,
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user