diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..29d9043a --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,19 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +language: "en-US" +early_access: false +reviews: + profile: "chill" + request_changes_workflow: false + changed_files_summary: false + high_level_summary: false + poem: false + review_status: true + commit_status: false + collapse_walkthrough: false + sequence_diagrams: false + related_prs: false + auto_review: + enabled: true + drafts: false +chat: + auto_reply: true diff --git a/.eslintrc b/.eslintrc index 6a62f9a7..c7994fc1 100644 --- a/.eslintrc +++ b/.eslintrc @@ -15,17 +15,16 @@ "@typescript-eslint/restrict-template-expressions": "warn", "@typescript-eslint/restrict-plus-operands": "warn", "@typescript-eslint/no-unsafe-member-access": "warn", - "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], - "@typescript-eslint/no-misused-promises": "warn", + "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "caughtErrors": "none" }], "@typescript-eslint/no-unsafe-argument": "error", "@typescript-eslint/no-unsafe-call": "warn", "@typescript-eslint/no-unsafe-assignment": "warn", "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/no-loss-of-precision": "warn", + "no-loss-of-precision": "warn", "@typescript-eslint/no-unnecessary-condition": "warn", + "@typescript-eslint/no-base-to-string": "off", "no-case-declarations": "error", "prettier/prettier": "error", - "@typescript-eslint/semi": "error", "no-mixed-spaces-and-tabs": "error", "require-await": "off", "@typescript-eslint/require-await": "error" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ec5e2082..5decfe6d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,5 +17,5 @@ jobs: node-version: ${{ matrix.version }} - run: npm ci - run: cp config.json.example config.json - - run: npm run build + - run: npm run verify - run: npm run lint diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 750a593c..55626376 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -5,6 +5,7 @@ on: - main jobs: docker: + if: github.repository == 'OpenWF/SpaceNinjaServer' runs-on: ubuntu-latest steps: - name: Set up Docker buildx diff --git a/.prettierignore b/.prettierignore index ab38eac9..8929f3d1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,4 @@ +src/routes/api.ts static/webui/libs/ *.html +*.md diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..897af65d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint" + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index f265957f..8913def2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,8 +10,6 @@ ENV APP_SKIP_TUTORIAL=true ENV APP_SKIP_ALL_DIALOGUE=true ENV APP_UNLOCK_ALL_SCANS=true ENV APP_UNLOCK_ALL_MISSIONS=true -ENV APP_UNLOCK_ALL_QUESTS=true -ENV APP_COMPLETE_ALL_QUESTS=true ENV APP_INFINITE_RESOURCES=true ENV APP_UNLOCK_ALL_SHIP_FEATURES=true ENV APP_UNLOCK_ALL_SHIP_DECORATIONS=true diff --git a/README.md b/README.md index 4dcab407..ef091058 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,14 @@ More information for the moment here: [https://discord.gg/PNNZ3asUuY](https://discord.gg/PNNZ3asUuY) +## Project Status + +This project is in active development at . + +To get an idea of what functionality you can expect to be missing [have a look through the issues](https://onlyg.it/OpenWF/SpaceNinjaServer/issues?q=&type=all&state=open&labels=-4%2C-10&milestone=0&assignee=0&poster=). However, many things have been implemented and *should* work as expected. Please open an issue for anything where that's not the case and/or the server is reporting errors. + ## config.json - `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`. - `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`. +- `worldState.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE. diff --git a/UPDATE AND START SERVER.bat b/UPDATE AND START SERVER.bat index a46472ed..d0937399 100644 --- a/UPDATE AND START SERVER.bat +++ b/UPDATE AND START SERVER.bat @@ -3,6 +3,7 @@ echo Updating SpaceNinjaServer... git config remote.origin.url https://openwf.io/SpaceNinjaServer.git git fetch --prune +git stash git reset --hard origin/main if exist static\data\0\ ( @@ -13,12 +14,13 @@ if exist static\data\0\ ( ) echo Updating dependencies... -call npm i +call npm i --omit=dev call npm run build -call npm run start - -echo SpaceNinjaServer seems to have crashed. +if %errorlevel% == 0 ( + call npm run start + echo SpaceNinjaServer seems to have crashed. +) :a pause > nul goto a diff --git a/config.json.example b/config.json.example index 1bd3b1c1..4e52c750 100644 --- a/config.json.example +++ b/config.json.example @@ -5,31 +5,45 @@ "level": "trace" }, "myAddress": "localhost", - "hubAddress": "https://localhost/api/", - "platformCDNs": ["https://localhost/"], - "NRS": ["localhost"], "httpPort": 80, "httpsPort": 443, + "NRS": ["localhost"], "administratorNames": [], "autoCreateAccount": true, - "skipTutorial": true, - "skipAllDialogue": true, - "unlockAllScans": true, - "unlockAllMissions": true, - "infiniteCredits": true, - "infinitePlatinum": true, - "infiniteEndo": true, - "infiniteRegalAya": true, + "skipTutorial": false, + "skipAllDialogue": false, + "unlockAllScans": false, + "unlockAllMissions": false, + "infiniteCredits": false, + "infinitePlatinum": false, + "infiniteEndo": false, + "infiniteRegalAya": false, "infiniteHelminthMaterials": false, - "unlockAllShipFeatures": true, - "unlockAllShipDecorations": true, - "unlockAllFlavourItems": true, - "unlockAllSkins": true, - "unlockAllCapturaScenes": true, - "universalPolarityEverywhere": true, - "unlockDoubleCapacityPotatoesEverywhere": true, - "unlockExilusEverywhere": true, - "unlockArcanesEverywhere": true, - "noDailyStandingLimits": true, - "spoofMasteryRank": -1 + "unlockAllShipFeatures": false, + "unlockAllShipDecorations": false, + "unlockAllFlavourItems": false, + "unlockAllSkins": false, + "unlockAllCapturaScenes": false, + "universalPolarityEverywhere": false, + "unlockDoubleCapacityPotatoesEverywhere": false, + "unlockExilusEverywhere": false, + "unlockArcanesEverywhere": false, + "noDailyStandingLimits": false, + "noArgonCrystalDecay": false, + "noMasteryRankUpCooldown": false, + "noVendorPurchaseLimits": true, + "instantResourceExtractorDrones": false, + "noDojoRoomBuildStage": false, + "fastDojoRoomDestruction": false, + "noDojoResearchCosts": false, + "noDojoResearchTime": false, + "fastClanAscension": false, + "spoofMasteryRank": -1, + "worldState": { + "creditBoost": false, + "affinityBoost": false, + "resourceBoost": false, + "starDays": true, + "lockTime": 0 + } } diff --git a/package-lock.json b/package-lock.json index 478f3d4e..c1fe1acd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,27 +9,29 @@ "version": "0.1.0", "license": "GNU", "dependencies": { - "copyfiles": "^2.4.1", + "@types/express": "^5", + "@types/morgan": "^1.9.9", + "crc-32": "^1.2.2", "express": "^5", - "mongoose": "^8.9.4", - "warframe-public-export-plus": "^0.5.36", + "json-with-bigint": "^3.2.2", + "mongoose": "^8.11.0", + "morgan": "^1.10.0", + "ncp": "^2.0.0", + "typescript": ">=5.5 <5.6.0", + "warframe-public-export-plus": "^0.5.52", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { - "@types/express": "^5", - "@types/morgan": "^1.9.9", - "@typescript-eslint/eslint-plugin": "^7.18", - "@typescript-eslint/parser": "^7.18", - "eslint": "^8.56.0", - "eslint-plugin-prettier": "^5.2.3", - "morgan": "^1.10.0", - "prettier": "^3.4.2", - "ts-node": "^10.9.2", + "@rxliuli/tsgo": "^2025.3.31", + "@typescript-eslint/eslint-plugin": "^8.28.0", + "@typescript-eslint/parser": "^8.28.0", + "eslint": "^8", + "eslint-plugin-prettier": "^5.2.5", + "prettier": "^3.5.3", "ts-node-dev": "^2.0.0", - "tsconfig-paths": "^4.2.0", - "typescript": ">=4.7.4 <5.6.0" + "tsconfig-paths": "^4.2.0" }, "engines": { "node": ">=18.15.0", @@ -70,9 +72,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.0.tgz", + "integrity": "sha512-RoV8Xs9eNwiDvhv7M+xcL4PWyRyIXRY/FLp3buU4h1EYfdF7unWUy3dOjPqb3C7rMUewIcqwW850PgS8h1o1yg==", "dev": true, "license": "MIT", "dependencies": { @@ -247,9 +249,9 @@ } }, "node_modules/@mongodb-js/saslprep": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", - "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.0.tgz", + "integrity": "sha512-+ywrb0AqkfaYuhHs6LxKWgqbh3I72EpEgESCw37o+9qPx9WTCkgDm2B+eMrwehGtHBWHFU4GXvnSCNiFhhausg==", "license": "MIT", "dependencies": { "sparse-bitfield": "^3.0.3" @@ -294,9 +296,9 @@ } }, "node_modules/@pkgr/core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", - "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.0.tgz", + "integrity": "sha512-vsJDAkYR6qCPu+ioGScGiMYR7LvZYIXh/dlQeviqoTWNCVfKTLYD/LkNWH4Mxsv2a5vpIRc77FN5DnmK1eBggQ==", "dev": true, "license": "MIT", "engines": { @@ -306,6 +308,32 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@rxliuli/tsgo": { + "version": "2025.3.31", + "resolved": "https://registry.npmjs.org/@rxliuli/tsgo/-/tsgo-2025.3.31.tgz", + "integrity": "sha512-jEistRy/+Mu79rDv/Q8xn2yIM56WF3rfQOkwrbtivumij5HBVTfY4W3EYNL3N7rop7yg9Trew3joDohDoxQ2Ow==", + "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": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -338,7 +366,6 @@ "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -349,7 +376,6 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -359,7 +385,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -369,10 +394,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.5.tgz", - "integrity": "sha512-GLZPrd9ckqEBFMcVM/qRFAP0Hg3qiVEojgEFsx/N/zKXsBzbGF6z5FBDpZ0+Xhp1xr+qRZYjfGr1cWHB9oFHSA==", - "dev": true, + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", "license": "MIT", "dependencies": { "@types/node": "*", @@ -385,31 +409,27 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true, "license": "MIT" }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, "license": "MIT" }, "node_modules/@types/morgan": { "version": "1.9.9", "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.9.tgz", "integrity": "sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/node": { - "version": "22.10.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.9.tgz", - "integrity": "sha512-Ir6hwgsKyNESl/gLOcEz3krR4CBGgliDqBQ2ma4wIhEx0w+xnoeTq3tdrNw15kU3SxogDjOgv9sqdtLW8mIHaw==", - "dev": true, + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -419,21 +439,18 @@ "version": "6.9.18", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", - "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -444,7 +461,6 @@ "version": "1.15.7", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -488,80 +504,72 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", - "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.28.0.tgz", + "integrity": "sha512-lvFK3TCGAHsItNdWZ/1FkvpzCxTHUVuFrdnOGLMa0GGCFIbCgQWVk3CzCGdA7kM3qGVc+dfW9tr0Z/sHnGDFyg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/type-utils": "7.18.0", - "@typescript-eslint/utils": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", + "@typescript-eslint/scope-manager": "8.28.0", + "@typescript-eslint/type-utils": "8.28.0", + "@typescript-eslint/utils": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.1" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", - "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.28.0.tgz", + "integrity": "sha512-LPcw1yHD3ToaDEoljFEfQ9j2xShY367h7FZ1sq5NJT9I3yj4LHer1Xd1yRSOdYy9BpsrxU7R+eoDokChYM53lQ==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", + "@typescript-eslint/scope-manager": "8.28.0", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/typescript-estree": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", "debug": "^4.3.4" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", - "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.28.0.tgz", + "integrity": "sha512-u2oITX3BJwzWCapoZ/pXw6BCOl8rJP4Ij/3wPoGvY8XwvXflOzd1kLrDUUUAIEdJSFh+ASwdTHqtan9xSg8buw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0" + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -569,41 +577,37 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", - "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.28.0.tgz", + "integrity": "sha512-oRoXu2v0Rsy/VoOGhtWrOKDiIehvI+YNrDk5Oqj40Mwm0Yt01FC/Q7nFqg088d3yAsR1ZcZFVfPCTTFCe/KPwg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/typescript-estree": "8.28.0", + "@typescript-eslint/utils": "8.28.0", "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.1" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/types": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", - "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.28.0.tgz", + "integrity": "sha512-bn4WS1bkKEjx7HqiwG2JNB3YJdC1q6Ue7GyGlwPHyt0TnVq6TtD/hiOdTZt71sq0s7UzqBFXD8t8o2e63tXgwA==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -611,79 +615,91 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", - "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", - "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.28.0.tgz", + "integrity": "sha512-H74nHEeBGeklctAVUvmDkxB1mk+PAZ9FiOMPFncdqeRBXxk1lWSYraHw8V12b7aa6Sg9HOBNbGdSHobBPuQSuA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0" + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", - "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "node_modules/@typescript-eslint/utils": { + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.28.0.tgz", + "integrity": "sha512-OELa9hbTYciYITqgurT1u/SzpQVtDLmQMFzy/N8pQE+tefOyCWT79jHsav294aTqV1q1u+VzqDGbuujvRYaeSQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.18.0", - "eslint-visitor-keys": "^3.4.3" + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.28.0", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/typescript-estree": "8.28.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.28.0.tgz", + "integrity": "sha512-hbn8SZ8w4u2pRwgQ1GlUrPKE+t2XvcCW5tTRF7j6SMYIuYG37XuzIW44JCZPa36evi0Oy2SnM664BlIaAuQcvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.28.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@ungap/structured-clone": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.1.tgz", - "integrity": "sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true, "license": "ISC" }, @@ -701,9 +717,9 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", "bin": { @@ -757,6 +773,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -766,6 +783,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -805,22 +823,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/array-flatten": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz", - "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==", - "license": "MIT" - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -831,13 +833,13 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "5.1.2" @@ -850,7 +852,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, "license": "MIT" }, "node_modules/binary-extensions": { @@ -867,82 +868,38 @@ } }, "node_modules/body-parser": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.0.2.tgz", - "integrity": "sha512-SNMk0OONlQ01uk8EPeiBvTW7W4ovpL5b1O3t1sjpPgfxOQ6BqQJ6XjxinDPR79Z6HdcD5zBBwr5ssiTlgdNztQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.1.0.tgz", + "integrity": "sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "3.1.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.5.2", - "on-finished": "2.4.1", - "qs": "6.13.0", + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.5.2", + "on-finished": "^2.4.1", + "qs": "^6.14.0", "raw-body": "^3.0.0", - "type-is": "~1.6.18" + "type-is": "^2.0.0" }, "engines": { "node": ">=18" } }, - "node_modules/body-parser/node_modules/debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "license": "MIT", + "node_modules/body-parser/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/body-parser/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/body-parser/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" + "side-channel": "^1.1.0" }, "engines": { - "node": ">= 0.6" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/body-parser/node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "node": ">=0.6" }, - "engines": { - "node": ">= 0.6" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/brace-expansion": { @@ -969,9 +926,9 @@ } }, "node_modules/bson": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.1.tgz", - "integrity": "sha512-P92xmHDQjSKPLHqFxefqMxASNq/aWJMEZugpCjf+AF/pgcUpMMQCg7t7+ewko0/u8AapvF3luf/FoehddEK+sA==", + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz", + "integrity": "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==", "license": "Apache-2.0", "engines": { "node": ">=16.20.1" @@ -994,9 +951,9 @@ } }, "node_modules/call-bind-apply-helpers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", - "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -1007,13 +964,13 @@ } }, "node_modules/call-bound": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", - "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "get-intrinsic": "^1.2.6" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -1087,17 +1044,6 @@ "node": ">= 6" } }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -1112,6 +1058,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -1165,6 +1112,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, "license": "MIT" }, "node_modules/content-disposition": { @@ -1206,53 +1154,18 @@ "node": ">=6.6.0" } }, - "node_modules/copyfiles": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", - "integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==", - "license": "MIT", - "dependencies": { - "glob": "^7.0.5", - "minimatch": "^3.0.3", - "mkdirp": "^1.0.4", - "noms": "0.0.0", - "through2": "^2.0.1", - "untildify": "^4.0.0", - "yargs": "^16.1.0" - }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", "bin": { - "copyfiles": "copyfiles", - "copyup": "copyfiles" - } - }, - "node_modules/copyfiles/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/copyfiles/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" + "crc32": "bin/crc32.njs" }, "engines": { - "node": "*" + "node": ">=0.8" } }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -1328,19 +1241,6 @@ "node": ">=0.3.1" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -1384,12 +1284,6 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, "node_modules/enabled": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", @@ -1435,15 +1329,6 @@ "node": ">= 0.4" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1521,14 +1406,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz", - "integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.5.tgz", + "integrity": "sha512-IKKP8R87pJyMl7WWamLgPkloB16dagPIdd2FjBDbyRYPKo93wS/NbCOPh6gH+ieNLC+XZrhJt/kWj0PS/DFdmg==", "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.9.1" + "synckit": "^0.10.2" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -1539,7 +1424,7 @@ "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", - "eslint-config-prettier": "*", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "peerDependenciesMeta": { @@ -1803,9 +1688,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", - "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, "license": "ISC", "dependencies": { @@ -1854,47 +1739,22 @@ } }, "node_modules/finalhandler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.0.0.tgz", - "integrity": "sha512-MX6Zo2adDViYh+GcxxB1dpO43eypOGUOL12rLCOTMQv/DfIbpSJUy4oQIIZhVZkH9e+bZWKMon0XHFEju16tkQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", "license": "MIT", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { "node": ">= 0.8" } }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -1928,9 +1788,9 @@ } }, "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, @@ -1962,6 +1822,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -1988,27 +1849,18 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/get-intrinsic": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", - "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", + "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "get-proto": "^1.0.0", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", @@ -2039,6 +1891,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -2072,6 +1925,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -2082,6 +1936,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -2106,27 +1961,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2219,9 +2053,9 @@ } }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2250,6 +2084,7 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -2316,15 +2151,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2376,12 +2202,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2423,6 +2243,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-with-bigint": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.2.2.tgz", + "integrity": "sha512-zbaZ+MZ2PEcAD0yINpxvlLMKzoC1GPqy5p8/ZgzRJRoB+NCczGrTX9x2ashSvkfYTitQKbV5aYQCJCiHxrzF2w==", + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -2642,6 +2468,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" @@ -2660,13 +2487,13 @@ } }, "node_modules/mongodb": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz", - "integrity": "sha512-RM7AHlvYfS7jv7+BXund/kR64DryVI+cHbVAy9P61fnb1RcWZqOW1/Wj2YhqMCx+MuYhqTRGv7AwHBzmsCKBfA==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.14.2.tgz", + "integrity": "sha512-kMEHNo0F3P6QKDq17zcDuPeaywK/YaJVCEQRzPF3TOM/Bl9MFg64YE5Tu7ifj37qZJMhwU1tl2Ioivws5gRG5Q==", "license": "Apache-2.0", "dependencies": { "@mongodb-js/saslprep": "^1.1.9", - "bson": "^6.10.1", + "bson": "^6.10.3", "mongodb-connection-string-url": "^3.0.0" }, "engines": { @@ -2716,14 +2543,14 @@ } }, "node_modules/mongoose": { - "version": "8.9.5", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.5.tgz", - "integrity": "sha512-SPhOrgBm0nKV3b+IIHGqpUTOmgVL5Z3OO9AwkFEmvOZznXTvplbomstCnPOGAyungtRXE5pJTgKpKcZTdjeESg==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.12.1.tgz", + "integrity": "sha512-UW22y8QFVYmrb36hm8cGncfn4ARc/XsYWQwRTaj0gxtQk1rDuhzDO1eBantS+hTTatfAIS96LlRCJrcNHvW5+Q==", "license": "MIT", "dependencies": { - "bson": "^6.10.1", + "bson": "^6.10.3", "kareem": "2.6.3", - "mongodb": "~6.12.0", + "mongodb": "~6.14.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", @@ -2741,7 +2568,6 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", - "dev": true, "license": "MIT", "dependencies": { "basic-auth": "~2.0.1", @@ -2758,7 +2584,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -2768,14 +2593,12 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, "license": "MIT" }, "node_modules/morgan/node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -2818,6 +2641,15 @@ "dev": true, "license": "MIT" }, + "node_modules/ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "license": "MIT", + "bin": { + "ncp": "bin/ncp" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -2827,16 +2659,6 @@ "node": ">= 0.6" } }, - "node_modules/noms": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", - "integrity": "sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==", - "license": "ISC", - "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "~1.0.31" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2857,9 +2679,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", - "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -2884,7 +2706,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -2994,6 +2815,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3025,16 +2847,6 @@ "node": ">=16" } }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -3059,9 +2871,9 @@ } }, "node_modules/prettier": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", - "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, "license": "MIT", "bin": { @@ -3087,12 +2899,6 @@ "node": ">=6.0.0" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -3187,18 +2993,6 @@ "node": ">=0.10.0" } }, - "node_modules/readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -3212,15 +3006,6 @@ "node": ">=8.10.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -3253,9 +3038,9 @@ } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -3281,21 +3066,17 @@ } }, "node_modules/router": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.0.0.tgz", - "integrity": "sha512-dIM5zVoG8xhC6rnSN8uoAgFARwTE7BQs8YwHEvK0VCmfxQXMaOuA1uiR1IPwsW7JyK5iTt7Od/TC9StasS2NPQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.1.0.tgz", + "integrity": "sha512-/m/NSLxeYEgWNtyC+WtNHCF7jbGxOibVWKnn+1Psff4dJGOfoXP+MuC/f2CwSmyiHdOIzYnYFp4W6GxWfekaLA==", "license": "MIT", "dependencies": { - "array-flatten": "3.0.0", - "is-promise": "4.0.0", - "methods": "~1.1.2", - "parseurl": "~1.3.3", - "path-to-regexp": "^8.0.0", - "setprototypeof": "1.2.0", - "utils-merge": "1.0.1" + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 18" } }, "node_modules/run-parallel": { @@ -3358,9 +3139,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "license": "ISC", "bin": { @@ -3554,16 +3335,6 @@ "is-arrayish": "^0.3.1" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -3612,30 +3383,11 @@ "node": ">= 0.8" } }, - "node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", - "license": "MIT" - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -3694,14 +3446,14 @@ } }, "node_modules/synckit": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", - "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.10.3.tgz", + "integrity": "sha512-R1urvuyiTaWfeCggqEvpDJwAlDVdsT9NM+IP//Tk2x7qHCkSvBk/fwFgw/TLAHzZlrAnnazMcRw0ZD8HlYFTEQ==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" + "@pkgr/core": "^0.2.0", + "tslib": "^2.8.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -3723,52 +3475,6 @@ "dev": true, "license": "MIT" }, - "node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "license": "MIT", - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/through2/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/through2/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/through2/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/through2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3823,16 +3529,16 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/ts-node": { @@ -4017,7 +3723,7 @@ "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", - "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4030,7 +3736,6 @@ "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -4042,15 +3747,6 @@ "node": ">= 0.8" } }, - "node_modules/untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -4093,9 +3789,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.36", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.36.tgz", - "integrity": "sha512-FYZECqBSnynl6lQvcQyEqpnGW9l84wzusekhtwKjvg3280CYdn7g5x0Q9tOMhj1jpc/1tuY+akHtHa94sPtqKw==" + "version": "0.5.52", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.52.tgz", + "integrity": "sha512-mJyQbTFMDwgBSkhUYJzcfJg9qrMTrL1pyZuAxV/Dov68xUikK5zigQSYM3ZkKYbhwBtg0Bx/+7q9GAmPzGaRhA==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", @@ -4112,9 +3808,9 @@ } }, "node_modules/whatwg-url": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz", - "integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.1.tgz", + "integrity": "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==", "license": "MIT", "dependencies": { "tr46": "^5.0.0", @@ -4250,23 +3946,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -4277,47 +3956,12 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4" } }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index d3446687..3c0c536d 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "start": "node --import ./build/src/pathman.js build/src/index.js", "dev": "ts-node-dev --openssl-legacy-provider -r tsconfig-paths/register src/index.ts ", - "build": "tsc && copyfiles static/webui/** build", + "build": "tsc --incremental --sourceMap && ncp static/webui build/static/webui", + "verify": "tsgo --noEmit", "lint": "eslint --ext .ts .", "lint:fix": "eslint --fix --ext .ts .", "prettier": "prettier --write .", @@ -14,27 +15,29 @@ }, "license": "GNU", "dependencies": { - "copyfiles": "^2.4.1", + "@types/express": "^5", + "@types/morgan": "^1.9.9", + "crc-32": "^1.2.2", "express": "^5", - "mongoose": "^8.9.4", - "warframe-public-export-plus": "^0.5.36", + "json-with-bigint": "^3.2.2", + "mongoose": "^8.11.0", + "morgan": "^1.10.0", + "ncp": "^2.0.0", + "typescript": ">=5.5 <5.6.0", + "warframe-public-export-plus": "^0.5.52", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { - "@types/express": "^5", - "@types/morgan": "^1.9.9", - "@typescript-eslint/eslint-plugin": "^7.18", - "@typescript-eslint/parser": "^7.18", - "eslint": "^8.56.0", - "eslint-plugin-prettier": "^5.2.3", - "morgan": "^1.10.0", - "prettier": "^3.4.2", - "ts-node": "^10.9.2", + "@rxliuli/tsgo": "^2025.3.31", + "@typescript-eslint/eslint-plugin": "^8.28.0", + "@typescript-eslint/parser": "^8.28.0", + "eslint": "^8", + "eslint-plugin-prettier": "^5.2.5", + "prettier": "^3.5.3", "ts-node-dev": "^2.0.0", - "tsconfig-paths": "^4.2.0", - "typescript": ">=4.7.4 <5.6.0" + "tsconfig-paths": "^4.2.0" }, "engines": { "node": ">=18.15.0", diff --git a/scripts/update-translations.js b/scripts/update-translations.js index 45067b25..568885e6 100644 --- a/scripts/update-translations.js +++ b/scripts/update-translations.js @@ -1,4 +1,4 @@ -// Based on http://209.141.38.3/OpenWF/Translations/src/branch/main/update.php +// Based on https://onlyg.it/OpenWF/Translations/src/branch/main/update.php // Converted via ChatGPT-4o const fs = require("fs"); diff --git a/src/app.ts b/src/app.ts index 6f9bc0b9..160a93c8 100644 --- a/src/app.ts +++ b/src/app.ts @@ -15,6 +15,15 @@ import { webuiRouter } from "@/src/routes/webui"; const app = express(); +app.use((req, _res, next) => { + // 38.5.0 introduced "ezip" for encrypted body blobs. + // The bootstrapper decrypts it for us but having an unsupported Content-Encoding here would still be an issue for Express, so removing it. + if (req.headers["content-encoding"] == "ezip") { + req.headers["content-encoding"] = undefined; + } + next(); +}); + app.use(bodyParser.raw()); app.use(express.json({ limit: "4mb" })); app.use(bodyParser.text()); @@ -23,6 +32,7 @@ app.use(requestLogger); app.use("/api", apiRouter); app.use("/", cacheRouter); app.use("/custom", customRouter); +app.use("/dynamic", dynamicController); app.use("/:id/dynamic", dynamicController); app.use("/pay", payRouter); app.use("/stats", statsRouter); diff --git a/src/controllers/api/abandonLibraryDailyTaskController.ts b/src/controllers/api/abandonLibraryDailyTaskController.ts new file mode 100644 index 00000000..ac515609 --- /dev/null +++ b/src/controllers/api/abandonLibraryDailyTaskController.ts @@ -0,0 +1,11 @@ +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const abandonLibraryDailyTaskController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + inventory.LibraryActiveDailyTaskInfo = undefined; + await inventory.save(); + res.status(200).end(); +}; diff --git a/src/controllers/api/abortDojoComponentController.ts b/src/controllers/api/abortDojoComponentController.ts new file mode 100644 index 00000000..3fcf770c --- /dev/null +++ b/src/controllers/api/abortDojoComponentController.ts @@ -0,0 +1,46 @@ +import { + getDojoClient, + getGuildForRequestEx, + hasAccessToDojo, + hasGuildPermission, + removeDojoDeco, + removeDojoRoom +} from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const abortDojoComponentController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId LevelKeys"); + const guild = await getGuildForRequestEx(req, inventory); + const request = JSON.parse(String(req.body)) as IAbortDojoComponentRequest; + + if ( + !hasAccessToDojo(inventory) || + !(await hasGuildPermission( + guild, + accountId, + request.DecoId ? GuildPermission.Decorator : GuildPermission.Architect + )) + ) { + res.json({ DojoRequestStatus: -1 }); + return; + } + + if (request.DecoId) { + removeDojoDeco(guild, request.ComponentId, request.DecoId); + } else { + await removeDojoRoom(guild, request.ComponentId); + } + + await guild.save(); + res.json(await getDojoClient(guild, 0, request.ComponentId)); +}; + +interface IAbortDojoComponentRequest { + DecoType?: string; + ComponentId: string; + DecoId?: string; +} diff --git a/src/controllers/api/abortDojoComponentDestructionController.ts b/src/controllers/api/abortDojoComponentDestructionController.ts new file mode 100644 index 00000000..75f08f37 --- /dev/null +++ b/src/controllers/api/abortDojoComponentDestructionController.ts @@ -0,0 +1,21 @@ +import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const abortDojoComponentDestructionController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId LevelKeys"); + const guild = await getGuildForRequestEx(req, inventory); + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Architect))) { + res.json({ DojoRequestStatus: -1 }); + return; + } + const componentId = req.query.componentId as string; + + guild.DojoComponents.id(componentId)!.DestructionTime = undefined; + + await guild.save(); + res.json(await getDojoClient(guild, 0, componentId)); +}; diff --git a/src/controllers/api/activateRandomModController.ts b/src/controllers/api/activateRandomModController.ts index cb84feba..4ac822c7 100644 --- a/src/controllers/api/activateRandomModController.ts +++ b/src/controllers/api/activateRandomModController.ts @@ -1,8 +1,9 @@ +import { toOid } from "@/src/helpers/inventoryHelpers"; +import { createVeiledRivenFingerprint, rivenRawToRealWeighted } from "@/src/helpers/rivenHelper"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { addMods, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getRandomElement, getRandomInt, getRandomReward, IRngResult } from "@/src/services/rngService"; -import { logger } from "@/src/utils/logger"; +import { getRandomElement } from "@/src/services/rngService"; import { RequestHandler } from "express"; import { ExportUpgrades } from "warframe-public-export-plus"; @@ -17,82 +18,23 @@ export const activateRandomModController: RequestHandler = async (req, res) => { } ]); const rivenType = getRandomElement(rivenRawToRealWeighted[request.ItemType]); - const challenge = getRandomElement(ExportUpgrades[rivenType].availableChallenges!); - const fingerprintChallenge: IRandomModChallenge = { - Type: challenge.fullName, - Progress: 0, - Required: getRandomInt(challenge.countRange[0], challenge.countRange[1]) - }; - if (Math.random() < challenge.complicationChance) { - const complicationsAsRngResults: IRngResult[] = []; - for (const complication of challenge.complications) { - complicationsAsRngResults.push({ - type: complication.fullName, - itemCount: 1, - probability: complication.weight - }); - } - fingerprintChallenge.Complication = getRandomReward(complicationsAsRngResults)!.type; - logger.debug( - `riven rolled challenge ${fingerprintChallenge.Type} with complication ${fingerprintChallenge.Complication}` - ); - const complication = challenge.complications.find(x => x.fullName == fingerprintChallenge.Complication)!; - fingerprintChallenge.Required *= complication.countMultiplier; - } else { - logger.debug(`riven rolled challenge ${fingerprintChallenge.Type}`); - } + const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]); const upgradeIndex = inventory.Upgrades.push({ ItemType: rivenType, - UpgradeFingerprint: JSON.stringify({ challenge: fingerprintChallenge }) + UpgradeFingerprint: JSON.stringify(fingerprint) }) - 1; await inventory.save(); + // For some reason, in this response, the UpgradeFingerprint is simply a nested object and not a string res.json({ - NewMod: inventory.Upgrades[upgradeIndex].toJSON() + NewMod: { + UpgradeFingerprint: fingerprint, + ItemType: rivenType, + ItemId: toOid(inventory.Upgrades[upgradeIndex]._id) + } }); }; interface IActiveRandomModRequest { ItemType: string; } - -interface IRandomModChallenge { - Type: string; - Progress: number; - Required: number; - Complication?: string; -} - -const rivenRawToRealWeighted: Record = { - "/Lotus/Upgrades/Mods/Randomized/RawArchgunRandomMod": [ - "/Lotus/Upgrades/Mods/Randomized/LotusArchgunRandomModRare" - ], - "/Lotus/Upgrades/Mods/Randomized/RawMeleeRandomMod": [ - "/Lotus/Upgrades/Mods/Randomized/PlayerMeleeWeaponRandomModRare" - ], - "/Lotus/Upgrades/Mods/Randomized/RawModularMeleeRandomMod": [ - "/Lotus/Upgrades/Mods/Randomized/LotusModularMeleeRandomModRare" - ], - "/Lotus/Upgrades/Mods/Randomized/RawModularPistolRandomMod": [ - "/Lotus/Upgrades/Mods/Randomized/LotusModularPistolRandomModRare" - ], - "/Lotus/Upgrades/Mods/Randomized/RawPistolRandomMod": ["/Lotus/Upgrades/Mods/Randomized/LotusPistolRandomModRare"], - "/Lotus/Upgrades/Mods/Randomized/RawRifleRandomMod": ["/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare"], - "/Lotus/Upgrades/Mods/Randomized/RawShotgunRandomMod": [ - "/Lotus/Upgrades/Mods/Randomized/LotusShotgunRandomModRare" - ], - "/Lotus/Upgrades/Mods/Randomized/RawSentinelWeaponRandomMod": [ - "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", - "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", - "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", - "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", - "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", - "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", - "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", - "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", - "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", - "/Lotus/Upgrades/Mods/Randomized/LotusShotgunRandomModRare", - "/Lotus/Upgrades/Mods/Randomized/LotusPistolRandomModRare", - "/Lotus/Upgrades/Mods/Randomized/PlayerMeleeWeaponRandomModRare" - ] -}; diff --git a/src/controllers/api/addFriendImageController.ts b/src/controllers/api/addFriendImageController.ts index 3772f7ef..5f224ad8 100644 --- a/src/controllers/api/addFriendImageController.ts +++ b/src/controllers/api/addFriendImageController.ts @@ -1,16 +1,25 @@ import { RequestHandler } from "express"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { IUpdateGlyphRequest } from "@/src/types/requestTypes"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getInventory } from "@/src/services/inventoryService"; +import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; -const addFriendImageController: RequestHandler = async (req, res) => { +export const addFriendImageController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const json = getJSONfromString(String(req.body)); - const inventory = await getInventory(accountId); - inventory.ActiveAvatarImageType = json.AvatarImageType; - await inventory.save(); + + await Inventory.updateOne( + { + accountOwnerId: accountId + }, + { + ActiveAvatarImageType: json.AvatarImageType + } + ); + res.json({}); }; -export { addFriendImageController }; +interface IUpdateGlyphRequest { + AvatarImageType: string; + AvatarImage: string; +} diff --git a/src/controllers/api/addToAllianceController.ts b/src/controllers/api/addToAllianceController.ts new file mode 100644 index 00000000..e7b24dec --- /dev/null +++ b/src/controllers/api/addToAllianceController.ts @@ -0,0 +1,117 @@ +import { getJSONfromString, regexEscape } from "@/src/helpers/stringHelpers"; +import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel"; +import { createMessage } from "@/src/services/inboxService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { logger } from "@/src/utils/logger"; +import { RequestHandler } from "express"; +import { ExportFlavour } from "warframe-public-export-plus"; + +export const addToAllianceController: RequestHandler = async (req, res) => { + // Check requester is a warlord in their guild + const account = await getAccountForRequest(req); + const guildMember = (await GuildMember.findOne({ accountId: account._id, status: 0 }))!; + if (guildMember.rank > 1) { + res.status(400).json({ Error: 104 }); + return; + } + + // Check guild has invite permissions in the alliance + const allianceMember = (await AllianceMember.findOne({ + allianceId: req.query.allianceId, + guildId: guildMember.guildId + }))!; + if (!(allianceMember.Permissions & GuildPermission.Recruiter)) { + res.status(400).json({ Error: 104 }); + return; + } + + // Find clan to invite + const payload = getJSONfromString(String(req.body)); + const guilds = await Guild.find( + { + Name: + payload.clanName.indexOf("#") == -1 + ? new RegExp("^" + regexEscape(payload.clanName) + "#...$") + : payload.clanName + }, + "Name" + ); + if (guilds.length == 0) { + res.status(400).json({ Error: 101 }); + return; + } + if (guilds.length > 1) { + const choices: IGuildChoice[] = []; + for (const guild of guilds) { + choices.push({ + OriginalPlatform: 0, + Name: guild.Name + }); + } + res.json(choices); + return; + } + + // Add clan as a pending alliance member + try { + await AllianceMember.insertOne({ + allianceId: req.query.allianceId, + guildId: guilds[0]._id, + Pending: true, + Permissions: 0 + }); + } catch (e) { + logger.debug(`alliance invite failed due to ${String(e)}`); + res.status(400).json({ Error: 102 }); + return; + } + + // Send inbox message to founding warlord + // TOVERIFY: Should other warlords get this as well? + // TOVERIFY: Who/what should the sender be? + // TOVERIFY: Should this message be highPriority? + const invitedClanOwnerMember = (await GuildMember.findOne({ guildId: guilds[0]._id, rank: 0 }))!; + const senderInventory = await getInventory(account._id.toString(), "ActiveAvatarImageType"); + const senderGuild = (await Guild.findById(allianceMember.guildId, "Name"))!; + const alliance = (await Alliance.findById(req.query.allianceId, "Name"))!; + await createMessage(invitedClanOwnerMember.accountId, [ + { + sndr: getSuffixedName(account), + msg: "/Lotus/Language/Menu/Mailbox_AllianceInvite_Body", + arg: [ + { + Key: "THEIR_CLAN", + Tag: senderGuild.Name + }, + { + Key: "CLAN", + Tag: guilds[0].Name + }, + { + Key: "ALLIANCE", + Tag: alliance.Name + } + ], + sub: "/Lotus/Language/Menu/Mailbox_AllianceInvite_Title", + icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon, + contextInfo: alliance._id.toString(), + highPriority: true, + acceptAction: "ALLIANCE_INVITE", + declineAction: "ALLIANCE_INVITE", + hasAccountAction: true + } + ]); + + res.end(); +}; + +interface IAddToAllianceRequest { + clanName: string; +} + +interface IGuildChoice { + OriginalPlatform: number; + Name: string; +} diff --git a/src/controllers/api/addToGuildController.ts b/src/controllers/api/addToGuildController.ts new file mode 100644 index 00000000..c67b8d1a --- /dev/null +++ b/src/controllers/api/addToGuildController.ts @@ -0,0 +1,105 @@ +import { Guild, GuildMember } from "@/src/models/guildModel"; +import { Account } from "@/src/models/loginModel"; +import { fillInInventoryDataForGuildMember, hasGuildPermission } from "@/src/services/guildService"; +import { createMessage } from "@/src/services/inboxService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService"; +import { IOid } from "@/src/types/commonTypes"; +import { GuildPermission, IGuildMemberClient } from "@/src/types/guildTypes"; +import { logger } from "@/src/utils/logger"; +import { RequestHandler } from "express"; +import { ExportFlavour } from "warframe-public-export-plus"; + +export const addToGuildController: RequestHandler = async (req, res) => { + const payload = JSON.parse(String(req.body)) as IAddToGuildRequest; + + if ("UserName" in payload) { + // Clan recruiter sending an invite + + const account = await Account.findOne({ DisplayName: payload.UserName }); + if (!account) { + res.status(400).json("Username does not exist"); + return; + } + + const inventory = await getInventory(account._id.toString(), "Settings"); + // TODO: Also consider GIFT_MODE_FRIENDS once friends are implemented + if (inventory.Settings?.GuildInvRestriction == "GIFT_MODE_NONE") { + res.status(400).json("Invite restricted"); + return; + } + + const guild = (await Guild.findById(payload.GuildId.$oid, "Name Ranks"))!; + const senderAccount = await getAccountForRequest(req); + if (!(await hasGuildPermission(guild, senderAccount._id.toString(), GuildPermission.Recruiter))) { + res.status(400).json("Invalid permission"); + } + + try { + await GuildMember.insertOne({ + accountId: account._id, + guildId: payload.GuildId.$oid, + status: 2 // outgoing invite + }); + } catch (e) { + logger.debug(`guild invite failed due to ${String(e)}`); + res.status(400).json("User already invited to clan"); + return; + } + + const senderInventory = await getInventory(senderAccount._id.toString(), "ActiveAvatarImageType"); + await createMessage(account._id, [ + { + sndr: getSuffixedName(senderAccount), + msg: "/Lotus/Language/Menu/Mailbox_ClanInvite_Body", + arg: [ + { + Key: "clan", + Tag: guild.Name + } + ], + sub: "/Lotus/Language/Menu/Mailbox_ClanInvite_Title", + icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon, + contextInfo: payload.GuildId.$oid, + highPriority: true, + acceptAction: "GUILD_INVITE", + declineAction: "GUILD_INVITE", + hasAccountAction: true + } + ]); + + const member: IGuildMemberClient = { + _id: { $oid: account._id.toString() }, + DisplayName: account.DisplayName, + Rank: 7, + Status: 2 + }; + await fillInInventoryDataForGuildMember(member); + res.json({ NewMember: member }); + } else if ("RequestMsg" in payload) { + // Player applying to join a clan + const accountId = await getAccountIdForRequest(req); + try { + await GuildMember.insertOne({ + accountId, + guildId: payload.GuildId.$oid, + status: 1, // incoming invite + RequestMsg: payload.RequestMsg, + RequestExpiry: new Date(Date.now() + 14 * 86400 * 1000) // TOVERIFY: I can't find any good information about this with regards to live, but 2 weeks seem reasonable. + }); + } catch (e) { + logger.debug(`guild invite failed due to ${String(e)}`); + res.status(400).send("Already requested"); + } + res.end(); + } else { + logger.error(`data provided to ${req.path}: ${String(req.body)}`); + res.status(400).end(); + } +}; + +interface IAddToGuildRequest { + UserName?: string; + GuildId: IOid; + RequestMsg?: string; +} diff --git a/src/controllers/api/artifactTransmutationController.ts b/src/controllers/api/artifactTransmutationController.ts new file mode 100644 index 00000000..78da93a1 --- /dev/null +++ b/src/controllers/api/artifactTransmutationController.ts @@ -0,0 +1,132 @@ +import { toOid } from "@/src/helpers/inventoryHelpers"; +import { createVeiledRivenFingerprint, rivenRawToRealWeighted } from "@/src/helpers/rivenHelper"; +import { addMiscItems, addMods, getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getRandomElement, getRandomWeightedReward, getRandomWeightedRewardUc } from "@/src/services/rngService"; +import { IOid } from "@/src/types/commonTypes"; +import { RequestHandler } from "express"; +import { ExportBoosterPacks, ExportUpgrades, TRarity } from "warframe-public-export-plus"; + +export const artifactTransmutationController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const payload = JSON.parse(String(req.body)) as IArtifactTransmutationRequest; + + inventory.RegularCredits -= payload.Cost; + inventory.FusionPoints -= payload.FusionPointCost; + + if (payload.RivenTransmute) { + addMiscItems(inventory, [ + { + ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/SentientSecretItem", + ItemCount: -1 + } + ]); + + payload.Consumed.forEach(upgrade => { + inventory.Upgrades.pull({ _id: upgrade.ItemId.$oid }); + }); + + const rawRivenType = getRandomRawRivenType(); + const rivenType = getRandomElement(rivenRawToRealWeighted[rawRivenType]); + const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]); + + const upgradeIndex = + inventory.Upgrades.push({ + ItemType: rivenType, + UpgradeFingerprint: JSON.stringify(fingerprint) + }) - 1; + await inventory.save(); + res.json({ + NewMods: [ + { + ItemId: toOid(inventory.Upgrades[upgradeIndex]._id), + ItemType: rivenType, + UpgradeFingerprint: fingerprint + } + ] + }); + } else { + const counts: Record = { + COMMON: 0, + UNCOMMON: 0, + RARE: 0, + LEGENDARY: 0 + }; + let forcedPolarity: string | undefined; + payload.Consumed.forEach(upgrade => { + const meta = ExportUpgrades[upgrade.ItemType]; + counts[meta.rarity] += upgrade.ItemCount; + addMods(inventory, [ + { + ItemType: upgrade.ItemType, + ItemCount: upgrade.ItemCount * -1 + } + ]); + if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/AttackTransmuteCore") { + forcedPolarity = "AP_ATTACK"; + } else if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/DefenseTransmuteCore") { + forcedPolarity = "AP_DEFENSE"; + } else if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/TacticTransmuteCore") { + forcedPolarity = "AP_TACTIC"; + } + }); + + // Based on the table on https://wiki.warframe.com/w/Transmutation + const weights: Record = { + COMMON: counts.COMMON * 95 + counts.UNCOMMON * 15 + counts.RARE * 4, + UNCOMMON: counts.COMMON * 4 + counts.UNCOMMON * 80 + counts.RARE * 10, + RARE: counts.COMMON * 1 + counts.UNCOMMON * 5 + counts.RARE * 50, + LEGENDARY: 0 + }; + + const options: { uniqueName: string; rarity: TRarity }[] = []; + Object.entries(ExportUpgrades).forEach(([uniqueName, upgrade]) => { + if (upgrade.canBeTransmutation && (!forcedPolarity || upgrade.polarity == forcedPolarity)) { + options.push({ uniqueName, rarity: upgrade.rarity }); + } + }); + + const newModType = getRandomWeightedReward(options, weights)!.uniqueName; + addMods(inventory, [ + { + ItemType: newModType, + ItemCount: 1 + } + ]); + + await inventory.save(); + res.json({ + NewMods: [ + { + ItemType: newModType, + ItemCount: 1 + } + ] + }); + } +}; + +const getRandomRawRivenType = (): string => { + const pack = ExportBoosterPacks["/Lotus/Types/BoosterPacks/CalendarRivenPack"]; + return getRandomWeightedRewardUc(pack.components, pack.rarityWeightsPerRoll[0])!.Item; +}; + +interface IArtifactTransmutationRequest { + Upgrade: IAgnosticUpgradeClient; + LevelDiff: number; + Consumed: IAgnosticUpgradeClient[]; + Cost: number; + FusionPointCost: number; + RivenTransmute?: boolean; +} + +interface IAgnosticUpgradeClient { + ItemType: string; + ItemId: IOid; + FromSKU: boolean; + UpgradeFingerprint: string; + PendingRerollFingerprint: string; + ItemCount: number; + LastAdded: IOid; +} diff --git a/src/controllers/api/cancelGuildAdvertisementController.ts b/src/controllers/api/cancelGuildAdvertisementController.ts new file mode 100644 index 00000000..aa587201 --- /dev/null +++ b/src/controllers/api/cancelGuildAdvertisementController.ts @@ -0,0 +1,20 @@ +import { GuildAd } from "@/src/models/guildModel"; +import { getGuildForRequestEx, hasGuildPermission } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const cancelGuildAdvertisementController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId"); + const guild = await getGuildForRequestEx(req, inventory); + if (!(await hasGuildPermission(guild, accountId, GuildPermission.Advertiser))) { + res.status(400).end(); + return; + } + + await GuildAd.deleteOne({ GuildId: guild._id }); + + res.end(); +}; diff --git a/src/controllers/api/changeDojoRootController.ts b/src/controllers/api/changeDojoRootController.ts index 568bf688..e28b92d9 100644 --- a/src/controllers/api/changeDojoRootController.ts +++ b/src/controllers/api/changeDojoRootController.ts @@ -1,15 +1,22 @@ import { RequestHandler } from "express"; -import { getDojoClient, getGuildForRequest } from "@/src/services/guildService"; +import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService"; import { logger } from "@/src/utils/logger"; -import { IDojoComponentDatabase } from "@/src/types/guildTypes"; +import { GuildPermission, IDojoComponentDatabase } from "@/src/types/guildTypes"; import { Types } from "mongoose"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getInventory } from "@/src/services/inventoryService"; export const changeDojoRootController: RequestHandler = async (req, res) => { - const guild = await getGuildForRequest(req); - // At this point, we know that a member of the guild is making this request. Assuming they are allowed to change the root. + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId LevelKeys"); + const guild = await getGuildForRequestEx(req, inventory); + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Architect))) { + res.json({ DojoRequestStatus: -1 }); + return; + } const idToNode: Record = {}; - guild.DojoComponents!.forEach(x => { + guild.DojoComponents.forEach(x => { idToNode[x._id.toString()] = { component: x, parent: undefined, @@ -18,7 +25,7 @@ export const changeDojoRootController: RequestHandler = async (req, res) => { }); let oldRoot: INode | undefined; - guild.DojoComponents!.forEach(x => { + guild.DojoComponents.forEach(x => { const node = idToNode[x._id.toString()]; if (x.pi) { idToNode[x.pi.toString()].children.push(node); @@ -47,7 +54,7 @@ export const changeDojoRootController: RequestHandler = async (req, res) => { ); top.children.forEach(x => stack.push(x)); } - guild.DojoComponents!.forEach(x => { + guild.DojoComponents.forEach(x => { x._id = idMap[x._id.toString()]; if (x.pi) { x.pi = idMap[x.pi.toString()]; @@ -58,7 +65,7 @@ export const changeDojoRootController: RequestHandler = async (req, res) => { await guild.save(); - res.json(getDojoClient(guild, 0)); + res.json(await getDojoClient(guild, 0)); }; interface INode { diff --git a/src/controllers/api/changeGuildRankController.ts b/src/controllers/api/changeGuildRankController.ts new file mode 100644 index 00000000..28a8113e --- /dev/null +++ b/src/controllers/api/changeGuildRankController.ts @@ -0,0 +1,38 @@ +import { GuildMember } from "@/src/models/guildModel"; +import { getGuildForRequest, hasGuildPermissionEx } from "@/src/services/guildService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const changeGuildRankController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const member = (await GuildMember.findOne({ + accountId: accountId, + guildId: req.query.guildId as string + }))!; + const newRank: number = parseInt(req.query.rankChange as string); + + const guild = await getGuildForRequest(req); + if (newRank < member.rank || !hasGuildPermissionEx(guild, member, GuildPermission.Promoter)) { + res.status(400).json("Invalid permission"); + return; + } + + const target = (await GuildMember.findOne({ + guildId: req.query.guildId as string, + accountId: req.query.targetId as string + }))!; + target.rank = parseInt(req.query.rankChange as string); + await target.save(); + + if (newRank == 0) { + // If we just promoted someone else to Founding Warlord, we need to demote ourselves to Warlord. + member.rank = 1; + await member.save(); + } + + res.json({ + _id: req.query.targetId as string, + Rank: newRank + }); +}; diff --git a/src/controllers/api/claimCompletedRecipeController.ts b/src/controllers/api/claimCompletedRecipeController.ts index ec9087ba..880b2267 100644 --- a/src/controllers/api/claimCompletedRecipeController.ts +++ b/src/controllers/api/claimCompletedRecipeController.ts @@ -7,9 +7,19 @@ import { getRecipe } from "@/src/services/itemDataService"; import { IOid } from "@/src/types/commonTypes"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getInventory, updateCurrency, addItem, addMiscItems, addRecipes } from "@/src/services/inventoryService"; +import { + getInventory, + updateCurrency, + addItem, + addRecipes, + occupySlot, + combineInventoryChanges +} from "@/src/services/inventoryService"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; -export interface IClaimCompletedRecipeRequest { +interface IClaimCompletedRecipeRequest { RecipeIds: IOid[]; } @@ -37,15 +47,36 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = } if (req.query.cancel) { - const currencyChanges = updateCurrency(inventory, recipe.buildPrice * -1, false); - addMiscItems(inventory, recipe.ingredients); + const inventoryChanges: IInventoryChanges = { + ...updateCurrency(inventory, recipe.buildPrice * -1, false) + }; + + const equipmentIngredients = new Set(); + for (const category of ["LongGuns", "Pistols", "Melee"] as const) { + if (pendingRecipe[category]) { + pendingRecipe[category].forEach(item => { + const index = inventory[category].push(item) - 1; + inventoryChanges[category] ??= []; + inventoryChanges[category].push(inventory[category][index].toJSON()); + equipmentIngredients.add(item.ItemType); + + occupySlot(inventory, InventorySlot.WEAPONS, false); + inventoryChanges.WeaponBin ??= { Slots: 0 }; + inventoryChanges.WeaponBin.Slots -= 1; + }); + } + } + for (const ingredient of recipe.ingredients) { + if (!equipmentIngredients.has(ingredient.ItemType)) { + combineInventoryChanges( + inventoryChanges, + await addItem(inventory, ingredient.ItemType, ingredient.ItemCount) + ); + } + } await inventory.save(); - // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root. - res.json({ - ...currencyChanges, - MiscItems: recipe.ingredients - }); + res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root. } else { logger.debug("Claiming Recipe", { recipe, pendingRecipe }); @@ -68,31 +99,40 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]); inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1); } + } else if (recipe.secretIngredientAction == "SIA_UNBRAND") { + inventory.BrandedSuits!.splice( + inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)), + 1 + ); } let InventoryChanges = {}; if (recipe.consumeOnUse) { - const recipeChanges = [ + addRecipes(inventory, [ { ItemType: pendingRecipe.ItemType, ItemCount: -1 } - ]; - - InventoryChanges = { ...InventoryChanges, Recipes: recipeChanges }; - - addRecipes(inventory, recipeChanges); + ]); } if (req.query.rush) { + const end = Math.trunc(pendingRecipe.CompletionDate.getTime() / 1000); + const start = end - recipe.buildTime; + const secondsElapsed = Math.trunc(Date.now() / 1000) - start; + const progress = secondsElapsed / recipe.buildTime; + logger.debug(`rushing recipe at ${Math.trunc(progress * 100)}% completion`); + const cost = Math.round(recipe.skipBuildTimePrice * (1 - (progress - 0.5))); InventoryChanges = { ...InventoryChanges, - ...updateCurrency(inventory, recipe.skipBuildTimePrice, true) + ...updateCurrency(inventory, cost, true) + }; + } + if (recipe.secretIngredientAction != "SIA_UNBRAND") { + InventoryChanges = { + ...InventoryChanges, + ...(await addItem(inventory, recipe.resultType, recipe.num, false)) }; } - InventoryChanges = { - ...InventoryChanges, - ...(await addItem(inventory, recipe.resultType, recipe.num)).InventoryChanges - }; await inventory.save(); res.json({ InventoryChanges }); } diff --git a/src/controllers/api/claimLibraryDailyTaskRewardController.ts b/src/controllers/api/claimLibraryDailyTaskRewardController.ts new file mode 100644 index 00000000..6d8e1d41 --- /dev/null +++ b/src/controllers/api/claimLibraryDailyTaskRewardController.ts @@ -0,0 +1,31 @@ +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const claimLibraryDailyTaskRewardController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + + const rewardQuantity = inventory.LibraryActiveDailyTaskInfo!.RewardQuantity; + const rewardStanding = inventory.LibraryActiveDailyTaskInfo!.RewardStanding; + inventory.LibraryActiveDailyTaskInfo = undefined; + inventory.LibraryAvailableDailyTaskInfo = undefined; + + let syndicate = inventory.Affiliations.find(x => x.Tag == "LibrarySyndicate"); + if (!syndicate) { + syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: "LibrarySyndicate", Standing: 0 }) - 1]; + } + syndicate.Standing += rewardStanding; + + inventory.FusionPoints += 80 * rewardQuantity; + await inventory.save(); + + res.json({ + RewardItem: "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/RareFusionBundle", + RewardQuantity: rewardQuantity, + StandingAwarded: rewardStanding, + InventoryChanges: { + FusionPoints: 80 * rewardQuantity + } + }); +}; diff --git a/src/controllers/api/clearNewEpisodeRewardController.ts b/src/controllers/api/clearNewEpisodeRewardController.ts new file mode 100644 index 00000000..1dd3010a --- /dev/null +++ b/src/controllers/api/clearNewEpisodeRewardController.ts @@ -0,0 +1,6 @@ +import { RequestHandler } from "express"; + +// example req.body: {"NewEpisodeReward":true,"crossPlaySetting":"ENABLED"} +export const clearNewEpisodeRewardController: RequestHandler = (_req, res) => { + res.status(200).end(); +}; diff --git a/src/controllers/api/completeRandomModChallengeController.ts b/src/controllers/api/completeRandomModChallengeController.ts index c2578eb7..a4e3cf08 100644 --- a/src/controllers/api/completeRandomModChallengeController.ts +++ b/src/controllers/api/completeRandomModChallengeController.ts @@ -4,8 +4,7 @@ import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inven import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { IUnveiledRivenFingerprint, randomiseRivenStats } from "@/src/helpers/rivenFingerprintHelper"; -import { getRandomElement, getRandomInt } from "@/src/services/rngService"; +import { createUnveiledRivenFingerprint } from "@/src/helpers/rivenHelper"; import { ExportUpgrades } from "warframe-public-export-plus"; export const completeRandomModChallengeController: RequestHandler = async (req, res) => { @@ -31,17 +30,7 @@ export const completeRandomModChallengeController: RequestHandler = async (req, // Update riven fingerprint to a randomised unveiled state const upgrade = inventory.Upgrades.id(request.ItemId)!; const meta = ExportUpgrades[upgrade.ItemType]; - const fingerprint: IUnveiledRivenFingerprint = { - compat: getRandomElement(meta.compatibleItems!), - lim: 0, - lvl: 0, - lvlReq: getRandomInt(8, 16), - pol: getRandomElement(["AP_ATTACK", "AP_DEFENSE", "AP_TACTIC"]), - buffs: [], - curses: [] - }; - randomiseRivenStats(meta, fingerprint); - upgrade.UpgradeFingerprint = JSON.stringify(fingerprint); + upgrade.UpgradeFingerprint = JSON.stringify(createUnveiledRivenFingerprint(meta)); await inventory.save(); diff --git a/src/controllers/api/confirmAllianceInvitationController.ts b/src/controllers/api/confirmAllianceInvitationController.ts new file mode 100644 index 00000000..8d998e77 --- /dev/null +++ b/src/controllers/api/confirmAllianceInvitationController.ts @@ -0,0 +1,37 @@ +import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel"; +import { getAllianceClient } from "@/src/services/guildService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const confirmAllianceInvitationController: RequestHandler = async (req, res) => { + // Check requester is a warlord in their guild + const accountId = await getAccountIdForRequest(req); + const guildMember = (await GuildMember.findOne({ accountId, status: 0 }))!; + if (guildMember.rank > 1) { + res.status(400).json({ Error: 104 }); + return; + } + + const allianceMember = await AllianceMember.findOne({ + allianceId: req.query.allianceId, + guildId: guildMember.guildId + }); + if (!allianceMember || !allianceMember.Pending) { + res.status(400); + return; + } + allianceMember.Pending = false; + + const guild = (await Guild.findById(guildMember.guildId))!; + guild.AllianceId = allianceMember.allianceId; + + await Promise.all([allianceMember.save(), guild.save()]); + + // Give client the new alliance data which uses "AllianceId" instead of "_id" in this response + const alliance = (await Alliance.findById(allianceMember.allianceId))!; + const { _id, ...rest } = await getAllianceClient(alliance, guild); + res.json({ + AllianceId: _id, + ...rest + }); +}; diff --git a/src/controllers/api/confirmGuildInvitationController.ts b/src/controllers/api/confirmGuildInvitationController.ts new file mode 100644 index 00000000..9f43b893 --- /dev/null +++ b/src/controllers/api/confirmGuildInvitationController.ts @@ -0,0 +1,124 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Guild, GuildMember } from "@/src/models/guildModel"; +import { Account } from "@/src/models/loginModel"; +import { deleteGuild, getGuildClient, hasGuildPermission, removeDojoKeyItems } from "@/src/services/guildService"; +import { addRecipes, combineInventoryChanges, getInventory } from "@/src/services/inventoryService"; +import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { RequestHandler } from "express"; +import { Types } from "mongoose"; + +// GET request: A player accepting an invite they got in their inbox. +export const confirmGuildInvitationGetController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + const invitedGuildMember = await GuildMember.findOne({ + accountId: account._id, + guildId: req.query.clanId as string + }); + if (invitedGuildMember && invitedGuildMember.status == 2) { + let inventoryChanges: IInventoryChanges = {}; + + // If this account is already in a guild, we need to do cleanup first. + const guildMember = await GuildMember.findOneAndDelete({ accountId: account._id, status: 0 }); + if (guildMember) { + const inventory = await getInventory(account._id.toString(), "LevelKeys Recipes"); + inventoryChanges = removeDojoKeyItems(inventory); + await inventory.save(); + + if (guildMember.rank == 0) { + await deleteGuild(guildMember.guildId); + } + } + + // Now that we're sure this account is not in a guild right now, we can just proceed with the normal updates. + invitedGuildMember.status = 0; + await invitedGuildMember.save(); + + // Remove pending applications for this account + await GuildMember.deleteMany({ accountId: account._id, status: 1 }); + + // Update inventory of new member + const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes"); + inventory.GuildId = new Types.ObjectId(req.query.clanId as string); + const recipeChanges = [ + { + ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint", + ItemCount: 1 + } + ]; + addRecipes(inventory, recipeChanges); + combineInventoryChanges(inventoryChanges, { Recipes: recipeChanges }); + await inventory.save(); + + const guild = (await Guild.findById(req.query.clanId as string))!; + + // Add join to clan log + guild.RosterActivity ??= []; + guild.RosterActivity.push({ + dateTime: new Date(), + entryType: 6, + details: getSuffixedName(account) + }); + await guild.save(); + + res.json({ + ...(await getGuildClient(guild, account._id.toString())), + InventoryChanges: inventoryChanges + }); + } else { + res.end(); + } +}; + +// POST request: Clan representative accepting invite(s). +export const confirmGuildInvitationPostController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const guild = (await Guild.findById(req.query.clanId as string, "Ranks RosterActivity"))!; + if (!(await hasGuildPermission(guild, accountId, GuildPermission.Recruiter))) { + res.status(400).json("Invalid permission"); + return; + } + const payload = getJSONfromString<{ userId: string }>(String(req.body)); + const filter: { accountId?: string; status: number } = { status: 1 }; + if (payload.userId != "all") { + filter.accountId = payload.userId; + } + const guildMembers = await GuildMember.find(filter); + const newMembers: string[] = []; + for (const guildMember of guildMembers) { + guildMember.status = 0; + guildMember.RequestMsg = undefined; + guildMember.RequestExpiry = undefined; + await guildMember.save(); + + // Remove other pending applications for this account + await GuildMember.deleteMany({ accountId: guildMember.accountId, status: 1 }); + + // Update inventory of new member + const inventory = await getInventory(guildMember.accountId.toString(), "GuildId Recipes"); + inventory.GuildId = new Types.ObjectId(req.query.clanId as string); + addRecipes(inventory, [ + { + ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint", + ItemCount: 1 + } + ]); + await inventory.save(); + + // Add join to clan log + const account = (await Account.findOne({ _id: guildMember.accountId }))!; + guild.RosterActivity ??= []; + guild.RosterActivity.push({ + dateTime: new Date(), + entryType: 6, + details: getSuffixedName(account) + }); + + newMembers.push(account._id.toString()); + } + await guild.save(); + res.json({ + NewMembers: newMembers + }); +}; diff --git a/src/controllers/api/contributeGuildClassController.ts b/src/controllers/api/contributeGuildClassController.ts new file mode 100644 index 00000000..1a20f4ef --- /dev/null +++ b/src/controllers/api/contributeGuildClassController.ts @@ -0,0 +1,104 @@ +import { toMongoDate } from "@/src/helpers/inventoryHelpers"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Guild, GuildMember } from "@/src/models/guildModel"; +import { config } from "@/src/services/configService"; +import { createMessage } from "@/src/services/inboxService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; +import { Types } from "mongoose"; + +export const contributeGuildClassController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const payload = getJSONfromString(String(req.body)); + const guild = (await Guild.findById(payload.GuildId))!; + + // First contributor initiates ceremony and locks the pending class. + if (!guild.CeremonyContributors) { + guild.CeremonyContributors = []; + guild.CeremonyClass = guildXpToClass(guild.XP); + guild.CeremonyEndo = 0; + for (let i = guild.Class; i != guild.CeremonyClass; ++i) { + guild.CeremonyEndo += (i + 1) * 1000; + } + guild.ClassChanges ??= []; + guild.ClassChanges.push({ + dateTime: new Date(), + entryType: 13, + details: guild.CeremonyClass + }); + } + + guild.CeremonyContributors.push(new Types.ObjectId(accountId)); + + // Once required contributor count is hit, the class is committed and there's 72 hours to claim endo. + if (guild.CeremonyContributors.length == payload.RequiredContributors) { + guild.Class = guild.CeremonyClass!; + guild.CeremonyClass = undefined; + guild.CeremonyResetDate = new Date(Date.now() + (config.fastClanAscension ? 5_000 : 72 * 3600_000)); + if (!config.fastClanAscension) { + // Send message to all active guild members + const members = await GuildMember.find({ guildId: payload.GuildId, status: 0 }, "accountId"); + for (const member of members) { + // somewhat unfaithful as on live the "msg" is not a loctag, but since we don't have the string, we'll let the client fill it in with "arg". + await createMessage(member.accountId, [ + { + sndr: guild.Name, + msg: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgressDetails", + arg: [ + { + Key: "RESETDATE", + Tag: + guild.CeremonyResetDate.getUTCMonth() + + "/" + + guild.CeremonyResetDate.getUTCDate() + + "/" + + (guild.CeremonyResetDate.getUTCFullYear() % 100) + + " " + + guild.CeremonyResetDate.getUTCHours().toString().padStart(2, "0") + + ":" + + guild.CeremonyResetDate.getUTCMinutes().toString().padStart(2, "0") + } + ], + sub: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgress", + icon: "/Lotus/Interface/Graphics/ClanTileImages/ClanEnterDojo.png", + highPriority: true + } + ]); + } + } + } + + await guild.save(); + + // Either way, endo is given to the contributor. + const inventory = await getInventory(accountId, "FusionPoints"); + inventory.FusionPoints += guild.CeremonyEndo!; + await inventory.save(); + + res.json({ + NumContributors: guild.CeremonyContributors.length, + FusionPointReward: guild.CeremonyEndo, + Class: guild.Class, + CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined + }); +}; + +interface IContributeGuildClassRequest { + GuildId: string; + RequiredContributors: number; +} + +const guildXpToClass = (xp: number): number => { + const cummXp = [ + 0, 11000, 34000, 69000, 114000, 168000, 231000, 302000, 381000, 68000, 563000, 665000, 774000, 891000 + ]; + let highest = 0; + for (let i = 0; i != cummXp.length; ++i) { + if (xp < cummXp[i]) { + break; + } + highest = i; + } + return highest; +}; diff --git a/src/controllers/api/contributeToDojoComponentController.ts b/src/controllers/api/contributeToDojoComponentController.ts new file mode 100644 index 00000000..6d0016eb --- /dev/null +++ b/src/controllers/api/contributeToDojoComponentController.ts @@ -0,0 +1,168 @@ +import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; +import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; +import { + addGuildMemberMiscItemContribution, + getDojoClient, + getGuildForRequestEx, + hasAccessToDojo, + processDojoBuildMaterialsGathered, + scaleRequiredCount, + setDojoRoomLogFunded +} from "@/src/services/guildService"; +import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IDojoContributable, IGuildMemberDatabase } from "@/src/types/guildTypes"; +import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { RequestHandler } from "express"; +import { ExportDojoRecipes, IDojoBuild } from "warframe-public-export-plus"; + +interface IContributeToDojoComponentRequest { + ComponentId: string; + DecoId?: string; + DecoType?: string; + IngredientContributions: IMiscItem[]; + RegularCredits: number; + VaultIngredientContributions: IMiscItem[]; + VaultCredits: number; +} + +export const contributeToDojoComponentController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + // Any clan member should have permission to contribute although notably permission is denied if they have not crafted the dojo key and were simply invited in. + if (!hasAccessToDojo(inventory)) { + res.json({ DojoRequestStatus: -1 }); + return; + } + const guild = await getGuildForRequestEx(req, inventory); + const guildMember = (await GuildMember.findOne( + { accountId, guildId: guild._id }, + "RegularCreditsContributed MiscItemsContributed" + ))!; + const request = JSON.parse(String(req.body)) as IContributeToDojoComponentRequest; + const component = guild.DojoComponents.id(request.ComponentId)!; + + const inventoryChanges: IInventoryChanges = {}; + if (!component.CompletionTime) { + // Room is in "Collecting Materials" state + if (request.DecoId) { + throw new Error("attempt to contribute to a deco in an unfinished room?!"); + } + const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!; + processContribution(guild, guildMember, request, inventory, inventoryChanges, meta, component); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (component.CompletionTime) { + setDojoRoomLogFunded(guild, component); + } + } else { + // Room is past "Collecting Materials" + if (request.DecoId) { + const deco = component.Decos!.find(x => x._id.equals(request.DecoId))!; + const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type)!; + processContribution(guild, guildMember, request, inventory, inventoryChanges, meta, deco); + } + } + + await Promise.all([guild.save(), inventory.save(), guildMember.save()]); + res.json({ + ...(await getDojoClient(guild, 0, component._id)), + InventoryChanges: inventoryChanges + }); +}; + +const processContribution = ( + guild: TGuildDatabaseDocument, + guildMember: IGuildMemberDatabase, + request: IContributeToDojoComponentRequest, + inventory: TInventoryDatabaseDocument, + inventoryChanges: IInventoryChanges, + meta: IDojoBuild, + component: IDojoContributable +): void => { + component.RegularCredits ??= 0; + if (request.RegularCredits) { + component.RegularCredits += request.RegularCredits; + inventoryChanges.RegularCredits = -request.RegularCredits; + updateCurrency(inventory, request.RegularCredits, false); + + guildMember.RegularCreditsContributed ??= 0; + guildMember.RegularCreditsContributed += request.RegularCredits; + } + if (request.VaultCredits) { + component.RegularCredits += request.VaultCredits; + guild.VaultRegularCredits! -= request.VaultCredits; + } + if (component.RegularCredits > scaleRequiredCount(guild.Tier, meta.price)) { + guild.VaultRegularCredits ??= 0; + guild.VaultRegularCredits += component.RegularCredits - scaleRequiredCount(guild.Tier, meta.price); + component.RegularCredits = scaleRequiredCount(guild.Tier, meta.price); + } + + component.MiscItems ??= []; + if (request.VaultIngredientContributions.length) { + for (const ingredientContribution of request.VaultIngredientContributions) { + const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredientContribution.ItemType); + if (componentMiscItem) { + const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!; + if ( + componentMiscItem.ItemCount + ingredientContribution.ItemCount > + scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) + ) { + ingredientContribution.ItemCount = + scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) - componentMiscItem.ItemCount; + } + componentMiscItem.ItemCount += ingredientContribution.ItemCount; + } else { + component.MiscItems.push(ingredientContribution); + } + const vaultMiscItem = guild.VaultMiscItems!.find(x => x.ItemType == ingredientContribution.ItemType)!; + vaultMiscItem.ItemCount -= ingredientContribution.ItemCount; + } + } + if (request.IngredientContributions.length) { + const miscItemChanges: IMiscItem[] = []; + for (const ingredientContribution of request.IngredientContributions) { + const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredientContribution.ItemType); + if (componentMiscItem) { + const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!; + if ( + componentMiscItem.ItemCount + ingredientContribution.ItemCount > + scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) + ) { + ingredientContribution.ItemCount = + scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) - componentMiscItem.ItemCount; + } + componentMiscItem.ItemCount += ingredientContribution.ItemCount; + } else { + component.MiscItems.push(ingredientContribution); + } + miscItemChanges.push({ + ItemType: ingredientContribution.ItemType, + ItemCount: ingredientContribution.ItemCount * -1 + }); + + addGuildMemberMiscItemContribution(guildMember, ingredientContribution); + } + addMiscItems(inventory, miscItemChanges); + inventoryChanges.MiscItems = miscItemChanges; + } + + if (component.RegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) { + let fullyFunded = true; + for (const ingredient of meta.ingredients) { + const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredient.ItemType); + if ( + !componentMiscItem || + componentMiscItem.ItemCount < scaleRequiredCount(guild.Tier, ingredient.ItemCount) + ) { + fullyFunded = false; + break; + } + } + if (fullyFunded) { + component.CompletionTime = new Date(Date.now() + meta.time * 1000); + processDojoBuildMaterialsGathered(guild, meta); + } + } +}; diff --git a/src/controllers/api/contributeToVaultController.ts b/src/controllers/api/contributeToVaultController.ts new file mode 100644 index 00000000..77a93097 --- /dev/null +++ b/src/controllers/api/contributeToVaultController.ts @@ -0,0 +1,113 @@ +import { + Alliance, + Guild, + GuildMember, + TGuildDatabaseDocument, + TGuildMemberDatabaseDocument +} from "@/src/models/guildModel"; +import { + addGuildMemberMiscItemContribution, + addGuildMemberShipDecoContribution, + addVaultFusionTreasures, + addVaultMiscItems, + addVaultShipDecos, + getGuildForRequestEx +} from "@/src/services/guildService"; +import { + addFusionTreasures, + addMiscItems, + addShipDecorations, + getInventory, + updateCurrency +} from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; + +export const contributeToVaultController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId RegularCredits MiscItems ShipDecorations FusionTreasures"); + const request = JSON.parse(String(req.body)) as IContributeToVaultRequest; + + if (request.Alliance) { + const guild = await getGuildForRequestEx(req, inventory); + const alliance = (await Alliance.findById(guild.AllianceId!))!; + alliance.VaultRegularCredits ??= 0; + alliance.VaultRegularCredits += request.RegularCredits; + if (request.FromVault) { + guild.VaultRegularCredits! -= request.RegularCredits; + await Promise.all([guild.save(), alliance.save()]); + } else { + updateCurrency(inventory, request.RegularCredits, false); + await Promise.all([inventory.save(), alliance.save()]); + } + res.end(); + return; + } + + let guild: TGuildDatabaseDocument; + let guildMember: TGuildMemberDatabaseDocument | undefined; + if (request.GuildVault) { + guild = (await Guild.findById(request.GuildVault))!; + } else { + guild = await getGuildForRequestEx(req, inventory); + guildMember = (await GuildMember.findOne( + { accountId, guildId: guild._id }, + "RegularCreditsContributed MiscItemsContributed ShipDecorationsContributed" + ))!; + } + + if (request.RegularCredits) { + updateCurrency(inventory, request.RegularCredits, false); + + guild.VaultRegularCredits ??= 0; + guild.VaultRegularCredits += request.RegularCredits; + + if (guildMember) { + guildMember.RegularCreditsContributed ??= 0; + guildMember.RegularCreditsContributed += request.RegularCredits; + } + } + if (request.MiscItems.length) { + addVaultMiscItems(guild, request.MiscItems); + for (const item of request.MiscItems) { + if (guildMember) { + addGuildMemberMiscItemContribution(guildMember, item); + } + addMiscItems(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]); + } + } + if (request.ShipDecorations.length) { + addVaultShipDecos(guild, request.ShipDecorations); + for (const item of request.ShipDecorations) { + if (guildMember) { + addGuildMemberShipDecoContribution(guildMember, item); + } + addShipDecorations(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]); + } + } + if (request.FusionTreasures.length) { + addVaultFusionTreasures(guild, request.FusionTreasures); + for (const item of request.FusionTreasures) { + addFusionTreasures(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]); + } + } + + const promises: Promise[] = [guild.save(), inventory.save()]; + if (guildMember) { + promises.push(guildMember.save()); + } + await Promise.all(promises); + + res.end(); +}; + +interface IContributeToVaultRequest { + RegularCredits: number; + MiscItems: IMiscItem[]; + ShipDecorations: ITypeCount[]; + FusionTreasures: IFusionTreasure[]; + Alliance?: boolean; + FromVault?: boolean; + GuildVault?: string; +} diff --git a/src/controllers/api/createAllianceController.ts b/src/controllers/api/createAllianceController.ts new file mode 100644 index 00000000..e3a81a24 --- /dev/null +++ b/src/controllers/api/createAllianceController.ts @@ -0,0 +1,50 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel"; +import { getAllianceClient } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const createAllianceController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId"); + const guild = (await Guild.findById(inventory.GuildId!, "Name Tier AllianceId"))!; + if (guild.AllianceId) { + res.status(400).send("Guild is already in an alliance").end(); + return; + } + const guildMember = (await GuildMember.findOne({ guildId: guild._id, accountId }, "rank"))!; + if (guildMember.rank > 1) { + res.status(400).send("Invalid permission").end(); + return; + } + const data = getJSONfromString(String(req.body)); + const alliance = new Alliance({ Name: data.allianceName }); + try { + await alliance.save(); + } catch (e) { + res.status(400).send("Alliance name already in use").end(); + return; + } + guild.AllianceId = alliance._id; + await Promise.all([ + guild.save(), + AllianceMember.insertOne({ + allianceId: alliance._id, + guildId: guild._id, + Pending: false, + Permissions: + GuildPermission.Ruler | + GuildPermission.Promoter | + GuildPermission.Recruiter | + GuildPermission.Treasurer | + GuildPermission.ChatModerator + }) + ]); + res.json(await getAllianceClient(alliance, guild)); +}; + +interface ICreateAllianceRequest { + allianceName: string; +} diff --git a/src/controllers/api/createGuildController.ts b/src/controllers/api/createGuildController.ts index 8bc34408..9b5bc768 100644 --- a/src/controllers/api/createGuildController.ts +++ b/src/controllers/api/createGuildController.ts @@ -1,35 +1,52 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; -import { Guild } from "@/src/models/guildModel"; +import { Guild, GuildMember } from "@/src/models/guildModel"; +import { createUniqueClanName, getGuildClient } from "@/src/services/guildService"; +import { addRecipes, getInventory } from "@/src/services/inventoryService"; export const createGuildController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const payload = getJSONfromString(String(req.body)); + // Remove pending applications for this account + await GuildMember.deleteMany({ accountId, status: 1 }); + // Create guild on database const guild = new Guild({ - Name: payload.guildName + Name: await createUniqueClanName(payload.guildName) }); await guild.save(); - // Update inventory - const inventory = await Inventory.findOne({ accountOwnerId: accountId }); - if (inventory) { - // Set GuildId - inventory.GuildId = guild._id; + // Create guild member on database + await GuildMember.insertOne({ + accountId: accountId, + guildId: guild._id, + status: 0, + rank: 0 + }); - // Give clan key (TODO: This should only be a blueprint) - inventory.LevelKeys.push({ - ItemType: "/Lotus/Types/Keys/DojoKey", + const inventory = await getInventory(accountId, "GuildId Recipes"); + inventory.GuildId = guild._id; + addRecipes(inventory, [ + { + ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint", ItemCount: 1 - }); + } + ]); + await inventory.save(); - await inventory.save(); - } - - res.json(guild); + res.json({ + ...(await getGuildClient(guild, accountId)), + InventoryChanges: { + Recipes: [ + { + ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint", + ItemCount: 1 + } + ] + } + }); }; interface ICreateGuildRequest { diff --git a/src/controllers/api/creditsController.ts b/src/controllers/api/creditsController.ts index c5dea4f7..5fb60575 100644 --- a/src/controllers/api/creditsController.ts +++ b/src/controllers/api/creditsController.ts @@ -19,7 +19,7 @@ export const creditsController: RequestHandler = async (req, res) => { response.RegularCredits = 999999999; } if (config.infinitePlatinum) { - response.PremiumCreditsFree = 999999999; + response.PremiumCreditsFree = 0; response.PremiumCredits = 999999999; } diff --git a/src/controllers/api/customObstacleCourseLeaderboardController.ts b/src/controllers/api/customObstacleCourseLeaderboardController.ts new file mode 100644 index 00000000..9b768def --- /dev/null +++ b/src/controllers/api/customObstacleCourseLeaderboardController.ts @@ -0,0 +1,48 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Guild } from "@/src/models/guildModel"; +import { getAccountForRequest } from "@/src/services/loginService"; +import { logger } from "@/src/utils/logger"; +import { RequestHandler } from "express"; + +export const customObstacleCourseLeaderboardController: RequestHandler = async (req, res) => { + const data = getJSONfromString(String(req.body)); + const guild = (await Guild.findById(data.g, "DojoComponents"))!; + const component = guild.DojoComponents.id(data.c)!; + if (req.query.act == "f") { + res.json({ + results: component.Leaderboard ?? [] + }); + } else if (req.query.act == "p") { + const account = await getAccountForRequest(req); + component.Leaderboard ??= []; + const entry = component.Leaderboard.find(x => x.n == account.DisplayName); + if (entry) { + entry.s = data.s!; + } else { + component.Leaderboard.push({ + s: data.s!, + n: account.DisplayName, + r: 0 + }); + } + component.Leaderboard.sort((a, b) => a.s - b.s); // In this case, the score is the time in milliseconds, so smaller is better. + if (component.Leaderboard.length > 10) { + component.Leaderboard.shift(); + } + let r = 0; + for (const entry of component.Leaderboard) { + entry.r = ++r; + } + await guild.save(); + res.status(200).end(); + } else { + logger.debug(`data provided to ${req.path}: ${String(req.body)}`); + throw new Error(`unknown customObstacleCourseLeaderboard act: ${String(req.query.act)}`); + } +}; + +interface ICustomObstacleCourseLeaderboardRequest { + g: string; + c: string; + s?: number; // act=p +} diff --git a/src/controllers/api/customizeGuildRanksController.ts b/src/controllers/api/customizeGuildRanksController.ts new file mode 100644 index 00000000..5ddca7b6 --- /dev/null +++ b/src/controllers/api/customizeGuildRanksController.ts @@ -0,0 +1,21 @@ +import { getGuildForRequest, hasGuildPermission } from "@/src/services/guildService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission, IGuildRank } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const customizeGuildRanksController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const guild = await getGuildForRequest(req); + const payload = JSON.parse(String(req.body)) as ICustomizeGuildRanksRequest; + if (!(await hasGuildPermission(guild, accountId, GuildPermission.Ruler))) { + res.status(400).json("Invalid permission"); + return; + } + guild.Ranks = payload.GuildRanks; + await guild.save(); + res.end(); +}; + +interface ICustomizeGuildRanksRequest { + GuildRanks: IGuildRank[]; +} diff --git a/src/controllers/api/declineAllianceInviteController.ts b/src/controllers/api/declineAllianceInviteController.ts new file mode 100644 index 00000000..2d9f9dd6 --- /dev/null +++ b/src/controllers/api/declineAllianceInviteController.ts @@ -0,0 +1,17 @@ +import { AllianceMember, GuildMember } from "@/src/models/guildModel"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const declineAllianceInviteController: RequestHandler = async (req, res) => { + // Check requester is a warlord in their guild + const accountId = await getAccountIdForRequest(req); + const guildMember = (await GuildMember.findOne({ accountId, status: 0 }))!; + if (guildMember.rank > 1) { + res.status(400).json({ Error: 104 }); + return; + } + + await AllianceMember.deleteOne({ allianceId: req.query.allianceId, guildId: guildMember.guildId }); + + res.end(); +}; diff --git a/src/controllers/api/declineGuildInviteController.ts b/src/controllers/api/declineGuildInviteController.ts new file mode 100644 index 00000000..c2bcd073 --- /dev/null +++ b/src/controllers/api/declineGuildInviteController.ts @@ -0,0 +1,14 @@ +import { GuildMember } from "@/src/models/guildModel"; +import { getAccountForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const declineGuildInviteController: RequestHandler = async (req, res) => { + const accountId = await getAccountForRequest(req); + + await GuildMember.deleteOne({ + accountId: accountId, + guildId: req.query.clanId as string + }); + + res.end(); +}; diff --git a/src/controllers/api/destroyDojoDecoController.ts b/src/controllers/api/destroyDojoDecoController.ts new file mode 100644 index 00000000..02323720 --- /dev/null +++ b/src/controllers/api/destroyDojoDecoController.ts @@ -0,0 +1,33 @@ +import { + getDojoClient, + getGuildForRequestEx, + hasAccessToDojo, + hasGuildPermission, + removeDojoDeco +} from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const destroyDojoDecoController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId LevelKeys"); + const guild = await getGuildForRequestEx(req, inventory); + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Decorator))) { + res.json({ DojoRequestStatus: -1 }); + return; + } + const request = JSON.parse(String(req.body)) as IDestroyDojoDecoRequest; + + removeDojoDeco(guild, request.ComponentId, request.DecoId); + + await guild.save(); + res.json(await getDojoClient(guild, 0, request.ComponentId)); +}; + +interface IDestroyDojoDecoRequest { + DecoType: string; + ComponentId: string; + DecoId: string; +} diff --git a/src/controllers/api/divvyAllianceVaultController.ts b/src/controllers/api/divvyAllianceVaultController.ts new file mode 100644 index 00000000..8847a19f --- /dev/null +++ b/src/controllers/api/divvyAllianceVaultController.ts @@ -0,0 +1,67 @@ +import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel"; +import { getAccountForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { parallelForeach } from "@/src/utils/async-utils"; +import { logger } from "@/src/utils/logger"; +import { RequestHandler } from "express"; + +export const divvyAllianceVaultController: RequestHandler = async (req, res) => { + // Afaict, there's no way to put anything other than credits in the alliance vault (anymore?), so just no-op if this is not a request to divvy credits. + if (req.query.credits == "1") { + // Check requester is a warlord in their guild + const account = await getAccountForRequest(req); + const guildMember = (await GuildMember.findOne({ accountId: account._id, status: 0 }))!; + if (guildMember.rank > 1) { + res.status(400).end(); + return; + } + + // Check guild has treasurer permissions in the alliance + const allianceMember = (await AllianceMember.findOne({ + allianceId: req.query.allianceId, + guildId: guildMember.guildId + }))!; + if (!(allianceMember.Permissions & GuildPermission.Treasurer)) { + res.status(400).end(); + return; + } + + const allianceMembers = await AllianceMember.find({ allianceId: req.query.allianceId }); + const memberCounts: Record = {}; + let totalMembers = 0; + await parallelForeach(allianceMembers, async allianceMember => { + const memberCount = await GuildMember.countDocuments({ + guildId: allianceMember.guildId + }); + memberCounts[allianceMember.guildId.toString()] = memberCount; + totalMembers += memberCount; + }); + logger.debug(`alliance has ${totalMembers} members between all its clans`); + + const alliance = (await Alliance.findById(allianceMember.allianceId, "VaultRegularCredits"))!; + if (alliance.VaultRegularCredits) { + let creditsHandedOutInTotal = 0; + await parallelForeach(allianceMembers, async allianceMember => { + const memberCount = memberCounts[allianceMember.guildId.toString()]; + const cutPercentage = memberCount / totalMembers; + const creditsToHandOut = Math.trunc(alliance.VaultRegularCredits! * cutPercentage); + logger.debug( + `${allianceMember.guildId.toString()} has ${memberCount} member(s) = ${Math.trunc(cutPercentage * 100)}% of alliance; giving ${creditsToHandOut} credit(s)` + ); + if (creditsToHandOut != 0) { + await Guild.updateOne( + { _id: allianceMember.guildId }, + { $inc: { VaultRegularCredits: creditsToHandOut } } + ); + creditsHandedOutInTotal += creditsToHandOut; + } + }); + alliance.VaultRegularCredits -= creditsHandedOutInTotal; + logger.debug( + `handed out ${creditsHandedOutInTotal} credits; alliance vault now has ${alliance.VaultRegularCredits} credit(s)` + ); + } + await alliance.save(); + } + res.end(); +}; diff --git a/src/controllers/api/dojoComponentRushController.ts b/src/controllers/api/dojoComponentRushController.ts new file mode 100644 index 00000000..899ed0af --- /dev/null +++ b/src/controllers/api/dojoComponentRushController.ts @@ -0,0 +1,76 @@ +import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; +import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, scaleRequiredCount } from "@/src/services/guildService"; +import { getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IDojoContributable } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; +import { ExportDojoRecipes, IDojoBuild } from "warframe-public-export-plus"; + +interface IDojoComponentRushRequest { + DecoType?: string; + DecoId?: string; + ComponentId: string; + Amount: number; + VaultAmount: number; + AllianceVaultAmount: number; +} + +export const dojoComponentRushController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + if (!hasAccessToDojo(inventory)) { + res.json({ DojoRequestStatus: -1 }); + return; + } + const guild = await getGuildForRequestEx(req, inventory); + const request = JSON.parse(String(req.body)) as IDojoComponentRushRequest; + const component = guild.DojoComponents.id(request.ComponentId)!; + + let platinumDonated = request.Amount; + const inventoryChanges = updateCurrency(inventory, request.Amount, true); + if (request.VaultAmount) { + platinumDonated += request.VaultAmount; + guild.VaultPremiumCredits! -= request.VaultAmount; + } + + if (request.DecoId) { + const deco = component.Decos!.find(x => x._id.equals(request.DecoId))!; + const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type)!; + processContribution(guild, deco, meta, platinumDonated); + } else { + const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!; + processContribution(guild, component, meta, platinumDonated); + + const entry = guild.RoomChanges?.find(x => x.componentId.equals(component._id)); + if (entry) { + entry.dateTime = component.CompletionTime!; + } + } + + const guildMember = (await GuildMember.findOne({ accountId, guildId: guild._id }, "PremiumCreditsContributed"))!; + guildMember.PremiumCreditsContributed ??= 0; + guildMember.PremiumCreditsContributed += request.Amount; + + await Promise.all([guild.save(), inventory.save(), guildMember.save()]); + + res.json({ + ...(await getDojoClient(guild, 0, component._id)), + InventoryChanges: inventoryChanges + }); +}; + +const processContribution = ( + guild: TGuildDatabaseDocument, + component: IDojoContributable, + meta: IDojoBuild, + platinumDonated: number +): void => { + const fullPlatinumCost = scaleRequiredCount(guild.Tier, meta.skipTimePrice); + const fullDurationSeconds = meta.time; + const secondsPerPlatinum = fullDurationSeconds / fullPlatinumCost; + component.CompletionTime = new Date( + component.CompletionTime!.getTime() - secondsPerPlatinum * platinumDonated * 1000 + ); + component.RushPlatinum ??= 0; + component.RushPlatinum += platinumDonated; +}; diff --git a/src/controllers/api/dronesController.ts b/src/controllers/api/dronesController.ts index bff5086c..97e0d478 100644 --- a/src/controllers/api/dronesController.ts +++ b/src/controllers/api/dronesController.ts @@ -1,7 +1,143 @@ +import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; +import { config } from "@/src/services/configService"; +import { addMiscItems, getInventory } from "@/src/services/inventoryService"; +import { fromStoreItem } from "@/src/services/itemDataService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getRandomInt, getRandomWeightedRewardUc } from "@/src/services/rngService"; +import { IMongoDate, IOid } from "@/src/types/commonTypes"; +import { IDroneClient } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { RequestHandler } from "express"; +import { ExportDrones, ExportResources, ExportSystems } from "warframe-public-export-plus"; -const dronesController: RequestHandler = (_req, res) => { - res.json({}); +export const dronesController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + if ("GetActive" in req.query) { + const inventory = await getInventory(accountId, "Drones"); + const activeDrones: IActiveDrone[] = []; + for (const drone of inventory.Drones) { + if (drone.DeployTime) { + activeDrones.push({ + DeployTime: toMongoDate(drone.DeployTime), + System: drone.System!, + ItemId: toOid(drone._id), + ItemType: drone.ItemType, + CurrentHP: drone.CurrentHP, + DamageTime: toMongoDate(drone.DamageTime!), + PendingDamage: drone.PendingDamage!, + Resources: [ + { + ItemType: drone.ResourceType!, + BinTotal: drone.ResourceCount!, + StartTime: toMongoDate(drone.DeployTime) + } + ] + }); + } + } + res.json({ + ActiveDrones: activeDrones + }); + } else if ("droneId" in req.query && "systemIndex" in req.query) { + const inventory = await getInventory(accountId, "Drones"); + const drone = inventory.Drones.id(req.query.droneId as string)!; + const droneMeta = ExportDrones[drone.ItemType]; + drone.DeployTime = config.instantResourceExtractorDrones ? new Date(0) : new Date(); + if (drone.RepairStart) { + const repairMinutes = (Date.now() - drone.RepairStart.getTime()) / 60_000; + const hpPerMinute = droneMeta.repairRate / 60; + drone.CurrentHP = Math.min(drone.CurrentHP + Math.round(repairMinutes * hpPerMinute), droneMeta.durability); + drone.RepairStart = undefined; + } + drone.System = parseInt(req.query.systemIndex as string); + const system = ExportSystems[drone.System - 1]; + drone.DamageTime = config.instantResourceExtractorDrones + ? new Date() + : new Date(Date.now() + getRandomInt(3 * 3600 * 1000, 4 * 3600 * 1000)); + drone.PendingDamage = + Math.random() < system.damageChance + ? getRandomInt(system.droneDamage.minValue, system.droneDamage.maxValue) + : 0; + const resource = getRandomWeightedRewardUc(system.resources, droneMeta.probabilities)!; + //logger.debug(`drone rolled`, resource); + drone.ResourceType = fromStoreItem(resource.StoreItem); + const resourceMeta = ExportResources[drone.ResourceType]; + if (resourceMeta.pickupQuantity) { + const pickupsToCollect = droneMeta.binCapacity * droneMeta.capacityMultipliers[resource.Rarity]; + drone.ResourceCount = 0; + for (let i = 0; i != pickupsToCollect; ++i) { + drone.ResourceCount += getRandomInt( + resourceMeta.pickupQuantity.minValue, + resourceMeta.pickupQuantity.maxValue + ); + } + } else { + drone.ResourceCount = 1; + } + await inventory.save(); + res.json({}); + } else if ("collectDroneId" in req.query) { + const inventory = await getInventory(accountId); + const drone = inventory.Drones.id(req.query.collectDroneId as string)!; + + if (new Date() >= drone.DamageTime!) { + drone.CurrentHP -= drone.PendingDamage!; + drone.RepairStart = new Date(); + } + + const inventoryChanges: IInventoryChanges = {}; + if (drone.CurrentHP <= 0) { + inventory.RegularCredits += 100; + inventoryChanges.RegularCredits = 100; + inventory.Drones.pull({ _id: req.query.collectDroneId as string }); + inventoryChanges.RemovedIdItems = [ + { + ItemId: { $oid: req.query.collectDroneId } + } + ]; + } else { + const completionTime = drone.DeployTime!.getTime() + ExportDrones[drone.ItemType].fillRate * 3600_000; + if (Date.now() >= completionTime) { + const miscItemChanges = [ + { + ItemType: drone.ResourceType!, + ItemCount: drone.ResourceCount! + } + ]; + addMiscItems(inventory, miscItemChanges); + inventoryChanges.MiscItems = miscItemChanges; + } + + drone.DeployTime = undefined; + drone.System = undefined; + drone.DamageTime = undefined; + drone.PendingDamage = undefined; + drone.ResourceType = undefined; + drone.ResourceCount = undefined; + + inventoryChanges.Drones = [drone.toJSON()]; + } + + await inventory.save(); + res.json({ + InventoryChanges: inventoryChanges + }); + } else { + throw new Error(`drones.php query not handled`); + } }; -export { dronesController }; +interface IActiveDrone { + DeployTime: IMongoDate; + System: number; + ItemId: IOid; + ItemType: string; + CurrentHP: number; + DamageTime: IMongoDate; + PendingDamage: number; + Resources: { + ItemType: string; + BinTotal: number; + StartTime: IMongoDate; + }[]; +} diff --git a/src/controllers/api/entratiLabConquestModeController.ts b/src/controllers/api/entratiLabConquestModeController.ts new file mode 100644 index 00000000..ae7b5845 --- /dev/null +++ b/src/controllers/api/entratiLabConquestModeController.ts @@ -0,0 +1,69 @@ +import { toMongoDate } from "@/src/helpers/inventoryHelpers"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const entratiLabConquestModeController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory( + accountId, + "EntratiVaultCountResetDate EntratiVaultCountLastPeriod EntratiLabConquestUnlocked EchoesHexConquestUnlocked EchoesHexConquestActiveFrameVariants EchoesHexConquestActiveStickers EntratiLabConquestActiveFrameVariants EntratiLabConquestCacheScoreMission EchoesHexConquestCacheScoreMission" + ); + const body = getJSONfromString(String(req.body)); + if (!inventory.EntratiVaultCountResetDate || Date.now() >= inventory.EntratiVaultCountResetDate.getTime()) { + const EPOCH = 1734307200 * 1000; // Mondays, amirite? + const day = Math.trunc((Date.now() - EPOCH) / 86400000); + const week = Math.trunc(day / 7); + const weekStart = EPOCH + week * 604800000; + const weekEnd = weekStart + 604800000; + inventory.EntratiVaultCountLastPeriod = 0; + inventory.EntratiVaultCountResetDate = new Date(weekEnd); + if (inventory.EntratiLabConquestUnlocked) { + inventory.EntratiLabConquestUnlocked = 0; + inventory.EntratiLabConquestActiveFrameVariants = []; + } + if (inventory.EchoesHexConquestUnlocked) { + inventory.EchoesHexConquestUnlocked = 0; + inventory.EchoesHexConquestActiveFrameVariants = []; + inventory.EchoesHexConquestActiveStickers = []; + } + } + if (body.BuyMode) { + inventory.EntratiVaultCountLastPeriod! += 2; + if (body.IsEchoesDeepArchemedea) { + inventory.EchoesHexConquestUnlocked = 1; + } else { + inventory.EntratiLabConquestUnlocked = 1; + } + } + if (body.IsEchoesDeepArchemedea) { + if (inventory.EchoesHexConquestUnlocked) { + inventory.EchoesHexConquestActiveFrameVariants = body.EchoesHexConquestActiveFrameVariants!; + inventory.EchoesHexConquestActiveStickers = body.EchoesHexConquestActiveStickers!; + } + } else { + if (inventory.EntratiLabConquestUnlocked) { + inventory.EntratiLabConquestActiveFrameVariants = body.EntratiLabConquestActiveFrameVariants!; + } + } + await inventory.save(); + res.json({ + EntratiVaultCountResetDate: toMongoDate(inventory.EntratiVaultCountResetDate), + EntratiVaultCountLastPeriod: inventory.EntratiVaultCountLastPeriod, + EntratiLabConquestUnlocked: inventory.EntratiLabConquestUnlocked, + EntratiLabConquestCacheScoreMission: inventory.EntratiLabConquestCacheScoreMission, + EchoesHexConquestUnlocked: inventory.EchoesHexConquestUnlocked, + EchoesHexConquestCacheScoreMission: inventory.EchoesHexConquestCacheScoreMission + }); +}; + +interface IEntratiLabConquestModeRequest { + BuyMode?: number; + IsEchoesDeepArchemedea?: number; + EntratiLabConquestUnlocked?: number; + EntratiLabConquestActiveFrameVariants?: string[]; + EchoesHexConquestUnlocked?: number; + EchoesHexConquestActiveFrameVariants?: string[]; + EchoesHexConquestActiveStickers?: string[]; +} diff --git a/src/controllers/api/evolveWeaponController.ts b/src/controllers/api/evolveWeaponController.ts index 3b2550bb..395b3340 100644 --- a/src/controllers/api/evolveWeaponController.ts +++ b/src/controllers/api/evolveWeaponController.ts @@ -17,7 +17,7 @@ export const evolveWeaponController: RequestHandler = async (req, res) => { recipe.ingredients.map(x => ({ ItemType: x.ItemType, ItemCount: x.ItemCount * -1 })) ); - const item = inventory[payload.Category].find(item => item._id.toString() == (req.query.ItemId as string))!; + const item = inventory[payload.Category].id(req.query.ItemId as string)!; item.Features ??= 0; item.Features |= EquipmentFeatures.INCARNON_GENESIS; @@ -39,7 +39,7 @@ export const evolveWeaponController: RequestHandler = async (req, res) => { } ]); - const item = inventory[payload.Category].find(item => item._id.toString() == (req.query.ItemId as string))!; + const item = inventory[payload.Category].id(req.query.ItemId as string)!; item.Features! &= ~EquipmentFeatures.INCARNON_GENESIS; } else { throw new Error(`unexpected evolve weapon action: ${payload.Action}`); diff --git a/src/controllers/api/focusController.ts b/src/controllers/api/focusController.ts index 29c92bf2..0d2b3f65 100644 --- a/src/controllers/api/focusController.ts +++ b/src/controllers/api/focusController.ts @@ -1,10 +1,11 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getInventory, addMiscItems, addEquipment } from "@/src/services/inventoryService"; -import { IMiscItem, TFocusPolarity, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; +import { getInventory, addMiscItems, addEquipment, occupySlot } from "@/src/services/inventoryService"; +import { IMiscItem, TFocusPolarity, TEquipmentKey, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { logger } from "@/src/utils/logger"; import { ExportFocusUpgrades } from "warframe-public-export-plus"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; export const focusController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -17,17 +18,15 @@ export const focusController: RequestHandler = async (req, res) => { case FocusOperation.InstallLens: { const request = JSON.parse(String(req.body)) as ILensInstallRequest; const inventory = await getInventory(accountId); - for (const item of inventory[request.Category]) { - if (item._id.toString() == request.WeaponId) { - item.FocusLens = request.LensType; - addMiscItems(inventory, [ - { - ItemType: request.LensType, - ItemCount: -1 - } satisfies IMiscItem - ]); - break; - } + const item = inventory[request.Category].id(request.WeaponId); + if (item) { + item.FocusLens = request.LensType; + addMiscItems(inventory, [ + { + ItemType: request.LensType, + ItemCount: -1 + } satisfies IMiscItem + ]); } await inventory.save(); res.json({ @@ -55,9 +54,16 @@ export const focusController: RequestHandler = async (req, res) => { } case FocusOperation.ActivateWay: { const focusType = (JSON.parse(String(req.body)) as IWayRequest).FocusType; - const inventory = await getInventory(accountId); - inventory.FocusAbility = focusType; - await inventory.save(); + + await Inventory.updateOne( + { + accountOwnerId: accountId + }, + { + FocusAbility: focusType + } + ); + res.end(); break; } @@ -105,6 +111,7 @@ export const focusController: RequestHandler = async (req, res) => { ]; const inventory = await getInventory(accountId); const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, parts); + occupySlot(inventory, InventorySlot.AMPS, false); await inventory.save(); res.json((inventoryChanges.OperatorAmps as IEquipmentClient[])[0]); break; diff --git a/src/controllers/api/fusionTreasuresController.ts b/src/controllers/api/fusionTreasuresController.ts index fa01ff97..086f017e 100644 --- a/src/controllers/api/fusionTreasuresController.ts +++ b/src/controllers/api/fusionTreasuresController.ts @@ -23,12 +23,11 @@ export const fusionTreasuresController: RequestHandler = async (req, res) => { const inventory = await getInventory(accountId); const request = JSON.parse(String(req.body)) as IFusionTreasureRequest; + // Swap treasures const oldTreasure = parseFusionTreasure(request.oldTreasureName, -1); const newTreasure = parseFusionTreasure(request.newTreasureName, 1); - - // Swap treasures - addFusionTreasures(inventory, [oldTreasure]); - addFusionTreasures(inventory, [newTreasure]); + const fusionTreasureChanges = [oldTreasure, newTreasure]; + addFusionTreasures(inventory, fusionTreasureChanges); // Remove consumed stars const miscItemChanges: IMiscItem[] = []; @@ -45,5 +44,9 @@ export const fusionTreasuresController: RequestHandler = async (req, res) => { addMiscItems(inventory, miscItemChanges); await inventory.save(); - res.end(); + // The response itself is the inventory changes for this endpoint. + res.json({ + MiscItems: miscItemChanges, + FusionTreasures: fusionTreasureChanges + }); }; diff --git a/src/controllers/api/genericUpdateController.ts b/src/controllers/api/genericUpdateController.ts index 79b6ba44..e5f0b593 100644 --- a/src/controllers/api/genericUpdateController.ts +++ b/src/controllers/api/genericUpdateController.ts @@ -10,8 +10,7 @@ import { IGenericUpdate } from "@/src/types/genericUpdate"; const genericUpdateController: RequestHandler = async (request, response) => { const accountId = await getAccountIdForRequest(request); const update = getJSONfromString(String(request.body)); - await updateGeneric(update, accountId); - response.json(update); + response.json(await updateGeneric(update, accountId)); }; export { genericUpdateController }; diff --git a/src/controllers/api/getAllianceController.ts b/src/controllers/api/getAllianceController.ts index 391dae5f..5da0966d 100644 --- a/src/controllers/api/getAllianceController.ts +++ b/src/controllers/api/getAllianceController.ts @@ -1,7 +1,25 @@ +import { Alliance, Guild } from "@/src/models/guildModel"; +import { getAllianceClient } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; import { RequestHandler } from "express"; -const getAllianceController: RequestHandler = (_req, res) => { - res.sendStatus(200); +export const getAllianceController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId"); + if (inventory.GuildId) { + const guild = (await Guild.findById(inventory.GuildId, "Name Tier AllianceId"))!; + if (guild.AllianceId) { + const alliance = (await Alliance.findById(guild.AllianceId))!; + res.json(await getAllianceClient(alliance, guild)); + return; + } + } + res.end(); }; -export { getAllianceController }; +/*interface IGetAllianceRequest { + memberCount: number; + clanLeaderName: string; + clanLeaderId: string; +}*/ diff --git a/src/controllers/api/getFriendsController.ts b/src/controllers/api/getFriendsController.ts index 292e107c..1227f84d 100644 --- a/src/controllers/api/getFriendsController.ts +++ b/src/controllers/api/getFriendsController.ts @@ -1,5 +1,6 @@ import { Request, Response } from "express"; +// POST with {} instead of GET as of 38.5.0 const getFriendsController = (_request: Request, response: Response): void => { response.writeHead(200, { //Connection: "keep-alive", diff --git a/src/controllers/api/getGuildContributionsController.ts b/src/controllers/api/getGuildContributionsController.ts new file mode 100644 index 00000000..c17729f7 --- /dev/null +++ b/src/controllers/api/getGuildContributionsController.ts @@ -0,0 +1,19 @@ +import { GuildMember } from "@/src/models/guildModel"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IGuildMemberClient } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const getGuildContributionsController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const guildId = (await getInventory(accountId, "GuildId")).GuildId; + const guildMember = (await GuildMember.findOne({ guildId, accountId: req.query.buddyId }))!; + res.json({ + _id: { $oid: req.query.buddyId as string }, + RegularCreditsContributed: guildMember.RegularCreditsContributed, + PremiumCreditsContributed: guildMember.PremiumCreditsContributed, + MiscItemsContributed: guildMember.MiscItemsContributed, + ConsumablesContributed: [], // ??? + ShipDecorationsContributed: guildMember.ShipDecorationsContributed + } satisfies Partial); +}; diff --git a/src/controllers/api/getGuildController.ts b/src/controllers/api/getGuildController.ts index d112f996..b834c289 100644 --- a/src/controllers/api/getGuildController.ts +++ b/src/controllers/api/getGuildController.ts @@ -1,73 +1,32 @@ import { RequestHandler } from "express"; -import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; import { Guild } from "@/src/models/guildModel"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { toOid } from "@/src/helpers/inventoryHelpers"; +import { logger } from "@/src/utils/logger"; +import { getInventory } from "@/src/services/inventoryService"; +import { createUniqueClanName, getGuildClient } from "@/src/services/guildService"; -const getGuildController: RequestHandler = async (req, res) => { +export const getGuildController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await Inventory.findOne({ accountOwnerId: accountId }); - if (!inventory) { - res.status(400).json({ error: "inventory was undefined" }); - return; - } + const inventory = await getInventory(accountId, "GuildId"); if (inventory.GuildId) { - const guild = await Guild.findOne({ _id: inventory.GuildId }); + const guild = await Guild.findById(inventory.GuildId); if (guild) { - res.json({ - _id: toOid(guild._id), - Name: guild.Name, - Members: [ - { - _id: { $oid: req.query.accountId }, - Rank: 0, - Status: 0 - } - ], - Ranks: [ - { - Name: "/Lotus/Language/Game/Rank_Creator", - Permissions: 16351 - }, - { - Name: "/Lotus/Language/Game/Rank_Warlord", - Permissions: 14303 - }, - { - Name: "/Lotus/Language/Game/Rank_General", - Permissions: 4318 - }, - { - Name: "/Lotus/Language/Game/Rank_Officer", - Permissions: 4314 - }, - { - Name: "/Lotus/Language/Game/Rank_Leader", - Permissions: 4106 - }, - { - Name: "/Lotus/Language/Game/Rank_Sage", - Permissions: 4304 - }, - { - Name: "/Lotus/Language/Game/Rank_Soldier", - Permissions: 4098 - }, - { - Name: "/Lotus/Language/Game/Rank_Initiate", - Permissions: 4096 - }, - { - Name: "/Lotus/Language/Game/Rank_Utility", - Permissions: 4096 - } - ], - Tier: 1 - }); + // Handle guilds created before we added discriminators + if (guild.Name.indexOf("#") == -1) { + guild.Name = await createUniqueClanName(guild.Name); + await guild.save(); + } + + if (guild.CeremonyResetDate && Date.now() >= guild.CeremonyResetDate.getTime()) { + logger.debug(`ascension ceremony is over`); + guild.CeremonyEndo = undefined; + guild.CeremonyContributors = undefined; + guild.CeremonyResetDate = undefined; + await guild.save(); + } + res.json(await getGuildClient(guild, accountId)); return; } } - res.json({}); + res.end(); }; - -export { getGuildController }; diff --git a/src/controllers/api/getGuildDojoController.ts b/src/controllers/api/getGuildDojoController.ts index 9d7ed93f..3c48a4f4 100644 --- a/src/controllers/api/getGuildDojoController.ts +++ b/src/controllers/api/getGuildDojoController.ts @@ -6,24 +6,28 @@ import { getDojoClient } from "@/src/services/guildService"; export const getGuildDojoController: RequestHandler = async (req, res) => { const guildId = req.query.guildId as string; - const guild = await Guild.findOne({ _id: guildId }); + const guild = await Guild.findById(guildId); if (!guild) { res.status(404).end(); return; } // Populate dojo info if not present - if (!guild.DojoComponents || guild.DojoComponents.length == 0) { - guild.DojoComponents = [ - { - _id: new Types.ObjectId(), - pf: "/Lotus/Levels/ClanDojo/DojoHall.level", - ppf: "", - CompletionTime: new Date(Date.now()) - } - ]; + if (guild.DojoComponents.length == 0) { + guild.DojoComponents.push({ + _id: new Types.ObjectId(), + pf: "/Lotus/Levels/ClanDojo/DojoHall.level", + ppf: "", + CompletionTime: new Date(Date.now()), + DecoCapacity: 600 + }); await guild.save(); } - res.json(getDojoClient(guild, 0)); + const payload: IGetGuildDojoRequest = req.body ? (JSON.parse(String(req.body)) as IGetGuildDojoRequest) : {}; + res.json(await getDojoClient(guild, 0, payload.ComponentId)); }; + +interface IGetGuildDojoRequest { + ComponentId?: string; +} diff --git a/src/controllers/api/getGuildLogController.ts b/src/controllers/api/getGuildLogController.ts index 2919ce31..037d07be 100644 --- a/src/controllers/api/getGuildLogController.ts +++ b/src/controllers/api/getGuildLogController.ts @@ -1,11 +1,60 @@ +import { toMongoDate } from "@/src/helpers/inventoryHelpers"; +import { Guild } from "@/src/models/guildModel"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IMongoDate } from "@/src/types/commonTypes"; import { RequestHandler } from "express"; -export const getGuildLogController: RequestHandler = (_req, res) => { - res.json({ - RoomChanges: [], - TechChanges: [], - RosterActivity: [], - StandingsUpdates: [], - ClassChanges: [] - }); +export const getGuildLogController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId"); + if (inventory.GuildId) { + const guild = await Guild.findById(inventory.GuildId); + if (guild) { + const log: Record = { + RoomChanges: [], + TechChanges: [], + RosterActivity: [], + StandingsUpdates: [], + ClassChanges: [] + }; + guild.RoomChanges?.forEach(entry => { + log.RoomChanges.push({ + dateTime: toMongoDate(entry.dateTime ?? new Date()), + entryType: entry.entryType, + details: entry.details + }); + }); + guild.TechChanges?.forEach(entry => { + log.TechChanges.push({ + dateTime: toMongoDate(entry.dateTime ?? new Date()), + entryType: entry.entryType, + details: entry.details + }); + }); + guild.RosterActivity?.forEach(entry => { + log.RosterActivity.push({ + dateTime: toMongoDate(entry.dateTime), + entryType: entry.entryType, + details: entry.details + }); + }); + guild.ClassChanges?.forEach(entry => { + log.ClassChanges.push({ + dateTime: toMongoDate(entry.dateTime), + entryType: entry.entryType, + details: entry.details + }); + }); + res.json(log); + return; + } + } + res.sendStatus(200); }; + +interface IGuildLogEntryClient { + dateTime: IMongoDate; + entryType: number; + details: number | string; +} diff --git a/src/controllers/api/getNewRewardSeedController.ts b/src/controllers/api/getNewRewardSeedController.ts index bbb3f71b..4ff82405 100644 --- a/src/controllers/api/getNewRewardSeedController.ts +++ b/src/controllers/api/getNewRewardSeedController.ts @@ -1,11 +1,21 @@ +import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; +import { generateRewardSeed } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; -export const getNewRewardSeedController: RequestHandler = (_req, res) => { - res.json({ rewardSeed: generateRewardSeed() }); -}; +export const getNewRewardSeedController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); -export function generateRewardSeed(): number { - const min = -Number.MAX_SAFE_INTEGER; - const max = Number.MAX_SAFE_INTEGER; - return Math.floor(Math.random() * (max - min + 1)) + min; -} + const rewardSeed = generateRewardSeed(); + logger.debug(`generated new reward seed: ${rewardSeed}`); + await Inventory.updateOne( + { + accountOwnerId: accountId + }, + { + RewardSeed: rewardSeed + } + ); + res.json({ rewardSeed: rewardSeed }); +}; diff --git a/src/controllers/api/giftingController.ts b/src/controllers/api/giftingController.ts new file mode 100644 index 00000000..f965e60c --- /dev/null +++ b/src/controllers/api/giftingController.ts @@ -0,0 +1,92 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Account } from "@/src/models/loginModel"; +import { createMessage } from "@/src/services/inboxService"; +import { getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; +import { IOid } from "@/src/types/commonTypes"; +import { IPurchaseParams } from "@/src/types/purchaseTypes"; +import { RequestHandler } from "express"; +import { ExportFlavour } from "warframe-public-export-plus"; + +export const giftingController: RequestHandler = async (req, res) => { + const data = getJSONfromString(String(req.body)); + if (data.PurchaseParams.Source != 0 || !data.PurchaseParams.UsePremium) { + throw new Error(`unexpected purchase params in gifting request: ${String(req.body)}`); + } + + const account = await Account.findOne( + data.RecipientId ? { _id: data.RecipientId.$oid } : { DisplayName: data.Recipient } + ); + if (!account) { + res.status(400).send("9").end(); + return; + } + const inventory = await getInventory(account._id.toString(), "Suits Settings"); + + // Cannot gift items to players that have not completed the tutorial. + if (inventory.Suits.length == 0) { + res.status(400).send("14").end(); + return; + } + + // Cannot gift to players who have gifting disabled. + // TODO: Also consider GIFT_MODE_FRIENDS once friends are implemented + if (inventory.Settings?.GiftMode == "GIFT_MODE_NONE") { + res.status(400).send("17").end(); + return; + } + + // TODO: Cannot gift items with mastery requirement to players who are too low level. (Code 2) + // TODO: Cannot gift archwing items to players that have not completed the archwing quest. (Code 7) + // TODO: Cannot gift necramechs to players that have not completed heart of deimos. (Code 20) + + const senderAccount = await getAccountForRequest(req); + const senderInventory = await getInventory( + senderAccount._id.toString(), + "PremiumCredits PremiumCreditsFree ActiveAvatarImageType GiftsRemaining" + ); + + if (senderInventory.GiftsRemaining == 0) { + res.status(400).send("10").end(); + return; + } + senderInventory.GiftsRemaining -= 1; + + updateCurrency(senderInventory, data.PurchaseParams.ExpectedPrice, true); + await senderInventory.save(); + + const senderName = getSuffixedName(senderAccount); + await createMessage(account._id, [ + { + sndr: senderName, + msg: data.Message || "/Lotus/Language/Menu/GiftReceivedBody_NoCustomMessage", + arg: [ + { + Key: "GIFTER_NAME", + Tag: senderName + }, + { + Key: "GIFT_QUANTITY", + Tag: data.PurchaseParams.Quantity + } + ], + sub: "/Lotus/Language/Menu/GiftReceivedSubject", + icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon, + gifts: [ + { + GiftType: data.PurchaseParams.StoreItem + } + ] + } + ]); + + res.end(); +}; + +interface IGiftingRequest { + PurchaseParams: IPurchaseParams; + Message?: string; + Recipient?: string; + RecipientId?: IOid; + buildLabel: string; +} diff --git a/src/controllers/api/gildWeaponController.ts b/src/controllers/api/gildWeaponController.ts index ea4f1194..3914b81a 100644 --- a/src/controllers/api/gildWeaponController.ts +++ b/src/controllers/api/gildWeaponController.ts @@ -1,29 +1,29 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { getInventory } from "@/src/services/inventoryService"; +import { addMiscItems, getInventory } from "@/src/services/inventoryService"; import { WeaponTypeInternal } from "@/src/services/itemDataService"; -import { ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { ArtifactPolarity, EquipmentFeatures, IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { ExportRecipes } from "warframe-public-export-plus"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; const modularWeaponCategory: (WeaponTypeInternal | "Hoverboards")[] = [ "LongGuns", "Pistols", "Melee", "OperatorAmps", - "Hoverboards" // Not sure about hoverboards just coppied from modual crafting + "Hoverboards" ]; interface IGildWeaponRequest { ItemName: string; - Recipe: string; // /Lotus/Weapons/SolarisUnited/LotusGildKitgunBlueprint + Recipe: string; // e.g. /Lotus/Weapons/SolarisUnited/LotusGildKitgunBlueprint PolarizeSlot?: number; PolarizeValue?: ArtifactPolarity; ItemId: string; Category: WeaponTypeInternal | "Hoverboards"; } -// In export there no recipes for gild action, so reputation and ressources only consumed visually - export const gildWeaponController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const data = getJSONfromString(String(req.body)); @@ -40,7 +40,8 @@ export const gildWeaponController: RequestHandler = async (req, res) => { } const weapon = inventory[data.Category][weaponIndex]; - weapon.Features = EquipmentFeatures.GILDED; // maybe 9 idk if DOUBLE_CAPACITY is also given + weapon.Features ??= 0; + weapon.Features |= EquipmentFeatures.GILDED; weapon.ItemName = data.ItemName; weapon.XP = 0; if (data.Category != "OperatorAmps" && data.PolarizeSlot && data.PolarizeValue) { @@ -52,11 +53,29 @@ export const gildWeaponController: RequestHandler = async (req, res) => { ]; } inventory[data.Category][weaponIndex] = weapon; - await inventory.save(); + const inventoryChanges: IInventoryChanges = {}; + inventoryChanges[data.Category] = [weapon.toJSON()]; + const recipe = ExportRecipes[data.Recipe]; + inventoryChanges.MiscItems = recipe.secretIngredients!.map(ingredient => ({ + ItemType: ingredient.ItemType, + ItemCount: ingredient.ItemCount * -1 + })); + addMiscItems(inventory, inventoryChanges.MiscItems); + + const affiliationMods = []; + if (recipe.syndicateStandingChange) { + const affiliation = inventory.Affiliations.find(x => x.Tag == recipe.syndicateStandingChange!.tag)!; + affiliation.Standing += recipe.syndicateStandingChange.value; + affiliationMods.push({ + Tag: recipe.syndicateStandingChange.tag, + Standing: recipe.syndicateStandingChange.value + }); + } + + await inventory.save(); res.json({ - InventoryChanges: { - [data.Category]: [weapon] - } + InventoryChanges: inventoryChanges, + AffiliationMods: affiliationMods }); }; diff --git a/src/controllers/api/giveKeyChainTriggeredItemsController.ts b/src/controllers/api/giveKeyChainTriggeredItemsController.ts index bff70c85..df8e8a80 100644 --- a/src/controllers/api/giveKeyChainTriggeredItemsController.ts +++ b/src/controllers/api/giveKeyChainTriggeredItemsController.ts @@ -2,8 +2,8 @@ import { RequestHandler } from "express"; import { parseString } from "@/src/helpers/general"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getInventory } from "@/src/services/inventoryService"; -import { IGroup } from "@/src/types/loginTypes"; import { giveKeyChainItem } from "@/src/services/questService"; +import { IKeyChainRequest } from "@/src/types/requestTypes"; export const giveKeyChainTriggeredItemsController: RequestHandler = async (req, res) => { const accountId = parseString(req.query.accountId); @@ -15,9 +15,3 @@ export const giveKeyChainTriggeredItemsController: RequestHandler = async (req, res.send(inventoryChanges); }; - -export interface IKeyChainRequest { - KeyChain: string; - ChainStage: number; - Groups?: IGroup[]; -} diff --git a/src/controllers/api/giveKeyChainTriggeredMessageController.ts b/src/controllers/api/giveKeyChainTriggeredMessageController.ts index dec4b8a1..3bc41c21 100644 --- a/src/controllers/api/giveKeyChainTriggeredMessageController.ts +++ b/src/controllers/api/giveKeyChainTriggeredMessageController.ts @@ -1,7 +1,7 @@ -import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { giveKeyChainMessage } from "@/src/services/questService"; +import { IKeyChainRequest } from "@/src/types/requestTypes"; import { RequestHandler } from "express"; export const giveKeyChainTriggeredMessageController: RequestHandler = async (req, res) => { diff --git a/src/controllers/api/giveQuestKey.ts b/src/controllers/api/giveQuestKey.ts index 070dea75..80846234 100644 --- a/src/controllers/api/giveQuestKey.ts +++ b/src/controllers/api/giveQuestKey.ts @@ -20,11 +20,11 @@ export const giveQuestKeyRewardController: RequestHandler = async (req, res) => //TODO: consider whishlist changes }; -export interface IQuestKeyRewardRequest { +interface IQuestKeyRewardRequest { reward: IQuestKeyReward; } -export interface IQuestKeyReward { +interface IQuestKeyReward { RewardType: string; CouponType: string; Icon: string; @@ -38,7 +38,7 @@ export interface IQuestKeyReward { Duration: number; CouponSku: number; Syndicate: string; - Milestones: any[]; + //Milestones: any[]; ChooseSetIndex: number; NewSystemReward: boolean; _id: IOid; diff --git a/src/controllers/api/giveStartingGearController.ts b/src/controllers/api/giveStartingGearController.ts index 118664b3..6556de93 100644 --- a/src/controllers/api/giveStartingGearController.ts +++ b/src/controllers/api/giveStartingGearController.ts @@ -1,19 +1,8 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { InventoryDocumentProps } from "@/src/models/inventoryModels/inventoryModel"; -import { - addEquipment, - addItem, - combineInventoryChanges, - getInventory, - updateSlots -} from "@/src/services/inventoryService"; +import { addStartingGear, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { IInventoryClient, IInventoryDatabase, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; -import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { TPartialStartingGear } from "@/src/types/inventoryTypes/inventoryTypes"; import { RequestHandler } from "express"; -import { HydratedDocument } from "mongoose"; - -type TPartialStartingGear = Pick; export const giveStartingGearController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -25,72 +14,3 @@ export const giveStartingGearController: RequestHandler = async (req, res) => { res.send(inventoryChanges); }; - -//TODO: RawUpgrades might need to return a LastAdded -const awakeningRewards = [ - "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem1", - "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem2", - "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem3", - "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem4", - "/Lotus/Types/Restoratives/LisetAutoHack", - "/Lotus/Upgrades/Mods/Warframe/AvatarShieldMaxMod" -]; - -export const addStartingGear = async ( - inventory: HydratedDocument, - startingGear: TPartialStartingGear | undefined = undefined -): Promise => { - const { LongGuns, Pistols, Suits, Melee } = startingGear || { - LongGuns: [{ ItemType: "/Lotus/Weapons/Tenno/Rifle/Rifle" }], - Pistols: [{ ItemType: "/Lotus/Weapons/Tenno/Pistol/Pistol" }], - Suits: [{ ItemType: "/Lotus/Powersuits/Excalibur/Excalibur" }], - Melee: [{ ItemType: "/Lotus/Weapons/Tenno/Melee/LongSword/LongSword" }] - }; - - //TODO: properly merge weapon bin changes it is currently static here - const inventoryChanges: IInventoryChanges = {}; - addEquipment(inventory, "LongGuns", LongGuns[0].ItemType, undefined, inventoryChanges); - addEquipment(inventory, "Pistols", Pistols[0].ItemType, undefined, inventoryChanges); - addEquipment(inventory, "Melee", Melee[0].ItemType, undefined, inventoryChanges); - addEquipment(inventory, "Suits", Suits[0].ItemType, undefined, inventoryChanges, { Configs: Suits[0].Configs }); - addEquipment( - inventory, - "DataKnives", - "/Lotus/Weapons/Tenno/HackingDevices/TnHackingDevice/TnHackingDeviceWeapon", - undefined, - inventoryChanges, - { XP: 450_000 } - ); - addEquipment( - inventory, - "Scoops", - "/Lotus/Weapons/Tenno/Speedball/SpeedballWeaponTest", - undefined, - inventoryChanges - ); - - updateSlots(inventory, InventorySlot.SUITS, 0, 1); - updateSlots(inventory, InventorySlot.WEAPONS, 0, 3); - inventoryChanges.SuitBin = { count: 1, platinum: 0, Slots: -1 }; - inventoryChanges.WeaponBin = { count: 3, platinum: 0, Slots: -3 }; - - await addItem(inventory, "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain"); - inventory.ActiveQuest = "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain"; - - inventory.PremiumCredits = 50; - inventory.PremiumCreditsFree = 50; - inventoryChanges.PremiumCredits = 50; - inventoryChanges.PremiumCreditsFree = 50; - inventory.RegularCredits = 3000; - inventoryChanges.RegularCredits = 3000; - - for (const item of awakeningRewards) { - const inventoryDelta = await addItem(inventory, item); - combineInventoryChanges(inventoryChanges, inventoryDelta.InventoryChanges); - } - - inventory.PlayedParkourTutorial = true; - inventory.ReceivedStartingGear = true; - - return inventoryChanges; -}; diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index 34022699..5b0b5374 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -1,44 +1,139 @@ import { RequestHandler } from "express"; -import { getGuildForRequestEx } from "@/src/services/guildService"; +import { + addGuildMemberMiscItemContribution, + getGuildForRequestEx, + getGuildVault, + hasAccessToDojo, + hasGuildPermission, + processFundedGuildTechProject, + processGuildTechProjectContributionsUpdate, + removePigmentsFromGuildMembers, + scaleRequiredCount, + setGuildTechLogState +} from "@/src/services/guildService"; import { ExportDojoRecipes } from "warframe-public-export-plus"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { addMiscItems, addRecipes, getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { + addItem, + addMiscItems, + addRecipes, + combineInventoryChanges, + getInventory, + updateCurrency +} from "@/src/services/inventoryService"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { config } from "@/src/services/configService"; +import { GuildPermission, ITechProjectClient } from "@/src/types/guildTypes"; +import { GuildMember } from "@/src/models/guildModel"; +import { toMongoDate } from "@/src/helpers/inventoryHelpers"; +import { logger } from "@/src/utils/logger"; export const guildTechController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId); const guild = await getGuildForRequestEx(req, inventory); const data = JSON.parse(String(req.body)) as TGuildTechRequest; - const action = data.Action.split(",")[0]; - if (action == "Sync") { - res.json({ - TechProjects: guild.toJSON().TechProjects - }); - } else if (action == "Start") { - const recipe = ExportDojoRecipes.research[data.RecipeType!]; + if (data.Action == "Sync") { + let needSave = false; + const techProjects: ITechProjectClient[] = []; + if (guild.TechProjects) { + for (const project of guild.TechProjects) { + const techProject: ITechProjectClient = { + ItemType: project.ItemType, + ReqCredits: project.ReqCredits, + ReqItems: project.ReqItems, + State: project.State + }; + if (project.CompletionDate) { + techProject.CompletionDate = toMongoDate(project.CompletionDate); + if (Date.now() >= project.CompletionDate.getTime()) { + needSave ||= setGuildTechLogState(guild, project.ItemType, 4, project.CompletionDate); + } + } + techProjects.push(techProject); + } + } + if (needSave) { + await guild.save(); + } + res.json({ TechProjects: techProjects }); + } else if (data.Action == "Start") { + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { + res.status(400).send("-1").end(); + return; + } + const recipe = ExportDojoRecipes.research[data.RecipeType]; guild.TechProjects ??= []; if (!guild.TechProjects.find(x => x.ItemType == data.RecipeType)) { - guild.TechProjects.push({ - ItemType: data.RecipeType!, - ReqCredits: scaleRequiredCount(recipe.price), - ReqItems: recipe.ingredients.map(x => ({ - ItemType: x.ItemType, - ItemCount: scaleRequiredCount(x.ItemCount) - })), - State: 0 - }); + const techProject = + guild.TechProjects[ + guild.TechProjects.push({ + ItemType: data.RecipeType, + ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price), + ReqItems: recipe.ingredients.map(x => ({ + ItemType: x.ItemType, + ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount) + })), + State: 0 + }) - 1 + ]; + setGuildTechLogState(guild, techProject.ItemType, 5); + if (config.noDojoResearchCosts) { + processFundedGuildTechProject(guild, techProject, recipe); + } else { + if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") { + guild.ActiveDojoColorResearch = data.RecipeType; + } + } } await guild.save(); res.end(); - } else if (action == "Contribute") { - const contributions = data as IGuildTechContributeFields; + } else if (data.Action == "Contribute") { + if (!hasAccessToDojo(inventory)) { + res.status(400).send("-1").end(); + return; + } + + const guildMember = (await GuildMember.findOne( + { accountId, guildId: guild._id }, + "RegularCreditsContributed MiscItemsContributed" + ))!; + + const contributions = data; const techProject = guild.TechProjects!.find(x => x.ItemType == contributions.RecipeType)!; + + if (contributions.VaultCredits) { + if (contributions.VaultCredits > techProject.ReqCredits) { + contributions.VaultCredits = techProject.ReqCredits; + } + techProject.ReqCredits -= contributions.VaultCredits; + guild.VaultRegularCredits! -= contributions.VaultCredits; + } + if (contributions.RegularCredits > techProject.ReqCredits) { contributions.RegularCredits = techProject.ReqCredits; } techProject.ReqCredits -= contributions.RegularCredits; + + guildMember.RegularCreditsContributed ??= 0; + guildMember.RegularCreditsContributed += contributions.RegularCredits; + + if (contributions.VaultMiscItems.length) { + for (const miscItem of contributions.VaultMiscItems) { + const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); + if (reqItem) { + if (miscItem.ItemCount > reqItem.ItemCount) { + miscItem.ItemCount = reqItem.ItemCount; + } + reqItem.ItemCount -= miscItem.ItemCount; + + const vaultMiscItem = guild.VaultMiscItems!.find(x => x.ItemType == miscItem.ItemType)!; + vaultMiscItem.ItemCount -= miscItem.ItemCount; + } + } + } + const miscItemChanges = []; for (const miscItem of contributions.MiscItems) { const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); @@ -51,30 +146,29 @@ export const guildTechController: RequestHandler = async (req, res) => { ItemType: miscItem.ItemType, ItemCount: miscItem.ItemCount * -1 }); + + addGuildMemberMiscItemContribution(guildMember, miscItem); } } addMiscItems(inventory, miscItemChanges); - const inventoryChanges: IInventoryChanges = { - ...updateCurrency(inventory, contributions.RegularCredits, false), - MiscItems: miscItemChanges - }; + const inventoryChanges: IInventoryChanges = updateCurrency(inventory, contributions.RegularCredits, false); + inventoryChanges.MiscItems = miscItemChanges; - if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) { - // This research is now fully funded. - techProject.State = 1; - const recipe = ExportDojoRecipes.research[data.RecipeType!]; - techProject.CompletionDate = new Date(new Date().getTime() + recipe.time * 1000); - } + // Check if research is fully funded now. + await processGuildTechProjectContributionsUpdate(guild, techProject); - await guild.save(); - await inventory.save(); + await Promise.all([guild.save(), inventory.save(), guildMember.save()]); res.json({ - InventoryChanges: inventoryChanges + InventoryChanges: inventoryChanges, + Vault: getGuildVault(guild) }); - } else if (action == "Buy") { - const purchase = data as IGuildTechBuyFields; + } else if (data.Action.split(",")[0] == "Buy") { + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) { + res.status(400).send("-1").end(); + return; + } + const purchase = data as IGuildTechBuyRequest; const quantity = parseInt(data.Action.split(",")[1]); - const inventory = await getInventory(accountId); const recipeChanges = [ { ItemType: purchase.RecipeType, @@ -95,24 +189,68 @@ export const guildTechController: RequestHandler = async (req, res) => { Recipes: recipeChanges } }); + } else if (data.Action == "Fabricate") { + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) { + res.status(400).send("-1").end(); + return; + } + const recipe = ExportDojoRecipes.fabrications[data.RecipeType]; + const inventoryChanges: IInventoryChanges = updateCurrency(inventory, recipe.price, false); + inventoryChanges.MiscItems = recipe.ingredients.map(x => ({ + ItemType: x.ItemType, + ItemCount: x.ItemCount * -1 + })); + addMiscItems(inventory, inventoryChanges.MiscItems); + combineInventoryChanges(inventoryChanges, await addItem(inventory, recipe.resultType)); + await inventory.save(); + // Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`. + res.json({ inventoryChanges: inventoryChanges }); + } else if (data.Action == "Pause") { + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { + res.status(400).send("-1").end(); + return; + } + const project = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!; + project.State = -2; + guild.ActiveDojoColorResearch = ""; + await guild.save(); + await removePigmentsFromGuildMembers(guild._id); + res.end(); + } else if (data.Action == "Unpause") { + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { + res.status(400).send("-1").end(); + return; + } + const project = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!; + project.State = 0; + guild.ActiveDojoColorResearch = data.RecipeType; + await guild.save(); + res.end(); } else { + logger.debug(`data provided to ${req.path}: ${String(req.body)}`); throw new Error(`unknown guildTech action: ${data.Action}`); } }; -type TGuildTechRequest = { - Action: string; -} & Partial & - Partial; +type TGuildTechRequest = + | { Action: "Sync" | "SomethingElseThatWeMightNotKnowAbout" } + | IGuildTechBasicRequest + | IGuildTechContributeRequest; -interface IGuildTechStartFields { +interface IGuildTechBasicRequest { + Action: "Start" | "Fabricate" | "Pause" | "Unpause"; Mode: "Guild"; RecipeType: string; } -type IGuildTechBuyFields = IGuildTechStartFields; +interface IGuildTechBuyRequest { + Action: string; + Mode: "Guild"; + RecipeType: string; +} -interface IGuildTechContributeFields { +interface IGuildTechContributeRequest { + Action: "Contribute"; ResearchId: ""; RecipeType: string; RegularCredits: number; @@ -120,8 +258,3 @@ interface IGuildTechContributeFields { VaultCredits: number; VaultMiscItems: IMiscItem[]; } - -const scaleRequiredCount = (count: number): number => { - // The recipes in the export are for Moon clans. For now we'll just assume we only have Ghost clans. - return Math.max(1, Math.trunc(count / 100)); -}; diff --git a/src/controllers/api/hubBlessingController.ts b/src/controllers/api/hubBlessingController.ts new file mode 100644 index 00000000..49b992e8 --- /dev/null +++ b/src/controllers/api/hubBlessingController.ts @@ -0,0 +1,45 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { addBooster, getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getRandomInt } from "@/src/services/rngService"; +import { RequestHandler } from "express"; +import { ExportBoosters } from "warframe-public-export-plus"; + +export const hubBlessingController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const data = getJSONfromString(String(req.body)); + const boosterType = ExportBoosters[data.booster].typeName; + if (req.query.mode == "send") { + const inventory = await getInventory(accountId, "BlessingCooldown Boosters"); + inventory.BlessingCooldown = new Date(Date.now() + 86400000); + addBooster(boosterType, 3 * 3600, inventory); + await inventory.save(); + + let token = ""; + for (let i = 0; i != 32; ++i) { + token += getRandomInt(0, 15).toString(16); + } + + res.json({ + BlessingCooldown: inventory.BlessingCooldown, + SendTime: Math.trunc(Date.now() / 1000).toString(), + Token: token + }); + } else { + const inventory = await getInventory(accountId, "Boosters"); + addBooster(boosterType, 3 * 3600, inventory); + await inventory.save(); + + res.json({ + BoosterType: data.booster, + Sender: data.senderId + }); + } +}; + +interface IHubBlessingRequest { + booster: string; + senderId?: string; // mode=request + sendTime?: string; // mode=request + token?: string; // mode=request +} diff --git a/src/controllers/api/inboxController.ts b/src/controllers/api/inboxController.ts index 896c01b6..7cb777e2 100644 --- a/src/controllers/api/inboxController.ts +++ b/src/controllers/api/inboxController.ts @@ -1,21 +1,24 @@ import { RequestHandler } from "express"; import { Inbox } from "@/src/models/inboxModel"; import { + createMessage, createNewEventMessages, deleteAllMessagesRead, deleteMessageRead, getAllMessagesSorted, getMessage } from "@/src/services/inboxService"; -import { getAccountIdForRequest } from "@/src/services/loginService"; -import { addItems, getInventory } from "@/src/services/inventoryService"; +import { getAccountForRequest, getAccountFromSuffixedName, getSuffixedName } from "@/src/services/loginService"; +import { addItems, combineInventoryChanges, getInventory } from "@/src/services/inventoryService"; import { logger } from "@/src/utils/logger"; -import { ExportGear } from "warframe-public-export-plus"; +import { ExportFlavour, ExportGear } from "warframe-public-export-plus"; +import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; export const inboxController: RequestHandler = async (req, res) => { const { deleteId, lastMessage: latestClientMessageId, messageId } = req.query; - const accountId = await getAccountIdForRequest(req); + const account = await getAccountForRequest(req); + const accountId = account._id.toString(); if (deleteId) { if (deleteId === "DeleteAllRead") { @@ -29,12 +32,12 @@ export const inboxController: RequestHandler = async (req, res) => { } else if (messageId) { const message = await getMessage(messageId as string); message.r = true; + await message.save(); + const attachmentItems = message.att; const attachmentCountedItems = message.countedAtt; - if (!attachmentItems && !attachmentCountedItems) { - await message.save(); - + if (!attachmentItems && !attachmentCountedItems && !message.gifts) { res.status(200).end(); return; } @@ -54,9 +57,43 @@ export const inboxController: RequestHandler = async (req, res) => { if (attachmentCountedItems) { await addItems(inventory, attachmentCountedItems, inventoryChanges); } + if (message.gifts) { + const sender = await getAccountFromSuffixedName(message.sndr); + const recipientName = getSuffixedName(account); + const giftQuantity = message.arg!.find(x => x.Key == "GIFT_QUANTITY")!.Tag as number; + for (const gift of message.gifts) { + combineInventoryChanges( + inventoryChanges, + (await handleStoreItemAcquisition(gift.GiftType, inventory, giftQuantity)).InventoryChanges + ); + if (sender) { + await createMessage(sender._id, [ + { + sndr: recipientName, + msg: "/Lotus/Language/Menu/GiftReceivedConfirmationBody", + arg: [ + { + Key: "RECIPIENT_NAME", + Tag: recipientName + }, + { + Key: "GIFT_TYPE", + Tag: gift.GiftType + }, + { + Key: "GIFT_QUANTITY", + Tag: giftQuantity + } + ], + sub: "/Lotus/Language/Menu/GiftReceivedConfirmationSubject", + icon: ExportFlavour[inventory.ActiveAvatarImageType].icon, + highPriority: true + } + ]); + } + } + } await inventory.save(); - await message.save(); - res.json({ InventoryChanges: inventoryChanges }); } else if (latestClientMessageId) { await createNewEventMessages(req); diff --git a/src/controllers/api/infestedFoundryController.ts b/src/controllers/api/infestedFoundryController.ts index acce1508..28e89adc 100644 --- a/src/controllers/api/infestedFoundryController.ts +++ b/src/controllers/api/infestedFoundryController.ts @@ -1,24 +1,26 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { getInventory, addMiscItems, updateCurrency, addRecipes } from "@/src/services/inventoryService"; +import { getInventory, addMiscItems, updateCurrency, addRecipes, freeUpSlot } from "@/src/services/inventoryService"; import { IOid } from "@/src/types/commonTypes"; import { IConsumedSuit, IHelminthFoodRecord, - IInfestedFoundryClient, - IInfestedFoundryDatabase, IInventoryClient, IMiscItem, - ITypeCount + InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; -import { ExportMisc, ExportRecipes } from "warframe-public-export-plus"; +import { ExportMisc } from "warframe-public-export-plus"; import { getRecipe } from "@/src/services/itemDataService"; -import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { logger } from "@/src/utils/logger"; import { colorToShard } from "@/src/helpers/shardHelper"; import { config } from "@/src/services/configService"; +import { + addInfestedFoundryXP, + applyCheatsToInfestedFoundry, + handleSubsumeCompletion +} from "@/src/services/infestedFoundryService"; export const infestedFoundryController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -27,7 +29,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { // shard installation const request = getJSONfromString(String(req.body)); const inventory = await getInventory(accountId); - const suit = inventory.Suits.find(suit => suit._id.toString() == request.SuitId.$oid)!; + const suit = inventory.Suits.id(request.SuitId.$oid)!; if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) { suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}]; } @@ -55,19 +57,20 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { // shard removal const request = getJSONfromString(String(req.body)); const inventory = await getInventory(accountId); - const suit = inventory.Suits.find(suit => suit._id.toString() == request.SuitId.$oid)!; + const suit = inventory.Suits.id(request.SuitId.$oid)!; - // refund shard - const shard = Object.entries(colorToShard).find( - ([color]) => color == suit.ArchonCrystalUpgrades![request.Slot].Color - )![1]; - const miscItemChanges = [ - { + const miscItemChanges: IMiscItem[] = []; + if (suit.ArchonCrystalUpgrades![request.Slot].Color) { + // refund shard + const shard = Object.entries(colorToShard).find( + ([color]) => color == suit.ArchonCrystalUpgrades![request.Slot].Color + )![1]; + miscItemChanges.push({ ItemType: shard, ItemCount: 1 - } - ]; - addMiscItems(inventory, miscItemChanges); + }); + addMiscItems(inventory, miscItemChanges); + } // remove from suit suit.ArchonCrystalUpgrades![request.Slot] = {}; @@ -126,7 +129,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { const miscItemChanges: IMiscItem[] = []; let totalPercentagePointsGained = 0; - const currentUnixSeconds = Math.trunc(new Date().getTime() / 1000); + const currentUnixSeconds = Math.trunc(Date.now() / 1000); for (const contribution of request.ResourceContributions) { const snack = ExportMisc.helminthSnacks[contribution.ItemType]; @@ -258,11 +261,10 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { inventory.InfestedFoundry!.ConsumedSuits ??= []; inventory.InfestedFoundry!.ConsumedSuits.push(consumedSuit); inventory.InfestedFoundry!.LastConsumedSuit = suit; - inventory.InfestedFoundry!.AbilityOverrideUnlockCooldown = new Date( - new Date().getTime() + 24 * 60 * 60 * 1000 - ); + inventory.InfestedFoundry!.AbilityOverrideUnlockCooldown = new Date(Date.now() + 24 * 60 * 60 * 1000); const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry!, 1600_00); addRecipes(inventory, recipeChanges); + freeUpSlot(inventory, InventorySlot.SUITS); await inventory.save(); const infestedFoundry = inventory.toJSON().InfestedFoundry!; applyCheatsToInfestedFoundry(infestedFoundry); @@ -307,7 +309,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { const request = getJSONfromString(String(req.body)); const inventory = await getInventory(accountId); const suit = inventory.Suits.id(request.SuitId.$oid)!; - const upgradesExpiry = new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000); + const upgradesExpiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); suit.OffensiveUpgrade = request.OffensiveUpgradeType; suit.DefensiveUpgrade = request.DefensiveUpgradeType; suit.UpgradesExpiry = upgradesExpiry; @@ -354,6 +356,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { } default: + logger.debug(`data provided to ${req.path}: ${String(req.body)}`); throw new Error(`unhandled infestedFoundry mode: ${String(req.query.mode)}`); } }; @@ -381,116 +384,11 @@ interface IHelminthFeedRequest { }[]; } -export const addInfestedFoundryXP = (infestedFoundry: IInfestedFoundryDatabase, delta: number): ITypeCount[] => { - const recipeChanges: ITypeCount[] = []; - infestedFoundry.XP ??= 0; - const prevXP = infestedFoundry.XP; - infestedFoundry.XP += delta; - if (prevXP < 2250_00 && infestedFoundry.XP >= 2250_00) { - infestedFoundry.Slots ??= 0; - infestedFoundry.Slots += 3; - } - if (prevXP < 5625_00 && infestedFoundry.XP >= 5625_00) { - recipeChanges.push({ - ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthShieldsBlueprint", - ItemCount: 1 - }); - } - if (prevXP < 10125_00 && infestedFoundry.XP >= 10125_00) { - recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthHackBlueprint", ItemCount: 1 }); - } - if (prevXP < 15750_00 && infestedFoundry.XP >= 15750_00) { - infestedFoundry.Slots ??= 0; - infestedFoundry.Slots += 10; - } - if (prevXP < 22500_00 && infestedFoundry.XP >= 22500_00) { - recipeChanges.push({ - ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthAmmoEfficiencyBlueprint", - ItemCount: 1 - }); - } - if (prevXP < 30375_00 && infestedFoundry.XP >= 30375_00) { - recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthStunBlueprint", ItemCount: 1 }); - } - if (prevXP < 39375_00 && infestedFoundry.XP >= 39375_00) { - infestedFoundry.Slots ??= 0; - infestedFoundry.Slots += 20; - } - if (prevXP < 60750_00 && infestedFoundry.XP >= 60750_00) { - recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthStatusBlueprint", ItemCount: 1 }); - } - if (prevXP < 73125_00 && infestedFoundry.XP >= 73125_00) { - infestedFoundry.Slots = 1; - } - if (prevXP < 86625_00 && infestedFoundry.XP >= 86625_00) { - recipeChanges.push({ - ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthShieldArmorBlueprint", - ItemCount: 1 - }); - } - if (prevXP < 101250_00 && infestedFoundry.XP >= 101250_00) { - recipeChanges.push({ - ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthProcBlockBlueprint", - ItemCount: 1 - }); - } - if (prevXP < 117000_00 && infestedFoundry.XP >= 117000_00) { - recipeChanges.push({ - ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthEnergyShareBlueprint", - ItemCount: 1 - }); - } - if (prevXP < 133875_00 && infestedFoundry.XP >= 133875_00) { - recipeChanges.push({ - ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthMaxStatusBlueprint", - ItemCount: 1 - }); - } - if (prevXP < 151875_00 && infestedFoundry.XP >= 151875_00) { - recipeChanges.push({ - ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthTreasureBlueprint", - ItemCount: 1 - }); - } - return recipeChanges; -}; - interface IHelminthSubsumeRequest { SuitId: IOid; Recipe: string; } -export const handleSubsumeCompletion = (inventory: TInventoryDatabaseDocument): ITypeCount[] => { - const [recipeType] = Object.entries(ExportRecipes).find( - ([_recipeType, recipe]) => - recipe.secretIngredientAction == "SIA_WARFRAME_ABILITY" && - recipe.secretIngredients![0].ItemType == inventory.InfestedFoundry!.LastConsumedSuit!.ItemType - )!; - inventory.InfestedFoundry!.LastConsumedSuit = undefined; - inventory.InfestedFoundry!.AbilityOverrideUnlockCooldown = undefined; - const recipeChanges: ITypeCount[] = [ - { - ItemType: recipeType, - ItemCount: 1 - } - ]; - addRecipes(inventory, recipeChanges); - return recipeChanges; -}; - -export const applyCheatsToInfestedFoundry = (infestedFoundry: IInfestedFoundryClient): void => { - if (config.infiniteHelminthMaterials) { - infestedFoundry.Resources = [ - { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthCalx", Count: 1000 }, - { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthBiotics", Count: 1000 }, - { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthSynthetics", Count: 1000 }, - { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthPheromones", Count: 1000 }, - { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthBile", Count: 1000 }, - { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthOxides", Count: 1000 } - ]; - } -}; - interface IHelminthOfferingsUpdate { OfferingsIndex: number; SuitTypes: string[]; diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 82b55ff5..2c659717 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -1,5 +1,5 @@ import { RequestHandler } from "express"; -import { getAccountForRequest } from "@/src/services/loginService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; import { Inventory, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { config } from "@/src/services/configService"; import allDialogue from "@/static/fixed_responses/allDialogue.json"; @@ -13,13 +13,15 @@ import { ExportResources, ExportVirtuals } from "warframe-public-export-plus"; -import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "./infestedFoundryController"; -import { allDailyAffiliationKeys } from "@/src/services/inventoryService"; +import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService"; +import { addMiscItems, allDailyAffiliationKeys, createLibraryDailyTask } from "@/src/services/inventoryService"; +import { logger } from "@/src/utils/logger"; +import { catBreadHash } from "@/src/helpers/stringHelpers"; export const inventoryController: RequestHandler = async (request, response) => { - const account = await getAccountForRequest(request); + const accountId = await getAccountIdForRequest(request); - const inventory = await Inventory.findOne({ accountOwnerId: account._id.toString() }); + const inventory = await Inventory.findOne({ accountOwnerId: accountId }); if (!inventory) { response.status(400).json({ error: "inventory was undefined" }); @@ -27,15 +29,57 @@ export const inventoryController: RequestHandler = async (request, response) => } // Handle daily reset - const today: number = Math.trunc(new Date().getTime() / 86400000); - if (account.LastLoginDay != today) { - account.LastLoginDay = today; - await account.save(); - + if (!inventory.NextRefill || Date.now() >= inventory.NextRefill.getTime()) { for (const key of allDailyAffiliationKeys) { inventory[key] = 16000 + inventory.PlayerLevel * 500; } inventory.DailyFocus = 250000 + inventory.PlayerLevel * 5000; + inventory.GiftsRemaining = Math.max(8, inventory.PlayerLevel); + inventory.TradesRemaining = inventory.PlayerLevel; + + inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask(); + + if (inventory.NextRefill) { + if (config.noArgonCrystalDecay) { + inventory.FoundToday = undefined; + } else { + const lastLoginDay = Math.trunc(inventory.NextRefill.getTime() / 86400000) - 1; + const today = Math.trunc(Date.now() / 86400000); + const daysPassed = today - lastLoginDay; + for (let i = 0; i != daysPassed; ++i) { + const numArgonCrystals = + inventory.MiscItems.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal") + ?.ItemCount ?? 0; + if (numArgonCrystals == 0) { + break; + } + const numStableArgonCrystals = Math.min( + numArgonCrystals, + inventory.FoundToday?.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal") + ?.ItemCount ?? 0 + ); + const numDecayingArgonCrystals = numArgonCrystals - numStableArgonCrystals; + const numDecayingArgonCrystalsToRemove = Math.ceil(numDecayingArgonCrystals / 2); + logger.debug(`ticking argon crystals for day ${i + 1} of ${daysPassed}`, { + numArgonCrystals, + numStableArgonCrystals, + numDecayingArgonCrystals, + numDecayingArgonCrystalsToRemove + }); + // Remove half of owned decaying argon crystals + addMiscItems(inventory, [ + { + ItemType: "/Lotus/Types/Items/MiscItems/ArgonCrystal", + ItemCount: numDecayingArgonCrystalsToRemove * -1 + } + ]); + // All stable argon crystals are now decaying + inventory.FoundToday = undefined; + } + } + } + + inventory.NextRefill = new Date((Math.trunc(Date.now() / 86400000) + 1) * 86400000); await inventory.save(); } @@ -67,7 +111,7 @@ export const getInventoryResponse = async ( inventoryResponse.RegularCredits = 999999999; } if (config.infinitePlatinum) { - inventoryResponse.PremiumCreditsFree = 999999999; + inventoryResponse.PremiumCreditsFree = 0; inventoryResponse.PremiumCredits = 999999999; } if (config.infiniteEndo) { @@ -207,8 +251,9 @@ export const getInventoryResponse = async ( } if (config.noDailyStandingLimits) { + const spoofedDailyAffiliation = Math.max(999_999, 16000 + inventoryResponse.PlayerLevel * 500); for (const key of allDailyAffiliationKeys) { - inventoryResponse[key] = 999_999; + inventoryResponse[key] = spoofedDailyAffiliation; } } @@ -216,19 +261,16 @@ export const getInventoryResponse = async ( applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry); } - // Fix for #380 - inventoryResponse.NextRefill = { $date: { $numberLong: "9999999999999" } }; - - // This determines if the "void fissures" tab is shown in navigation. - inventoryResponse.HasOwnedVoidProjectionsPreviously = true; - // Omitting this field so opening the navigation resyncs the inventory which is more desirable for typical usage. //inventoryResponse.LastInventorySync = toOid(new Types.ObjectId()); + // Set 2FA enabled so trading post can be used + inventoryResponse.HWIDProtectEnabled = true; + return inventoryResponse; }; -export const addString = (arr: string[], str: string): void => { +const addString = (arr: string[], str: string): void => { if (!arr.find(x => x == str)) { arr.push(str); } @@ -255,15 +297,6 @@ const resourceGetParent = (resourceName: string): string | undefined => { if (resourceName in ExportResources) { return ExportResources[resourceName].parentName; } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition return ExportVirtuals[resourceName]?.parentName; }; - -// This is FNV1a-32 except operating under modulus 2^31 because JavaScript is stinky and likes producing negative integers out of nowhere. -const catBreadHash = (name: string): number => { - let hash = 2166136261; - for (let i = 0; i != name.length; ++i) { - hash = (hash ^ name.charCodeAt(i)) & 0x7fffffff; - hash = (hash * 16777619) & 0x7fffffff; - } - return hash; -}; diff --git a/src/controllers/api/inventorySlotsController.ts b/src/controllers/api/inventorySlotsController.ts index 4caea9d0..d32bc0db 100644 --- a/src/controllers/api/inventorySlotsController.ts +++ b/src/controllers/api/inventorySlotsController.ts @@ -3,6 +3,7 @@ import { getInventory, updateCurrency } from "@/src/services/inventoryService"; import { RequestHandler } from "express"; import { updateSlots } from "@/src/services/inventoryService"; import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; +import { logger } from "@/src/utils/logger"; /* loadout slots are additionally purchased slots only @@ -20,14 +21,20 @@ import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; export const inventorySlotsController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - //const body = JSON.parse(req.body as string) as IInventorySlotsRequest; + const body = JSON.parse(req.body as string) as IInventorySlotsRequest; - //TODO: check which slot was purchased because pvpBonus is also possible + if (body.Bin != InventorySlot.SUITS && body.Bin != InventorySlot.PVE_LOADOUTS) { + logger.warn(`unexpected slot purchase of type ${body.Bin}, account may be overcharged`); + } const inventory = await getInventory(accountId); const currencyChanges = updateCurrency(inventory, 20, true); - updateSlots(inventory, InventorySlot.PVE_LOADOUTS, 1, 1); + updateSlots(inventory, body.Bin, 1, 1); await inventory.save(); res.json({ InventoryChanges: currencyChanges }); }; + +interface IInventorySlotsRequest { + Bin: InventorySlot; +} diff --git a/src/controllers/api/joinSessionController.ts b/src/controllers/api/joinSessionController.ts index 4212c90f..7dff9fc9 100644 --- a/src/controllers/api/joinSessionController.ts +++ b/src/controllers/api/joinSessionController.ts @@ -2,12 +2,13 @@ import { RequestHandler } from "express"; import { getSessionByID } from "@/src/managers/sessionManager"; import { logger } from "@/src/utils/logger"; -const joinSessionController: RequestHandler = (_req, res) => { - const reqBody = JSON.parse(String(_req.body)); +export const joinSessionController: RequestHandler = (req, res) => { + const reqBody = JSON.parse(String(req.body)) as IJoinSessionRequest; logger.debug(`JoinSession Request`, { reqBody }); - const req = JSON.parse(String(_req.body)); - const session = getSessionByID(req.sessionIds[0] as string); + const session = getSessionByID(reqBody.sessionIds[0]); res.json({ rewardSeed: session?.rewardSeed, sessionId: { $oid: session?.sessionId } }); }; -export { joinSessionController }; +interface IJoinSessionRequest { + sessionIds: string[]; +} diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index d57fd7e1..f01c405a 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -19,10 +19,12 @@ export const loginController: RequestHandler = async (request, response) => { ? request.query.buildLabel.split(" ").join("+") : buildConfig.buildLabel; + const myAddress = request.host.indexOf("warframe.com") == -1 ? request.host : config.myAddress; + if (!account && config.autoCreateAccount && loginRequest.ClientType != "webui") { try { const nameFromEmail = loginRequest.email.substring(0, loginRequest.email.indexOf("@")); - let name = nameFromEmail; + let name = nameFromEmail || loginRequest.email.substring(1) || "SpaceNinja"; if (await isNameTaken(name)) { let suffix = 0; do { @@ -40,11 +42,10 @@ export const loginController: RequestHandler = async (request, response) => { ForceLogoutVersion: 0, ConsentNeeded: false, TrackedSettings: [], - Nonce: nonce, - LatestEventMessageDate: new Date(0) + Nonce: nonce }); logger.debug("created new account"); - response.json(createLoginResponse(newAccount, buildLabel)); + response.json(createLoginResponse(myAddress, newAccount, buildLabel)); return; } catch (error: unknown) { if (error instanceof Error) { @@ -53,24 +54,37 @@ export const loginController: RequestHandler = async (request, response) => { } } - //email not found or incorrect password - if (!account || !isCorrectPassword(loginRequest.password, account.password)) { + if (!account) { + response.status(400).json({ error: "unknown user" }); + return; + } + + if (!isCorrectPassword(loginRequest.password, account.password)) { response.status(400).json({ error: "incorrect login data" }); return; } - if (account.Nonce == 0 || loginRequest.ClientType != "webui") { + if (loginRequest.ClientType == "webui") { + if (!account.Nonce) { + account.ClientType = "webui"; + account.Nonce = nonce; + } + } else { + if (account.Nonce && account.ClientType != "webui" && !account.Dropped && !loginRequest.kick) { + response.status(400).json({ error: "nonce still set" }); + return; + } + + account.ClientType = loginRequest.ClientType; account.Nonce = nonce; - } - if (loginRequest.ClientType != "webui") { account.CountryCode = loginRequest.lang.toUpperCase(); } await account.save(); - response.json(createLoginResponse(account.toJSON(), buildLabel)); + response.json(createLoginResponse(myAddress, account.toJSON(), buildLabel)); }; -const createLoginResponse = (account: IDatabaseAccountJson, buildLabel: string): ILoginResponse => { +const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, buildLabel: string): ILoginResponse => { return { id: account.id, DisplayName: account.DisplayName, @@ -84,9 +98,9 @@ const createLoginResponse = (account: IDatabaseAccountJson, buildLabel: string): TrackedSettings: account.TrackedSettings, Nonce: account.Nonce, Groups: [], - IRC: config.myIrcAddresses ?? [config.myAddress], - platformCDNs: config.platformCDNs, - HUB: config.hubAddress, + IRC: config.myIrcAddresses ?? [myAddress], + platformCDNs: [`https://${myAddress}/`], + HUB: `https://${myAddress}/api/`, NRS: config.NRS, DTLS: 99, BuildLabel: buildLabel, diff --git a/src/controllers/api/loginRewardsController.ts b/src/controllers/api/loginRewardsController.ts index fd4b40c4..5280b77f 100644 --- a/src/controllers/api/loginRewardsController.ts +++ b/src/controllers/api/loginRewardsController.ts @@ -1,8 +1,55 @@ import { RequestHandler } from "express"; -import loginRewards from "@/static/fixed_responses/loginRewards.json"; +import { getAccountForRequest } from "@/src/services/loginService"; +import { + claimLoginReward, + getRandomLoginRewards, + ILoginRewardsReponse, + isLoginRewardAChoice, + setAccountGotLoginRewardToday +} from "@/src/services/loginRewardService"; +import { getInventory } from "@/src/services/inventoryService"; -const loginRewardsController: RequestHandler = (_req, res) => { - res.json(loginRewards); +export const loginRewardsController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + const today = Math.trunc(Date.now() / 86400000) * 86400; + const isMilestoneDay = account.LoginDays == 5 || account.LoginDays % 50 == 0; + const nextMilestoneDay = account.LoginDays < 5 ? 5 : (Math.trunc(account.LoginDays / 50) + 1) * 50; + + if (today == account.LastLoginRewardDate) { + res.json({ + DailyTributeInfo: { + IsMilestoneDay: isMilestoneDay, + IsChooseRewardSet: isLoginRewardAChoice(account), + LoginDays: account.LoginDays, + NextMilestoneReward: "", + NextMilestoneDay: nextMilestoneDay + } + } satisfies ILoginRewardsReponse); + return; + } + + const inventory = await getInventory(account._id.toString()); + const randomRewards = getRandomLoginRewards(account, inventory); + const response: ILoginRewardsReponse = { + DailyTributeInfo: { + Rewards: randomRewards, + IsMilestoneDay: isMilestoneDay, + IsChooseRewardSet: randomRewards.length != 1, + LoginDays: account.LoginDays, + NextMilestoneReward: "", + NextMilestoneDay: nextMilestoneDay, + HasChosenReward: false + }, + LastLoginRewardDate: today + }; + if (!isMilestoneDay && randomRewards.length == 1) { + response.DailyTributeInfo.HasChosenReward = true; + response.DailyTributeInfo.ChosenReward = randomRewards[0]; + response.DailyTributeInfo.NewInventory = await claimLoginReward(inventory, randomRewards[0]); + await inventory.save(); + + setAccountGotLoginRewardToday(account); + await account.save(); + } + res.json(response); }; - -export { loginRewardsController }; diff --git a/src/controllers/api/loginRewardsSelectionController.ts b/src/controllers/api/loginRewardsSelectionController.ts new file mode 100644 index 00000000..4b6fc210 --- /dev/null +++ b/src/controllers/api/loginRewardsSelectionController.ts @@ -0,0 +1,65 @@ +import { getInventory } from "@/src/services/inventoryService"; +import { + claimLoginReward, + getRandomLoginRewards, + setAccountGotLoginRewardToday +} from "@/src/services/loginRewardService"; +import { getAccountForRequest } from "@/src/services/loginService"; +import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { logger } from "@/src/utils/logger"; +import { RequestHandler } from "express"; + +export const loginRewardsSelectionController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + const inventory = await getInventory(account._id.toString()); + const body = JSON.parse(String(req.body)) as ILoginRewardsSelectionRequest; + const isMilestoneDay = account.LoginDays == 5 || account.LoginDays % 50 == 0; + if (body.IsMilestoneReward != isMilestoneDay) { + logger.warn(`Client disagrees on login milestone (got ${body.IsMilestoneReward}, expected ${isMilestoneDay})`); + } + let chosenReward; + let inventoryChanges: IInventoryChanges; + if (body.IsMilestoneReward) { + chosenReward = { + RewardType: "RT_STORE_ITEM", + StoreItemType: body.ChosenReward + }; + inventoryChanges = (await handleStoreItemAcquisition(body.ChosenReward, inventory)).InventoryChanges; + if (!evergreenRewards.find(x => x == body.ChosenReward)) { + inventory.LoginMilestoneRewards.push(body.ChosenReward); + } + } else { + const randomRewards = getRandomLoginRewards(account, inventory); + chosenReward = randomRewards.find(x => x.StoreItemType == body.ChosenReward)!; + inventoryChanges = await claimLoginReward(inventory, chosenReward); + } + await inventory.save(); + + setAccountGotLoginRewardToday(account); + await account.save(); + + res.json({ + DailyTributeInfo: { + NewInventory: inventoryChanges, + ChosenReward: chosenReward + } + }); +}; + +interface ILoginRewardsSelectionRequest { + ChosenReward: string; + IsMilestoneReward: boolean; +} + +const evergreenRewards = [ + "/Lotus/Types/StoreItems/Packages/EvergreenTripleForma", + "/Lotus/Types/StoreItems/Packages/EvergreenTripleRifleRiven", + "/Lotus/Types/StoreItems/Packages/EvergreenTripleMeleeRiven", + "/Lotus/Types/StoreItems/Packages/EvergreenTripleSecondaryRiven", + "/Lotus/Types/StoreItems/Packages/EvergreenWeaponSlots", + "/Lotus/Types/StoreItems/Packages/EvergreenKuva", + "/Lotus/Types/StoreItems/Packages/EvergreenBoosters", + "/Lotus/Types/StoreItems/Packages/EvergreenEndo", + "/Lotus/Types/StoreItems/Packages/EvergreenExilus" +]; diff --git a/src/controllers/api/logoutController.ts b/src/controllers/api/logoutController.ts index 735014d4..889e7d78 100644 --- a/src/controllers/api/logoutController.ts +++ b/src/controllers/api/logoutController.ts @@ -1,19 +1,28 @@ import { RequestHandler } from "express"; -import { getAccountIdForRequest } from "@/src/services/loginService"; import { Account } from "@/src/models/loginModel"; -const logoutController: RequestHandler = async (req, res) => { - const accountId = await getAccountIdForRequest(req); - const account = await Account.findOne({ _id: accountId }); - if (account) { - account.Nonce = 0; - await account.save(); +export const logoutController: RequestHandler = async (req, res) => { + if (!req.query.accountId) { + throw new Error("Request is missing accountId parameter"); } + const nonce: number = parseInt(req.query.nonce as string); + if (!nonce) { + throw new Error("Request is missing nonce parameter"); + } + + await Account.updateOne( + { + _id: req.query.accountId, + Nonce: nonce + }, + { + Nonce: 0 + } + ); + res.writeHead(200, { "Content-Type": "text/html", "Content-Length": 1 }); res.end("1"); }; - -export { logoutController }; diff --git a/src/controllers/api/maturePetController.ts b/src/controllers/api/maturePetController.ts new file mode 100644 index 00000000..1bfb83f6 --- /dev/null +++ b/src/controllers/api/maturePetController.ts @@ -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 maturePetController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "KubrowPets"); + const data = getJSONfromString(String(req.body)); + const details = inventory.KubrowPets.id(data.petId)!.Details!; + details.IsPuppy = data.revert; + await inventory.save(); + res.json({ + petId: data.petId, + updateCollar: true, + armorSkins: ["", "", ""], + furPatterns: data.revert + ? ["", "", ""] + : [details.DominantTraits.FurPattern, details.DominantTraits.FurPattern, details.DominantTraits.FurPattern], + unmature: data.revert + }); +}; + +interface IMaturePetRequest { + petId: string; + revert: boolean; +} diff --git a/src/controllers/api/missionInventoryUpdateController.ts b/src/controllers/api/missionInventoryUpdateController.ts index 39bf8180..4dc5bd66 100644 --- a/src/controllers/api/missionInventoryUpdateController.ts +++ b/src/controllers/api/missionInventoryUpdateController.ts @@ -47,14 +47,13 @@ import { logger } from "@/src/utils/logger"; - [ ] FpsSamples */ //move credit calc in here, return MissionRewards: [] if no reward info -// eslint-disable-next-line @typescript-eslint/no-misused-promises export const missionInventoryUpdateController: RequestHandler = async (req, res): Promise => { const accountId = await getAccountIdForRequest(req); const missionReport = getJSONfromString((req.body as string).toString()); logger.debug("mission report:", missionReport); const inventory = await getInventory(accountId); - const inventoryUpdates = addMissionInventoryUpdates(inventory, missionReport); + const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport); if (missionReport.MissionStatus !== "GS_SUCCESS") { await inventory.save(); diff --git a/src/controllers/api/modularWeaponCraftingController.ts b/src/controllers/api/modularWeaponCraftingController.ts index 4be0d980..bab27b23 100644 --- a/src/controllers/api/modularWeaponCraftingController.ts +++ b/src/controllers/api/modularWeaponCraftingController.ts @@ -1,33 +1,24 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { getInventory, updateCurrency, addEquipment, addMiscItems, - applyDefaultUpgrades + applyDefaultUpgrades, + occupySlot, + productCategoryToInventoryBin, + combineInventoryChanges, + addSpecialItem } from "@/src/services/inventoryService"; -import { ExportWeapons } from "warframe-public-export-plus"; - -const modularWeaponTypes: Record = { - "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary": "LongGuns", - "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam": "LongGuns", - "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryLauncher": "LongGuns", - "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun": "LongGuns", - "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimarySniper": "LongGuns", - "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary": "Pistols", - "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam": "Pistols", - "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun": "Pistols", - "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon": "Melee", - "/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon": "OperatorAmps", - "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit": "Hoverboards", - "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit": "MoaPets", - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit": "MoaPets", - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit": "MoaPets", - "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit": "MoaPets" -}; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { getDefaultUpgrades } from "@/src/services/itemDataService"; +import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper"; +import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { getRandomInt } from "@/src/services/rngService"; +import { ExportSentinels } from "warframe-public-export-plus"; +import { Status } from "@/src/types/inventoryTypes/inventoryTypes"; interface IModularCraftRequest { WeaponType: string; @@ -43,11 +34,110 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res) const category = modularWeaponTypes[data.WeaponType]; const inventory = await getInventory(accountId); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - const configs = applyDefaultUpgrades(inventory, ExportWeapons[data.Parts[0]]?.defaultUpgrades); + const defaultUpgrades = getDefaultUpgrades(data.Parts); + const defaultOverwrites: Partial = { + Configs: applyDefaultUpgrades(inventory, defaultUpgrades) + }; + const inventoryChanges: IInventoryChanges = {}; + if (category == "KubrowPets") { + const traits = { + "/Lotus/Types/Friendly/Pets/CreaturePets/ArmoredInfestedCatbrowPetPowerSuit": { + BaseColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareBase", + SecondaryColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareSecondary", + TertiaryColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareTertiary", + AccentColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareAccent", + EyeColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareEyes", + FurPattern: "/Lotus/Types/Game/InfestedKavatPet/Patterns/InfestedCritterPatternDefault", + Personality: data.WeaponType, + BodyType: "/Lotus/Types/Game/CatbrowPet/BodyTypes/InfestedCatbrowPetRegularBodyType", + Head: "/Lotus/Types/Game/InfestedKavatPet/Heads/InfestedCritterHeadC" + }, + "/Lotus/Types/Friendly/Pets/CreaturePets/HornedInfestedCatbrowPetPowerSuit": { + BaseColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorUncommonBase", + SecondaryColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorUncommonSecondary", + TertiaryColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorUncommonTertiary", + AccentColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorUncommonAccent", + EyeColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorUncommonEyes", + FurPattern: "/Lotus/Types/Game/InfestedKavatPet/Patterns/InfestedCritterPatternDefault", + Personality: data.WeaponType, + BodyType: "/Lotus/Types/Game/CatbrowPet/BodyTypes/InfestedCatbrowPetRegularBodyType", + Head: "/Lotus/Types/Game/InfestedKavatPet/Heads/InfestedCritterHeadB" + }, + "/Lotus/Types/Friendly/Pets/CreaturePets/MedjayPredatorKubrowPetPowerSuit": { + BaseColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareBase", + SecondaryColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareSecondary", + TertiaryColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareTertiary", + AccentColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareAccent", + EyeColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareEyes", + FurPattern: "/Lotus/Types/Game/InfestedPredatorPet/Patterns/InfestedPredatorPatternDefault", + Personality: data.WeaponType, + BodyType: "/Lotus/Types/Game/KubrowPet/BodyTypes/InfestedKubrowPetBodyType", + Head: "/Lotus/Types/Game/InfestedPredatorPet/Heads/InfestedPredatorHeadA" + }, + "/Lotus/Types/Friendly/Pets/CreaturePets/PharaohPredatorKubrowPetPowerSuit": { + BaseColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorUncommonBase", + SecondaryColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorUncommonSecondary", + TertiaryColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorUncommonTertiary", + AccentColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorUncommonAccent", + EyeColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorUncommonEyes", + FurPattern: "/Lotus/Types/Game/InfestedPredatorPet/Patterns/InfestedPredatorPatternDefault", + Personality: data.WeaponType, + BodyType: "/Lotus/Types/Game/KubrowPet/BodyTypes/InfestedKubrowPetBodyType", + Head: "/Lotus/Types/Game/InfestedPredatorPet/Heads/InfestedPredatorHeadB" + }, + "/Lotus/Types/Friendly/Pets/CreaturePets/VizierPredatorKubrowPetPowerSuit": { + BaseColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorCommonBase", + SecondaryColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorCommonSecondary", + TertiaryColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorCommonTertiary", + AccentColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorCommonAccent", + EyeColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorCommonEyes", + FurPattern: "/Lotus/Types/Game/InfestedPredatorPet/Patterns/InfestedPredatorPatternDefault", + Personality: data.WeaponType, + BodyType: "/Lotus/Types/Game/KubrowPet/BodyTypes/InfestedKubrowPetBodyType", + Head: "/Lotus/Types/Game/InfestedPredatorPet/Heads/InfestedPredatorHeadC" + }, + "/Lotus/Types/Friendly/Pets/CreaturePets/VulpineInfestedCatbrowPetPowerSuit": { + BaseColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorCommonBase", + SecondaryColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorCommonSecondary", + TertiaryColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorCommonTertiary", + AccentColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorCommonAccent", + EyeColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorCommonEyes", + FurPattern: "/Lotus/Types/Game/InfestedKavatPet/Patterns/InfestedCritterPatternDefault", + Personality: data.WeaponType, + BodyType: "/Lotus/Types/Game/CatbrowPet/BodyTypes/InfestedCatbrowPetRegularBodyType", + Head: "/Lotus/Types/Game/InfestedKavatPet/Heads/InfestedCritterHeadA" + } + }[data.WeaponType]; - // Give weapon - const inventoryChanges = addEquipment(inventory, category, data.WeaponType, data.Parts, {}, { Configs: configs }); + if (!traits) { + throw new Error(`unknown KubrowPets type: ${data.WeaponType}`); + } + + defaultOverwrites.Details = { + Name: "", + IsPuppy: false, + HasCollar: true, + PrintsRemaining: 2, + Status: Status.StatusStasis, + HatchDate: new Date(Math.trunc(Date.now() / 86400000) * 86400000), + IsMale: !!getRandomInt(0, 1), + Size: getRandomInt(70, 100) / 100, + DominantTraits: traits, + RecessiveTraits: traits + }; + + // Only save mutagen & antigen in the ModularParts. + defaultOverwrites.ModularParts = [data.Parts[1], data.Parts[2]]; + + for (const specialItem of ExportSentinels[data.WeaponType].exalted!) { + addSpecialItem(inventory, specialItem, inventoryChanges); + } + } + addEquipment(inventory, category, data.WeaponType, data.Parts, inventoryChanges, defaultOverwrites); + combineInventoryChanges(inventoryChanges, occupySlot(inventory, productCategoryToInventoryBin(category)!, false)); + if (defaultUpgrades) { + inventoryChanges.RawUpgrades = defaultUpgrades.map(x => ({ ItemType: x.ItemType, ItemCount: 1 })); + } // Remove credits & parts const miscItemChanges = []; @@ -59,7 +149,13 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res) } const currencyChanges = updateCurrency( inventory, - category == "Hoverboards" || category == "MoaPets" ? 5000 : 4000, + category == "Hoverboards" || + category == "MoaPets" || + category == "LongGuns" || + category == "Pistols" || + category == "KubrowPets" + ? 5000 + : 4000, // Definitely correct for Melee & OperatorAmps false ); addMiscItems(inventory, miscItemChanges); diff --git a/src/controllers/api/modularWeaponSaleController.ts b/src/controllers/api/modularWeaponSaleController.ts index fac479f3..a8ddfc20 100644 --- a/src/controllers/api/modularWeaponSaleController.ts +++ b/src/controllers/api/modularWeaponSaleController.ts @@ -1,8 +1,185 @@ import { RequestHandler } from "express"; -import modularWeaponSale from "@/static/fixed_responses/modularWeaponSale.json"; +import { ExportWeapons } from "warframe-public-export-plus"; +import { IMongoDate } from "@/src/types/commonTypes"; +import { toMongoDate } from "@/src/helpers/inventoryHelpers"; +import { CRng } from "@/src/services/rngService"; +import { ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { + addEquipment, + applyDefaultUpgrades, + getInventory, + occupySlot, + productCategoryToInventoryBin, + updateCurrency +} from "@/src/services/inventoryService"; +import { getDefaultUpgrades } from "@/src/services/itemDataService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; -const modularWeaponSaleController: RequestHandler = (_req, res) => { - res.json(modularWeaponSale); +export const modularWeaponSaleController: RequestHandler = async (req, res) => { + const partTypeToParts: Record = {}; + for (const [uniqueName, data] of Object.entries(ExportWeapons)) { + if (data.partType && data.premiumPrice) { + partTypeToParts[data.partType] ??= []; + partTypeToParts[data.partType].push(uniqueName); + } + } + + if (req.query.op == "SyncAll") { + res.json({ + SaleInfos: getSaleInfos(partTypeToParts, Math.trunc(Date.now() / 86400000)) + }); + } else if (req.query.op == "Purchase") { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const payload = getJSONfromString(String(req.body)); + const weaponInfo = getSaleInfos(partTypeToParts, payload.Revision).find(x => x.Name == payload.SaleName)! + .Weapons[payload.ItemIndex]; + const category = modularWeaponTypes[weaponInfo.ItemType]; + const defaultUpgrades = getDefaultUpgrades(weaponInfo.ModularParts); + const configs = applyDefaultUpgrades(inventory, defaultUpgrades); + const inventoryChanges: IInventoryChanges = { + ...addEquipment( + inventory, + category, + weaponInfo.ItemType, + weaponInfo.ModularParts, + {}, + { + Features: EquipmentFeatures.DOUBLE_CAPACITY | EquipmentFeatures.GILDED, + ItemName: payload.ItemName, + Configs: configs, + Polarity: [ + { + Slot: payload.PolarizeSlot, + Value: payload.PolarizeValue + } + ] + } + ), + ...occupySlot(inventory, productCategoryToInventoryBin(category)!, true), + ...updateCurrency(inventory, weaponInfo.PremiumPrice, true) + }; + if (defaultUpgrades) { + inventoryChanges.RawUpgrades = defaultUpgrades.map(x => ({ ItemType: x.ItemType, ItemCount: 1 })); + } + await inventory.save(); + res.json({ + InventoryChanges: inventoryChanges + }); + } else { + throw new Error(`unknown modularWeaponSale op: ${String(req.query.op)}`); + } }; -export { modularWeaponSaleController }; +const getSaleInfos = (partTypeToParts: Record, day: number): IModularWeaponSaleInfo[] => { + const kitgunIsPrimary: boolean = (day & 1) != 0; + return [ + getModularWeaponSale( + partTypeToParts, + day, + "Ostron", + ["LWPT_HILT", "LWPT_BLADE", "LWPT_HILT_WEIGHT"], + () => "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon" + ), + getModularWeaponSale( + partTypeToParts, + day, + "SolarisUnitedHoverboard", + ["LWPT_HB_DECK", "LWPT_HB_ENGINE", "LWPT_HB_FRONT", "LWPT_HB_JET"], + () => "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit" + ), + getModularWeaponSale( + partTypeToParts, + day, + "SolarisUnitedMoaPet", + ["LWPT_MOA_LEG", "LWPT_MOA_HEAD", "LWPT_MOA_ENGINE", "LWPT_MOA_PAYLOAD"], + () => "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit" + ), + getModularWeaponSale( + partTypeToParts, + day, + "SolarisUnitedKitGun", + [ + kitgunIsPrimary ? "LWPT_GUN_PRIMARY_HANDLE" : "LWPT_GUN_SECONDARY_HANDLE", + "LWPT_GUN_BARREL", + "LWPT_GUN_CLIP" + ], + (parts: string[]) => { + const barrel = parts[1]; + const gunType = ExportWeapons[barrel].gunType!; + if (kitgunIsPrimary) { + return { + GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", + GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun", + GT_BEAM: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam" + }[gunType]; + } else { + return { + GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary", + GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun", + GT_BEAM: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam" + }[gunType]; + } + } + ) + ]; +}; + +const priceFactor: Record = { + Ostron: 0.9, + SolarisUnitedHoverboard: 0.85, + SolarisUnitedMoaPet: 0.95, + SolarisUnitedKitGun: 0.9 +}; + +const getModularWeaponSale = ( + partTypeToParts: Record, + day: number, + name: string, + partTypes: string[], + getItemType: (parts: string[]) => string +): IModularWeaponSaleInfo => { + const rng = new CRng(day); + const parts = partTypes.map(partType => rng.randomElement(partTypeToParts[partType])); + let partsCost = 0; + for (const part of parts) { + partsCost += ExportWeapons[part].premiumPrice!; + } + return { + Name: name, + Expiry: toMongoDate(new Date((day + 1) * 86400000)), + Revision: day, + Weapons: [ + { + ItemType: getItemType(parts), + PremiumPrice: Math.trunc(partsCost * priceFactor[name]), + ModularParts: parts + } + ] + }; +}; + +interface IModularWeaponSaleInfo { + Name: string; + Expiry: IMongoDate; + Revision: number; + Weapons: IModularWeaponSaleItem[]; +} + +interface IModularWeaponSaleItem { + ItemType: string; + PremiumPrice: number; + ModularParts: string[]; +} + +interface IModularWeaponPurchaseRequest { + SaleName: string; + ItemIndex: number; + Revision: number; + ItemName: string; + PolarizeSlot: number; + PolarizeValue: ArtifactPolarity; +} diff --git a/src/controllers/api/nameWeaponController.ts b/src/controllers/api/nameWeaponController.ts index 843feeb9..5d1011be 100644 --- a/src/controllers/api/nameWeaponController.ts +++ b/src/controllers/api/nameWeaponController.ts @@ -12,15 +12,17 @@ export const nameWeaponController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId); const body = getJSONfromString(String(req.body)); - const item = inventory[req.query.Category as string as TEquipmentKey].find( - item => item._id.toString() == (req.query.ItemId as string) - )!; + const item = inventory[req.query.Category as string as TEquipmentKey].id(req.query.ItemId as string)!; if (body.ItemName != "") { item.ItemName = body.ItemName; } else { item.ItemName = undefined; } - const currencyChanges = updateCurrency(inventory, "webui" in req.query ? 0 : 15, true); + const currencyChanges = updateCurrency( + inventory, + req.query.Category == "Horses" || "webui" in req.query ? 0 : 15, + true + ); await inventory.save(); res.json({ InventoryChanges: currencyChanges diff --git a/src/controllers/api/nemesisController.ts b/src/controllers/api/nemesisController.ts new file mode 100644 index 00000000..34e6bd4a --- /dev/null +++ b/src/controllers/api/nemesisController.ts @@ -0,0 +1,210 @@ +import { getInfNodes, getNemesisPasscode } from "@/src/helpers/nemesisHelpers"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { freeUpSlot, getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { SRng } from "@/src/services/rngService"; +import { IMongoDate, IOid } from "@/src/types/commonTypes"; +import { IInnateDamageFingerprint, InventorySlot, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; +import { logger } from "@/src/utils/logger"; +import { RequestHandler } from "express"; + +export const nemesisController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + if ((req.query.mode as string) == "f") { + const body = getJSONfromString(String(req.body)); + const inventory = await getInventory(accountId, body.Category + " WeaponBin"); + const destWeapon = inventory[body.Category].id(body.DestWeapon.$oid)!; + const sourceWeapon = inventory[body.Category].id(body.SourceWeapon.$oid)!; + const destFingerprint = JSON.parse(destWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint; + const sourceFingerprint = JSON.parse(sourceWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint; + + // Update destination damage type if desired + if (body.UseSourceDmgType) { + destFingerprint.buffs[0].Tag = sourceFingerprint.buffs[0].Tag; + } + + // Upgrade destination damage value + const destDamage = 0.25 + (destFingerprint.buffs[0].Value / 0x3fffffff) * (0.6 - 0.25); + const sourceDamage = 0.25 + (sourceFingerprint.buffs[0].Value / 0x3fffffff) * (0.6 - 0.25); + let newDamage = Math.max(destDamage, sourceDamage) * 1.1; + if (newDamage >= 0.5794998) { + newDamage = 0.6; + } + destFingerprint.buffs[0].Value = Math.trunc(((newDamage - 0.25) / (0.6 - 0.25)) * 0x3fffffff); + + // Commit fingerprint + destWeapon.UpgradeFingerprint = JSON.stringify(destFingerprint); + + // Remove source weapon + inventory[body.Category].pull({ _id: body.SourceWeapon.$oid }); + freeUpSlot(inventory, InventorySlot.WEAPONS); + + await inventory.save(); + res.json({ + InventoryChanges: { + [body.Category]: [destWeapon.toJSON()], + RemovedIdItems: [{ ItemId: body.SourceWeapon }] + } + }); + } else if ((req.query.mode as string) == "p") { + const inventory = await getInventory(accountId, "Nemesis"); + const body = getJSONfromString(String(req.body)); + const passcode = getNemesisPasscode(inventory.Nemesis!.fp, inventory.Nemesis!.Faction); + let guessResult = 0; + if (inventory.Nemesis!.Faction == "FC_INFESTATION") { + for (let i = 0; i != 3; ++i) { + if (body.guess[i] == passcode[0]) { + guessResult = 1 + i; + break; + } + } + } else { + for (let i = 0; i != 3; ++i) { + if (body.guess[i] == passcode[i]) { + ++guessResult; + } + } + } + res.json({ GuessResult: guessResult }); + } else if ((req.query.mode as string) == "s") { + const inventory = await getInventory(accountId, "Nemesis"); + const body = getJSONfromString(String(req.body)); + body.target.fp = BigInt(body.target.fp); + + let weaponIdx = -1; + if (body.target.Faction != "FC_INFESTATION") { + let weapons: readonly string[]; + if (body.target.manifest == "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix") { + weapons = kuvaLichVersionSixWeapons; + } else if ( + body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour" || + body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree" + ) { + weapons = corpusVersionThreeWeapons; + } else { + throw new Error(`unknown nemesis manifest: ${body.target.manifest}`); + } + + const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1); + weaponIdx = initialWeaponIdx; + do { + const weapon = weapons[weaponIdx]; + if (!body.target.DisallowedWeapons.find(x => x == weapon)) { + break; + } + weaponIdx = (weaponIdx + 1) % weapons.length; + } while (weaponIdx != initialWeaponIdx); + } + + inventory.Nemesis = { + fp: body.target.fp, + manifest: body.target.manifest, + KillingSuit: body.target.KillingSuit, + killingDamageType: body.target.killingDamageType, + ShoulderHelmet: body.target.ShoulderHelmet, + WeaponIdx: weaponIdx, + AgentIdx: body.target.AgentIdx, + BirthNode: body.target.BirthNode, + Faction: body.target.Faction, + Rank: 0, + k: false, + Traded: false, + d: new Date(), + InfNodes: getInfNodes(body.target.Faction, 0), + GuessHistory: [], + Hints: [], + HintProgress: 0, + Weakened: body.target.Weakened, + PrevOwners: 0, + HenchmenKilled: 0, + SecondInCommand: body.target.SecondInCommand, + MissionCount: 0, + LastEnc: 0 + }; + await inventory.save(); + + res.json({ + target: inventory.toJSON().Nemesis + }); + } else { + logger.debug(`data provided to ${req.path}: ${String(req.body)}`); + throw new Error(`unknown nemesis mode: ${String(req.query.mode)}`); + } +}; + +interface IValenceFusionRequest { + DestWeapon: IOid; + SourceWeapon: IOid; + Category: TEquipmentKey; + UseSourceDmgType: boolean; +} + +interface INemesisStartRequest { + target: { + fp: number | bigint; + manifest: string; + KillingSuit: string; + killingDamageType: number; + ShoulderHelmet: string; + DisallowedWeapons: string[]; + WeaponIdx: number; + AgentIdx: number; + BirthNode: string; + Faction: string; + Rank: number; + k: boolean; + Traded: boolean; + d: IMongoDate; + InfNodes: []; + GuessHistory: []; + Hints: []; + HintProgress: number; + Weakened: boolean; + PrevOwners: number; + HenchmenKilled: number; + MissionCount?: number; // Added in 38.5.0 + LastEnc?: number; // Added in 38.5.0 + SecondInCommand: boolean; + }; +} + +interface INemesisPrespawnCheckRequest { + guess: number[]; // .length == 3 + potency?: number[]; +} + +const kuvaLichVersionSixWeapons = [ + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak", + "/Lotus/Weapons/Grineer/Melee/GrnKuvaLichScythe/GrnKuvaLichScytheWeapon", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Kohm/KuvaKohm", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Ogris/KuvaOgris", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Quartakk/KuvaQuartakk", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Tonkor/KuvaTonkor", + "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Brakk/KuvaBrakk", + "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Kraken/KuvaKraken", + "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Seer/KuvaSeer", + "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Stubba/KuvaStubba", + "/Lotus/Weapons/Grineer/HeavyWeapons/GrnHeavyGrenadeLauncher", + "/Lotus/Weapons/Grineer/LongGuns/GrnKuvaLichRifle/GrnKuvaLichRifleWeapon", + "/Lotus/Weapons/Grineer/Bows/GrnBow/GrnBowWeapon", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hind/KuvaHind", + "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Nukor/KuvaNukor", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hek/KuvaHekWeapon", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Zarr/KuvaZarr", + "/Lotus/Weapons/Grineer/KuvaLich/HeavyWeapons/Grattler/KuvaGrattler", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Sobek/KuvaSobek" +]; + +const corpusVersionThreeWeapons = [ + "/Lotus/Weapons/Corpus/LongGuns/CrpBriefcaseLauncher/CrpBriefcaseLauncher", + "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEArcaPlasmor/CrpBEArcaPlasmor", + "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEFluxRifle/CrpBEFluxRifle", + "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBETetra/CrpBETetra", + "/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBECycron/CrpBECycron", + "/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEDetron/CrpBEDetron", + "/Lotus/Weapons/Corpus/Pistols/CrpIgniterPistol/CrpIgniterPistol", + "/Lotus/Weapons/Corpus/Pistols/CrpBriefcaseAkimbo/CrpBriefcaseAkimboPistol", + "/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEPlinx/CrpBEPlinxWeapon", + "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEGlaxion/CrpBEGlaxion" +]; diff --git a/src/controllers/api/placeDecoInComponentController.ts b/src/controllers/api/placeDecoInComponentController.ts new file mode 100644 index 00000000..ea40dae3 --- /dev/null +++ b/src/controllers/api/placeDecoInComponentController.ts @@ -0,0 +1,116 @@ +import { + getDojoClient, + getGuildForRequestEx, + getVaultMiscItemCount, + hasAccessToDojo, + hasGuildPermission, + processDojoBuildMaterialsGathered, + scaleRequiredCount +} from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; +import { Types } from "mongoose"; +import { ExportDojoRecipes, ExportResources } from "warframe-public-export-plus"; + +export const placeDecoInComponentController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId LevelKeys"); + const guild = await getGuildForRequestEx(req, inventory); + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Decorator))) { + res.json({ DojoRequestStatus: -1 }); + return; + } + const request = JSON.parse(String(req.body)) as IPlaceDecoInComponentRequest; + const component = guild.DojoComponents.id(request.ComponentId)!; + + if (component.DecoCapacity === undefined) { + component.DecoCapacity = Object.values(ExportDojoRecipes.rooms).find( + x => x.resultType == component.pf + )!.decoCapacity; + } + + component.Decos ??= []; + if (request.MoveId) { + const deco = component.Decos.find(x => x._id.equals(request.MoveId))!; + deco.Pos = request.Pos; + deco.Rot = request.Rot; + } else { + const deco = + component.Decos[ + component.Decos.push({ + _id: new Types.ObjectId(), + Type: request.Type, + Pos: request.Pos, + Rot: request.Rot, + Name: request.Name, + Sockets: request.Sockets + }) - 1 + ]; + const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == request.Type); + if (meta) { + if (meta.capacityCost) { + component.DecoCapacity -= meta.capacityCost; + } + } else { + const itemType = Object.entries(ExportResources).find(arr => arr[1].deco == deco.Type)![0]; + if (deco.Sockets !== undefined) { + guild.VaultFusionTreasures!.find(x => x.ItemType == itemType && x.Sockets == deco.Sockets)!.ItemCount -= + 1; + } else { + guild.VaultShipDecorations!.find(x => x.ItemType == itemType)!.ItemCount -= 1; + } + } + if (!meta || (meta.price == 0 && meta.ingredients.length == 0)) { + deco.CompletionTime = new Date(); + } else if (guild.AutoContributeFromVault && guild.VaultRegularCredits && guild.VaultMiscItems) { + if (guild.VaultRegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) { + let enoughMiscItems = true; + for (const ingredient of meta.ingredients) { + if ( + getVaultMiscItemCount(guild, ingredient.ItemType) < + scaleRequiredCount(guild.Tier, ingredient.ItemCount) + ) { + enoughMiscItems = false; + break; + } + } + if (enoughMiscItems) { + guild.VaultRegularCredits -= scaleRequiredCount(guild.Tier, meta.price); + deco.RegularCredits = scaleRequiredCount(guild.Tier, meta.price); + + deco.MiscItems = []; + for (const ingredient of meta.ingredients) { + guild.VaultMiscItems.find(x => x.ItemType == ingredient.ItemType)!.ItemCount -= + scaleRequiredCount(guild.Tier, ingredient.ItemCount); + deco.MiscItems.push({ + ItemType: ingredient.ItemType, + ItemCount: scaleRequiredCount(guild.Tier, ingredient.ItemCount) + }); + } + + deco.CompletionTime = new Date(Date.now() + meta.time * 1000); + processDojoBuildMaterialsGathered(guild, meta); + } + } + } + } + + await guild.save(); + res.json(await getDojoClient(guild, 0, component._id)); +}; + +interface IPlaceDecoInComponentRequest { + ComponentId: string; + Revision: number; + Type: string; + Pos: number[]; + Rot: number[]; + Name?: string; + Sockets?: number; + Scale?: number; // only provided alongside MoveId and seems to always be 1 + MoveId?: string; + ShipDeco?: boolean; + VaultDeco?: boolean; +} diff --git a/src/controllers/api/playerSkillsController.ts b/src/controllers/api/playerSkillsController.ts index de7ab4c8..5c8b301a 100644 --- a/src/controllers/api/playerSkillsController.ts +++ b/src/controllers/api/playerSkillsController.ts @@ -6,7 +6,7 @@ import { RequestHandler } from "express"; export const playerSkillsController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId); + const inventory = await getInventory(accountId, "PlayerSkills"); const request = getJSONfromString(String(req.body)); const oldRank: number = inventory.PlayerSkills[request.Skill as keyof IPlayerSkills]; diff --git a/src/controllers/api/postGuildAdvertisementController.ts b/src/controllers/api/postGuildAdvertisementController.ts new file mode 100644 index 00000000..69b28323 --- /dev/null +++ b/src/controllers/api/postGuildAdvertisementController.ts @@ -0,0 +1,75 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { GuildAd, GuildMember } from "@/src/models/guildModel"; +import { + addGuildMemberMiscItemContribution, + addVaultMiscItems, + getGuildForRequestEx, + getVaultMiscItemCount, + hasGuildPermissionEx +} from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getVendorManifestByTypeName } from "@/src/services/serversideVendorsService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { IPurchaseParams } from "@/src/types/purchaseTypes"; +import { RequestHandler } from "express"; + +export const postGuildAdvertisementController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId MiscItems"); + const guild = await getGuildForRequestEx(req, inventory); + const guildMember = (await GuildMember.findOne({ accountId, guildId: guild._id }))!; + if (!hasGuildPermissionEx(guild, guildMember, GuildPermission.Advertiser)) { + res.status(400).end(); + return; + } + const payload = getJSONfromString(String(req.body)); + + // Handle resource cost + const vendor = getVendorManifestByTypeName( + "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest" + )!; + const offer = vendor.VendorInfo.ItemManifest.find(x => x.StoreItem == payload.PurchaseParams.StoreItem)!; + if (getVaultMiscItemCount(guild, offer.ItemPrices![0].ItemType) >= offer.ItemPrices![0].ItemCount) { + addVaultMiscItems(guild, [ + { + ItemType: offer.ItemPrices![0].ItemType, + ItemCount: offer.ItemPrices![0].ItemCount * -1 + } + ]); + } else { + const miscItem = inventory.MiscItems.find(x => x.ItemType == offer.ItemPrices![0].ItemType); + if (!miscItem || miscItem.ItemCount < offer.ItemPrices![0].ItemCount) { + res.status(400).json("Insufficient funds"); + return; + } + miscItem.ItemCount -= offer.ItemPrices![0].ItemCount; + addGuildMemberMiscItemContribution(guildMember, offer.ItemPrices![0]); + await guildMember.save(); + await inventory.save(); + } + + // Create or update ad + await GuildAd.findOneAndUpdate( + { GuildId: guild._id }, + { + Emblem: guild.Emblem, + Expiry: new Date(Date.now() + 12 * 3600 * 1000), + Features: payload.Features, + GuildName: guild.Name, + MemberCount: await GuildMember.countDocuments({ guildId: guild._id, status: 0 }), + RecruitMsg: payload.RecruitMsg, + Tier: guild.Tier + }, + { upsert: true } + ); + + res.end(); +}; + +interface IPostGuildAdvertisementRequest { + Features: number; + RecruitMsg: string; + Languages: string[]; + PurchaseParams: IPurchaseParams; +} diff --git a/src/controllers/api/queueDojoComponentDestructionController.ts b/src/controllers/api/queueDojoComponentDestructionController.ts index 7f612896..037b5c27 100644 --- a/src/controllers/api/queueDojoComponentDestructionController.ts +++ b/src/controllers/api/queueDojoComponentDestructionController.ts @@ -1,19 +1,24 @@ -import { getDojoClient, getGuildForRequest } from "@/src/services/guildService"; +import { config } from "@/src/services/configService"; +import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; import { RequestHandler } from "express"; -import { ExportDojoRecipes } from "warframe-public-export-plus"; export const queueDojoComponentDestructionController: RequestHandler = async (req, res) => { - const guild = await getGuildForRequest(req); - const componentId = req.query.componentId as string; - const component = guild.DojoComponents!.splice( - guild.DojoComponents!.findIndex(x => x._id.toString() === componentId), - 1 - )[0]; - const room = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf); - if (room) { - guild.DojoCapacity -= room.capacity; - guild.DojoEnergy -= room.energy; + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId LevelKeys"); + const guild = await getGuildForRequestEx(req, inventory); + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Architect))) { + res.json({ DojoRequestStatus: -1 }); + return; } + const componentId = req.query.componentId as string; + + guild.DojoComponents.id(componentId)!.DestructionTime = new Date( + (Math.trunc(Date.now() / 1000) + (config.fastDojoRoomDestruction ? 5 : 2 * 3600)) * 1000 + ); + await guild.save(); - res.json(getDojoClient(guild, 1)); + res.json(await getDojoClient(guild, 0, componentId)); }; diff --git a/src/controllers/api/redeemPromoCodeController.ts b/src/controllers/api/redeemPromoCodeController.ts new file mode 100644 index 00000000..f0e615bc --- /dev/null +++ b/src/controllers/api/redeemPromoCodeController.ts @@ -0,0 +1,34 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { RequestHandler } from "express"; +import glyphCodes from "@/static/fixed_responses/glyphsCodes.json"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { addItem, getInventory } from "@/src/services/inventoryService"; + +export const redeemPromoCodeController: RequestHandler = async (req, res) => { + const body = getJSONfromString(String(req.body)); + if (!(body.codeId in glyphCodes)) { + res.status(400).send("INVALID_CODE").end(); + return; + } + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "FlavourItems"); + const acquiredGlyphs: string[] = []; + for (const glyph of (glyphCodes as Record)[body.codeId]) { + if (!inventory.FlavourItems.find(x => x.ItemType == glyph)) { + acquiredGlyphs.push(glyph); + await addItem(inventory, glyph); + } + } + if (acquiredGlyphs.length == 0) { + res.status(400).send("USED_CODE").end(); + return; + } + await inventory.save(); + res.json({ + FlavourItems: acquiredGlyphs + }); +}; + +interface IRedeemPromoCodeRequest { + codeId: string; +} diff --git a/src/controllers/api/releasePetController.ts b/src/controllers/api/releasePetController.ts new file mode 100644 index 00000000..10625778 --- /dev/null +++ b/src/controllers/api/releasePetController.ts @@ -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 releasePetController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "RegularCredits KubrowPets"); + const payload = getJSONfromString(String(req.body)); + + const inventoryChanges = updateCurrency(inventory, 25000, false); + + inventoryChanges.RemovedIdItems = [{ ItemId: { $oid: payload.petId } }]; + inventory.KubrowPets.pull({ _id: payload.petId }); + + await inventory.save(); + res.json({ inventoryChanges }); // Not a mistake; it's "inventoryChanges" here. +}; + +interface IReleasePetRequest { + recipeName: "/Lotus/Types/Game/KubrowPet/ReleasePetRecipe"; + petId: string; +} diff --git a/src/controllers/api/removeFromAllianceController.ts b/src/controllers/api/removeFromAllianceController.ts new file mode 100644 index 00000000..f6dc8acc --- /dev/null +++ b/src/controllers/api/removeFromAllianceController.ts @@ -0,0 +1,38 @@ +import { AllianceMember, Guild, GuildMember } from "@/src/models/guildModel"; +import { deleteAlliance } from "@/src/services/guildService"; +import { getAccountForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const removeFromAllianceController: RequestHandler = async (req, res) => { + // Check requester is a warlord in their guild + const account = await getAccountForRequest(req); + const guildMember = (await GuildMember.findOne({ accountId: account._id, status: 0 }))!; + if (guildMember.rank > 1) { + res.status(400).json({ Error: 104 }); + return; + } + + let allianceMember = (await AllianceMember.findOne({ guildId: guildMember.guildId }))!; + if (!guildMember.guildId.equals(req.query.guildId as string)) { + // Removing a guild that is not our own needs additional permissions + if (!(allianceMember.Permissions & GuildPermission.Ruler)) { + res.status(400).json({ Error: 104 }); + return; + } + + // Update allianceMember to point to the alliance to kick + allianceMember = (await AllianceMember.findOne({ guildId: req.query.guildId }))!; + } + + if (allianceMember.Permissions & GuildPermission.Ruler) { + await deleteAlliance(allianceMember.allianceId); + } else { + await Promise.all([ + await Guild.updateOne({ _id: allianceMember.guildId }, { $unset: { AllianceId: "" } }), + await AllianceMember.deleteOne({ _id: allianceMember._id }) + ]); + } + + res.end(); +}; diff --git a/src/controllers/api/removeFromGuildController.ts b/src/controllers/api/removeFromGuildController.ts new file mode 100644 index 00000000..db5a2ea3 --- /dev/null +++ b/src/controllers/api/removeFromGuildController.ts @@ -0,0 +1,88 @@ +import { GuildMember } from "@/src/models/guildModel"; +import { Inbox } from "@/src/models/inboxModel"; +import { Account } from "@/src/models/loginModel"; +import { deleteGuild, getGuildForRequest, hasGuildPermission, removeDojoKeyItems } from "@/src/services/guildService"; +import { createMessage } from "@/src/services/inboxService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const removeFromGuildController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + const guild = await getGuildForRequest(req); + const payload = JSON.parse(String(req.body)) as IRemoveFromGuildRequest; + const isKick = !account._id.equals(payload.userId); + if (isKick && !(await hasGuildPermission(guild, account._id, GuildPermission.Regulator))) { + res.status(400).json("Invalid permission"); + return; + } + + const guildMember = (await GuildMember.findOne({ accountId: payload.userId, guildId: guild._id }))!; + if (guildMember.rank == 0) { + await deleteGuild(guild._id); + } else { + if (guildMember.status == 0) { + const inventory = await getInventory(payload.userId, "GuildId LevelKeys Recipes"); + inventory.GuildId = undefined; + removeDojoKeyItems(inventory); + await inventory.save(); + } else if (guildMember.status == 1) { + // TOVERIFY: Is this inbox message actually sent on live? + await createMessage(guildMember.accountId, [ + { + sndr: "/Lotus/Language/Bosses/Ordis", + msg: "/Lotus/Language/Clan/RejectedFromClan", + sub: "/Lotus/Language/Clan/RejectedFromClanHeader", + arg: [ + { + Key: "PLAYER_NAME", + Tag: (await Account.findOne({ _id: guildMember.accountId }, "DisplayName"))!.DisplayName + }, + { + Key: "CLAN_NAME", + Tag: guild.Name + } + ] + // TOVERIFY: If this message is sent on live, is it highPriority? + } + ]); + } else if (guildMember.status == 2) { + // Delete the inbox message for the invite + await Inbox.deleteOne({ + ownerId: guildMember.accountId, + contextInfo: guild._id.toString(), + acceptAction: "GUILD_INVITE" + }); + } + await GuildMember.deleteOne({ _id: guildMember._id }); + + guild.RosterActivity ??= []; + if (isKick) { + const kickee = (await Account.findById(payload.userId))!; + guild.RosterActivity.push({ + dateTime: new Date(), + entryType: 12, + details: getSuffixedName(kickee) + "," + getSuffixedName(account) + }); + } else { + guild.RosterActivity.push({ + dateTime: new Date(), + entryType: 7, + details: getSuffixedName(account) + }); + } + await guild.save(); + } + + res.json({ + _id: payload.userId, + ItemToRemove: "/Lotus/Types/Keys/DojoKey", + RecipeToRemove: "/Lotus/Types/Keys/DojoKeyBlueprint" + }); +}; + +interface IRemoveFromGuildRequest { + userId: string; + kicker?: string; +} diff --git a/src/controllers/api/rerollRandomModController.ts b/src/controllers/api/rerollRandomModController.ts index e35df1a8..20e7218a 100644 --- a/src/controllers/api/rerollRandomModController.ts +++ b/src/controllers/api/rerollRandomModController.ts @@ -2,43 +2,54 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { addMiscItems, getInventory } from "@/src/services/inventoryService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { IUnveiledRivenFingerprint, randomiseRivenStats } from "@/src/helpers/rivenFingerprintHelper"; +import { createUnveiledRivenFingerprint, randomiseRivenStats, RivenFingerprint } from "@/src/helpers/rivenHelper"; import { ExportUpgrades } from "warframe-public-export-plus"; +import { IOid } from "@/src/types/commonTypes"; export const rerollRandomModController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const request = getJSONfromString(String(req.body)); if ("ItemIds" in request) { const inventory = await getInventory(accountId, "Upgrades MiscItems"); - const upgrade = inventory.Upgrades.id(request.ItemIds[0])!; - const fingerprint = JSON.parse(upgrade.UpgradeFingerprint!) as IUnveiledRivenFingerprint; + const changes: IChange[] = []; + let totalKuvaCost = 0; + request.ItemIds.forEach(itemId => { + const upgrade = inventory.Upgrades.id(itemId)!; + const fingerprint = JSON.parse(upgrade.UpgradeFingerprint!) as RivenFingerprint; + if ("challenge" in fingerprint) { + upgrade.UpgradeFingerprint = JSON.stringify( + createUnveiledRivenFingerprint(ExportUpgrades[upgrade.ItemType]) + ); + } else { + fingerprint.rerolls ??= 0; + const kuvaCost = fingerprint.rerolls < rerollCosts.length ? rerollCosts[fingerprint.rerolls] : 3500; + totalKuvaCost += kuvaCost; + addMiscItems(inventory, [ + { + ItemType: "/Lotus/Types/Items/MiscItems/Kuva", + ItemCount: kuvaCost * -1 + } + ]); - fingerprint.rerolls ??= 0; - const kuvaCost = fingerprint.rerolls < rerollCosts.length ? rerollCosts[fingerprint.rerolls] : 3500; - addMiscItems(inventory, [ - { - ItemType: "/Lotus/Types/Items/MiscItems/Kuva", - ItemCount: kuvaCost * -1 + fingerprint.rerolls++; + upgrade.UpgradeFingerprint = JSON.stringify(fingerprint); + + randomiseRivenStats(ExportUpgrades[upgrade.ItemType], fingerprint); + upgrade.PendingRerollFingerprint = JSON.stringify(fingerprint); } - ]); - fingerprint.rerolls++; - upgrade.UpgradeFingerprint = JSON.stringify(fingerprint); - - randomiseRivenStats(ExportUpgrades[upgrade.ItemType], fingerprint); - upgrade.PendingRerollFingerprint = JSON.stringify(fingerprint); + changes.push({ + ItemId: { $oid: request.ItemIds[0] }, + UpgradeFingerprint: upgrade.UpgradeFingerprint, + PendingRerollFingerprint: upgrade.PendingRerollFingerprint + }); + }); await inventory.save(); res.json({ - changes: [ - { - ItemId: { $oid: request.ItemIds[0] }, - UpgradeFingerprint: upgrade.UpgradeFingerprint, - PendingRerollFingerprint: upgrade.PendingRerollFingerprint - } - ], - cost: kuvaCost + changes: changes, + cost: totalKuvaCost }); } else { const inventory = await getInventory(accountId, "Upgrades"); @@ -63,4 +74,10 @@ interface AwDangitRequest { CommitReroll: boolean; } +interface IChange { + ItemId: IOid; + UpgradeFingerprint?: string; + PendingRerollFingerprint?: string; +} + const rerollCosts = [900, 1000, 1200, 1400, 1700, 2000, 2350, 2750, 3150]; diff --git a/src/controllers/api/retrievePetFromStasisController.ts b/src/controllers/api/retrievePetFromStasisController.ts new file mode 100644 index 00000000..8547f7f4 --- /dev/null +++ b/src/controllers/api/retrievePetFromStasisController.ts @@ -0,0 +1,33 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { Status } from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; + +export const retrievePetFromStasisController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "KubrowPets"); + const data = getJSONfromString(String(req.body)); + + let oldPetId: string | undefined; + for (const pet of inventory.KubrowPets) { + if (pet.Details!.Status == Status.StatusAvailable) { + pet.Details!.Status = Status.StatusStasis; + oldPetId = pet._id.toString(); + break; + } + } + + inventory.KubrowPets.id(data.petId)!.Details!.Status = Status.StatusAvailable; + + await inventory.save(); + res.json({ + petId: data.petId, + oldPetId, + status: Status.StatusAvailable + }); +}; + +interface IRetrievePetFromStasisRequest { + petId: string; +} diff --git a/src/controllers/api/saveDialogueController.ts b/src/controllers/api/saveDialogueController.ts index 332cb235..55819636 100644 --- a/src/controllers/api/saveDialogueController.ts +++ b/src/controllers/api/saveDialogueController.ts @@ -81,5 +81,11 @@ interface SaveCompletedDialogueRequest { Booleans: string[]; ResetBooleans: string[]; Data: ICompletedDialogue; - OtherDialogueInfos: string[]; // unsure + OtherDialogueInfos: IOtherDialogueInfo[]; // unsure +} + +interface IOtherDialogueInfo { + Dialogue: string; + Tag: string; + Value: number; } diff --git a/src/controllers/api/saveSettingsController.ts b/src/controllers/api/saveSettingsController.ts index 72bf8bfa..15304626 100644 --- a/src/controllers/api/saveSettingsController.ts +++ b/src/controllers/api/saveSettingsController.ts @@ -13,10 +13,10 @@ const saveSettingsController: RequestHandler = async (req, res): Promise = const settingResults = getJSONfromString(String(req.body)); - const inventory = await getInventory(accountId); - inventory.Settings = Object.assign(inventory.Settings, settingResults.Settings); + const inventory = await getInventory(accountId, "Settings"); + inventory.Settings = Object.assign(inventory.Settings ?? {}, settingResults.Settings); await inventory.save(); - res.json(inventory.Settings); + res.json({ Settings: inventory.Settings }); }; export { saveSettingsController }; diff --git a/src/controllers/api/saveVaultAutoContributeController.ts b/src/controllers/api/saveVaultAutoContributeController.ts new file mode 100644 index 00000000..5c5f51c5 --- /dev/null +++ b/src/controllers/api/saveVaultAutoContributeController.ts @@ -0,0 +1,25 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Guild } from "@/src/models/guildModel"; +import { hasGuildPermission } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const saveVaultAutoContributeController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId"); + const guild = (await Guild.findById(inventory.GuildId!, "Ranks AutoContributeFromVault"))!; + if (!(await hasGuildPermission(guild, accountId, GuildPermission.Treasurer))) { + res.status(400).send("Invalid permission").end(); + return; + } + const data = getJSONfromString(String(req.body)); + guild.AutoContributeFromVault = data.autoContributeFromVault; + await guild.save(); + res.end(); +}; + +interface ISetVaultAutoContributeRequest { + autoContributeFromVault: boolean; +} diff --git a/src/controllers/api/sellController.ts b/src/controllers/api/sellController.ts index bf3a346f..ad31c259 100644 --- a/src/controllers/api/sellController.ts +++ b/src/controllers/api/sellController.ts @@ -1,11 +1,54 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getInventory, addMods, addRecipes, addMiscItems, addConsumables } from "@/src/services/inventoryService"; +import { + getInventory, + addMods, + addRecipes, + addMiscItems, + addConsumables, + freeUpSlot +} from "@/src/services/inventoryService"; +import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; export const sellController: RequestHandler = async (req, res) => { const payload = JSON.parse(String(req.body)) as ISellRequest; const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId); + const requiredFields = new Set(); + if (payload.SellCurrency == "SC_RegularCredits") { + requiredFields.add("RegularCredits"); + } else if (payload.SellCurrency == "SC_FusionPoints") { + requiredFields.add("FusionPoints"); + } else { + requiredFields.add("MiscItems"); + } + for (const key of Object.keys(payload.Items)) { + requiredFields.add(key); + } + if (requiredFields.has("Upgrades")) { + requiredFields.add("RawUpgrades"); + } + if (payload.Items.Suits) { + requiredFields.add(InventorySlot.SUITS); + } + if (payload.Items.LongGuns || payload.Items.Pistols || payload.Items.Melee) { + requiredFields.add(InventorySlot.WEAPONS); + } + if (payload.Items.SpaceSuits) { + requiredFields.add(InventorySlot.SPACESUITS); + } + if (payload.Items.SpaceGuns || payload.Items.SpaceMelee) { + requiredFields.add(InventorySlot.SPACEWEAPONS); + } + if (payload.Items.Sentinels || payload.Items.SentinelWeapons) { + requiredFields.add(InventorySlot.SENTINELS); + } + if (payload.Items.OperatorAmps) { + requiredFields.add(InventorySlot.AMPS); + } + if (payload.Items.Hoverboards) { + requiredFields.add(InventorySlot.SPACESUITS); + } + const inventory = await getInventory(accountId, Array.from(requiredFields).join(" ")); // Give currency if (payload.SellCurrency == "SC_RegularCredits") { @@ -34,56 +77,72 @@ export const sellController: RequestHandler = async (req, res) => { if (payload.Items.Suits) { payload.Items.Suits.forEach(sellItem => { inventory.Suits.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.SUITS); }); } if (payload.Items.LongGuns) { payload.Items.LongGuns.forEach(sellItem => { inventory.LongGuns.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.WEAPONS); }); } if (payload.Items.Pistols) { payload.Items.Pistols.forEach(sellItem => { inventory.Pistols.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.WEAPONS); }); } if (payload.Items.Melee) { payload.Items.Melee.forEach(sellItem => { inventory.Melee.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.WEAPONS); }); } if (payload.Items.SpaceSuits) { payload.Items.SpaceSuits.forEach(sellItem => { inventory.SpaceSuits.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.SPACESUITS); }); } if (payload.Items.SpaceGuns) { payload.Items.SpaceGuns.forEach(sellItem => { inventory.SpaceGuns.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.SPACEWEAPONS); }); } if (payload.Items.SpaceMelee) { payload.Items.SpaceMelee.forEach(sellItem => { inventory.SpaceMelee.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.SPACEWEAPONS); }); } if (payload.Items.Sentinels) { payload.Items.Sentinels.forEach(sellItem => { inventory.Sentinels.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.SENTINELS); }); } if (payload.Items.SentinelWeapons) { payload.Items.SentinelWeapons.forEach(sellItem => { inventory.SentinelWeapons.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.SENTINELS); }); } if (payload.Items.OperatorAmps) { payload.Items.OperatorAmps.forEach(sellItem => { inventory.OperatorAmps.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.AMPS); }); } if (payload.Items.Hoverboards) { payload.Items.Hoverboards.forEach(sellItem => { inventory.Hoverboards.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.SPACESUITS); + }); + } + if (payload.Items.Drones) { + payload.Items.Drones.forEach(sellItem => { + inventory.Drones.pull({ _id: sellItem.String }); }); } if (payload.Items.Consumables) { @@ -152,6 +211,7 @@ interface ISellRequest { SentinelWeapons?: ISellItem[]; OperatorAmps?: ISellItem[]; Hoverboards?: ISellItem[]; + Drones?: ISellItem[]; }; SellPrice: number; SellCurrency: diff --git a/src/controllers/api/setAllianceGuildPermissionsController.ts b/src/controllers/api/setAllianceGuildPermissionsController.ts new file mode 100644 index 00000000..ce3caaf8 --- /dev/null +++ b/src/controllers/api/setAllianceGuildPermissionsController.ts @@ -0,0 +1,38 @@ +import { AllianceMember, GuildMember } from "@/src/models/guildModel"; +import { getAccountForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const setAllianceGuildPermissionsController: RequestHandler = async (req, res) => { + // Check requester is a warlord in their guild + const account = await getAccountForRequest(req); + const guildMember = (await GuildMember.findOne({ accountId: account._id, status: 0 }))!; + if (guildMember.rank > 1) { + res.status(400).end(); + return; + } + + // Check guild is the creator of the alliance and don't allow changing of own permissions. (Technically changing permissions requires the Promoter permission, but both are exclusive to the creator guild.) + const allianceMember = (await AllianceMember.findOne({ + guildId: guildMember.guildId, + Pending: false + }))!; + if ( + !(allianceMember.Permissions & GuildPermission.Ruler) || + allianceMember.guildId.equals(req.query.guildId as string) + ) { + res.status(400).end(); + return; + } + + const targetAllianceMember = (await AllianceMember.findOne({ + allianceId: allianceMember.allianceId, + guildId: req.query.guildId + }))!; + targetAllianceMember.Permissions = + parseInt(req.query.perms as string) & + (GuildPermission.Recruiter | GuildPermission.Treasurer | GuildPermission.ChatModerator); + await targetAllianceMember.save(); + + res.end(); +}; diff --git a/src/controllers/api/setDojoComponentMessageController.ts b/src/controllers/api/setDojoComponentMessageController.ts index 714dd7a5..92931a54 100644 --- a/src/controllers/api/setDojoComponentMessageController.ts +++ b/src/controllers/api/setDojoComponentMessageController.ts @@ -4,7 +4,7 @@ import { getDojoClient, getGuildForRequest } from "@/src/services/guildService"; export const setDojoComponentMessageController: RequestHandler = async (req, res) => { const guild = await getGuildForRequest(req); // At this point, we know that a member of the guild is making this request. Assuming they are allowed to change the message. - const component = guild.DojoComponents!.find(x => x._id.equals(req.query.componentId as string))!; + const component = guild.DojoComponents.id(req.query.componentId as string)!; const payload = JSON.parse(String(req.body)) as SetDojoComponentMessageRequest; if ("Name" in payload) { component.Name = payload.Name; @@ -12,7 +12,7 @@ export const setDojoComponentMessageController: RequestHandler = async (req, res component.Message = payload.Message; } await guild.save(); - res.json(getDojoClient(guild, 1)); + res.json(await getDojoClient(guild, 0, component._id)); }; type SetDojoComponentMessageRequest = { Name: string } | { Message: string }; diff --git a/src/controllers/api/setEquippedInstrumentController.ts b/src/controllers/api/setEquippedInstrumentController.ts index 5b4b9800..bb80a815 100644 --- a/src/controllers/api/setEquippedInstrumentController.ts +++ b/src/controllers/api/setEquippedInstrumentController.ts @@ -1,14 +1,21 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getInventory } from "@/src/services/inventoryService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; export const setEquippedInstrumentController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId); const body = getJSONfromString(String(req.body)); - inventory.EquippedInstrument = body.Instrument; - await inventory.save(); + + await Inventory.updateOne( + { + accountOwnerId: accountId + }, + { + EquippedInstrument: body.Instrument + } + ); + res.end(); }; diff --git a/src/controllers/api/setGuildMotdController.ts b/src/controllers/api/setGuildMotdController.ts new file mode 100644 index 00000000..8f1e28a7 --- /dev/null +++ b/src/controllers/api/setGuildMotdController.ts @@ -0,0 +1,59 @@ +import { Alliance, Guild, GuildMember } from "@/src/models/guildModel"; +import { hasGuildPermissionEx } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; +import { GuildPermission, ILongMOTD } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const setGuildMotdController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + const inventory = await getInventory(account._id.toString(), "GuildId"); + const guild = (await Guild.findById(inventory.GuildId!))!; + const member = (await GuildMember.findOne({ accountId: account._id, guildId: guild._id }))!; + + const IsLongMOTD = "longMOTD" in req.query; + const MOTD = req.body ? String(req.body) : undefined; + + if ("alliance" in req.query) { + if (member.rank > 1) { + res.status(400).json("Invalid permission"); + return; + } + + const alliance = (await Alliance.findById(guild.AllianceId!))!; + const motd = MOTD + ? ({ + message: MOTD, + authorName: getSuffixedName(account), + authorGuildName: guild.Name + } satisfies ILongMOTD) + : undefined; + if (IsLongMOTD) { + alliance.LongMOTD = motd; + } else { + alliance.MOTD = motd; + } + await alliance.save(); + } else { + if (!hasGuildPermissionEx(guild, member, GuildPermission.Herald)) { + res.status(400).json("Invalid permission"); + return; + } + + if (IsLongMOTD) { + if (MOTD) { + guild.LongMOTD = { + message: MOTD, + authorName: getSuffixedName(account) + }; + } else { + guild.LongMOTD = undefined; + } + } else { + guild.MOTD = MOTD ?? ""; + } + await guild.save(); + } + + res.json({ IsLongMOTD, MOTD }); +}; diff --git a/src/controllers/api/setSupportedSyndicateController.ts b/src/controllers/api/setSupportedSyndicateController.ts index e22b659f..40ce4af3 100644 --- a/src/controllers/api/setSupportedSyndicateController.ts +++ b/src/controllers/api/setSupportedSyndicateController.ts @@ -1,11 +1,18 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getInventory } from "@/src/services/inventoryService"; +import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; export const setSupportedSyndicateController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId); - inventory.SupportedSyndicate = req.query.syndicate as string; - await inventory.save(); + + await Inventory.updateOne( + { + accountOwnerId: accountId + }, + { + SupportedSyndicate: req.query.syndicate as string + } + ); + res.end(); }; diff --git a/src/controllers/api/setWeaponSkillTreeController.ts b/src/controllers/api/setWeaponSkillTreeController.ts index 98fe7652..a750c3ce 100644 --- a/src/controllers/api/setWeaponSkillTreeController.ts +++ b/src/controllers/api/setWeaponSkillTreeController.ts @@ -1,20 +1,25 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getInventory } from "@/src/services/inventoryService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { WeaponTypeInternal } from "@/src/services/itemDataService"; +import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; +import { equipmentKeys, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; export const setWeaponSkillTreeController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId); const payload = getJSONfromString(String(req.body)); - const item = inventory[req.query.Category as WeaponTypeInternal].find( - item => item._id.toString() == (req.query.ItemId as string) - )!; - item.SkillTree = payload.SkillTree; + if (equipmentKeys.indexOf(req.query.Category as TEquipmentKey) != -1) { + await Inventory.updateOne( + { + accountOwnerId: accountId, + [`${req.query.Category as string}._id`]: req.query.ItemId as string + }, + { + [`${req.query.Category as string}.$.SkillTree`]: payload.SkillTree + } + ); + } - await inventory.save(); res.end(); }; diff --git a/src/controllers/api/startCollectibleEntryController.ts b/src/controllers/api/startCollectibleEntryController.ts new file mode 100644 index 00000000..ffc440c1 --- /dev/null +++ b/src/controllers/api/startCollectibleEntryController.ts @@ -0,0 +1,27 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IIncentiveState } from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; + +export const startCollectibleEntryController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const request = getJSONfromString(String(req.body)); + inventory.CollectibleSeries ??= []; + inventory.CollectibleSeries.push({ + CollectibleType: request.target, + Count: 0, + Tracking: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ReqScans: request.reqScans, + IncentiveStates: request.other + }); + await inventory.save(); + res.status(200).end(); +}; + +interface IStartCollectibleEntryRequest { + target: string; + reqScans: number; + other: IIncentiveState[]; +} diff --git a/src/controllers/api/startDojoRecipeController.ts b/src/controllers/api/startDojoRecipeController.ts index 6fd2b5a9..d2865165 100644 --- a/src/controllers/api/startDojoRecipeController.ts +++ b/src/controllers/api/startDojoRecipeController.ts @@ -1,8 +1,18 @@ import { RequestHandler } from "express"; -import { IDojoComponentClient } from "@/src/types/guildTypes"; -import { getDojoClient, getGuildForRequest } from "@/src/services/guildService"; +import { GuildPermission, IDojoComponentClient } from "@/src/types/guildTypes"; +import { + getDojoClient, + getGuildForRequestEx, + hasAccessToDojo, + hasGuildPermission, + processDojoBuildMaterialsGathered, + setDojoRoomLogFunded +} from "@/src/services/guildService"; import { Types } from "mongoose"; import { ExportDojoRecipes } from "warframe-public-export-plus"; +import { config } from "@/src/services/configService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getInventory } from "@/src/services/inventoryService"; interface IStartDojoRecipeRequest { PlacedComponent: IDojoComponentClient; @@ -10,8 +20,13 @@ interface IStartDojoRecipeRequest { } export const startDojoRecipeController: RequestHandler = async (req, res) => { - const guild = await getGuildForRequest(req); - // At this point, we know that a member of the guild is making this request. Assuming they are allowed to start a build. + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId LevelKeys"); + const guild = await getGuildForRequestEx(req, inventory); + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Architect))) { + res.json({ DojoRequestStatus: -1 }); + return; + } const request = JSON.parse(String(req.body)) as IStartDojoRecipeRequest; const room = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == request.PlacedComponent.pf); @@ -20,15 +35,34 @@ export const startDojoRecipeController: RequestHandler = async (req, res) => { guild.DojoEnergy += room.energy; } - guild.DojoComponents!.push({ - _id: new Types.ObjectId(), - pf: request.PlacedComponent.pf, - ppf: request.PlacedComponent.ppf, - pi: new Types.ObjectId(request.PlacedComponent.pi!.$oid), - op: request.PlacedComponent.op, - pp: request.PlacedComponent.pp, - CompletionTime: new Date(Date.now()) // TOOD: Omit this field & handle the "Collecting Materials" state. + const componentId = new Types.ObjectId(); + + guild.RoomChanges ??= []; + guild.RoomChanges.push({ + entryType: 2, + details: request.PlacedComponent.pf, + componentId: componentId }); + + const component = + guild.DojoComponents[ + guild.DojoComponents.push({ + _id: componentId, + pf: request.PlacedComponent.pf, + ppf: request.PlacedComponent.ppf, + pi: new Types.ObjectId(request.PlacedComponent.pi!.$oid), + op: request.PlacedComponent.op, + pp: request.PlacedComponent.pp, + DecoCapacity: room?.decoCapacity + }) - 1 + ]; + if (config.noDojoRoomBuildStage) { + component.CompletionTime = new Date(Date.now()); + if (room) { + processDojoBuildMaterialsGathered(guild, room); + } + setDojoRoomLogFunded(guild, component); + } await guild.save(); - res.json(getDojoClient(guild, 0)); + res.json(await getDojoClient(guild, 0)); }; diff --git a/src/controllers/api/startLibraryDailyTaskController.ts b/src/controllers/api/startLibraryDailyTaskController.ts new file mode 100644 index 00000000..e8b8425b --- /dev/null +++ b/src/controllers/api/startLibraryDailyTaskController.ts @@ -0,0 +1,11 @@ +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const startLibraryDailyTaskController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + inventory.LibraryActiveDailyTaskInfo = inventory.LibraryAvailableDailyTaskInfo; + await inventory.save(); + res.json(inventory.LibraryAvailableDailyTaskInfo); +}; diff --git a/src/controllers/api/startLibraryPersonalTargetController.ts b/src/controllers/api/startLibraryPersonalTargetController.ts index 388dc897..7bfa5ff6 100644 --- a/src/controllers/api/startLibraryPersonalTargetController.ts +++ b/src/controllers/api/startLibraryPersonalTargetController.ts @@ -8,7 +8,7 @@ export const startLibraryPersonalTargetController: RequestHandler = async (req, inventory.LibraryPersonalTarget = req.query.target as string; await inventory.save(); res.json({ - IsQuest: false, + IsQuest: req.query.target == "/Lotus/Types/Game/Library/Targets/DragonframeQuestTarget", Target: req.query.target }); }; diff --git a/src/controllers/api/startRecipeController.ts b/src/controllers/api/startRecipeController.ts index 642d0cfc..495b8d26 100644 --- a/src/controllers/api/startRecipeController.ts +++ b/src/controllers/api/startRecipeController.ts @@ -3,10 +3,12 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; import { getRecipe } from "@/src/services/itemDataService"; -import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { addItem, freeUpSlot, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { unixTimesInMs } from "@/src/constants/timeConstants"; import { Types } from "mongoose"; -import { ISpectreLoadout } from "@/src/types/inventoryTypes/inventoryTypes"; +import { InventorySlot, ISpectreLoadout } from "@/src/types/inventoryTypes/inventoryTypes"; +import { toOid } from "@/src/helpers/inventoryHelpers"; +import { ExportWeapons } from "warframe-public-export-plus"; interface IStartRecipeRequest { RecipeName: string; @@ -26,23 +28,36 @@ export const startRecipeController: RequestHandler = async (req, res) => { throw new Error(`unknown recipe ${recipeName}`); } - const ingredientsInverse = recipe.ingredients.map(component => ({ - ItemType: component.ItemType, - ItemCount: component.ItemCount * -1 - })); - const inventory = await getInventory(accountId); updateCurrency(inventory, recipe.buildPrice, false); - addMiscItems(inventory, ingredientsInverse); - //buildtime is in seconds - const completionDate = new Date(Date.now() + recipe.buildTime * unixTimesInMs.second); + const pr = + inventory.PendingRecipes[ + inventory.PendingRecipes.push({ + ItemType: recipeName, + CompletionDate: new Date(Date.now() + recipe.buildTime * unixTimesInMs.second), + _id: new Types.ObjectId() + }) - 1 + ]; - inventory.PendingRecipes.push({ - ItemType: recipeName, - CompletionDate: completionDate, - _id: new Types.ObjectId() - }); + for (let i = 0; i != recipe.ingredients.length; ++i) { + if (startRecipeRequest.Ids[i]) { + 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); + } else { + await addItem(inventory, recipe.ingredients[i].ItemType, recipe.ingredients[i].ItemCount * -1); + } + } if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") { const spectreLoadout: ISpectreLoadout = { @@ -75,7 +90,6 @@ export const startRecipeController: RequestHandler = async (req, res) => { spectreLoadout.LongGuns = item.ItemType; spectreLoadout.LongGunsModularParts = item.ModularParts; } else { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition console.assert(type == "/Lotus/Types/Game/LotusMeleeWeapon"); const item = inventory.Melee.id(oid)!; spectreLoadout.Melee = item.ItemType; @@ -96,11 +110,11 @@ export const startRecipeController: RequestHandler = async (req, res) => { inventory.PendingSpectreLoadouts.push(spectreLoadout); logger.debug("pending spectre loadout", spectreLoadout); } + } else if (recipe.secretIngredientAction == "SIA_UNBRAND") { + pr.SuitToUnbrand = new Types.ObjectId(startRecipeRequest.Ids[recipe.ingredients.length + 0]); } - const newInventory = await inventory.save(); + await inventory.save(); - res.json({ - RecipeId: { $oid: newInventory.PendingRecipes[newInventory.PendingRecipes.length - 1]._id.toString() } - }); + res.json({ RecipeId: toOid(pr._id) }); }; diff --git a/src/controllers/api/syndicateSacrificeController.ts b/src/controllers/api/syndicateSacrificeController.ts index 77d2424a..b20df3bf 100644 --- a/src/controllers/api/syndicateSacrificeController.ts +++ b/src/controllers/api/syndicateSacrificeController.ts @@ -1,10 +1,17 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus"; +import { ExportNightwave, ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; -import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { + addItem, + addMiscItems, + combineInventoryChanges, + getInventory, + updateCurrency +} from "@/src/services/inventoryService"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { fromStoreItem, isStoreItem } from "@/src/services/itemDataService"; export const syndicateSacrificeController: RequestHandler = async (request, response) => { const accountId = await getAccountIdForRequest(request); @@ -22,7 +29,7 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp InventoryChanges: {}, Level: data.SacrificeLevel, LevelIncrease: level <= 0 ? 1 : level, - NewEpisodeReward: syndicate.Tag == "RadioLegionIntermission9Syndicate" + NewEpisodeReward: false }; const manifest = ExportSyndicates[data.AffiliationTag]; @@ -64,6 +71,19 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp ); } + if (data.AffiliationTag == ExportNightwave.affiliationTag) { + const index = syndicate.Title - 1; + if (index < ExportNightwave.rewards.length) { + res.NewEpisodeReward = true; + const reward = ExportNightwave.rewards[index]; + let rewardType = reward.uniqueName; + if (isStoreItem(rewardType)) { + rewardType = fromStoreItem(rewardType); + } + combineInventoryChanges(res.InventoryChanges, await addItem(inventory, rewardType, reward.itemCount ?? 1)); + } + } + await inventory.save(); response.json(res); diff --git a/src/controllers/api/syndicateStandingBonusController.ts b/src/controllers/api/syndicateStandingBonusController.ts index 6899ee3e..4067e9db 100644 --- a/src/controllers/api/syndicateStandingBonusController.ts +++ b/src/controllers/api/syndicateStandingBonusController.ts @@ -1,10 +1,19 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { addMiscItems, getInventory, getStandingLimit, updateStandingLimit } from "@/src/services/inventoryService"; -import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; +import { + addMiscItems, + freeUpSlot, + getInventory, + getStandingLimit, + updateStandingLimit +} from "@/src/services/inventoryService"; +import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { IOid } from "@/src/types/commonTypes"; -import { ExportSyndicates } from "warframe-public-export-plus"; +import { ExportSyndicates, ExportWeapons } from "warframe-public-export-plus"; import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper"; +import { logger } from "@/src/utils/logger"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes"; export const syndicateStandingBonusController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -12,6 +21,7 @@ export const syndicateStandingBonusController: RequestHandler = async (req, res) const syndicateMeta = ExportSyndicates[request.Operation.AffiliationTag]; + // Process items let gainedStanding = 0; request.Operation.Items.forEach(item => { const medallion = (syndicateMeta.medallions ?? []).find(medallion => medallion.itemType == item.ItemType); @@ -21,9 +31,35 @@ export const syndicateStandingBonusController: RequestHandler = async (req, res) item.ItemCount *= -1; }); - const inventory = await getInventory(accountId); addMiscItems(inventory, request.Operation.Items); + const inventoryChanges: IInventoryChanges = {}; + inventoryChanges.MiscItems = request.Operation.Items; + + // Process modular weapon + if (request.Operation.ModularWeaponId.$oid != "000000000000000000000000") { + const category = req.query.Category as "LongGuns" | "Pistols" | "Melee" | "OperatorAmps"; + const weapon = inventory[category].id(request.Operation.ModularWeaponId.$oid)!; + if (gainedStanding !== 0) { + throw new Error(`modular weapon standing bonus should be mutually exclusive`); + } + weapon.ModularParts!.forEach(part => { + const partStandingBonus = ExportWeapons[part].donationStandingBonus; + if (partStandingBonus === undefined) { + throw new Error(`no standing bonus for ${part}`); + } + logger.debug(`modular weapon part ${part} gives ${partStandingBonus} standing`); + gainedStanding += partStandingBonus; + }); + if (weapon.Features && (weapon.Features & EquipmentFeatures.GILDED) != 0) { + gainedStanding *= 2; + } + inventoryChanges.RemovedIdItems = [{ ItemId: request.Operation.ModularWeaponId }]; + inventory[category].pull({ _id: request.Operation.ModularWeaponId.$oid }); + const slotBin = category == "OperatorAmps" ? InventorySlot.AMPS : InventorySlot.WEAPONS; + freeUpSlot(inventory, slotBin); + inventoryChanges[slotBin] = { count: -1, platinum: 0, Slots: 1 }; + } let syndicate = inventory.Affiliations.find(x => x.Tag == request.Operation.AffiliationTag); if (!syndicate) { @@ -50,9 +86,7 @@ export const syndicateStandingBonusController: RequestHandler = async (req, res) await inventory.save(); res.json({ - InventoryChanges: { - MiscItems: request.Operation.Items - }, + InventoryChanges: inventoryChanges, AffiliationMods: [ { Tag: request.Operation.AffiliationTag, @@ -67,6 +101,6 @@ interface ISyndicateStandingBonusRequest { AffiliationTag: string; AlternateBonusReward: ""; // ??? Items: IMiscItem[]; + ModularWeaponId: IOid; }; - ModularWeaponId: IOid; // Seems to just be "000000000000000000000000", also note there's a "Category" query field } diff --git a/src/controllers/api/tradingController.ts b/src/controllers/api/tradingController.ts new file mode 100644 index 00000000..af6e94f9 --- /dev/null +++ b/src/controllers/api/tradingController.ts @@ -0,0 +1,23 @@ +import { getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const tradingController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId LevelKeys"); + const guild = await getGuildForRequestEx(req, inventory); + const op = req.query.op as string; + if (op == "5") { + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Treasurer))) { + res.status(400).send("-1").end(); + return; + } + guild.TradeTax = parseInt(req.query.tax as string); + await guild.save(); + res.send(guild.TradeTax).end(); + } else { + throw new Error(`unknown trading op: ${op}`); + } +}; diff --git a/src/controllers/api/trainingResultController.ts b/src/controllers/api/trainingResultController.ts index 022d6c10..f44d7dae 100644 --- a/src/controllers/api/trainingResultController.ts +++ b/src/controllers/api/trainingResultController.ts @@ -5,6 +5,8 @@ import { IMongoDate } from "@/src/types/commonTypes"; import { RequestHandler } from "express"; import { unixTimesInMs } from "@/src/constants/timeConstants"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { createMessage } from "@/src/services/inboxService"; +import { config } from "@/src/services/configService"; interface ITrainingResultsRequest { numLevelsGained: number; @@ -21,11 +23,36 @@ const trainingResultController: RequestHandler = async (req, res): Promise const trainingResults = getJSONfromString(String(req.body)); - const inventory = await getInventory(accountId); + const inventory = await getInventory(accountId, "TrainingDate PlayerLevel TradesRemaining"); if (trainingResults.numLevelsGained == 1) { - inventory.TrainingDate = new Date(Date.now() + unixTimesInMs.hour * 23); + let time = Date.now(); + if (!config.noMasteryRankUpCooldown) { + time += unixTimesInMs.hour * 23; + } + inventory.TrainingDate = new Date(time); + inventory.PlayerLevel += 1; + inventory.TradesRemaining += 1; + + await createMessage(accountId, [ + { + sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender", + msg: "/Lotus/Language/Inbox/MasteryRewardMsg", + arg: [ + { + Key: "NEW_RANK", + Tag: inventory.PlayerLevel + } + ], + att: [ + `/Lotus/Types/Items/ShipDecos/MasteryTrophies/Rank${inventory.PlayerLevel.toString().padStart(2, "0")}Trophy` + ], + sub: "/Lotus/Language/Inbox/MasteryRewardTitle", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + highPriority: true + } + ]); } const changedinventory = await inventory.save(); diff --git a/src/controllers/api/unlockShipFeatureController.ts b/src/controllers/api/unlockShipFeatureController.ts index cdccc2ec..4a3ecd1e 100644 --- a/src/controllers/api/unlockShipFeatureController.ts +++ b/src/controllers/api/unlockShipFeatureController.ts @@ -3,7 +3,6 @@ import { updateShipFeature } from "@/src/services/personalRoomsService"; import { IUnlockShipFeatureRequest } from "@/src/types/requestTypes"; import { parseString } from "@/src/helpers/general"; -// eslint-disable-next-line @typescript-eslint/no-misused-promises export const unlockShipFeatureController: RequestHandler = async (req, res) => { const accountId = parseString(req.query.accountId); const shipFeatureRequest = JSON.parse((req.body as string).toString()) as IUnlockShipFeatureRequest; diff --git a/src/controllers/api/updateAlignmentController.ts b/src/controllers/api/updateAlignmentController.ts new file mode 100644 index 00000000..2942ad6f --- /dev/null +++ b/src/controllers/api/updateAlignmentController.ts @@ -0,0 +1,25 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IAlignment } from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; + +export const updateAlignmentController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const body = getJSONfromString(String(req.body)); + inventory.Alignment = { + Alignment: body.Alignment, + Wisdom: body.Wisdom + }; + await inventory.save(); + res.json(inventory.Alignment); +}; + +interface IUpdateAlignmentRequest { + Wisdom: number; + Alignment: number; + PreviousAlignment: IAlignment; + AlignmentAction: string; // e.g. "/Lotus/Language/Game/MawCinematicDualChoice" + KeyChainName: string; +} diff --git a/src/controllers/api/updateChallengeProgressController.ts b/src/controllers/api/updateChallengeProgressController.ts index 2889a333..3e056538 100644 --- a/src/controllers/api/updateChallengeProgressController.ts +++ b/src/controllers/api/updateChallengeProgressController.ts @@ -1,16 +1,62 @@ import { RequestHandler } from "express"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { updateChallengeProgress } from "@/src/services/inventoryService"; -import { IUpdateChallengeProgressRequest } from "@/src/types/requestTypes"; +import { addChallenges, addSeasonalChallengeHistory, getInventory } from "@/src/services/inventoryService"; +import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes"; +import { ExportNightwave } from "warframe-public-export-plus"; +import { logger } from "@/src/utils/logger"; +import { IAffiliationMods } from "@/src/types/purchaseTypes"; -const updateChallengeProgressController: RequestHandler = async (req, res) => { - const payload = getJSONfromString(String(req.body)); +export const updateChallengeProgressController: RequestHandler = async (req, res) => { + const challenges = getJSONfromString(String(req.body)); const accountId = await getAccountIdForRequest(req); - await updateChallengeProgress(payload, accountId); + const inventory = await getInventory(accountId, "ChallengeProgress SeasonChallengeHistory Affiliations"); + if (challenges.ChallengeProgress) { + addChallenges(inventory, challenges.ChallengeProgress); + } + if (challenges.SeasonChallengeHistory) { + addSeasonalChallengeHistory(inventory, challenges.SeasonChallengeHistory); + } + const affiliationMods: IAffiliationMods[] = []; + if (challenges.ChallengeProgress && challenges.SeasonChallengeCompletions) { + for (const challenge of challenges.SeasonChallengeCompletions) { + // Ignore challenges that weren't completed just now + if (!challenges.ChallengeProgress.find(x => challenge.challenge.indexOf(x.Name) != -1)) { + continue; + } - res.status(200).end(); + const meta = ExportNightwave.challenges[challenge.challenge]; + logger.debug("Completed challenge", meta); + + let affiliation = inventory.Affiliations.find(x => x.Tag == ExportNightwave.affiliationTag); + if (!affiliation) { + affiliation = + inventory.Affiliations[ + inventory.Affiliations.push({ + 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; + } + } + await inventory.save(); + + res.json({ + AffiliationMods: affiliationMods + }); }; -export { updateChallengeProgressController }; +interface IUpdateChallengeProgressRequest { + ChallengeProgress?: IChallengeProgress[]; + SeasonChallengeHistory?: ISeasonChallenge[]; + SeasonChallengeCompletions?: ISeasonChallenge[]; +} diff --git a/src/controllers/api/updateQuestController.ts b/src/controllers/api/updateQuestController.ts index 3a91ada0..c251094f 100644 --- a/src/controllers/api/updateQuestController.ts +++ b/src/controllers/api/updateQuestController.ts @@ -1,13 +1,10 @@ import { RequestHandler } from "express"; import { parseString } from "@/src/helpers/general"; -import { logger } from "@/src/utils/logger"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { updateQuestKey, IUpdateQuestRequest } from "@/src/services/questService"; -import { getQuestCompletionItems } from "@/src/services/itemDataService"; -import { addItems, getInventory } from "@/src/services/inventoryService"; +import { getInventory } from "@/src/services/inventoryService"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; -// eslint-disable-next-line @typescript-eslint/no-misused-promises export const updateQuestController: RequestHandler = async (req, res) => { const accountId = parseString(req.query.accountId); const updateQuestRequest = getJSONfromString((req.body as string).toString()); @@ -22,20 +19,7 @@ export const updateQuestController: RequestHandler = async (req, res) => { const updateQuestResponse: { CustomData?: string; InventoryChanges?: IInventoryChanges; MissionRewards: [] } = { MissionRewards: [] }; - updateQuestKey(inventory, updateQuestRequest.QuestKeys); - - if (updateQuestRequest.QuestKeys[0].Completed) { - logger.debug(`completed quest ${updateQuestRequest.QuestKeys[0].ItemType} `); - const questKeyName = updateQuestRequest.QuestKeys[0].ItemType; - const questCompletionItems = getQuestCompletionItems(questKeyName); - logger.debug(`quest completion items`, questCompletionItems); - - if (questCompletionItems) { - const inventoryChanges = await addItems(inventory, questCompletionItems); - updateQuestResponse.InventoryChanges = inventoryChanges; - } - inventory.ActiveQuest = ""; - } + updateQuestResponse.InventoryChanges = await updateQuestKey(inventory, updateQuestRequest.QuestKeys); //TODO: might need to parse the custom data and add the associated items to inventory if (updateQuestRequest.QuestKeys[0].CustomData) { diff --git a/src/controllers/api/updateSongChallengeController.ts b/src/controllers/api/updateSongChallengeController.ts new file mode 100644 index 00000000..e0a10fc8 --- /dev/null +++ b/src/controllers/api/updateSongChallengeController.ts @@ -0,0 +1,50 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { addShipDecorations, getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { RequestHandler } from "express"; + +export const updateSongChallengeController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const request = getJSONfromString(String(req.body)); + inventory.SongChallenges ??= []; + let songChallenge = inventory.SongChallenges.find(x => x.Song == request.Song); + if (!songChallenge) { + songChallenge = + inventory.SongChallenges[inventory.SongChallenges.push({ Song: request.Song, Difficulties: [] }) - 1]; + } + songChallenge.Difficulties.push(request.Difficulty); + + const response: IUpdateSongChallengeResponse = { + Song: request.Song, + Difficulty: request.Difficulty + }; + + // Handle all songs being completed on all difficulties + if (inventory.SongChallenges.length == 12 && !inventory.SongChallenges.find(x => x.Difficulties.length != 2)) { + response.Reward = "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropShawzinDuviri"; + const shipDecorationChanges = [ + { ItemType: "/Lotus/Types/Items/ShipDecos/LisetPropShawzinDuviri", ItemCount: 1 } + ]; + response.InventoryChanges = { + ShipDecorations: shipDecorationChanges + }; + addShipDecorations(inventory, shipDecorationChanges); + } + + await inventory.save(); + res.json(response); +}; + +interface IUpdateSongChallengeRequest { + Song: string; + Difficulty: number; +} + +interface IUpdateSongChallengeResponse { + Song: string; + Difficulty: number; + Reward?: string; + InventoryChanges?: IInventoryChanges; +} diff --git a/src/controllers/api/updateThemeController.ts b/src/controllers/api/updateThemeController.ts index ccb4ab57..ce31d27d 100644 --- a/src/controllers/api/updateThemeController.ts +++ b/src/controllers/api/updateThemeController.ts @@ -1,25 +1,23 @@ import { getAccountIdForRequest } from "@/src/services/loginService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { updateTheme } from "@/src/services/inventoryService"; -import { IThemeUpdateRequest } from "@/src/types/requestTypes"; import { RequestHandler } from "express"; +import { getInventory } from "@/src/services/inventoryService"; -const updateThemeController: RequestHandler = async (request, response) => { +export const updateThemeController: RequestHandler = async (request, response) => { const accountId = await getAccountIdForRequest(request); - const body = String(request.body); + const data = getJSONfromString(String(request.body)); - try { - const json = getJSONfromString(body); - if (typeof json !== "object") { - throw new Error("Invalid data format"); - } - - await updateTheme(json, accountId); - } catch (err) { - console.error("Error parsing JSON data:", err); - } + const inventory = await getInventory(accountId, "ThemeStyle ThemeBackground ThemeSounds"); + if (data.Style) inventory.ThemeStyle = data.Style; + if (data.Background) inventory.ThemeBackground = data.Background; + if (data.Sounds) inventory.ThemeSounds = data.Sounds; + await inventory.save(); response.json({}); }; -export { updateThemeController }; +interface IThemeUpdateRequest { + Style?: string; + Background?: string; + Sounds?: string; +} diff --git a/src/controllers/api/upgradesController.ts b/src/controllers/api/upgradesController.ts index c29e20fa..855d2aa7 100644 --- a/src/controllers/api/upgradesController.ts +++ b/src/controllers/api/upgradesController.ts @@ -11,7 +11,7 @@ import { getAccountIdForRequest } from "@/src/services/loginService"; import { addMiscItems, addRecipes, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getRecipeByResult } from "@/src/services/itemDataService"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; -import { addInfestedFoundryXP } from "./infestedFoundryController"; +import { addInfestedFoundryXP, applyCheatsToInfestedFoundry } from "@/src/services/infestedFoundryService"; import { config } from "@/src/services/configService"; export const upgradesController: RequestHandler = async (req, res) => { @@ -25,7 +25,7 @@ export const upgradesController: RequestHandler = async (req, res) => { operation.UpgradeRequirement == "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker" ) { updateCurrency(inventory, 10, true); - } else { + } else if (operation.OperationType != "UOT_ABILITY_OVERRIDE") { addMiscItems(inventory, [ { ItemType: operation.UpgradeRequirement, @@ -66,95 +66,78 @@ export const upgradesController: RequestHandler = async (req, res) => { inventoryChanges.Recipes = recipeChanges; inventoryChanges.InfestedFoundry = inventory.toJSON().InfestedFoundry; + applyCheatsToInfestedFoundry(inventoryChanges.InfestedFoundry!); } else switch (operation.UpgradeRequirement) { case "/Lotus/Types/Items/MiscItems/OrokinReactor": - case "/Lotus/Types/Items/MiscItems/OrokinCatalyst": - for (const item of inventory[payload.ItemCategory]) { - if (item._id.toString() == payload.ItemId.$oid) { - item.Features ??= 0; - item.Features |= EquipmentFeatures.DOUBLE_CAPACITY; - break; - } - } + case "/Lotus/Types/Items/MiscItems/OrokinCatalyst": { + const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!; + item.Features ??= 0; + item.Features |= EquipmentFeatures.DOUBLE_CAPACITY; break; + } case "/Lotus/Types/Items/MiscItems/UtilityUnlocker": - case "/Lotus/Types/Items/MiscItems/WeaponUtilityUnlocker": - for (const item of inventory[payload.ItemCategory]) { - if (item._id.toString() == payload.ItemId.$oid) { - item.Features ??= 0; - item.Features |= EquipmentFeatures.UTILITY_SLOT; - break; - } - } + case "/Lotus/Types/Items/MiscItems/WeaponUtilityUnlocker": { + const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!; + item.Features ??= 0; + item.Features |= EquipmentFeatures.UTILITY_SLOT; break; - case "/Lotus/Types/Items/MiscItems/HeavyWeaponCatalyst": + } + case "/Lotus/Types/Items/MiscItems/HeavyWeaponCatalyst": { console.assert(payload.ItemCategory == "SpaceGuns"); - for (const item of inventory[payload.ItemCategory]) { - if (item._id.toString() == payload.ItemId.$oid) { - item.Features ??= 0; - item.Features |= EquipmentFeatures.GRAVIMAG_INSTALLED; - break; - } - } + const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!; + item.Features ??= 0; + item.Features |= EquipmentFeatures.GRAVIMAG_INSTALLED; break; + } case "/Lotus/Types/Items/MiscItems/WeaponPrimaryArcaneUnlocker": case "/Lotus/Types/Items/MiscItems/WeaponSecondaryArcaneUnlocker": case "/Lotus/Types/Items/MiscItems/WeaponMeleeArcaneUnlocker": - case "/Lotus/Types/Items/MiscItems/WeaponAmpArcaneUnlocker": - for (const item of inventory[payload.ItemCategory]) { - if (item._id.toString() == payload.ItemId.$oid) { - item.Features ??= 0; - item.Features |= EquipmentFeatures.ARCANE_SLOT; - break; - } - } + case "/Lotus/Types/Items/MiscItems/WeaponAmpArcaneUnlocker": { + const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!; + item.Features ??= 0; + item.Features |= EquipmentFeatures.ARCANE_SLOT; break; + } + case "/Lotus/Types/Items/MiscItems/ValenceAdapter": { + const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!; + item.Features ??= 0; + item.Features |= EquipmentFeatures.VALENCE_SWAP; + break; + } case "/Lotus/Types/Items/MiscItems/Forma": case "/Lotus/Types/Items/MiscItems/FormaUmbra": case "/Lotus/Types/Items/MiscItems/FormaAura": - case "/Lotus/Types/Items/MiscItems/FormaStance": - for (const item of inventory[payload.ItemCategory]) { - if (item._id.toString() == payload.ItemId.$oid) { - item.XP = 0; - setSlotPolarity(item, operation.PolarizeSlot, operation.PolarizeValue); - item.Polarized ??= 0; - item.Polarized += 1; - break; - } - } + case "/Lotus/Types/Items/MiscItems/FormaStance": { + const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!; + item.XP = 0; + setSlotPolarity(item, operation.PolarizeSlot, operation.PolarizeValue); + item.Polarized ??= 0; + item.Polarized += 1; break; - case "/Lotus/Types/Items/MiscItems/ModSlotUnlocker": - for (const item of inventory[payload.ItemCategory]) { - if (item._id.toString() == payload.ItemId.$oid) { - item.ModSlotPurchases ??= 0; - item.ModSlotPurchases += 1; - break; - } - } + } + case "/Lotus/Types/Items/MiscItems/ModSlotUnlocker": { + const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!; + item.ModSlotPurchases ??= 0; + item.ModSlotPurchases += 1; break; - case "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker": - for (const item of inventory[payload.ItemCategory]) { - if (item._id.toString() == payload.ItemId.$oid) { - item.CustomizationSlotPurchases ??= 0; - item.CustomizationSlotPurchases += 1; - break; - } - } + } + case "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker": { + const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!; + item.CustomizationSlotPurchases ??= 0; + item.CustomizationSlotPurchases += 1; break; - case "": + } + case "": { console.assert(operation.OperationType == "UOT_SWAP_POLARITY"); - for (const item of inventory[payload.ItemCategory]) { - if (item._id.toString() == payload.ItemId.$oid) { - for (let i = 0; i != operation.PolarityRemap.length; ++i) { - if (operation.PolarityRemap[i].Slot != i) { - setSlotPolarity(item, i, operation.PolarityRemap[i].Value); - } - } - break; + const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!; + for (let i = 0; i != operation.PolarityRemap.length; ++i) { + if (operation.PolarityRemap[i].Slot != i) { + setSlotPolarity(item, i, operation.PolarityRemap[i].Value); } } break; + } default: throw new Error("Unsupported upgrade: " + operation.UpgradeRequirement); } diff --git a/src/controllers/api/valenceSwapController.ts b/src/controllers/api/valenceSwapController.ts new file mode 100644 index 00000000..0c3976b0 --- /dev/null +++ b/src/controllers/api/valenceSwapController.ts @@ -0,0 +1,29 @@ +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IOid } from "@/src/types/commonTypes"; +import { IInnateDamageFingerprint, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; + +export const valenceSwapController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const body = JSON.parse(String(req.body)) as IValenceSwapRequest; + const inventory = await getInventory(accountId, body.WeaponCategory); + const weapon = inventory[body.WeaponCategory].id(body.WeaponId.$oid)!; + + const upgradeFingerprint = JSON.parse(weapon.UpgradeFingerprint!) as IInnateDamageFingerprint; + upgradeFingerprint.buffs[0].Tag = body.NewValenceUpgradeTag; + weapon.UpgradeFingerprint = JSON.stringify(upgradeFingerprint); + + await inventory.save(); + res.json({ + InventoryChanges: { + [body.WeaponCategory]: [weapon.toJSON()] + } + }); +}; + +interface IValenceSwapRequest { + WeaponId: IOid; + WeaponCategory: TEquipmentKey; + NewValenceUpgradeTag: string; +} diff --git a/src/controllers/api/wishlistController.ts b/src/controllers/api/wishlistController.ts new file mode 100644 index 00000000..cfef2329 --- /dev/null +++ b/src/controllers/api/wishlistController.ts @@ -0,0 +1,24 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const wishlistController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "Wishlist"); + const body = getJSONfromString(String(req.body)); + for (const item of body.WishlistItems) { + const i = inventory.Wishlist.findIndex(x => x == item); + if (i == -1) { + inventory.Wishlist.push(item); + } else { + inventory.Wishlist.splice(i, 1); + } + } + await inventory.save(); + res.end(); +}; + +interface IWishlistRequest { + WishlistItems: string[]; +} diff --git a/src/controllers/custom/addItemsController.ts b/src/controllers/custom/addItemsController.ts index 1eb50ed6..15837602 100644 --- a/src/controllers/custom/addItemsController.ts +++ b/src/controllers/custom/addItemsController.ts @@ -7,7 +7,7 @@ export const addItemsController: RequestHandler = async (req, res) => { const requests = req.body as IAddItemRequest[]; const inventory = await getInventory(accountId); for (const request of requests) { - await addItem(inventory, request.ItemType, request.ItemCount); + await addItem(inventory, request.ItemType, request.ItemCount, true); } await inventory.save(); res.end(); diff --git a/src/controllers/custom/addModularEquipmentController.ts b/src/controllers/custom/addModularEquipmentController.ts new file mode 100644 index 00000000..984acd75 --- /dev/null +++ b/src/controllers/custom/addModularEquipmentController.ts @@ -0,0 +1,98 @@ +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { + getInventory, + addEquipment, + occupySlot, + productCategoryToInventoryBin, + applyDefaultUpgrades +} from "@/src/services/inventoryService"; +import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper"; +import { getDefaultUpgrades } from "@/src/services/itemDataService"; +import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { ExportWeapons } from "warframe-public-export-plus"; +import { RequestHandler } from "express"; + +export const addModularEquipmentController: RequestHandler = async (req, res) => { + const requiredFields = new Set(); + const accountId = await getAccountIdForRequest(req); + const request = req.body as IAddModularEquipmentRequest; + const category = modularWeaponTypes[request.ItemType]; + const inventoryBin = productCategoryToInventoryBin(category)!; + requiredFields.add(category); + requiredFields.add(inventoryBin); + + request.ModularParts.forEach(part => { + if (ExportWeapons[part].gunType) { + if (category == "LongGuns") { + request.ItemType = { + GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", + GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun", + GT_BEAM: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam" + }[ExportWeapons[part].gunType]; + } else { + request.ItemType = { + GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary", + GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun", + GT_BEAM: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam" + }[ExportWeapons[part].gunType]; + } + } else if (request.ItemType == "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetPowerSuit") { + if (part.includes("ZanukaPetPartHead")) { + request.ItemType = { + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA": + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB": + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC": + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit" + }[part]!; + } + } + }); + const defaultUpgrades = getDefaultUpgrades(request.ModularParts); + if (defaultUpgrades) { + requiredFields.add("RawUpgrades"); + } + const defaultWeaponsMap: Record = { + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit": [ + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetMeleeWeaponIP" + ], + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit": [ + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetMeleeWeaponIS" + ], + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit": [ + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetMeleeWeaponPS" + ] + }; + const defaultWeapons = defaultWeaponsMap[request.ItemType] as string[] | undefined; + if (defaultWeapons) { + for (const defaultWeapon of defaultWeapons) { + const category = ExportWeapons[defaultWeapon].productCategory; + requiredFields.add(category); + requiredFields.add(productCategoryToInventoryBin(category)); + } + } + + const inventory = await getInventory(accountId, Array.from(requiredFields).join(" ")); + if (defaultWeapons) { + for (const defaultWeapon of defaultWeapons) { + const category = ExportWeapons[defaultWeapon].productCategory; + addEquipment(inventory, category, defaultWeapon); + occupySlot(inventory, productCategoryToInventoryBin(category)!, true); + } + } + + const defaultOverwrites: Partial = { + Configs: applyDefaultUpgrades(inventory, defaultUpgrades) + }; + + addEquipment(inventory, category, request.ItemType, request.ModularParts, undefined, defaultOverwrites); + occupySlot(inventory, inventoryBin, true); + await inventory.save(); + res.end(); +}; + +interface IAddModularEquipmentRequest { + ItemType: string; + ModularParts: string[]; +} diff --git a/src/controllers/custom/deleteAccountController.ts b/src/controllers/custom/deleteAccountController.ts index fb8ca399..fad4485b 100644 --- a/src/controllers/custom/deleteAccountController.ts +++ b/src/controllers/custom/deleteAccountController.ts @@ -7,16 +7,28 @@ import { Loadout } from "@/src/models/inventoryModels/loadoutModel"; import { PersonalRooms } from "@/src/models/personalRoomsModel"; import { Ship } from "@/src/models/shipModel"; import { Stats } from "@/src/models/statsModel"; +import { GuildMember } from "@/src/models/guildModel"; +import { Leaderboard } from "@/src/models/leaderboardModel"; +import { deleteGuild } from "@/src/services/guildService"; export const deleteAccountController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); + + // If account is the founding warlord of a guild, delete that guild as well. + const guildMember = await GuildMember.findOne({ accountId, rank: 0, status: 0 }); + if (guildMember) { + await deleteGuild(guildMember.guildId); + } + await Promise.all([ Account.deleteOne({ _id: accountId }), + GuildMember.deleteMany({ accountId: accountId }), Inbox.deleteMany({ ownerId: accountId }), Inventory.deleteOne({ accountOwnerId: accountId }), + Leaderboard.deleteMany({ ownerId: accountId }), Loadout.deleteOne({ loadoutOwnerId: accountId }), PersonalRooms.deleteOne({ personalRoomsOwnerId: accountId }), - Ship.deleteOne({ ShipOwnerId: accountId }), + Ship.deleteMany({ ShipOwnerId: accountId }), Stats.deleteOne({ accountOwnerId: accountId }) ]); res.end(); diff --git a/src/controllers/custom/getAccountInfoController.ts b/src/controllers/custom/getAccountInfoController.ts new file mode 100644 index 00000000..0f6524ad --- /dev/null +++ b/src/controllers/custom/getAccountInfoController.ts @@ -0,0 +1,40 @@ +import { AllianceMember, Guild, GuildMember } from "@/src/models/guildModel"; +import { getAccountForRequest, isAdministrator } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const getAccountInfoController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + const info: IAccountInfo = { + DisplayName: account.DisplayName + }; + if (isAdministrator(account)) { + info.IsAdministrator = true; + } + const guildMember = await GuildMember.findOne({ accountId: account._id, status: 0 }, "guildId rank"); + if (guildMember) { + const guild = (await Guild.findById(guildMember.guildId, "Ranks AllianceId"))!; + info.GuildId = guildMember.guildId.toString(); + info.GuildPermissions = guild.Ranks[guildMember.rank].Permissions; + info.GuildRank = guildMember.rank; + if (guild.AllianceId) { + //const alliance = (await Alliance.findById(guild.AllianceId))!; + const allianceMember = (await AllianceMember.findOne({ + allianceId: guild.AllianceId, + guildId: guild._id + }))!; + info.AllianceId = guild.AllianceId.toString(); + info.AlliancePermissions = allianceMember.Permissions; + } + } + res.json(info); +}; + +interface IAccountInfo { + DisplayName: string; + IsAdministrator?: boolean; + GuildId?: string; + GuildPermissions?: number; + GuildRank?: number; + AllianceId?: string; + AlliancePermissions?: number; +} diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index 86488d12..7d60f896 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -3,15 +3,20 @@ import { getDict, getItemName, getString } from "@/src/services/itemDataService" import { ExportArcanes, ExportAvionics, + ExportDrones, ExportGear, + ExportKeys, ExportMisc, + ExportRailjackWeapons, ExportRecipes, + ExportRelics, ExportResources, ExportSentinels, ExportSyndicates, ExportUpgrades, ExportWarframes, - ExportWeapons + ExportWeapons, + TRelicQuality } from "warframe-public-export-plus"; import archonCrystalUpgrades from "@/static/fixed_responses/webuiArchonCrystalUpgrades.json"; @@ -20,8 +25,18 @@ interface ListedItem { name: string; fusionLimit?: number; exalted?: string[]; + badReason?: "starter" | "frivolous" | "notraw"; + partType?: string; + chainLength?: number; } +const relicQualitySuffixes: Record = { + VPQ_BRONZE: "", + VPQ_SILVER: " [Flawless]", + VPQ_GOLD: " [Radiant]", + VPQ_PLATINUM: " [Exceptional]" +}; + const getItemListsController: RequestHandler = (req, response) => { const lang = getDict(typeof req.query.lang == "string" ? req.query.lang : "en"); const res: Record = {}; @@ -38,6 +53,8 @@ const getItemListsController: RequestHandler = (req, response) => { res.MechSuits = []; res.miscitems = []; res.Syndicates = []; + res.OperatorAmps = []; + res.QuestKeys = []; for (const [uniqueName, item] of Object.entries(ExportWarframes)) { res[item.productCategory].push({ uniqueName, @@ -54,23 +71,15 @@ const getItemListsController: RequestHandler = (req, response) => { } } for (const [uniqueName, item] of Object.entries(ExportWeapons)) { - if ( - uniqueName.split("/")[4] == "OperatorAmplifiers" || - uniqueName.split("/")[5] == "SUModularSecondarySet1" || - uniqueName.split("/")[5] == "SUModularPrimarySet1" || - uniqueName.split("/")[5] == "InfKitGun" || - uniqueName.split("/")[5] == "HoverboardParts" || - uniqueName.split("/")[5] == "ModularMelee01" || - uniqueName.split("/")[5] == "ModularMelee02" || - uniqueName.split("/")[5] == "ModularMeleeInfested" - ) { + if (item.partType) { res.ModularParts.push({ uniqueName, - name: getString(item.name, lang) + name: getString(item.name, lang), + partType: item.partType }); if (uniqueName.split("/")[5] != "SentTrainingAmplifier") { res.miscitems.push({ - uniqueName: "MiscItems:" + uniqueName, + uniqueName: uniqueName, name: getString(item.name, lang) }); } @@ -81,7 +90,8 @@ const getItemListsController: RequestHandler = (req, response) => { item.productCategory == "Melee" || item.productCategory == "SpaceGuns" || item.productCategory == "SpaceMelee" || - item.productCategory == "SentinelWeapons" + item.productCategory == "SentinelWeapons" || + item.productCategory == "OperatorAmps" ) { res[item.productCategory].push({ uniqueName, @@ -90,7 +100,7 @@ const getItemListsController: RequestHandler = (req, response) => { } } else if (!item.excludeFromCodex) { res.miscitems.push({ - uniqueName: "MiscItems:" + uniqueName, + uniqueName: uniqueName, name: getString(item.name, lang) }); } @@ -107,14 +117,31 @@ const getItemListsController: RequestHandler = (req, response) => { name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeSmallAbbrev", lang)); } } + if ( + name && + uniqueName.substr(0, 30) != "/Lotus/Types/Game/Projections/" && + uniqueName != "/Lotus/Types/Gameplay/EntratiLab/Resources/EntratiLanthornBundle" + ) { + res.miscitems.push({ + uniqueName: uniqueName, + name: name + }); + } + } + for (const [uniqueName, item] of Object.entries(ExportRelics)) { res.miscitems.push({ - uniqueName: item.productCategory + ":" + uniqueName, - name: name + uniqueName: uniqueName, + name: + getString("/Lotus/Language/Relics/VoidProjectionName", lang) + .split("|ERA|") + .join(item.era) + .split("|CATEGORY|") + .join(item.category) + relicQualitySuffixes[item.quality] }); } for (const [uniqueName, item] of Object.entries(ExportGear)) { res.miscitems.push({ - uniqueName: "Consumables:" + uniqueName, + uniqueName: uniqueName, name: getString(item.name, lang) }); } @@ -124,24 +151,40 @@ const getItemListsController: RequestHandler = (req, response) => { const resultName = getItemName(item.resultType); if (resultName) { res.miscitems.push({ - uniqueName: "Recipes:" + uniqueName, + uniqueName: uniqueName, name: recipeNameTemplate.replace("|ITEM|", getString(resultName, lang)) }); } } } + for (const [uniqueName, item] of Object.entries(ExportDrones)) { + res.miscitems.push({ + uniqueName: uniqueName, + name: getString(item.name, lang) + }); + } + for (const [uniqueName, item] of Object.entries(ExportRailjackWeapons)) { + res.miscitems.push({ + uniqueName: uniqueName, + name: getString(item.name, lang) + }); + } res.mods = []; - const badItems: Record = {}; for (const [uniqueName, upgrade] of Object.entries(ExportUpgrades)) { - res.mods.push({ + const mod: ListedItem = { uniqueName, name: getString(upgrade.name, lang), fusionLimit: upgrade.fusionLimit - }); - if (upgrade.isStarter || upgrade.isFrivolous || upgrade.upgradeEntries) { - badItems[uniqueName] = true; + }; + if (upgrade.isStarter) { + mod.badReason = "starter"; + } else if (upgrade.isFrivolous) { + mod.badReason = "frivolous"; + } else if (upgrade.upgradeEntries) { + mod.badReason = "notraw"; } + res.mods.push(mod); } for (const [uniqueName, upgrade] of Object.entries(ExportAvionics)) { res.mods.push({ @@ -151,12 +194,15 @@ const getItemListsController: RequestHandler = (req, response) => { }); } for (const [uniqueName, arcane] of Object.entries(ExportArcanes)) { - res.mods.push({ - uniqueName, - name: getString(arcane.name, lang) - }); - if (arcane.isFrivolous) { - badItems[uniqueName] = true; + if (uniqueName.substring(0, 18) != "/Lotus/Types/Game/") { + const mod: ListedItem = { + uniqueName, + name: getString(arcane.name, lang) + }; + if (arcane.isFrivolous) { + mod.badReason = "frivolous"; + } + res.mods.push(mod); } } for (const [uniqueName, syndicate] of Object.entries(ExportSyndicates)) { @@ -165,9 +211,17 @@ const getItemListsController: RequestHandler = (req, response) => { name: getString(syndicate.name, lang) }); } + for (const [uniqueName, key] of Object.entries(ExportKeys)) { + if (key.chainStages) { + res.QuestKeys.push({ + uniqueName, + name: getString(key.name || "", lang), + chainLength: key.chainStages.length + }); + } + } response.json({ - badItems, archonCrystalUpgrades, uniqueLevelCaps: ExportMisc.uniqueLevelCaps, ...res diff --git a/src/controllers/custom/getNameController.ts b/src/controllers/custom/getNameController.ts new file mode 100644 index 00000000..bc4a94f3 --- /dev/null +++ b/src/controllers/custom/getNameController.ts @@ -0,0 +1,7 @@ +import { RequestHandler } from "express"; +import { getAccountForRequest } from "@/src/services/loginService"; + +export const getNameController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + res.json(account.DisplayName); +}; diff --git a/src/controllers/custom/gildEquipmentController.ts b/src/controllers/custom/gildEquipmentController.ts new file mode 100644 index 00000000..46716207 --- /dev/null +++ b/src/controllers/custom/gildEquipmentController.ts @@ -0,0 +1,23 @@ +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; + +export const gildEquipmentController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const request = req.body as IGildEquipmentRequest; + const inventory = await getInventory(accountId, request.Category); + const weapon = inventory[request.Category].id(request.ItemId); + if (weapon) { + weapon.Features ??= 0; + weapon.Features |= EquipmentFeatures.GILDED; + await inventory.save(); + } + res.end(); +}; + +type IGildEquipmentRequest = { + ItemId: string; + Category: TEquipmentKey; +}; diff --git a/src/controllers/custom/ircDroppedController.ts b/src/controllers/custom/ircDroppedController.ts new file mode 100644 index 00000000..1621defc --- /dev/null +++ b/src/controllers/custom/ircDroppedController.ts @@ -0,0 +1,24 @@ +import { Account } from "@/src/models/loginModel"; +import { RequestHandler } from "express"; + +export const ircDroppedController: RequestHandler = async (req, res) => { + if (!req.query.accountId) { + throw new Error("Request is missing accountId parameter"); + } + const nonce: number = parseInt(req.query.nonce as string); + if (!nonce) { + throw new Error("Request is missing nonce parameter"); + } + + await Account.updateOne( + { + _id: req.query.accountId, + Nonce: nonce + }, + { + Dropped: true + } + ); + + res.end(); +}; diff --git a/src/controllers/custom/manageQuestsController.ts b/src/controllers/custom/manageQuestsController.ts index 6899b76d..49ae004c 100644 --- a/src/controllers/custom/manageQuestsController.ts +++ b/src/controllers/custom/manageQuestsController.ts @@ -1,7 +1,11 @@ -import { addString } from "@/src/controllers/api/inventoryController"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { addQuestKey, completeQuest, IUpdateQuestRequest, updateQuestKey } from "@/src/services/questService"; +import { + addQuestKey, + completeQuest, + giveKeyChainMissionReward, + giveKeyChainStageTriggered +} from "@/src/services/questService"; import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; import { ExportKeys } from "warframe-public-export-plus"; @@ -9,13 +13,17 @@ import { ExportKeys } from "warframe-public-export-plus"; export const manageQuestsController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const operation = req.query.operation as - | "unlockAll" | "completeAll" - | "ResetAll" - | "completeAllUnlocked" - | "updateKey" - | "giveAll"; - const questKeyUpdate = req.body as IUpdateQuestRequest["QuestKeys"]; + | "resetAll" + | "giveAll" + | "completeKey" + | "deleteKey" + | "resetKey" + | "prevStage" + | "nextStage" + | "setInactive"; + + const questItemType = req.query.itemType as string; const allQuestKeys: string[] = []; for (const [k, v] of Object.entries(ExportKeys)) { @@ -26,47 +34,15 @@ export const manageQuestsController: RequestHandler = async (req, res) => { const inventory = await getInventory(accountId); switch (operation) { - case "updateKey": { - //TODO: if this is intended to be used, one needs to add a updateQuestKeyMultiple, the game does never intend to do it, so it errors for multiple keys. - updateQuestKey(inventory, questKeyUpdate); - break; - } - case "unlockAll": { - for (const questKey of allQuestKeys) { - addQuestKey(inventory, { ItemType: questKey, Completed: false, unlock: true, Progress: [] }); - } - break; - } case "completeAll": { - logger.info("completing all quests.."); - for (const questKey of allQuestKeys) { - try { - await completeQuest(inventory, questKey); - } catch (error) { - if (error instanceof Error) { - logger.error( - `Something went wrong completing quest ${questKey}, probably could not add some item` - ); - logger.error(error.message); - } - } - - //Skip "Watch The Maker" - if (questKey === "/Lotus/Types/Keys/NewWarIntroQuest/NewWarIntroKeyChain") { - addString( - inventory.NodeIntrosCompleted, - "/Lotus/Levels/Cinematics/NewWarIntro/NewWarStageTwo.level" - ); - } - - if (questKey === "/Lotus/Types/Keys/ArchwingQuest/ArchwingQuestKeyChain") { - inventory.ArchwingEnabled = true; + if (allQuestKeys.includes(questItemType)) { + for (const questKey of inventory.QuestKeys) { + await completeQuest(inventory, questKey.ItemType); } } break; } - case "ResetAll": { - logger.info("resetting all quests.."); + case "resetAll": { for (const questKey of inventory.QuestKeys) { questKey.Completed = false; questKey.Progress = []; @@ -75,40 +51,113 @@ export const manageQuestsController: RequestHandler = async (req, res) => { inventory.ActiveQuest = ""; break; } - case "completeAllUnlocked": { - logger.info("completing all unlocked quests.."); - for (const questKey of inventory.QuestKeys) { - try { + case "giveAll": { + allQuestKeys.forEach(questKey => addQuestKey(inventory, { ItemType: questKey })); + break; + } + case "deleteKey": { + if (allQuestKeys.includes(questItemType)) { + const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType); + if (!questKey) { + logger.error(`Quest key not found in inventory: ${questItemType}`); + break; + } + + inventory.QuestKeys.pull({ ItemType: questItemType }); + } + break; + } + case "completeKey": { + if (allQuestKeys.includes(questItemType)) { + const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType); + if (!questKey) { + logger.error(`Quest key not found in inventory: ${questItemType}`); + break; + } + + await completeQuest(inventory, questItemType); + } + break; + } + case "resetKey": { + if (allQuestKeys.includes(questItemType)) { + const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType); + if (!questKey) { + logger.error(`Quest key not found in inventory: ${questItemType}`); + break; + } + + questKey.Completed = false; + questKey.Progress = []; + questKey.CompletionDate = undefined; + } + break; + } + case "prevStage": { + if (allQuestKeys.includes(questItemType)) { + const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType); + if (!questKey) { + logger.error(`Quest key not found in inventory: ${questItemType}`); + break; + } + if (!questKey.Progress) break; + + if (questKey.Completed) { + questKey.Completed = false; + questKey.CompletionDate = undefined; + } + questKey.Progress.pop(); + const stage = questKey.Progress.length - 1; + if (stage > 0) { + await giveKeyChainStageTriggered(inventory, { + KeyChain: questKey.ItemType, + ChainStage: stage + }); + } + } + break; + } + case "nextStage": { + if (allQuestKeys.includes(questItemType)) { + const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType); + const questManifest = ExportKeys[questItemType]; + if (!questKey) { + logger.error(`Quest key not found in inventory: ${questItemType}`); + break; + } + if (!questKey.Progress) break; + + const currentStage = questKey.Progress.length; + if (currentStage + 1 == questManifest.chainStages?.length) { + logger.debug(`Trying to complete last stage with nextStage, calling completeQuest instead`); await completeQuest(inventory, questKey.ItemType); - } catch (error) { - if (error instanceof Error) { - logger.error( - `Something went wrong completing quest ${questKey.ItemType}, probably could not add some item` - ); - logger.error(error.message); + } else { + const progress = { + c: questManifest.chainStages![currentStage].key ? -1 : 0, + i: false, + m: false, + b: [] + }; + questKey.Progress.push(progress); + + await giveKeyChainStageTriggered(inventory, { + KeyChain: questKey.ItemType, + ChainStage: currentStage + }); + + if (currentStage > 0) { + await giveKeyChainMissionReward(inventory, { + KeyChain: questKey.ItemType, + ChainStage: currentStage - 1 + }); } } - - //Skip "Watch The Maker" - if (questKey.ItemType === "/Lotus/Types/Keys/NewWarIntroQuest/NewWarIntroKeyChain") { - addString( - inventory.NodeIntrosCompleted, - "/Lotus/Levels/Cinematics/NewWarIntro/NewWarStageTwo.level" - ); - } - - if (questKey.ItemType === "/Lotus/Types/Keys/ArchwingQuest/ArchwingQuestKeyChain") { - inventory.ArchwingEnabled = true; - } } break; } - case "giveAll": { - for (const questKey of allQuestKeys) { - addQuestKey(inventory, { ItemType: questKey }); - } + case "setInactive": + inventory.ActiveQuest = ""; break; - } } await inventory.save(); diff --git a/src/controllers/custom/popArchonCrystalUpgradeController.ts b/src/controllers/custom/popArchonCrystalUpgradeController.ts index c9c84b85..34e87ec6 100644 --- a/src/controllers/custom/popArchonCrystalUpgradeController.ts +++ b/src/controllers/custom/popArchonCrystalUpgradeController.ts @@ -5,7 +5,7 @@ import { getInventory } from "@/src/services/inventoryService"; export const popArchonCrystalUpgradeController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId); - const suit = inventory.Suits.find(suit => suit._id.toString() == (req.query.oid as string)); + const suit = inventory.Suits.id(req.query.oid as string); if (suit && suit.ArchonCrystalUpgrades) { suit.ArchonCrystalUpgrades = suit.ArchonCrystalUpgrades.filter( x => x.UpgradeType != (req.query.type as string) diff --git a/src/controllers/custom/pushArchonCrystalUpgradeController.ts b/src/controllers/custom/pushArchonCrystalUpgradeController.ts index 093b0678..3a9286ee 100644 --- a/src/controllers/custom/pushArchonCrystalUpgradeController.ts +++ b/src/controllers/custom/pushArchonCrystalUpgradeController.ts @@ -5,7 +5,7 @@ import { getInventory } from "@/src/services/inventoryService"; export const pushArchonCrystalUpgradeController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId); - const suit = inventory.Suits.find(suit => suit._id.toString() == (req.query.oid as string)); + const suit = inventory.Suits.id(req.query.oid as string); if (suit) { suit.ArchonCrystalUpgrades ??= []; const count = (req.query.count as number | undefined) ?? 1; diff --git a/src/controllers/custom/renameAccountController.ts b/src/controllers/custom/renameAccountController.ts index c5b733e8..5f950550 100644 --- a/src/controllers/custom/renameAccountController.ts +++ b/src/controllers/custom/renameAccountController.ts @@ -1,5 +1,7 @@ import { RequestHandler } from "express"; -import { getAccountForRequest, isNameTaken } from "@/src/services/loginService"; +import { getAccountForRequest, isAdministrator, isNameTaken } from "@/src/services/loginService"; +import { config } from "@/src/services/configService"; +import { saveConfig } from "@/src/services/configWatcherService"; export const renameAccountController: RequestHandler = async (req, res) => { const account = await getAccountForRequest(req); @@ -7,8 +9,18 @@ export const renameAccountController: RequestHandler = async (req, res) => { if (await isNameTaken(req.query.newname)) { res.status(409).json("Name already in use"); } else { + if (isAdministrator(account)) { + for (let i = 0; i != config.administratorNames!.length; ++i) { + if (config.administratorNames![i] == account.DisplayName) { + config.administratorNames![i] = req.query.newname; + } + } + await saveConfig(); + } + account.DisplayName = req.query.newname; await account.save(); + res.end(); } } else { diff --git a/src/controllers/custom/unlockAllIntrinsicsController.ts b/src/controllers/custom/unlockAllIntrinsicsController.ts new file mode 100644 index 00000000..cd48bdcc --- /dev/null +++ b/src/controllers/custom/unlockAllIntrinsicsController.ts @@ -0,0 +1,19 @@ +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const unlockAllIntrinsicsController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "PlayerSkills"); + inventory.PlayerSkills.LPS_PILOTING = 10; + inventory.PlayerSkills.LPS_GUNNERY = 10; + inventory.PlayerSkills.LPS_TACTICAL = 10; + inventory.PlayerSkills.LPS_ENGINEERING = 10; + inventory.PlayerSkills.LPS_COMMAND = 10; + inventory.PlayerSkills.LPS_DRIFT_COMBAT = 10; + inventory.PlayerSkills.LPS_DRIFT_RIDING = 10; + inventory.PlayerSkills.LPS_DRIFT_OPPORTUNITY = 10; + inventory.PlayerSkills.LPS_DRIFT_ENDURANCE = 10; + await inventory.save(); + res.end(); +}; diff --git a/src/controllers/custom/updateConfigDataController.ts b/src/controllers/custom/updateConfigDataController.ts index 961cccb1..534dfe0f 100644 --- a/src/controllers/custom/updateConfigDataController.ts +++ b/src/controllers/custom/updateConfigDataController.ts @@ -1,5 +1,5 @@ import { RequestHandler } from "express"; -import { updateConfig } from "@/src/services/configService"; +import { updateConfig } from "@/src/services/configWatcherService"; import { getAccountForRequest, isAdministrator } from "@/src/services/loginService"; const updateConfigDataController: RequestHandler = async (req, res) => { diff --git a/src/controllers/dynamic/getGuildAdsController.ts b/src/controllers/dynamic/getGuildAdsController.ts new file mode 100644 index 00000000..1dbe8217 --- /dev/null +++ b/src/controllers/dynamic/getGuildAdsController.ts @@ -0,0 +1,26 @@ +import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; +import { GuildAd } from "@/src/models/guildModel"; +import { IGuildAdInfoClient } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const getGuildAdsController: RequestHandler = async (req, res) => { + const ads = await GuildAd.find(req.query.tier ? { Tier: req.query.tier } : {}); + const guildAdInfos: IGuildAdInfoClient[] = []; + for (const ad of ads) { + guildAdInfos.push({ + _id: toOid(ad.GuildId), + CrossPlatformEnabled: true, + Emblem: ad.Emblem, + Expiry: toMongoDate(ad.Expiry), + Features: ad.Features, + GuildName: ad.GuildName, + MemberCount: ad.MemberCount, + OriginalPlatform: 0, + RecruitMsg: ad.RecruitMsg, + Tier: ad.Tier + }); + } + res.json({ + GuildAdInfos: guildAdInfos + }); +}; diff --git a/src/controllers/dynamic/getProfileViewingDataController.ts b/src/controllers/dynamic/getProfileViewingDataController.ts new file mode 100644 index 00000000..766e40a2 --- /dev/null +++ b/src/controllers/dynamic/getProfileViewingDataController.ts @@ -0,0 +1,307 @@ +import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; +import { Guild, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; +import { Inventory, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; +import { Loadout } from "@/src/models/inventoryModels/loadoutModel"; +import { Account } from "@/src/models/loginModel"; +import { Stats, TStatsDatabaseDocument } from "@/src/models/statsModel"; +import { allDailyAffiliationKeys } from "@/src/services/inventoryService"; +import { IMongoDate, IOid } from "@/src/types/commonTypes"; +import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { + IAffiliation, + IAlignment, + IChallengeProgress, + IDailyAffiliations, + ILoadoutConfigClient, + IMission, + IPlayerSkills, + ITypeXPItem +} from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; +import { catBreadHash } from "@/src/helpers/stringHelpers"; +import { ExportCustoms, ExportDojoRecipes } from "warframe-public-export-plus"; +import { IStatsClient } from "@/src/types/statTypes"; +import { toStoreItem } from "@/src/services/itemDataService"; + +export const getProfileViewingDataController: RequestHandler = async (req, res) => { + if (req.query.playerId) { + const account = await Account.findById(req.query.playerId as string, "DisplayName"); + if (!account) { + res.status(409).send("Could not find requested account"); + return; + } + const inventory = (await Inventory.findOne({ accountOwnerId: account._id }))!; + + const result: IPlayerProfileViewingDataResult = { + AccountId: toOid(account._id), + DisplayName: account.DisplayName, + PlayerLevel: inventory.PlayerLevel, + LoadOutInventory: { + WeaponSkins: [], + XPInfo: inventory.XPInfo + }, + PlayerSkills: inventory.PlayerSkills, + ChallengeProgress: inventory.ChallengeProgress, + DeathMarks: inventory.DeathMarks, + Harvestable: inventory.Harvestable, + DeathSquadable: inventory.DeathSquadable, + Created: toMongoDate(inventory.Created), + MigratedToConsole: false, + Missions: inventory.Missions, + Affiliations: inventory.Affiliations, + DailyFocus: inventory.DailyFocus, + Wishlist: inventory.Wishlist, + Alignment: inventory.Alignment + }; + await populateLoadout(inventory, result); + if (inventory.GuildId) { + const guild = (await Guild.findById(inventory.GuildId, "Name Tier XP Class Emblem"))!; + populateGuild(guild, result); + } + for (const key of allDailyAffiliationKeys) { + result[key] = inventory[key]; + } + + const stats = (await Stats.findOne({ accountOwnerId: account._id }))!.toJSON>(); + delete stats._id; + delete stats.__v; + delete stats.accountOwnerId; + + res.json({ + Results: [result], + TechProjects: [], + XpComponents: [], + //XpCacheExpiryDate, some IMongoDate in the future, no clue what it's for + Stats: stats + }); + } else if (req.query.guildId) { + const guild = await Guild.findById(req.query.guildId, "Name Tier XP Class Emblem TechProjects ClaimedXP"); + if (!guild) { + res.status(409).send("Could not find guild"); + return; + } + const members = await GuildMember.find({ guildId: guild._id, status: 0 }); + const results: IPlayerProfileViewingDataResult[] = []; + for (let i = 0; i != Math.min(4, members.length); ++i) { + const member = members[i]; + const [account, inventory] = await Promise.all([ + Account.findById(member.accountId, "DisplayName"), + Inventory.findOne( + { accountOwnerId: member.accountId }, + "DisplayName PlayerLevel XPInfo LoadOutPresets CurrentLoadOutIds WeaponSkins Suits Pistols LongGuns Melee" + ) + ]); + const result: IPlayerProfileViewingDataResult = { + AccountId: toOid(account!._id), + DisplayName: account!.DisplayName, + PlayerLevel: inventory!.PlayerLevel, + LoadOutInventory: { + WeaponSkins: [], + XPInfo: inventory!.XPInfo + } + }; + await populateLoadout(inventory!, result); + results.push(result); + } + populateGuild(guild, results[0]); + + const combinedStats: IStatsClient = {}; + const statsArr = await Stats.find({ accountOwnerId: { $in: members.map(x => x.accountId) } }).lean(); // need this as POJO so Object.entries works as expected + for (const stats of statsArr) { + for (const [key, value] of Object.entries(stats)) { + if (typeof value == "number" && key != "__v") { + (combinedStats[key as keyof IStatsClient] as number | undefined) ??= 0; + (combinedStats[key as keyof IStatsClient] as number) += value; + } + } + for (const arrayName of ["Weapons", "Enemies", "Scans", "Missions", "PVP"] as const) { + if (stats[arrayName]) { + combinedStats[arrayName] ??= []; + for (const entry of stats[arrayName]) { + const combinedEntry = combinedStats[arrayName].find(x => x.type == entry.type); + if (combinedEntry) { + for (const [key, value] of Object.entries(entry)) { + if (typeof value == "number") { + (combinedEntry[key as keyof typeof combinedEntry] as unknown as + | number + | undefined) ??= 0; + (combinedEntry[key as keyof typeof combinedEntry] as unknown as number) += value; + } + } + } else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + combinedStats[arrayName].push(entry as any); + } + } + } + } + } + + const xpComponents: IXPComponentClient[] = []; + if (guild.ClaimedXP) { + for (const componentName of guild.ClaimedXP) { + if (componentName.endsWith(".level")) { + const [key] = Object.entries(ExportDojoRecipes.rooms).find( + ([_key, value]) => value.resultType == componentName + )!; + xpComponents.push({ + StoreTypeName: toStoreItem(key) + }); + } else { + const [key] = Object.entries(ExportDojoRecipes.decos).find( + ([_key, value]) => value.resultType == componentName + )!; + xpComponents.push({ + StoreTypeName: toStoreItem(key) + }); + } + } + } + + res.json({ + Results: results, + TechProjects: guild.TechProjects, + XpComponents: xpComponents, + //XpCacheExpiryDate, some IMongoDate in the future, no clue what it's for + Stats: combinedStats + }); + } else { + res.sendStatus(400); + } +}; + +interface IPlayerProfileViewingDataResult extends Partial { + AccountId: IOid; + DisplayName: string; + PlayerLevel: number; + LoadOutPreset?: Omit & { ItemId?: IOid }; + LoadOutInventory: { + WeaponSkins: { ItemType: string }[]; + Suits?: IEquipmentClient[]; + Pistols?: IEquipmentClient[]; + LongGuns?: IEquipmentClient[]; + Melee?: IEquipmentClient[]; + XPInfo: ITypeXPItem[]; + }; + GuildId?: IOid; + GuildName?: string; + GuildTier?: number; + GuildXp?: number; + GuildClass?: number; + GuildEmblem?: boolean; + PlayerSkills?: IPlayerSkills; + ChallengeProgress?: IChallengeProgress[]; + DeathMarks?: string[]; + Harvestable?: boolean; + DeathSquadable?: boolean; + Created?: IMongoDate; + MigratedToConsole?: boolean; + Missions?: IMission[]; + Affiliations?: IAffiliation[]; + DailyFocus?: number; + Wishlist?: string[]; + Alignment?: IAlignment; +} + +interface IXPComponentClient { + _id?: IOid; + StoreTypeName: string; + TypeName?: string; + PurchaseQuantity?: number; + ProductCategory?: "Recipes"; + Rarity?: "COMMON"; + RegularPrice?: number; + PremiumPrice?: number; + SellingPrice?: number; + DateAddedToManifest?: number; + PrimeSellingPrice?: number; + GuildXp?: number; + ResultPrefab?: string; + ResultDecoration?: string; + ShowInMarket?: boolean; + ShowInInventory?: boolean; + locTags?: Record; +} + +let skinLookupTable: Record | undefined; + +const resolveAndCollectSkins = ( + inventory: TInventoryDatabaseDocument, + skins: Set, + item: IEquipmentClient +): void => { + for (const config of item.Configs) { + if (config.Skins) { + for (let i = 0; i != config.Skins.length; ++i) { + // Resolve oids to type names + if (config.Skins[i].length == 24) { + if (config.Skins[i].substring(0, 16) == "ca70ca70ca70ca70") { + if (!skinLookupTable) { + skinLookupTable = {}; + for (const key of Object.keys(ExportCustoms)) { + skinLookupTable[catBreadHash(key)] = key; + } + } + config.Skins[i] = skinLookupTable[parseInt(config.Skins[i].substring(16), 16)]; + } else { + const skinItem = inventory.WeaponSkins.id(config.Skins[i]); + config.Skins[i] = skinItem ? skinItem.ItemType : ""; + } + } + + // Collect type names + if (config.Skins[i]) { + skins.add(config.Skins[i]); + } + } + } + } +}; + +const populateLoadout = async ( + inventory: TInventoryDatabaseDocument, + result: IPlayerProfileViewingDataResult +): Promise => { + if (inventory.CurrentLoadOutIds.length) { + const loadout = (await Loadout.findById(inventory.LoadOutPresets, "NORMAL"))!; + result.LoadOutPreset = loadout.NORMAL.id(inventory.CurrentLoadOutIds[0].$oid)!.toJSON(); + result.LoadOutPreset.ItemId = undefined; + const skins = new Set(); + if (result.LoadOutPreset.s) { + result.LoadOutInventory.Suits = [ + inventory.Suits.id(result.LoadOutPreset.s.ItemId.$oid)!.toJSON() + ]; + resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.Suits[0]); + } + if (result.LoadOutPreset.p) { + result.LoadOutInventory.Pistols = [ + inventory.Pistols.id(result.LoadOutPreset.p.ItemId.$oid)!.toJSON() + ]; + resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.Pistols[0]); + } + if (result.LoadOutPreset.l) { + result.LoadOutInventory.LongGuns = [ + inventory.LongGuns.id(result.LoadOutPreset.l.ItemId.$oid)!.toJSON() + ]; + resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.LongGuns[0]); + } + if (result.LoadOutPreset.m) { + result.LoadOutInventory.Melee = [ + inventory.Melee.id(result.LoadOutPreset.m.ItemId.$oid)!.toJSON() + ]; + resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.Melee[0]); + } + for (const skin of skins) { + result.LoadOutInventory.WeaponSkins.push({ ItemType: skin }); + } + } +}; + +const populateGuild = (guild: TGuildDatabaseDocument, result: IPlayerProfileViewingDataResult): void => { + result.GuildId = toOid(guild._id); + result.GuildName = guild.Name; + result.GuildTier = guild.Tier; + result.GuildXp = guild.XP; + result.GuildClass = guild.Class; + result.GuildEmblem = guild.Emblem; +}; diff --git a/src/controllers/dynamic/worldStateController.ts b/src/controllers/dynamic/worldStateController.ts index a954aae9..05c7f60f 100644 --- a/src/controllers/dynamic/worldStateController.ts +++ b/src/controllers/dynamic/worldStateController.ts @@ -7,33 +7,93 @@ import static1999WinterDays from "@/static/fixed_responses/worldState/1999_winte import { buildConfig } from "@/src/services/buildConfigService"; import { IMongoDate, IOid } from "@/src/types/commonTypes"; import { unixTimesInMs } from "@/src/constants/timeConstants"; +import { config } from "@/src/services/configService"; +import { CRng } from "@/src/services/rngService"; +import { ExportNightwave, ExportRegions } from "warframe-public-export-plus"; + +const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0 export const worldStateController: RequestHandler = (req, res) => { + const day = Math.trunc((Date.now() - EPOCH) / 86400000); + const week = Math.trunc(day / 7); + const weekStart = EPOCH + week * 604800000; + const weekEnd = weekStart + 604800000; + const worldState: IWorldState = { BuildLabel: typeof req.query.buildLabel == "string" ? req.query.buildLabel.split(" ").join("+") : buildConfig.buildLabel, - Time: Math.round(Date.now() / 1000), + Time: config.worldState?.lockTime || Math.round(Date.now() / 1000), + Goals: [], + GlobalUpgrades: [], + LiteSorties: [], EndlessXpChoices: [], + SeasonInfo: { + Activation: { $date: { $numberLong: "1715796000000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + AffiliationTag: "RadioLegionIntermission12Syndicate", + Season: 14, + Phase: 0, + Params: "", + ActiveChallenges: [ + getSeasonDailyChallenge(day - 2), + getSeasonDailyChallenge(day - 1), + getSeasonDailyChallenge(day - 0), + getSeasonWeeklyChallenge(week, 0), + getSeasonWeeklyChallenge(week, 1), + getSeasonWeeklyHardChallenge(week, 2), + getSeasonWeeklyHardChallenge(week, 3), + { + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: + "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" + (week - 12) + }, + { + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" + (week - 12) + }, + { + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" + (week - 12) + } + ] + }, ...staticWorldState }; - const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0 - const day = Math.trunc((new Date().getTime() - EPOCH) / 86400000); - const week = Math.trunc(day / 7); - const weekStart = EPOCH + week * 604800000; - const weekEnd = weekStart + 604800000; + if (config.worldState?.starDays) { + worldState.Goals.push({ + _id: { $oid: "67a4dcce2a198564d62e1647" }, + Activation: { $date: { $numberLong: "1738868400000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Count: 0, + Goal: 0, + Success: 0, + Personal: true, + Desc: "/Lotus/Language/Events/ValentinesFortunaName", + ToolTip: "/Lotus/Language/Events/ValentinesFortunaName", + Icon: "/Lotus/Interface/Icons/WorldStatePanel/ValentinesEventIcon.png", + Tag: "FortunaValentines", + Node: "SolarisUnitedHub1" + }); + } // Elite Sanctuary Onslaught cycling every week worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = week; // unfaithful // Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation - const bountyCycle = Math.trunc(new Date().getTime() / 9000000); + const bountyCycle = Math.trunc(Date.now() / 9000000); const bountyCycleStart = bountyCycle * 9000000; const bountyCycleEnd = bountyCycleStart + 9000000; worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "ZarimanSyndicate")] = { - _id: { $oid: bountyCycleStart.toString(16) + "0000000000000029" }, + _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000029" }, Activation: { $date: { $numberLong: bountyCycleStart.toString() } }, Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } }, Tag: "ZarimanSyndicate", @@ -41,7 +101,7 @@ export const worldStateController: RequestHandler = (req, res) => { Nodes: [] }; worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "EntratiLabSyndicate")] = { - _id: { $oid: bountyCycleStart.toString(16) + "0000000000000004" }, + _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000004" }, Activation: { $date: { $numberLong: bountyCycleStart.toString() } }, Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } }, Tag: "EntratiLabSyndicate", @@ -49,7 +109,7 @@ export const worldStateController: RequestHandler = (req, res) => { Nodes: [] }; worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "HexSyndicate")] = { - _id: { $oid: bountyCycleStart.toString(16) + "0000000000000006" }, + _id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000006" }, Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, Tag: "HexSyndicate", @@ -57,6 +117,104 @@ export const worldStateController: RequestHandler = (req, res) => { Nodes: [] }; + if (config.worldState?.creditBoost) { + worldState.GlobalUpgrades.push({ + _id: { $oid: "5b23106f283a555109666672" }, + Activation: { $date: { $numberLong: "1740164400000" } }, + ExpiryDate: { $date: { $numberLong: "2000000000000" } }, + UpgradeType: "GAMEPLAY_MONEY_REWARD_AMOUNT", + OperationType: "MULTIPLY", + Value: 2, + LocalizeTag: "", + LocalizeDescTag: "" + }); + } + if (config.worldState?.affinityBoost) { + worldState.GlobalUpgrades.push({ + _id: { $oid: "5b23106f283a555109666673" }, + Activation: { $date: { $numberLong: "1740164400000" } }, + ExpiryDate: { $date: { $numberLong: "2000000000000" } }, + UpgradeType: "GAMEPLAY_KILL_XP_AMOUNT", + OperationType: "MULTIPLY", + Value: 2, + LocalizeTag: "", + LocalizeDescTag: "" + }); + } + if (config.worldState?.resourceBoost) { + worldState.GlobalUpgrades.push({ + _id: { $oid: "5b23106f283a555109666674" }, + Activation: { $date: { $numberLong: "1740164400000" } }, + ExpiryDate: { $date: { $numberLong: "2000000000000" } }, + UpgradeType: "GAMEPLAY_PICKUP_AMOUNT", + OperationType: "MULTIPLY", + Value: 2, + LocalizeTag: "", + LocalizeDescTag: "" + }); + } + + // Archon Hunt cycling every week + { + const boss = ["SORTIE_BOSS_AMAR", "SORTIE_BOSS_NIRA", "SORTIE_BOSS_BOREAL"][week % 3]; + const showdownNode = ["SolNode99", "SolNode53", "SolNode24"][week % 3]; + const systemIndex = [3, 4, 2][week % 3]; // Mars, Jupiter, Earth + + const nodes: string[] = []; + for (const [key, value] of Object.entries(ExportRegions)) { + if ( + value.systemIndex === systemIndex && + value.factionIndex !== undefined && + value.factionIndex < 2 && + value.name.indexOf("Archwing") == -1 && + value.missionIndex != 0 // Exclude MT_ASSASSINATION + ) { + nodes.push(key); + } + } + + const rng = new CRng(week); + const firstNodeIndex = rng.randomInt(0, nodes.length - 1); + const firstNode = nodes[firstNodeIndex]; + nodes.splice(firstNodeIndex, 1); + worldState.LiteSorties.push({ + _id: { + $oid: Math.trunc(weekStart / 1000).toString(16) + "5e23a244740a190c" + }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards", + Seed: week, + Boss: boss, + Missions: [ + { + missionType: rng.randomElement([ + "MT_INTEL", + "MT_MOBILE_DEFENSE", + "MT_EXTERMINATION", + "MT_SABOTAGE", + "MT_RESCUE" + ]), + node: firstNode + }, + { + missionType: rng.randomElement([ + "MT_DEFENSE", + "MT_TERRITORY", + "MT_ARTIFACT", + "MT_EXCAVATE", + "MT_SURVIVAL" + ]), + node: rng.randomElement(nodes) + }, + { + missionType: "MT_ASSASSINATION", + node: showdownNode + } + ] + }); + } + // Circuit choices cycling every week worldState.EndlessXpChoices.push({ Category: "EXC_NORMAL", @@ -101,7 +259,7 @@ export const worldStateController: RequestHandler = (req, res) => { worldState.KnownCalendarSeasons[0].YearIteration = Math.trunc(week / 4); // Sentient Anomaly cycling every 30 minutes - const halfHour = Math.trunc(new Date().getTime() / (unixTimesInMs.hour / 2)); + const halfHour = Math.trunc(Date.now() / (unixTimesInMs.hour / 2)); const tmp = { cavabegin: "1690761600", PurchasePlatformLockEnabled: true, @@ -134,15 +292,43 @@ export const worldStateController: RequestHandler = (req, res) => { }; interface IWorldState { + Version: number; // for goals BuildLabel: string; Time: number; + Goals: IGoal[]; SyndicateMissions: ISyndicateMission[]; + GlobalUpgrades: IGlobalUpgrade[]; + LiteSorties: ILiteSortie[]; NodeOverrides: INodeOverride[]; EndlessXpChoices: IEndlessXpChoice[]; + SeasonInfo: { + Activation: IMongoDate; + Expiry: IMongoDate; + AffiliationTag: string; + Season: number; + Phase: number; + Params: string; + ActiveChallenges: ISeasonChallenge[]; + }; KnownCalendarSeasons: ICalendarSeason[]; Tmp?: string; } +interface IGoal { + _id: IOid; + Activation: IMongoDate; + Expiry: IMongoDate; + Count: number; + Goal: number; + Success: number; + Personal: boolean; + Desc: string; + ToolTip: string; + Icon: string; + Tag: string; + Node: string; +} + interface ISyndicateMission { _id: IOid; Activation: IMongoDate; @@ -152,6 +338,17 @@ interface ISyndicateMission { Nodes: string[]; } +interface IGlobalUpgrade { + _id: IOid; + Activation: IMongoDate; + ExpiryDate: IMongoDate; + UpgradeType: string; + OperationType: string; + Value: number; + LocalizeTag: string; + LocalizeDescTag: string; +} + interface INodeOverride { _id: IOid; Activation?: IMongoDate; @@ -164,11 +361,32 @@ interface INodeOverride { CustomNpcEncounters?: string; } +interface ILiteSortie { + _id: IOid; + Activation: IMongoDate; + Expiry: IMongoDate; + Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards"; + Seed: number; + Boss: string; // "SORTIE_BOSS_AMAR" | "SORTIE_BOSS_NIRA" | "SORTIE_BOSS_BOREAL" + Missions: { + missionType: string; + node: string; + }[]; +} + interface IEndlessXpChoice { Category: string; Choices: string[]; } +interface ISeasonChallenge { + _id: IOid; + Daily?: boolean; + Activation: IMongoDate; + Expiry: IMongoDate; + Challenge: string; +} + interface ICalendarSeason { Activation: IMongoDate; Expiry: IMongoDate; @@ -178,3 +396,56 @@ interface ICalendarSeason { }[]; YearIteration: number; } + +const dailyChallenges = Object.keys(ExportNightwave.challenges).filter(x => + x.startsWith("/Lotus/Types/Challenges/Seasons/Daily/") +); + +const getSeasonDailyChallenge = (day: number): ISeasonChallenge => { + const dayStart = EPOCH + day * 86400000; + const dayEnd = EPOCH + (day + 3) * 86400000; + const rng = new CRng(day); + return { + _id: { $oid: "67e1b5ca9d00cb47" + day.toString().padStart(8, "0") }, + Daily: true, + Activation: { $date: { $numberLong: dayStart.toString() } }, + Expiry: { $date: { $numberLong: dayEnd.toString() } }, + Challenge: rng.randomElement(dailyChallenges) + }; +}; + +const weeklyChallenges = Object.keys(ExportNightwave.challenges).filter( + 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 weekEnd = weekStart + 604800000; + const challengeId = week * 7 + id; + const rng = new CRng(challengeId); + return { + _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: rng.randomElement(weeklyChallenges) + }; +}; + +const weeklyHardChallenges = Object.keys(ExportNightwave.challenges).filter(x => + x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/") +); + +const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChallenge => { + const weekStart = EPOCH + week * 604800000; + const weekEnd = weekStart + 604800000; + const challengeId = week * 7 + id; + const rng = new CRng(challengeId); + return { + _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: rng.randomElement(weeklyHardChallenges) + }; +}; diff --git a/src/controllers/stats/leaderboardController.ts b/src/controllers/stats/leaderboardController.ts new file mode 100644 index 00000000..f5550f2b --- /dev/null +++ b/src/controllers/stats/leaderboardController.ts @@ -0,0 +1,25 @@ +import { getLeaderboard } from "@/src/services/leaderboardService"; +import { RequestHandler } from "express"; + +export const leaderboardController: RequestHandler = async (req, res) => { + const payload = JSON.parse(String(req.body)) as ILeaderboardRequest; + res.json({ + results: await getLeaderboard( + payload.field, + payload.before, + payload.after, + payload.pivotId, + payload.guildId, + payload.guildTier + ) + }); +}; + +interface ILeaderboardRequest { + field: string; + before: number; + after: number; + pivotId?: string; + guildId?: string; + guildTier?: number; +} diff --git a/src/controllers/stats/uploadController.ts b/src/controllers/stats/uploadController.ts index 89e5dfc3..c1c1d2e5 100644 --- a/src/controllers/stats/uploadController.ts +++ b/src/controllers/stats/uploadController.ts @@ -1,6 +1,6 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getStats, updateStats } from "@/src/services/statsService"; +import { updateStats } from "@/src/services/statsService"; import { IStatsUpdate } from "@/src/types/statTypes"; import { RequestHandler } from "express"; @@ -8,8 +8,7 @@ const uploadController: RequestHandler = async (req, res) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { PS, ...payload } = getJSONfromString(String(req.body)); const accountId = await getAccountIdForRequest(req); - const playerStats = await getStats(accountId); - await updateStats(playerStats, payload); + await updateStats(accountId, payload); res.status(200).end(); }; diff --git a/src/controllers/stats/viewController.ts b/src/controllers/stats/viewController.ts index 594efd3b..55fbbb7f 100644 --- a/src/controllers/stats/viewController.ts +++ b/src/controllers/stats/viewController.ts @@ -17,7 +17,7 @@ const viewController: RequestHandler = async (req, res) => { for (const item of inventory.XPInfo) { const weaponIndex = responseJson.Weapons.findIndex(element => element.type == item.ItemType); if (weaponIndex !== -1) { - responseJson.Weapons[weaponIndex].xp == item.XP; + responseJson.Weapons[weaponIndex].xp = item.XP; } else { responseJson.Weapons.push({ type: item.ItemType, xp: item.XP }); } diff --git a/src/helpers/customHelpers/customHelpers.ts b/src/helpers/customHelpers/customHelpers.ts index e1173d1f..a0163833 100644 --- a/src/helpers/customHelpers/customHelpers.ts +++ b/src/helpers/customHelpers/customHelpers.ts @@ -1,5 +1,5 @@ import { IAccountCreation } from "@/src/types/customTypes"; -import { IDatabaseAccount } from "@/src/types/loginTypes"; +import { IDatabaseAccountRequiredFields } from "@/src/types/loginTypes"; import crypto from "crypto"; import { isString, parseEmail, parseString } from "../general"; @@ -40,7 +40,7 @@ const toAccountCreation = (accountCreation: unknown): IAccountCreation => { throw new Error("incorrect account creation data: incorrect properties"); }; -const toDatabaseAccount = (createAccount: IAccountCreation): IDatabaseAccount => { +const toDatabaseAccount = (createAccount: IAccountCreation): IDatabaseAccountRequiredFields => { return { ...createAccount, ClientType: "", @@ -48,9 +48,8 @@ const toDatabaseAccount = (createAccount: IAccountCreation): IDatabaseAccount => CrossPlatformAllowed: true, ForceLogoutVersion: 0, TrackedSettings: [], - Nonce: 0, - LatestEventMessageDate: new Date(0) - } satisfies IDatabaseAccount; + Nonce: 0 + } satisfies IDatabaseAccountRequiredFields; }; export { toDatabaseAccount, toAccountCreation as toCreateAccount }; diff --git a/src/helpers/modularWeaponHelper.ts b/src/helpers/modularWeaponHelper.ts new file mode 100644 index 00000000..5651f373 --- /dev/null +++ b/src/helpers/modularWeaponHelper.ts @@ -0,0 +1,26 @@ +import { TEquipmentKey } from "../types/inventoryTypes/inventoryTypes"; + +export const modularWeaponTypes: Record = { + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary": "LongGuns", + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam": "LongGuns", + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryLauncher": "LongGuns", + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun": "LongGuns", + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimarySniper": "LongGuns", + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary": "Pistols", + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam": "Pistols", + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun": "Pistols", + "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon": "Melee", + "/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon": "OperatorAmps", + "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit": "Hoverboards", + "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit": "MoaPets", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetPowerSuit": "MoaPets", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit": "MoaPets", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit": "MoaPets", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit": "MoaPets", + "/Lotus/Types/Friendly/Pets/CreaturePets/ArmoredInfestedCatbrowPetPowerSuit": "KubrowPets", + "/Lotus/Types/Friendly/Pets/CreaturePets/HornedInfestedCatbrowPetPowerSuit": "KubrowPets", + "/Lotus/Types/Friendly/Pets/CreaturePets/VulpineInfestedCatbrowPetPowerSuit": "KubrowPets", + "/Lotus/Types/Friendly/Pets/CreaturePets/MedjayPredatorKubrowPetPowerSuit": "KubrowPets", + "/Lotus/Types/Friendly/Pets/CreaturePets/PharaohPredatorKubrowPetPowerSuit": "KubrowPets", + "/Lotus/Types/Friendly/Pets/CreaturePets/VizierPredatorKubrowPetPowerSuit": "KubrowPets" +}; diff --git a/src/helpers/nemesisHelpers.ts b/src/helpers/nemesisHelpers.ts new file mode 100644 index 00000000..839e0675 --- /dev/null +++ b/src/helpers/nemesisHelpers.ts @@ -0,0 +1,44 @@ +import { ExportRegions } from "warframe-public-export-plus"; +import { IInfNode } from "@/src/types/inventoryTypes/inventoryTypes"; +import { SRng } from "@/src/services/rngService"; + +export const getInfNodes = (faction: string, rank: number): IInfNode[] => { + const infNodes = []; + const systemIndex = systemIndexes[faction][rank]; + for (const [key, value] of Object.entries(ExportRegions)) { + if ( + value.systemIndex === systemIndex && + value.nodeType != 3 && // not hub + value.nodeType != 7 && // not junction + value.missionIndex && // must have a mission type and not assassination + value.missionIndex != 28 && // not open world + value.missionIndex != 32 && // not railjack + value.missionIndex != 41 && // not saya's visions + value.missionIndex != 42 && // not face off + value.name.indexOf("1999NodeI") == -1 && // not stage defence + value.name.indexOf("1999NodeJ") == -1 && // not lich bounty + value.name.indexOf("Archwing") == -1 + ) { + //console.log(dict_en[value.name]); + infNodes.push({ Node: key, Influence: 1 }); + } + } + return infNodes; +}; + +const systemIndexes: Record = { + FC_GRINEER: [2, 3, 9, 11, 18], + FC_CORPUS: [1, 15, 4, 7, 8], + FC_INFESTATION: [23] +}; + +// Get a parazon 'passcode' based on the nemesis fingerprint so it's always the same for the same nemesis. +export const getNemesisPasscode = (fp: bigint, faction: string): number[] => { + const rng = new SRng(fp); + const passcode = [rng.randomInt(0, 7)]; + if (faction != "FC_INFESTATION") { + passcode.push(rng.randomInt(0, 7)); + passcode.push(rng.randomInt(0, 7)); + } + return passcode; +}; diff --git a/src/helpers/pathHelper.ts b/src/helpers/pathHelper.ts new file mode 100644 index 00000000..95621f6a --- /dev/null +++ b/src/helpers/pathHelper.ts @@ -0,0 +1,4 @@ +import path from "path"; + +export const rootDir = path.join(__dirname, "../.."); +export const repoDir = path.basename(rootDir) == "build" ? path.join(rootDir, "..") : rootDir; diff --git a/src/helpers/relicHelper.ts b/src/helpers/relicHelper.ts index 6e28aef0..13d7d7d4 100644 --- a/src/helpers/relicHelper.ts +++ b/src/helpers/relicHelper.ts @@ -1,19 +1,21 @@ import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { IVoidTearParticipantInfo } from "@/src/types/requestTypes"; import { ExportRelics, ExportRewards, TRarity } from "warframe-public-export-plus"; -import { getRandomWeightedReward2 } from "@/src/services/rngService"; +import { getRandomWeightedReward, IRngResult } from "@/src/services/rngService"; import { logger } from "@/src/utils/logger"; -import { addMiscItems } from "@/src/services/inventoryService"; +import { addMiscItems, combineInventoryChanges } from "@/src/services/inventoryService"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; +import { IInventoryChanges } from "../types/purchaseTypes"; export const crackRelic = async ( inventory: TInventoryDatabaseDocument, - participant: IVoidTearParticipantInfo -): Promise => { + participant: IVoidTearParticipantInfo, + inventoryChanges: IInventoryChanges = {} +): Promise => { const relic = ExportRelics[participant.VoidProjection]; const weights = refinementToWeights[relic.quality]; logger.debug(`opening a relic of quality ${relic.quality}; rarity weights are`, weights); - const reward = getRandomWeightedReward2( + const reward = getRandomWeightedReward( ExportRewards[relic.rewardManifest][0] as { type: string; itemCount: number; rarity: TRarity }[], // rarity is nullable in PE+ typings, but always present for relics weights )!; @@ -21,15 +23,22 @@ export const crackRelic = async ( participant.Reward = reward.type; // Remove relic - addMiscItems(inventory, [ + const miscItemChanges = [ { ItemType: participant.VoidProjection, ItemCount: -1 } - ]); + ]; + addMiscItems(inventory, miscItemChanges); + combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges }); // Give reward - await handleStoreItemAcquisition(reward.type, inventory, reward.itemCount); + combineInventoryChanges( + inventoryChanges, + (await handleStoreItemAcquisition(reward.type, inventory, reward.itemCount)).InventoryChanges + ); + + return reward; }; const refinementToWeights = { diff --git a/src/helpers/rivenFingerprintHelper.ts b/src/helpers/rivenFingerprintHelper.ts deleted file mode 100644 index c3742391..00000000 --- a/src/helpers/rivenFingerprintHelper.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { IUpgrade } from "warframe-public-export-plus"; -import { getRandomElement } from "../services/rngService"; - -export interface IUnveiledRivenFingerprint { - compat: string; - lim: 0; - lvl: number; - lvlReq: number; - rerolls?: number; - pol: string; - buffs: IRivenStat[]; - curses: IRivenStat[]; -} - -interface IRivenStat { - Tag: string; - Value: number; -} - -export const randomiseRivenStats = (meta: IUpgrade, fingerprint: IUnveiledRivenFingerprint): void => { - fingerprint.buffs = []; - const numBuffs = 2 + Math.trunc(Math.random() * 2); // 2 or 3 - const buffEntries = meta.upgradeEntries!.filter(x => x.canBeBuff); - for (let i = 0; i != numBuffs; ++i) { - const buffIndex = Math.trunc(Math.random() * buffEntries.length); - const entry = buffEntries[buffIndex]; - fingerprint.buffs.push({ Tag: entry.tag, Value: Math.trunc(Math.random() * 0x40000000) }); - buffEntries.splice(buffIndex, 1); - } - - fingerprint.curses = []; - if (Math.random() < 0.5) { - const entry = getRandomElement( - meta.upgradeEntries!.filter(x => x.canBeCurse && !fingerprint.buffs.find(y => y.Tag == x.tag)) - ); - fingerprint.curses.push({ Tag: entry.tag, Value: Math.trunc(Math.random() * 0x40000000) }); - } -}; diff --git a/src/helpers/rivenHelper.ts b/src/helpers/rivenHelper.ts new file mode 100644 index 00000000..35426a29 --- /dev/null +++ b/src/helpers/rivenHelper.ts @@ -0,0 +1,121 @@ +import { IUpgrade } from "warframe-public-export-plus"; +import { getRandomElement, getRandomInt, getRandomReward } from "../services/rngService"; + +export type RivenFingerprint = IVeiledRivenFingerprint | IUnveiledRivenFingerprint; + +export interface IVeiledRivenFingerprint { + challenge: IRivenChallenge; +} + +export interface IRivenChallenge { + Type: string; + Progress: number; + Required: number; + Complication?: string; +} + +export interface IUnveiledRivenFingerprint { + compat: string; + lim: 0; + lvl: number; + lvlReq: number; + rerolls?: number; + pol: string; + buffs: IFingerprintStat[]; + curses: IFingerprintStat[]; +} + +export interface IFingerprintStat { + Tag: string; + Value: number; +} + +export const createVeiledRivenFingerprint = (meta: IUpgrade): IVeiledRivenFingerprint => { + const challenge = getRandomElement(meta.availableChallenges!); + const fingerprintChallenge: IRivenChallenge = { + Type: challenge.fullName, + Progress: 0, + Required: getRandomInt(challenge.countRange[0], challenge.countRange[1]) + }; + if (Math.random() < challenge.complicationChance) { + const complications: { type: string; probability: number }[] = []; + for (const complication of challenge.complications) { + complications.push({ + type: complication.fullName, + probability: complication.weight + }); + } + fingerprintChallenge.Complication = getRandomReward(complications)!.type; + const complication = challenge.complications.find(x => x.fullName == fingerprintChallenge.Complication)!; + fingerprintChallenge.Required *= complication.countMultiplier; + } + return { challenge: fingerprintChallenge }; +}; + +export const createUnveiledRivenFingerprint = (meta: IUpgrade): IUnveiledRivenFingerprint => { + const fingerprint: IUnveiledRivenFingerprint = { + compat: getRandomElement(meta.compatibleItems!), + lim: 0, + lvl: 0, + lvlReq: getRandomInt(8, 16), + pol: getRandomElement(["AP_ATTACK", "AP_DEFENSE", "AP_TACTIC"]), + buffs: [], + curses: [] + }; + randomiseRivenStats(meta, fingerprint); + return fingerprint; +}; + +export const randomiseRivenStats = (meta: IUpgrade, fingerprint: IUnveiledRivenFingerprint): void => { + fingerprint.buffs = []; + const numBuffs = 2 + Math.trunc(Math.random() * 2); // 2 or 3 + const buffEntries = meta.upgradeEntries!.filter(x => x.canBeBuff); + for (let i = 0; i != numBuffs; ++i) { + const buffIndex = Math.trunc(Math.random() * buffEntries.length); + const entry = buffEntries[buffIndex]; + fingerprint.buffs.push({ Tag: entry.tag, Value: Math.trunc(Math.random() * 0x40000000) }); + buffEntries.splice(buffIndex, 1); + } + + fingerprint.curses = []; + if (Math.random() < 0.5) { + const entry = getRandomElement( + meta.upgradeEntries!.filter(x => x.canBeCurse && !fingerprint.buffs.find(y => y.Tag == x.tag)) + ); + fingerprint.curses.push({ Tag: entry.tag, Value: Math.trunc(Math.random() * 0x40000000) }); + } +}; + +export const rivenRawToRealWeighted: Record = { + "/Lotus/Upgrades/Mods/Randomized/RawArchgunRandomMod": [ + "/Lotus/Upgrades/Mods/Randomized/LotusArchgunRandomModRare" + ], + "/Lotus/Upgrades/Mods/Randomized/RawMeleeRandomMod": [ + "/Lotus/Upgrades/Mods/Randomized/PlayerMeleeWeaponRandomModRare" + ], + "/Lotus/Upgrades/Mods/Randomized/RawModularMeleeRandomMod": [ + "/Lotus/Upgrades/Mods/Randomized/LotusModularMeleeRandomModRare" + ], + "/Lotus/Upgrades/Mods/Randomized/RawModularPistolRandomMod": [ + "/Lotus/Upgrades/Mods/Randomized/LotusModularPistolRandomModRare" + ], + "/Lotus/Upgrades/Mods/Randomized/RawPistolRandomMod": ["/Lotus/Upgrades/Mods/Randomized/LotusPistolRandomModRare"], + "/Lotus/Upgrades/Mods/Randomized/RawRifleRandomMod": ["/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare"], + "/Lotus/Upgrades/Mods/Randomized/RawShotgunRandomMod": [ + "/Lotus/Upgrades/Mods/Randomized/LotusShotgunRandomModRare" + ], + "/Lotus/Upgrades/Mods/Randomized/RawSentinelWeaponRandomMod": [ + "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", + "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", + "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", + "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", + "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", + "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", + "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", + "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", + "/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare", + "/Lotus/Upgrades/Mods/Randomized/LotusShotgunRandomModRare", + "/Lotus/Upgrades/Mods/Randomized/LotusPistolRandomModRare", + "/Lotus/Upgrades/Mods/Randomized/PlayerMeleeWeaponRandomModRare" + ] +}; diff --git a/src/helpers/stringHelpers.ts b/src/helpers/stringHelpers.ts index 6ab13851..8925aea2 100644 --- a/src/helpers/stringHelpers.ts +++ b/src/helpers/stringHelpers.ts @@ -1,6 +1,8 @@ +import { JSONParse } from "json-with-bigint"; + export const getJSONfromString = (str: string): T => { const jsonSubstring = str.substring(0, str.lastIndexOf("}") + 1); - return JSON.parse(jsonSubstring) as T; + return JSONParse(jsonSubstring); }; export const getSubstringFromKeyword = (str: string, keyword: string): string => { @@ -24,3 +26,31 @@ export const getIndexAfter = (str: string, searchWord: string): number => { } return index + searchWord.length; }; + +// This is FNV1a-32 except operating under modulus 2^31 because JavaScript is stinky and likes producing negative integers out of nowhere. +export const catBreadHash = (name: string): number => { + let hash = 2166136261; + for (let i = 0; i != name.length; ++i) { + hash = (hash ^ name.charCodeAt(i)) & 0x7fffffff; + hash = (hash * 16777619) & 0x7fffffff; + } + return hash; +}; + +export const regexEscape = (str: string): string => { + str = str.split(".").join("\\."); + str = str.split("\\").join("\\\\"); + str = str.split("[").join("\\["); + str = str.split("]").join("\\]"); + str = str.split("+").join("\\+"); + str = str.split("*").join("\\*"); + str = str.split("$").join("\\$"); + str = str.split("^").join("\\^"); + str = str.split("?").join("\\?"); + str = str.split("|").join("\\|"); + str = str.split("(").join("\\("); + str = str.split(")").join("\\)"); + str = str.split("{").join("\\{"); + str = str.split("}").join("\\}"); + return str; +}; diff --git a/src/index.ts b/src/index.ts index 8bf614ef..9a942606 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,16 +1,30 @@ -import { logger } from "./utils/logger"; +// First, init config. +import { config, loadConfig } from "@/src/services/configService"; +try { + loadConfig(); +} catch (e) { + console.log("ERROR: Failed to load config.json. You can copy config.json.example to create your config.json."); + process.exit(1); +} +// Now we can init the logger with the settings provided in the config. +import { logger } from "@/src/utils/logger"; logger.info("Starting up..."); +// 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 { config, validateConfig } from "./services/configService"; -import { registerLogFileCreationListener } from "@/src/utils/logger"; import mongoose from "mongoose"; +import { Json, JSONStringify } from "json-with-bigint"; +import { validateConfig } from "@/src/services/configWatcherService"; + +// Patch JSON.stringify to work flawlessly with Bigints. +JSON.stringify = (obj: Exclude, _replacer?: unknown, space?: string | number): string => { + return JSONStringify(obj, space); +}; -registerLogFileCreationListener(); validateConfig(); mongoose diff --git a/src/managers/sessionManager.ts b/src/managers/sessionManager.ts index 98bcc912..d933407a 100644 --- a/src/managers/sessionManager.ts +++ b/src/managers/sessionManager.ts @@ -44,7 +44,7 @@ function getSessionByID(sessionId: string): ISession | undefined { return sessions.find(session => session.sessionId === sessionId); } -function getSession(sessionIdOrRequest: string | IFindSessionRequest): any[] { +function getSession(sessionIdOrRequest: string | IFindSessionRequest): { createdBy: string; id: string }[] { if (typeof sessionIdOrRequest === "string") { const session = sessions.find(session => session.sessionId === sessionIdOrRequest); if (session) { @@ -107,8 +107,7 @@ function updateSession(sessionId: string, sessionData: string): boolean { const session = sessions.find(session => session.sessionId === sessionId); if (!session) return false; try { - const updatedData = JSON.parse(sessionData); - Object.assign(session, updatedData); + Object.assign(session, JSON.parse(sessionData)); return true; } catch (error) { console.error("Invalid JSON string for session update."); diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 0582dc64..563cb7d2 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -2,11 +2,44 @@ import { IGuildDatabase, IDojoComponentDatabase, ITechProjectDatabase, - ITechProjectClient + IDojoDecoDatabase, + ILongMOTD, + IGuildMemberDatabase, + IGuildLogEntryNumber, + IGuildRank, + IGuildLogRoomChange, + IGuildLogEntryRoster, + IGuildLogEntryContributable, + IDojoLeaderboardEntry, + IGuildAdDatabase, + IAllianceDatabase, + IAllianceMemberDatabase } from "@/src/types/guildTypes"; -import { model, Schema } from "mongoose"; -import { typeCountSchema } from "./inventoryModels/inventoryModel"; -import { toMongoDate } from "../helpers/inventoryHelpers"; +import { Document, Model, model, Schema, Types } from "mongoose"; +import { fusionTreasuresSchema, typeCountSchema } from "./inventoryModels/inventoryModel"; +import { pictureFrameInfoSchema } from "./personalRoomsModel"; + +const dojoDecoSchema = new Schema({ + Type: String, + Pos: [Number], + Rot: [Number], + Name: String, + Sockets: Number, + RegularCredits: Number, + MiscItems: { type: [typeCountSchema], default: undefined }, + CompletionTime: Date, + RushPlatinum: Number, + PictureFrameInfo: pictureFrameInfoSchema +}); + +const dojoLeaderboardEntrySchema = new Schema( + { + s: Number, + r: Number, + n: String + }, + { _id: false } +); const dojoComponentSchema = new Schema({ pf: { type: String, required: true }, @@ -16,7 +49,15 @@ const dojoComponentSchema = new Schema({ pp: String, Name: String, Message: String, - CompletionTime: Date + RegularCredits: Number, + MiscItems: { type: [typeCountSchema], default: undefined }, + CompletionTime: Date, + CompletionLogPending: Boolean, + RushPlatinum: Number, + DestructionTime: Date, + Decos: [dojoDecoSchema], + DecoCapacity: Number, + Leaderboard: { type: [dojoLeaderboardEntrySchema], default: undefined } }); const techProjectSchema = new Schema( @@ -30,26 +71,216 @@ const techProjectSchema = new Schema( { _id: false } ); -techProjectSchema.set("toJSON", { - virtuals: true, - transform(_doc, obj) { - const db = obj as ITechProjectDatabase; - const client = obj as ITechProjectClient; - if (db.CompletionDate) { - client.CompletionDate = toMongoDate(db.CompletionDate); - } +const longMOTDSchema = new Schema( + { + message: String, + authorName: String, + authorGuildName: String + }, + { _id: false } +); + +const guildRankSchema = new Schema( + { + Name: String, + Permissions: Number + }, + { _id: false } +); + +const defaultRanks: IGuildRank[] = [ + { + Name: "/Lotus/Language/Game/Rank_Creator", + Permissions: 16351 + }, + { + Name: "/Lotus/Language/Game/Rank_Warlord", + Permissions: 16351 + }, + { + Name: "/Lotus/Language/Game/Rank_General", + Permissions: 4318 + }, + { + Name: "/Lotus/Language/Game/Rank_Officer", + Permissions: 4314 + }, + { + Name: "/Lotus/Language/Game/Rank_Leader", + Permissions: 4106 + }, + { + Name: "/Lotus/Language/Game/Rank_Sage", + Permissions: 4304 + }, + { + Name: "/Lotus/Language/Game/Rank_Soldier", + Permissions: 4098 + }, + { + Name: "/Lotus/Language/Game/Rank_Initiate", + Permissions: 4096 + }, + { + Name: "/Lotus/Language/Game/Rank_Utility", + Permissions: 4096 } -}); +]; + +const guildLogRoomChangeSchema = new Schema( + { + dateTime: Date, + entryType: Number, + details: String, + componentId: Types.ObjectId + }, + { _id: false } +); + +const guildLogEntryContributableSchema = new Schema( + { + dateTime: Date, + entryType: Number, + details: String + }, + { _id: false } +); + +const guildLogEntryRosterSchema = new Schema( + { + dateTime: Date, + entryType: Number, + details: String + }, + { _id: false } +); + +const guildLogEntryNumberSchema = new Schema( + { + dateTime: Date, + entryType: Number, + details: Number + }, + { _id: false } +); const guildSchema = new Schema( { - Name: { type: String, required: true }, - DojoComponents: [dojoComponentSchema], + Name: { type: String, required: true, unique: true }, + MOTD: { type: String, default: "" }, + LongMOTD: { type: longMOTDSchema, default: undefined }, + Ranks: { type: [guildRankSchema], default: defaultRanks }, + TradeTax: { type: Number, default: 0 }, + Tier: { type: Number, default: 1 }, + Emblem: { type: Boolean }, + AutoContributeFromVault: { type: Boolean }, + AllianceId: { type: Types.ObjectId }, + DojoComponents: { type: [dojoComponentSchema], default: [] }, DojoCapacity: { type: Number, default: 100 }, DojoEnergy: { type: Number, default: 5 }, - TechProjects: { type: [techProjectSchema], default: undefined } + VaultRegularCredits: Number, + VaultPremiumCredits: Number, + VaultMiscItems: { type: [typeCountSchema], default: undefined }, + VaultShipDecorations: { type: [typeCountSchema], default: undefined }, + VaultFusionTreasures: { type: [fusionTreasuresSchema], default: undefined }, + TechProjects: { type: [techProjectSchema], default: undefined }, + ActiveDojoColorResearch: { type: String, default: "" }, + Class: { type: Number, default: 0 }, + XP: { type: Number, default: 0 }, + ClaimedXP: { type: [String], default: undefined }, + CeremonyClass: Number, + CeremonyContributors: { type: [Types.ObjectId], default: undefined }, + CeremonyResetDate: Date, + CeremonyEndo: Number, + RoomChanges: { type: [guildLogRoomChangeSchema], default: undefined }, + TechChanges: { type: [guildLogEntryContributableSchema], default: undefined }, + RosterActivity: { type: [guildLogEntryRosterSchema], default: undefined }, + ClassChanges: { type: [guildLogEntryNumberSchema], default: undefined } }, { id: false } ); -export const Guild = model("Guild", guildSchema); +type GuildDocumentProps = { + DojoComponents: Types.DocumentArray; +}; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +type GuildModel = Model; + +export const Guild = model("Guild", guildSchema); + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export type TGuildDatabaseDocument = Document & + Omit< + IGuildDatabase & { + _id: Types.ObjectId; + } & { + __v: number; + }, + keyof GuildDocumentProps + > & + GuildDocumentProps; + +const guildMemberSchema = new Schema({ + accountId: Types.ObjectId, + guildId: Types.ObjectId, + status: { type: Number, required: true }, + rank: { type: Number, default: 7 }, + RequestMsg: String, + RequestExpiry: Date, + RegularCreditsContributed: Number, + PremiumCreditsContributed: Number, + MiscItemsContributed: { type: [typeCountSchema], default: undefined }, + ShipDecorationsContributed: { type: [typeCountSchema], default: undefined } +}); + +guildMemberSchema.index({ accountId: 1, guildId: 1 }, { unique: true }); +guildMemberSchema.index({ RequestExpiry: 1 }, { expireAfterSeconds: 0 }); + +export const GuildMember = model("GuildMember", guildMemberSchema); + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export type TGuildMemberDatabaseDocument = Document & + IGuildMemberDatabase & { + _id: Types.ObjectId; + __v: number; + }; + +const guildAdSchema = new Schema({ + GuildId: { type: Schema.Types.ObjectId, required: true }, + Emblem: Boolean, + Expiry: { type: Date, required: true }, + Features: { type: Number, required: true }, + GuildName: { type: String, required: true }, + MemberCount: { type: Number, required: true }, + RecruitMsg: { type: String, required: true }, + Tier: { type: Number, required: true } +}); + +guildAdSchema.index({ GuildId: 1 }, { unique: true }); +guildAdSchema.index({ Expiry: 1 }, { expireAfterSeconds: 0 }); + +export const GuildAd = model("GuildAd", guildAdSchema); + +const allianceSchema = new Schema({ + Name: String, + MOTD: longMOTDSchema, + LongMOTD: longMOTDSchema, + Emblem: Boolean, + VaultRegularCredits: Number +}); + +allianceSchema.index({ Name: 1 }, { unique: true }); + +export const Alliance = model("Alliance", allianceSchema); + +const allianceMemberSchema = new Schema({ + allianceId: { type: Schema.Types.ObjectId, required: true }, + guildId: { type: Schema.Types.ObjectId, required: true }, + Pending: { type: Boolean, required: true }, + Permissions: { type: Number, required: true } +}); + +allianceMemberSchema.index({ allianceId: 1, guildId: 1 }, { unique: true }); + +export const AllianceMember = model("AllianceMember", allianceMemberSchema); diff --git a/src/models/inboxModel.ts b/src/models/inboxModel.ts index 10b1930e..c2d8af44 100644 --- a/src/models/inboxModel.ts +++ b/src/models/inboxModel.ts @@ -31,12 +31,21 @@ export interface IMessage { countedAtt?: ITypeCount[]; transmission?: string; arg?: Arg[]; + gifts?: IGift[]; r?: boolean; + contextInfo?: string; + acceptAction?: string; + declineAction?: string; + hasAccountAction?: boolean; } export interface Arg { Key: string; - Tag: string; + Tag: string | number; +} + +export interface IGift { + GiftType: string; } //types are wrong @@ -76,6 +85,14 @@ export interface Arg { // cinematic: string; // requiredLevel: string; // } + +const giftSchema = new Schema( + { + GiftType: String + }, + { _id: false } +); + const messageSchema = new Schema( { ownerId: Schema.Types.ObjectId, @@ -89,18 +106,23 @@ const messageSchema = new Schema( endDate: Date, r: Boolean, att: { type: [String], default: undefined }, + gifts: { type: [giftSchema], default: undefined }, countedAtt: { type: [typeCountSchema], default: undefined }, transmission: String, arg: { type: [ { Key: String, - Tag: String, + Tag: Schema.Types.Mixed, _id: false } ], default: undefined - } + }, + contextInfo: String, + acceptAction: String, + declineAction: String, + hasAccountAction: Boolean }, { timestamps: { createdAt: "date", updatedAt: false }, id: false } ); @@ -130,4 +152,6 @@ messageSchema.set("toJSON", { } }); +messageSchema.index({ ownerId: 1 }); + export const Inbox = model("Inbox", messageSchema, "inbox"); diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 0e82181c..a142273e 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -9,8 +9,8 @@ import { ISlots, IMailboxDatabase, IDuviriInfo, - IPendingRecipe as IPendingRecipeDatabase, - IPendingRecipeResponse, + IPendingRecipeDatabase, + IPendingRecipeClient, ITypeCount, IFocusXP, IFocusUpgrade, @@ -68,9 +68,23 @@ import { ICalendarProgress, IPendingCouponDatabase, IPendingCouponClient, - ILibraryAvailableDailyTaskInfo, + ILibraryDailyTaskInfo, IDroneDatabase, - IDroneClient + IDroneClient, + IAlignment, + ICollectibleEntry, + IIncentiveState, + ISongChallenge, + ILibraryPersonalProgress, + IRecentVendorPurchaseDatabase, + IVendorPurchaseHistoryEntryDatabase, + IVendorPurchaseHistoryEntryClient, + INemesisDatabase, + INemesisClient, + IInfNode, + IDiscoveredMarker, + IWeeklyMission, + ILockedWeaponGroupDatabase } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -88,6 +102,16 @@ import { EquipmentSelectionSchema } from "./loadoutModel"; export const typeCountSchema = new Schema({ ItemType: String, ItemCount: Number }, { _id: false }); +typeCountSchema.set("toJSON", { + transform(_doc, obj) { + if (obj.ItemCount > 2147483647) { + obj.ItemCount = 2147483647; + } else if (obj.ItemCount < -2147483648) { + obj.ItemCount = -2147483648; + } + } +}); + const focusXPSchema = new Schema( { AP_POWER: Number, @@ -108,29 +132,6 @@ const focusUpgradeSchema = new Schema( { _id: false } ); -const pendingRecipeSchema = new Schema( - { - ItemType: String, - CompletionDate: Date - }, - { id: false } -); - -pendingRecipeSchema.virtual("ItemId").get(function () { - return { $oid: this._id.toString() }; -}); - -pendingRecipeSchema.set("toJSON", { - virtuals: true, - transform(_document, returnedObject) { - delete returnedObject._id; - delete returnedObject.__v; - (returnedObject as IPendingRecipeResponse).CompletionDate = { - $date: { $numberLong: (returnedObject as IPendingRecipeDatabase).CompletionDate.getTime().toString() } - }; - } -}); - const polaritySchema = new Schema( { Slot: Number, @@ -355,7 +356,14 @@ const droneSchema = new Schema( { ItemType: String, CurrentHP: Number, - RepairStart: { type: Date, default: undefined } + RepairStart: { type: Date, default: undefined }, + + DeployTime: { type: Date, default: undefined }, + System: Number, + DamageTime: { type: Date, default: undefined }, + PendingDamage: Number, + ResourceType: String, + ResourceCount: Number }, { id: false } ); @@ -366,12 +374,30 @@ droneSchema.set("toJSON", { const db = obj as IDroneDatabase; client.ItemId = toOid(db._id); + if (db.RepairStart) { + client.RepairStart = toMongoDate(db.RepairStart); + } + + delete db.DeployTime; + delete db.System; + delete db.DamageTime; + delete db.PendingDamage; + delete db.ResourceType; + delete db.ResourceCount; delete obj._id; delete obj.__v; } }); +const discoveredMarkerSchema = new Schema( + { + tag: String, + discoveryState: [Number] + }, + { _id: false } +); + const challengeProgressSchema = new Schema( { Progress: Number, @@ -431,6 +457,18 @@ kubrowPetEggSchema.set("toJSON", { } }); +const weeklyMissionSchema = new Schema( + { + MissionIndex: Number, + CompletedMission: Boolean, + JobManifest: String, + Challenges: [String], + ChallengesReset: Boolean, + WeekCount: Number + }, + { _id: false } +); + const affiliationsSchema = new Schema( { Initiated: Boolean, @@ -438,6 +476,7 @@ const affiliationsSchema = new Schema( Title: Number, FreeFavorsEarned: { type: [Number], default: undefined }, FreeFavorsUsed: { type: [Number], default: undefined }, + WeeklyMissions: { type: [weeklyMissionSchema], default: undefined }, Tag: String }, { _id: false } @@ -462,7 +501,6 @@ const seasonChallengeHistorySchema = new Schema( //TODO: check whether this is complete const playerSkillsSchema = new Schema( { - LPP_NONE: { type: Number, default: 0 }, LPP_SPACE: { type: Number, default: 0 }, LPS_PILOTING: { type: Number, default: 0 }, LPS_GUNNERY: { type: Number, default: 0 }, @@ -483,7 +521,8 @@ const settingsSchema = new Schema({ GiftMode: String, GuildInvRestriction: String, ShowFriendInvNotifications: Boolean, - TradingRulesConfirmed: Boolean + TradingRulesConfirmed: Boolean, + SubscribedToSurveys: Boolean }); const consumedSchuitsSchema = new Schema( @@ -554,7 +593,7 @@ questKeysSchema.set("toJSON", { } }); -const fusionTreasuresSchema = new Schema().add(typeCountSchema).add({ Sockets: Number }); +export const fusionTreasuresSchema = new Schema().add(typeCountSchema).add({ Sockets: Number }); const spectreLoadoutsSchema = new Schema( { @@ -572,7 +611,8 @@ const spectreLoadoutsSchema = new Schema( const weaponSkinsSchema = new Schema( { - ItemType: String + ItemType: String, + IsNew: Boolean }, { id: false } ); @@ -801,7 +841,9 @@ detailsSchema.set("toJSON", { const db = returnedObject as IKubrowPetDetailsDatabase; const client = returnedObject as IKubrowPetDetailsClient; - client.HatchDate = toMongoDate(db.HatchDate); + if (db.HatchDate) { + client.HatchDate = toMongoDate(db.HatchDate); + } } }); @@ -835,7 +877,8 @@ const EquipmentSchema = new Schema( Customization: crewShipCustomizationSchema, RailjackImage: FlavourItemSchema, CrewMembers: crewShipMembersSchema, - Details: detailsSchema + Details: detailsSchema, + IsNew: Boolean }, { id: false } ); @@ -856,6 +899,9 @@ EquipmentSchema.set("toJSON", { if (db.InfestationDate) { client.InfestationDate = toMongoDate(db.InfestationDate); } + if (db.UpgradesExpiry) { + client.UpgradesExpiry = toMongoDate(db.UpgradesExpiry); + } } }); @@ -865,6 +911,37 @@ equipmentKeys.forEach(key => { equipmentFields[key] = { type: [EquipmentSchema] }; }); +const pendingRecipeSchema = new Schema( + { + ItemType: String, + CompletionDate: Date, + LongGuns: { type: [EquipmentSchema], default: undefined }, + Pistols: { type: [EquipmentSchema], default: undefined }, + Melee: { type: [EquipmentSchema], default: undefined }, + SuitToUnbrand: { type: Schema.Types.ObjectId, default: undefined } + }, + { id: false } +); + +pendingRecipeSchema.virtual("ItemId").get(function () { + return { $oid: this._id.toString() }; +}); + +pendingRecipeSchema.set("toJSON", { + virtuals: true, + transform(_document, returnedObject) { + delete returnedObject._id; + delete returnedObject.__v; + delete returnedObject.LongGuns; + delete returnedObject.Pistols; + delete returnedObject.Melees; + delete returnedObject.SuitToUnbrand; + (returnedObject as IPendingRecipeClient).CompletionDate = { + $date: { $numberLong: (returnedObject as IPendingRecipeDatabase).CompletionDate.getTime().toString() } + }; + } +}); + const infestedFoundrySchema = new Schema( { Name: String, @@ -936,6 +1013,59 @@ const calenderProgressSchema = new Schema( { _id: false } ); +const incentiveStateSchema = new Schema( + { + threshold: Number, + complete: Boolean, + sent: Boolean + }, + { _id: false } +); + +const vendorPurchaseHistoryEntrySchema = new Schema( + { + Expiry: Date, + NumPurchased: Number, + ItemId: String + }, + { _id: false } +); + +vendorPurchaseHistoryEntrySchema.set("toJSON", { + transform(_doc, obj) { + const db = obj as IVendorPurchaseHistoryEntryDatabase; + const client = obj as IVendorPurchaseHistoryEntryClient; + client.Expiry = toMongoDate(db.Expiry); + } +}); + +const recentVendorPurchaseSchema = new Schema( + { + VendorType: String, + PurchaseHistory: [vendorPurchaseHistoryEntrySchema] + }, + { _id: false } +); + +const collectibleEntrySchema = new Schema( + { + CollectibleType: String, + Count: Number, + Tracking: String, + ReqScans: Number, + IncentiveStates: [incentiveStateSchema] + }, + { _id: false } +); + +const songChallengeSchema = new Schema( + { + Song: String, + Difficulties: [Number] + }, + { _id: false } +); + const pendingCouponSchema = new Schema( { Expiry: { type: Date, default: new Date(0) }, @@ -950,11 +1080,21 @@ pendingCouponSchema.set("toJSON", { } }); -const libraryAvailableDailyTaskInfoSchema = new Schema( +const libraryPersonalProgressSchema = new Schema( + { + TargetType: String, + Scans: Number, + Completed: Boolean + }, + { _id: false } +); + +const libraryDailyTaskInfoSchema = new Schema( { EnemyTypes: [String], EnemyLocTag: String, EnemyIcon: String, + Scans: Number, ScansRequired: Number, RewardStoreItem: String, RewardQuantity: Number, @@ -963,6 +1103,75 @@ const libraryAvailableDailyTaskInfoSchema = new Schema( + { + Node: String, + Influence: Number + }, + { _id: false } +); + +const nemesisSchema = new Schema( + { + fp: BigInt, + manifest: String, + KillingSuit: String, + killingDamageType: Number, + ShoulderHelmet: String, + WeaponIdx: Number, + AgentIdx: Number, + BirthNode: String, + Faction: String, + Rank: Number, + k: Boolean, + Traded: Boolean, + d: Date, + PrevOwners: Number, + SecondInCommand: Boolean, + Weakened: Boolean, + InfNodes: [infNodeSchema], + HenchmenKilled: Number, + HintProgress: Number, + Hints: [Number], + GuessHistory: [Number], + MissionCount: Number, + LastEnc: Number + }, + { _id: false } +); + +nemesisSchema.set("toJSON", { + virtuals: true, + transform(_doc, obj) { + const db = obj as INemesisDatabase; + const client = obj as INemesisClient; + + client.d = toMongoDate(db.d); + + delete obj._id; + delete obj.__v; + } +}); + +const alignmentSchema = new Schema( + { + Alignment: Number, + Wisdom: Number + }, + { _id: false } +); + +const lockedWeaponGroupSchema = new Schema( + { + s: Schema.Types.ObjectId, + p: Schema.Types.ObjectId, + l: Schema.Types.ObjectId, + m: Schema.Types.ObjectId, + sn: Schema.Types.ObjectId + }, + { _id: false } +); + const inventorySchema = new Schema( { accountOwnerId: Schema.Types.ObjectId, @@ -1039,7 +1248,8 @@ const inventorySchema = new Schema( ChallengeProgress: [challengeProgressSchema], //Account Item like Ferrite,Form,Kuva etc - MiscItems: [typeCountSchema], + MiscItems: { type: [typeCountSchema], default: [] }, + FoundToday: { type: [typeCountSchema], default: undefined }, //Non Upgrade Mods Example:I have 999 item WeaponElectricityDamageMod (only "ItemCount"+"ItemType") RawUpgrades: [RawUpgrades], @@ -1063,6 +1273,7 @@ const inventorySchema = new Schema( ReceivedStartingGear: Boolean, ArchwingEnabled: Boolean, + HasOwnedVoidProjectionsPreviously: Boolean, //Use Operator\Drifter UseAdultOperatorLoadout: Boolean, @@ -1083,20 +1294,18 @@ const inventorySchema = new Schema( //Default RailJack CrewShipAmmo: [typeCountSchema], - CrewShipWeapons: [Schema.Types.Mixed], - CrewShipWeaponSkins: [Schema.Types.Mixed], + CrewShipWeaponSkins: [upgradeSchema], + CrewShipSalvagedWeaponSkins: [upgradeSchema], - //NPC Crew and weapon + //RailJack Crew CrewMembers: [Schema.Types.Mixed], - CrewShipSalvagedWeaponSkins: [Schema.Types.Mixed], - CrewShipSalvagedWeapons: [Schema.Types.Mixed], //Complete Mission\Quests Missions: [missionSchema], QuestKeys: [questKeysSchema], ActiveQuest: { type: String, default: "" }, //item like DojoKey or Boss missions key - LevelKeys: [Schema.Types.Mixed], + LevelKeys: [typeCountSchema], //Active quests Quests: [Schema.Types.Mixed], @@ -1157,15 +1366,15 @@ const inventorySchema = new Schema( SpectreLoadouts: { type: [spectreLoadoutsSchema], default: undefined }, //New Quest Email - EmailItems: [TypeXPItemSchema], + EmailItems: [typeCountSchema], //Profile->Wishlist Wishlist: [String], //https://warframe.fandom.com/wiki/Alignment //like "Alignment": { "Wisdom": 9, "Alignment": 1 }, - Alignment: Schema.Types.Mixed, - AlignmentReplay: Schema.Types.Mixed, + Alignment: alignmentSchema, + AlignmentReplay: alignmentSchema, //https://warframe.fandom.com/wiki/Sortie CompletedSorties: [String], @@ -1175,10 +1384,10 @@ const inventorySchema = new Schema( Drones: [droneSchema], //Active profile ico - ActiveAvatarImageType: String, + ActiveAvatarImageType: { type: String, default: "/Lotus/Types/StoreItems/AvatarImages/AvatarImageDefault" }, // open location store like EidolonPlainsDiscoverable or OrbVallisCaveDiscoverable - DiscoveredMarkers: [Schema.Types.Mixed], + DiscoveredMarkers: [discoveredMarkerSchema], //Open location mission like "JobId" + "StageCompletions" CompletedJobs: [Schema.Types.Mixed], @@ -1191,7 +1400,7 @@ const inventorySchema = new Schema( ThemeSounds: String, //Daily LoginRewards - LoginMilestoneRewards: [String], + LoginMilestoneRewards: { type: [String], default: [] }, //You first Dialog with NPC or use new Item NodeIntrosCompleted: [String], @@ -1207,16 +1416,17 @@ const inventorySchema = new Schema( LibraryPersonalTarget: String, //Cephalon Simaris Entries Example:"TargetType"+"Scans"(1-10)+"Completed": true|false - LibraryPersonalProgress: [Schema.Types.Mixed], + LibraryPersonalProgress: { type: [libraryPersonalProgressSchema], default: [] }, //Cephalon Simaris Daily Task - LibraryAvailableDailyTaskInfo: libraryAvailableDailyTaskInfoSchema, + LibraryAvailableDailyTaskInfo: libraryDailyTaskInfoSchema, + LibraryActiveDailyTaskInfo: libraryDailyTaskInfoSchema, //https://warframe.fandom.com/wiki/Invasion InvasionChainProgress: [Schema.Types.Mixed], //CorpusLich or GrineerLich - NemesisAbandonedRewards: [String], - //CorpusLich\KuvaLich + NemesisAbandonedRewards: { type: [String], default: [] }, + Nemesis: nemesisSchema, NemesisHistory: [Schema.Types.Mixed], LastNemesisAllySpawnTime: Schema.Types.Mixed, @@ -1237,7 +1447,7 @@ const inventorySchema = new Schema( //https://warframe.fandom.com/wiki/Helminth InfestedFoundry: infestedFoundrySchema, - NextRefill: Schema.Types.Mixed, // Date, convert to IMongoDate + NextRefill: { type: Date, default: undefined }, //Purchase this new permanent skin from the Lotus customization options in Personal Quarters located in your Orbiter. //https://warframe.fandom.com/wiki/Lotus#The_New_War @@ -1266,16 +1476,16 @@ const inventorySchema = new Schema( RandomUpgradesIdentified: Number, BountyScore: Number, ChallengeInstanceStates: [Schema.Types.Mixed], - RecentVendorPurchases: [Schema.Types.Mixed], + RecentVendorPurchases: { type: [recentVendorPurchaseSchema], default: undefined }, Robotics: [Schema.Types.Mixed], UsedDailyDeals: [Schema.Types.Mixed], - CollectibleSeries: [Schema.Types.Mixed], + CollectibleSeries: { type: [collectibleEntrySchema], default: undefined }, HasResetAccount: { type: Boolean, default: false }, //Discount Coupon PendingCoupon: pendingCouponSchema, //Like BossAladV,BossCaptainVor come for you on missions % chance - DeathMarks: [String], + DeathMarks: { type: [String], default: [] }, //Zanuka Harvestable: Boolean, //Grustag three @@ -1284,7 +1494,26 @@ const inventorySchema = new Schema( EndlessXP: { type: [endlessXpProgressSchema], default: undefined }, DialogueHistory: dialogueHistorySchema, - CalendarProgress: calenderProgressSchema + CalendarProgress: calenderProgressSchema, + + SongChallenges: { type: [songChallengeSchema], default: undefined }, + + // Netracells + Deep Archimedea + EntratiVaultCountLastPeriod: { type: Number, default: undefined }, + EntratiVaultCountResetDate: { type: Date, default: undefined }, + EntratiLabConquestUnlocked: { type: Number, default: undefined }, + EntratiLabConquestHardModeStatus: { type: Number, default: undefined }, + EntratiLabConquestCacheScoreMission: { type: Number, default: undefined }, + EntratiLabConquestActiveFrameVariants: { type: [String], default: undefined }, + EchoesHexConquestUnlocked: { type: Number, default: undefined }, + EchoesHexConquestHardModeStatus: { type: Number, default: undefined }, + EchoesHexConquestCacheScoreMission: { type: Number, default: undefined }, + EchoesHexConquestActiveFrameVariants: { type: [String], default: undefined }, + EchoesHexConquestActiveStickers: { type: [String], default: undefined }, + + // G3 + Zanuka + BrandedSuits: { type: [Schema.Types.ObjectId], default: undefined }, + LockedWeaponGroup: { type: lockedWeaponGroupSchema, default: undefined } }, { timestamps: { createdAt: "Created", updatedAt: false } } ); @@ -1295,20 +1524,44 @@ inventorySchema.set("toJSON", { delete returnedObject.__v; delete returnedObject.accountOwnerId; - const inventoryDatabase = returnedObject as IInventoryDatabase; + const inventoryDatabase = returnedObject as Partial; const inventoryResponse = returnedObject as IInventoryClient; - inventoryResponse.TrainingDate = toMongoDate(inventoryDatabase.TrainingDate); - inventoryResponse.Created = toMongoDate(inventoryDatabase.Created); + if (inventoryDatabase.TrainingDate) { + inventoryResponse.TrainingDate = toMongoDate(inventoryDatabase.TrainingDate); + } + if (inventoryDatabase.Created) { + inventoryResponse.Created = toMongoDate(inventoryDatabase.Created); + } if (inventoryDatabase.GuildId) { inventoryResponse.GuildId = toOid(inventoryDatabase.GuildId); } if (inventoryDatabase.BlessingCooldown) { inventoryResponse.BlessingCooldown = toMongoDate(inventoryDatabase.BlessingCooldown); } + if (inventoryDatabase.NextRefill) { + inventoryResponse.NextRefill = toMongoDate(inventoryDatabase.NextRefill); + } + if (inventoryDatabase.EntratiVaultCountResetDate) { + inventoryResponse.EntratiVaultCountResetDate = toMongoDate(inventoryDatabase.EntratiVaultCountResetDate); + } + if (inventoryDatabase.BrandedSuits) { + inventoryResponse.BrandedSuits = inventoryDatabase.BrandedSuits.map(toOid); + } + if (inventoryDatabase.LockedWeaponGroup) { + inventoryResponse.LockedWeaponGroup = { + s: toOid(inventoryDatabase.LockedWeaponGroup.s), + l: inventoryDatabase.LockedWeaponGroup.l ? toOid(inventoryDatabase.LockedWeaponGroup.l) : undefined, + p: inventoryDatabase.LockedWeaponGroup.p ? toOid(inventoryDatabase.LockedWeaponGroup.p) : undefined, + m: inventoryDatabase.LockedWeaponGroup.m ? toOid(inventoryDatabase.LockedWeaponGroup.m) : undefined, + sn: inventoryDatabase.LockedWeaponGroup.sn ? toOid(inventoryDatabase.LockedWeaponGroup.sn) : undefined + }; + } } }); +inventorySchema.index({ accountOwnerId: 1 }, { unique: true }); + // type overwrites for subdocuments/subdocument arrays export type InventoryDocumentProps = { FlavourItems: Types.DocumentArray; @@ -1323,14 +1576,16 @@ export type InventoryDocumentProps = { WeaponSkins: Types.DocumentArray; QuestKeys: Types.DocumentArray; Drones: Types.DocumentArray; + CrewShipWeaponSkins: Types.DocumentArray; + CrewShipSalvagedWeaponsSkins: Types.DocumentArray; } & { [K in TEquipmentKey]: Types.DocumentArray }; -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-empty-object-type type InventoryModelType = Model; export const Inventory = model("Inventory", inventorySchema); -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export type TInventoryDatabaseDocument = Document & Omit< IInventoryDatabase & { diff --git a/src/models/inventoryModels/loadoutModel.ts b/src/models/inventoryModels/loadoutModel.ts index 8eba69c1..73343c8b 100644 --- a/src/models/inventoryModels/loadoutModel.ts +++ b/src/models/inventoryModels/loadoutModel.ts @@ -1,7 +1,7 @@ import { IOid } from "@/src/types/commonTypes"; import { IEquipmentSelection } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { ILoadoutConfigDatabase, ILoadoutDatabase } from "@/src/types/saveLoadoutTypes"; -import { Model, Schema, Types, model } from "mongoose"; +import { Document, Model, Schema, Types, model } from "mongoose"; const oidSchema = new Schema( { @@ -78,6 +78,8 @@ loadoutSchema.set("toJSON", { } }); +loadoutSchema.index({ loadoutOwnerId: 1 }, { unique: true }); + //create database typefor ILoadoutConfig type loadoutDocumentProps = { NORMAL: Types.DocumentArray; @@ -93,7 +95,19 @@ type loadoutDocumentProps = { DRIFTER: Types.DocumentArray; }; -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-empty-object-type type loadoutModelType = Model; export const Loadout = model("Loadout", loadoutSchema); + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export type TLoadoutDatabaseDocument = Document & + Omit< + ILoadoutDatabase & { + _id: Types.ObjectId; + } & { + __v: number; + }, + keyof loadoutDocumentProps + > & + loadoutDocumentProps; diff --git a/src/models/leaderboardModel.ts b/src/models/leaderboardModel.ts new file mode 100644 index 00000000..2db984d3 --- /dev/null +++ b/src/models/leaderboardModel.ts @@ -0,0 +1,27 @@ +import { Document, model, Schema, Types } from "mongoose"; +import { ILeaderboardEntryDatabase } from "../types/leaderboardTypes"; + +const leaderboardEntrySchema = new Schema( + { + leaderboard: { type: String, required: true }, + ownerId: { type: Schema.Types.ObjectId, required: true }, + displayName: { type: String, required: true }, + score: { type: Number, required: true }, + guildId: Schema.Types.ObjectId, + expiry: { type: Date, required: true }, + guildTier: Number + }, + { id: false } +); + +leaderboardEntrySchema.index({ leaderboard: 1 }); +leaderboardEntrySchema.index({ leaderboard: 1, ownerId: 1 }, { unique: true }); +leaderboardEntrySchema.index({ expiry: 1 }, { expireAfterSeconds: 0 }); // With this, MongoDB will automatically delete expired entries. + +export const Leaderboard = model("Leaderboard", leaderboardEntrySchema); + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export type TLeaderboardEntryDocument = Document & { + _id: Types.ObjectId; + __v: number; +} & ILeaderboardEntryDatabase; diff --git a/src/models/loginModel.ts b/src/models/loginModel.ts index eb3d1576..7b12c07a 100644 --- a/src/models/loginModel.ts +++ b/src/models/loginModel.ts @@ -20,8 +20,10 @@ const databaseAccountSchema = new Schema( ConsentNeeded: { type: Boolean, required: true }, TrackedSettings: { type: [String], default: [] }, Nonce: { type: Number, default: 0 }, - LastLoginDay: { type: Number }, - LatestEventMessageDate: { type: Date, default: 0 } + Dropped: Boolean, + LatestEventMessageDate: { type: Date, default: 0 }, + LastLoginRewardDate: { type: Number, default: 0 }, + LoginDays: { type: Number, default: 1 } }, opts ); diff --git a/src/models/personalRoomsModel.ts b/src/models/personalRoomsModel.ts index e54d1b1c..1c6a7c6d 100644 --- a/src/models/personalRoomsModel.ts +++ b/src/models/personalRoomsModel.ts @@ -12,7 +12,7 @@ import { } from "@/src/types/shipTypes"; import { Schema, model } from "mongoose"; -const pictureFrameInfoSchema = new Schema( +export const pictureFrameInfoSchema = new Schema( { Image: String, Filter: String, @@ -57,7 +57,7 @@ const roomSchema = new Schema( { Name: String, MaxCapacity: Number, - PlacedDecos: { type: [placedDecosSchema], default: undefined } + PlacedDecos: { type: [placedDecosSchema], default: [] } }, { _id: false } ); @@ -152,6 +152,8 @@ export const personalRoomsSchema = new Schema({ TailorShop: { type: tailorShopSchema, default: tailorShopDefault } }); +personalRoomsSchema.index({ personalRoomsOwnerId: 1 }, { unique: true }); + export const PersonalRooms = model( "PersonalRooms", personalRoomsSchema diff --git a/src/models/shipModel.ts b/src/models/shipModel.ts index 13e1ef53..5176defb 100644 --- a/src/models/shipModel.ts +++ b/src/models/shipModel.ts @@ -1,4 +1,4 @@ -import { Schema, model } from "mongoose"; +import { Document, Schema, Types, model } from "mongoose"; import { IShipDatabase } from "../types/shipTypes"; import { toOid } from "@/src/helpers/inventoryHelpers"; import { colorSchema } from "@/src/models/inventoryModels/inventoryModel"; @@ -47,3 +47,11 @@ shipSchema.set("toObject", { }); export const Ship = model("Ships", shipSchema); + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export type TShipDatabaseDocument = Document & + IShipDatabase & { + _id: Types.ObjectId; + } & { + __v: number; + }; diff --git a/src/models/statsModel.ts b/src/models/statsModel.ts index e11ca652..62e53c04 100644 --- a/src/models/statsModel.ts +++ b/src/models/statsModel.ts @@ -4,7 +4,7 @@ import { IEnemy, IMission, IScan, ITutorial, IAbility, IWeapon, IStatsDatabase, const abilitySchema = new Schema( { type: { type: String, required: true }, - used: Number + used: { type: Number, required: true } }, { _id: false } ); @@ -16,7 +16,8 @@ const enemySchema = new Schema( headshots: Number, kills: Number, assists: Number, - deaths: Number + deaths: Number, + captures: Number }, { _id: false } ); @@ -32,7 +33,7 @@ const missionSchema = new Schema( const scanSchema = new Schema( { type: { type: String, required: true }, - scans: Number + scans: { type: Number, required: true } }, { _id: false } ); @@ -91,7 +92,12 @@ const statsSchema = new Schema({ Deaths: Number, HealCount: Number, ReviveCount: Number, - Races: { type: Map, of: raceSchema, default: {} } + Races: { type: Map, of: raceSchema, default: {} }, + ZephyrScore: Number, + SentinelGameScore: Number, + CaliberChicksScore: Number, + OlliesCrashCourseScore: Number, + DojoObstacleScore: Number }); statsSchema.set("toJSON", { @@ -102,9 +108,11 @@ statsSchema.set("toJSON", { } }); +statsSchema.index({ accountOwnerId: 1 }, { unique: true }); + export const Stats = model("Stats", statsSchema); -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export type TStatsDatabaseDocument = Document & { _id: Types.ObjectId; __v: number; diff --git a/src/routes/api.ts b/src/routes/api.ts index 0a2c6716..81415dc6 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -1,20 +1,44 @@ import express from "express"; +import { abandonLibraryDailyTaskController } from "@/src/controllers/api/abandonLibraryDailyTaskController"; +import { abortDojoComponentController } from "@/src/controllers/api/abortDojoComponentController"; +import { abortDojoComponentDestructionController } from "@/src/controllers/api/abortDojoComponentDestructionController"; import { activateRandomModController } from "@/src/controllers/api/activateRandomModController"; import { addFriendImageController } from "@/src/controllers/api/addFriendImageController"; +import { addToAllianceController } from "@/src/controllers/api/addToAllianceController"; +import { addToGuildController } from "@/src/controllers/api/addToGuildController"; import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController"; import { archonFusionController } from "@/src/controllers/api/archonFusionController"; import { artifactsController } from "@/src/controllers/api/artifactsController"; +import { artifactTransmutationController } from "@/src/controllers/api/artifactTransmutationController"; +import { cancelGuildAdvertisementController } from "@/src/controllers/api/cancelGuildAdvertisementController"; import { changeDojoRootController } from "@/src/controllers/api/changeDojoRootController"; +import { changeGuildRankController } from "@/src/controllers/api/changeGuildRankController"; import { checkDailyMissionBonusController } from "@/src/controllers/api/checkDailyMissionBonusController"; import { claimCompletedRecipeController } from "@/src/controllers/api/claimCompletedRecipeController"; +import { claimLibraryDailyTaskRewardController } from "@/src/controllers/api/claimLibraryDailyTaskRewardController"; import { clearDialogueHistoryController } from "@/src/controllers/api/clearDialogueHistoryController"; +import { clearNewEpisodeRewardController } from "@/src/controllers/api/clearNewEpisodeRewardController"; import { completeRandomModChallengeController } from "@/src/controllers/api/completeRandomModChallengeController"; +import { confirmAllianceInvitationController } from "@/src/controllers/api/confirmAllianceInvitationController"; +import { confirmGuildInvitationGetController, confirmGuildInvitationPostController } from "@/src/controllers/api/confirmGuildInvitationController"; +import { contributeGuildClassController } from "@/src/controllers/api/contributeGuildClassController"; +import { contributeToDojoComponentController } from "@/src/controllers/api/contributeToDojoComponentController"; +import { contributeToVaultController } from "@/src/controllers/api/contributeToVaultController"; +import { createAllianceController } from "@/src/controllers/api/createAllianceController"; import { createGuildController } from "@/src/controllers/api/createGuildController"; import { creditsController } from "@/src/controllers/api/creditsController"; +import { customizeGuildRanksController } from "@/src/controllers/api/customizeGuildRanksController"; +import { customObstacleCourseLeaderboardController } from "@/src/controllers/api/customObstacleCourseLeaderboardController"; +import { declineAllianceInviteController } from "@/src/controllers/api/declineAllianceInviteController"; +import { declineGuildInviteController } from "@/src/controllers/api/declineGuildInviteController"; import { deleteSessionController } from "@/src/controllers/api/deleteSessionController"; +import { destroyDojoDecoController } from "@/src/controllers/api/destroyDojoDecoController"; +import { divvyAllianceVaultController } from "@/src/controllers/api/divvyAllianceVaultController"; +import { dojoComponentRushController } from "@/src/controllers/api/dojoComponentRushController"; import { dojoController } from "@/src/controllers/api/dojoController"; import { dronesController } from "@/src/controllers/api/dronesController"; import { endlessXpController } from "@/src/controllers/api/endlessXpController"; +import { entratiLabConquestModeController } from "@/src/controllers/api/entratiLabConquestModeController"; import { evolveWeaponController } from "@/src/controllers/api/evolveWeaponController"; import { findSessionsController } from "@/src/controllers/api/findSessionsController"; import { fishmongerController } from "@/src/controllers/api/fishmongerController"; @@ -24,6 +48,7 @@ import { genericUpdateController } from "@/src/controllers/api/genericUpdateCont import { getAllianceController } from "@/src/controllers/api/getAllianceController"; import { getDailyDealStockLevelsController } from "@/src/controllers/api/getDailyDealStockLevelsController"; import { getFriendsController } from "@/src/controllers/api/getFriendsController"; +import { getGuildContributionsController } from "@/src/controllers/api/getGuildContributionsController"; import { getGuildController } from "@/src/controllers/api/getGuildController"; import { getGuildDojoController } from "@/src/controllers/api/getGuildDojoController"; import { getGuildLogController } from "@/src/controllers/api/getGuildLogController"; @@ -32,6 +57,7 @@ import { getNewRewardSeedController } from "@/src/controllers/api/getNewRewardSe import { getShipController } from "@/src/controllers/api/getShipController"; import { getVendorInfoController } from "@/src/controllers/api/getVendorInfoController"; import { getVoidProjectionRewardsController } from "@/src/controllers/api/getVoidProjectionRewardsController"; +import { giftingController } from "@/src/controllers/api/giftingController"; import { gildWeaponController } from "@/src/controllers/api/gildWeaponController"; import { giveKeyChainTriggeredItemsController } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; import { giveKeyChainTriggeredMessageController } from "@/src/controllers/api/giveKeyChainTriggeredMessageController"; @@ -39,6 +65,7 @@ import { giveQuestKeyRewardController } from "@/src/controllers/api/giveQuestKey import { giveStartingGearController } from "@/src/controllers/api/giveStartingGearController"; import { guildTechController } from "@/src/controllers/api/guildTechController"; import { hostSessionController } from "@/src/controllers/api/hostSessionController"; +import { hubBlessingController } from "@/src/controllers/api/hubBlessingController"; import { hubController } from "@/src/controllers/api/hubController"; import { hubInstancesController } from "@/src/controllers/api/hubInstancesController"; import { inboxController } from "@/src/controllers/api/inboxController"; @@ -48,33 +75,48 @@ import { inventorySlotsController } from "@/src/controllers/api/inventorySlotsCo import { joinSessionController } from "@/src/controllers/api/joinSessionController"; import { loginController } from "@/src/controllers/api/loginController"; import { loginRewardsController } from "@/src/controllers/api/loginRewardsController"; +import { loginRewardsSelectionController } from "@/src/controllers/api/loginRewardsSelectionController"; import { logoutController } from "@/src/controllers/api/logoutController"; import { marketRecommendationsController } from "@/src/controllers/api/marketRecommendationsController"; +import { maturePetController } from "@/src/controllers/api/maturePetController"; import { missionInventoryUpdateController } from "@/src/controllers/api/missionInventoryUpdateController"; import { modularWeaponCraftingController } from "@/src/controllers/api/modularWeaponCraftingController"; import { modularWeaponSaleController } from "@/src/controllers/api/modularWeaponSaleController"; import { nameWeaponController } from "@/src/controllers/api/nameWeaponController"; +import { nemesisController } from "@/src/controllers/api/nemesisController"; +import { placeDecoInComponentController } from "@/src/controllers/api/placeDecoInComponentController"; import { playerSkillsController } from "@/src/controllers/api/playerSkillsController"; +import { postGuildAdvertisementController } from "@/src/controllers/api/postGuildAdvertisementController"; import { projectionManagerController } from "@/src/controllers/api/projectionManagerController"; import { purchaseController } from "@/src/controllers/api/purchaseController"; import { queueDojoComponentDestructionController } from "@/src/controllers/api/queueDojoComponentDestructionController"; +import { redeemPromoCodeController } from "@/src/controllers/api/redeemPromoCodeController"; +import { releasePetController } from "@/src/controllers/api/releasePetController"; +import { removeFromAllianceController } from "@/src/controllers/api/removeFromAllianceController"; +import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController"; import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController"; +import { retrievePetFromStasisController } from "@/src/controllers/api/retrievePetFromStasisController"; import { saveDialogueController } from "@/src/controllers/api/saveDialogueController"; import { saveLoadoutController } from "@/src/controllers/api/saveLoadout"; import { saveSettingsController } from "@/src/controllers/api/saveSettingsController"; +import { saveVaultAutoContributeController } from "@/src/controllers/api/saveVaultAutoContributeController"; import { sellController } from "@/src/controllers/api/sellController"; import { setActiveQuestController } from "@/src/controllers/api/setActiveQuestController"; import { setActiveShipController } from "@/src/controllers/api/setActiveShipController"; +import { setAllianceGuildPermissionsController } from "@/src/controllers/api/setAllianceGuildPermissionsController"; import { setBootLocationController } from "@/src/controllers/api/setBootLocationController"; import { setDojoComponentMessageController } from "@/src/controllers/api/setDojoComponentMessageController"; import { setEquippedInstrumentController } from "@/src/controllers/api/setEquippedInstrumentController"; +import { setGuildMotdController } from "@/src/controllers/api/setGuildMotdController"; import { setPlacedDecoInfoController } from "@/src/controllers/api/setPlacedDecoInfoController"; import { setShipCustomizationsController } from "@/src/controllers/api/setShipCustomizationsController"; import { setShipFavouriteLoadoutController } from "@/src/controllers/api/setShipFavouriteLoadoutController"; import { setSupportedSyndicateController } from "@/src/controllers/api/setSupportedSyndicateController"; import { setWeaponSkillTreeController } from "@/src/controllers/api/setWeaponSkillTreeController"; import { shipDecorationsController } from "@/src/controllers/api/shipDecorationsController"; +import { startCollectibleEntryController } from "@/src/controllers/api/startCollectibleEntryController"; import { startDojoRecipeController } from "@/src/controllers/api/startDojoRecipeController"; +import { startLibraryDailyTaskController } from "@/src/controllers/api/startLibraryDailyTaskController"; import { startLibraryPersonalTargetController } from "@/src/controllers/api/startLibraryPersonalTargetController"; import { startRecipeController } from "@/src/controllers/api/startRecipeController"; import { stepSequencersController } from "@/src/controllers/api/stepSequencersController"; @@ -82,25 +124,41 @@ import { surveysController } from "@/src/controllers/api/surveysController"; import { syndicateSacrificeController } from "@/src/controllers/api/syndicateSacrificeController"; import { syndicateStandingBonusController } from "@/src/controllers/api/syndicateStandingBonusController"; import { tauntHistoryController } from "@/src/controllers/api/tauntHistoryController"; +import { tradingController } from "@/src/controllers/api/tradingController"; import { trainingResultController } from "@/src/controllers/api/trainingResultController"; import { unlockShipFeatureController } from "@/src/controllers/api/unlockShipFeatureController"; +import { updateAlignmentController } from "@/src/controllers/api/updateAlignmentController"; import { updateChallengeProgressController } from "@/src/controllers/api/updateChallengeProgressController"; import { updateQuestController } from "@/src/controllers/api/updateQuestController"; import { updateSessionGetController, updateSessionPostController } from "@/src/controllers/api/updateSessionController"; +import { updateSongChallengeController } from "@/src/controllers/api/updateSongChallengeController"; import { updateThemeController } from "@/src/controllers/api/updateThemeController"; import { upgradesController } from "@/src/controllers/api/upgradesController"; +import { valenceSwapController } from "@/src/controllers/api/valenceSwapController"; +import { wishlistController } from "@/src/controllers/api/wishlistController"; const apiRouter = express.Router(); // get +apiRouter.get("/abandonLibraryDailyTask.php", abandonLibraryDailyTaskController); +apiRouter.get("/abortDojoComponentDestruction.php", abortDojoComponentDestructionController); +apiRouter.get("/cancelGuildAdvertisement.php", cancelGuildAdvertisementController); +apiRouter.get("/changeGuildRank.php", changeGuildRankController); apiRouter.get("/checkDailyMissionBonus.php", checkDailyMissionBonusController); +apiRouter.get("/claimLibraryDailyTaskReward.php", claimLibraryDailyTaskRewardController); +apiRouter.get("/confirmAllianceInvitation.php", confirmAllianceInvitationController); +apiRouter.get("/confirmGuildInvitation.php", confirmGuildInvitationGetController); apiRouter.get("/credits.php", creditsController); +apiRouter.get("/declineAllianceInvite.php", declineAllianceInviteController); +apiRouter.get("/declineGuildInvite.php", declineGuildInviteController); apiRouter.get("/deleteSession.php", deleteSessionController); +apiRouter.get("/divvyAllianceVault.php", divvyAllianceVaultController); apiRouter.get("/dojo", dojoController); apiRouter.get("/drones.php", dronesController); apiRouter.get("/getDailyDealStockLevels.php", getDailyDealStockLevelsController); apiRouter.get("/getFriends.php", getFriendsController); apiRouter.get("/getGuild.php", getGuildController); +apiRouter.get("/getGuildContributions.php", getGuildContributionsController); apiRouter.get("/getGuildDojo.php", getGuildDojoController); apiRouter.get("/getGuildLog.php", getGuildLogController); apiRouter.get("/getIgnoredUsers.php", getIgnoredUsersController); @@ -117,26 +175,47 @@ apiRouter.get("/marketRecommendations.php", marketRecommendationsController); apiRouter.get("/marketSearchRecommendations.php", marketRecommendationsController); apiRouter.get("/modularWeaponSale.php", modularWeaponSaleController); apiRouter.get("/queueDojoComponentDestruction.php", queueDojoComponentDestructionController); +apiRouter.get("/removeFromAlliance.php", removeFromAllianceController); apiRouter.get("/setActiveQuest.php", setActiveQuestController); apiRouter.get("/setActiveShip.php", setActiveShipController); +apiRouter.get("/setAllianceGuildPermissions.php", setAllianceGuildPermissionsController); apiRouter.get("/setBootLocation.php", setBootLocationController); +apiRouter.get("/setGuildMotd.php", setGuildMotdController); apiRouter.get("/setSupportedSyndicate.php", setSupportedSyndicateController); +apiRouter.get("/startLibraryDailyTask.php", startLibraryDailyTaskController); apiRouter.get("/startLibraryPersonalTarget.php", startLibraryPersonalTargetController); apiRouter.get("/surveys.php", surveysController); +apiRouter.get("/trading.php", tradingController); apiRouter.get("/updateSession.php", updateSessionGetController); // post +apiRouter.post("/abortDojoComponent.php", abortDojoComponentController); apiRouter.post("/activateRandomMod.php", activateRandomModController); apiRouter.post("/addFriendImage.php", addFriendImageController); +apiRouter.post("/addToAlliance.php", addToAllianceController); +apiRouter.post("/addToGuild.php", addToGuildController); apiRouter.post("/arcaneCommon.php", arcaneCommonController); apiRouter.post("/archonFusion.php", archonFusionController); apiRouter.post("/artifacts.php", artifactsController); +apiRouter.post("/artifactTransmutation.php", artifactTransmutationController); apiRouter.post("/changeDojoRoot.php", changeDojoRootController); apiRouter.post("/claimCompletedRecipe.php", claimCompletedRecipeController); apiRouter.post("/clearDialogueHistory.php", clearDialogueHistoryController); +apiRouter.post("/clearNewEpisodeReward.php", clearNewEpisodeRewardController); apiRouter.post("/completeRandomModChallenge.php", completeRandomModChallengeController); +apiRouter.post("/confirmGuildInvitation.php", confirmGuildInvitationPostController); +apiRouter.post("/contributeGuildClass.php", contributeGuildClassController); +apiRouter.post("/contributeToDojoComponent.php", contributeToDojoComponentController); +apiRouter.post("/contributeToVault.php", contributeToVaultController); +apiRouter.post("/createAlliance.php", createAllianceController); apiRouter.post("/createGuild.php", createGuildController); +apiRouter.post("/customizeGuildRanks.php", customizeGuildRanksController); +apiRouter.post("/customObstacleCourseLeaderboard.php", customObstacleCourseLeaderboardController); +apiRouter.post("/destroyDojoDeco.php", destroyDojoDecoController); +apiRouter.post("/dojoComponentRush.php", dojoComponentRushController); +apiRouter.post("/drones.php", dronesController); apiRouter.post("/endlessXp.php", endlessXpController); +apiRouter.post("/entratiLabConquestMode.php", entratiLabConquestModeController); apiRouter.post("/evolveWeapon.php", evolveWeaponController); apiRouter.post("/findSessions.php", findSessionsController); apiRouter.post("/fishmonger.php", fishmongerController); @@ -144,7 +223,10 @@ apiRouter.post("/focus.php", focusController); apiRouter.post("/fusionTreasures.php", fusionTreasuresController); apiRouter.post("/genericUpdate.php", genericUpdateController); apiRouter.post("/getAlliance.php", getAllianceController); +apiRouter.post("/getFriends.php", getFriendsController); +apiRouter.post("/getGuildDojo.php", getGuildDojoController); apiRouter.post("/getVoidProjectionRewards.php", getVoidProjectionRewardsController); +apiRouter.post("/gifting.php", giftingController); apiRouter.post("/gildWeapon.php", gildWeaponController); apiRouter.post("/giveKeyChainTriggeredItems.php", giveKeyChainTriggeredItemsController); apiRouter.post("/giveKeyChainTriggeredMessage.php", giveKeyChainTriggeredMessageController); @@ -152,28 +234,42 @@ apiRouter.post("/giveQuestKeyReward.php", giveQuestKeyRewardController); apiRouter.post("/giveStartingGear.php", giveStartingGearController); apiRouter.post("/guildTech.php", guildTechController); apiRouter.post("/hostSession.php", hostSessionController); +apiRouter.post("/hubBlessing.php", hubBlessingController); apiRouter.post("/infestedFoundry.php", infestedFoundryController); apiRouter.post("/inventorySlots.php", inventorySlotsController); apiRouter.post("/joinSession.php", joinSessionController); apiRouter.post("/login.php", loginController); +apiRouter.post("/loginRewardsSelection.php", loginRewardsSelectionController); +apiRouter.post("/maturePet.php", maturePetController); apiRouter.post("/missionInventoryUpdate.php", missionInventoryUpdateController); apiRouter.post("/modularWeaponCrafting.php", modularWeaponCraftingController); +apiRouter.post("/modularWeaponSale.php", modularWeaponSaleController); apiRouter.post("/nameWeapon.php", nameWeaponController); +apiRouter.post("/nemesis.php", nemesisController); +apiRouter.post("/placeDecoInComponent.php", placeDecoInComponentController); apiRouter.post("/playerSkills.php", playerSkillsController); +apiRouter.post("/postGuildAdvertisement.php", postGuildAdvertisementController); apiRouter.post("/projectionManager.php", projectionManagerController); apiRouter.post("/purchase.php", purchaseController); +apiRouter.post("/redeemPromoCode.php", redeemPromoCodeController); +apiRouter.post("/releasePet.php", releasePetController); +apiRouter.post("/removeFromGuild.php", removeFromGuildController); apiRouter.post("/rerollRandomMod.php", rerollRandomModController); +apiRouter.post("/retrievePetFromStasis.php", retrievePetFromStasisController); apiRouter.post("/saveDialogue.php", saveDialogueController); apiRouter.post("/saveLoadout.php", saveLoadoutController); apiRouter.post("/saveSettings.php", saveSettingsController); +apiRouter.post("/saveVaultAutoContribute.php", saveVaultAutoContributeController); apiRouter.post("/sell.php", sellController); apiRouter.post("/setDojoComponentMessage.php", setDojoComponentMessageController); apiRouter.post("/setEquippedInstrument.php", setEquippedInstrumentController); +apiRouter.post("/setGuildMotd.php", setGuildMotdController); apiRouter.post("/setPlacedDecoInfo.php", setPlacedDecoInfoController); apiRouter.post("/setShipCustomizations.php", setShipCustomizationsController); apiRouter.post("/setShipFavouriteLoadout.php", setShipFavouriteLoadoutController); apiRouter.post("/setWeaponSkillTree.php", setWeaponSkillTreeController); apiRouter.post("/shipDecorations.php", shipDecorationsController); +apiRouter.post("/startCollectibleEntry.php", startCollectibleEntryController); apiRouter.post("/startDojoRecipe.php", startDojoRecipeController); apiRouter.post("/startRecipe.php", startRecipeController); apiRouter.post("/stepSequencers.php", stepSequencersController); @@ -182,11 +278,15 @@ apiRouter.post("/syndicateStandingBonus.php", syndicateStandingBonusController); apiRouter.post("/tauntHistory.php", tauntHistoryController); apiRouter.post("/trainingResult.php", trainingResultController); apiRouter.post("/unlockShipFeature.php", unlockShipFeatureController); +apiRouter.post("/updateAlignment.php", updateAlignmentController); apiRouter.post("/updateChallengeProgress.php", updateChallengeProgressController); apiRouter.post("/updateNodeIntros.php", genericUpdateController); apiRouter.post("/updateQuest.php", updateQuestController); apiRouter.post("/updateSession.php", updateSessionPostController); +apiRouter.post("/updateSongChallenge.php", updateSongChallengeController); apiRouter.post("/updateTheme.php", updateThemeController); apiRouter.post("/upgrades.php", upgradesController); +apiRouter.post("/valenceSwap.php", valenceSwapController); +apiRouter.post("/wishlist.php", wishlistController); export { apiRouter }; diff --git a/src/routes/custom.ts b/src/routes/custom.ts index a69afc1c..0834f90b 100644 --- a/src/routes/custom.ts +++ b/src/routes/custom.ts @@ -5,13 +5,19 @@ import { getItemListsController } from "@/src/controllers/custom/getItemListsCon import { pushArchonCrystalUpgradeController } from "@/src/controllers/custom/pushArchonCrystalUpgradeController"; import { popArchonCrystalUpgradeController } from "@/src/controllers/custom/popArchonCrystalUpgradeController"; import { deleteAccountController } from "@/src/controllers/custom/deleteAccountController"; +import { getNameController } from "@/src/controllers/custom/getNameController"; +import { getAccountInfoController } from "@/src/controllers/custom/getAccountInfoController"; import { renameAccountController } from "@/src/controllers/custom/renameAccountController"; +import { ircDroppedController } from "@/src/controllers/custom/ircDroppedController"; +import { unlockAllIntrinsicsController } from "@/src/controllers/custom/unlockAllIntrinsicsController"; import { createAccountController } from "@/src/controllers/custom/createAccountController"; import { createMessageController } from "@/src/controllers/custom/createMessageController"; -import { addCurrencyController } from "../controllers/custom/addCurrencyController"; +import { addCurrencyController } from "@/src/controllers/custom/addCurrencyController"; import { addItemsController } from "@/src/controllers/custom/addItemsController"; +import { addModularEquipmentController } from "@/src/controllers/custom/addModularEquipmentController"; import { addXpController } from "@/src/controllers/custom/addXpController"; +import { gildEquipmentController } from "@/src/controllers/custom/gildEquipmentController"; import { importController } from "@/src/controllers/custom/importController"; import { getConfigDataController } from "@/src/controllers/custom/getConfigDataController"; @@ -25,13 +31,19 @@ customRouter.get("/getItemLists", getItemListsController); customRouter.get("/pushArchonCrystalUpgrade", pushArchonCrystalUpgradeController); customRouter.get("/popArchonCrystalUpgrade", popArchonCrystalUpgradeController); customRouter.get("/deleteAccount", deleteAccountController); +customRouter.get("/getName", getNameController); +customRouter.get("/getAccountInfo", getAccountInfoController); customRouter.get("/renameAccount", renameAccountController); +customRouter.get("/ircDropped", ircDroppedController); +customRouter.get("/unlockAllIntrinsics", unlockAllIntrinsicsController); customRouter.post("/createAccount", createAccountController); customRouter.post("/createMessage", createMessageController); customRouter.post("/addCurrency", addCurrencyController); customRouter.post("/addItems", addItemsController); +customRouter.post("/addModularEquipment", addModularEquipmentController); customRouter.post("/addXp", addXpController); +customRouter.post("/gildEquipment", gildEquipmentController); customRouter.post("/import", importController); customRouter.post("/manageQuests", manageQuestsController); diff --git a/src/routes/dynamic.ts b/src/routes/dynamic.ts index 0e808d48..fb24fe58 100644 --- a/src/routes/dynamic.ts +++ b/src/routes/dynamic.ts @@ -1,10 +1,14 @@ -import { aggregateSessionsController } from "@/src/controllers/dynamic/aggregateSessionsController"; -import { worldStateController } from "@/src/controllers/dynamic/worldStateController"; import express from "express"; +import { aggregateSessionsController } from "@/src/controllers/dynamic/aggregateSessionsController"; +import { getGuildAdsController } from "@/src/controllers/dynamic/getGuildAdsController"; +import { getProfileViewingDataController } from "@/src/controllers/dynamic/getProfileViewingDataController"; +import { worldStateController } from "@/src/controllers/dynamic/worldStateController"; const dynamicController = express.Router(); -dynamicController.get("/worldState.php", worldStateController); dynamicController.get("/aggregateSessions.php", aggregateSessionsController); +dynamicController.get("/getGuildAds.php", getGuildAdsController); +dynamicController.get("/getProfileViewingData.php", getProfileViewingDataController); +dynamicController.get("/worldState.php", worldStateController); export { dynamicController }; diff --git a/src/routes/stats.ts b/src/routes/stats.ts index 59290675..11705259 100644 --- a/src/routes/stats.ts +++ b/src/routes/stats.ts @@ -1,11 +1,12 @@ -import { viewController } from "../controllers/stats/viewController"; -import { uploadController } from "@/src/controllers/stats/uploadController"; - import express from "express"; +import { viewController } from "@/src/controllers/stats/viewController"; +import { uploadController } from "@/src/controllers/stats/uploadController"; +import { leaderboardController } from "@/src/controllers/stats/leaderboardController"; const statsRouter = express.Router(); statsRouter.get("/view.php", viewController); statsRouter.post("/upload.php", uploadController); +statsRouter.post("/leaderboardWeekly.php", leaderboardController); export { statsRouter }; diff --git a/src/routes/webui.ts b/src/routes/webui.ts index 48f9f2fd..02224903 100644 --- a/src/routes/webui.ts +++ b/src/routes/webui.ts @@ -1,9 +1,8 @@ import express from "express"; import path from "path"; +import { repoDir, rootDir } from "@/src/helpers/pathHelper"; const webuiRouter = express.Router(); -const rootDir = path.join(__dirname, "../.."); -const repoDir = path.basename(rootDir) == "build" ? path.join(rootDir, "..") : rootDir; // Redirect / to /webui/ webuiRouter.get("/", (_req, res) => { @@ -31,6 +30,9 @@ webuiRouter.get("/webui/mods", (_req, res) => { webuiRouter.get("/webui/settings", (_req, res) => { res.sendFile(path.join(rootDir, "static/webui/index.html")); }); +webuiRouter.get("/webui/quests", (_req, res) => { + res.sendFile(path.join(rootDir, "static/webui/index.html")); +}); webuiRouter.get("/webui/cheats", (_req, res) => { res.sendFile(path.join(rootDir, "static/webui/index.html")); }); diff --git a/src/services/buildConfigService.ts b/src/services/buildConfigService.ts index dda9b909..007b1bed 100644 --- a/src/services/buildConfigService.ts +++ b/src/services/buildConfigService.ts @@ -1,5 +1,6 @@ import path from "path"; import fs from "fs"; +import { repoDir } from "@/src/helpers/pathHelper"; interface IBuildConfig { version: string; @@ -13,8 +14,6 @@ export const buildConfig: IBuildConfig = { matchmakingBuildId: "" }; -const rootDir = path.join(__dirname, "../.."); -const repoDir = path.basename(rootDir) == "build" ? path.join(rootDir, "..") : rootDir; const buildConfigPath = path.join(repoDir, "static/data/buildConfig.json"); if (fs.existsSync(buildConfigPath)) { Object.assign(buildConfig, JSON.parse(fs.readFileSync(buildConfigPath, "utf-8")) as IBuildConfig); diff --git a/src/services/configService.ts b/src/services/configService.ts index e1b9c984..04c47d36 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -1,42 +1,19 @@ -import path from "path"; import fs from "fs"; -import fsPromises from "fs/promises"; -import { logger } from "@/src/utils/logger"; - -const rootDir = path.join(__dirname, "../.."); -const repoDir = path.basename(rootDir) == "build" ? path.join(rootDir, "..") : rootDir; -const configPath = path.join(repoDir, "config.json"); -export const config = JSON.parse(fs.readFileSync(configPath, "utf-8")) as IConfig; - -let amnesia = false; -fs.watchFile(configPath, () => { - if (amnesia) { - amnesia = false; - } else { - logger.info("Detected a change to config.json, reloading its contents."); - - // Set all values to undefined now so if the new config.json omits some fields that were previously present, it's correct in-memory. - for (const key of Object.keys(config)) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - (config as any)[key] = undefined; - } - - Object.assign(config, JSON.parse(fs.readFileSync(configPath, "utf-8"))); - validateConfig(); - } -}); +import path from "path"; +import { repoDir } from "@/src/helpers/pathHelper"; interface IConfig { mongodbUrl: string; - logger: ILoggerConfig; + logger: { + files: boolean; + level: string; // "fatal" | "error" | "warn" | "info" | "http" | "debug" | "trace"; + }; myAddress: string; httpPort?: number; httpsPort?: number; myIrcAddresses?: string[]; - platformCDNs?: string[]; - hubAddress?: string; NRS?: string[]; - administratorNames?: string[] | string; + administratorNames?: string[]; autoCreateAccount?: boolean; skipTutorial?: boolean; skipAllDialogue?: boolean; @@ -57,24 +34,42 @@ interface IConfig { unlockExilusEverywhere?: boolean; unlockArcanesEverywhere?: boolean; noDailyStandingLimits?: boolean; + noArgonCrystalDecay?: boolean; + noMasteryRankUpCooldown?: boolean; + noVendorPurchaseLimits?: boolean; + instantResourceExtractorDrones?: boolean; + noDojoRoomBuildStage?: boolean; + fastDojoRoomDestruction?: boolean; + noDojoResearchCosts?: boolean; + noDojoResearchTime?: boolean; + fastClanAscension?: boolean; spoofMasteryRank?: number; + worldState?: { + creditBoost?: boolean; + affinityBoost?: boolean; + resourceBoost?: boolean; + starDays?: boolean; + lockTime?: number; + }; } -interface ILoggerConfig { - files: boolean; - level: string; // "fatal" | "error" | "warn" | "info" | "http" | "debug" | "trace"; -} +export const configPath = path.join(repoDir, "config.json"); -export const updateConfig = async (data: string): Promise => { - amnesia = true; - await fsPromises.writeFile(configPath, data); - Object.assign(config, JSON.parse(data)); +export const config: IConfig = { + mongodbUrl: "mongodb://127.0.0.1:27017/openWF", + logger: { + files: true, + level: "trace" + }, + myAddress: "localhost" }; -export const validateConfig = (): void => { - if (typeof config.administratorNames == "string") { - logger.warn( - `"administratorNames" should be an array; please add square brackets: ["${config.administratorNames}"]` - ); +export const loadConfig = (): void => { + // Set all values to undefined now so if the new config.json omits some fields that were previously present, it's correct in-memory. + for (const key of Object.keys(config)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + (config as any)[key] = undefined; } + + Object.assign(config, JSON.parse(fs.readFileSync(configPath, "utf-8"))); }; diff --git a/src/services/configWatcherService.ts b/src/services/configWatcherService.ts new file mode 100644 index 00000000..e8584785 --- /dev/null +++ b/src/services/configWatcherService.ts @@ -0,0 +1,39 @@ +import fs from "fs"; +import fsPromises from "fs/promises"; +import { logger } from "../utils/logger"; +import { config, configPath, loadConfig } from "./configService"; + +let amnesia = false; +fs.watchFile(configPath, () => { + if (amnesia) { + amnesia = false; + } else { + logger.info("Detected a change to config.json, reloading its contents."); + try { + loadConfig(); + } catch (e) { + logger.error("Failed to reload config.json. Did you delete it?! Execution cannot continue."); + process.exit(1); + } + validateConfig(); + } +}); + +export const validateConfig = (): void => { + if (typeof config.administratorNames == "string") { + logger.info(`Updating config.json to make administratorNames an array.`); + config.administratorNames = [config.administratorNames]; + void saveConfig(); + } +}; + +export const updateConfig = async (data: string): Promise => { + amnesia = true; + await fsPromises.writeFile(configPath, data); + Object.assign(config, JSON.parse(data)); +}; + +export const saveConfig = async (): Promise => { + amnesia = true; + await fsPromises.writeFile(configPath, JSON.stringify(config, null, 2)); +}; diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 210bc1a6..6388acef 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -1,11 +1,35 @@ import { Request } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { getInventory } from "@/src/services/inventoryService"; -import { Guild } from "@/src/models/guildModel"; +import { Alliance, AllianceMember, Guild, GuildAd, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; -import { IDojoClient, IDojoComponentClient, IGuildDatabase } from "@/src/types/guildTypes"; -import { Document, Types } from "mongoose"; +import { + GuildPermission, + IAllianceClient, + IAllianceDatabase, + IAllianceMemberClient, + IDojoClient, + IDojoComponentClient, + IDojoComponentDatabase, + IDojoContributable, + IDojoDecoClient, + IGuildClient, + IGuildMemberClient, + IGuildMemberDatabase, + IGuildVault, + ITechProjectDatabase +} from "@/src/types/guildTypes"; import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; +import { Types } from "mongoose"; +import { ExportDojoRecipes, ExportResources, IDojoBuild, IDojoResearch } from "warframe-public-export-plus"; +import { logger } from "../utils/logger"; +import { config } from "./configService"; +import { Account } from "../models/loginModel"; +import { getRandomInt } from "./rngService"; +import { Inbox } from "../models/inboxModel"; +import { IFusionTreasure, ITypeCount } from "../types/inventoryTypes/inventoryTypes"; +import { IInventoryChanges } from "../types/purchaseTypes"; +import { parallelForeach } from "../utils/async-utils"; export const getGuildForRequest = async (req: Request): Promise => { const accountId = await getAccountIdForRequest(req); @@ -21,53 +45,627 @@ export const getGuildForRequestEx = async ( if (!inventory.GuildId || inventory.GuildId.toString() != guildId) { throw new Error("Account is not in the guild that it has sent a request for"); } - const guild = await Guild.findOne({ _id: guildId }); + const guild = await Guild.findById(guildId); if (!guild) { throw new Error("Account thinks it is in a guild that doesn't exist"); } return guild; }; -export const getDojoClient = (guild: TGuildDatabaseDocument, status: number): IDojoClient => { +export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: string): Promise => { + const guildMembers = await GuildMember.find({ guildId: guild._id }); + + const members: IGuildMemberClient[] = []; + let missingEntry = true; + for (const guildMember of guildMembers) { + const member: IGuildMemberClient = { + _id: toOid(guildMember.accountId), + Rank: guildMember.rank, + Status: guildMember.status, + Note: guildMember.RequestMsg, + RequestExpiry: guildMember.RequestExpiry ? toMongoDate(guildMember.RequestExpiry) : undefined + }; + if (guildMember.accountId.equals(accountId)) { + missingEntry = false; + } else { + member.DisplayName = (await Account.findById(guildMember.accountId, "DisplayName"))!.DisplayName; + await fillInInventoryDataForGuildMember(member); + } + members.push(member); + } + if (missingEntry) { + // Handle clans created prior to creation of the GuildMember model. + await GuildMember.insertOne({ + accountId: accountId, + guildId: guild._id, + status: 0, + rank: 0 + }); + members.push({ + _id: { $oid: accountId }, + Status: 0, + Rank: 0 + }); + } + + return { + _id: toOid(guild._id), + Name: guild.Name, + MOTD: guild.MOTD, + LongMOTD: guild.LongMOTD, + Members: members, + Ranks: guild.Ranks, + Tier: guild.Tier, + Vault: getGuildVault(guild), + ActiveDojoColorResearch: guild.ActiveDojoColorResearch, + Class: guild.Class, + XP: guild.XP, + IsContributor: !!guild.CeremonyContributors?.find(x => x.equals(accountId)), + NumContributors: guild.CeremonyContributors?.length ?? 0, + CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined, + AutoContributeFromVault: guild.AutoContributeFromVault, + AllianceId: guild.AllianceId ? toOid(guild.AllianceId) : undefined + }; +}; + +export const getGuildVault = (guild: TGuildDatabaseDocument): IGuildVault => { + return { + DojoRefundRegularCredits: guild.VaultRegularCredits, + DojoRefundMiscItems: guild.VaultMiscItems, + DojoRefundPremiumCredits: guild.VaultPremiumCredits, + ShipDecorations: guild.VaultShipDecorations, + FusionTreasures: guild.VaultFusionTreasures + }; +}; + +export const getDojoClient = async ( + guild: TGuildDatabaseDocument, + status: number, + componentId: Types.ObjectId | string | undefined = undefined +): Promise => { const dojo: IDojoClient = { _id: { $oid: guild._id.toString() }, Name: guild.Name, - Tier: 1, + Tier: guild.Tier, + GuildEmblem: guild.Emblem, + TradeTax: guild.TradeTax, + NumContributors: guild.CeremonyContributors?.length ?? 0, + CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined, FixedContributions: true, DojoRevision: 1, + Vault: getGuildVault(guild), RevisionTime: Math.round(Date.now() / 1000), Energy: guild.DojoEnergy, Capacity: guild.DojoCapacity, DojoRequestStatus: status, DojoComponents: [] }; - guild.DojoComponents!.forEach(dojoComponent => { - const clientComponent: IDojoComponentClient = { - id: toOid(dojoComponent._id), - pf: dojoComponent.pf, - ppf: dojoComponent.ppf, - Name: dojoComponent.Name, - Message: dojoComponent.Message, - DecoCapacity: 600 - }; - if (dojoComponent.pi) { - clientComponent.pi = toOid(dojoComponent.pi); - clientComponent.op = dojoComponent.op!; - clientComponent.pp = dojoComponent.pp!; + const roomsToRemove: Types.ObjectId[] = []; + let needSave = false; + for (const dojoComponent of guild.DojoComponents) { + if (!componentId || dojoComponent._id.equals(componentId)) { + const clientComponent: IDojoComponentClient = { + id: toOid(dojoComponent._id), + pf: dojoComponent.pf, + ppf: dojoComponent.ppf, + Name: dojoComponent.Name, + Message: dojoComponent.Message, + DecoCapacity: dojoComponent.DecoCapacity ?? 600 + }; + if (dojoComponent.pi) { + clientComponent.pi = toOid(dojoComponent.pi); + clientComponent.op = dojoComponent.op!; + clientComponent.pp = dojoComponent.pp!; + } + if (dojoComponent.CompletionTime) { + clientComponent.CompletionTime = toMongoDate(dojoComponent.CompletionTime); + if (dojoComponent.CompletionLogPending && Date.now() >= dojoComponent.CompletionTime.getTime()) { + const entry = guild.RoomChanges?.find(x => x.componentId.equals(dojoComponent._id)); + if (entry) { + dojoComponent.CompletionLogPending = undefined; + entry.entryType = 1; + needSave = true; + } + + let newTier: number | undefined; + switch (dojoComponent.pf) { + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksShadow.level": + newTier = 2; + break; + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksStorm.level": + newTier = 3; + break; + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksMountain.level": + newTier = 4; + break; + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksMoon.level": + newTier = 5; + break; + } + if (newTier) { + logger.debug(`clan finished building barracks, updating to tier ${newTier}`); + await setGuildTier(guild, newTier); + needSave = true; + } + } + if (dojoComponent.DestructionTime) { + if (Date.now() >= dojoComponent.DestructionTime.getTime()) { + roomsToRemove.push(dojoComponent._id); + continue; + } + clientComponent.DestructionTime = toMongoDate(dojoComponent.DestructionTime); + } + } else { + clientComponent.RegularCredits = dojoComponent.RegularCredits; + clientComponent.MiscItems = dojoComponent.MiscItems; + } + if (dojoComponent.Decos) { + clientComponent.Decos = []; + for (const deco of dojoComponent.Decos) { + const clientDeco: IDojoDecoClient = { + id: toOid(deco._id), + Type: deco.Type, + Pos: deco.Pos, + Rot: deco.Rot, + Name: deco.Name, + Sockets: deco.Sockets, + PictureFrameInfo: deco.PictureFrameInfo + }; + if (deco.CompletionTime) { + clientDeco.CompletionTime = toMongoDate(deco.CompletionTime); + } else { + clientDeco.RegularCredits = deco.RegularCredits; + clientDeco.MiscItems = deco.MiscItems; + } + clientComponent.Decos.push(clientDeco); + } + } + dojo.DojoComponents.push(clientComponent); } - if (dojoComponent.CompletionTime) { - clientComponent.CompletionTime = toMongoDate(dojoComponent.CompletionTime); + } + if (roomsToRemove.length) { + logger.debug(`removing now-destroyed rooms`, roomsToRemove); + for (const id of roomsToRemove) { + await removeDojoRoom(guild, id); } - dojo.DojoComponents.push(clientComponent); - }); + needSave = true; + } + if (needSave) { + await guild.save(); + } + dojo.Tier = guild.Tier; return dojo; }; -// eslint-disable-next-line @typescript-eslint/ban-types -export type TGuildDatabaseDocument = Document & - IGuildDatabase & - Required<{ - _id: Types.ObjectId; - }> & { - __v: number; +const guildTierScalingFactors = [0.01, 0.03, 0.1, 0.3, 1]; +export const scaleRequiredCount = (tier: number, count: number): number => { + return Math.max(1, Math.trunc(count * guildTierScalingFactors[tier - 1])); +}; + +export const removeDojoRoom = async ( + guild: TGuildDatabaseDocument, + componentId: Types.ObjectId | string +): Promise => { + const component = guild.DojoComponents.splice( + guild.DojoComponents.findIndex(x => x._id.equals(componentId)), + 1 + )[0]; + const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf); + if (meta) { + guild.DojoCapacity -= meta.capacity; + guild.DojoEnergy -= meta.energy; + } + moveResourcesToVault(guild, component); + component.Decos?.forEach(deco => moveResourcesToVault(guild, deco)); + + if (guild.RoomChanges) { + const index = guild.RoomChanges.findIndex(x => x.componentId.equals(component._id)); + if (index != -1) { + guild.RoomChanges.splice(index, 1); + } + } + + switch (component.pf) { + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksShadow.level": + await setGuildTier(guild, 1); + break; + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksStorm.level": + await setGuildTier(guild, 2); + break; + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksMountain.level": + await setGuildTier(guild, 3); + break; + case "/Lotus/Levels/ClanDojo/ClanDojoBarracksMoon.level": + await setGuildTier(guild, 4); + break; + } +}; + +export const removeDojoDeco = ( + guild: TGuildDatabaseDocument, + componentId: Types.ObjectId | string, + decoId: Types.ObjectId | string +): void => { + const component = guild.DojoComponents.id(componentId)!; + const deco = component.Decos!.splice( + component.Decos!.findIndex(x => x._id.equals(decoId)), + 1 + )[0]; + const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type); + if (meta) { + if (meta.capacityCost) { + component.DecoCapacity! += meta.capacityCost; + } + } else { + const itemType = Object.entries(ExportResources).find(arr => arr[1].deco == deco.Type)![0]; + if (deco.Sockets !== undefined) { + addVaultFusionTreasures(guild, [ + { + ItemType: itemType, + ItemCount: 1, + Sockets: deco.Sockets + } + ]); + } else { + addVaultShipDecos(guild, [ + { + ItemType: itemType, + ItemCount: 1 + } + ]); + } + } + moveResourcesToVault(guild, deco); +}; + +const moveResourcesToVault = (guild: TGuildDatabaseDocument, component: IDojoContributable): void => { + if (component.RegularCredits) { + guild.VaultRegularCredits ??= 0; + guild.VaultRegularCredits += component.RegularCredits; + } + if (component.MiscItems) { + addVaultMiscItems(guild, component.MiscItems); + } + if (component.RushPlatinum) { + guild.VaultPremiumCredits ??= 0; + guild.VaultPremiumCredits += component.RushPlatinum; + } +}; + +export const getVaultMiscItemCount = (guild: TGuildDatabaseDocument, itemType: string): number => { + return guild.VaultMiscItems?.find(x => x.ItemType == itemType)?.ItemCount ?? 0; +}; + +export const addVaultMiscItems = (guild: TGuildDatabaseDocument, miscItems: ITypeCount[]): void => { + guild.VaultMiscItems ??= []; + for (const item of miscItems) { + const vaultItem = guild.VaultMiscItems.find(x => x.ItemType == item.ItemType); + if (vaultItem) { + vaultItem.ItemCount += item.ItemCount; + } else { + guild.VaultMiscItems.push(item); + } + } +}; + +export const addVaultShipDecos = (guild: TGuildDatabaseDocument, shipDecos: ITypeCount[]): void => { + guild.VaultShipDecorations ??= []; + for (const item of shipDecos) { + const vaultItem = guild.VaultShipDecorations.find(x => x.ItemType == item.ItemType); + if (vaultItem) { + vaultItem.ItemCount += item.ItemCount; + } else { + guild.VaultShipDecorations.push(item); + } + } +}; + +export const addVaultFusionTreasures = (guild: TGuildDatabaseDocument, fusionTreasures: IFusionTreasure[]): void => { + guild.VaultFusionTreasures ??= []; + for (const item of fusionTreasures) { + const vaultItem = guild.VaultFusionTreasures.find( + x => x.ItemType == item.ItemType && x.Sockets == item.Sockets + ); + if (vaultItem) { + vaultItem.ItemCount += item.ItemCount; + } else { + guild.VaultFusionTreasures.push(item); + } + } +}; + +export const addGuildMemberMiscItemContribution = (guildMember: IGuildMemberDatabase, item: ITypeCount): void => { + guildMember.MiscItemsContributed ??= []; + const miscItemContribution = guildMember.MiscItemsContributed.find(x => x.ItemType == item.ItemType); + if (miscItemContribution) { + miscItemContribution.ItemCount += item.ItemCount; + } else { + guildMember.MiscItemsContributed.push(item); + } +}; + +export const addGuildMemberShipDecoContribution = (guildMember: IGuildMemberDatabase, item: ITypeCount): void => { + guildMember.ShipDecorationsContributed ??= []; + const shipDecoContribution = guildMember.ShipDecorationsContributed.find(x => x.ItemType == item.ItemType); + if (shipDecoContribution) { + shipDecoContribution.ItemCount += item.ItemCount; + } else { + guildMember.ShipDecorationsContributed.push(item); + } +}; + +export const processDojoBuildMaterialsGathered = (guild: TGuildDatabaseDocument, build: IDojoBuild): void => { + if (build.guildXpValue) { + guild.ClaimedXP ??= []; + if (!guild.ClaimedXP.find(x => x == build.resultType)) { + guild.ClaimedXP.push(build.resultType); + guild.XP += build.guildXpValue; + } + } +}; + +// guild.save(); is expected some time after this function is called +export const setDojoRoomLogFunded = (guild: TGuildDatabaseDocument, component: IDojoComponentDatabase): void => { + const entry = guild.RoomChanges?.find(x => x.componentId.equals(component._id)); + if (entry && entry.entryType == 2) { + entry.entryType = 0; + entry.dateTime = component.CompletionTime!; + component.CompletionLogPending = true; + } +}; + +export const fillInInventoryDataForGuildMember = async (member: IGuildMemberClient): Promise => { + const inventory = await getInventory(member._id.$oid, "PlayerLevel ActiveAvatarImageType"); + member.PlayerLevel = config.spoofMasteryRank == -1 ? inventory.PlayerLevel : config.spoofMasteryRank; + member.ActiveAvatarImageType = inventory.ActiveAvatarImageType; +}; + +export const createUniqueClanName = async (name: string): Promise => { + const initialDiscriminator = getRandomInt(0, 999); + let discriminator = initialDiscriminator; + do { + const fullName = name + "#" + discriminator.toString().padStart(3, "0"); + if (!(await Guild.exists({ Name: fullName }))) { + return fullName; + } + discriminator = (discriminator + 1) % 1000; + } while (discriminator != initialDiscriminator); + throw new Error(`clan name is so unoriginal it's already been done 1000 times: ${name}`); +}; + +export const hasAccessToDojo = (inventory: TInventoryDatabaseDocument): boolean => { + return inventory.LevelKeys.find(x => x.ItemType == "/Lotus/Types/Keys/DojoKey") !== undefined; +}; + +export const hasGuildPermission = async ( + guild: TGuildDatabaseDocument, + accountId: string | Types.ObjectId, + perm: GuildPermission +): Promise => { + const member = await GuildMember.findOne({ accountId: accountId, guildId: guild._id }); + if (member) { + return hasGuildPermissionEx(guild, member, perm); + } + return false; +}; + +export const hasGuildPermissionEx = ( + guild: TGuildDatabaseDocument, + member: IGuildMemberDatabase, + perm: GuildPermission +): boolean => { + const rank = guild.Ranks[member.rank]; + return (rank.Permissions & perm) != 0; +}; + +export const removePigmentsFromGuildMembers = async (guildId: string | Types.ObjectId): Promise => { + const members = await GuildMember.find({ guildId, status: 0 }, "accountId"); + for (const member of members) { + const inventory = await getInventory(member.accountId.toString(), "MiscItems"); + const index = inventory.MiscItems.findIndex( + x => x.ItemType == "/Lotus/Types/Items/Research/DojoColors/GenericDojoColorPigment" + ); + if (index != -1) { + inventory.MiscItems.splice(index, 1); + await inventory.save(); + } + } +}; + +export const processGuildTechProjectContributionsUpdate = async ( + guild: TGuildDatabaseDocument, + techProject: ITechProjectDatabase +): Promise => { + if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) { + // This research is now fully funded. + + if ( + techProject.State == 0 && + techProject.ItemType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/" + ) { + guild.ActiveDojoColorResearch = ""; + await removePigmentsFromGuildMembers(guild._id); + } + + const recipe = ExportDojoRecipes.research[techProject.ItemType]; + processFundedGuildTechProject(guild, techProject, recipe); + } +}; + +export const processFundedGuildTechProject = ( + guild: TGuildDatabaseDocument, + techProject: ITechProjectDatabase, + recipe: IDojoResearch +): void => { + techProject.State = 1; + techProject.CompletionDate = new Date(Date.now() + (config.noDojoResearchTime ? 0 : recipe.time) * 1000); + if (recipe.guildXpValue) { + guild.XP += recipe.guildXpValue; + } + setGuildTechLogState(guild, techProject.ItemType, config.noDojoResearchTime ? 4 : 3, techProject.CompletionDate); +}; + +export const setGuildTechLogState = ( + guild: TGuildDatabaseDocument, + type: string, + state: number, + dateTime: Date | undefined = undefined +): boolean => { + guild.TechChanges ??= []; + const entry = guild.TechChanges.find(x => x.details == type); + if (entry) { + if (entry.entryType == state) { + return false; + } + entry.dateTime = dateTime; + entry.entryType = state; + } else { + guild.TechChanges.push({ + dateTime: dateTime, + entryType: state, + details: type + }); + } + return true; +}; + +const setGuildTier = async (guild: TGuildDatabaseDocument, newTier: number): Promise => { + const oldTier = guild.Tier; + guild.Tier = newTier; + if (guild.TechProjects) { + for (const project of guild.TechProjects) { + if (project.State == 1) { + continue; + } + + const meta = ExportDojoRecipes.research[project.ItemType]; + + { + const numContributed = scaleRequiredCount(oldTier, meta.price) - project.ReqCredits; + project.ReqCredits = scaleRequiredCount(newTier, meta.price) - numContributed; + if (project.ReqCredits < 0) { + guild.VaultRegularCredits ??= 0; + guild.VaultRegularCredits += project.ReqCredits * -1; + project.ReqCredits = 0; + } + } + + for (let i = 0; i != project.ReqItems.length; ++i) { + const numContributed = + scaleRequiredCount(oldTier, meta.ingredients[i].ItemCount) - project.ReqItems[i].ItemCount; + project.ReqItems[i].ItemCount = + scaleRequiredCount(newTier, meta.ingredients[i].ItemCount) - numContributed; + if (project.ReqItems[i].ItemCount < 0) { + project.ReqItems[i].ItemCount *= -1; + addVaultMiscItems(guild, [project.ReqItems[i]]); + project.ReqItems[i].ItemCount = 0; + } + } + + // Check if research is fully funded now due to lowered requirements. + await processGuildTechProjectContributionsUpdate(guild, project); + } + } +}; + +export const removeDojoKeyItems = (inventory: TInventoryDatabaseDocument): IInventoryChanges => { + const inventoryChanges: IInventoryChanges = {}; + + const itemIndex = inventory.LevelKeys.findIndex(x => x.ItemType == "/Lotus/Types/Keys/DojoKey"); + if (itemIndex != -1) { + inventoryChanges.LevelKeys = [ + { + ItemType: "/Lotus/Types/Keys/DojoKey", + ItemCount: inventory.LevelKeys[itemIndex].ItemCount * -1 + } + ]; + inventory.LevelKeys.splice(itemIndex, 1); + } + + const recipeIndex = inventory.Recipes.findIndex(x => x.ItemType == "/Lotus/Types/Keys/DojoKeyBlueprint"); + if (recipeIndex != -1) { + inventoryChanges.Recipes = [ + { + ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint", + ItemCount: inventory.Recipes[recipeIndex].ItemCount * -1 + } + ]; + inventory.Recipes.splice(recipeIndex, 1); + } + + return inventoryChanges; +}; + +export const deleteGuild = async (guildId: Types.ObjectId): Promise => { + await Guild.deleteOne({ _id: guildId }); + + const guildMembers = await GuildMember.find({ guildId, status: 0 }, "accountId"); + await parallelForeach(guildMembers, async member => { + const inventory = await getInventory(member.accountId.toString(), "GuildId LevelKeys Recipes"); + inventory.GuildId = undefined; + removeDojoKeyItems(inventory); + await inventory.save(); + }); + + await GuildMember.deleteMany({ guildId }); + + // If guild sent any invites, delete those inbox messages as well. + await Inbox.deleteMany({ + contextInfo: guildId.toString(), + acceptAction: "GUILD_INVITE" + }); + + await GuildAd.deleteOne({ GuildId: guildId }); + + // If guild is the creator of an alliance, delete that as well. + const allianceMember = await AllianceMember.findOne({ guildId, Pending: false }); + if (allianceMember) { + if (allianceMember.Permissions & GuildPermission.Ruler) { + await deleteAlliance(allianceMember.allianceId); + } + } + + await AllianceMember.deleteMany({ guildId }); +}; + +export const deleteAlliance = async (allianceId: Types.ObjectId): Promise => { + const allianceMembers = await AllianceMember.find({ allianceId, Pending: false }); + await parallelForeach(allianceMembers, async allianceMember => { + await Guild.updateOne({ _id: allianceMember.guildId }, { $unset: { AllianceId: "" } }); + }); + + await AllianceMember.deleteMany({ allianceId }); + + await Alliance.deleteOne({ _id: allianceId }); +}; + +export const getAllianceClient = async ( + alliance: IAllianceDatabase, + guild: TGuildDatabaseDocument +): Promise => { + const allianceMembers = await AllianceMember.find({ allianceId: alliance._id }); + const clans: IAllianceMemberClient[] = []; + for (const allianceMember of allianceMembers) { + const memberGuild = allianceMember.guildId.equals(guild._id) + ? guild + : (await Guild.findById(allianceMember.guildId))!; + clans.push({ + _id: toOid(allianceMember.guildId), + Name: memberGuild.Name, + Tier: memberGuild.Tier, + Pending: allianceMember.Pending, + Permissions: allianceMember.Permissions, + MemberCount: await GuildMember.countDocuments({ guildId: memberGuild._id, status: 0 }) + }); + } + return { + _id: toOid(alliance._id), + Name: alliance.Name, + MOTD: alliance.MOTD, + LongMOTD: alliance.LongMOTD, + Clans: clans, + AllianceVault: { + DojoRefundRegularCredits: alliance.VaultRegularCredits + } }; +}; diff --git a/src/services/importService.ts b/src/services/importService.ts index 979221c8..ae16e86d 100644 --- a/src/services/importService.ts +++ b/src/services/importService.ts @@ -23,6 +23,12 @@ import { IKubrowPetDetailsDatabase, ILoadoutConfigClient, ILoadOutPresets, + INemesisClient, + INemesisDatabase, + IPendingRecipeClient, + IPendingRecipeDatabase, + IQuestKeyClient, + IQuestKeyDatabase, ISlots, IUpgradeClient, IUpgradeDatabase, @@ -144,6 +150,27 @@ const convertKubrowDetails = (client: IKubrowPetDetailsClient): IKubrowPetDetail }; }; +const convertQuestKey = (client: IQuestKeyClient): IQuestKeyDatabase => { + return { + ...client, + CompletionDate: convertOptionalDate(client.CompletionDate) + }; +}; + +const convertPendingRecipe = (client: IPendingRecipeClient): IPendingRecipeDatabase => { + return { + ...client, + CompletionDate: convertDate(client.CompletionDate) + }; +}; + +const convertNemesis = (client: INemesisClient): INemesisDatabase => { + return { + ...client, + d: convertDate(client.d) + }; +}; + export const importInventory = (db: TInventoryDatabaseDocument, client: Partial): void => { for (const key of equipmentKeys) { if (client[key] !== undefined) { @@ -153,10 +180,22 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< if (client.WeaponSkins !== undefined) { replaceArray(db.WeaponSkins, client.WeaponSkins.map(convertWeaponSkin)); } - if (client.Upgrades !== undefined) { - replaceArray(db.Upgrades, client.Upgrades.map(convertUpgrade)); + for (const key of ["Upgrades", "CrewShipSalvagedWeaponSkins", "CrewShipWeaponSkins"] as const) { + if (client[key] !== undefined) { + replaceArray(db[key], client[key].map(convertUpgrade)); + } } - for (const key of ["RawUpgrades", "MiscItems", "Consumables"] as const) { + for (const key of [ + "RawUpgrades", + "MiscItems", + "Consumables", + "Recipes", + "LevelKeys", + "EmailItems", + "ShipDecorations", + "CrewShipAmmo", + "CrewShipRawSalvage" + ] as const) { if (client[key] !== undefined) { db[key].splice(0, db[key].length); client[key].forEach(x => { @@ -190,8 +229,16 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< replaceSlots(db[key], client[key]); } } - if (client.UseAdultOperatorLoadout !== undefined) { - db.UseAdultOperatorLoadout = client.UseAdultOperatorLoadout; + for (const key of [ + "UseAdultOperatorLoadout", + "HasOwnedVoidProjectionsPreviously", + "ReceivedStartingGear", + "ArchwingEnabled", + "PlayedParkourTutorial" + ] as const) { + if (client[key] !== undefined) { + db[key] = client[key]; + } } for (const key of [ "PlayerLevel", @@ -199,18 +246,37 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< "PremiumCredits", "PremiumCreditsFree", "FusionPoints", - "PrimeTokens" + "PrimeTokens", + "TradesRemaining", + "GiftsRemaining", + "ChallengesFixVersion" ] as const) { if (client[key] !== undefined) { db[key] = client[key]; } } - for (const key of ["ThemeStyle", "ThemeBackground", "ThemeSounds", "EquippedInstrument", "FocusAbility"] as const) { + for (const key of [ + "ThemeStyle", + "ThemeBackground", + "ThemeSounds", + "EquippedInstrument", + "FocusAbility", + "ActiveQuest", + "SupportedSyndicate", + "ActiveAvatarImageType" + ] as const) { if (client[key] !== undefined) { db[key] = client[key]; } } - for (const key of ["EquippedGear", "EquippedEmotes", "NodeIntrosCompleted"] as const) { + for (const key of [ + "EquippedGear", + "EquippedEmotes", + "NodeIntrosCompleted", + "DeathMarks", + "Wishlist", + "NemesisAbandonedRewards" + ] as const) { if (client[key] !== undefined) { db[key] = client[key]; } @@ -239,6 +305,80 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< if (client.DialogueHistory !== undefined) { db.DialogueHistory = convertDialogueHistory(client.DialogueHistory); } + if (client.CustomMarkers !== undefined) { + db.CustomMarkers = client.CustomMarkers; + } + if (client.ChallengeProgress !== undefined) { + db.ChallengeProgress = client.ChallengeProgress; + } + if (client.QuestKeys !== undefined) { + replaceArray(db.QuestKeys, client.QuestKeys.map(convertQuestKey)); + } + if (client.LastRegionPlayed !== undefined) { + db.LastRegionPlayed = client.LastRegionPlayed; + } + if (client.PendingRecipes !== undefined) { + replaceArray(db.PendingRecipes, client.PendingRecipes.map(convertPendingRecipe)); + } + if (client.TauntHistory !== undefined) { + db.TauntHistory = client.TauntHistory; + } + if (client.LoreFragmentScans !== undefined) { + db.LoreFragmentScans = client.LoreFragmentScans; + } + for (const key of ["PendingSpectreLoadouts", "SpectreLoadouts"] as const) { + if (client[key] !== undefined) { + db[key] = client[key]; + } + } + if (client.FocusXP !== undefined) { + db.FocusXP = client.FocusXP; + } + for (const key of ["Alignment", "AlignmentReplay"] as const) { + if (client[key] !== undefined) { + db[key] = client[key]; + } + } + if (client.StepSequencers !== undefined) { + db.StepSequencers = client.StepSequencers; + } + if (client.CompletedJobChains !== undefined) { + db.CompletedJobChains = client.CompletedJobChains; + } + if (client.Nemesis !== undefined) { + db.Nemesis = convertNemesis(client.Nemesis); + } + if (client.PlayerSkills !== undefined) { + db.PlayerSkills = client.PlayerSkills; + } + if (client.LotusCustomization !== undefined) { + db.LotusCustomization = client.LotusCustomization; + } + if (client.CollectibleSeries !== undefined) { + db.CollectibleSeries = client.CollectibleSeries; + } + for (const key of ["LibraryAvailableDailyTaskInfo", "LibraryActiveDailyTaskInfo"] as const) { + if (client[key] !== undefined) { + db[key] = client[key]; + } + } + if (client.EndlessXP !== undefined) { + db.EndlessXP = client.EndlessXP; + } + if (client.SongChallenges !== undefined) { + db.SongChallenges = client.SongChallenges; + } + if (client.Missions !== undefined) { + db.Missions = client.Missions; + } + if (client.FlavourItems !== undefined) { + db.FlavourItems.splice(0, db.FlavourItems.length); + client.FlavourItems.forEach(x => { + db.FlavourItems.push({ + ItemType: x.ItemType + }); + }); + } }; const convertLoadOutConfig = (client: ILoadoutConfigClient): ILoadoutConfigDatabase => { diff --git a/src/services/inboxService.ts b/src/services/inboxService.ts index fa7a3812..0c2d698d 100644 --- a/src/services/inboxService.ts +++ b/src/services/inboxService.ts @@ -1,6 +1,6 @@ import { IMessageDatabase, Inbox } from "@/src/models/inboxModel"; import { getAccountForRequest } from "@/src/services/loginService"; -import { HydratedDocument } from "mongoose"; +import { HydratedDocument, Types } from "mongoose"; import { Request } from "express"; import eventMessages from "@/static/fixed_responses/eventMessages.json"; import { logger } from "@/src/utils/logger"; @@ -11,7 +11,7 @@ export const getAllMessagesSorted = async (accountId: string): Promise> => { - const message = await Inbox.findOne({ _id: messageId }); + const message = await Inbox.findById(messageId); if (!message) { throw new Error(`Message not found ${messageId}`); @@ -39,7 +39,7 @@ export const createNewEventMessages = async (req: Request): Promise => { return; } - const savedEventMessages = await createMessage(account._id.toString(), newEventMessages); + const savedEventMessages = await createMessage(account._id, newEventMessages); logger.debug("created event messages", savedEventMessages); const latestEventMessage = newEventMessages.reduce((prev, current) => @@ -50,7 +50,7 @@ export const createNewEventMessages = async (req: Request): Promise => { await account.save(); }; -export const createMessage = async (accountId: string, messages: IMessageCreationTemplate[]) => { +export const createMessage = async (accountId: string | Types.ObjectId, messages: IMessageCreationTemplate[]) => { const ownerIdMessages = messages.map(m => ({ ...m, ownerId: accountId diff --git a/src/services/infestedFoundryService.ts b/src/services/infestedFoundryService.ts new file mode 100644 index 00000000..5afc93fa --- /dev/null +++ b/src/services/infestedFoundryService.ts @@ -0,0 +1,110 @@ +import { ExportRecipes } from "warframe-public-export-plus"; +import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel"; +import { IInfestedFoundryClient, IInfestedFoundryDatabase, ITypeCount } from "../types/inventoryTypes/inventoryTypes"; +import { addRecipes } from "./inventoryService"; +import { config } from "./configService"; + +export const addInfestedFoundryXP = (infestedFoundry: IInfestedFoundryDatabase, delta: number): ITypeCount[] => { + const recipeChanges: ITypeCount[] = []; + infestedFoundry.XP ??= 0; + const prevXP = infestedFoundry.XP; + infestedFoundry.XP += delta; + if (prevXP < 2250_00 && infestedFoundry.XP >= 2250_00) { + infestedFoundry.Slots ??= 0; + infestedFoundry.Slots += 3; + } + if (prevXP < 5625_00 && infestedFoundry.XP >= 5625_00) { + recipeChanges.push({ + ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthShieldsBlueprint", + ItemCount: 1 + }); + } + if (prevXP < 10125_00 && infestedFoundry.XP >= 10125_00) { + recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthHackBlueprint", ItemCount: 1 }); + } + if (prevXP < 15750_00 && infestedFoundry.XP >= 15750_00) { + infestedFoundry.Slots ??= 0; + infestedFoundry.Slots += 10; + } + if (prevXP < 22500_00 && infestedFoundry.XP >= 22500_00) { + recipeChanges.push({ + ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthAmmoEfficiencyBlueprint", + ItemCount: 1 + }); + } + if (prevXP < 30375_00 && infestedFoundry.XP >= 30375_00) { + recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthStunBlueprint", ItemCount: 1 }); + } + if (prevXP < 39375_00 && infestedFoundry.XP >= 39375_00) { + infestedFoundry.Slots ??= 0; + infestedFoundry.Slots += 20; + } + if (prevXP < 60750_00 && infestedFoundry.XP >= 60750_00) { + recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthStatusBlueprint", ItemCount: 1 }); + } + if (prevXP < 73125_00 && infestedFoundry.XP >= 73125_00) { + infestedFoundry.Slots = 1; + } + if (prevXP < 86625_00 && infestedFoundry.XP >= 86625_00) { + recipeChanges.push({ + ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthShieldArmorBlueprint", + ItemCount: 1 + }); + } + if (prevXP < 101250_00 && infestedFoundry.XP >= 101250_00) { + recipeChanges.push({ + ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthProcBlockBlueprint", + ItemCount: 1 + }); + } + if (prevXP < 117000_00 && infestedFoundry.XP >= 117000_00) { + recipeChanges.push({ + ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthEnergyShareBlueprint", + ItemCount: 1 + }); + } + if (prevXP < 133875_00 && infestedFoundry.XP >= 133875_00) { + recipeChanges.push({ + ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthMaxStatusBlueprint", + ItemCount: 1 + }); + } + if (prevXP < 151875_00 && infestedFoundry.XP >= 151875_00) { + recipeChanges.push({ + ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthTreasureBlueprint", + ItemCount: 1 + }); + } + return recipeChanges; +}; + +export const handleSubsumeCompletion = (inventory: TInventoryDatabaseDocument): ITypeCount[] => { + const [recipeType] = Object.entries(ExportRecipes).find( + ([_recipeType, recipe]) => + recipe.secretIngredientAction == "SIA_WARFRAME_ABILITY" && + recipe.secretIngredients![0].ItemType == inventory.InfestedFoundry!.LastConsumedSuit!.ItemType + )!; + inventory.InfestedFoundry!.LastConsumedSuit = undefined; + inventory.InfestedFoundry!.AbilityOverrideUnlockCooldown = undefined; + const recipeChanges: ITypeCount[] = [ + { + ItemType: recipeType, + ItemCount: 1 + } + ]; + addRecipes(inventory, recipeChanges); + return recipeChanges; +}; + +export const applyCheatsToInfestedFoundry = (infestedFoundry: IInfestedFoundryClient): void => { + if (config.infiniteHelminthMaterials) { + infestedFoundry.Resources = [ + { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthCalx", Count: 1000 }, + { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthBiotics", Count: 1000 }, + { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthSynthetics", Count: 1000 }, + { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthPheromones", Count: 1000 }, + { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthBile", Count: 1000 }, + { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthOxides", Count: 1000 } + ]; + } +}; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 1f895825..904c90fa 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1,14 +1,9 @@ -import { - Inventory, - InventoryDocumentProps, - TInventoryDatabaseDocument -} from "@/src/models/inventoryModels/inventoryModel"; +import { Inventory, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { config } from "@/src/services/configService"; -import { HydratedDocument, Types } from "mongoose"; -import { SlotNames, IInventoryChanges, IBinChanges, ICurrencyChanges } from "@/src/types/purchaseTypes"; +import { Types } from "mongoose"; +import { SlotNames, IInventoryChanges, IBinChanges, slotNames } from "@/src/types/purchaseTypes"; import { IChallengeProgress, - IConsumable, IFlavourItem, IMiscItem, IMission, @@ -20,48 +15,55 @@ import { TEquipmentKey, IFusionTreasure, IDailyAffiliations, - IInventoryDatabase, IKubrowPetEggDatabase, IKubrowPetEggClient, - ILibraryAvailableDailyTaskInfo, + ILibraryDailyTaskInfo, ICalendarProgress, - IDroneClient + IDroneClient, + IUpgradeClient, + TPartialStartingGear } from "@/src/types/inventoryTypes/inventoryTypes"; -import { IGenericUpdate } from "../types/genericUpdate"; -import { - IMissionInventoryUpdateRequest, - IThemeUpdateRequest, - IUpdateChallengeProgressRequest -} from "../types/requestTypes"; +import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate"; +import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; -import { getExalted, getKeyChainItems } from "@/src/services/itemDataService"; -import { IEquipmentClient, IEquipmentDatabase, IItemConfig } from "../types/inventoryTypes/commonInventoryTypes"; +import { convertInboxMessage, fromStoreItem, getKeyChainItems } from "@/src/services/itemDataService"; +import { + EquipmentFeatures, + IEquipmentClient, + IEquipmentDatabase, + IItemConfig +} from "../types/inventoryTypes/commonInventoryTypes"; import { ExportArcanes, ExportBundles, ExportCustoms, ExportDrones, + ExportEmailItems, + ExportEnemies, ExportFlavour, ExportFusionBundles, ExportGear, ExportKeys, ExportMisc, + ExportRailjackWeapons, ExportRecipes, ExportResources, ExportSentinels, ExportSyndicates, ExportUpgrades, + ExportWarframes, ExportWeapons, IDefaultUpgrade, + IPowersuit, TStandingLimitBin } from "warframe-public-export-plus"; import { createShip } from "./shipService"; -import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; import { toOid } from "../helpers/inventoryHelpers"; -import { generateRewardSeed } from "@/src/controllers/api/getNewRewardSeedController"; -import { addStartingGear } from "@/src/controllers/api/giveStartingGearController"; import { addQuestKey, completeQuest } from "@/src/services/questService"; import { handleBundleAcqusition } from "./purchaseService"; +import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; +import { getRandomElement, getRandomInt, SRng } from "./rngService"; +import { createMessage } from "./inboxService"; export const createInventory = async ( accountOwnerId: Types.ObjectId, @@ -76,7 +78,7 @@ export const createInventory = async ( ReceivedStartingGear: config.skipTutorial }); - inventory.LibraryAvailableDailyTaskInfo = createLibraryAvailableDailyTaskInfo(); + inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask(); inventory.CalendarProgress = createCalendar(); inventory.RewardSeed = generateRewardSeed(); inventory.DuviriInfo = { @@ -105,6 +107,81 @@ export const createInventory = async ( } }; +export const generateRewardSeed = (): number => { + const min = -Number.MAX_SAFE_INTEGER; + const max = Number.MAX_SAFE_INTEGER; + return Math.floor(Math.random() * (max - min + 1)) + min; +}; + +//TODO: RawUpgrades might need to return a LastAdded +const awakeningRewards = [ + "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem1", + "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem2", + "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem3", + "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem4", + "/Lotus/Types/Restoratives/LisetAutoHack", + "/Lotus/Upgrades/Mods/Warframe/AvatarShieldMaxMod" +]; + +export const addStartingGear = async ( + inventory: TInventoryDatabaseDocument, + startingGear: TPartialStartingGear | undefined = undefined +): Promise => { + const { LongGuns, Pistols, Suits, Melee } = startingGear || { + LongGuns: [{ ItemType: "/Lotus/Weapons/Tenno/Rifle/Rifle" }], + Pistols: [{ ItemType: "/Lotus/Weapons/Tenno/Pistol/Pistol" }], + Suits: [{ ItemType: "/Lotus/Powersuits/Excalibur/Excalibur" }], + Melee: [{ ItemType: "/Lotus/Weapons/Tenno/Melee/LongSword/LongSword" }] + }; + + //TODO: properly merge weapon bin changes it is currently static here + const inventoryChanges: IInventoryChanges = {}; + addEquipment(inventory, "LongGuns", LongGuns[0].ItemType, undefined, inventoryChanges); + addEquipment(inventory, "Pistols", Pistols[0].ItemType, undefined, inventoryChanges); + addEquipment(inventory, "Melee", Melee[0].ItemType, undefined, inventoryChanges); + await addPowerSuit(inventory, Suits[0].ItemType, inventoryChanges); + addEquipment( + inventory, + "DataKnives", + "/Lotus/Weapons/Tenno/HackingDevices/TnHackingDevice/TnHackingDeviceWeapon", + undefined, + inventoryChanges, + { XP: 450_000 } + ); + addEquipment( + inventory, + "Scoops", + "/Lotus/Weapons/Tenno/Speedball/SpeedballWeaponTest", + undefined, + inventoryChanges + ); + + updateSlots(inventory, InventorySlot.SUITS, 0, 1); + updateSlots(inventory, InventorySlot.WEAPONS, 0, 3); + inventoryChanges.SuitBin = { count: 1, platinum: 0, Slots: -1 }; + inventoryChanges.WeaponBin = { count: 3, platinum: 0, Slots: -3 }; + + await addItem(inventory, "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain"); + inventory.ActiveQuest = "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain"; + + inventory.PremiumCredits = 50; + inventory.PremiumCreditsFree = 50; + inventoryChanges.PremiumCredits = 50; + inventoryChanges.PremiumCreditsFree = 50; + inventory.RegularCredits = 3000; + inventoryChanges.RegularCredits = 3000; + + for (const item of awakeningRewards) { + const inventoryDelta = await addItem(inventory, item); + combineInventoryChanges(inventoryChanges, inventoryDelta); + } + + inventory.PlayedParkourTutorial = true; + inventory.ReceivedStartingGear = true; + + return inventoryChanges; +}; + /** * Combines two inventory changes objects into one. * @@ -118,17 +195,21 @@ export const combineInventoryChanges = (InventoryChanges: IInventoryChanges, del InventoryChanges[key] = delta[key]; } else if (Array.isArray(delta[key])) { const left = InventoryChanges[key] as object[]; - const right: object[] | string[] = delta[key]; + const right: object[] = delta[key]; for (const item of right) { left.push(item); } - } else if (typeof delta[key] == "object") { - console.assert(key.substring(-3) == "Bin"); - console.assert(key != "InfestedFoundry"); - const left = InventoryChanges[key] as IBinChanges; - const right = delta[key] as IBinChanges; - left.count += right.count; - left.platinum += right.platinum; + } else if (slotNames.indexOf(key as SlotNames) != -1) { + const left = InventoryChanges[key as SlotNames]!; + const right = delta[key as SlotNames]!; + if (right.count) { + left.count ??= 0; + left.count += right.count; + } + if (right.platinum) { + left.platinum ??= 0; + left.platinum += right.platinum; + } left.Slots += right.Slots; if (right.Extra) { left.Extra ??= 0; @@ -155,14 +236,74 @@ export const getInventory = async ( return inventory; }; +export const productCategoryToInventoryBin = (productCategory: string): InventorySlot | undefined => { + switch (productCategory) { + case "Suits": + return InventorySlot.SUITS; + case "Pistols": + case "LongGuns": + case "Melee": + return InventorySlot.WEAPONS; + case "Sentinels": + case "SentinelWeapons": + case "KubrowPets": + case "MoaPets": + return InventorySlot.SENTINELS; + case "SpaceSuits": + case "Hoverboards": + return InventorySlot.SPACESUITS; + case "SpaceGuns": + case "SpaceMelee": + return InventorySlot.SPACEWEAPONS; + case "OperatorAmps": + return InventorySlot.AMPS; + case "CrewShipWeapons": + case "CrewShipWeaponSkins": + return InventorySlot.RJ_COMPONENT_AND_ARMAMENTS; + case "MechSuits": + return InventorySlot.MECHSUITS; + case "CrewMembers": + return InventorySlot.CREWMEMBERS; + } + return undefined; +}; + +export const occupySlot = ( + inventory: TInventoryDatabaseDocument, + bin: InventorySlot, + premiumPurchase: boolean +): IInventoryChanges => { + const slotChanges = { + Slots: 0, + Extra: 0 + }; + if (premiumPurchase) { + slotChanges.Extra += 1; + } else { + // { count: 1, platinum: 0, Slots: -1 } + slotChanges.Slots -= 1; + } + updateSlots(inventory, bin, slotChanges.Slots, slotChanges.Extra); + const inventoryChanges: IInventoryChanges = {}; + inventoryChanges[bin] = slotChanges satisfies IBinChanges; + return inventoryChanges; +}; + +export const freeUpSlot = (inventory: TInventoryDatabaseDocument, bin: InventorySlot): void => { + // { count: -1, platinum: 0, Slots: 1 } + updateSlots(inventory, bin, 1, 0); +}; + export const addItem = async ( inventory: TInventoryDatabaseDocument, typeName: string, - quantity: number = 1 -): Promise<{ InventoryChanges: IInventoryChanges }> => { + quantity: number = 1, + premiumPurchase: boolean = false, + seed?: bigint +): Promise => { // Bundles are technically StoreItems but a) they don't have a normal counterpart, and b) they are used in non-StoreItem contexts, e.g. email attachments. if (typeName in ExportBundles) { - return { InventoryChanges: await handleBundleAcqusition(typeName, inventory, quantity) }; + return await handleBundleAcqusition(typeName, inventory, quantity); } // Strict typing @@ -175,9 +316,7 @@ export const addItem = async ( ]; addRecipes(inventory, recipeChanges); return { - InventoryChanges: { - Recipes: recipeChanges - } + Recipes: recipeChanges }; } if (typeName in ExportResources) { @@ -190,25 +329,33 @@ export const addItem = async ( ]; addMiscItems(inventory, miscItemChanges); return { - InventoryChanges: { - MiscItems: miscItemChanges - } + MiscItems: miscItemChanges + }; + } else if (ExportResources[typeName].productCategory == "FusionTreasures") { + const fusionTreasureChanges = [ + { + ItemType: typeName, + ItemCount: quantity, + Sockets: 0 + } satisfies IFusionTreasure + ]; + addFusionTreasures(inventory, fusionTreasureChanges); + return { + FusionTreasures: fusionTreasureChanges }; } else if (ExportResources[typeName].productCategory == "Ships") { const oid = await createShip(inventory.accountOwnerId, typeName); inventory.Ships.push(oid); return { - InventoryChanges: { - Ships: [ - { - ItemId: { $oid: oid.toString() }, - ItemType: typeName - } - ] - } + Ships: [ + { + ItemId: { $oid: oid.toString() }, + ItemType: typeName + } + ] }; } else if (ExportResources[typeName].productCategory == "CrewShips") { - const inventoryChanges = { + return { ...addCrewShip(inventory, typeName), // fix to unlock railjack modding, item bellow supposed to be obtained from archwing quest // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition @@ -216,8 +363,6 @@ export const addItem = async ( ? addCrewShipHarness(inventory, "/Lotus/Types/Game/CrewShip/RailJack/DefaultHarness") : {}) }; - - return { InventoryChanges: inventoryChanges }; } else if (ExportResources[typeName].productCategory == "ShipDecorations") { const changes = [ { @@ -227,12 +372,13 @@ export const addItem = async ( ]; addShipDecorations(inventory, changes); return { - InventoryChanges: { - ShipDecorations: changes - } + ShipDecorations: changes }; } else if (ExportResources[typeName].productCategory == "KubrowPetEggs") { const changes: IKubrowPetEggClient[] = []; + if (quantity < 0 || quantity > 100) { + throw new Error(`unexpected acquisition quantity of KubrowPetEggs: ${quantity}`); + } for (let i = 0; i != quantity; ++i) { const egg: IKubrowPetEggDatabase = { ItemType: "/Lotus/Types/Game/KubrowPet/Eggs/KubrowEgg", @@ -247,19 +393,29 @@ export const addItem = async ( }); } return { - InventoryChanges: { - KubrowPetEggs: changes - } + KubrowPetEggs: changes }; + } else { + throw new Error(`unknown product category: ${ExportResources[typeName].productCategory}`); } } if (typeName in ExportCustoms) { - const inventoryChanges = addSkin(inventory, typeName); - return { InventoryChanges: inventoryChanges }; + const meta = ExportCustoms[typeName]; + let inventoryChanges: IInventoryChanges; + if (meta.productCategory == "CrewShipWeaponSkins") { + inventoryChanges = addCrewShipWeaponSkin(inventory, typeName); + } else { + inventoryChanges = addSkin(inventory, typeName); + } + if (meta.additionalItems) { + for (const item of meta.additionalItems) { + combineInventoryChanges(inventoryChanges, await addItem(inventory, item)); + } + } + return inventoryChanges; } if (typeName in ExportFlavour) { - const inventoryChanges = addCustomization(inventory, typeName); - return { InventoryChanges: inventoryChanges }; + return addCustomization(inventory, typeName); } if (typeName in ExportUpgrades || typeName in ExportArcanes) { const changes = [ @@ -270,9 +426,7 @@ export const addItem = async ( ]; addMods(inventory, changes); return { - InventoryChanges: { - RawUpgrades: changes - } + RawUpgrades: changes }; } if (typeName in ExportGear) { @@ -280,30 +434,66 @@ export const addItem = async ( { ItemType: typeName, ItemCount: quantity - } satisfies IConsumable + } satisfies ITypeCount ]; addConsumables(inventory, consumablesChanges); return { - InventoryChanges: { - Consumables: consumablesChanges - } + Consumables: consumablesChanges }; } if (typeName in ExportWeapons) { const weapon = ExportWeapons[typeName]; if (weapon.totalDamage != 0) { - const inventoryChanges = addEquipment(inventory, weapon.productCategory, typeName); + const defaultOverwrites: Partial = {}; + if (premiumPurchase) { + defaultOverwrites.Features = EquipmentFeatures.DOUBLE_CAPACITY; + } + if (weapon.maxLevelCap == 40 && typeName.indexOf("BallasSword") == -1) { + if (!seed) { + seed = BigInt(Math.round(Math.random() * Number.MAX_SAFE_INTEGER)); + } + const rng = new SRng(seed); + const tag = rng.randomElement([ + "InnateElectricityDamage", + "InnateFreezeDamage", + "InnateHeatDamage", + "InnateImpactDamage", + "InnateMagDamage", + "InnateRadDamage", + "InnateToxinDamage" + ]); + const WeaponUpgradeValueAttenuationExponent = 2.25; + let value = Math.pow(rng.randomFloat(), WeaponUpgradeValueAttenuationExponent); + if (value >= 0.941428) { + value = 1; + } + defaultOverwrites.UpgradeType = "/Lotus/Weapons/Grineer/KuvaLich/Upgrades/InnateDamageRandomMod"; + defaultOverwrites.UpgradeFingerprint = JSON.stringify({ + compat: typeName, + buffs: [ + { + Tag: tag, + Value: Math.trunc(value * 0x40000000) + } + ] + }); + } + const inventoryChanges = addEquipment( + inventory, + weapon.productCategory, + typeName, + [], + {}, + defaultOverwrites + ); if (weapon.additionalItems) { for (const item of weapon.additionalItems) { combineInventoryChanges(inventoryChanges, await addItem(inventory, item, 1)); } } - updateSlots(inventory, InventorySlot.WEAPONS, 0, 1); return { - InventoryChanges: { - ...inventoryChanges, - WeaponBin: { count: 1, platinum: 0, Slots: -1 } - } + ...inventoryChanges, + ...occupySlot(inventory, InventorySlot.WEAPONS, premiumPurchase) }; } else { // Modular weapon parts @@ -315,28 +505,28 @@ export const addItem = async ( ]; addMiscItems(inventory, miscItemChanges); return { - InventoryChanges: { - MiscItems: miscItemChanges - } + MiscItems: miscItemChanges }; } } + if (typeName in ExportRailjackWeapons) { + return { + ...addEquipment(inventory, ExportRailjackWeapons[typeName].productCategory, typeName), + ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, premiumPurchase) + }; + } if (typeName in ExportMisc.creditBundles) { const creditsTotal = ExportMisc.creditBundles[typeName] * quantity; inventory.RegularCredits += creditsTotal; return { - InventoryChanges: { - RegularCredits: creditsTotal - } + RegularCredits: creditsTotal }; } if (typeName in ExportFusionBundles) { const fusionPointsTotal = ExportFusionBundles[typeName].fusionPoints * quantity; inventory.FusionPoints += fusionPointsTotal; return { - InventoryChanges: { - FusionPoints: fusionPointsTotal - } + FusionPoints: fusionPointsTotal }; } if (typeName in ExportKeys) { @@ -345,25 +535,25 @@ export const addItem = async ( if (key.chainStages) { const key = addQuestKey(inventory, { ItemType: typeName }); - if (!key) return { InventoryChanges: {} }; - return { InventoryChanges: { QuestKeys: [key] } }; + if (!key) return {}; + return { QuestKeys: [key] }; } else { const key = { ItemType: typeName, ItemCount: quantity }; const index = inventory.LevelKeys.findIndex(levelKey => levelKey.ItemType == typeName); - if (index) { + if (index != -1) { inventory.LevelKeys[index].ItemCount += quantity; } else { inventory.LevelKeys.push(key); } - return { InventoryChanges: { LevelKeys: [key] } }; + return { LevelKeys: [key] }; } } if (typeName in ExportDrones) { - const inventoryChanges = addDrone(inventory, typeName); - return { - InventoryChanges: inventoryChanges - }; + return addDrone(inventory, typeName); + } + if (typeName in ExportEmailItems) { + return await addEmailItem(inventory, typeName); } // Path-based duck typing @@ -371,45 +561,37 @@ export const addItem = async ( case "Powersuits": switch (typeName.substr(1).split("/")[2]) { default: { - const inventoryChanges = addPowerSuit(inventory, typeName); - updateSlots(inventory, InventorySlot.SUITS, 0, 1); return { - InventoryChanges: { - ...inventoryChanges, - SuitBin: { - count: 1, - platinum: 0, - Slots: -1 - } - } + ...(await addPowerSuit( + inventory, + typeName, + {}, + premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined + )), + ...occupySlot(inventory, InventorySlot.SUITS, premiumPurchase) }; } case "Archwing": { - const inventoryChanges = addSpaceSuit(inventory, typeName); - updateSlots(inventory, InventorySlot.SPACESUITS, 0, 1); + inventory.ArchwingEnabled = true; return { - InventoryChanges: { - ...inventoryChanges, - SpaceSuitBin: { - count: 1, - platinum: 0, - Slots: -1 - } - } + ...addSpaceSuit( + inventory, + typeName, + {}, + premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined + ), + ...occupySlot(inventory, InventorySlot.SPACESUITS, premiumPurchase) }; } case "EntratiMech": { - const inventoryChanges = addMechSuit(inventory, typeName); - updateSlots(inventory, InventorySlot.MECHSUITS, 0, 1); return { - InventoryChanges: { - ...inventoryChanges, - MechBin: { - count: 1, - platinum: 0, - Slots: -1 - } - } + ...(await addMechSuit( + inventory, + typeName, + {}, + premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined + )), + ...occupySlot(inventory, InventorySlot.MECHSUITS, premiumPurchase) }; } } @@ -427,44 +609,46 @@ export const addItem = async ( ]; addMods(inventory, changes); return { - InventoryChanges: { - RawUpgrades: changes - } + RawUpgrades: changes }; } break; + + case "Stickers": + { + const entry = inventory.RawUpgrades.find(x => x.ItemType == typeName); + if (entry && entry.ItemCount >= 10) { + const miscItemChanges = [ + { + ItemType: "/Lotus/Types/Items/MiscItems/1999ConquestBucks", + ItemCount: 1 + } + ]; + addMiscItems(inventory, miscItemChanges); + return { + MiscItems: miscItemChanges + }; + } else { + const changes = [ + { + ItemType: typeName, + ItemCount: quantity + } + ]; + addMods(inventory, changes); + return { + RawUpgrades: changes + }; + } + } + break; } break; } case "Types": switch (typeName.substr(1).split("/")[2]) { case "Sentinels": { - const inventoryChanges = addSentinel(inventory, typeName); - updateSlots(inventory, InventorySlot.SENTINELS, 0, 1); - return { - InventoryChanges: { - ...inventoryChanges, - SentinelBin: { count: 1, platinum: 0, Slots: -1 } - } - }; - } - case "Items": { - switch (typeName.substr(1).split("/")[3]) { - default: { - const miscItemChanges = [ - { - ItemType: typeName, - ItemCount: quantity - } satisfies IMiscItem - ]; - addMiscItems(inventory, miscItemChanges); - return { - InventoryChanges: { - MiscItems: miscItemChanges - } - }; - } - } + return addSentinel(inventory, typeName, premiumPurchase); } case "Game": { if (typeName.substr(1).split("/")[3] == "Projections") { @@ -476,10 +660,9 @@ export const addItem = async ( } satisfies IMiscItem ]; addMiscItems(inventory, miscItemChanges); + inventory.HasOwnedVoidProjectionsPreviously = true; return { - InventoryChanges: { - MiscItems: miscItemChanges - } + MiscItems: miscItemChanges }; } break; @@ -487,27 +670,12 @@ export const addItem = async ( case "NeutralCreatures": { const horseIndex = inventory.Horses.push({ ItemType: typeName }); return { - InventoryChanges: { - Horses: [inventory.Horses[horseIndex - 1].toJSON()] - } - }; - } - case "Recipes": { - inventory.MiscItems.push({ ItemType: typeName, ItemCount: quantity }); - return { - InventoryChanges: { - MiscItems: [ - { - ItemType: typeName, - ItemCount: quantity - } - ] - } + Horses: [inventory.Horses[horseIndex - 1].toJSON()] }; } case "Vehicles": if (typeName == "/Lotus/Types/Vehicles/Motorcycle/MotorcyclePowerSuit") { - return { InventoryChanges: addMotorcycle(inventory, typeName) }; + return addMotorcycle(inventory, typeName); } break; } @@ -524,11 +692,11 @@ export const addItems = async ( let inventoryDelta; for (const item of items) { if (typeof item === "string") { - inventoryDelta = await addItem(inventory, item); + inventoryDelta = await addItem(inventory, item, 1, true); } else { - inventoryDelta = await addItem(inventory, item.ItemType, item.ItemCount); + inventoryDelta = await addItem(inventory, item.ItemType, item.ItemCount, true); } - combineInventoryChanges(inventoryChanges, inventoryDelta.InventoryChanges); + combineInventoryChanges(inventoryChanges, inventoryDelta); } return inventoryChanges; }; @@ -544,6 +712,9 @@ export const applyDefaultUpgrades = ( for (const defaultUpgrade of defaultUpgrades) { modsToGive.push({ ItemType: defaultUpgrade.ItemType, ItemCount: 1 }); if (defaultUpgrade.Slot != -1) { + while (upgrades.length < defaultUpgrade.Slot) { + upgrades.push(""); + } upgrades[defaultUpgrade.Slot] = defaultUpgrade.ItemType; } } @@ -556,65 +727,105 @@ export const applyDefaultUpgrades = ( }; //TODO: maybe genericMethod for all the add methods, they share a lot of logic -export const addSentinel = ( +const addSentinel = ( inventory: TInventoryDatabaseDocument, sentinelName: string, + premiumPurchase: boolean, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { + // Sentinel itself occupies a slot in the sentinels bin + combineInventoryChanges(inventoryChanges, occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase)); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (ExportSentinels[sentinelName]?.defaultWeapon) { - addSentinelWeapon(inventory, ExportSentinels[sentinelName].defaultWeapon, inventoryChanges); + addSentinelWeapon(inventory, ExportSentinels[sentinelName].defaultWeapon, premiumPurchase, inventoryChanges); } // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const configs: IItemConfig[] = applyDefaultUpgrades(inventory, ExportSentinels[sentinelName]?.defaultUpgrades); - const sentinelIndex = inventory.Sentinels.push({ ItemType: sentinelName, Configs: configs, XP: 0 }) - 1; + const features = premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined; + const sentinelIndex = + inventory.Sentinels.push({ ItemType: sentinelName, Configs: configs, XP: 0, Features: features }) - 1; inventoryChanges.Sentinels ??= []; inventoryChanges.Sentinels.push(inventory.Sentinels[sentinelIndex].toJSON()); return inventoryChanges; }; -export const addSentinelWeapon = ( +const addSentinelWeapon = ( inventory: TInventoryDatabaseDocument, typeName: string, + premiumPurchase: boolean, inventoryChanges: IInventoryChanges ): void => { + // Sentinel weapons also occupy a slot in the sentinels bin + combineInventoryChanges(inventoryChanges, occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase)); + const index = inventory.SentinelWeapons.push({ ItemType: typeName, XP: 0 }) - 1; inventoryChanges.SentinelWeapons ??= []; inventoryChanges.SentinelWeapons.push(inventory.SentinelWeapons[index].toJSON()); }; -export const addPowerSuit = ( +export const addPowerSuit = async ( inventory: TInventoryDatabaseDocument, powersuitName: string, - inventoryChanges: IInventoryChanges = {} -): IInventoryChanges => { - const specialItems = getExalted(powersuitName); - if (specialItems) { - for (const specialItem of specialItems) { - addSpecialItem(inventory, specialItem, inventoryChanges); + inventoryChanges: IInventoryChanges = {}, + features: number | undefined = undefined +): Promise => { + const powersuit = ExportWarframes[powersuitName] as IPowersuit | undefined; + const exalted = powersuit?.exalted ?? []; + for (const specialItem of exalted) { + addSpecialItem(inventory, specialItem, inventoryChanges); + } + if (powersuit?.additionalItems) { + for (const item of powersuit.additionalItems) { + if (exalted.indexOf(item) == -1) { + combineInventoryChanges(inventoryChanges, await addItem(inventory, item, 1)); + } } } - const suitIndex = inventory.Suits.push({ ItemType: powersuitName, Configs: [], UpgradeVer: 101, XP: 0 }) - 1; + const suitIndex = + inventory.Suits.push({ + ItemType: powersuitName, + Configs: [], + UpgradeVer: 101, + XP: 0, + Features: features, + IsNew: true + }) - 1; inventoryChanges.Suits ??= []; inventoryChanges.Suits.push(inventory.Suits[suitIndex].toJSON()); return inventoryChanges; }; -export const addMechSuit = ( +export const addMechSuit = async ( inventory: TInventoryDatabaseDocument, mechsuitName: string, - inventoryChanges: IInventoryChanges = {} -): IInventoryChanges => { - const specialItems = getExalted(mechsuitName); - if (specialItems) { - for (const specialItem of specialItems) { - addSpecialItem(inventory, specialItem, inventoryChanges); + inventoryChanges: IInventoryChanges = {}, + features: number | undefined = undefined +): Promise => { + const powersuit = ExportWarframes[mechsuitName] as IPowersuit | undefined; + const exalted = powersuit?.exalted ?? []; + for (const specialItem of exalted) { + addSpecialItem(inventory, specialItem, inventoryChanges); + } + if (powersuit?.additionalItems) { + for (const item of powersuit.additionalItems) { + if (exalted.indexOf(item) == -1) { + combineInventoryChanges(inventoryChanges, await addItem(inventory, item, 1)); + } } } - const suitIndex = inventory.MechSuits.push({ ItemType: mechsuitName, Configs: [], UpgradeVer: 101, XP: 0 }) - 1; + const suitIndex = + inventory.MechSuits.push({ + ItemType: mechsuitName, + Configs: [], + UpgradeVer: 101, + XP: 0, + Features: features, + IsNew: true + }) - 1; inventoryChanges.MechSuits ??= []; inventoryChanges.MechSuits.push(inventory.MechSuits[suitIndex].toJSON()); return inventoryChanges; @@ -643,9 +854,18 @@ export const addSpecialItem = ( export const addSpaceSuit = ( inventory: TInventoryDatabaseDocument, spacesuitName: string, - inventoryChanges: IInventoryChanges = {} + inventoryChanges: IInventoryChanges = {}, + features: number | undefined = undefined ): IInventoryChanges => { - const suitIndex = inventory.SpaceSuits.push({ ItemType: spacesuitName, Configs: [], UpgradeVer: 101, XP: 0 }) - 1; + const suitIndex = + inventory.SpaceSuits.push({ + ItemType: spacesuitName, + Configs: [], + UpgradeVer: 101, + XP: 0, + Features: features, + IsNew: true + }) - 1; inventoryChanges.SpaceSuits ??= []; inventoryChanges.SpaceSuits.push(inventory.SpaceSuits[suitIndex].toJSON()); return inventoryChanges; @@ -658,9 +878,8 @@ export const updateSlots = ( extraAmount: number ): void => { inventory[slotName].Slots += slotAmount; - if (inventory[slotName].Extra === undefined) { - inventory[slotName].Extra = extraAmount; - } else { + if (extraAmount != 0) { + inventory[slotName].Extra ??= 0; inventory[slotName].Extra += extraAmount; } }; @@ -673,8 +892,8 @@ export const updateCurrency = ( inventory: TInventoryDatabaseDocument, price: number, usePremium: boolean -): ICurrencyChanges => { - const currencyChanges: ICurrencyChanges = {}; +): IInventoryChanges => { + const currencyChanges: IInventoryChanges = {}; if (price != 0 && isCurrencyTracked(usePremium)) { if (usePremium) { if (inventory.PremiumCreditsFree > 0) { @@ -732,14 +951,27 @@ export const updateStandingLimit = ( }; // TODO: AffiliationMods support (Nightwave). -export const updateGeneric = async (data: IGenericUpdate, accountId: string): Promise => { - const inventory = await getInventory(accountId); +export const updateGeneric = async (data: IGenericUpdate, accountId: string): Promise => { + const inventory = await getInventory(accountId, "NodeIntrosCompleted MiscItems"); // Make it an array for easier parsing. if (typeof data.NodeIntrosCompleted === "string") { data.NodeIntrosCompleted = [data.NodeIntrosCompleted]; } + const inventoryChanges: IInventoryChanges = {}; + for (const node of data.NodeIntrosCompleted) { + if (node == "KayaFirstVisitPack") { + inventoryChanges.MiscItems = [ + { + ItemType: "/Lotus/Types/Items/MiscItems/1999FixedStickersPack", + ItemCount: 1 + } + ]; + addMiscItems(inventory, inventoryChanges.MiscItems); + } + } + // Combine the two arrays into one. data.NodeIntrosCompleted = inventory.NodeIntrosCompleted.concat(data.NodeIntrosCompleted); @@ -748,15 +980,11 @@ export const updateGeneric = async (data: IGenericUpdate, accountId: string): Pr inventory.NodeIntrosCompleted = nodes; await inventory.save(); -}; -export const updateTheme = async (data: IThemeUpdateRequest, accountId: string): Promise => { - const inventory = await getInventory(accountId); - if (data.Style) inventory.ThemeStyle = data.Style; - if (data.Background) inventory.ThemeBackground = data.Background; - if (data.Sounds) inventory.ThemeSounds = data.Sounds; - - await inventory.save(); + return { + MissionRewards: [], + InventoryChanges: inventoryChanges + }; }; export const addEquipment = ( @@ -772,7 +1000,8 @@ export const addEquipment = ( ItemType: type, Configs: [], XP: 0, - ModularParts: modularParts + ModularParts: modularParts, + IsNew: true }, defaultOverwrites ); @@ -789,6 +1018,7 @@ export const addCustomization = ( inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { const flavourItemIndex = inventory.FlavourItems.push({ ItemType: customizationName }) - 1; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition inventoryChanges.FlavourItems ??= []; (inventoryChanges.FlavourItems as IFlavourItem[]).push( inventory.FlavourItems[flavourItemIndex].toJSON() @@ -801,7 +1031,8 @@ export const addSkin = ( typeName: string, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { - const index = inventory.WeaponSkins.push({ ItemType: typeName }) - 1; + const index = inventory.WeaponSkins.push({ ItemType: typeName, IsNew: true }) - 1; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition inventoryChanges.WeaponSkins ??= []; (inventoryChanges.WeaponSkins as IWeaponSkinClient[]).push( inventory.WeaponSkins[index].toJSON() @@ -809,17 +1040,32 @@ export const addSkin = ( return inventoryChanges; }; +const addCrewShipWeaponSkin = ( + inventory: TInventoryDatabaseDocument, + typeName: string, + inventoryChanges: IInventoryChanges = {} +): IInventoryChanges => { + const index = inventory.CrewShipWeaponSkins.push({ ItemType: typeName }) - 1; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + inventoryChanges.CrewShipWeaponSkins ??= []; + (inventoryChanges.CrewShipWeaponSkins as IUpgradeClient[]).push( + inventory.CrewShipWeaponSkins[index].toJSON() + ); + return inventoryChanges; +}; + const addCrewShip = ( inventory: TInventoryDatabaseDocument, typeName: string, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { if (inventory.CrewShips.length != 0) { - throw new Error("refusing to add CrewShip because account already has one"); + logger.warn("refusing to add CrewShip because account already has one"); + } else { + const index = inventory.CrewShips.push({ ItemType: typeName }) - 1; + inventoryChanges.CrewShips ??= []; + inventoryChanges.CrewShips.push(inventory.CrewShips[index].toJSON()); } - const index = inventory.CrewShips.push({ ItemType: typeName }) - 1; - inventoryChanges.CrewShips ??= []; - inventoryChanges.CrewShips.push(inventory.CrewShips[index].toJSON()); return inventoryChanges; }; @@ -828,12 +1074,13 @@ const addCrewShipHarness = ( typeName: string, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { - if (inventory.CrewShips.length != 0) { - throw new Error("refusing to add CrewShipHarness because account already has one"); + if (inventory.CrewShipHarnesses.length != 0) { + logger.warn("refusing to add CrewShipHarness because account already has one"); + } else { + const index = inventory.CrewShipHarnesses.push({ ItemType: typeName }) - 1; + inventoryChanges.CrewShipHarnesses ??= []; + inventoryChanges.CrewShipHarnesses.push(inventory.CrewShipHarnesses[index].toJSON()); } - const index = inventory.CrewShipHarnesses.push({ ItemType: typeName }) - 1; - inventoryChanges.CrewShipHarnesses ??= []; - inventoryChanges.CrewShipHarnesses.push(inventory.CrewShipHarnesses[index].toJSON()); return inventoryChanges; }; @@ -843,11 +1090,12 @@ const addMotorcycle = ( inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { if (inventory.Motorcycles.length != 0) { - throw new Error("refusing to add Motorcycle because account already has one"); + logger.warn("refusing to add Motorcycle because account already has one"); + } else { + const index = inventory.Motorcycles.push({ ItemType: typeName }) - 1; + inventoryChanges.Motorcycles ??= []; + inventoryChanges.Motorcycles.push(inventory.Motorcycles[index].toJSON()); } - const index = inventory.Motorcycles.push({ ItemType: typeName }) - 1; - inventoryChanges.Motorcycles ??= []; - inventoryChanges.Motorcycles.push(inventory.Motorcycles[index].toJSON()); return inventoryChanges; }; @@ -862,15 +1110,37 @@ const addDrone = ( return inventoryChanges; }; +export const addEmailItem = async ( + inventory: TInventoryDatabaseDocument, + typeName: string, + inventoryChanges: IInventoryChanges = {} +): Promise => { + const meta = ExportEmailItems[typeName]; + const emailItem = inventory.EmailItems.find(x => x.ItemType == typeName); + if (!emailItem || !meta.sendOnlyOnce) { + await createMessage(inventory.accountOwnerId, [convertInboxMessage(meta.message)]); + + if (emailItem) { + emailItem.ItemCount += 1; + } else { + inventory.EmailItems.push({ ItemType: typeName, ItemCount: 1 }); + } + + inventoryChanges.EmailItems ??= []; + inventoryChanges.EmailItems.push({ ItemType: typeName, ItemCount: 1 }); + } + return inventoryChanges; +}; + //TODO: wrong id is not erroring export const addGearExpByCategory = ( inventory: TInventoryDatabaseDocument, - gearArray: IEquipmentClient[] | undefined, + gearArray: IEquipmentClient[], categoryName: TEquipmentKey ): void => { const category = inventory[categoryName]; - gearArray?.forEach(({ ItemId, XP }) => { + gearArray.forEach(({ ItemId, XP }) => { if (!XP) { return; } @@ -894,10 +1164,10 @@ export const addGearExpByCategory = ( }); }; -export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray: IMiscItem[] | undefined): void => { +export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray: IMiscItem[]): void => { const { MiscItems } = inventory; - itemsArray?.forEach(({ ItemCount, ItemType }) => { + itemsArray.forEach(({ ItemCount, ItemType }) => { if (ItemCount == 0) { return; } @@ -908,6 +1178,22 @@ export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray: } MiscItems[itemIndex].ItemCount += ItemCount; + + if (ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal" && ItemCount > 0) { + inventory.FoundToday ??= []; + let foundTodayIndex = inventory.FoundToday.findIndex(x => x.ItemType == ItemType); + if (foundTodayIndex == -1) { + foundTodayIndex = inventory.FoundToday.push({ ItemType, ItemCount: 0 }) - 1; + } + inventory.FoundToday[foundTodayIndex].ItemCount += ItemCount; + if (inventory.FoundToday[foundTodayIndex].ItemCount <= 0) { + inventory.FoundToday.splice(foundTodayIndex, 1); + } + if (inventory.FoundToday.length == 0) { + inventory.FoundToday = undefined; + } + } + if (MiscItems[itemIndex].ItemCount == 0) { MiscItems.splice(itemIndex, 1); } else if (MiscItems[itemIndex].ItemCount <= 0) { @@ -916,86 +1202,48 @@ export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray: }); }; -export const addShipDecorations = ( - inventory: TInventoryDatabaseDocument, - itemsArray: IConsumable[] | undefined -): void => { - const { ShipDecorations } = inventory; +const applyArrayChanges = (arr: ITypeCount[], changes: ITypeCount[]): void => { + for (const change of changes) { + if (change.ItemCount != 0) { + let itemIndex = arr.findIndex(x => x.ItemType === change.ItemType); + if (itemIndex == -1) { + itemIndex = arr.push({ ItemType: change.ItemType, ItemCount: 0 }) - 1; + } - itemsArray?.forEach(({ ItemCount, ItemType }) => { - const itemIndex = ShipDecorations.findIndex(miscItem => miscItem.ItemType === ItemType); - - if (itemIndex !== -1) { - ShipDecorations[itemIndex].ItemCount += ItemCount; - } else { - ShipDecorations.push({ ItemCount, ItemType }); + arr[itemIndex].ItemCount += change.ItemCount; + if (arr[itemIndex].ItemCount == 0) { + arr.splice(itemIndex, 1); + } else if (arr[itemIndex].ItemCount <= 0) { + logger.warn(`account now owns a negative amount of ${change.ItemType}`); + } } - }); + } }; -export const addConsumables = (inventory: TInventoryDatabaseDocument, itemsArray: IConsumable[] | undefined): void => { - const { Consumables } = inventory; - - itemsArray?.forEach(({ ItemCount, ItemType }) => { - const itemIndex = Consumables.findIndex(i => i.ItemType === ItemType); - - if (itemIndex !== -1) { - Consumables[itemIndex].ItemCount += ItemCount; - } else { - Consumables.push({ ItemCount, ItemType }); - } - }); +export const addShipDecorations = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { + applyArrayChanges(inventory.ShipDecorations, itemsArray); }; -export const addCrewShipRawSalvage = ( - inventory: TInventoryDatabaseDocument, - itemsArray: ITypeCount[] | undefined -): void => { - const { CrewShipRawSalvage } = inventory; - - itemsArray?.forEach(({ ItemCount, ItemType }) => { - const itemIndex = CrewShipRawSalvage.findIndex(i => i.ItemType === ItemType); - - if (itemIndex !== -1) { - CrewShipRawSalvage[itemIndex].ItemCount += ItemCount; - } else { - CrewShipRawSalvage.push({ ItemCount, ItemType }); - } - }); +export const addConsumables = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { + applyArrayChanges(inventory.Consumables, itemsArray); }; -export const addCrewShipAmmo = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[] | undefined): void => { - const { CrewShipAmmo } = inventory; - - itemsArray?.forEach(({ ItemCount, ItemType }) => { - const itemIndex = CrewShipAmmo.findIndex(i => i.ItemType === ItemType); - - if (itemIndex !== -1) { - CrewShipAmmo[itemIndex].ItemCount += ItemCount; - } else { - CrewShipAmmo.push({ ItemCount, ItemType }); - } - }); +export const addCrewShipRawSalvage = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { + applyArrayChanges(inventory.CrewShipRawSalvage, itemsArray); }; -export const addRecipes = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[] | undefined): void => { - const { Recipes } = inventory; - - itemsArray?.forEach(({ ItemCount, ItemType }) => { - const itemIndex = Recipes.findIndex(i => i.ItemType === ItemType); - - if (itemIndex !== -1) { - Recipes[itemIndex].ItemCount += ItemCount; - } else { - Recipes.push({ ItemCount, ItemType }); - } - }); +export const addCrewShipAmmo = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { + applyArrayChanges(inventory.CrewShipAmmo, itemsArray); }; -export const addMods = (inventory: TInventoryDatabaseDocument, itemsArray: IRawUpgrade[] | undefined): void => { +export const addRecipes = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { + applyArrayChanges(inventory.Recipes, itemsArray); +}; + +export const addMods = (inventory: TInventoryDatabaseDocument, itemsArray: IRawUpgrade[]): void => { const { RawUpgrades } = inventory; - itemsArray?.forEach(({ ItemType, ItemCount }) => { + itemsArray.forEach(({ ItemType, ItemCount }) => { if (ItemCount == 0) { return; } @@ -1014,23 +1262,25 @@ export const addMods = (inventory: TInventoryDatabaseDocument, itemsArray: IRawU }); }; -export const addFusionTreasures = ( - inventory: TInventoryDatabaseDocument, - itemsArray: IFusionTreasure[] | undefined -): void => { +export const addFusionTreasures = (inventory: TInventoryDatabaseDocument, itemsArray: IFusionTreasure[]): void => { const { FusionTreasures } = inventory; - itemsArray?.forEach(({ ItemType, ItemCount, Sockets }) => { + itemsArray.forEach(({ ItemType, ItemCount, Sockets }) => { const itemIndex = FusionTreasures.findIndex(i => i.ItemType == ItemType && (i.Sockets || 0) == (Sockets || 0)); if (itemIndex !== -1) { FusionTreasures[itemIndex].ItemCount += ItemCount; + if (FusionTreasures[itemIndex].ItemCount == 0) { + FusionTreasures.splice(itemIndex, 1); + } else if (FusionTreasures[itemIndex].ItemCount <= 0) { + logger.warn(`account now owns a negative amount of ${ItemType}`); + } } else { FusionTreasures.push({ ItemCount, ItemType, Sockets }); } }); }; -export const addFocusXpIncreases = (inventory: TInventoryDatabaseDocument, focusXpPlus: number[] | undefined): void => { +export const addFocusXpIncreases = (inventory: TInventoryDatabaseDocument, focusXpPlus: number[]): void => { enum FocusType { AP_UNIVERSAL, AP_ATTACK, @@ -1044,35 +1294,23 @@ export const addFocusXpIncreases = (inventory: TInventoryDatabaseDocument, focus AP_ANY } - if (focusXpPlus) { - inventory.FocusXP ??= { AP_ATTACK: 0, AP_DEFENSE: 0, AP_TACTIC: 0, AP_POWER: 0, AP_WARD: 0 }; - inventory.FocusXP.AP_ATTACK += focusXpPlus[FocusType.AP_ATTACK]; - inventory.FocusXP.AP_DEFENSE += focusXpPlus[FocusType.AP_DEFENSE]; - inventory.FocusXP.AP_TACTIC += focusXpPlus[FocusType.AP_TACTIC]; - inventory.FocusXP.AP_POWER += focusXpPlus[FocusType.AP_POWER]; - inventory.FocusXP.AP_WARD += focusXpPlus[FocusType.AP_WARD]; - } -}; + inventory.FocusXP ??= { AP_ATTACK: 0, AP_DEFENSE: 0, AP_TACTIC: 0, AP_POWER: 0, AP_WARD: 0 }; + inventory.FocusXP.AP_ATTACK += focusXpPlus[FocusType.AP_ATTACK]; + inventory.FocusXP.AP_DEFENSE += focusXpPlus[FocusType.AP_DEFENSE]; + inventory.FocusXP.AP_TACTIC += focusXpPlus[FocusType.AP_TACTIC]; + inventory.FocusXP.AP_POWER += focusXpPlus[FocusType.AP_POWER]; + inventory.FocusXP.AP_WARD += focusXpPlus[FocusType.AP_WARD]; -export const updateChallengeProgress = async ( - challenges: IUpdateChallengeProgressRequest, - accountId: string -): Promise => { - const inventory = await getInventory(accountId); - - addChallenges(inventory, challenges.ChallengeProgress); - addSeasonalChallengeHistory(inventory, challenges.SeasonChallengeHistory); - - await inventory.save(); + inventory.DailyFocus -= focusXpPlus.reduce((a, b) => a + b, 0); }; export const addSeasonalChallengeHistory = ( inventory: TInventoryDatabaseDocument, - itemsArray: ISeasonChallenge[] | undefined + itemsArray: ISeasonChallenge[] ): void => { const category = inventory.SeasonChallengeHistory; - itemsArray?.forEach(({ challenge, id }) => { + itemsArray.forEach(({ challenge, id }) => { const itemIndex = category.findIndex(i => i.challenge === challenge); if (itemIndex !== -1) { @@ -1083,17 +1321,14 @@ export const addSeasonalChallengeHistory = ( }); }; -export const addChallenges = ( - inventory: TInventoryDatabaseDocument, - itemsArray: IChallengeProgress[] | undefined -): void => { +export const addChallenges = (inventory: TInventoryDatabaseDocument, itemsArray: IChallengeProgress[]): void => { const category = inventory.ChallengeProgress; - itemsArray?.forEach(({ Name, Progress }) => { + itemsArray.forEach(({ Name, Progress }) => { const itemIndex = category.findIndex(i => i.Name === Name); if (itemIndex !== -1) { - category[itemIndex].Progress += Progress; + category[itemIndex].Progress = Progress; } else { category.push({ Name, Progress }); } @@ -1112,7 +1347,7 @@ export const addMissionComplete = (inventory: TInventoryDatabaseDocument, { Tag, }; export const addBooster = (ItemType: string, time: number, inventory: TInventoryDatabaseDocument): void => { - const currentTime = Math.floor(Date.now() / 1000) - 129600; // Value is wrong without 129600. Figure out why, please. :) + const currentTime = Math.floor(Date.now() / 1000); const { Boosters } = inventory; @@ -1127,7 +1362,7 @@ export const addBooster = (ItemType: string, time: number, inventory: TInventory }; export const updateSyndicate = ( - inventory: HydratedDocument, + inventory: TInventoryDatabaseDocument, syndicateUpdate: IMissionInventoryUpdateRequest["AffiliationChanges"] ): void => { syndicateUpdate?.forEach(affiliation => { @@ -1161,27 +1396,30 @@ export const addKeyChainItems = async ( `adding key chain items ${keyChainItems.join()} for ${keyChainData.KeyChain} at stage ${keyChainData.ChainStage}` ); - const nonStoreItems = keyChainItems.map(item => item.replace("StoreItems/", "")); + const nonStoreItems = keyChainItems.map(item => fromStoreItem(item)); - //TODO: inventoryChanges is not typed correctly - const inventoryChanges = {}; + const inventoryChanges: IInventoryChanges = {}; for (const item of nonStoreItems) { const inventoryChangesDelta = await addItem(inventory, item); - combineInventoryChanges(inventoryChanges, inventoryChangesDelta.InventoryChanges); + combineInventoryChanges(inventoryChanges, inventoryChangesDelta); } return inventoryChanges; }; -const createLibraryAvailableDailyTaskInfo = (): ILibraryAvailableDailyTaskInfo => { + +export const createLibraryDailyTask = (): ILibraryDailyTaskInfo => { + const enemyTypes = getRandomElement(libraryDailyTasks); + const enemyAvatar = ExportEnemies.avatars[enemyTypes[0]]; + const scansRequired = getRandomInt(2, 4); return { - EnemyTypes: ["/Lotus/Types/Enemies/Orokin/RifleLancerAvatar"], - EnemyLocTag: "/Lotus/Language/Game/CorruptedLancer", - EnemyIcon: "/Lotus/Interface/Icons/Npcs/OrokinRifleLancerAvatar.png", - ScansRequired: 3, - RewardStoreItem: "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/UncommonFusionBundle", - RewardQuantity: 7, - RewardStanding: 7500 + EnemyTypes: enemyTypes, + EnemyLocTag: enemyAvatar.name, + EnemyIcon: enemyAvatar.icon!, + ScansRequired: scansRequired, + RewardStoreItem: "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/RareFusionBundle", + RewardQuantity: Math.trunc(scansRequired * 2.5), + RewardStanding: 2500 * scansRequired }; }; @@ -1198,3 +1436,20 @@ const createCalendar = (): ICalendarProgress => { } }; }; + +export const setupKahlSyndicate = (inventory: TInventoryDatabaseDocument): void => { + inventory.Affiliations.push({ + Title: 1, + Standing: 1, + WeeklyMissions: [ + { + MissionIndex: 0, + CompletedMission: false, + JobManifest: "/Lotus/Syndicates/Kahl/KahlJobManifestVersionThree", + WeekCount: 0, + Challenges: [] + } + ], + Tag: "KahlSyndicate" + }); +}; diff --git a/src/services/itemDataService.ts b/src/services/itemDataService.ts index c18f3021..f6feae12 100644 --- a/src/services/itemDataService.ts +++ b/src/services/itemDataService.ts @@ -1,4 +1,4 @@ -import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; +import { IKeyChainRequest } from "@/src/types/requestTypes"; import { getIndexAfter } from "@/src/helpers/stringHelpers"; import { ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; import { logger } from "@/src/utils/logger"; @@ -20,19 +20,22 @@ import { dict_zh, ExportArcanes, ExportCustoms, + ExportDrones, ExportGear, ExportKeys, ExportRecipes, - ExportRegions, ExportResources, ExportSentinels, ExportWarframes, ExportWeapons, - IPowersuit, + IDefaultUpgrade, + IInboxMessage, + IMissionReward, IRecipe, - IRegion + TReward } from "warframe-public-export-plus"; import questCompletionItems from "@/static/fixed_responses/questCompletionRewards.json"; +import { IMessage } from "../models/inboxModel"; export type WeaponTypeInternal = | "LongGuns" @@ -52,10 +55,6 @@ export const getRecipeByResult = (resultType: string): IRecipe | undefined => { return Object.values(ExportRecipes).find(x => x.resultType == resultType); }; -export const getExalted = (uniqueName: string): string[] | undefined => { - return getSuitByUniqueName(uniqueName)?.exalted; -}; - export const getItemCategoryByUniqueName = (uniqueName: string): string => { //Lotus/Types/Items/MiscItems/PolymerBundle @@ -72,10 +71,6 @@ export const getItemCategoryByUniqueName = (uniqueName: string): string => { return category; }; -export const getSuitByUniqueName = (uniqueName: string): IPowersuit | undefined => { - return ExportWarframes[uniqueName]; -}; - export const getItemName = (uniqueName: string): string | undefined => { if (uniqueName in ExportArcanes) { return ExportArcanes[uniqueName].name; @@ -83,6 +78,9 @@ export const getItemName = (uniqueName: string): string | undefined => { if (uniqueName in ExportCustoms) { return ExportCustoms[uniqueName].name; } + if (uniqueName in ExportDrones) { + return ExportDrones[uniqueName].name; + } if (uniqueName in ExportKeys) { return ExportKeys[uniqueName].name; } @@ -149,6 +147,7 @@ export const getKeyChainItems = ({ KeyChain, ChainStage }: IKeyChainRequest): st } const keyChainStage = chainStages[ChainStage]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!keyChainStage) { throw new Error(`KeyChainStage ${ChainStage} not found`); } @@ -162,13 +161,15 @@ export const getKeyChainItems = ({ KeyChain, ChainStage }: IKeyChainRequest): st return keyChainStage.itemsToGiveWhenTriggered; }; -export const getLevelKeyRewards = (levelKey: string) => { - if (!ExportKeys[levelKey]) { +export const getLevelKeyRewards = ( + levelKey: string +): { levelKeyRewards?: IMissionReward; levelKeyRewards2?: TReward[] } => { + if (!(levelKey in ExportKeys)) { throw new Error(`LevelKey ${levelKey} not found`); } - const levelKeyRewards = ExportKeys[levelKey]?.missionReward; - const levelKeyRewards2 = ExportKeys[levelKey]?.rewards; + const levelKeyRewards = ExportKeys[levelKey].missionReward; + const levelKeyRewards2 = ExportKeys[levelKey].rewards; if (!levelKeyRewards && !levelKeyRewards2) { throw new Error(`LevelKey ${levelKey} does not contain either rewards1 or rewards2`); @@ -180,33 +181,41 @@ export const getLevelKeyRewards = (levelKey: string) => { }; }; -export const getNode = (nodeName: string): IRegion => { - const node = ExportRegions[nodeName]; - if (!node) { - throw new Error(`Node ${nodeName} not found`); +export const getQuestCompletionItems = (questKey: string): ITypeCount[] | undefined => { + if (questKey in questCompletionItems) { + return questCompletionItems[questKey as keyof typeof questCompletionItems]; } + logger.warn(`Quest ${questKey} not found in questCompletionItems`); - return node; -}; - -export const getQuestCompletionItems = (questKey: string) => { - const items = (questCompletionItems as unknown as Record | undefined)?.[questKey]; - - if (!items) { - logger.error( - `Quest ${questKey} not found in questCompletionItems, quest completion items have not been given. This is a temporary solution` - ); + const items: ITypeCount[] = []; + const meta = ExportKeys[questKey]; + if (meta.rewards) { + for (const reward of meta.rewards) { + if (reward.rewardType == "RT_STORE_ITEM") { + items.push({ + ItemType: fromStoreItem(reward.itemType), + ItemCount: 1 + }); + } else if (reward.rewardType == "RT_RESOURCE" || reward.rewardType == "RT_RECIPE") { + items.push({ + ItemType: reward.itemType, + ItemCount: reward.amount + }); + } + } } return items; }; -export const getKeyChainMessage = ({ KeyChain, ChainStage }: IKeyChainRequest) => { +export const getKeyChainMessage = ({ KeyChain, ChainStage }: IKeyChainRequest): IMessage => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const chainStages = ExportKeys[KeyChain]?.chainStages; if (!chainStages) { throw new Error(`KeyChain ${KeyChain} does not contain chain stages`); } const keyChainStage = chainStages[ChainStage]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!keyChainStage) { throw new Error(`KeyChainStage ${ChainStage} not found`); } @@ -218,5 +227,43 @@ export const getKeyChainMessage = ({ KeyChain, ChainStage }: IKeyChainRequest) = `client requested key chain message in keychain ${KeyChain} at stage ${ChainStage} but they did not exist` ); } - return chainStageMessage; + return convertInboxMessage(chainStageMessage); +}; + +export const convertInboxMessage = (message: IInboxMessage): IMessage => { + return { + sndr: message.sender, + msg: message.body, + sub: message.title, + att: message.attachments.length > 0 ? message.attachments : undefined, + countedAtt: message.countedAttachments.length > 0 ? message.countedAttachments : undefined, + icon: message.icon ?? "", + transmission: message.transmission ?? "", + highPriority: message.highPriority ?? false, + r: false + } satisfies IMessage; +}; + +export const isStoreItem = (type: string): boolean => { + return type.startsWith("/Lotus/StoreItems/"); +}; + +export const toStoreItem = (type: string): string => { + return "/Lotus/StoreItems/" + type.substring("/Lotus/".length); +}; + +export const fromStoreItem = (type: string): string => { + return "/Lotus/" + type.substring("/Lotus/StoreItems/".length); +}; + +export const getDefaultUpgrades = (parts: string[]): IDefaultUpgrade[] | undefined => { + const allDefaultUpgrades: IDefaultUpgrade[] = []; + for (const part of parts) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const defaultUpgrades = ExportWeapons[part]?.defaultUpgrades; + if (defaultUpgrades) { + allDefaultUpgrades.push(...defaultUpgrades); + } + } + return allDefaultUpgrades.length == 0 ? undefined : allDefaultUpgrades; }; diff --git a/src/services/leaderboardService.ts b/src/services/leaderboardService.ts new file mode 100644 index 00000000..b0e03518 --- /dev/null +++ b/src/services/leaderboardService.ts @@ -0,0 +1,96 @@ +import { Guild } from "../models/guildModel"; +import { Leaderboard, TLeaderboardEntryDocument } from "../models/leaderboardModel"; +import { ILeaderboardEntryClient } from "../types/leaderboardTypes"; + +export const submitLeaderboardScore = async ( + schedule: "weekly" | "daily", + leaderboard: string, + ownerId: string, + displayName: string, + score: number, + guildId?: string +): Promise => { + let expiry: Date; + if (schedule == "daily") { + expiry = new Date(Math.trunc(Date.now() / 86400000) * 86400000 + 86400000); + } else { + const EPOCH = 1734307200 * 1000; // Monday + const week = Math.trunc((Date.now() - EPOCH) / 604800000); + const weekStart = EPOCH + week * 604800000; + const weekEnd = weekStart + 604800000; + expiry = new Date(weekEnd); + } + await Leaderboard.findOneAndUpdate( + { leaderboard: `${schedule}.accounts.${leaderboard}`, ownerId }, + { $max: { score }, $set: { displayName, guildId, expiry } }, + { upsert: true } + ); + if (guildId) { + const guild = (await Guild.findById(guildId, "Name Tier"))!; + await Leaderboard.findOneAndUpdate( + { leaderboard: `${schedule}.guilds.${leaderboard}`, ownerId: guildId }, + { $max: { score }, $set: { displayName: guild.Name, guildTier: guild.Tier, expiry } }, + { upsert: true } + ); + } +}; + +export const getLeaderboard = async ( + leaderboard: string, + before: number, + after: number, + pivotId?: string, + guildId?: string, + guildTier?: number +): Promise => { + const filter: { leaderboard: string; guildId?: string; guildTier?: number } = { leaderboard }; + if (guildId) { + filter.guildId = guildId; + } + if (guildTier) { + filter.guildTier = guildTier; + } + + let entries: TLeaderboardEntryDocument[]; + let r: number; + if (pivotId) { + const pivotDoc = await Leaderboard.findOne({ ...filter, ownerId: pivotId }); + if (!pivotDoc) { + return []; + } + const beforeDocs = await Leaderboard.find({ + ...filter, + score: { $gt: pivotDoc.score } + }) + .sort({ score: 1 }) + .limit(before); + const afterDocs = await Leaderboard.find({ + ...filter, + score: { $lt: pivotDoc.score } + }) + .sort({ score: -1 }) + .limit(after); + entries = [...beforeDocs.reverse(), pivotDoc, ...afterDocs]; + r = + (await Leaderboard.countDocuments({ + ...filter, + score: { $gt: pivotDoc.score } + })) - beforeDocs.length; + } else { + entries = await Leaderboard.find(filter) + .sort({ score: -1 }) + .skip(before) + .limit(after - before); + r = before; + } + const res: ILeaderboardEntryClient[] = []; + for (const entry of entries) { + res.push({ + _id: entry.ownerId.toString(), + s: entry.score, + r: ++r, + n: entry.displayName + }); + } + return res; +}; diff --git a/src/services/loadoutService.ts b/src/services/loadoutService.ts index f9c385c5..265a8150 100644 --- a/src/services/loadoutService.ts +++ b/src/services/loadoutService.ts @@ -1,6 +1,6 @@ -import { Loadout } from "@/src/models/inventoryModels/loadoutModel"; +import { Loadout, TLoadoutDatabaseDocument } from "@/src/models/inventoryModels/loadoutModel"; -export const getLoadout = async (accountId: string) => { +export const getLoadout = async (accountId: string): Promise => { const loadout = await Loadout.findOne({ loadoutOwnerId: accountId }); if (!loadout) { diff --git a/src/services/loginRewardService.ts b/src/services/loginRewardService.ts new file mode 100644 index 00000000..2f42f04b --- /dev/null +++ b/src/services/loginRewardService.ts @@ -0,0 +1,156 @@ +import randomRewards from "@/static/fixed_responses/loginRewards/randomRewards.json"; +import { IInventoryChanges } from "../types/purchaseTypes"; +import { TAccountDocument } from "./loginService"; +import { CRng, mixSeeds } from "./rngService"; +import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel"; +import { addBooster, updateCurrency } from "./inventoryService"; +import { handleStoreItemAcquisition } from "./purchaseService"; +import { ExportBoosters, ExportRecipes, ExportWarframes, ExportWeapons } from "warframe-public-export-plus"; +import { toStoreItem } from "./itemDataService"; + +export interface ILoginRewardsReponse { + DailyTributeInfo: { + Rewards?: ILoginReward[]; // only set on first call of the day + IsMilestoneDay?: boolean; + IsChooseRewardSet?: boolean; + LoginDays?: number; // when calling multiple times per day, this is already incremented to represent "tomorrow" + NextMilestoneReward?: ""; + NextMilestoneDay?: number; // seems to not be used if IsMilestoneDay + HasChosenReward?: boolean; + NewInventory?: IInventoryChanges; + ChosenReward?: ILoginReward; + }; + LastLoginRewardDate?: number; // only set on first call of the day; today at 0 UTC +} + +export interface ILoginReward { + //_id: IOid; + RewardType: string; + //CouponType: "CPT_PLATINUM"; + Icon: string; + //ItemType: ""; + StoreItemType: string; // uniquely identifies the reward + //ProductCategory: "Pistols"; + Amount: number; + ScalingMultiplier: number; + //Durability: "COMMON"; + //DisplayName: ""; + Duration: number; + //CouponSku: number; + //Rarity: number; + Transmission: string; +} + +const scaleAmount = (day: number, amount: number, scalingMultiplier: number): number => { + const divisor = 200 / (amount * scalingMultiplier); + return amount + Math.min(day, 3000) / divisor; +}; + +// Always produces the same result for the same account _id & LoginDays pair. +export const isLoginRewardAChoice = (account: TAccountDocument): boolean => { + const accountSeed = parseInt(account._id.toString().substring(16), 16); + const rng = new CRng(mixSeeds(accountSeed, account.LoginDays)); + return rng.random() < 0.25; // Using 25% as an approximate chance for pick-a-doors. More conclusive data analysis is needed. +}; + +// Always produces the same result for the same account _id & LoginDays pair. +export const getRandomLoginRewards = ( + account: TAccountDocument, + inventory: TInventoryDatabaseDocument +): ILoginReward[] => { + const accountSeed = parseInt(account._id.toString().substring(16), 16); + const rng = new CRng(mixSeeds(accountSeed, account.LoginDays)); + const pick_a_door = rng.random() < 0.25; // Using 25% as an approximate chance for pick-a-doors. More conclusive data analysis is needed. + const rewards = [getRandomLoginReward(rng, account.LoginDays, inventory)]; + if (pick_a_door) { + do { + const reward = getRandomLoginReward(rng, account.LoginDays, inventory); + if (!rewards.find(x => x.StoreItemType == reward.StoreItemType)) { + rewards.push(reward); + } + } while (rewards.length != 3); + } + return rewards; +}; + +const getRandomLoginReward = (rng: CRng, day: number, inventory: TInventoryDatabaseDocument): ILoginReward => { + const reward = rng.randomReward(randomRewards)!; + //const reward = randomRewards.find(x => x.RewardType == "RT_BOOSTER")!; + if (reward.RewardType == "RT_RANDOM_RECIPE") { + // Not very faithful implementation but roughly the same idea + const masteredItems = new Set(); + for (const entry of inventory.XPInfo) { + masteredItems.add(entry.ItemType); + } + const unmasteredItems = new Set(); + for (const [uniqueName, data] of Object.entries(ExportWeapons)) { + if (data.totalDamage != 0 && data.variantType == "VT_NORMAL" && !masteredItems.has(uniqueName)) { + unmasteredItems.add(uniqueName); + } + } + for (const [uniqueName, data] of Object.entries(ExportWarframes)) { + if (data.variantType == "VT_NORMAL" && !masteredItems.has(uniqueName)) { + unmasteredItems.add(uniqueName); + } + } + const eligibleRecipes: string[] = []; + for (const [uniqueName, recipe] of Object.entries(ExportRecipes)) { + if (unmasteredItems.has(recipe.resultType)) { + eligibleRecipes.push(uniqueName); + } + } + if (eligibleRecipes.length == 0) { + // This account has all warframes and weapons already mastered (filthy cheater), need a different reward. + return getRandomLoginReward(rng, day, inventory); + } + reward.StoreItemType = toStoreItem(rng.randomElement(eligibleRecipes)); + } + return { + //_id: toOid(new Types.ObjectId()), + RewardType: reward.RewardType, + //CouponType: "CPT_PLATINUM", + Icon: reward.Icon ?? "", + //ItemType: "", + StoreItemType: reward.StoreItemType, + //ProductCategory: "Pistols", + Amount: reward.Duration ? 1 : Math.round(scaleAmount(day, reward.Amount, reward.ScalingMultiplier)), + ScalingMultiplier: reward.ScalingMultiplier, + //Durability: "COMMON", + //DisplayName: "", + Duration: reward.Duration ? Math.round(reward.Duration * scaleAmount(day, 1, reward.ScalingMultiplier)) : 0, + //CouponSku: 0, + //Rarity: 0, + Transmission: reward.Transmission + }; +}; + +export const claimLoginReward = async ( + inventory: TInventoryDatabaseDocument, + reward: ILoginReward +): Promise => { + switch (reward.RewardType) { + case "RT_RESOURCE": + case "RT_STORE_ITEM": + case "RT_RECIPE": + case "RT_RANDOM_RECIPE": + return (await handleStoreItemAcquisition(reward.StoreItemType, inventory, reward.Amount)).InventoryChanges; + + case "RT_CREDITS": + return updateCurrency(inventory, -reward.Amount, false); + + case "RT_BOOSTER": { + const ItemType = ExportBoosters[reward.StoreItemType].typeName; + const ExpiryDate = 3600 * reward.Duration; + addBooster(ItemType, ExpiryDate, inventory); + return { + Boosters: [{ ItemType, ExpiryDate }] + }; + } + } + throw new Error(`unknown login reward type: ${reward.RewardType}`); +}; + +export const setAccountGotLoginRewardToday = (account: TAccountDocument): void => { + account.LoginDays += 1; + account.LastLoginRewardDate = Math.trunc(Date.now() / 86400000) * 86400; +}; diff --git a/src/services/loginService.ts b/src/services/loginService.ts index 35b3feea..41e33ad3 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -1,6 +1,6 @@ import { Account } from "@/src/models/loginModel"; import { createInventory } from "@/src/services/inventoryService"; -import { IDatabaseAccount, IDatabaseAccountJson } from "@/src/types/loginTypes"; +import { IDatabaseAccountJson, IDatabaseAccountRequiredFields } from "@/src/types/loginTypes"; import { createShip } from "./shipService"; import { Document, Types } from "mongoose"; import { Loadout } from "@/src/models/inventoryModels/loadoutModel"; @@ -8,6 +8,7 @@ import { PersonalRooms } from "@/src/models/personalRoomsModel"; import { Request } from "express"; import { config } from "@/src/services/configService"; import { createStats } from "@/src/services/statsService"; +import crc32 from "crc-32"; export const isCorrectPassword = (requestPassword: string, databasePassword: string): boolean => { return requestPassword === databasePassword; @@ -17,7 +18,7 @@ export const isNameTaken = async (name: string): Promise => { return !!(await Account.findOne({ DisplayName: name })); }; -export const createAccount = async (accountData: IDatabaseAccount): Promise => { +export const createAccount = async (accountData: IDatabaseAccountRequiredFields): Promise => { const account = new Account(accountData); try { await account.save(); @@ -60,51 +61,46 @@ export const createPersonalRooms = async (accountId: Types.ObjectId, shipId: Typ await personalRooms.save(); }; -// eslint-disable-next-line @typescript-eslint/ban-types -type TAccountDocument = Document & +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export type TAccountDocument = Document & IDatabaseAccountJson & { _id: Types.ObjectId; __v: number }; export const getAccountForRequest = async (req: Request): Promise => { if (!req.query.accountId) { throw new Error("Request is missing accountId parameter"); } - if (!req.query.nonce || parseInt(req.query.nonce as string) === 0) { + const nonce: number = parseInt(req.query.nonce as string); + if (!nonce) { throw new Error("Request is missing nonce parameter"); } - const account = await Account.findOne({ - _id: req.query.accountId, - Nonce: req.query.nonce - }); - if (!account) { + + const account = await Account.findById(req.query.accountId); + if (!account || account.Nonce != nonce) { throw new Error("Invalid accountId-nonce pair"); } + if (account.Dropped && req.query.ct) { + account.Dropped = undefined; + await account.save(); + } return account; }; export const getAccountIdForRequest = async (req: Request): Promise => { - if (!req.query.accountId) { - throw new Error("Request is missing accountId parameter"); - } - if (!req.query.nonce || parseInt(req.query.nonce as string) === 0) { - throw new Error("Request is missing nonce parameter"); - } - if ( - !(await Account.exists({ - _id: req.query.accountId, - Nonce: req.query.nonce - })) - ) { - throw new Error("Invalid accountId-nonce pair"); - } - return req.query.accountId as string; + return (await getAccountForRequest(req))._id.toString(); }; export const isAdministrator = (account: TAccountDocument): boolean => { - if (!config.administratorNames) { - return false; - } - if (typeof config.administratorNames == "string") { - return config.administratorNames == account.DisplayName; - } - return !!config.administratorNames.find(x => x == account.DisplayName); + return !!config.administratorNames?.find(x => x == account.DisplayName); +}; + +const platform_magics = [753, 639, 247, 37, 60]; +export const getSuffixedName = (account: TAccountDocument): string => { + const name = account.DisplayName; + const platformId = 0; + const suffix = ((crc32.str(name.toLowerCase() + "595") >>> 0) + platform_magics[platformId]) % 1000; + return name + "#" + suffix.toString().padStart(3, "0"); +}; + +export const getAccountFromSuffixedName = (name: string): Promise => { + return Account.findOne({ DisplayName: name.split("#")[0] }); }; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 2d393e22..54620a1a 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -1,39 +1,54 @@ import { + ExportEnemies, ExportFusionBundles, ExportRegions, ExportRewards, IMissionReward as IMissionRewardExternal, + IRegion, IReward } from "warframe-public-export-plus"; import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; -import { IRngResult, getRandomReward } from "@/src/services/rngService"; -import { equipmentKeys, IInventoryDatabase, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IRngResult, getRandomElement, getRandomReward } from "@/src/services/rngService"; +import { equipmentKeys, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { + addBooster, addChallenges, addConsumables, addCrewShipAmmo, addCrewShipRawSalvage, + addEmailItem, addFocusXpIncreases, addFusionTreasures, addGearExpByCategory, + addItem, addMiscItems, addMissionComplete, addMods, addRecipes, + addShipDecorations, combineInventoryChanges, + updateCurrency, updateSyndicate } from "@/src/services/inventoryService"; import { updateQuestKey } from "@/src/services/questService"; -import { HydratedDocument } from "mongoose"; +import { Types } from "mongoose"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; -import { getLevelKeyRewards, getNode } from "@/src/services/itemDataService"; -import { InventoryDocumentProps, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; +import { getLevelKeyRewards, toStoreItem } from "@/src/services/itemDataService"; +import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { getEntriesUnsafe } from "@/src/utils/ts-utils"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { handleStoreItemAcquisition } from "./purchaseService"; import { IMissionReward } from "../types/missionTypes"; import { crackRelic } from "@/src/helpers/relicHelper"; +import { createMessage } from "./inboxService"; +import kuriaMessage50 from "@/static/fixed_responses/kuriaMessages/fiftyPercent.json"; +import kuriaMessage75 from "@/static/fixed_responses/kuriaMessages/seventyFivePercent.json"; +import kuriaMessage100 from "@/static/fixed_responses/kuriaMessages/oneHundredPercent.json"; +import conservationAnimals from "@/static/fixed_responses/conservationAnimals.json"; +import { getInfNodes } from "@/src/helpers/nemesisHelpers"; +import { Loadout } from "../models/inventoryModels/loadoutModel"; +import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes"; const getRotations = (rotationCount: number): number[] => { if (rotationCount === 0) return [0]; @@ -57,26 +72,68 @@ const getRandomRewardByChance = (pool: IReward[]): IRngResult | undefined => { //type TignoredInventoryUpdateKeys = (typeof ignoredInventoryUpdateKeys)[number]; //const knownUnhandledKeys: readonly string[] = ["test"] as const; // for unimplemented but important keys -export const addMissionInventoryUpdates = ( - inventory: HydratedDocument, +export const addMissionInventoryUpdates = async ( + inventory: TInventoryDatabaseDocument, inventoryUpdates: IMissionInventoryUpdateRequest -): Partial | undefined => { - //TODO: type this properly - const inventoryChanges: Partial = {}; - if (inventoryUpdates.MissionFailed === true) { - return; +): Promise => { + const inventoryChanges: IInventoryChanges = {}; + if ( + inventoryUpdates.EndOfMatchUpload && + inventoryUpdates.Missions && + inventoryUpdates.Missions.Tag in ExportRegions + ) { + const node = ExportRegions[inventoryUpdates.Missions.Tag]; + if (node.miscItemFee) { + addMiscItems(inventory, [ + { + ItemType: node.miscItemFee.ItemType, + ItemCount: node.miscItemFee.ItemCount * -1 + } + ]); + } } - if (inventoryUpdates.RewardInfo && inventoryUpdates.RewardInfo.periodicMissionTag) { - const tag = inventoryUpdates.RewardInfo.periodicMissionTag; - const existingCompletion = inventory.PeriodicMissionCompletions.find(completion => completion.tag === tag); + if (inventoryUpdates.RewardInfo) { + if (inventoryUpdates.RewardInfo.periodicMissionTag) { + const tag = inventoryUpdates.RewardInfo.periodicMissionTag; + const existingCompletion = inventory.PeriodicMissionCompletions.find(completion => completion.tag === tag); - if (existingCompletion) { - existingCompletion.date = new Date(); - } else { - inventory.PeriodicMissionCompletions.push({ - tag: tag, - date: new Date() - }); + if (existingCompletion) { + existingCompletion.date = new Date(); + } else { + inventory.PeriodicMissionCompletions.push({ + tag: tag, + date: new Date() + }); + } + } + if (inventoryUpdates.RewardInfo.NemesisAbandonedRewards) { + inventory.NemesisAbandonedRewards = inventoryUpdates.RewardInfo.NemesisAbandonedRewards; + } + } + if ( + inventoryUpdates.MissionFailed && + inventoryUpdates.MissionStatus == "GS_FAILURE" && + inventoryUpdates.EndOfMatchUpload && + inventoryUpdates.ObjectiveReached && + !inventoryUpdates.LockedWeaponGroup + ) { + const loadout = (await Loadout.findById(inventory.LoadOutPresets, "NORMAL"))!; + const config = loadout.NORMAL.id(inventory.CurrentLoadOutIds[0].$oid)!; + const SuitId = new Types.ObjectId(config.s!.ItemId.$oid); + + inventory.BrandedSuits ??= []; + if (!inventory.BrandedSuits.find(x => x.equals(SuitId))) { + inventory.BrandedSuits.push(SuitId); + + await createMessage(inventory.accountOwnerId, [ + { + sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender", + msg: "/Lotus/Language/G1Quests/BrandedMessage", + sub: "/Lotus/Language/G1Quests/BrandedTitle", + att: ["/Lotus/Types/Recipes/Components/BrandRemovalBlueprint"], + highPriority: true // TOVERIFY: I cannot find any content of this within the last 10 years so I can only assume that highPriority is set (it certainly would make sense), but I just don't know for sure that it is so on live. + } + ]); } } for (const [key, value] of getEntriesUnsafe(inventoryUpdates)) { @@ -89,7 +146,7 @@ export const addMissionInventoryUpdates = ( inventory.RegularCredits += value; break; case "QuestKeys": - updateQuestKey(inventory, value); + await updateQuestKey(inventory, value); break; case "AffiliationChanges": updateSyndicate(inventory, value); @@ -141,6 +198,10 @@ export const addMissionInventoryUpdates = ( case "CrewShipAmmo": addCrewShipAmmo(inventory, value); break; + case "ShipDecorations": + // e.g. when getting a 50+ score in happy zephyr, this is how the poster is given. + addShipDecorations(inventory, value); + break; case "FusionBundles": { let fusionPoints = 0; for (const fusionBundle of value) { @@ -152,6 +213,12 @@ export const addMissionInventoryUpdates = ( inventoryChanges.FusionPoints = fusionPoints; break; } + case "EmailItems": { + for (const tc of value) { + await addEmailItem(inventory, tc.ItemType); + } + break; + } case "FocusXpIncreases": { addFocusXpIncreases(inventory, value); break; @@ -176,8 +243,99 @@ export const addMissionInventoryUpdates = ( break; } case "LoreFragmentScans": - value.forEach(x => { - inventory.LoreFragmentScans.push(x); + value.forEach(clientFragment => { + const fragment = inventory.LoreFragmentScans.find(x => x.ItemType == clientFragment.ItemType); + if (fragment) { + fragment.Progress += clientFragment.Progress; + } else { + inventory.LoreFragmentScans.push(clientFragment); + } + }); + break; + case "LibraryScans": + value.forEach(scan => { + let synthesisIgnored = true; + if ( + inventory.LibraryPersonalTarget && + libraryPersonalTargetToAvatar[inventory.LibraryPersonalTarget] == scan.EnemyType + ) { + let progress = inventory.LibraryPersonalProgress.find( + x => x.TargetType == inventory.LibraryPersonalTarget + ); + if (!progress) { + progress = + inventory.LibraryPersonalProgress[ + inventory.LibraryPersonalProgress.push({ + TargetType: inventory.LibraryPersonalTarget, + Scans: 0, + Completed: false + }) - 1 + ]; + } + progress.Scans += scan.Count; + if ( + progress.Scans >= + (inventory.LibraryPersonalTarget == + "/Lotus/Types/Game/Library/Targets/DragonframeQuestTarget" + ? 3 + : 10) + ) { + progress.Completed = true; + } + logger.debug(`synthesis of ${scan.EnemyType} added to personal target progress`); + synthesisIgnored = false; + } + if ( + inventory.LibraryActiveDailyTaskInfo && + inventory.LibraryActiveDailyTaskInfo.EnemyTypes.find(x => x == scan.EnemyType) + ) { + inventory.LibraryActiveDailyTaskInfo.Scans ??= 0; + inventory.LibraryActiveDailyTaskInfo.Scans += scan.Count; + logger.debug(`synthesis of ${scan.EnemyType} added to daily task progress`); + synthesisIgnored = false; + } + if (synthesisIgnored) { + logger.warn(`ignoring synthesis of ${scan.EnemyType} due to not knowing why you did that`); + } + }); + break; + case "CollectibleScans": + for (const scan of value) { + const entry = inventory.CollectibleSeries?.find(x => x.CollectibleType == scan.CollectibleType); + if (entry) { + entry.Count = scan.Count; + entry.Tracking = scan.Tracking; + if (entry.CollectibleType == "/Lotus/Objects/Orokin/Props/CollectibleSeriesOne") { + const progress = entry.Count / entry.ReqScans; + for (const gate of entry.IncentiveStates) { + gate.complete = progress >= gate.threshold; + if (gate.complete && !gate.sent) { + gate.sent = true; + if (gate.threshold == 0.5) { + await createMessage(inventory.accountOwnerId, [kuriaMessage50]); + } else { + await createMessage(inventory.accountOwnerId, [kuriaMessage75]); + } + } + } + if (progress >= 1.0) { + await createMessage(inventory.accountOwnerId, [kuriaMessage100]); + } + } + } else { + logger.warn(`${scan.CollectibleType} was not found in inventory, ignoring scans`); + } + } + break; + case "Upgrades": + value.forEach(clientUpgrade => { + const upgrade = inventory.Upgrades.id(clientUpgrade.ItemId.$oid)!; + upgrade.UpgradeFingerprint = clientUpgrade.UpgradeFingerprint; // primitive way to copy over the riven challenge progress + }); + break; + case "Boosters": + value.forEach(booster => { + addBooster(booster.ItemType, booster.ExpiryDate, inventory); }); break; case "SyndicateId": { @@ -196,6 +354,111 @@ export const addMissionInventoryUpdates = ( inventory.SeasonChallengeHistory.push(...processedCompletions); break; } + case "DeathMarks": { + for (const deathMark of value) { + if (!inventory.DeathMarks.find(x => x == deathMark)) { + // It's a new death mark; we have to say the line. + await createMessage(inventory.accountOwnerId, [ + { + sub: "/Lotus/Language/G1Quests/DeathMarkTitle", + sndr: "/Lotus/Language/G1Quests/DeathMarkSender", + msg: "/Lotus/Language/G1Quests/DeathMarkMessage", + icon: "/Lotus/Interface/Icons/Npcs/Stalker_d.png", + highPriority: true + } + ]); + // TODO: This type of inbox message seems to automatically delete itself. Figure out under which conditions. + } + } + inventory.DeathMarks = value; + break; + } + case "CapturedAnimals": { + for (const capturedAnimal of value) { + const meta = conservationAnimals[capturedAnimal.AnimalType as keyof typeof conservationAnimals]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (meta) { + if (capturedAnimal.NumTags) { + addMiscItems(inventory, [ + { + ItemType: meta.tag, + ItemCount: capturedAnimal.NumTags + } + ]); + } + if (capturedAnimal.NumExtraRewards) { + if ("extraReward" in meta) { + addMiscItems(inventory, [ + { + ItemType: meta.extraReward, + ItemCount: capturedAnimal.NumExtraRewards + } + ]); + } else { + logger.warn( + `client attempted to claim unknown extra rewards for conservation of ${capturedAnimal.AnimalType}` + ); + } + } + } else { + logger.warn(`ignoring conservation of unknown AnimalType: ${capturedAnimal.AnimalType}`); + } + } + break; + } + case "DiscoveredMarkers": { + for (const clientMarker of value) { + const dbMarker = inventory.DiscoveredMarkers.find(x => x.tag == clientMarker.tag); + if (dbMarker) { + dbMarker.discoveryState = clientMarker.discoveryState; + } else { + inventory.DiscoveredMarkers.push(clientMarker); + } + } + break; + } + case "LockedWeaponGroup": { + inventory.LockedWeaponGroup = { + s: new Types.ObjectId(value.s.$oid), + l: value.l ? new Types.ObjectId(value.l.$oid) : undefined, + p: value.p ? new Types.ObjectId(value.p.$oid) : undefined, + m: value.m ? new Types.ObjectId(value.m.$oid) : undefined, + sn: value.sn ? new Types.ObjectId(value.sn.$oid) : undefined + }; + break; + } + case "UnlockWeapons": { + inventory.LockedWeaponGroup = undefined; + break; + } + case "CurrentLoadOutIds": { + if (value.LoadOuts) { + const loadout = await Loadout.findOne({ loadoutOwnerId: inventory.accountOwnerId }); + if (loadout) { + for (const [loadoutId, loadoutConfig] of Object.entries(value.LoadOuts.NORMAL)) { + const { ItemId, ...loadoutConfigItemIdRemoved } = loadoutConfig; + const loadoutConfigDatabase: ILoadoutConfigDatabase = { + _id: new Types.ObjectId(ItemId.$oid), + ...loadoutConfigItemIdRemoved + }; + const dbConfig = loadout.NORMAL.id(loadoutId); + if (dbConfig) { + dbConfig.overwrite(loadoutConfigDatabase); + } else { + logger.warn(`couldn't update loadout because there's no config with id ${loadoutId}`); + } + } + await loadout.save(); + } + } + break; + } + case "creditsFee": { + updateCurrency(inventory, value, false); + inventoryChanges.RegularCredits ??= 0; + inventoryChanges.RegularCredits -= value; + break; + } default: // Equipment XP updates if (equipmentKeys.includes(key as TEquipmentKey)) { @@ -215,17 +478,25 @@ export const addMissionInventoryUpdates = ( return inventoryChanges; }; +interface AddMissionRewardsReturnType { + MissionRewards: IMissionReward[]; + inventoryChanges?: IInventoryChanges; + credits?: IMissionCredits; +} + //TODO: return type of partial missioninventoryupdate response export const addMissionRewards = async ( inventory: TInventoryDatabaseDocument, { + Nemesis: nemesis, RewardInfo: rewardInfo, LevelKeyName: levelKeyName, Missions: missions, RegularCredits: creditDrops, - VoidTearParticipantsCurrWave: voidTearWave + VoidTearParticipantsCurrWave: voidTearWave, + StrippedItems: strippedItems }: IMissionInventoryUpdateRequest -) => { +): Promise => { if (!rewardInfo) { //TODO: if there is a case where you can have credits collected during a mission but no rewardInfo, add credits needs to be handled earlier logger.debug(`Mission ${missions!.Tag} did not have Reward Info `); @@ -261,15 +532,15 @@ export const addMissionRewards = async ( } } - if ( - missions && - missions.Tag != "" // #1013 - ) { - const node = getNode(missions.Tag); + // ignoring tags not in ExportRegions, because it can just be garbage: + // - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1013 + // - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1365 + if (missions && missions.Tag in ExportRegions) { + const node = ExportRegions[missions.Tag]; //node based credit rewards for mission completion if (node.missionIndex !== 28) { - const levelCreditReward = getLevelCreditRewards(missions.Tag); + const levelCreditReward = getLevelCreditRewards(node); missionCompletionCredits += levelCreditReward; inventory.RegularCredits += levelCreditReward; logger.debug(`levelCreditReward ${levelCreditReward}`); @@ -280,8 +551,21 @@ export const addMissionRewards = async ( } } + if (rewardInfo.useVaultManifest) { + MissionRewards.push({ + StoreItem: getRandomElement(corruptedMods), + ItemCount: 1 + }); + } + for (const reward of MissionRewards) { - const inventoryChange = await handleStoreItemAcquisition(reward.StoreItem, inventory, reward.ItemCount); + const inventoryChange = await handleStoreItemAcquisition( + reward.StoreItem, + inventory, + reward.ItemCount, + undefined, + true + ); //TODO: combineInventoryChanges improve type safety, merging 2 of the same item? //TODO: check for the case when two of the same item are added, combineInventoryChanges should merge them, but the client also merges them //TODO: some conditional types to rule out binchanges? @@ -299,27 +583,100 @@ export const addMissionRewards = async ( voidTearWave.Participants[0].QualifiesForReward && !voidTearWave.Participants[0].HaveRewardResponse ) { - await crackRelic(inventory, voidTearWave.Participants[0]); - MissionRewards.push({ StoreItem: voidTearWave.Participants[0].Reward, ItemCount: 1 }); + const reward = await crackRelic(inventory, voidTearWave.Participants[0], inventoryChanges); + MissionRewards.push({ StoreItem: reward.type, ItemCount: reward.itemCount }); } + if (strippedItems) { + for (const si of strippedItems) { + const droptable = ExportEnemies.droptables[si.DropTable]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!droptable) { + logger.error(`unknown droptable ${si.DropTable}`); + } else { + for (let i = 0; i != si.DROP_MOD.length; ++i) { + for (const pool of droptable) { + const reward = getRandomReward(pool.items)!; + logger.debug(`stripped droptable rolled`, reward); + await addItem(inventory, reward.type); + MissionRewards.push({ + StoreItem: toStoreItem(reward.type), + ItemCount: 1, + FromEnemyCache: true // to show "identified" + }); + } + } + } + } + } + + if (inventory.Nemesis) { + if ( + nemesis || + (inventory.Nemesis.Faction == "FC_INFESTATION" && + inventory.Nemesis.InfNodes.find(obj => obj.Node == rewardInfo.node)) + ) { + inventoryChanges.Nemesis ??= {}; + const nodeIndex = inventory.Nemesis.InfNodes.findIndex(obj => obj.Node === rewardInfo.node); + if (nodeIndex !== -1) inventory.Nemesis.InfNodes.splice(nodeIndex, 1); + + if (inventory.Nemesis.InfNodes.length <= 0) { + if (inventory.Nemesis.Faction != "FC_INFESTATION") { + inventory.Nemesis.Rank = Math.min(inventory.Nemesis.Rank + 1, 4); + inventoryChanges.Nemesis.Rank = inventory.Nemesis.Rank; + } + inventory.Nemesis.InfNodes = getInfNodes(inventory.Nemesis.Faction, inventory.Nemesis.Rank); + } + + if (inventory.Nemesis.Faction == "FC_INFESTATION") { + inventoryChanges.Nemesis.HenchmenKilled ??= 0; + inventoryChanges.Nemesis.MissionCount ??= 0; + + inventory.Nemesis.HenchmenKilled += 5; + inventory.Nemesis.MissionCount += 1; + + inventoryChanges.Nemesis.HenchmenKilled += 5; + inventoryChanges.Nemesis.MissionCount += 1; + + if (inventory.Nemesis.HenchmenKilled >= 100) { + inventory.Nemesis.InfNodes = [ + { + Node: "CrewBattleNode559", + Influence: 1 + } + ]; + inventory.Nemesis.Weakened = true; + inventoryChanges.Nemesis.Weakened = true; + } + } + + inventoryChanges.Nemesis.InfNodes = inventory.Nemesis.InfNodes; + } + } return { inventoryChanges, MissionRewards, credits }; }; +interface IMissionCredits { + MissionCredits: number[]; + CreditBonus: number[]; + TotalCredits: number[]; + DailyMissionBonus?: boolean; +} + //creditBonus is not entirely accurate. //TODO: consider ActiveBoosters export const addCredits = ( - inventory: HydratedDocument, + inventory: TInventoryDatabaseDocument, { missionDropCredits, missionCompletionCredits, rngRewardCredits }: { missionDropCredits: number; missionCompletionCredits: number; rngRewardCredits: number } -) => { +): IMissionCredits => { const hasDailyCreditBonus = true; const totalCredits = missionDropCredits + missionCompletionCredits + rngRewardCredits; - const finalCredits = { + const finalCredits: IMissionCredits = { MissionCredits: [missionDropCredits, missionDropCredits], CreditBonus: [missionCompletionCredits, missionCompletionCredits], TotalCredits: [totalCredits, totalCredits] @@ -342,7 +699,7 @@ export const addFixedLevelRewards = ( rewards: IMissionRewardExternal, inventory: TInventoryDatabaseDocument, MissionRewards: IMissionReward[] -) => { +): number => { let missionBonusCredits = 0; if (rewards.credits) { missionBonusCredits += rewards.credits; @@ -351,7 +708,7 @@ export const addFixedLevelRewards = ( if (rewards.items) { for (const item of rewards.items) { MissionRewards.push({ - StoreItem: item.includes(`/StoreItems/`) ? item : `/Lotus/StoreItems${item.substring("Lotus/".length)}`, + StoreItem: item, ItemCount: 1 }); } @@ -359,9 +716,7 @@ export const addFixedLevelRewards = ( if (rewards.countedItems) { for (const item of rewards.countedItems) { MissionRewards.push({ - StoreItem: item.ItemType.includes(`/StoreItems/`) - ? item.ItemType - : `/Lotus/StoreItems${item.ItemType.substring("Lotus/".length)}`, + StoreItem: `/Lotus/StoreItems${item.ItemType.substring("Lotus/".length)}`, ItemCount: item.ItemCount }); } @@ -371,11 +726,25 @@ export const addFixedLevelRewards = ( MissionRewards.push(item); } } + if (rewards.droptable) { + if (rewards.droptable in ExportRewards) { + logger.debug(`rolling ${rewards.droptable} for level key rewards`); + const reward = getRandomRewardByChance(ExportRewards[rewards.droptable][0]); + if (reward) { + MissionRewards.push({ + StoreItem: reward.type, + ItemCount: reward.itemCount + }); + } + } else { + logger.error(`unknown droptable ${rewards.droptable}`); + } + } return missionBonusCredits; }; -function getLevelCreditRewards(nodeName: string): number { - const minEnemyLevel = getNode(nodeName).minEnemyLevel; +function getLevelCreditRewards(node: IRegion): number { + const minEnemyLevel = node.minEnemyLevel; return 1000 + (minEnemyLevel - 1) * 100; @@ -443,3 +812,52 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo): IMissionReward[] { } return drops; } + +const corruptedMods = [ + "/Lotus/StoreItems/Upgrades/Mods/Melee/DualStat/CorruptedHeavyDamageChargeSpeedMod", // Corrupt Charge + "/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedCritDamagePistol", // Hollow Point + "/Lotus/StoreItems/Upgrades/Mods/Melee/DualStat/CorruptedDamageSpeedMod", // Spoiled Strike + "/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedDamageRecoilPistol", // Magnum Force + "/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedMaxClipReloadSpeedPistol", // Tainted Clip + "/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedCritRateFireRateRifle", // Critical Delay + "/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedDamageRecoilRifle", // Heavy Caliber + "/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedMaxClipReloadSpeedRifle", // Tainted Mag + "/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedRecoilFireRateRifle", // Vile Precision + "/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/CorruptedDurationRangeWarframe", // Narrow Minded + "/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/CorruptedEfficiencyDurationWarframe", // Fleeting Expertise + "/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/CorruptedPowerEfficiencyWarframe", // Blind Rage + "/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/CorruptedRangePowerWarframe", // Overextended + "/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/CorruptedAccuracyFireRateShotgun", // Tainted Shell + "/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/CorruptedDamageAccuracyShotgun", // Vicious Spread + "/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/CorruptedMaxClipReloadSpeedShotgun", // Burdened Magazine + "/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedFireRateDamagePistol", // Anemic Agility + "/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedFireRateDamageRifle", // Vile Acceleration + "/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/CorruptedFireRateDamageShotgun", // Frail Momentum + "/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/CorruptedCritChanceFireRateShotgun", // Critical Deceleration + "/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedCritChanceFireRatePistol", // Creeping Bullseye + "/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/CorruptedPowerStrengthPowerDurationWarframe", // Transient Fortitude + "/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedReloadSpeedMaxClipRifle", // Depleted Reload + "/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/FixedShieldAndShieldGatingDuration" // Catalyzing Shields +]; + +const libraryPersonalTargetToAvatar: Record = { + "/Lotus/Types/Game/Library/Targets/DragonframeQuestTarget": + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/RifleLancerAvatar", + "/Lotus/Types/Game/Library/Targets/Research1Target": + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/RifleLancerAvatar", + "/Lotus/Types/Game/Library/Targets/Research2Target": + "/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/LaserDiscBipedAvatar", + "/Lotus/Types/Game/Library/Targets/Research3Target": + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/EvisceratorLancerAvatar", + "/Lotus/Types/Game/Library/Targets/Research4Target": "/Lotus/Types/Enemies/Orokin/OrokinHealingAncientAvatar", + "/Lotus/Types/Game/Library/Targets/Research5Target": + "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/ShotgunSpacemanAvatar", + "/Lotus/Types/Game/Library/Targets/Research6Target": "/Lotus/Types/Enemies/Infested/AiWeek/Runners/RunnerAvatar", + "/Lotus/Types/Game/Library/Targets/Research7Target": + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/GrineerMeleeStaffAvatar", + "/Lotus/Types/Game/Library/Targets/Research8Target": "/Lotus/Types/Enemies/Orokin/OrokinHeavyFemaleAvatar", + "/Lotus/Types/Game/Library/Targets/Research9Target": + "/Lotus/Types/Enemies/Infested/AiWeek/Quadrupeds/QuadrupedAvatar", + "/Lotus/Types/Game/Library/Targets/Research10Target": + "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/NullifySpacemanAvatar" +}; diff --git a/src/services/personalRoomsService.ts b/src/services/personalRoomsService.ts index 5325af1c..24399655 100644 --- a/src/services/personalRoomsService.ts +++ b/src/services/personalRoomsService.ts @@ -1,7 +1,8 @@ import { PersonalRooms } from "@/src/models/personalRoomsModel"; import { addItem, getInventory } from "@/src/services/inventoryService"; +import { TPersonalRoomsDatabaseDocument } from "../types/personalRoomsTypes"; -export const getPersonalRooms = async (accountId: string) => { +export const getPersonalRooms = async (accountId: string): Promise => { const personalRooms = await PersonalRooms.findOne({ personalRoomsOwnerId: accountId }); if (!personalRooms) { @@ -10,7 +11,7 @@ export const getPersonalRooms = async (accountId: string) => { return personalRooms; }; -export const updateShipFeature = async (accountId: string, shipFeature: string) => { +export const updateShipFeature = async (accountId: string, shipFeature: string): Promise => { const personalRooms = await getPersonalRooms(accountId); if (personalRooms.Ship.Features.includes(shipFeature)) { diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 76a6d111..32f3af38 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -8,7 +8,7 @@ import { updateCurrency, updateSlots } from "@/src/services/inventoryService"; -import { getRandomWeightedReward } from "@/src/services/rngService"; +import { getRandomWeightedRewardUc } from "@/src/services/rngService"; import { getVendorManifestByOid } from "@/src/services/serversideVendorsService"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IPurchaseRequest, IPurchaseResponse, SlotPurchase, IInventoryChanges } from "@/src/types/purchaseTypes"; @@ -27,6 +27,7 @@ import { } from "warframe-public-export-plus"; import { config } from "./configService"; import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel"; +import { fromStoreItem, toStoreItem } from "./itemDataService"; export const getStoreItemCategory = (storeItem: string): string => { const storeItemString = getSubstringFromKeyword(storeItem, "StoreItems/"); @@ -49,38 +50,101 @@ export const handlePurchase = async ( ): Promise => { logger.debug("purchase request", purchaseRequest); - const inventoryChanges: IInventoryChanges = {}; + const prePurchaseInventoryChanges: IInventoryChanges = {}; + let seed: bigint | undefined; if (purchaseRequest.PurchaseParams.Source == 7) { const manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!); if (manifest) { - const ItemId = (JSON.parse(purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson!) as { ItemId: string }) - .ItemId; - const offer = manifest.VendorInfo.ItemManifest.find(x => x.Id.$oid == ItemId); + let ItemId: string | undefined; + if (purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) { + ItemId = (JSON.parse(purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) as { ItemId: string }) + .ItemId; + } + const offer = ItemId + ? manifest.VendorInfo.ItemManifest.find(x => x.Id.$oid == ItemId) + : manifest.VendorInfo.ItemManifest.find(x => x.StoreItem == purchaseRequest.PurchaseParams.StoreItem); if (!offer) { - throw new Error(`unknown vendor offer: ${ItemId}`); + throw new Error(`unknown vendor offer: ${ItemId ? ItemId : purchaseRequest.PurchaseParams.StoreItem}`); } if (offer.ItemPrices) { handleItemPrices( inventory, offer.ItemPrices, purchaseRequest.PurchaseParams.Quantity, - inventoryChanges + prePurchaseInventoryChanges ); } + if (offer.LocTagRandSeed !== undefined) { + seed = BigInt(offer.LocTagRandSeed); + } + if (!config.noVendorPurchaseLimits && ItemId) { + inventory.RecentVendorPurchases ??= []; + let vendorPurchases = inventory.RecentVendorPurchases.find( + x => x.VendorType == manifest.VendorInfo.TypeName + ); + if (!vendorPurchases) { + vendorPurchases = + inventory.RecentVendorPurchases[ + inventory.RecentVendorPurchases.push({ + VendorType: manifest.VendorInfo.TypeName, + PurchaseHistory: [] + }) - 1 + ]; + } + let expiry = parseInt(offer.Expiry.$date.$numberLong); + if (purchaseRequest.PurchaseParams.IsWeekly) { + const EPOCH = 1734307200 * 1000; // Monday + const week = Math.trunc((Date.now() - EPOCH) / 604800000); + const weekStart = EPOCH + week * 604800000; + expiry = weekStart + 604800000; + } + const historyEntry = vendorPurchases.PurchaseHistory.find(x => x.ItemId == ItemId); + let numPurchased = purchaseRequest.PurchaseParams.Quantity; + if (historyEntry) { + if (Date.now() >= historyEntry.Expiry.getTime()) { + historyEntry.NumPurchased = numPurchased; + historyEntry.Expiry = new Date(expiry); + } else { + numPurchased += historyEntry.NumPurchased; + historyEntry.NumPurchased += purchaseRequest.PurchaseParams.Quantity; + } + } else { + vendorPurchases.PurchaseHistory.push({ + ItemId: ItemId, + NumPurchased: purchaseRequest.PurchaseParams.Quantity, + Expiry: new Date(expiry) + }); + } + prePurchaseInventoryChanges.NewVendorPurchase = { + VendorType: manifest.VendorInfo.TypeName, + PurchaseHistory: [ + { + ItemId: ItemId, + NumPurchased: numPurchased, + Expiry: { $date: { $numberLong: expiry.toString() } } + } + ] + }; + prePurchaseInventoryChanges.RecentVendorPurchases = prePurchaseInventoryChanges.NewVendorPurchase; + } purchaseRequest.PurchaseParams.Quantity *= offer.QuantityMultiplier; - } else if (!ExportVendors[purchaseRequest.PurchaseParams.SourceId!]) { - throw new Error(`unknown vendor: ${purchaseRequest.PurchaseParams.SourceId!}`); + } else { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!ExportVendors[purchaseRequest.PurchaseParams.SourceId!]) { + throw new Error(`unknown vendor: ${purchaseRequest.PurchaseParams.SourceId!}`); + } } } const purchaseResponse = await handleStoreItemAcquisition( purchaseRequest.PurchaseParams.StoreItem, inventory, - purchaseRequest.PurchaseParams.Quantity + purchaseRequest.PurchaseParams.Quantity, + undefined, + undefined, + seed ); - combineInventoryChanges(purchaseResponse.InventoryChanges, inventoryChanges); - - if (!purchaseResponse) throw new Error("purchase response was undefined"); + combineInventoryChanges(purchaseResponse.InventoryChanges, prePurchaseInventoryChanges); const currencyChanges = updateCurrency( inventory, @@ -93,6 +157,29 @@ export const handlePurchase = async ( }; switch (purchaseRequest.PurchaseParams.Source) { + case 1: { + if (purchaseRequest.PurchaseParams.SourceId! != worldState.VoidTraders[0]._id.$oid) { + throw new Error("invalid request source"); + } + const offer = worldState.VoidTraders[0].Manifest.find( + x => x.ItemType == purchaseRequest.PurchaseParams.StoreItem + ); + if (offer) { + combineInventoryChanges( + purchaseResponse.InventoryChanges, + updateCurrency(inventory, offer.RegularPrice, false) + ); + + const invItem: IMiscItem = { + ItemType: "/Lotus/Types/Items/MiscItems/PrimeBucks", + ItemCount: offer.PrimePrice * purchaseRequest.PurchaseParams.Quantity * -1 + }; + addMiscItems(inventory, [invItem]); + purchaseResponse.InventoryChanges.MiscItems ??= []; + purchaseResponse.InventoryChanges.MiscItems.push(invItem); + } + break; + } case 2: { const syndicateTag = purchaseRequest.PurchaseParams.SyndicateTag!; @@ -109,6 +196,7 @@ export const handlePurchase = async ( ]; } else { const syndicate = ExportSyndicates[syndicateTag]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (syndicate) { const favour = syndicate.favours.find( x => x.storeItem == purchaseRequest.PurchaseParams.StoreItem @@ -133,13 +221,21 @@ export const handlePurchase = async ( if (purchaseRequest.PurchaseParams.SourceId! in ExportVendors) { const vendor = ExportVendors[purchaseRequest.PurchaseParams.SourceId!]; const offer = vendor.items.find(x => x.storeItem == purchaseRequest.PurchaseParams.StoreItem); - if (offer && offer.itemPrices) { - handleItemPrices( - inventory, - offer.itemPrices, - purchaseRequest.PurchaseParams.Quantity, - purchaseResponse.InventoryChanges - ); + if (offer) { + if (offer.credits) { + combineInventoryChanges( + purchaseResponse.InventoryChanges, + updateCurrency(inventory, offer.credits, false) + ); + } + if (offer.itemPrices) { + handleItemPrices( + inventory, + offer.itemPrices, + purchaseRequest.PurchaseParams.Quantity, + purchaseResponse.InventoryChanges + ); + } } } break; @@ -164,9 +260,13 @@ export const handlePurchase = async ( addMiscItems(inventory, [invItem]); purchaseResponse.InventoryChanges.MiscItems ??= []; - (purchaseResponse.InventoryChanges.MiscItems as IMiscItem[]).push(invItem); + purchaseResponse.InventoryChanges.MiscItems.push(invItem); } else if (!config.infiniteRegalAya) { inventory.PrimeTokens -= offer.PrimePrice! * purchaseRequest.PurchaseParams.Quantity; + + purchaseResponse.InventoryChanges.PrimeTokens ??= 0; + purchaseResponse.InventoryChanges.PrimeTokens -= + offer.PrimePrice! * purchaseRequest.PurchaseParams.Quantity; } } break; @@ -191,11 +291,11 @@ const handleItemPrices = ( addMiscItems(inventory, [invItem]); inventoryChanges.MiscItems ??= []; - const change = (inventoryChanges.MiscItems as IMiscItem[]).find(x => x.ItemType == item.ItemType); + const change = inventoryChanges.MiscItems.find(x => x.ItemType == item.ItemType); if (change) { change.ItemCount += invItem.ItemCount; } else { - (inventoryChanges.MiscItems as IMiscItem[]).push(invItem); + inventoryChanges.MiscItems.push(invItem); } } }; @@ -230,7 +330,8 @@ export const handleStoreItemAcquisition = async ( inventory: TInventoryDatabaseDocument, quantity: number = 1, durability: TRarity = "COMMON", - ignorePurchaseQuantity: boolean = false + ignorePurchaseQuantity: boolean = false, + seed?: bigint ): Promise => { let purchaseResponse = { InventoryChanges: {} @@ -240,7 +341,7 @@ export const handleStoreItemAcquisition = async ( await handleBundleAcqusition(storeItemName, inventory, quantity, purchaseResponse.InventoryChanges); } else { const storeCategory = getStoreItemCategory(storeItemName); - const internalName = storeItemName.replace("/StoreItems", ""); + const internalName = fromStoreItem(storeItemName); logger.debug(`store category ${storeCategory}`); if (!ignorePurchaseQuantity) { if (internalName in ExportGear) { @@ -251,11 +352,11 @@ export const handleStoreItemAcquisition = async ( } switch (storeCategory) { default: { - purchaseResponse = await addItem(inventory, internalName, quantity); + purchaseResponse = { InventoryChanges: await addItem(inventory, internalName, quantity, true, seed) }; break; } case "Types": - purchaseResponse = await handleTypesPurchase(internalName, inventory, quantity); + purchaseResponse = await handleTypesPurchase(internalName, inventory, quantity, ignorePurchaseQuantity); break; case "Boosters": purchaseResponse = handleBoostersPurchase(storeItemName, inventory, durability); @@ -266,16 +367,16 @@ export const handleStoreItemAcquisition = async ( }; export const slotPurchaseNameToSlotName: SlotPurchase = { - SuitSlotItem: { name: "SuitBin", slotsPerPurchase: 1 }, - TwoSentinelSlotItem: { name: "SentinelBin", slotsPerPurchase: 2 }, - TwoWeaponSlotItem: { name: "WeaponBin", slotsPerPurchase: 2 }, - SpaceSuitSlotItem: { name: "SpaceSuitBin", slotsPerPurchase: 1 }, - TwoSpaceWeaponSlotItem: { name: "SpaceWeaponBin", slotsPerPurchase: 2 }, - MechSlotItem: { name: "MechBin", slotsPerPurchase: 1 }, - TwoOperatorWeaponSlotItem: { name: "OperatorAmpBin", slotsPerPurchase: 2 }, - RandomModSlotItem: { name: "RandomModBin", slotsPerPurchase: 3 }, - TwoCrewShipSalvageSlotItem: { name: "CrewShipSalvageBin", slotsPerPurchase: 2 }, - CrewMemberSlotItem: { name: "CrewMemberBin", slotsPerPurchase: 1 } + SuitSlotItem: { name: "SuitBin", purchaseQuantity: 1 }, + TwoSentinelSlotItem: { name: "SentinelBin", purchaseQuantity: 2 }, + TwoWeaponSlotItem: { name: "WeaponBin", purchaseQuantity: 2 }, + SpaceSuitSlotItem: { name: "SpaceSuitBin", purchaseQuantity: 1 }, + TwoSpaceWeaponSlotItem: { name: "SpaceWeaponBin", purchaseQuantity: 2 }, + MechSlotItem: { name: "MechBin", purchaseQuantity: 1 }, + TwoOperatorWeaponSlotItem: { name: "OperatorAmpBin", purchaseQuantity: 2 }, + RandomModSlotItem: { name: "RandomModBin", purchaseQuantity: 3 }, + TwoCrewShipSalvageSlotItem: { name: "CrewShipSalvageBin", purchaseQuantity: 2 }, + CrewMemberSlotItem: { name: "CrewMemberBin", purchaseQuantity: 1 } }; // // extra = everything above the base +2 slots (depending on slot type) @@ -285,7 +386,8 @@ export const slotPurchaseNameToSlotName: SlotPurchase = { const handleSlotPurchase = ( slotPurchaseNameFull: string, inventory: TInventoryDatabaseDocument, - quantity: number + quantity: number, + ignorePurchaseQuantity: boolean ): IPurchaseResponse => { logger.debug(`slot name ${slotPurchaseNameFull}`); const slotPurchaseName = parseSlotPurchaseName( @@ -294,22 +396,23 @@ const handleSlotPurchase = ( logger.debug(`slot purchase name ${slotPurchaseName}`); const slotName = slotPurchaseNameToSlotName[slotPurchaseName].name; - const slotsPurchased = slotPurchaseNameToSlotName[slotPurchaseName].slotsPerPurchase * quantity; + let slotsPurchased = quantity; + if (!ignorePurchaseQuantity) { + slotsPurchased *= slotPurchaseNameToSlotName[slotPurchaseName].purchaseQuantity; + } updateSlots(inventory, slotName, slotsPurchased, slotsPurchased); logger.debug(`added ${slotsPurchased} slot ${slotName}`); - return { - InventoryChanges: { - [slotName]: { - count: 0, - platinum: 1, - Slots: slotsPurchased, - Extra: slotsPurchased - } - } + const inventoryChanges: IInventoryChanges = {}; + inventoryChanges[slotName] = { + count: 0, + platinum: 1, + Slots: slotsPurchased, + Extra: slotsPurchased }; + return { InventoryChanges: inventoryChanges }; }; const handleBoosterPackPurchase = async ( @@ -318,6 +421,7 @@ const handleBoosterPackPurchase = async ( quantity: number ): Promise => { const pack = ExportBoosterPacks[typeName]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!pack) { throw new Error(`unknown booster pack: ${typeName}`); } @@ -325,18 +429,29 @@ const handleBoosterPackPurchase = async ( BoosterPackItems: "", InventoryChanges: {} }; + if (quantity > 100) { + throw new Error( + "attempt to roll over 100 booster packs in a single go. possible but unlikely to be desirable for the user or the server." + ); + } for (let i = 0; i != quantity; ++i) { - for (const weights of pack.rarityWeightsPerRoll) { - const result = getRandomWeightedReward(pack.components, weights); + const disallowedItems = new Set(); + for (let roll = 0; roll != pack.rarityWeightsPerRoll.length; ) { + const weights = pack.rarityWeightsPerRoll[roll]; + const result = getRandomWeightedRewardUc(pack.components, weights); if (result) { logger.debug(`booster pack rolled`, result); - purchaseResponse.BoosterPackItems += - result.type.split("/Lotus/").join("/Lotus/StoreItems/") + ',{"lvl":0};'; - combineInventoryChanges( - purchaseResponse.InventoryChanges, - (await addItem(inventory, result.type, result.itemCount)).InventoryChanges - ); + if (disallowedItems.has(result.Item)) { + logger.debug(`oops, can't use that one; trying again`); + continue; + } + if (!pack.canGiveDuplicates) { + disallowedItems.add(result.Item); + } + purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + ',{"lvl":0};'; + combineInventoryChanges(purchaseResponse.InventoryChanges, await addItem(inventory, result.Item, 1)); } + ++roll; } } return purchaseResponse; @@ -362,17 +477,18 @@ const handleCreditBundlePurchase = async ( const handleTypesPurchase = async ( typesName: string, inventory: TInventoryDatabaseDocument, - quantity: number + quantity: number, + ignorePurchaseQuantity: boolean ): Promise => { const typeCategory = getStoreItemTypesCategory(typesName); logger.debug(`type category ${typeCategory}`); switch (typeCategory) { default: - return await addItem(inventory, typesName, quantity); + return { InventoryChanges: await addItem(inventory, typesName, quantity) }; case "BoosterPacks": return handleBoosterPackPurchase(typesName, inventory, quantity); case "SlotItems": - return handleSlotPurchase(typesName, inventory, quantity); + return handleSlotPurchase(typesName, inventory, quantity, ignorePurchaseQuantity); case "CreditBundles": return handleCreditBundlePurchase(typesName, inventory); } diff --git a/src/services/questService.ts b/src/services/questService.ts index d0bdd9db..6319a724 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -1,18 +1,17 @@ -import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; +import { IKeyChainRequest } from "@/src/types/requestTypes"; import { isEmptyObject } from "@/src/helpers/general"; -import { IMessage } from "@/src/models/inboxModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { createMessage } from "@/src/services/inboxService"; -import { addItem, addKeyChainItems } from "@/src/services/inventoryService"; -import { getKeyChainMessage, getLevelKeyRewards } from "@/src/services/itemDataService"; +import { addItem, addItems, addKeyChainItems, setupKahlSyndicate } from "@/src/services/inventoryService"; import { - IInventoryDatabase, - IQuestKeyClient, - IQuestKeyDatabase, - IQuestStage -} from "@/src/types/inventoryTypes/inventoryTypes"; + fromStoreItem, + getKeyChainMessage, + getLevelKeyRewards, + getQuestCompletionItems +} from "@/src/services/itemDataService"; +import { IQuestKeyClient, IQuestKeyDatabase, IQuestStage } from "@/src/types/inventoryTypes/inventoryTypes"; import { logger } from "@/src/utils/logger"; -import { HydratedDocument } from "mongoose"; +import { Types } from "mongoose"; import { ExportKeys } from "warframe-public-export-plus"; import { addFixedLevelRewards } from "./missionInventoryUpdateService"; import { IInventoryChanges } from "../types/purchaseTypes"; @@ -26,10 +25,10 @@ export interface IUpdateQuestRequest { DoQuestReward: boolean; } -export const updateQuestKey = ( - inventory: HydratedDocument, +export const updateQuestKey = async ( + inventory: TInventoryDatabaseDocument, questKeyUpdate: IUpdateQuestRequest["QuestKeys"] -): void => { +): Promise => { if (questKeyUpdate.length > 1) { logger.error(`more than 1 quest key not supported`); throw new Error("more than 1 quest key not supported"); @@ -41,11 +40,27 @@ export const updateQuestKey = ( throw new Error(`quest key ${questKeyUpdate[0].ItemType} not found`); } - inventory.QuestKeys[questKeyIndex] = questKeyUpdate[0]; + inventory.QuestKeys[questKeyIndex].overwrite(questKeyUpdate[0]); + let inventoryChanges: IInventoryChanges = {}; if (questKeyUpdate[0].Completed) { inventory.QuestKeys[questKeyIndex].CompletionDate = new Date(); + + logger.debug(`completed quest ${questKeyUpdate[0].ItemType} `); + const questKeyName = questKeyUpdate[0].ItemType; + const questCompletionItems = getQuestCompletionItems(questKeyName); + logger.debug(`quest completion items`, questCompletionItems); + + if (questCompletionItems) { + inventoryChanges = await addItems(inventory, questCompletionItems); + } + inventory.ActiveQuest = ""; + + if (questKeyUpdate[0].ItemType == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain") { + setupKahlSyndicate(inventory); + } } + return inventoryChanges; }; export const updateQuestStage = ( @@ -77,17 +92,32 @@ export const updateQuestStage = ( Object.assign(questStage, questStageUpdate); }; -export const addQuestKey = (inventory: TInventoryDatabaseDocument, questKey: IQuestKeyDatabase) => { +export const addQuestKey = ( + inventory: TInventoryDatabaseDocument, + questKey: IQuestKeyDatabase +): IQuestKeyClient | undefined => { if (inventory.QuestKeys.some(q => q.ItemType === questKey.ItemType)) { logger.warn(`Quest key ${questKey.ItemType} already exists. It will not be added`); return; } + + if (questKey.ItemType == "/Lotus/Types/Keys/InfestedMicroplanetQuest/InfestedMicroplanetQuestKeyChain") { + void createMessage(inventory.accountOwnerId, [ + { + sndr: "/Lotus/Language/Bosses/Loid", + icon: "/Lotus/Interface/Icons/Npcs/Entrati/Loid.png", + sub: "/Lotus/Language/InfestedMicroplanet/DeimosIntroQuestInboxTitle", + msg: "/Lotus/Language/InfestedMicroplanet/DeimosIntroQuestInboxMessage" + } + ]); + } + const index = inventory.QuestKeys.push(questKey); return inventory.QuestKeys[index - 1].toJSON(); }; -export const completeQuest = async (inventory: TInventoryDatabaseDocument, questKey: string) => { +export const completeQuest = async (inventory: TInventoryDatabaseDocument, questKey: string): Promise => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const chainStages = ExportKeys[questKey]?.chainStages; @@ -95,76 +125,69 @@ export const completeQuest = async (inventory: TInventoryDatabaseDocument, quest throw new Error(`Quest ${questKey} does not contain chain stages`); } - const chainStageTotal = ExportKeys[questKey].chainStages?.length ?? 0; + const chainStageTotal = chainStages.length; const existingQuestKey = inventory.QuestKeys.find(qk => qk.ItemType === questKey); + const startingStage = Math.max((existingQuestKey?.Progress?.length ?? 0) - 1, 0); + if (existingQuestKey?.Completed) { return; } - const Progress = Array(chainStageTotal).fill({ - c: 0, - i: false, - m: false, - b: [] - } satisfies IQuestStage); - - const completedQuestKey: IQuestKeyDatabase = { - ItemType: questKey, - Completed: true, - unlock: true, - Progress: Progress, - CompletionDate: new Date() - }; - - //overwrite current quest progress, might lead to multiple quest item rewards if (existingQuestKey) { - existingQuestKey.overwrite(completedQuestKey); - //Object.assign(existingQuestKey, completedQuestKey); + existingQuestKey.Progress = existingQuestKey.Progress ?? []; + + const existingProgressLength = existingQuestKey.Progress.length; + + if (existingProgressLength < chainStageTotal) { + const missingProgress: IQuestStage[] = Array.from( + { length: chainStageTotal - existingProgressLength }, + () => + ({ + c: 0, + i: false, + m: false, + b: [] + }) as IQuestStage + ); + + existingQuestKey.Progress.push(...missingProgress); + existingQuestKey.CompletionDate = new Date(); + existingQuestKey.Completed = true; + } } else { + const completedQuestKey: IQuestKeyDatabase = { + ItemType: questKey, + Completed: true, + unlock: true, + Progress: Array(chainStageTotal).fill({ + c: 0, + i: false, + m: false, + b: [] + } satisfies IQuestStage), + CompletionDate: new Date() + }; addQuestKey(inventory, completedQuestKey); } - for (let i = 0; i < chainStageTotal; i++) { - if (chainStages[i].itemsToGiveWhenTriggered.length > 0) { - await giveKeyChainItem(inventory, { KeyChain: questKey, ChainStage: i }); - } + for (let i = startingStage; i < chainStageTotal; i++) { + await giveKeyChainStageTriggered(inventory, { KeyChain: questKey, ChainStage: i }); - if (chainStages[i].messageToSendWhenTriggered) { - await giveKeyChainMessage(inventory, inventory.accountOwnerId.toString(), { - KeyChain: questKey, - ChainStage: i - }); - } - - const missionName = chainStages[i].key; - if (missionName) { - const fixedLevelRewards = getLevelKeyRewards(missionName); - //logger.debug(`fixedLevelRewards`, fixedLevelRewards); - if (fixedLevelRewards.levelKeyRewards) { - const missionRewards: { StoreItem: string; ItemCount: number }[] = []; - addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, inventory, missionRewards); - - for (const reward of missionRewards) { - await addItem(inventory, reward.StoreItem.replace("StoreItems/", ""), reward.ItemCount); - } - } else if (fixedLevelRewards.levelKeyRewards2) { - for (const reward of fixedLevelRewards.levelKeyRewards2) { - if (reward.rewardType == "RT_CREDITS") { - inventory.RegularCredits += reward.amount; - continue; - } - if (reward.rewardType == "RT_RESOURCE") { - await addItem(inventory, reward.itemType.replace("StoreItems/", ""), reward.amount); - } else { - await addItem(inventory, reward.itemType.replace("StoreItems/", "")); - } - } - } - } + await giveKeyChainMissionReward(inventory, { KeyChain: questKey, ChainStage: i }); + } + + const questCompletionItems = getQuestCompletionItems(questKey); + logger.debug(`quest completion items`, questCompletionItems); + if (questCompletionItems) { + await addItems(inventory, questCompletionItems); + } + + if (inventory.ActiveQuest == questKey) inventory.ActiveQuest = ""; + + if (questKey == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain") { + setupKahlSyndicate(inventory); } - inventory.ActiveQuest = ""; - //TODO: handle quest completion items }; export const giveKeyChainItem = async ( @@ -193,24 +216,69 @@ export const giveKeyChainItem = async ( export const giveKeyChainMessage = async ( inventory: TInventoryDatabaseDocument, - accountId: string, + accountId: string | Types.ObjectId, keyChainInfo: IKeyChainRequest ): Promise => { const keyChainMessage = getKeyChainMessage(keyChainInfo); - const message = { - sndr: keyChainMessage.sender, - msg: keyChainMessage.body, - sub: keyChainMessage.title, - att: keyChainMessage.attachments.length > 0 ? keyChainMessage.attachments : undefined, - countedAtt: keyChainMessage.countedAttachments.length > 0 ? keyChainMessage.countedAttachments : undefined, - icon: keyChainMessage.icon ?? "", - transmission: keyChainMessage.transmission ?? "", - highPriority: keyChainMessage.highPriority ?? false, - r: false - } satisfies IMessage; - - await createMessage(accountId, [message]); + await createMessage(accountId, [keyChainMessage]); updateQuestStage(inventory, keyChainInfo, { m: true }); }; + +export const giveKeyChainMissionReward = async ( + inventory: TInventoryDatabaseDocument, + keyChainInfo: IKeyChainRequest +): Promise => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const chainStages = ExportKeys[keyChainInfo.KeyChain]?.chainStages; + + if (chainStages) { + const missionName = chainStages[keyChainInfo.ChainStage].key; + if (missionName) { + const fixedLevelRewards = getLevelKeyRewards(missionName); + if (fixedLevelRewards.levelKeyRewards) { + const missionRewards: { StoreItem: string; ItemCount: number }[] = []; + addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, inventory, missionRewards); + + for (const reward of missionRewards) { + await addItem(inventory, fromStoreItem(reward.StoreItem), reward.ItemCount); + } + + updateQuestStage(inventory, keyChainInfo, { c: 0 }); + } else if (fixedLevelRewards.levelKeyRewards2) { + for (const reward of fixedLevelRewards.levelKeyRewards2) { + if (reward.rewardType == "RT_CREDITS") { + inventory.RegularCredits += reward.amount; + continue; + } + if (reward.rewardType == "RT_RESOURCE") { + await addItem(inventory, fromStoreItem(reward.itemType), reward.amount); + } else { + await addItem(inventory, fromStoreItem(reward.itemType)); + } + } + + updateQuestStage(inventory, keyChainInfo, { c: 0 }); + } + } + } +}; + +export const giveKeyChainStageTriggered = async ( + inventory: TInventoryDatabaseDocument, + keyChainInfo: IKeyChainRequest +): Promise => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const chainStages = ExportKeys[keyChainInfo.KeyChain]?.chainStages; + + if (chainStages) { + if (chainStages[keyChainInfo.ChainStage].itemsToGiveWhenTriggered.length > 0) { + await giveKeyChainItem(inventory, keyChainInfo); + } + + if (chainStages[keyChainInfo.ChainStage].messageToSendWhenTriggered) { + await giveKeyChainMessage(inventory, inventory.accountOwnerId, keyChainInfo); + } + } +}; diff --git a/src/services/rngService.ts b/src/services/rngService.ts index f519c0d8..b98f7bd3 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -18,11 +18,11 @@ export const getRandomInt = (min: number, max: number): number => { return Math.floor(Math.random() * (max - min + 1)) + min; }; -export const getRandomReward = (pool: IRngResult[]): IRngResult | undefined => { +const getRewardAtPercentage = (pool: T[], percentage: number): T | undefined => { if (pool.length == 0) return; const totalChance = pool.reduce((accum, item) => accum + item.probability, 0); - const randomValue = Math.random() * totalChance; + const randomValue = percentage * totalChance; let cumulativeChance = 0; for (const item of pool) { @@ -34,40 +34,106 @@ export const getRandomReward = (pool: IRngResult[]): IRngResult | undefined => { throw new Error("What the fuck?"); }; -export const getRandomWeightedReward = ( - pool: { Item: string; Rarity: TRarity }[], - weights: Record -): IRngResult | undefined => { - const resultPool: IRngResult[] = []; - const rarityCounts: Record = { COMMON: 0, UNCOMMON: 0, RARE: 0, LEGENDARY: 0 }; - for (const entry of pool) { - ++rarityCounts[entry.Rarity]; - } - for (const entry of pool) { - resultPool.push({ - type: entry.Item, - itemCount: 1, - probability: weights[entry.Rarity] / rarityCounts[entry.Rarity] - }); - } - return getRandomReward(resultPool); +export const getRandomReward = (pool: T[]): T | undefined => { + return getRewardAtPercentage(pool, Math.random()); }; -export const getRandomWeightedReward2 = ( - pool: { type: string; itemCount: number; rarity: TRarity }[], +export const getRandomWeightedReward = ( + pool: T[], weights: Record -): IRngResult | undefined => { - const resultPool: IRngResult[] = []; +): (T & { probability: number }) | undefined => { + const resultPool: (T & { probability: number })[] = []; const rarityCounts: Record = { COMMON: 0, UNCOMMON: 0, RARE: 0, LEGENDARY: 0 }; for (const entry of pool) { ++rarityCounts[entry.rarity]; } for (const entry of pool) { resultPool.push({ - type: entry.type, - itemCount: entry.itemCount, + ...entry, probability: weights[entry.rarity] / rarityCounts[entry.rarity] }); } return getRandomReward(resultPool); }; + +export const getRandomWeightedRewardUc = ( + pool: T[], + weights: Record +): (T & { probability: number }) | undefined => { + const resultPool: (T & { probability: number })[] = []; + const rarityCounts: Record = { COMMON: 0, UNCOMMON: 0, RARE: 0, LEGENDARY: 0 }; + for (const entry of pool) { + ++rarityCounts[entry.Rarity]; + } + for (const entry of pool) { + resultPool.push({ + ...entry, + probability: weights[entry.Rarity] / rarityCounts[entry.Rarity] + }); + } + return getRandomReward(resultPool); +}; + +// ChatGPT generated this. It seems to have a good enough distribution. +export const mixSeeds = (seed1: number, seed2: number): number => { + let seed = seed1 ^ seed2; + seed ^= seed >>> 21; + seed ^= seed << 35; + seed ^= seed >>> 4; + return seed >>> 0; +}; + +// Seeded RNG for internal usage. Based on recommendations in the ISO C standards. +export class CRng { + state: number; + + constructor(seed: number = 1) { + this.state = seed; + } + + random(): number { + this.state = (this.state * 1103515245 + 12345) & 0x7fffffff; + return (this.state & 0x3fffffff) / 0x3fffffff; + } + + randomInt(min: number, max: number): number { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(this.random() * (max - min + 1)) + min; + } + + randomElement(arr: T[]): T { + return arr[Math.floor(this.random() * arr.length)]; + } + + randomReward(pool: T[]): T | undefined { + return getRewardAtPercentage(pool, this.random()); + } +} + +// Seeded RNG for cases where we need identical results to the game client. Based on work by Donald Knuth. +export class SRng { + state: bigint; + + constructor(seed: bigint) { + this.state = seed; + } + + randomInt(min: number, max: number): number { + const diff = max - min; + if (diff != 0) { + this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn; + min += (Number(this.state >> 32n) & 0x3fffffff) % (diff + 1); + } + return min; + } + + randomElement(arr: T[]): T { + return arr[this.randomInt(0, arr.length - 1)]; + } + + randomFloat(): number { + this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn; + return (Number(this.state >> 38n) & 0xffffff) * 0.000000059604645; + } +} diff --git a/src/services/saveLoadoutService.ts b/src/services/saveLoadoutService.ts index 63533e43..2f8711c6 100644 --- a/src/services/saveLoadoutService.ts +++ b/src/services/saveLoadoutService.ts @@ -28,8 +28,7 @@ export const handleInventoryItemConfigChange = async ( ): Promise => { const inventory = await getInventory(accountId); - for (const [_equipmentName, _equipment] of Object.entries(equipmentChanges)) { - const equipment = _equipment as ISaveLoadoutRequestNoUpgradeVer[keyof ISaveLoadoutRequestNoUpgradeVer]; + for (const [_equipmentName, equipment] of Object.entries(equipmentChanges)) { const equipmentName = _equipmentName as keyof ISaveLoadoutRequestNoUpgradeVer; if (isEmptyObject(equipment)) { @@ -85,9 +84,7 @@ export const handleInventoryItemConfigChange = async ( continue; } - const oldLoadoutConfig = loadout[loadoutSlot].find( - loadout => loadout._id.toString() === loadoutId - ); + const oldLoadoutConfig = loadout[loadoutSlot].id(loadoutId); const { ItemId, ...loadoutConfigItemIdRemoved } = loadoutConfig; const loadoutConfigDatabase: ILoadoutConfigDatabase = { @@ -140,6 +137,13 @@ export const handleInventoryItemConfigChange = async ( inventory.UseAdultOperatorLoadout = equipment as boolean; break; } + case "WeaponSkins": { + const itemEntries = equipment as IItemEntry; + for (const [itemId, itemConfigEntries] of Object.entries(itemEntries)) { + inventory.WeaponSkins.id(itemId)!.IsNew = itemConfigEntries.IsNew; + } + break; + } default: { if (equipmentKeys.includes(equipmentName as TEquipmentKey) && equipmentName != "ValidNewLoadoutId") { logger.debug(`general Item config saved of type ${equipmentName}`, { @@ -155,7 +159,12 @@ export const handleInventoryItemConfigChange = async ( } for (const [configId, config] of Object.entries(itemConfigEntries)) { - inventoryItem.Configs[parseInt(configId)] = config; + if (typeof config !== "boolean") { + inventoryItem.Configs[parseInt(configId)] = config; + } + } + if ("IsNew" in itemConfigEntries) { + inventoryItem.IsNew = itemConfigEntries.IsNew; } } break; diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index b06f6ae7..961b5faa 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -1,98 +1,143 @@ -import { IMongoDate, IOid } from "@/src/types/commonTypes"; +import fs from "fs"; +import path from "path"; +import { repoDir } from "@/src/helpers/pathHelper"; +import { CRng, mixSeeds } from "@/src/services/rngService"; +import { IMongoDate } from "@/src/types/commonTypes"; +import { IItemManifestPreprocessed, IRawVendorManifest, IVendorManifestPreprocessed } from "@/src/types/vendorTypes"; +import { JSONParse } from "json-with-bigint"; +import { ExportVendors } from "warframe-public-export-plus"; -import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json"; -import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json"; -import DeimosFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosFishmongerVendorManifest.json"; -import DeimosHivemindCommisionsManifestFishmonger from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestFishmonger.json"; -import DeimosHivemindCommisionsManifestPetVendor from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestPetVendor.json"; -import DeimosHivemindCommisionsManifestProspector from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestProspector.json"; -import DeimosHivemindCommisionsManifestTokenVendor from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestTokenVendor.json"; -import DeimosHivemindCommisionsManifestWeaponsmith from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestWeaponsmith.json"; -import DeimosHivemindTokenVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosHivemindTokenVendorManifest.json"; -import DeimosPetVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosPetVendorManifest.json"; -import DeimosProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosProspectorVendorManifest.json"; -import DuviriAcrithisVendorManifest from "@/static/fixed_responses/getVendorInfo/DuviriAcrithisVendorManifest.json"; -import EntratiLabsEntratiLabsCommisionsManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabsCommisionsManifest.json"; -import EntratiLabsEntratiLabVendorManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabVendorManifest.json"; -import HubsIronwakeDondaVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json"; -import HubsPerrinSequenceWeaponVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsPerrinSequenceWeaponVendorManifest.json"; -import HubsRailjackCrewMemberVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json"; -import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSalesmanManifest.json"; -import OstronFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronFishmongerVendorManifest.json"; -import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json"; -import OstronProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronProspectorVendorManifest.json"; -import RadioLegionIntermission12VendorManifest from "@/static/fixed_responses/getVendorInfo/RadioLegionIntermission12VendorManifest.json"; -import SolarisDebtTokenVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorManifest.json"; -import SolarisDebtTokenVendorRepossessionsManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorRepossessionsManifest.json"; -import SolarisFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisFishmongerVendorManifest.json"; -import SolarisProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisProspectorVendorManifest.json"; -import TeshinHardModeVendorManifest from "@/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json"; -import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVendorInfo/ZarimanCommisionsManifestArchimedean.json"; +const getVendorManifestJson = (name: string): IRawVendorManifest => { + return JSONParse(fs.readFileSync(path.join(repoDir, `static/fixed_responses/getVendorInfo/${name}.json`), "utf-8")); +}; -interface IVendorManifest { - VendorInfo: { - _id: IOid; - TypeName: string; - ItemManifest: { - StoreItem: string; - ItemPrices?: { ItemType: string; ItemCount: number; ProductCategory: string }[]; - Bin: string; - QuantityMultiplier: number; - Expiry: IMongoDate; - PurchaseQuantityLimit?: number; - RotatedWeekly?: boolean; - AllowMultipurchase: boolean; - Id: IOid; - }[]; - Expiry: IMongoDate; - }; -} - -const vendorManifests: IVendorManifest[] = [ - ArchimedeanVendorManifest, - DeimosEntratiFragmentVendorProductsManifest, - DeimosFishmongerVendorManifest, - DeimosHivemindCommisionsManifestFishmonger, - DeimosHivemindCommisionsManifestPetVendor, - DeimosHivemindCommisionsManifestProspector, - DeimosHivemindCommisionsManifestTokenVendor, - DeimosHivemindCommisionsManifestWeaponsmith, - DeimosHivemindTokenVendorManifest, - DeimosPetVendorManifest, - DeimosProspectorVendorManifest, - DuviriAcrithisVendorManifest, - EntratiLabsEntratiLabsCommisionsManifest, - EntratiLabsEntratiLabVendorManifest, - HubsIronwakeDondaVendorManifest, - HubsPerrinSequenceWeaponVendorManifest, - HubsRailjackCrewMemberVendorManifest, - MaskSalesmanManifest, - OstronFishmongerVendorManifest, - OstronPetVendorManifest, - OstronProspectorVendorManifest, - RadioLegionIntermission12VendorManifest, - SolarisDebtTokenVendorManifest, - SolarisDebtTokenVendorRepossessionsManifest, - SolarisFishmongerVendorManifest, - SolarisProspectorVendorManifest, - TeshinHardModeVendorManifest, - ZarimanCommisionsManifestArchimedean +const rawVendorManifests: IRawVendorManifest[] = [ + getVendorManifestJson("ArchimedeanVendorManifest"), + getVendorManifestJson("DeimosEntratiFragmentVendorProductsManifest"), + getVendorManifestJson("DeimosFishmongerVendorManifest"), + getVendorManifestJson("DeimosHivemindCommisionsManifestFishmonger"), + getVendorManifestJson("DeimosHivemindCommisionsManifestPetVendor"), + getVendorManifestJson("DeimosHivemindCommisionsManifestProspector"), + getVendorManifestJson("DeimosHivemindCommisionsManifestTokenVendor"), + getVendorManifestJson("DeimosHivemindCommisionsManifestWeaponsmith"), + getVendorManifestJson("DeimosHivemindTokenVendorManifest"), + getVendorManifestJson("DeimosPetVendorManifest"), + getVendorManifestJson("DeimosProspectorVendorManifest"), + getVendorManifestJson("DuviriAcrithisVendorManifest"), + getVendorManifestJson("EntratiLabsEntratiLabsCommisionsManifest"), + getVendorManifestJson("EntratiLabsEntratiLabVendorManifest"), + getVendorManifestJson("GuildAdvertisementVendorManifest"), // uses preprocessing + getVendorManifestJson("HubsIronwakeDondaVendorManifest"), // uses preprocessing + getVendorManifestJson("HubsPerrinSequenceWeaponVendorManifest"), + getVendorManifestJson("HubsRailjackCrewMemberVendorManifest"), + getVendorManifestJson("MaskSalesmanManifest"), + getVendorManifestJson("Nova1999ConquestShopManifest"), + getVendorManifestJson("OstronFishmongerVendorManifest"), + getVendorManifestJson("OstronPetVendorManifest"), + getVendorManifestJson("OstronProspectorVendorManifest"), + getVendorManifestJson("RadioLegionIntermission12VendorManifest"), + getVendorManifestJson("SolarisDebtTokenVendorManifest"), + getVendorManifestJson("SolarisDebtTokenVendorRepossessionsManifest"), + getVendorManifestJson("SolarisFishmongerVendorManifest"), + getVendorManifestJson("SolarisProspectorVendorManifest"), + getVendorManifestJson("TeshinHardModeVendorManifest"), // uses preprocessing + getVendorManifestJson("ZarimanCommisionsManifestArchimedean") ]; -export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => { - for (const vendorManifest of vendorManifests) { +export const getVendorManifestByTypeName = (typeName: string): IVendorManifestPreprocessed | undefined => { + for (const vendorManifest of rawVendorManifests) { if (vendorManifest.VendorInfo.TypeName == typeName) { - return vendorManifest; + return preprocessVendorManifest(vendorManifest); } } + if (typeName == "/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest") { + return generateCodaWeaponVendorManifest(); + } return undefined; }; -export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined => { - for (const vendorManifest of vendorManifests) { +export const getVendorManifestByOid = (oid: string): IVendorManifestPreprocessed | undefined => { + for (const vendorManifest of rawVendorManifests) { if (vendorManifest.VendorInfo._id.$oid == oid) { - return vendorManifest; + return preprocessVendorManifest(vendorManifest); } } + if (oid == "67dadc30e4b6e0e5979c8d84") { + return generateCodaWeaponVendorManifest(); + } return undefined; }; + +const preprocessVendorManifest = (originalManifest: IRawVendorManifest): IVendorManifestPreprocessed => { + if (Date.now() >= parseInt(originalManifest.VendorInfo.Expiry.$date.$numberLong)) { + const manifest = structuredClone(originalManifest); + const info = manifest.VendorInfo; + refreshExpiry(info.Expiry); + for (const offer of info.ItemManifest) { + const iteration = refreshExpiry(offer.Expiry); + if (offer.ItemPrices) { + for (const price of offer.ItemPrices) { + if (typeof price.ItemType != "string") { + const itemSeed = parseInt(offer.Id.$oid.substring(16), 16); + const rng = new CRng(mixSeeds(itemSeed, iteration)); + price.ItemType = rng.randomElement(price.ItemType); + } + } + } + } + return manifest as IVendorManifestPreprocessed; + } + return originalManifest as IVendorManifestPreprocessed; +}; + +const refreshExpiry = (expiry: IMongoDate): number => { + const period = parseInt(expiry.$date.$numberLong); + if (Date.now() >= period) { + const epoch = 1734307200 * 1000; // Monday (for weekly schedules) + const iteration = Math.trunc((Date.now() - epoch) / period); + const start = epoch + iteration * period; + const end = start + period; + expiry.$date.$numberLong = end.toString(); + return iteration; + } + return 0; +}; + +const generateCodaWeaponVendorManifest = (): IVendorManifestPreprocessed => { + const EPOCH = 1740960000 * 1000; + const DUR = 4 * 86400 * 1000; + const cycle = Math.trunc((Date.now() - EPOCH) / DUR); + const cycleStart = EPOCH + cycle * DUR; + const cycleEnd = cycleStart + DUR; + const binThisCycle = cycle % 2; // isOneBinPerCycle + const items: IItemManifestPreprocessed[] = []; + const manifest = ExportVendors["/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest"]; + const rng = new CRng(cycle); + for (const rawItem of manifest.items) { + if (rawItem.bin != binThisCycle) { + continue; + } + items.push({ + StoreItem: rawItem.storeItem, + ItemPrices: rawItem.itemPrices!.map(item => ({ ...item, ProductCategory: "MiscItems" })), + Bin: "BIN_" + rawItem.bin, + QuantityMultiplier: 1, + Expiry: { $date: { $numberLong: cycleEnd.toString() } }, + AllowMultipurchase: false, + LocTagRandSeed: (BigInt(rng.randomInt(0, 0xffffffff)) << 32n) | BigInt(rng.randomInt(0, 0xffffffff)), + Id: { $oid: "67e9da12793a120d" + rng.randomInt(0, 0xffffffff).toString(16).padStart(8, "0") } + }); + } + return { + VendorInfo: { + _id: { $oid: "67dadc30e4b6e0e5979c8d84" }, + TypeName: "/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest", + ItemManifest: items, + PropertyTextHash: "77093DD05A8561A022DEC9A4B9BB4A56", + RandomSeedType: "VRST_WEAPON", + RequiredGoalTag: "", + WeaponUpgradeValueAttenuationExponent: 2.25, + Expiry: { $date: { $numberLong: cycleEnd.toString() } } + } + }; +}; diff --git a/src/services/shipCustomizationsService.ts b/src/services/shipCustomizationsService.ts index bc7ca5fb..47764917 100644 --- a/src/services/shipCustomizationsService.ts +++ b/src/services/shipCustomizationsService.ts @@ -10,6 +10,9 @@ import { logger } from "@/src/utils/logger"; import { Types } from "mongoose"; import { addShipDecorations, getInventory } from "./inventoryService"; import { config } from "./configService"; +import { Guild } from "../models/guildModel"; +import { hasGuildPermission } from "./guildService"; +import { GuildPermission } from "../types/guildTypes"; export const setShipCustomizations = async ( accountId: string, @@ -61,19 +64,17 @@ export const handleSetShipDecorations = async ( if (placedDecoration.MoveId) { //moved within the same room if (placedDecoration.OldRoom === placedDecoration.Room) { - const existingDecorationIndex = roomToPlaceIn.PlacedDecos.findIndex( - deco => deco._id.toString() === placedDecoration.MoveId - ); + const existingDecoration = roomToPlaceIn.PlacedDecos.id(placedDecoration.MoveId); - if (existingDecorationIndex === -1) { + if (!existingDecoration) { throw new Error("decoration to be moved not found"); } - roomToPlaceIn.PlacedDecos[existingDecorationIndex].Pos = placedDecoration.Pos; - roomToPlaceIn.PlacedDecos[existingDecorationIndex].Rot = placedDecoration.Rot; + existingDecoration.Pos = placedDecoration.Pos; + existingDecoration.Rot = placedDecoration.Rot; if (placedDecoration.Scale) { - roomToPlaceIn.PlacedDecos[existingDecorationIndex].Scale = placedDecoration.Scale; + existingDecoration.Scale = placedDecoration.Scale; } await personalRooms.save(); @@ -156,6 +157,17 @@ export const handleSetShipDecorations = async ( }; export const handleSetPlacedDecoInfo = async (accountId: string, req: ISetPlacedDecoInfoRequest): Promise => { + if (req.GuildId && req.ComponentId) { + const guild = (await Guild.findById(req.GuildId))!; + if (await hasGuildPermission(guild, accountId, GuildPermission.Decorator)) { + const component = guild.DojoComponents.id(req.ComponentId)!; + const deco = component.Decos!.find(x => x._id.equals(req.DecoId))!; + deco.PictureFrameInfo = req.PictureFrameInfo; + await guild.save(); + } + return; + } + const personalRooms = await getPersonalRooms(accountId); const room = personalRooms.Ship.Rooms.find(room => room.Name === req.Room); diff --git a/src/services/shipService.ts b/src/services/shipService.ts index 0925a409..7552fb97 100644 --- a/src/services/shipService.ts +++ b/src/services/shipService.ts @@ -1,11 +1,10 @@ -import { Ship } from "@/src/models/shipModel"; -import { ILoadoutDatabase } from "@/src/types/saveLoadoutTypes"; +import { Ship, TShipDatabaseDocument } from "@/src/models/shipModel"; import { Types } from "mongoose"; export const createShip = async ( accountOwnerId: Types.ObjectId, typeName: string = "/Lotus/Types/Items/Ships/DefaultShip" -) => { +): Promise => { try { const ship = new Ship({ ItemType: typeName, @@ -21,8 +20,8 @@ export const createShip = async ( } }; -export const getShip = async (shipId: Types.ObjectId, fieldSelection: string = "") => { - const ship = await Ship.findOne({ _id: shipId }, fieldSelection); +export const getShip = async (shipId: Types.ObjectId, fieldSelection: string = ""): Promise => { + const ship = await Ship.findById(shipId, fieldSelection); if (!ship) { throw new Error(`error finding a ship with id ${shipId.toString()}`); @@ -30,15 +29,3 @@ export const getShip = async (shipId: Types.ObjectId, fieldSelection: string = " return ship; }; - -export const getShipLean = async (shipOwnerId: string) => { - const ship = await Ship.findOne({ ShipOwnerId: shipOwnerId }).lean().populate<{ - LoadOutInventory: { LoadOutPresets: ILoadoutDatabase }; - }>("LoadOutInventory.LoadOutPresets"); - - if (!ship) { - throw new Error(`error finding a ship for account ${shipOwnerId}`); - } - - return ship; -}; diff --git a/src/services/statsService.ts b/src/services/statsService.ts index 49bcea3f..601aae7b 100644 --- a/src/services/statsService.ts +++ b/src/services/statsService.ts @@ -9,7 +9,9 @@ import { IUploadEntry, IWeapon } from "@/src/types/statTypes"; -import { logger } from "../utils/logger"; +import { logger } from "@/src/utils/logger"; +import { addEmailItem, getInventory } from "@/src/services/inventoryService"; +import { submitLeaderboardScore } from "./leaderboardService"; export const createStats = async (accountId: string): Promise => { const stats = new Stats({ accountOwnerId: accountId }); @@ -25,8 +27,9 @@ export const getStats = async (accountOwnerId: string): Promise => { +export const updateStats = async (accountOwnerId: string, payload: IStatsUpdate): Promise => { const unknownCategories: Record = {}; + const playerStats = await getStats(accountOwnerId); for (const [action, actionData] of Object.entries(payload)) { switch (action) { @@ -82,7 +85,6 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: for (const [type, scans] of Object.entries(data as IUploadEntry)) { const scan = playerStats.Scans.find(element => element.type === type); if (scan) { - scan.scans ??= 0; scan.scans += scans; } else { playerStats.Scans.push({ type: type, scans }); @@ -95,7 +97,6 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: for (const [type, used] of Object.entries(data as IUploadEntry)) { const ability = playerStats.Abilities.find(element => element.type === type); if (ability) { - ability.used ??= 0; ability.used += used; } else { playerStats.Abilities.push({ type: type, used }); @@ -106,14 +107,16 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: case "FIRE_WEAPON": case "HIT_ENTITY_ITEM": case "HEADSHOT_ITEM": - case "KILL_ENEMY_ITEM": { + case "KILL_ENEMY_ITEM": + case "KILL_ASSIST_ITEM": { playerStats.Weapons ??= []; const statKey = { FIRE_WEAPON: "fired", HIT_ENTITY_ITEM: "hits", HEADSHOT_ITEM: "headshots", - KILL_ENEMY_ITEM: "kills" - }[category] as "fired" | "hits" | "headshots" | "kills"; + KILL_ENEMY_ITEM: "kills", + KILL_ASSIST_ITEM: "assists" + }[category] as "fired" | "hits" | "headshots" | "kills" | "assists"; for (const [type, count] of Object.entries(data as IUploadEntry)) { const weapon = playerStats.Weapons.find(element => element.type === type); @@ -131,19 +134,33 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: case "KILL_ENEMY": case "EXECUTE_ENEMY": - case "HEADSHOT": { + case "HEADSHOT": + case "KILL_ASSIST": { playerStats.Enemies ??= []; const enemyStatKey = { KILL_ENEMY: "kills", EXECUTE_ENEMY: "executions", - HEADSHOT: "headshots" - }[category] as "kills" | "executions" | "headshots"; + HEADSHOT: "headshots", + KILL_ASSIST: "assists" + }[category] as "kills" | "executions" | "headshots" | "assists"; for (const [type, count] of Object.entries(data as IUploadEntry)) { const enemy = playerStats.Enemies.find(element => element.type === type); if (enemy) { - enemy[enemyStatKey] ??= 0; - enemy[enemyStatKey] += count; + if (category === "KILL_ENEMY") { + enemy.kills ??= 0; + const captureCount = (actionData as IStatsAdd)["CAPTURE_ENEMY"]?.[type]; + if (captureCount) { + enemy.kills += Math.max(count - captureCount, 0); + enemy.captures ??= 0; + enemy.captures += captureCount; + } else { + enemy.kills += count; + } + } else { + enemy[enemyStatKey] ??= 0; + enemy[enemyStatKey] += count; + } } else { const newEnemy: IEnemy = { type: type }; newEnemy[enemyStatKey] = count; @@ -182,21 +199,19 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: break; case "CIPHER": - if (data["0"] > 0) { + if ((data as IUploadEntry)["0"] > 0) { playerStats.CiphersFailed ??= 0; - playerStats.CiphersFailed += data["0"]; + playerStats.CiphersFailed += (data as IUploadEntry)["0"]; } - if (data["1"] > 0) { + if ((data as IUploadEntry)["1"] > 0) { playerStats.CiphersSolved ??= 0; - playerStats.CiphersSolved += data["1"]; + playerStats.CiphersSolved += (data as IUploadEntry)["1"]; } break; default: if (!ignoredCategories.includes(category)) { - if (!unknownCategories[action]) { - unknownCategories[action] = []; - } + unknownCategories[action] ??= []; unknownCategories[action].push(category); } break; @@ -271,6 +286,15 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: } else { playerStats.Missions.push({ type: type, highScore }); } + await submitLeaderboardScore( + "weekly", + type, + accountOwnerId, + payload.displayName, + highScore, + payload.guildId + ); + break; } break; @@ -287,15 +311,80 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: } else { playerStats.Races.set(race, { highScore }); } + + await submitLeaderboardScore( + "daily", + race, + accountOwnerId, + payload.displayName, + highScore, + payload.guildId + ); } break; + case "ZephyrScore": + case "CaliberChicksScore": + playerStats[category] ??= 0; + if (data > playerStats[category]) playerStats[category] = data as number; + break; + + case "SentinelGameScore": + playerStats[category] ??= 0; + if (data > playerStats[category]) playerStats[category] = data as number; + await submitLeaderboardScore( + "weekly", + category, + accountOwnerId, + payload.displayName, + data as number, + payload.guildId + ); + break; + + case "DojoObstacleScore": + playerStats[category] ??= 0; + if (data > playerStats[category]) playerStats[category] = data as number; + await submitLeaderboardScore( + "weekly", + category, + accountOwnerId, + payload.displayName, + data as number, + payload.guildId + ); + break; + + case "OlliesCrashCourseScore": + playerStats[category] ??= 0; + if (!playerStats[category]) { + const inventory = await getInventory(accountOwnerId, "EmailItems"); + await addEmailItem( + inventory, + "/Lotus/Types/Items/EmailItems/PlayedOlliesCrashCourseEmailItem" + ); + } + if (data >= 9991000 && playerStats[category] < 9991000) { + const inventory = await getInventory(accountOwnerId, "EmailItems"); + await addEmailItem( + inventory, + "/Lotus/Types/Items/EmailItems/BeatOlliesCrashCourseInNinetySecEmailItem" + ); + } + if (data > playerStats[category]) playerStats[category] = data as number; + await submitLeaderboardScore( + "weekly", + category, + accountOwnerId, + payload.displayName, + data as number + ); + break; + default: if (!ignoredCategories.includes(category)) { - if (!unknownCategories[action]) { - unknownCategories[action] = []; - } + unknownCategories[action] ??= []; unknownCategories[action].push(category); } break; @@ -307,22 +396,20 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: for (const [category, value] of Object.entries(actionData as IStatsSet)) { switch (category) { case "ELO_RATING": - playerStats.Rating = value; + playerStats.Rating = value as number; break; case "RANK": - playerStats.Rank = value; + playerStats.Rank = value as number; break; case "PLAYER_LEVEL": - playerStats.PlayerLevel = value; + playerStats.PlayerLevel = value as number; break; default: if (!ignoredCategories.includes(category)) { - if (!unknownCategories[action]) { - unknownCategories[action] = []; - } + unknownCategories[action] ??= []; unknownCategories[action].push(category); } break; @@ -398,6 +485,7 @@ const ignoredCategories = [ "PRE_DIE_ITEM", "GEAR_USED", "DIE_ITEM", + "CAPTURE_ENEMY", // handled in KILL_ENEMY // timers action "IN_SHIP_TIME", diff --git a/src/types/genericUpdate.ts b/src/types/genericUpdate.ts index fa231be9..93551b05 100644 --- a/src/types/genericUpdate.ts +++ b/src/types/genericUpdate.ts @@ -1,4 +1,11 @@ +import { IInventoryChanges } from "./purchaseTypes"; + export interface IGenericUpdate { NodeIntrosCompleted: string | string[]; // AffiliationMods: any[]; } + +export interface IUpdateNodeIntrosResponse { + MissionRewards: []; + InventoryChanges: IInventoryChanges; +} diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 28a51c71..8af68809 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -1,30 +1,162 @@ import { Types } from "mongoose"; import { IOid, IMongoDate } from "@/src/types/commonTypes"; -import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IPictureFrameInfo } from "./shipTypes"; -export interface IGuild { +export interface IGuildClient { + _id: IOid; Name: string; + MOTD: string; + LongMOTD?: ILongMOTD; + Members: IGuildMemberClient[]; + Ranks: IGuildRank[]; + Tier: number; + Vault: IGuildVault; + ActiveDojoColorResearch: string; + Class: number; + XP: number; + IsContributor: boolean; + NumContributors: number; + CeremonyResetDate?: IMongoDate; + CrossPlatformEnabled?: boolean; + AutoContributeFromVault?: boolean; + AllianceId?: IOid; } -export interface IGuildDatabase extends IGuild { +export interface IGuildDatabase { _id: Types.ObjectId; - DojoComponents?: IDojoComponentDatabase[]; + Name: string; + MOTD: string; + LongMOTD?: ILongMOTD; + Ranks: IGuildRank[]; + TradeTax: number; + Tier: number; + Emblem?: boolean; + AutoContributeFromVault?: boolean; + AllianceId?: Types.ObjectId; + + DojoComponents: IDojoComponentDatabase[]; DojoCapacity: number; DojoEnergy: number; + + VaultRegularCredits?: number; + VaultPremiumCredits?: number; + VaultMiscItems?: IMiscItem[]; + VaultShipDecorations?: ITypeCount[]; + VaultFusionTreasures?: IFusionTreasure[]; + TechProjects?: ITechProjectDatabase[]; + ActiveDojoColorResearch: string; + + Class: number; + XP: number; + ClaimedXP?: string[]; // track rooms and decos that have already granted XP + CeremonyClass?: number; + CeremonyEndo?: number; + CeremonyContributors?: Types.ObjectId[]; + CeremonyResetDate?: Date; + + RoomChanges?: IGuildLogRoomChange[]; + TechChanges?: IGuildLogEntryContributable[]; + RosterActivity?: IGuildLogEntryRoster[]; + ClassChanges?: IGuildLogEntryNumber[]; +} + +export interface ILongMOTD { + message: string; + authorName: string; + authorGuildName?: string; +} + +// 32 seems to be reserved +export enum GuildPermission { + Ruler = 1, // Clan: Change hierarchy. Alliance (Creator only): Kick clans. + Advertiser = 8192, + Recruiter = 2, // Send invites (Clans & Alliances) + Regulator = 4, // Kick members + Promoter = 8, // Clan: Promote and demote members. Alliance (Creator only): Change clan permissions. + Architect = 16, // Create and destroy rooms + Decorator = 1024, // Create and destroy decos + Treasurer = 64, // Clan: Contribute from vault and edit tax rate. Alliance: Divvy vault. + Tech = 128, // Queue research + ChatModerator = 512, // (Clans & Alliances) + Herald = 2048, // Change MOTD + Fabricator = 4096 // Replicate research +} + +export interface IGuildRank { + Name: string; + Permissions: number; +} + +export interface IGuildMemberDatabase { + accountId: Types.ObjectId; + guildId: Types.ObjectId; + status: number; + rank: number; + RequestMsg?: string; + RequestExpiry?: Date; + RegularCreditsContributed?: number; + PremiumCreditsContributed?: number; + MiscItemsContributed?: IMiscItem[]; + ShipDecorationsContributed?: ITypeCount[]; +} + +interface IFriendInfo { + _id: IOid; + DisplayName?: string; + PlatformNames?: string[]; + PlatformAccountId?: string; + Status: number; + ActiveAvatarImageType?: string; + LastLogin?: IMongoDate; + PlayerLevel?: number; + Suffix?: number; + Note?: string; + Favorite?: boolean; + NewRequest?: boolean; +} + +// GuildMemberInfo +export interface IGuildMemberClient extends IFriendInfo { + Rank: number; + Joined?: IMongoDate; + RequestExpiry?: IMongoDate; + RegularCreditsContributed?: number; + PremiumCreditsContributed?: number; + MiscItemsContributed?: IMiscItem[]; + ConsumablesContributed?: ITypeCount[]; + ShipDecorationsContributed?: ITypeCount[]; +} + +export interface IGuildVault { + DojoRefundRegularCredits?: number; + DojoRefundMiscItems?: IMiscItem[]; + DojoRefundPremiumCredits?: number; + ShipDecorations?: ITypeCount[]; + FusionTreasures?: IFusionTreasure[]; + DecoRecipes?: ITypeCount[]; // Event Trophies } export interface IDojoClient { _id: IOid; // ID of the guild Name: string; Tier: number; + TradeTax?: number; FixedContributions: boolean; DojoRevision: number; + AllianceId?: IOid; + Vault?: IGuildVault; + Class?: number; // Level RevisionTime: number; Energy: number; Capacity: number; DojoRequestStatus: number; + ContentURL?: string; + GuildEmblem?: boolean; DojoComponents: IDojoComponentClient[]; + NumContributors?: number; + CeremonyResetDate?: IMongoDate; } export interface IDojoComponentClient { @@ -39,14 +171,48 @@ export interface IDojoComponentClient { RegularCredits?: number; // "Collecting Materials" state: Number of credits that were donated. MiscItems?: IMiscItem[]; // "Collecting Materials" state: Resources that were donated. CompletionTime?: IMongoDate; + RushPlatinum?: number; + DestructionTime?: IMongoDate; + Decos?: IDojoDecoClient[]; DecoCapacity?: number; } export interface IDojoComponentDatabase - extends Omit { + extends Omit { _id: Types.ObjectId; pi?: Types.ObjectId; CompletionTime?: Date; + CompletionLogPending?: boolean; + DestructionTime?: Date; + Decos?: IDojoDecoDatabase[]; + Leaderboard?: IDojoLeaderboardEntry[]; +} + +export interface IDojoDecoClient { + id: IOid; + Type: string; + Pos: number[]; + Rot: number[]; + Name?: string; // for teleporters + Sockets?: number; + RegularCredits?: number; + MiscItems?: IMiscItem[]; + CompletionTime?: IMongoDate; + RushPlatinum?: number; + PictureFrameInfo?: IPictureFrameInfo; +} + +export interface IDojoDecoDatabase extends Omit { + _id: Types.ObjectId; + CompletionTime?: Date; +} + +// A common subset of the database representation of rooms & decos. +export interface IDojoContributable { + RegularCredits?: number; + MiscItems?: IMiscItem[]; + CompletionTime?: Date; + RushPlatinum?: number; } export interface ITechProjectClient { @@ -60,3 +226,96 @@ export interface ITechProjectClient { export interface ITechProjectDatabase extends Omit { CompletionDate?: Date; } + +export interface IGuildLogEntryContributable { + dateTime?: Date; + entryType: number; + details: string; +} + +export interface IGuildLogRoomChange extends IGuildLogEntryContributable { + componentId: Types.ObjectId; +} + +export interface IGuildLogEntryRoster { + dateTime: Date; + entryType: number; + details: string; +} + +export interface IGuildLogEntryNumber { + dateTime: Date; + entryType: number; + details: number; +} + +export interface IDojoLeaderboardEntry { + s: number; // score + r: number; // rank + n: string; // displayName +} + +export interface IGuildAdInfoClient { + _id: IOid; // Guild ID + CrossPlatformEnabled: boolean; + Emblem?: boolean; + Expiry: IMongoDate; + Features: number; + GuildName: string; + MemberCount: number; + OriginalPlatform: number; + RecruitMsg: string; + Tier: number; +} + +export interface IGuildAdDatabase { + GuildId: Types.ObjectId; + Emblem?: boolean; + Expiry: Date; + Features: number; + GuildName: string; + MemberCount: number; + RecruitMsg: string; + Tier: number; +} + +export interface IAllianceClient { + _id: IOid; + Name: string; + MOTD?: ILongMOTD; + LongMOTD?: ILongMOTD; + Emblem?: boolean; + CrossPlatformEnabled?: boolean; + Clans: IAllianceMemberClient[]; + OriginalPlatform?: number; + AllianceVault?: IGuildVault; +} + +export interface IAllianceDatabase { + _id: Types.ObjectId; + Name: string; + MOTD?: ILongMOTD; + LongMOTD?: ILongMOTD; + Emblem?: boolean; + VaultRegularCredits?: number; +} + +export interface IAllianceMemberClient { + _id: IOid; + Name: string; + Tier: number; + Pending: boolean; + Emblem?: boolean; + Permissions: number; + MemberCount: number; + ClanLeader?: string; + ClanLeaderId?: IOid; + OriginalPlatform?: number; +} + +export interface IAllianceMemberDatabase { + allianceId: Types.ObjectId; + guildId: Types.ObjectId; + Pending: boolean; + Permissions: number; +} diff --git a/src/types/inventoryTypes/commonInventoryTypes.ts b/src/types/inventoryTypes/commonInventoryTypes.ts index 5f3aeac0..ea26992a 100644 --- a/src/types/inventoryTypes/commonInventoryTypes.ts +++ b/src/types/inventoryTypes/commonInventoryTypes.ts @@ -106,7 +106,8 @@ export enum EquipmentFeatures { GRAVIMAG_INSTALLED = 4, GILDED = 8, ARCANE_SLOT = 32, - INCARNON_GENESIS = 512 + INCARNON_GENESIS = 512, + VALENCE_SWAP = 1024 } export interface IEquipmentDatabase { @@ -139,6 +140,7 @@ export interface IEquipmentDatabase { RailjackImage?: IFlavourItem; CrewMembers?: ICrewShipMembersDatabase; Details?: IKubrowPetDetailsDatabase; + IsNew?: boolean; _id: Types.ObjectId; } diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index e93f6b3c..c25b7123 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -2,7 +2,6 @@ import { Types } from "mongoose"; import { IOid, IMongoDate } from "../commonTypes"; import { - ArtifactPolarity, IColor, IItemConfig, IOperatorConfigClient, @@ -11,6 +10,7 @@ import { IEquipmentClient, IOperatorConfigDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { IFingerprintStat, RivenFingerprint } from "@/src/helpers/rivenHelper"; export type InventoryDatabaseEquipment = { [_ in TEquipmentKey]: IEquipmentDatabase[]; @@ -30,8 +30,8 @@ export interface IInventoryDatabase | "Ships" | "WeaponSkins" | "Upgrades" - | "CrewShipSalvagedWeaponSkins" | "CrewShipWeaponSkins" + | "CrewShipSalvagedWeaponSkins" | "AdultOperatorLoadOuts" | "OperatorLoadOuts" | "KahlLoadOuts" @@ -40,6 +40,12 @@ export interface IInventoryDatabase | "KubrowPetEggs" | "PendingCoupon" | "Drones" + | "RecentVendorPurchases" + | "NextRefill" + | "Nemesis" + | "EntratiVaultCountResetDate" + | "BrandedSuits" + | "LockedWeaponGroup" | TEquipmentKey >, InventoryDatabaseEquipment { @@ -49,14 +55,14 @@ export interface IInventoryDatabase LoadOutPresets: Types.ObjectId; // LoadOutPresets changed from ILoadOutPresets to Types.ObjectId for population Mailbox?: IMailboxDatabase; GuildId?: Types.ObjectId; - PendingRecipes: IPendingRecipe[]; + PendingRecipes: IPendingRecipeDatabase[]; QuestKeys: IQuestKeyDatabase[]; BlessingCooldown?: Date; Ships: Types.ObjectId[]; WeaponSkins: IWeaponSkinDatabase[]; Upgrades: IUpgradeDatabase[]; - CrewShipSalvagedWeaponSkins: IUpgradeDatabase[]; CrewShipWeaponSkins: IUpgradeDatabase[]; + CrewShipSalvagedWeaponSkins: IUpgradeDatabase[]; AdultOperatorLoadOuts: IOperatorConfigDatabase[]; OperatorLoadOuts: IOperatorConfigDatabase[]; KahlLoadOuts: IOperatorConfigDatabase[]; @@ -65,6 +71,12 @@ export interface IInventoryDatabase KubrowPetEggs?: IKubrowPetEggDatabase[]; PendingCoupon?: IPendingCouponDatabase; Drones: IDroneDatabase[]; + RecentVendorPurchases?: IRecentVendorPurchaseDatabase[]; + NextRefill?: Date; + Nemesis?: INemesisDatabase; + EntratiVaultCountResetDate?: Date; + BrandedSuits?: Types.ObjectId[]; + LockedWeaponGroup?: ILockedWeaponGroupDatabase; } export interface IQuestKeyDatabase { @@ -104,7 +116,9 @@ export const equipmentKeys = [ "DataKnives", "MechSuits", "CrewShipHarnesses", - "KubrowPets" + "KubrowPets", + "CrewShipWeapons", + "CrewShipSalvagedWeapons" ] as const; export type TEquipmentKey = (typeof equipmentKeys)[number]; @@ -143,10 +157,6 @@ export type TSolarMapRegion = //TODO: perhaps split response and database into their own files -export interface IPendingRecipeResponse extends Omit { - CompletionDate: IMongoDate; -} - export interface IDailyAffiliations { DailyAffiliation: number; DailyAffiliationPvp: number; @@ -217,7 +227,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu XPInfo: ITypeXPItem[]; Recipes: ITypeCount[]; WeaponSkins: IWeaponSkinClient[]; - PendingRecipes: IPendingRecipeResponse[]; + PendingRecipes: IPendingRecipeClient[]; TrainingDate: IMongoDate; PlayerLevel: number; Staff?: boolean; @@ -234,8 +244,8 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu FusionTreasures: IFusionTreasure[]; WebFlags: IWebFlags; CompletedAlerts: string[]; - Consumables: IConsumable[]; - LevelKeys: IConsumable[]; + Consumables: ITypeCount[]; + LevelKeys: ITypeCount[]; TauntHistory?: ITaunt[]; StoryModeChoice: string; PeriodicMissionCompletions: IPeriodicMissionCompletionDatabase[]; @@ -250,20 +260,20 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu Affiliations: IAffiliation[]; QualifyingInvasions: any[]; FactionScores: number[]; - ArchwingEnabled: boolean; + ArchwingEnabled?: boolean; PendingSpectreLoadouts?: ISpectreLoadout[]; SpectreLoadouts?: ISpectreLoadout[]; EmailItems: ITypeCount[]; CompletedSyndicates: string[]; FocusXP?: IFocusXP; Wishlist: string[]; - Alignment: IAlignment; + Alignment?: IAlignment; CompletedSorties: string[]; LastSortieReward: ILastSortieReward[]; Drones: IDroneClient[]; StepSequencers: IStepSequencer[]; ActiveAvatarImageType: string; - ShipDecorations: IConsumable[]; + ShipDecorations: ITypeCount[]; DiscoveredMarkers: IDiscoveredMarker[]; CompletedJobs: ICompletedJob[]; FocusAbility?: string; @@ -271,7 +281,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu HasContributedToDojo?: boolean; HWIDProtectEnabled?: boolean; KubrowPetPrints: IKubrowPetPrint[]; - AlignmentReplay: IAlignment; + AlignmentReplay?: IAlignment; PersonalGoalProgress: IPersonalGoalProgress[]; ThemeStyle: string; ThemeBackground: string; @@ -279,35 +289,34 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu BountyScore: number; ChallengeInstanceStates: IChallengeInstanceState[]; LoginMilestoneRewards: string[]; - RecentVendorPurchases: Array; + RecentVendorPurchases?: IRecentVendorPurchaseClient[]; NodeIntrosCompleted: string[]; GuildId?: IOid; CompletedJobChains: ICompletedJobChain[]; SeasonChallengeHistory: ISeasonChallenge[]; EquippedInstrument?: string; InvasionChainProgress: IInvasionChainProgress[]; - NemesisHistory: INemesisHistory[]; + Nemesis?: INemesisClient; + NemesisHistory: INemesisBaseClient[]; LastNemesisAllySpawnTime?: IMongoDate; - Settings: ISettings; + Settings?: ISettings; PersonalTechProjects: IPersonalTechProject[]; PlayerSkills: IPlayerSkills; - CrewShipAmmo: IConsumable[]; - CrewShipSalvagedWeaponSkins: IUpgradeClient[]; - CrewShipWeapons: ICrewShipWeapon[]; - CrewShipSalvagedWeapons: ICrewShipWeapon[]; + CrewShipAmmo: ITypeCount[]; CrewShipWeaponSkins: IUpgradeClient[]; + CrewShipSalvagedWeaponSkins: IUpgradeClient[]; TradeBannedUntil?: IMongoDate; PlayedParkourTutorial: boolean; SubscribedToEmailsPersonalized: number; InfestedFoundry?: IInfestedFoundryClient; BlessingCooldown?: IMongoDate; - CrewShipRawSalvage: IConsumable[]; + CrewShipRawSalvage: ITypeCount[]; CrewMembers: ICrewMember[]; LotusCustomization: ILotusCustomization; UseAdultOperatorLoadout?: boolean; NemesisAbandonedRewards: string[]; LastInventorySync: IOid; - NextRefill: IMongoDate; // Next time argon crystals will have a decay tick + NextRefill?: IMongoDate; FoundToday?: IMiscItem[]; // for Argon Crystals CustomMarkers?: ICustomMarkers[]; ActiveLandscapeTraps: any[]; @@ -317,10 +326,11 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu Quests: any[]; Robotics: any[]; UsedDailyDeals: any[]; - LibraryPersonalTarget: string; + LibraryPersonalTarget?: string; LibraryPersonalProgress: ILibraryPersonalProgress[]; - CollectibleSeries: ICollectibleSery[]; - LibraryAvailableDailyTaskInfo: ILibraryAvailableDailyTaskInfo; + CollectibleSeries?: ICollectibleEntry[]; + LibraryAvailableDailyTaskInfo?: ILibraryDailyTaskInfo; + LibraryActiveDailyTaskInfo?: ILibraryDailyTaskInfo; HasResetAccount: boolean; PendingCoupon?: IPendingCouponClient; Harvestable: boolean; @@ -328,6 +338,20 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu EndlessXP?: IEndlessXpProgress[]; DialogueHistory?: IDialogueHistoryClient; CalendarProgress: ICalendarProgress; + SongChallenges?: ISongChallenge[]; + EntratiVaultCountLastPeriod?: number; + EntratiVaultCountResetDate?: IMongoDate; + EntratiLabConquestUnlocked?: number; + EntratiLabConquestHardModeStatus?: number; + EntratiLabConquestCacheScoreMission?: number; + EntratiLabConquestActiveFrameVariants?: string[]; + EchoesHexConquestUnlocked?: number; + EchoesHexConquestHardModeStatus?: number; + EchoesHexConquestCacheScoreMission?: number; + EchoesHexConquestActiveFrameVariants?: string[]; + EchoesHexConquestActiveStickers?: string[]; + BrandedSuits?: IOid[]; + LockedWeaponGroup?: ILockedWeaponGroupClient; } export interface IAffiliation { @@ -336,9 +360,19 @@ export interface IAffiliation { Title?: number; FreeFavorsEarned?: number[]; FreeFavorsUsed?: number[]; + WeeklyMissions?: IWeeklyMission[]; // Kahl Tag: string; } +export interface IWeeklyMission { + MissionIndex: number; + CompletedMission: boolean; + JobManifest: string; + Challenges: string[]; + ChallengesReset?: boolean; + WeekCount: number; +} + export interface IAlignment { Wisdom: number; Alignment: number; @@ -347,6 +381,7 @@ export interface IAlignment { export interface IBooster { ExpiryDate: number; ItemType: string; + UsesRemaining?: number; } export interface IChallengeInstanceState { @@ -361,13 +396,35 @@ export interface IParam { v: string; } +export interface IRecentVendorPurchaseClient { + VendorType: string; + PurchaseHistory: IVendorPurchaseHistoryEntryClient[]; +} + +export interface IVendorPurchaseHistoryEntryClient { + Expiry: IMongoDate; + NumPurchased: number; + ItemId: string; +} + +export interface IRecentVendorPurchaseDatabase { + VendorType: string; + PurchaseHistory: IVendorPurchaseHistoryEntryDatabase[]; +} + +export interface IVendorPurchaseHistoryEntryDatabase { + Expiry: Date; + NumPurchased: number; + ItemId: string; +} + export interface IChallengeProgress { Progress: number; Name: string; Completed?: string[]; } -export interface ICollectibleSery { +export interface ICollectibleEntry { CollectibleType: string; Count: number; Tracking: string; @@ -391,11 +448,6 @@ export interface ICompletedJob { StageCompletions: number[]; } -export interface IConsumable { - ItemCount: number; - ItemType: string; -} - export interface ICrewMember { ItemType: string; NemesisFingerprint: number; @@ -428,13 +480,17 @@ export enum InventorySlot { SUITS = "SuitBin", WEAPONS = "WeaponBin", SPACESUITS = "SpaceSuitBin", + SPACEWEAPONS = "SpaceWeaponBin", MECHSUITS = "MechBin", PVE_LOADOUTS = "PveBonusLoadoutBin", - SENTINELS = "SentinelBin" + SENTINELS = "SentinelBin", + AMPS = "OperatorAmpBin", + RJ_COMPONENT_AND_ARMAMENTS = "CrewShipSalvageBin", + CREWMEMBERS = "CrewMemberBin" } export interface ISlots { - Extra: number; // can be undefined, but not if used via mongoose + Extra?: number; Slots: number; } @@ -491,6 +547,7 @@ export interface IFlavourItem { export type IMiscItem = ITypeCount; +// inventory.CrewShips[0].Weapon export interface ICrewShipWeapon { PILOT: ICrewShipPilotWeapon; PORT_GUNS: ICrewShipPortGuns; @@ -522,6 +579,13 @@ export interface IDroneDatabase { CurrentHP: number; _id: Types.ObjectId; RepairStart?: Date; + + DeployTime?: Date; + System?: number; + DamageTime?: Date; + PendingDamage?: number; + ResourceType?: string; + ResourceCount?: number; } export interface ITypeXPItem { @@ -631,12 +695,12 @@ export enum KubrowPetPrintItemType { } export interface IKubrowPetDetailsDatabase { - Name: string; - IsPuppy: boolean; + Name?: string; + IsPuppy?: boolean; HasCollar: boolean; - PrintsRemaining: number; + PrintsRemaining?: number; Status: Status; - HatchDate: Date; + HatchDate?: Date; DominantTraits: ITraits; RecessiveTraits: ITraits; IsMale: boolean; @@ -658,10 +722,11 @@ export interface ILastSortieReward { Manifest: string; } -export interface ILibraryAvailableDailyTaskInfo { +export interface ILibraryDailyTaskInfo { EnemyTypes: string[]; EnemyLocTag: string; EnemyIcon: string; + Scans?: number; ScansRequired: number; RewardStoreItem: string; RewardQuantity: number; @@ -737,38 +802,46 @@ export interface IMission extends IMissionDatabase { RewardsCooldownTime?: IMongoDate; } -export interface INemesisHistory { - fp: number; - manifest: Manifest; +export interface INemesisBaseClient { + fp: bigint; + manifest: string; KillingSuit: string; killingDamageType: number; ShoulderHelmet: string; + WeaponIdx: number; AgentIdx: number; - BirthNode: BirthNode; + BirthNode: string; + Faction: string; Rank: number; k: boolean; + Traded: boolean; d: IMongoDate; - GuessHistory?: number[]; - currentGuess?: number; - Traded?: boolean; - PrevOwners?: number; - SecondInCommand?: boolean; - Faction?: string; - Weakened?: boolean; + PrevOwners: number; + SecondInCommand: boolean; + Weakened: boolean; } -export enum BirthNode { - SolNode181 = "SolNode181", - SolNode4 = "SolNode4", - SolNode70 = "SolNode70", - SolNode76 = "SolNode76" +export interface INemesisBaseDatabase extends Omit { + d: Date; } -export enum Manifest { - LotusTypesEnemiesCorpusLawyersLawyerManifest = "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifest", - LotusTypesGameNemesisKuvaLichKuvaLichManifest = "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifest", - LotusTypesGameNemesisKuvaLichKuvaLichManifestVersionThree = "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionThree", - LotusTypesGameNemesisKuvaLichKuvaLichManifestVersionTwo = "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionTwo" +export interface INemesisClient extends INemesisBaseClient { + InfNodes: IInfNode[]; + HenchmenKilled: number; + HintProgress: number; + Hints: number[]; + GuessHistory: number[]; + MissionCount: number; + LastEnc: number; +} + +export interface INemesisDatabase extends Omit { + d: Date; +} + +export interface IInfNode { + Node: string; + Influence: number; } export interface IPendingCouponDatabase { @@ -781,12 +854,21 @@ export interface IPendingCouponClient { Discount: number; } -export interface IPendingRecipe { +export interface IPendingRecipeDatabase { ItemType: string; CompletionDate: Date; ItemId: IOid; TargetItemId?: string; // likely related to liches TargetFingerprint?: string; // likely related to liches + LongGuns?: IEquipmentDatabase[]; + Pistols?: IEquipmentDatabase[]; + Melee?: IEquipmentDatabase[]; + SuitToUnbrand?: Types.ObjectId; +} + +export interface IPendingRecipeClient + extends Omit { + CompletionDate: IMongoDate; } export interface IPendingTrade { @@ -807,23 +889,14 @@ export interface IGetting { } export interface IRandomUpgrade { - UpgradeFingerprint: IUpgradeFingerprint; + UpgradeFingerprint: RivenFingerprint; ItemType: string; ItemId: IOid; } -export interface IUpgradeFingerprint { +export interface IInnateDamageFingerprint { compat: string; - lim: number; - lvlReq: number; - pol: ArtifactPolarity; - buffs: IBuff[]; - curses: IBuff[]; -} - -export interface IBuff { - Tag: string; - Value: number; + buffs: IFingerprintStat[]; } export enum GettingSlotOrderInfo { @@ -833,7 +906,7 @@ export enum GettingSlotOrderInfo { } export interface IGiving { - RawUpgrades: IConsumable[]; + RawUpgrades: ITypeCount[]; _SlotOrderInfo: GivingSlotOrderInfo[]; } @@ -866,7 +939,7 @@ export interface IPersonalTechProject { State: number; ReqCredits: number; ItemType: string; - ReqItems: IConsumable[]; + ReqItems: ITypeCount[]; CompletionDate?: IMongoDate; ItemId: IOid; ProductCategory?: string; @@ -875,7 +948,6 @@ export interface IPersonalTechProject { } export interface IPlayerSkills { - LPP_NONE: number; LPP_SPACE: number; LPS_PILOTING: number; LPS_GUNNERY: number; @@ -916,11 +988,12 @@ export interface ISentientSpawnChanceBoosters { } export interface ISettings { - FriendInvRestriction: string; - GiftMode: string; - GuildInvRestriction: string; + FriendInvRestriction: "GIFT_MODE_ALL" | "GIFT_MODE_FRIENDS" | "GIFT_MODE_NONE"; + GiftMode: "GIFT_MODE_ALL" | "GIFT_MODE_FRIENDS" | "GIFT_MODE_NONE"; + GuildInvRestriction: "GIFT_MODE_ALL" | "GIFT_MODE_FRIENDS" | "GIFT_MODE_NONE"; ShowFriendInvNotifications: boolean; TradingRulesConfirmed: boolean; + SubscribedToSurveys?: boolean; } export interface IShipInventory { @@ -961,6 +1034,7 @@ export interface ITaunt { export interface IWeaponSkinDatabase { ItemType: string; + IsNew?: boolean; _id: Types.ObjectId; } @@ -1073,3 +1147,26 @@ export interface ICalendarProgress { YearProgress: { Upgrades: unknown[] }; SeasonProgress: ISeasonProgress; } + +export interface ISongChallenge { + Song: string; + Difficulties: number[]; +} + +export interface ILockedWeaponGroupClient { + s: IOid; + p?: IOid; + l?: IOid; + m?: IOid; + sn?: IOid; +} + +export interface ILockedWeaponGroupDatabase { + s: Types.ObjectId; + p?: Types.ObjectId; + l?: Types.ObjectId; + m?: Types.ObjectId; + sn?: Types.ObjectId; +} + +export type TPartialStartingGear = Pick; diff --git a/src/types/leaderboardTypes.ts b/src/types/leaderboardTypes.ts new file mode 100644 index 00000000..ed14c94d --- /dev/null +++ b/src/types/leaderboardTypes.ts @@ -0,0 +1,18 @@ +import { Types } from "mongoose"; + +export interface ILeaderboardEntryDatabase { + leaderboard: string; + ownerId: Types.ObjectId; + displayName: string; + score: number; + guildId?: Types.ObjectId; + expiry: Date; + guildTier?: number; +} + +export interface ILeaderboardEntryClient { + _id: string; // owner id + s: number; // score + r: number; // rank + n: string; // displayName +} diff --git a/src/types/loginTypes.ts b/src/types/loginTypes.ts index 687d611e..8c443797 100644 --- a/src/types/loginTypes.ts +++ b/src/types/loginTypes.ts @@ -11,11 +11,16 @@ export interface IAccountAndLoginResponseCommons { Nonce: number; } -export interface IDatabaseAccount extends IAccountAndLoginResponseCommons { +export interface IDatabaseAccountRequiredFields extends IAccountAndLoginResponseCommons { email: string; password: string; - LastLoginDay?: number; +} + +export interface IDatabaseAccount extends IDatabaseAccountRequiredFields { + Dropped?: boolean; LatestEventMessageDate: Date; + LastLoginRewardDate: number; + LoginDays: number; } // Includes virtual ID @@ -32,6 +37,7 @@ export interface ILoginRequest { date: number; ClientType: string; PS: string; + kick?: boolean; } export interface ILoginResponse extends IAccountAndLoginResponseCommons { diff --git a/src/types/personalRoomsTypes.ts b/src/types/personalRoomsTypes.ts index f91a5b88..fb672955 100644 --- a/src/types/personalRoomsTypes.ts +++ b/src/types/personalRoomsTypes.ts @@ -7,7 +7,7 @@ import { ITailorShopDatabase, TBootLocation } from "@/src/types/shipTypes"; -import { Model, Types } from "mongoose"; +import { Document, Model, Types } from "mongoose"; export interface IOrbiter { Features: string[]; @@ -46,5 +46,17 @@ export type PersonalRoomsDocumentProps = { }; }; -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export type PersonalRoomsModelType = Model; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export type TPersonalRoomsDatabaseDocument = Document & + Omit< + IPersonalRoomsDatabase & { + _id: Types.ObjectId; + } & { + __v: number; + }, + keyof PersonalRoomsDocumentProps + > & + PersonalRoomsDocumentProps; diff --git a/src/types/purchaseTypes.ts b/src/types/purchaseTypes.ts index f1e864be..51459454 100644 --- a/src/types/purchaseTypes.ts +++ b/src/types/purchaseTypes.ts @@ -1,5 +1,13 @@ import { IEquipmentClient } from "./inventoryTypes/commonInventoryTypes"; -import { IDroneClient, IInfestedFoundryClient, TEquipmentKey } from "./inventoryTypes/inventoryTypes"; +import { + IDroneClient, + IInfestedFoundryClient, + IMiscItem, + INemesisClient, + ITypeCount, + IRecentVendorPurchaseClient, + TEquipmentKey +} from "./inventoryTypes/inventoryTypes"; export interface IPurchaseRequest { PurchaseParams: IPurchaseParams; @@ -8,7 +16,7 @@ export interface IPurchaseRequest { export interface IPurchaseParams { Source: number; - SourceId?: string; // for Source 7 & 18 + SourceId?: string; // for Source 1, 7 & 18 StoreItem: string; StorePage: string; SearchTerm: string; @@ -22,20 +30,37 @@ export interface IPurchaseParams { IsWeekly?: boolean; // for Source 7 } -export interface ICurrencyChanges { - RegularCredits?: number; - PremiumCredits?: number; - PremiumCreditsFree?: number; -} - export type IInventoryChanges = { [_ in SlotNames]?: IBinChanges; } & { [_ in TEquipmentKey]?: IEquipmentClient[]; -} & ICurrencyChanges & { - InfestedFoundry?: IInfestedFoundryClient; - Drones?: IDroneClient[]; - } & Record; +} & { + RegularCredits?: number; + PremiumCredits?: number; + PremiumCreditsFree?: number; + PrimeTokens?: number; + InfestedFoundry?: IInfestedFoundryClient; + Drones?: IDroneClient[]; + MiscItems?: IMiscItem[]; + EmailItems?: ITypeCount[]; + Nemesis?: Partial; + NewVendorPurchase?: IRecentVendorPurchaseClient; // >= 38.5.0 + RecentVendorPurchases?: IRecentVendorPurchaseClient; // < 38.5.0 +} & Record< + Exclude< + string, + | SlotNames + | TEquipmentKey + | "RegularCredits" + | "PremiumCredits" + | "PremiumCreditsFree" + | "InfestedFoundry" + | "Drones" + | "MiscItems" + | "EmailItems" + >, + number | object[] + >; export interface IAffiliationMods { Tag: string; @@ -51,8 +76,8 @@ export interface IPurchaseResponse { } export type IBinChanges = { - count: number; - platinum: number; + count?: number; + platinum?: number; Slots: number; Extra?: number; }; @@ -69,19 +94,22 @@ export type SlotPurchaseName = | "TwoCrewShipSalvageSlotItem" | "CrewMemberSlotItem"; -export type SlotNames = - | "SuitBin" - | "WeaponBin" - | "MechBin" - | "PveBonusLoadoutBin" - | "SentinelBin" - | "SpaceSuitBin" - | "SpaceWeaponBin" - | "OperatorAmpBin" - | "RandomModBin" - | "CrewShipSalvageBin" - | "CrewMemberBin"; +export const slotNames = [ + "SuitBin", + "WeaponBin", + "MechBin", + "PveBonusLoadoutBin", + "SentinelBin", + "SpaceSuitBin", + "SpaceWeaponBin", + "OperatorAmpBin", + "RandomModBin", + "CrewShipSalvageBin", + "CrewMemberBin" +] as const; + +export type SlotNames = (typeof slotNames)[number]; export type SlotPurchase = { - [P in SlotPurchaseName]: { name: SlotNames; slotsPerPurchase: number }; + [P in SlotPurchaseName]: { name: SlotNames; purchaseQuantity: number }; }; diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 0220240a..d9139ec7 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -14,14 +14,14 @@ import { ICustomMarkers, IPlayerSkills, IQuestKeyDatabase, - ILoreFragmentScan + ILoreFragmentScan, + IUpgradeClient, + ICollectibleEntry, + IDiscoveredMarker, + ILockedWeaponGroupClient, + ILoadOutPresets } from "./inventoryTypes/inventoryTypes"; - -export interface IThemeUpdateRequest { - Style?: string; - Background?: string; - Sounds?: string; -} +import { IGroup } from "./loginTypes"; export interface IAffiliationChange { Tag: string; @@ -29,12 +29,6 @@ export interface IAffiliationChange { Title: number; } -export interface IUpdateChallengeProgressRequest { - ChallengeProgress: IChallengeProgress[]; - SeasonChallengeHistory: ISeasonChallenge[]; - SeasonChallengeCompletions: ISeasonChallenge[]; -} - export type IMissionInventoryUpdateRequest = { MiscItems?: ITypeCount[]; Recipes?: ITypeCount[]; @@ -44,6 +38,8 @@ export type IMissionInventoryUpdateRequest = { CrewShipRawSalvage?: ITypeCount[]; CrewShipAmmo?: ITypeCount[]; BonusMiscItems?: ITypeCount[]; + EmailItems?: ITypeCount[]; + ShipDecorations?: ITypeCount[]; SyndicateId?: string; SortieId?: string; @@ -92,6 +88,37 @@ export type IMissionInventoryUpdateRequest = { IsFinalWave: boolean; Participants: IVoidTearParticipantInfo[]; }; + LibraryScans?: { + EnemyType: string; + Count: number; + CodexScanCount: number; + Standing: number; + }[]; + CollectibleScans?: ICollectibleEntry[]; + Upgrades?: IUpgradeClient[]; // riven challenge progress + StrippedItems?: { + DropTable: string; + DROP_MOD: number[]; + }[]; + DeathMarks?: string[]; + Nemesis?: number; + Boosters?: IBooster[]; + CapturedAnimals?: { + AnimalType: string; + CaptureRating: number; + NumTags: number; + NumExtraRewards: number; + Count: number; + }[]; + DiscoveredMarkers?: IDiscoveredMarker[]; + LockedWeaponGroup?: ILockedWeaponGroupClient; // sent when captured by zanuka + UnlockWeapons?: boolean; // sent when recovered weapons from zanuka capture + IncHarvester?: boolean; // sent when recovered weapons from zanuka capture + CurrentLoadOutIds?: { + LoadOuts?: ILoadOutPresets; // sent when recovered weapons from zanuka capture + }; + wagerTier?: number; // the index + creditsFee?: number; // the index } & { [K in TEquipmentKey]?: IEquipmentClient[]; }; @@ -106,6 +133,7 @@ export interface IRewardInfo { toxinOk?: boolean; lostTargetWave?: number; defenseTargetCount?: number; + NemesisAbandonedRewards?: string[]; EOM_AFK?: number; rewardQualifications?: string; // did a Survival for 5 minutes and this was "1" PurgatoryRewardQualifications?: string; @@ -115,13 +143,6 @@ export interface IRewardInfo { export type IMissionStatus = "GS_SUCCESS" | "GS_FAILURE" | "GS_DUMPED" | "GS_QUIT" | "GS_INTERRUPTED"; -export interface IInventorySlotsRequest { - Bin: "PveBonusLoadoutBin"; -} -export interface IUpdateGlyphRequest { - AvatarImageType: string; - AvatarImage: string; -} export interface IUpgradesRequest { ItemCategory: TEquipmentKey; ItemId: IOid; @@ -155,3 +176,9 @@ export interface IVoidTearParticipantInfo { RewardProjection: string; HardModeReward: ITypeCount; } + +export interface IKeyChainRequest { + KeyChain: string; + ChainStage: number; + Groups?: IGroup[]; +} diff --git a/src/types/saveLoadoutTypes.ts b/src/types/saveLoadoutTypes.ts index e04ec6a2..b496a4ea 100644 --- a/src/types/saveLoadoutTypes.ts +++ b/src/types/saveLoadoutTypes.ts @@ -36,9 +36,10 @@ export interface ISaveLoadoutRequest { EquippedGear: string[]; EquippedEmotes: string[]; UseAdultOperatorLoadout: boolean; + WeaponSkins: IItemEntry; } -export interface ISaveLoadoutRequestNoUpgradeVer extends Omit {} +export type ISaveLoadoutRequestNoUpgradeVer = Omit; export interface IOperatorConfigEntry { [configId: string]: IOperatorConfigClient; @@ -48,11 +49,11 @@ export interface IItemEntry { [itemId: string]: IConfigEntry; } -export interface IConfigEntry { - [configId: string]: IItemConfig; -} +export type IConfigEntry = { + [configId in "0" | "1" | "2" | "3" | "4" | "5"]: IItemConfig; +} & { IsNew?: boolean }; -export interface ILoadoutClient extends Omit {} +export type ILoadoutClient = Omit; // keep in sync with ILoadOutPresets export interface ILoadoutDatabase { diff --git a/src/types/session.ts b/src/types/session.ts index 2e534f70..d7f6e68a 100644 --- a/src/types/session.ts +++ b/src/types/session.ts @@ -1,29 +1,29 @@ export interface ISession { sessionId: string; creatorId: string; - maxPlayers: number; - minPlayers: number; - privateSlots: number; - scoreLimit: number; - timeLimit: number; - gameModeId: number; - eloRating: number; - regionId: number; - difficulty: number; - hasStarted: boolean; - enableVoice: boolean; - matchType: string; - maps: string[]; - originalSessionId: string; - customSettings: string; - rewardSeed: number; - guildId: string; - buildId: number; - platform: number; - xplatform: boolean; - freePublic: number; - freePrivate: number; - fullReset: number; + maxPlayers?: number; + minPlayers?: number; + privateSlots?: number; + scoreLimit?: number; + timeLimit?: number; + gameModeId?: number; + eloRating?: number; + regionId?: number; + difficulty?: number; + hasStarted?: boolean; + enableVoice?: boolean; + matchType?: string; + maps?: string[]; + originalSessionId?: string; + customSettings?: string; + rewardSeed?: number; + guildId?: string; + buildId?: number; + platform?: number; + xplatform?: boolean; + freePublic?: number; + freePrivate?: number; + fullReset?: number; } export interface IFindSessionRequest { diff --git a/src/types/shipTypes.ts b/src/types/shipTypes.ts index 936a5cc6..23c46c48 100644 --- a/src/types/shipTypes.ts +++ b/src/types/shipTypes.ts @@ -127,7 +127,9 @@ export interface ISetPlacedDecoInfoRequest { DecoId: string; Room: string; PictureFrameInfo: IPictureFrameInfo; - BootLocation: string; + BootLocation?: string; + ComponentId?: string; + GuildId?: string; } export interface IPictureFrameInfo { diff --git a/src/types/statTypes.ts b/src/types/statTypes.ts index 0b49e20e..970d1be5 100644 --- a/src/types/statTypes.ts +++ b/src/types/statTypes.ts @@ -26,6 +26,19 @@ export interface IStatsClient { HealCount?: number; ReviveCount?: number; Races?: Map; + ZephyrScore?: number; + SentinelGameScore?: number; + CaliberChicksScore?: number; + OlliesCrashCourseScore?: number; + DojoObstacleScore?: number; + + // not in schema + PVP?: { + suitDeaths?: number; + suitKills?: number; + weaponKills?: number; + type: string; + }[]; } export interface IStatsDatabase extends IStatsClient { @@ -44,6 +57,7 @@ export interface IEnemy { kills?: number; assists?: number; deaths?: number; + captures?: number; } export interface IMission { @@ -126,6 +140,9 @@ export interface IStatsAdd { DIE_ITEM?: IUploadEntry; EXECUTE_ENEMY?: IUploadEntry; EXECUTE_ENEMY_ITEM?: IUploadEntry; + KILL_ASSIST?: IUploadEntry; + KILL_ASSIST_ITEM?: IUploadEntry; + CAPTURE_ENEMY?: IUploadEntry; } export interface IUploadEntry { @@ -136,6 +153,11 @@ export interface IStatsMax { WEAPON_XP?: IUploadEntry; MISSION_SCORE?: IUploadEntry; RACE_SCORE?: IUploadEntry; + ZephyrScore?: number; + SentinelGameScore?: number; + CaliberChicksScore?: number; + OlliesCrashCourseScore?: number; + DojoObstacleScore?: number; } export interface IStatsSet { diff --git a/src/types/vendorTypes.ts b/src/types/vendorTypes.ts new file mode 100644 index 00000000..f962a494 --- /dev/null +++ b/src/types/vendorTypes.ts @@ -0,0 +1,51 @@ +import { IMongoDate, IOid } from "./commonTypes"; + +interface IItemPrice { + ItemType: string | string[]; // If string[], preprocessing will use RNG to pick one for the current period. + ItemCount: number; + ProductCategory: string; +} + +interface IItemPricePreprocessed extends Omit { + ItemType: string; +} + +interface IItemManifest { + StoreItem: string; + ItemPrices?: IItemPrice[]; + Bin: string; + QuantityMultiplier: number; + Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing. + PurchaseQuantityLimit?: number; + RotatedWeekly?: boolean; + AllowMultipurchase: boolean; + LocTagRandSeed?: number | bigint; + Id: IOid; +} + +export interface IItemManifestPreprocessed extends Omit { + ItemPrices?: IItemPricePreprocessed[]; +} + +interface IVendorInfo { + _id: IOid; + TypeName: string; + ItemManifest: IItemManifest[]; + PropertyTextHash?: string; + RandomSeedType?: "VRST_WEAPON"; + RequiredGoalTag?: string; + WeaponUpgradeValueAttenuationExponent?: number; + Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing. +} + +interface IVendorInfoPreprocessed extends Omit { + ItemManifest: IItemManifestPreprocessed[]; +} + +export interface IRawVendorManifest { + VendorInfo: IVendorInfo; +} + +export interface IVendorManifestPreprocessed { + VendorInfo: IVendorInfoPreprocessed; +} diff --git a/src/utils/async-utils.ts b/src/utils/async-utils.ts new file mode 100644 index 00000000..b2d40c0d --- /dev/null +++ b/src/utils/async-utils.ts @@ -0,0 +1,7 @@ +export const parallelForeach = async (data: T[], op: (datum: T) => Promise): Promise => { + const promises: Promise[] = []; + for (const datum of data) { + promises.push(op(datum)); + } + await Promise.all(promises); +}; diff --git a/src/utils/logger.ts b/src/utils/logger.ts index f3873591..f02c0db4 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -104,9 +104,7 @@ export const logger = createLogger({ addColors(logLevels.colors); -export function registerLogFileCreationListener(): void { - errorLog.on("new", filename => logger.info(`Using error log file: ${filename}`)); - combinedLog.on("new", filename => logger.info(`Using combined log file: ${filename}`)); - errorLog.on("rotate", filename => logger.info(`Rotated error log file: ${filename}`)); - combinedLog.on("rotate", filename => logger.info(`Rotated combined log file: ${filename}`)); -} +errorLog.on("new", filename => logger.info(`Using error log file: ${filename}`)); +combinedLog.on("new", filename => logger.info(`Using combined log file: ${filename}`)); +errorLog.on("rotate", filename => logger.info(`Rotated error log file: ${filename}`)); +combinedLog.on("rotate", filename => logger.info(`Rotated combined log file: ${filename}`)); diff --git a/static/certs/cert.pem b/static/certs/cert.pem index dce14b62..4f043e8a 100644 --- a/static/certs/cert.pem +++ b/static/certs/cert.pem @@ -1,38 +1,38 @@ -----BEGIN CERTIFICATE----- -MIIGLjCCBRagAwIBAgIRAPeLmReXnv+ALT/3Tm2Vts4wDQYJKoZIhvcNAQELBQAw +MIIGLzCCBRegAwIBAgIRAILIyLcitteoEGcJt1QBXvcwDQYJKoZIhvcNAQELBQAw gY8xCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO BgNVBAcTB1NhbGZvcmQxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDE3MDUGA1UE AxMuU2VjdGlnbyBSU0EgRG9tYWluIFZhbGlkYXRpb24gU2VjdXJlIFNlcnZlciBD -QTAeFw0yNDA0MTUwMDAwMDBaFw0yNTA0MTUyMzU5NTlaMBcxFTATBgNVBAMMDCou -cDJwdGxzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKoxU6lW -K5iAXZfLrKOY5lcy7z+mML2cYZkW0XXJeC6jYDyYSGAPJogeIgd3JsJWjZvHxnj7 -8KJGjO5j8B8kz4CVcV6aEx4ExJvtFUSzkgXHhlvSo2p0TTtWxC+ib3vWv+5kBSzb -4mdKKHiaz9shcLNKB77305xSBnKjAPGElgaZRwjwMqUSbPyjx4KrehyPQZDOU0aR -TKUbQNDbKYbeEmmUku0FTpao35GNsJrwzKKFIgzWAGKY+QiywIMeOGf0dTqX60GQ -MeXkKbueibuFKA12foV8RGojdT+bPIdRQyyEyntUkbu+UMknJ9bsPbKTEyQgv5nY -62O+A2lYG89Ub7MCAwEAAaOCAvowggL2MB8GA1UdIwQYMBaAFI2MXsRUrYrhd+mb -+ZsF4bgBjWHhMB0GA1UdDgQWBBQgFEQlEKO9vXkpBU7pQjbMU8MZvTAOBgNVHQ8B +QTAeFw0yNDA4MDIwMDAwMDBaFw0yNTA4MDIyMzU5NTlaMBcxFTATBgNVBAMMDCou +dmlhdGxzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMTToSjY +3aUIxjghIkikJfFExVwSEzIM2XaQNJE+SxQ6Cc+xUR5QJrMJnM/39sH5c5imMEUo +2OnstCIaVMPx5ZPN+HXLsvmoVAe2/xYe7emnZ5ZFTUXPyqkzDRg0hkMJiWWo/Nmf +ypZfUJoz6hVkXwsgNFPTVuo7aECQFlZslh2HQVDOfBaNBxQBaOJ5vf6nllf/aLyB +tZ74nlLynVYV9kYzISP4dUcxQ+D4HZgIxyOQfcN3EHUS1ZVaIp8hupOygF8zGQyJ +uzFozzg5I59U+hT1yQG3FlwTBnP+sA0+hW0LBTbWSISm0If1SgHlUEqxLlosjuTG +BG45h9o2bAz9po0CAwEAAaOCAvswggL3MB8GA1UdIwQYMBaAFI2MXsRUrYrhd+mb ++ZsF4bgBjWHhMB0GA1UdDgQWBBQ/OeA2gLbVIKIuIitYRqSRUWMP3TAOBgNVHQ8B Af8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB BQUHAwIwSQYDVR0gBEIwQDA0BgsrBgEEAbIxAQICBzAlMCMGCCsGAQUFBwIBFhdo dHRwczovL3NlY3RpZ28uY29tL0NQUzAIBgZngQwBAgEwgYQGCCsGAQUFBwEBBHgw djBPBggrBgEFBQcwAoZDaHR0cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUlNB RG9tYWluVmFsaWRhdGlvblNlY3VyZVNlcnZlckNBLmNydDAjBggrBgEFBQcwAYYX -aHR0cDovL29jc3Auc2VjdGlnby5jb20wIwYDVR0RBBwwGoIMKi5wMnB0bHMuY29t -ggpwMnB0bHMuY29tMIIBfgYKKwYBBAHWeQIEAgSCAW4EggFqAWgAdgDPEVbu1S58 -r/OHW9lpLpvpGnFnSrAX7KwB0lt3zsw7CAAAAY7jjWjnAAAEAwBHMEUCIQD/BajQ -AYjbiSmZZaTZ1j2miDHS4onTeIwMA5/jeAYzLgIgTAoSaQnX6Niyld5gmysgfkRC -zkiI/WwEJUxmI+R3Ll4AdwCi4wrkRe+9rZt+OO1HZ3dT14JbhJTXK14bLMS5UKRH -5wAAAY7jjWiVAAAEAwBIMEYCIQC1tH+VO0bRco4oSYvfsPaJDbLoJ2vfqSrCjtqu -nLavHwIhANuDbW4fRFA/myvN7mrLm3VLHI63RTl/gnzNqxodfB5oAHUATnWjJ1ya -EMM4W2zU3z9S6x3w4I4bjWnAsfpksWKaOd8AAAGO441ojgAABAMARjBEAiAzv6zf -dPxtnecz30Rb63+UiyvT2SdmdTTP+ap3r1rpCgIgX5z8mLnJJ3WL0LIB5NRC9qPn -/t324TkyWDHKgMPom2gwDQYJKoZIhvcNAQELBQADggEBAH7mgrQLmTkMs6/F/RoE -nsHQ9ddsDAA+Fs04alH8D8kuuXSsUWhaf0OYfBHLtOZ238qfigLxXZ6oGj9qNQ0I -hMP56sjEqd2IF2Vfi/qV3igLuJcICWnqqKIegCcS4fmy90NwYVtp2Z/7ovUa8aY/ -yKGoXTfmDQwuyaH88j14Ft95lmvOJ4VPheGmSotZOaIkp1os/wPIoQAmWoecj173 -jnLQ6O5/IZC4s/xKLKVt+vW+nmyR5U7VjUqAFN8eBHgdGWRcAiEaTRLBZMwWYP2D -XPFWmwT8vkvvK0WagFYOoITH9Zu13dHHzReIEyBhCDXWYyfib8i3K+acXidmi7Lu -fAw= +aHR0cDovL29jc3Auc2VjdGlnby5jb20wIwYDVR0RBBwwGoIMKi52aWF0bHMuY29t +ggp2aWF0bHMuY29tMIIBfwYKKwYBBAHWeQIEAgSCAW8EggFrAWkAdQDd3Mo0ldfh +FgXnlTL6x5/4PRxQ39sAOhQSdgosrLvIKgAAAZEVLi9VAAAEAwBGMEQCIGiZNOV7 +IvcHKU7nEaxFgWPpUu2CxyULg1ueJTYwTT12AiAJWQv3RrqCtOJC7JEdztILs3Bn +an9s0Bf93uOE4C/LiAB3AA3h8jAr0w3BQGISCepVLvxHdHyx1+kw7w5CHrR+Tqo0 +AAABkRUuLxAAAAQDAEgwRgIhAOhlC+IpJV3uAaDCRXi6RZ+V8++QaLaTEtqFp2UP +yWeSAiEA8qtGDk1RE1VGwQQcJCf3CBYU5YTlsZNi7F6hEONLuzMAdwAS8U40vVNy +TIQGGcOPP3oT+Oe1YoeInG0wBYTr5YYmOgAAAZEVLi7kAAAEAwBIMEYCIQDWCnSm +N+2/xxo8bl3pbpNBEBZZIwnYPQW0A+SJ0+dboQIhANjH2L0xV/+rPuPMzK40vk3J +1fWHLocLjpgaxGhsBAOzMA0GCSqGSIb3DQEBCwUAA4IBAQBcObVjc1zFdOER50ZF +mI+WyVF8t6nV6dm3zIDraLA4++zKUu9UKNJm9YPqLdPP7uTLHz6wuBNmyuWPdF0r +qAf4vsK3tcAds7kjK8injewEUCPG20mtNMUHyhlNEOJR2ySPPQ6Q+t+TtGAnimKa +Zr86quYgYaJYhoEEXcbB9fMoDQYlJDzgT2DXvfM4cyoden2tYZ3gQS6ftiXacBe0 +WzFWYZ8mIP2Kb+D9tCapB9MVUzu3XJVy3S2FLQEWcWIvjnpad73a0/35i/nro6/k +TSK+MKBEBaNZuHJ8ubCToo1BftnsS8HuEPTNe8W1hyc2YmT9f5YQP6HWB2rxjH42 +OTXh -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIGEzCCA/ugAwIBAgIQfVtRJrR2uhHbdBYLvFMNpzANBgkqhkiG9w0BAQwFADCB @@ -68,36 +68,4 @@ l6lFhd2zi+WJN44pDfwGF/Y4QA5C5BIG+3vzxhFoYt/jmPQT2BVPi7Fp2RBgvGQq LcmsJWTyXnW0OMGuf1pGg+pRyrbxmRE1a6Vqe8YAsOf4vmSyrcjC8azjUeqkk+B5 yOGBQMkKW+ESPMFgKuOXwIlCypTPRpgSabuY0MLTDXJLR27lk8QyKGOHQ+SwMj4K 00u/I5sUKUErmgQfky3xxzlIPK1aEn8= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIFgTCCBGmgAwIBAgIQOXJEOvkit1HX02wQ3TE1lTANBgkqhkiG9w0BAQwFADB7 -MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD -VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE -AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTE5MDMxMjAwMDAwMFoXDTI4 -MTIzMTIzNTk1OVowgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5 -MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBO -ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0 -aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgBJlFzYOw9sI -s9CsVw127c0n00ytUINh4qogTQktZAnczomfzD2p7PbPwdzx07HWezcoEStH2jnG -vDoZtF+mvX2do2NCtnbyqTsrkfjib9DsFiCQCT7i6HTJGLSR1GJk23+jBvGIGGqQ -Ijy8/hPwhxR79uQfjtTkUcYRZ0YIUcuGFFQ/vDP+fmyc/xadGL1RjjWmp2bIcmfb -IWax1Jt4A8BQOujM8Ny8nkz+rwWWNR9XWrf/zvk9tyy29lTdyOcSOk2uTIq3XJq0 -tyA9yn8iNK5+O2hmAUTnAU5GU5szYPeUvlM3kHND8zLDU+/bqv50TmnHa4xgk97E -xwzf4TKuzJM7UXiVZ4vuPVb+DNBpDxsP8yUmazNt925H+nND5X4OpWaxKXwyhGNV -icQNwZNUMBkTrNN9N6frXTpsNVzbQdcS2qlJC9/YgIoJk2KOtWbPJYjNhLixP6Q5 -D9kCnusSTJV882sFqV4Wg8y4Z+LoE53MW4LTTLPtW//e5XOsIzstAL81VXQJSdhJ -WBp/kjbmUZIO8yZ9HE0XvMnsQybQv0FfQKlERPSZ51eHnlAfV1SoPv10Yy+xUGUJ -5lhCLkMaTLTwJUdZ+gQek9QmRkpQgbLevni3/GcV4clXhB4PY9bpYrrWX1Uu6lzG -KAgEJTm4Diup8kyXHAc/DVL17e8vgg8CAwEAAaOB8jCB7zAfBgNVHSMEGDAWgBSg -EQojPpbxB+zirynvgqV/0DCktDAdBgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rID -ZsswDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAG -BgRVHSAAMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29t -L0FBQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggr -BgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUA -A4IBAQAYh1HcdCE9nIrgJ7cz0C7M7PDmy14R3iJvm3WOnnL+5Nb+qh+cli3vA0p+ -rvSNb3I8QzvAP+u431yqqcau8vzY7qN7Q/aGNnwU4M309z/+3ri0ivCRlv79Q2R+ -/czSAaF9ffgZGclCKxO/WIu6pKJmBHaIkU4MiRTOok3JMrO66BQavHHxW/BBC5gA -CiIDEOUMsfnNkjcZ7Tvx5Dq2+UUTJnWvu6rvP3t3O9LEApE9GQDTF1w52z97GA1F -zZOFli9d31kWTz9RvdVFGD/tSo7oBmF0Ixa1DVBzJ0RHfxBdiSprhTEUxOipakyA -vGp4z7h/jnZymQyd/teRCBaho1+V ------END CERTIFICATE----- +-----END CERTIFICATE----- \ No newline at end of file diff --git a/static/certs/key.pem b/static/certs/key.pem index 892d7dfa..42a099a8 100644 --- a/static/certs/key.pem +++ b/static/certs/key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCqMVOpViuYgF2X -y6yjmOZXMu8/pjC9nGGZFtF1yXguo2A8mEhgDyaIHiIHdybCVo2bx8Z4+/CiRozu -Y/AfJM+AlXFemhMeBMSb7RVEs5IFx4Zb0qNqdE07VsQvom971r/uZAUs2+JnSih4 -ms/bIXCzSge+99OcUgZyowDxhJYGmUcI8DKlEmz8o8eCq3ocj0GQzlNGkUylG0DQ -2ymG3hJplJLtBU6WqN+RjbCa8MyihSIM1gBimPkIssCDHjhn9HU6l+tBkDHl5Cm7 -nom7hSgNdn6FfERqI3U/mzyHUUMshMp7VJG7vlDJJyfW7D2ykxMkIL+Z2OtjvgNp -WBvPVG+zAgMBAAECggEAAzoWM2Xxdt3DaIcxfPr/YXRGYJ2R22myPzw7uN3ODCXu -EDGoknGwsfBoUsRQLtHqgD0K2h/+XjiAn/bmUzpxpY18oP+PRAikT0e9suTFhjVU -EQk7lSwi8fB7BDAydVWk1ywV6qJsqeqx1vLDsb++xEqvpOl/NwqMs4widQtytymu -4n7/5OJik0wMNwSoBApOdRgX4EeGmbPjZj+U8zu1h+xVGDLSAd9stYsZ7jktAZVc -NIiBmNk+d0Laywq+XdD+t3PrbT/IbvqOlq/tAvMI7mAs3t/g6xYWABR6YzkMa0FV -xywzICEgum/ssilWWgnxlAdmhONC/5UNRtg1QflsaQKBgQDkOVN3uTEFuLXnsvyp -IKSxRXnIOc+1RHJiVAZhMGD3Kjr8tuAfTwHFng6CFV6vwAAhli1zU8UJw7U/9rph -aIzNk02RMAPMWQYk1nfUlQkzniG0ydhzI48yEvULSC6t+KKBaQYvmNu6a6pSh+aj -R08r9EzVNRXI9pV22mC+g5C7zQKBgQC+5/JFg55FFyLBzR0SMKHRj6gR1WC0Vovh -tu69yVpg/8JdXUPr7vmtgk617vLP9yttQ4rmBsjeUCG1jtWFDSI9dgtVqolfK+qX -0bh3fmdgolxmta0B51CWdF57zhBnPSoOSuI+d+C4p3AS5Ay1SfPsOCfGu+mZ6KLf -Ee+jYzFZfwKBgQCM7nGCnxOMqvF5sOehMQ1CgtqfMEP5ddkEq0p9PbjDKIrgf7WK -3+kCNYZUAgpEkVYDZ4+Nhg9I5lfItf2GJV+9mtbtby8JQ3gty1qYJahW/bFmyLYm -87B7hYVYgCyDNeRz8Xzma4hUaCP3bwCXl3NmeyfvCSb4wHyvtk7Dls8LiQKBgFZr -IxXqreOyxG4cjtNkJmx57mgcQomAQBQuPka1dm9Ad9jR1mRgKrArs7vR7iLMTeFJ -WQAmBBn3Bjts7CUtu9k8rYbbCxKFC84sBqg5FUz+UnvANBAPiUCCbx72OiCx5G7R -4TbMB3MvgKFckJAkaQH+rard97JPSCNYuDUrOvS7AoGAPRqzqsY1NuSX4NET/5kX -WNpI0C1Y02SodiZEOJiSd1lZdOs+RzKJv0yGZ4bTGzF5g0pPQzRVh7X/RkqvOooi -AdlKGykSXMNzrdgShNxr/RjC+n9+a4pfZWnW8eMbCJWW0ptjycNRbU/rLwmLSuV8 -SOEKVYljbu9o5nFbg1zU0Ck= +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDE06Eo2N2lCMY4 +ISJIpCXxRMVcEhMyDNl2kDSRPksUOgnPsVEeUCazCZzP9/bB+XOYpjBFKNjp7LQi +GlTD8eWTzfh1y7L5qFQHtv8WHu3pp2eWRU1Fz8qpMw0YNIZDCYllqPzZn8qWX1Ca +M+oVZF8LIDRT01bqO2hAkBZWbJYdh0FQznwWjQcUAWjieb3+p5ZX/2i8gbWe+J5S +8p1WFfZGMyEj+HVHMUPg+B2YCMcjkH3DdxB1EtWVWiKfIbqTsoBfMxkMibsxaM84 +OSOfVPoU9ckBtxZcEwZz/rANPoVtCwU21kiEptCH9UoB5VBKsS5aLI7kxgRuOYfa +NmwM/aaNAgMBAAECggEAEYK8bzxf96tAq0SzXqAP6heSsV7AS28eN7CbpKJUnp+N +OOePDnHWB46e31exoc82DAoY+EYqiiEvY2tRSD9wi8ZCyQQOz6w8kZUju42T3/ov +Ooy+06upXYU3sIQXv8YM7bjridbv+JHRQ27D8BRGamB6l0yRinQvkbLf8d9mOYkj +P5yYrpMPV/mfgkCir/aBlGOzmI+CuOv7FdF9DIz2OehtPXOzbExuab4xOQ4NQrN9 +TfzWWS798D86e5uDx+Ab0pfege8IJvEBjU5ngZo3aeS/F5i2us+BXImu1P6IrYdb +ekXUo9VJPEHiD02iyLW/gFz3/AsWa3ztinXN0I069wKBgQD7yGPX6LG7MXlXEqL2 +DuCrFsKzVC4z0c/cpPXO8Xb8rGzvHR7Oa0i5Bo7i5wRiVXS87HT25iZmB78yjKnI +bVmWA3tVWInx6fixbZUO7L4D/Q1Ddfin/DiXyNpAhKii0QgpD61P7HJnrfnwUar5 +Vpwd2grnPNCbuILZxAZhtIXRnwKBgQDIH5hmyiIUAvrz+4UpE55ecxTMOkj0+Pgx +79KpSjXfEIk5V7UmCSk1SusQWq8Ri9d6QqPcTptVhxmC/geolp9bCW14JdORbjNv +5+3JfAwgZJtbDP4l3GKf168fLQXzSpWCW3vT1lCBz4x4nNs2EudTdDCn5aUVLGEJ +v15Iz0dQUwKBgHuZh8n55SXrx5FDCNSZwRi796Bo9rVhjhTWtgR87NhlHKTVOsZC +TFToL0Sb+776DHCh81kw6jC0JNv/yWkmpQ/LbcQbzrv/C6KuFLpa5Xy3wMcZJpPw +cSex5dI+TTqAOu1NUNsnS5IyCbw7mx8DsWfGHgweApovHa0hWbClGfwpAoGAfSt9 +6DTfkcK3cilMhX+2236BcKe4ADlFC/7jtW0sOsQeAFbCf/LU6ndchVMjEwdzlA3g +bahg8eLZaxw2cBUdwRQpey+1n83csE7RZOeIsi4bGZ0LzWSF71I5P3eqtBxfXTSZ +Q8tVeYv2YW5CkhTKyWDwGePCGHc0jqM6drHm+e8CgYEA+IkvplnK76zx3M579TpI +M2ffiC2b12FtJMpgnNrk6HZ19o2mhbte/7wf9SHVqsRw4V7/n6rzBJ5wzEHOWre7 +7PrkLgS0mY2SRfMmeT74m59dA8GpvhBUW/Xa4wlDtZkU2iIcDkKnYLjgrTKjlK86 +ER+evzmHeTqYJzuK8Mfq93I= -----END PRIVATE KEY----- diff --git a/static/fixed_responses/allScans.json b/static/fixed_responses/allScans.json index 2263ad95..716c1a3b 100644 --- a/static/fixed_responses/allScans.json +++ b/static/fixed_responses/allScans.json @@ -35,6 +35,7 @@ "/Lotus/Types/Items/Plants/WildGingerBPlant", "/Lotus/Objects/Guild/Props/Computers/PanelADeco", "/Lotus/Types/PickUps/LootContainers/CorpusLootCrateCommon", + "/Lotus/Objects/Gameplay/InfestedHiveMode/InfestedTumorObjectiveDeco", "/Lotus/Objects/Gameplay/InfestedHiveMode/InfestedTumorObjectiveSpawnedDeco", "/Lotus/Types/Friendly/Agents/HiveMode/InfestedHiveAvatarF", "/Lotus/Types/Friendly/Pets/DecoyCatbrowPetAvatar", @@ -1088,5 +1089,8 @@ "/Lotus/Weapons/Infested/Melee/InfBoomerang/InfBoomerangSpawnAvatar", "/Lotus/Types/Game/CrewShip/GrineerDestroyer/DeepSpace/GrineerDSDestroyerAvatar", "/Lotus/Types/Game/CrewShip/GrineerDestroyer/Saturn/GrineerSaturnDestroyerAvatar", - "/Lotus/Types/Game/CrewShip/GrineerDestroyer/GrineerDestroyerAvatar" + "/Lotus/Types/Game/CrewShip/GrineerDestroyer/GrineerDestroyerAvatar", + "/Lotus/Types/LevelObjects/Zariman/ZarLootCrateUltraRare", + "/Lotus/Objects/DomestikDrone/GrineerOceanDomestikDroneMover", + "/Lotus/Types/Gameplay/1999Wf/Extermination/SupplyCrate" ] diff --git a/static/fixed_responses/conservationAnimals.json b/static/fixed_responses/conservationAnimals.json new file mode 100644 index 00000000..81dc1c0e --- /dev/null +++ b/static/fixed_responses/conservationAnimals.json @@ -0,0 +1,491 @@ +{ + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/CommonBirdOfPreyAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagCondrocCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/CommonFemaleBirdOfPreyAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagCondrocCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/CommonMaleBirdOfPreyAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagCondrocCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/RareBirdOfPreyAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagCondrocRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/RareFemaleBirdOfPreyAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagCondrocRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/RareMaleBirdOfPreyAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagCondrocRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/UncommonBirdOfPreyAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagCondrocUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/UncommonFemaleBirdOfPreyAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagCondrocUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/UncommonMaleBirdOfPreyAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagCondrocUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedCritter/BaseInfestedCritterAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem", + "extraReward": "/Lotus/Types/Items/Conservation/WoundedAnimalRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedCritter/CommonInfestedCritterAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedCritterCommon", + "extraReward": "/Lotus/Types/Items/Deimos/WoundedInfestedCritterCommonRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedCritter/RareInfestedCritterAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedCritterRare", + "extraReward": "/Lotus/Types/Items/Deimos/WoundedInfestedCritterRareRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedCritter/UncommonInfestedCritterAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedCritterUncommon", + "extraReward": "/Lotus/Types/Items/Deimos/WoundedInfestedCritterUncommonRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedKDrive/GrottoInfKDriveAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedKdriveUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedKDrive/HighlandInfKDriveAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedKdriveRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedKDrive/SwampInfKDriveAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedKdriveCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMaggot/CommonInfestedMaggotAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedMaggotCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMaggot/RareInfestedMaggotAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedMaggotRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMaggot/UncommonInfestedMaggotAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedMaggotUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMergoo/CommonInfestedMergooAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedMergooCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMergoo/RareInfestedMergooAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedMergooRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMergoo/UncommonInfestedMergooAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedMergooUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedNexifera/BaseInfestedNexiferaAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedNexiferaCommon", + "extraReward": "/Lotus/Types/Items/Conservation/WoundedAnimalRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedNexifera/CommonInfestedNexiferaAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedNexiferaCommon", + "extraReward": "/Lotus/Types/Items/Conservation/WoundedAnimalRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedNexifera/RareInfestedNexiferaAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedNexiferaRare", + "extraReward": "/Lotus/Types/Items/Conservation/WoundedAnimalRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedNexifera/UncommonInfestedNexiferaAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedNexiferaUncommon", + "extraReward": "/Lotus/Types/Items/Conservation/WoundedAnimalRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedPredator/BaseInfestedPredatorAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem", + "extraReward": "/Lotus/Types/Items/Conservation/WoundedAnimalRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedPredator/CommonInfestedPredatorAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedPredatorCommon", + "extraReward": "/Lotus/Types/Items/Deimos/WoundedInfestedPredatorCommonRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedPredator/RareInfestedPredatorAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedPredatorRare", + "extraReward": "/Lotus/Types/Items/Deimos/WoundedInfestedPredatorRareRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedPredator/UncommonInfestedPredatorAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedPredatorUncommon", + "extraReward": "/Lotus/Types/Items/Deimos/WoundedInfestedPredatorUncommonRewardItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedUndazoa/BaseUndazoaAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedZongroCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedUndazoa/CommonUndazoaAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedZongroCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedUndazoa/RareUndazoaAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedZongroRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedUndazoa/UncommonUndazoaAvatar": { + "tag": "/Lotus/Types/Items/Deimos/AnimalTagInfestedZongroUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Duviri/Rabbit/BaseDuviriRabbitAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Duviri/Rabbit/TeshinRabbitAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Duviri/Rabbit/TeshinRabbitOnHandAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Duviri/Wolf/DuviriWolfAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/Duviri/Wolf/DuviriWolfConservationAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/CommonFemaleForestRodentAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagKuakaCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/CommonForestRodentAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagKuakaCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/CommonMaleForestRodentAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagKuakaCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/RareFemaleForestRodentAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagKuakaRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/RareForestRodentAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagKuakaRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/RareMaleForestRodentAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagKuakaRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/TutorialForestRodentAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagKuakaCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/UncommonFemaleForestRodentAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagKuakaUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/UncommonForestRodentAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagKuakaUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/UncommonMaleForestRodentAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagKuakaUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/BaseLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/CommonFemaleLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/CommonLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/CommonMaleLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/CommonPupLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/RareFemaleLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/RareLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/RareMaleLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/RarePupLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/UncommonFemaleLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/UncommonLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/UncommonMaleLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/UncommonPupLegendaryKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagKubrodonUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/BaseOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/CommonFemaleOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/CommonMaleOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/CommonOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/CommonPupOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/RareFemaleOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/RareMaleOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/RareOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/RarePupOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/UncommonFemaleOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/UncommonMaleOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/UncommonOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/UncommonPupOrokinKubrowAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagStoverUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/CommonFemaleOstronSeaBirdAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagMergooCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/CommonMaleOstronSeaBirdAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagMergooCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/CommonOstronSeaBirdAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagMergooCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/RareFemaleOstronSeaBirdAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagMergooRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/RareMaleOstronSeaBirdAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagMergooRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/RareOstronSeaBirdAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagMergooRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/UncommonFemaleOstronSeaBirdAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagMergooUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/UncommonMaleOstronSeaBirdAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagMergooUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/UncommonOstronSeaBirdAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagMergooUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/BaseSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/CommonFemaleSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/CommonMaleSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/CommonPupSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/CommonSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/RareFemaleSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/RareMaleSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/RarePupSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/RareSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/UncommonFemaleSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/UncommonMaleSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/UncommonPupSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/UncommonSnowArmadilloAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagBolarolaUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/BaseSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/CommonFemaleSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/CommonMaleSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/CommonPupSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/CommonSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/RareFemaleSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/RareMaleSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/RarePupSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/RareSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/UncommonFemaleSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/UncommonMaleSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/UncommonPupSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/UncommonSnowBirdAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagSawgawUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/BaseSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/CommonFemaleSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/CommonMaleSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/CommonPupSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/CommonSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/RareFemaleSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/RareMaleSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/RarePupSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/RareSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/UncommonFemaleSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/UncommonMaleSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/UncommonPupSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/UncommonSnowCritterAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagVirminkUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/BaseSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/CommonFemaleSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/CommonMaleSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/CommonPupSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/CommonSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/RareFemaleSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/RareMaleSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/RarePupSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/RareSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/UncommonFemaleSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/UncommonMaleSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/UncommonPupSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/UncommonSnowPredatorAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagHorrasqueUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/BaseSnowRodentAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/CommonFemaleSnowRodentAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagPobbersCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/CommonMaleSnowRodentAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagPobbersCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/CommonSnowRodentAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagPobbersCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/RareFemaleSnowRodentAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagPobbersRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/RareMaleSnowRodentAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagPobbersRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/RareSnowRodentAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagPobbersRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/UncommonFemaleSnowRodentAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagPobbersUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/UncommonMaleSnowRodentAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagPobbersUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/UncommonSnowRodentAvatar": { + "tag": "/Lotus/Types/Items/Solaris/AnimalTagPobbersUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/BaseVampireKavatAvatar": { + "tag": "/Lotus/Types/Items/Conservation/ConservationTagItem" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/CommonVampireKavatAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/CommonVampireKavatCubAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/CommonVampireKavatFemaleAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/CommonVampireKavatMaleAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatCommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/RareVampireKavatAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/RareVampireKavatCubAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/RareVampireKavatFemaleAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/RareVampireKavatMaleAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatRare" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/UncommonVampireKavatAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/UncommonVampireKavatCubAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/UncommonVampireKavatFemaleAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatUncommon" + }, + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/UncommonVampireKavatMaleAvatar": { + "tag": "/Lotus/Types/Items/Eidolon/AnimalTagVampireKavatUncommon" + } +} diff --git a/static/fixed_responses/eventMessages.json b/static/fixed_responses/eventMessages.json index 6ecf6d44..62cb477a 100644 --- a/static/fixed_responses/eventMessages.json +++ b/static/fixed_responses/eventMessages.json @@ -4,7 +4,7 @@ "sub": "Welcome to Space Ninja Server", "sndr": "/Lotus/Language/Bosses/Ordis", "msg": "Enjoy your Space Ninja Experience", - "icon": "/Lotus/Interface/Icons/Npcs/Darvo.png", + "icon": "/Lotus/Interface/Icons/Npcs/Ordis.png", "eventMessageDate": "2025-01-30T13:00:00.000Z", "r": false } diff --git a/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json b/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json new file mode 100644 index 00000000..20e3e3a3 --- /dev/null +++ b/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json @@ -0,0 +1,101 @@ +{ + "VendorInfo": { + "_id": { "$oid": "61ba123467e5d37975aeeb03" }, + "TypeName": "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest", + "ItemManifest": [ + { + "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementMoon", + "ItemPrices": [ + { + "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"], + "ItemCount": 12, + "ProductCategory": "MiscItems" + } + ], + "RegularPrice": [1, 1], + "Bin": "BIN_4", + "QuantityMultiplier": 1, + "Expiry": { "$date": { "$numberLong": "604800000" } }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "LocTagRandSeed": 79554843, + "Id": { "$oid": "67bbb592e1534511d6c1c1e2" } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementMountain", + "ItemPrices": [ + { + "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"], + "ItemCount": 7, + "ProductCategory": "MiscItems" + } + ], + "RegularPrice": [1, 1], + "Bin": "BIN_3", + "QuantityMultiplier": 1, + "Expiry": { "$date": { "$numberLong": "604800000" } }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "LocTagRandSeed": 2413820225, + "Id": { "$oid": "67bbb592e1534511d6c1c1e3" } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementStorm", + "ItemPrices": [ + { + "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"], + "ItemCount": 3, + "ProductCategory": "MiscItems" + } + ], + "RegularPrice": [1, 1], + "Bin": "BIN_2", + "QuantityMultiplier": 1, + "Expiry": { "$date": { "$numberLong": "604800000" } }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "LocTagRandSeed": 3262300883, + "Id": { "$oid": "67bbb592e1534511d6c1c1e4" } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementShadow", + "ItemPrices": [ + { + "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"], + "ItemCount": 20, + "ProductCategory": "MiscItems" + } + ], + "RegularPrice": [1, 1], + "Bin": "BIN_1", + "QuantityMultiplier": 1, + "Expiry": { "$date": { "$numberLong": "604800000" } }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "LocTagRandSeed": 2797325750, + "Id": { "$oid": "67bbb592e1534511d6c1c1e5" } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementGhost", + "ItemPrices": [ + { + "ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"], + "ItemCount": 10, + "ProductCategory": "MiscItems" + } + ], + "RegularPrice": [1, 1], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { "$date": { "$numberLong": "604800000" } }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "LocTagRandSeed": 554932310, + "Id": { "$oid": "67bbb592e1534511d6c1c1e6" } + } + ], + "PropertyTextHash": "255AFE2169BAE4130B4B20D7C55D14FA", + "RandomSeedType": "VRST_FLAVOUR_TEXT", + "Expiry": { "$date": { "$numberLong": "604800000" } } + } +} diff --git a/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json b/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json index 0dabeb95..bec20cc1 100644 --- a/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json +++ b/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json @@ -18,7 +18,7 @@ "QuantityMultiplier": 1, "Expiry": { "$date": { - "$numberLong": "9999999000000" + "$numberLong": "604800000" } }, "AllowMultipurchase": true, @@ -39,7 +39,7 @@ "QuantityMultiplier": 1, "Expiry": { "$date": { - "$numberLong": "9999999000000" + "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 1, @@ -61,7 +61,7 @@ "QuantityMultiplier": 1, "Expiry": { "$date": { - "$numberLong": "9999999000000" + "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 1, @@ -83,7 +83,7 @@ "QuantityMultiplier": 35000, "Expiry": { "$date": { - "$numberLong": "9999999000000" + "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 1, @@ -105,7 +105,7 @@ "QuantityMultiplier": 1, "Expiry": { "$date": { - "$numberLong": "9999999000000" + "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 1, @@ -118,7 +118,7 @@ "PropertyTextHash": "62B64A8065B7C0FA345895D4BC234621", "Expiry": { "$date": { - "$numberLong": "9999999000000" + "$numberLong": "604800000" } } } diff --git a/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json b/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json new file mode 100644 index 00000000..59afcd65 --- /dev/null +++ b/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json @@ -0,0 +1,188 @@ +{ + "VendorInfo": { + "_id": { + "$oid": "67dadc30e4b6e0e5979c8d6a" + }, + "TypeName": "/Lotus/Types/Game/VendorManifests/TheHex/Nova1999ConquestShopManifest", + "ItemManifest": [ + { + "StoreItem": "/Lotus/StoreItems/Types/BoosterPacks/1999StickersPackEchoesArchimedea", + "ItemPrices": [ + { + "ItemCount": 10, + "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67db32b983b2ad79a9c1c18c" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/BoosterPacks/1999StickersPackEchoesArchimedeaFree", + "ItemPrices": [ + { + "ItemCount": 1, + "ItemType": "/Lotus/Types/Items/MiscItems/1999FreeStickersPack", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67db32b983b2ad79a9c1c18d" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/BoosterPacks/1999StickersPackEchoesArchimedeaFixed", + "ItemPrices": [ + { + "ItemCount": 1, + "ItemType": "/Lotus/Types/Items/MiscItems/1999FixedStickersPack", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67db32b983b2ad79a9c1c18e" + } + }, + { + "StoreItem": "/Lotus/Types/StoreItems/Packages/SyndicateVosforPack", + "ItemPrices": [ + { + "ItemCount": 6, + "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67db32b983b2ad79a9c1c18f" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/StickerPictureFrame", + "ItemPrices": [ + { + "ItemCount": 10, + "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67db32b983b2ad79a9c1c190" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Upgrades/CosmeticEnhancers/Utility/AbilityRadiationProcsCreateUniversalOrbsOnKill", + "ItemPrices": [ + { + "ItemCount": 5, + "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_1", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "Id": { + "$oid": "67db32b983b2ad79a9c1c191" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Upgrades/CosmeticEnhancers/Offensive/AbilityHeatProcsGiveCritChance", + "ItemPrices": [ + { + "ItemCount": 5, + "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_1", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "Id": { + "$oid": "67db32b983b2ad79a9c1c192" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Upgrades/CosmeticEnhancers/Defensive/InvulnerabilityOnDeathOnMercyKill", + "ItemPrices": [ + { + "ItemCount": 5, + "ItemType": "/Lotus/Types/Items/MiscItems/1999ConquestBucks", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_1", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "PurchaseQuantityLimit": 1, + "AllowMultipurchase": false, + "Id": { + "$oid": "67db32b983b2ad79a9c1c193" + } + } + ], + "PropertyTextHash": "CB7D0E807FD5E2BCD059195201D963B9", + "RequiredGoalTag": "", + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + } + } +} diff --git a/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json b/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json index 4572855f..7934f0a3 100644 --- a/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json +++ b/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json @@ -561,7 +561,7 @@ "QuantityMultiplier": 1, "Expiry": { "$date": { - "$numberLong": "2051240400000" + "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 25, @@ -583,7 +583,7 @@ "QuantityMultiplier": 10000, "Expiry": { "$date": { - "$numberLong": "2051240400000" + "$numberLong": "604800000" } }, "PurchaseQuantityLimit": 25, @@ -596,7 +596,7 @@ "PropertyTextHash": "0A0F20AFA748FBEE490510DBF5A33A0D", "Expiry": { "$date": { - "$numberLong": "2051240400000" + "$numberLong": "604800000" } } } diff --git a/static/fixed_responses/glyphsCodes.json b/static/fixed_responses/glyphsCodes.json new file mode 100644 index 00000000..f1ad8a22 --- /dev/null +++ b/static/fixed_responses/glyphsCodes.json @@ -0,0 +1,259 @@ +{ + "1999-QUINCY": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImagePartyCDGlyph"], + "1999-VOICEPLAY": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageBigBytesPizzaGlyph"], + "6IXGATSU": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSixixgatsu"], + "ADMIRALBAHROO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageAdmiralBahroo"], + "AEONKNIGHT86": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageAeonKnight"], + "AGAYGUYPLAYS": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorAGGP"], + "AKARI": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageAkariayataka"], + "ALAINLOVEGLYPH": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageAlainLove"], + "ALEXANDERDARIO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageAlexanderDario"], + "AMPROV": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageGoku"], + "ANGRYUNICORN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageAngryUnicorn"], + "ANJETCAT": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageAnJetCat"], + "ANNOYINGKILLAH": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageAnnoyingKillah"], + "ARGONSIX": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageArgonSix"], + "ASHISOGITENNO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageAshisogiTenno"], + "ASURATENSHI": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTenshi"], + "AUNTIETAN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageFromThe70s"], + "AVELNA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageAvelna"], + "AZNITROUS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageAznitrous"], + "BIGJIMID": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBigJimID"], + "BLACKONI": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBlackOni"], + "BLAZINGCOBALT": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBlazingCobalt"], + "BLUEBERRYCAT": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBlueberryCat"], + "BRAZILCOMMUNITYDISCORD": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBRCommunityDiscord"], + "BRICKY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBricky"], + "BROTHERDAZ": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageOldDirtyDaz"], + "BROZIME": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBrozime"], + "BUFF00N": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBuff00n"], + "BURNBXX": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBurnBxx"], + "BWANA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBwana"], + "CALAMITYDEATH": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCalamityDeath"], + "CALEYEMERALD": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCaleyEmerald"], + "CANOFCRAIG": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCanOfCraig"], + "CARCHARA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCarchara"], + "CASARDIS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCasardis"], + "CEPHALONSQUARED": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCephalonSquared"], + "CGSKNACKIE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCGsKnackie"], + "CHACYTAY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageChacytay"], + "CHAR": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageChar"], + "CHELESTRA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageChelestra"], + "CLEONATURIN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCleoNaturin"], + "CODOMA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCodoma"], + "COHHCARNAGE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCohhCarnage"], + "COLDSCAR": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageColdScar"], + "COLDTIGER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageColdTiger"], + "CONCLAVEDISCORD": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageConclaveDiscord"], + "CONFUSEDWARFRAME": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageConfusedWarframe"], + "CONQUERA2024": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageConqueraGlyphVI", "/Lotus/Types/StoreItems/AvatarImages/AvatarImageConqueraGlyphVII"], + "COPYKAVAT": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCopyKavat"], + "CPT_KIMGLYPH": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCptKim"], + "CROWDI": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageCrowdi"], + "DAIDAIKIRI": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDaiDaiKiri"], + "DANIELTHEDEMON": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorDanieltheDemon"], + "DANILY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDanily"], + "DARIKAART": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDarikaArt"], + "DASTERCREATIONS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDasterCreations"], + "DATLOON": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDatLoon"], + "DAYJOBO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDayJoBo"], + "DEATHMAGGOT": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagedeathma666ot"], + "DEBBYSHEEN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDebbysheen"], + "DEEJAYKNIGHT": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDeejayKnight"], + "DEEPBLUEBEARD": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDeepBlueBeard"], + "DESTROHIDO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDestrohido"], + "DEUCETHEGAMER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDeuceTheGamer"], + "DILLYFRAME": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDillyFrame"], + "DIMITRIV2": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDimitriVTwo"], + "DISFUSIONAL": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDisfusional"], + "DJTECHLIVE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDJTechlive"], + "DKDIAMANTES": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorDKDiamantes"], + "DNEXUS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDNexus"], + "EDRICK": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageEdrick"], + "EDUIY16": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageEduiy"], + "ELDANKER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageD4NK3R"], + "ELGRINEEREXILIADO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageElGrineerExiliado"], + "ELICEGAMEPLAY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageEliceGameplay"], + "ELNORAELEO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageElNoraEleo"], + "EMOVJ": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageEmovj"], + "EMPYREANCAP": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageEmpyreanCap"], + "ENDOTTI_": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageEndotti"], + "ETERION": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageEterion"], + "EXTRACREDITS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageExtraCredits"], + "FACELESSBEANIE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageFacelessBeanie"], + "FASHIONFRAMEISENDGAME": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageFashionFrameIsEndgame"], + "FATED2PERISH": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageFated2Perish"], + "FEELLIKEAPLAYER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageFeelLikeAPlayer"], + "FERREUSDEMON": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageFerreusDemon"], + "FINLAENA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageFinlaena"], + "FLOOFYDWAGON": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageFloofyDwagon"], + "FR4G-TP": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageFR4GTP"], + "FROSTYNOVAPRIME": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageFrostyNovaPrime"], + "FROZENBAWZ": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageFrozenbawz"], + "GARA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageGara"], + "GERMANCOMMUNITYDISCORD": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageGermanCommunityDiscord"], + "GINGY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageGingy"], + "GLAMSHATTERSKULL": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageGlamShatterskull"], + "GRINDHARDSQUAD": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageGrindHardSquad"], + "H3DSH0T": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorH3dsh0t"], + "HAPPINESSDARK": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageHappinessDark"], + "HOKUPROPS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageHokuProps"], + "HOMIINVOCADO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageHomiInvocado"], + "HOTSHOMSTORIES": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageHotsHomStories"], + "HYDROXATE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageHydroxate"], + "IFLYNN": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorIflynn"], + "IKEDO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageIkedo"], + "IM7HECLOWN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageIm7heClown"], + "INEXPENSIVEGAMER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageInexpensiveGamer"], + "INFERNOTHEFIRELORD": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageInfernoTheFirelord"], + "INFODIVERSAO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageInfodiversao"], + "ITSJUSTTOE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageToxickToe"], + "IWOPLY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageIwoply"], + "JAMIEVOICEOVER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageJamieVoiceOver"], + "JESSITHROWER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageJessiThrower"], + "JOEYZERO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageJoeyZero"], + "JORIALE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageJoriale"], + "JUSTHAILEY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageJustHailey"], + "JUSTRLC": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRLCGaming"], + "K1LLERBARBIE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageKillerBarbie"], + "KAVATSSCHROEDINGER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageKavatsSchroedinger"], + "KENSHINWF": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageKenshinWF"], + "KINGGOTHALION": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageKingGothalion"], + "KIRARAHIME": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageKirarahime"], + "KIRDY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageKirdy"], + "KIWAD": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageKiwad"], + "KR1PTONPLAYER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageKr1ptonPlayer"], + "KRETDUY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageKretduy"], + "KYAII": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagekyaii"], + "L1FEWATER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageLifewater"], + "LADYNOVITA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageLadyNovita"], + "LADYTHELADDY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageLadyTheLaddy"], + "LEODOODLING": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageLeoDoodling"], + "LEYZARGAMINGVIEWS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageLeyzarViewGaming"], + "LIGHTMICKE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageLightmicke"], + "LIGHTNINGCOSPLAY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageLightningCosplay"], + "LILLEXI": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageLilLexi"], + "LUCIANPLAYSALLDAY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageLucianPlaysAllDay"], + "LYNXARIA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageLynxaria"], + "MACHO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageLokKingMacho"], + "MADFURY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageHypercaptai"], + "MAKARIMORPH": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMakarimorph"], + "MAOMIX": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMaomix"], + "MCGAMERCZ": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMCGamerCZ"], + "MCIK": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMCIK"], + "MCMONKEYS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMCMonkeys"], + "MECORE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMeCore"], + "MEDUSACAPTURES": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMedusaCaptures"], + "MHBLACKY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMHBlacky"], + "MICHELPOSTMA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTheNextLevel"], + "MIKETHEBARD": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTVSBOH"], + "MISSFWUFFY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMissFwuffy"], + "MISTERGAMER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTennoForever"], + "MJIKTHIZE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMjikThize"], + "MOGAMU": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorMogamu"], + "MOVEMBER2024": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageMovember"], + "MRROADBLOCK": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMrRoadBlock"], + "MRSTEELWAR": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMrSteelWar"], + "MRWARFRAMEGUY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageMrWarframeGuy"], + "N00BLSHOWTEK": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorN00blShowtek"], + "NELOSART": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageNelosart"], + "NOMNOM": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageNononom"], + "NOSYMPATHYY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageNoSympathyy"], + "NP161": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagenponesixtyone"], + "ODDIEOWL": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageOddieowl"], + "OOSIJ": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageOOSIJ"], + "ORIGINALWICKEDFUN": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorOriginalWickedfun"], + "ORPHEUSDELUXE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageOrpheusDeluxe"], + "OTTOFYRE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageOttofyre"], + "OZKU": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageOzku"], + "PAMMYJAMMY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePammyJammy"], + "PANDAAHH": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePandaahhhhh"], + "PAPATLION": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePapaTLion"], + "PHONGFU": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePhongFu"], + "PLAGUEDIRECTOR": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePlagueDirector"], + "PLEXICOSPLAY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePlexiCosplay"], + "POKKETNINJA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePokketNinja"], + "POSTITV": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePostiTV"], + "PRIDE2024": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImagePrideGlyph"], + "PRIMEDAVERAGE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePrimedAverage"], + "PROFESSORBROMAN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageProfessorBroman"], + "PURKINJE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePurkinje"], + "PURPLEFLURP": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePurpleFlurp"], + "PYRAH": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePyrah"], + "PYRRHICSERENITY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImagePyrrhicSerenity"], + "QUADLYSTOP": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageQuadlyStop"], + "R/WARFRAME": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageReddit"], + "RAGINGTERROR": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRagingTerror"], + "RAHETALIUS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRahetalius"], + "RAHNY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRahny"], + "RAINBOWWAFFLES": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRainbowWaffles"], + "RELENTLESSZEN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRelentlessZen"], + "RETROALCHEMIST": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRetroAlchemist"], + "REYGANSO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageReyGanso"], + "RIKENZ": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRIKENZ"], + "RIPPZ0R": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRippz0r"], + "RITENS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRitens"], + "ROYALPRAT": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRoyalPrat"], + "RUSTYFIN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageRustyFin"], + "SAPMATIC": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSapmatic"], + "SARAHTSANG": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSarahTsang"], + "SCALLION": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageScallion"], + "SCARLETMOON": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageScarletMoon"], + "SEARYN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSearyn"], + "SERDARSARI": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBBSChainWarden"], + "SHARLAZARD": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSharlazard"], + "SHENZHAO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageShenzhao"], + "SHERPA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSherpaRage"], + "SHUL": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageShulGaming"], + "SIEJOUMBRA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSiejoUmbra"], + "SILENTMASHIKO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSilentMashiko"], + "SILLFIX": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSillfix"], + "SILVERVALE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSilvervale"], + "SKILLUP": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSkillUp"], + "SMOODIE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSmoodie"], + "SN0WRC": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSn0wRC"], + "SPACEWAIFU": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSpaceWaifu"], + "SPANDY": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageSpandy"], + "STR8OPTICROYAL": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageStr8opticroyal"], + "STRIPPIN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageStrippin"], + "STUDIOCYEN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageStudioCyen"], + "TACTICALPOTATO": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorTacticalPotato"], + "TANCHAN": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorTanchan"], + "TBGKARU": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTBGKaru"], + "TEAWREX": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTeawrex"], + "THEGAMIO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTheGamio"], + "THEKENGINEER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageKengineer"], + "THEPANDA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageThePandaNEight"], + "TINBEARS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTinBears"], + "TIOMARIO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTioMario"], + "TIORAMON": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTioRamon"], + "TORTOISE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageWDTortoise"], + "TOTALN3WB": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageDayTotalN3wb"], + "TRASHFRAME": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTrashFrame"], + "TRIBUROS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTriburos"], + "TWILA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageTwila"], + "UNREALYUKI": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageUnrealYuki"], + "UREIFEN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageUreiFen"], + "VAMP6X6X6X": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageWarframeMadness"], + "VAMPPIRE": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageVamppire"], + "VARLINATOR": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageVarlinator"], + "VASHCOWAII": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageVashCowaii"], + "VASHKA": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageVashka"], + "VERNOC": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageVernoc"], + "VOIDFISSUREBR": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageVoidFissureBR"], + "VOLI": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageVoli"], + "VOLTTHEHERO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageVoltTheHero"], + "VVHITEANGEL": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorVVhiteAngel"], + "WALTERDV": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageWalterDV"], + "WANDERBOTS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageWanderbots"], + "WARFRAMEFLO": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageWarframeFlo"], + "WEALWEST": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageWealWest"], + "WIDESCREENJOHN": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageWidescreenJohn"], + "WOXLI": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageWoxli"], + "XBOCCHANVTX": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageBocchanVT"], + "XENOGELION": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageCreatorXenogelion"], + "XXVAMPIXX": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageXxVampixx"], + "YOURLUCKYCLOVER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageYourLuckyClover"], + "ZARIONIS": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageZarionis"], + "ZXPFER": ["/Lotus/Types/StoreItems/AvatarImages/FanChannel/AvatarImageZxpfer"] +} diff --git a/static/fixed_responses/junctionRewards.json b/static/fixed_responses/junctionRewards.json deleted file mode 100644 index e09c56a6..00000000 --- a/static/fixed_responses/junctionRewards.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "VenusToMercuryJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Keys/InfestedIntroQuest/InfestedIntroQuestKeyChain", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Keys/KubrowQuest/KubrowQuestKeyChain", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Rifle/BoltoRifle", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Warframe/AvatarShieldRechargeRateMod", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Warframe/AvatarAbilityEfficiencyMod", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Game/KubrowPet/EggHatcher", "ItemCount": 1 } - ], - "credits": 10000 - }, - "EarthToVenusJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/FurisBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/WeaponFreezeDamageMod", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/WeaponElectricityDamageMod", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Recipes/SentinelRecipes/TnSentinelCrossBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/MeleeTrees/StaffCmbOneMeleeTree", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Items/MiscItems/OrokinReactor", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Aura/PlayerEnergyHealthRegenAuraMod", "ItemCount": 1 } - ], - "credits": 5000 - }, - "EarthToMarsJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Keys/ArchwingQuest/ArchwingQuestKeyChain", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Game/KubrowPet/EggHatcher", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Items/ShipFeatureItems/VoidProjectionFeatureItem", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionRevenantPrimeABronze", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Hammer/HammerWeapon", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/MeleeTrees/IronPhoenixMeleeTree", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Keys/InfestedMicroplanetQuest/InfestedMicroplanetQuestKeyChain", "ItemCount": 1 } - ], - "credits": 15000 - }, - "MarsToCeresJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/GrnSniperRifleBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/WeaponToxinDamageMod", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/MeleeTrees/DualSwordCmbOneMeleeTree", "ItemCount": 1 } - ], - "credits": 20000 - }, - "MarsToPhobosJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Keys/SpyQuestKeyChain/SpyQuestKeyChain", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/GrnHeavyPistolBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/StoreItems/Consumables/CipherBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/WeaponReloadSpeedMod", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Warframe/AvatarLootRadarMod", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Items/MiscItems/OrokinCatalyst", "ItemCount": 1 } - ], - "credits": 20000 - }, - "JupiterToEuropaJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Keys/LimboQuest/LimboQuestKeyChain", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Keys/DragonQuest/DragonQuestKeyChain", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/CorpusMinigunBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Aura/PlayerHealthAuraMod", "ItemCount": 1 } - ], - "credits": 40000 - }, - "JupiterToSaturnJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/GrenadeLauncherBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Keys/ProteaQuest/ProteaQuestKeyChain", "ItemCount": 1 } - ], - "credits": 40000 - }, - "SaturnToUranusJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/CorpusWhipBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaHelmetBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Keys/DuviriQuest/DuviriQuestKeyChain", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/NeutralCreatures/ErsatzHorse/ErsatzHorsePowerSuit", "ItemCount": 1 } - ], - "credits": 60000 - }, - "UranusToNeptuneJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Keys/OrokinMoonQuest/OrokinMoonQuestKeyChain", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/ReconnasorBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaChassisBlueprint", "ItemCount": 1 } - ], - "credits": 80000 - }, - "NeptuneToPlutoJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/GrineerFlakCannonBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaSystemsBlueprint", "ItemCount": 1 } - ], - "credits": 80000 - }, - "PlutoToSednaJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Keys/WarWithinQuest/WarWithinQuestKeyChain", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Keys/MirageQuest/MirageQuestKeyChain", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/DualDaggerBlueprint", "ItemCount": 1 } - ], - "credits": 100000 - }, - "PlutoToErisJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Keys/InfestedAladVQuest/InfestedAladVQuestKeyChain", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/MireSwordBlueprint", "ItemCount": 1 } - ], - "credits": 100000 - }, - "CeresToJupiterJunction": { - "items": [ - { "ItemType": "/Lotus/StoreItems/Types/Recipes/Weapons/GrnStaffBlueprint", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Archwing/Suit/ArchwingSuitHealthMaxMod", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Archwing/Rifle/ArchwingRifleDamageAmountMod", "ItemCount": 1 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Archwing/Melee/ArchwingMeleeDamageMod", "ItemCount": 1 } - ], - "credits": 30000 - } -} diff --git a/static/fixed_responses/kuriaMessages/fiftyPercent.json b/static/fixed_responses/kuriaMessages/fiftyPercent.json new file mode 100644 index 00000000..8466a215 --- /dev/null +++ b/static/fixed_responses/kuriaMessages/fiftyPercent.json @@ -0,0 +1,7 @@ +{ + "sub": "/Lotus/Language/Oddities/SeriesOne50PercentInboxMessageSubject", + "sndr": "/Lotus/Language/Menu/ScribeName", + "msg": "/Lotus/Language/Oddities/SeriesOne50PercentInboxMessage", + "icon": "/Lotus/Interface/Icons/Syndicates/FactionOddityGold.png", + "att": ["/Lotus/Upgrades/Skins/Clan/OrokittyBadgeItem"] +} diff --git a/static/fixed_responses/kuriaMessages/oneHundredPercent.json b/static/fixed_responses/kuriaMessages/oneHundredPercent.json new file mode 100644 index 00000000..3e73e97a --- /dev/null +++ b/static/fixed_responses/kuriaMessages/oneHundredPercent.json @@ -0,0 +1,8 @@ +{ + "sub": "/Lotus/Language/Oddities/SeriesOneRewardSubject", + "sndr": "/Lotus/Language/Menu/ScribeName", + "msg": "/Lotus/Language/Oddities/SeriesOneRewardInboxMessage", + "icon": "/Lotus/Interface/Icons/Syndicates/FactionOddityGold.png", + "att": ["/Lotus/Types/Items/ShipDecos/OrokinFelisBobbleHead"], + "highPriority": true +} diff --git a/static/fixed_responses/kuriaMessages/seventyFivePercent.json b/static/fixed_responses/kuriaMessages/seventyFivePercent.json new file mode 100644 index 00000000..fe496790 --- /dev/null +++ b/static/fixed_responses/kuriaMessages/seventyFivePercent.json @@ -0,0 +1,7 @@ +{ + "sub": "/Lotus/Language/Oddities/SeriesOne75PercentInboxMessageSubject", + "sndr": "/Lotus/Language/Menu/ScribeName", + "msg": "/Lotus/Language/Oddities/SeriesOne75PercentInboxMessage", + "icon": "/Lotus/Interface/Icons/Syndicates/FactionOddityGold.png", + "att": ["/Lotus/Types/StoreItems/AvatarImages/AvatarImageOroKitty"] +} diff --git a/static/fixed_responses/libraryDailyTasks.json b/static/fixed_responses/libraryDailyTasks.json new file mode 100644 index 00000000..5e070893 --- /dev/null +++ b/static/fixed_responses/libraryDailyTasks.json @@ -0,0 +1,98 @@ +[ + [ + "/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/LaserCannonBipedAvatar", + "/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/RailgunBipedAvatar", + "/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/ShockwaveBipedAvatar" + ], + ["/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/LaserDiscBipedAvatar"], + ["/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/SuperMoaBipedAvatar"], + [ + "/Lotus/Types/Enemies/Corpus/Spaceman/EliteSpacemanAvatar", + "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/RifleSpacemanAvatar", + "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/DeployableSpacemanAvatar", + "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/MeleeSpacemanAvatar", + "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/ShotgunSpacemanAvatar", + "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/SniperSpacemanAvatar" + ], + ["/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/NullifySpacemanAvatar"], + ["/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/BeastMasterAvatar"], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/BladeSawmanAvatar", + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/BlowtorchSawmanAvatar", + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/PistonSawmanAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/BladeSawmanAvatar", + "/Lotus/Types/Enemies/Grineer/Forest/Avatars/BladeSawmanAvatar", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/BladeSawmanAvatar" + ], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/RifleLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/EliteRifleLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/RifleLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Forest/Avatars/EliteRifleLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Forest/Avatars/RifleLancerAvatar", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/EliteRifleLancerAvatar", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/RifleLancerAvatar", + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/EliteRifleLancerAvatar" + ], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/EviseratorLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Forest/Avatars/EvisceratorLancerAvatar", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/EvisceratorLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/EvisceratorLancerAvatar" + ], + ["/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/FemaleGrineerAvatar", "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/FemaleGrineerSniperAvatar"], + ["/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/FlameLancerAvatar"], + ["/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/GrineerMeleeStaffAvatar"], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/HeavyFemaleGrineerAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/HeavyFemaleGrineerAvatarDesert", + "/Lotus/Types/Enemies/Grineer/Forest/HeavyFemaleGrineerAvatarDesert", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/FemaleGrineerHeavyAvatar" + ], + ["/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/IncendiaryBombardAvatar"], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/JetpackMarineAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/JetpackMarineAvatar", + "/Lotus/Types/Enemies/Grineer/Forest/Avatars/JetpackMarineAvatar", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/JetpackMarineAvatar" + ], + ["/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/MacheteWomanAvatar", "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/FemaleGrineerMacheteAvatar"], + ["/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/ShieldLancerAvatar"], + [ + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/ShotgunLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/ShotgunLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Forest/Avatars/ShotgunLancerAvatar", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/ShotgunLancerAvatar" + ], + [ + "/Lotus/Types/Enemies/Grineer/GrineerAvatars/GrineerMarinePistolAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/GrineerMarinePistolAvatar", + "/Lotus/Types/Enemies/Grineer/Forest/Avatars/GrineerMarinePistolAvatar", + "/Lotus/Types/Enemies/Grineer/SeaLab/Avatars/GrineerMarinePistolAvatar" + ], + [ + "/Lotus/Types/Enemies/Infested/AiWeek/Ancients/AncientAvatar", + "/Lotus/Types/Enemies/Infested/AiWeek/Ancients/HealingAncientAvatar", + "/Lotus/Types/Enemies/Infested/AiWeek/Ancients/ToxicAncientAvatar" + ], + ["/Lotus/Types/Enemies/Infested/AiWeek/Ancients/DiseasedAncientAvatar"], + ["/Lotus/Types/Enemies/Infested/AiWeek/Ancients/SpawningAncientAvatar"], + [ + "/Lotus/Types/Enemies/Infested/AiWeek/Crawlers/CrawlerAvatar", + "/Lotus/Types/Enemies/Infested/AiWeek/Crawlers/NoxiousCrawlerAvatar", + "/Lotus/Types/Enemies/Infested/AiWeek/Crawlers/GraspingCrawlerAvatar", + "/Lotus/Types/Enemies/Infested/AiWeek/Crawlers/GrenadeAvatar", + "/Lotus/Types/Enemies/Infested/AiWeek/Crawlers/LightningAvatar" + ], + ["/Lotus/Types/Enemies/Infested/AiWeek/InfestedMoas/NaniteCloudBipedAvatar", "/Lotus/Types/Enemies/Infested/AiWeek/InfestedMoas/SlowBombBipedAvatar"], + ["/Lotus/Types/Enemies/Infested/AiWeek/Quadrupeds/QuadrupedAvatar"], + ["/Lotus/Types/Enemies/Infested/AiWeek/Runners/LeapingRunnerAvatar", "/Lotus/Types/Enemies/Infested/AiWeek/Runners/RunnerAvatar"], + ["/Lotus/Types/Enemies/Orokin/OrokinBladeSawmanAvatar"], + ["/Lotus/Types/Enemies/Orokin/OrokinHealingAncientAvatar"], + ["/Lotus/Types/Enemies/Orokin/OrokinHeavyFemaleAvatar"], + ["/Lotus/Types/Enemies/Orokin/OrokinNullifySpacemanAvatar"], + ["/Lotus/Types/Enemies/Orokin/OrokinRocketBombardAvatar"], + ["/Lotus/Types/Enemies/Orokin/RifleLancerAvatar"], + ["/Lotus/Types/Enemies/Orokin/RifleSpacemanAvatar"], + ["/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/RocketBombardAvatar"] +] diff --git a/static/fixed_responses/loginRewards.json b/static/fixed_responses/loginRewards.json deleted file mode 100644 index b6632556..00000000 --- a/static/fixed_responses/loginRewards.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "DailyTributeInfo": { - "IsMilestoneDay": false, - "IsChooseRewardSet": true, - "LoginDays": 1337, - "NextMilestoneReward": "", - "NextMilestoneDay": 50 - } -} diff --git a/static/fixed_responses/loginRewards/randomRewards.json b/static/fixed_responses/loginRewards/randomRewards.json new file mode 100644 index 00000000..20d3b60c --- /dev/null +++ b/static/fixed_responses/loginRewards/randomRewards.json @@ -0,0 +1,187 @@ +[ + { + "RewardType": "RT_RESOURCE", + "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/OxiumAlloy", + "Amount": 100, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Ordis/DDayTribOrdis" + }, + { + "RewardType": "RT_RESOURCE", + "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/Gallium", + "Amount": 1, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo" + }, + { + "RewardType": "RT_RESOURCE", + "StoreItemType": "/Lotus/StoreItems/Types/Items/Research/ChemFragment", + "Amount": 2, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo" + }, + { + "RewardType": "RT_RESOURCE", + "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/Morphic", + "Amount": 1, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo" + }, + { + "RewardType": "RT_RESOURCE", + "StoreItemType": "/Lotus/StoreItems/Types/Items/Research/EnergyFragment", + "Amount": 2, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo" + }, + { + "RewardType": "RT_RESOURCE", + "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/NeuralSensor", + "Amount": 1, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo" + }, + { + "RewardType": "RT_RESOURCE", + "StoreItemType": "/Lotus/StoreItems/Types/Items/Research/BioFragment", + "Amount": 2, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo" + }, + { + "RewardType": "RT_RESOURCE", + "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/Neurode", + "Amount": 1, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo" + }, + { + "RewardType": "RT_RESOURCE", + "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/OrokinCell", + "Amount": 1, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo" + }, + { + "RewardType": "RT_RESOURCE", + "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/Cryotic", + "Amount": 50, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo" + }, + { + "RewardType": "RT_RESOURCE", + "StoreItemType": "/Lotus/StoreItems/Types/Items/MiscItems/Tellurium", + "Amount": 1, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Darvo/DDayTribDarvo" + }, + { + "RewardType": "RT_CREDITS", + "StoreItemType": "", + "Icon": "/Lotus/Interface/Icons/StoreIcons/Currency/CreditsLarge.png", + "Amount": 10000, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Simaris/DDayTribSmrs" + }, + { + "RewardType": "RT_BOOSTER", + "StoreItemType": "/Lotus/Types/StoreItems/Boosters/AffinityBoosterStoreItem", + "Amount": 1, + "ScalingMultiplier": 2, + "Duration": 3, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Simaris/DDayTribSmrs" + }, + { + "RewardType": "RT_BOOSTER", + "StoreItemType": "/Lotus/Types/StoreItems/Boosters/CreditBoosterStoreItem", + "Amount": 1, + "ScalingMultiplier": 2, + "Duration": 3, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Simaris/DDayTribSmrs" + }, + { + "RewardType": "RT_BOOSTER", + "StoreItemType": "/Lotus/Types/StoreItems/Boosters/ResourceAmountBoosterStoreItem", + "Amount": 1, + "ScalingMultiplier": 2, + "Duration": 3, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Simaris/DDayTribSmrs" + }, + { + "RewardType": "RT_BOOSTER", + "StoreItemType": "/Lotus/Types/StoreItems/Boosters/ResourceDropChanceBoosterStoreItem", + "Amount": 1, + "ScalingMultiplier": 2, + "Duration": 3, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Simaris/DDayTribSmrs" + }, + { + "RewardType": "RT_STORE_ITEM", + "StoreItemType": "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/RareFusionBundle", + "Amount": 1, + "ScalingMultiplier": 1, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Maroo/DDayTribMaroo" + }, + { + "RewardType": "RT_RECIPE", + "StoreItemType": "/Lotus/StoreItems/Types/Recipes/Components/FormaBlueprint", + "Amount": 1, + "ScalingMultiplier": 0.5, + "Rarity": "RARE", + "probability": 0.001467351430667816, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Maroo/DDayTribMaroo" + }, + { + "RewardType": "RT_RANDOM_RECIPE", + "StoreItemType": "", + "Amount": 1, + "ScalingMultiplier": 0, + "Rarity": "COMMON", + "probability": 0.055392516507703576, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Teshin/DDayTribTeshin" + }, + { + "RewardType": "RT_STORE_ITEM", + "StoreItemType": "/Lotus/StoreItems/Types/BoosterPacks/LoginRewardRandomProjection", + "Amount": 1, + "ScalingMultiplier": 1, + "Rarity": "RARE", + "probability": 0.001467351430667816, + "Transmission": "/Lotus/Sounds/Dialog/DailyTribute/Ordis/DDayTribOrdis" + } +] diff --git a/static/fixed_responses/modularWeaponSale.json b/static/fixed_responses/modularWeaponSale.json deleted file mode 100644 index 6d5bd172..00000000 --- a/static/fixed_responses/modularWeaponSale.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "SaleInfos": [ - { - "Name": "Ostron", - "Expiry": { - "$date": { - "$numberLong": "9999999900000" - } - }, - "Revision": 3453, - "Weapons": [ - { - "ItemType": "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon", - "PremiumPrice": 162, - "ModularParts": [ - "/Lotus/Weapons/Ostron/Melee/ModularMelee01/Handle/HandleFive", - "/Lotus/Weapons/Ostron/Melee/ModularMelee01/Tip/TipFour", - "/Lotus/Weapons/Ostron/Melee/ModularMelee01/Balance/BalanceSpeedICritII" - ] - } - ] - }, - { - "Name": "SolarisUnitedHoverboard", - "Expiry": { - "$date": { - "$numberLong": "9999999900000" - } - }, - "Revision": 2058, - "Weapons": [ - { - "ItemType": "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit", - "PremiumPrice": 51, - "ModularParts": [ - "/Lotus/Types/Vehicles/Hoverboard/HoverboardParts/PartComponents/HoverboardSolarisA/HoverboardSolarisADeck", - "/Lotus/Types/Vehicles/Hoverboard/HoverboardParts/PartComponents/HoverboardCorpusA/HoverboardCorpusAEngine", - "/Lotus/Types/Vehicles/Hoverboard/HoverboardParts/PartComponents/HoverboardSolarisA/HoverboardSolarisAFront", - "/Lotus/Types/Vehicles/Hoverboard/HoverboardParts/PartComponents/HoverboardCorpusB/HoverboardCorpusBJet" - ] - } - ] - }, - { - "Name": "SolarisUnitedMoaPet", - "Expiry": { - "$date": { - "$numberLong": "9999999900000" - } - }, - "Revision": 2058, - "Weapons": [ - { - "ItemType": "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit", - "PremiumPrice": 180, - "ModularParts": [ - "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetParts/MoaPetLegB", - "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetParts/MoaPetHeadPara", - "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetParts/MoaPetEngineArcotek", - "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetParts/MoaPetPayloadMunitron" - ] - } - ] - }, - { - "Name": "SolarisUnitedKitGun", - "Expiry": { - "$date": { - "$numberLong": "9999999900000" - } - }, - "Revision": 2058, - "Weapons": [ - { - "ItemType": "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary", - "PremiumPrice": 184, - "ModularParts": [ - "/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Handle/SUModularSecondaryHandleCPart", - "/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelBPart", - "/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Clip/SUModularStatIReloadIIClipPart" - ] - } - ] - } - ] -} diff --git a/static/fixed_responses/questCompletionRewards.json b/static/fixed_responses/questCompletionRewards.json index 9d727f64..d0f692aa 100644 --- a/static/fixed_responses/questCompletionRewards.json +++ b/static/fixed_responses/questCompletionRewards.json @@ -1,9 +1,5 @@ { "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain": [ - { - "ItemType": "/Lotus/Types/Keys/DuviriQuest/DuviriQuestKeyChain", - "ItemCount": 1 - }, { "ItemType": "/Lotus/Types/NeutralCreatures/ErsatzHorse/ErsatzHorsePowerSuit", "ItemCount": 1 diff --git a/static/fixed_responses/worldState/worldState.json b/static/fixed_responses/worldState/worldState.json index eef85698..74463e4d 100644 --- a/static/fixed_responses/worldState/worldState.json +++ b/static/fixed_responses/worldState/worldState.json @@ -1,4 +1,5 @@ { + "Version": 10, "Events": [ { "Msg": "Join the OpenWF Discord!", @@ -21,6 +22,46 @@ "Icon": "/Lotus/Interface/Icons/DiscordIconNoBacker.png" } ], + "Invasions": [ + { + "_id": { + "$oid": "67c8ec8b3d0d86b236c1c18f" + }, + "Faction": "FC_INFESTATION", + "DefenderFaction": "FC_CORPUS", + "Node": "SolNode53", + "Count": -28558, + "Goal": 30000, + "LocTag": "/Lotus/Language/Menu/InfestedInvasionBoss", + "Completed": false, + "ChainID": { + "$oid": "67c8b6a2bde0dfd0f7c1c18d" + }, + "AttackerReward": [], + "AttackerMissionInfo": { + "seed": 488863, + "faction": "FC_CORPUS" + }, + "DefenderReward": { + "countedItems": [ + { + "ItemType": "/Lotus/Types/Items/Research/EnergyComponent", + "ItemCount": 3 + } + ] + }, + "DefenderMissionInfo": { + "seed": 127653, + "faction": "FC_INFESTATION", + "missionReward": [] + }, + "Activation": { + "$date": { + "$numberLong": "1741221003031" + } + } + } + ], "Sorties": [ { "_id": { "$oid": "663a4c7d4d932c97c0a3acd7" }, @@ -38,21 +79,6 @@ "Twitter": true } ], - "LiteSorties": [ - { - "_id": { "$oid": "663819fd1cec9ebe9d83a06e" }, - "Activation": { "$date": { "$numberLong": "1714953600000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Reward": "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards", - "Seed": 58034, - "Boss": "SORTIE_BOSS_NIRA", - "Missions": [ - { "missionType": "MT_MOBILE_DEFENSE", "node": "SolNode125" }, - { "missionType": "MT_SURVIVAL", "node": "SolNode74" }, - { "missionType": "MT_ASSASSINATION", "node": "SolNode53" } - ] - } - ], "SyndicateMissions": [ { "_id": { "$oid": "663a4fc5ba6f84724fa48049" }, @@ -730,31 +756,1861 @@ "Character": "Baro'Ki Teel", "Node": "PlutoHUB", "Manifest": [ - { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Skins/GaussSentinelSkin", "PrimePrice": 500, "RegularPrice": 425000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/ElectEventPistolMod", "PrimePrice": 300, "RegularPrice": 150000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/WeaponEventSlashDamageMod", "PrimePrice": 375, "RegularPrice": 150000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/WeaponEventSlashDamageMod", "PrimePrice": 375, "RegularPrice": 150000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/ElectEventShotgunMod", "PrimePrice": 300, "RegularPrice": 150000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Liset/LisetSkinVoidTrader", "PrimePrice": 120, "RegularPrice": 150000 }, - { "ItemType": "/Lotus/StoreItems/Types/Items/MiscItems/PhotoboothTileInarosTomb", "PrimePrice": 325, "RegularPrice": 175000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Liset/LisetBlueSkySkinPrimeTrader", "PrimePrice": 210, "RegularPrice": 450000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MeleeDangles/BaroInarosMeleeDangle", "PrimePrice": 250, "RegularPrice": 250000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/BaroInarosPolearmSkin", "PrimePrice": 325, "RegularPrice": 250000 }, - { "ItemType": "/Lotus/Types/StoreItems/Boosters/CreditBooster3DayStoreItem", "PrimePrice": 350, "RegularPrice": 75000 }, - { "ItemType": "/Lotus/Types/StoreItems/Packages/VTEosArmourBundle", "PrimePrice": 285, "RegularPrice": 260000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/ElixisSonicor", "PrimePrice": 380, "RegularPrice": 175000 }, - { "ItemType": "/Lotus/StoreItems/Weapons/Grineer/LongGuns/GrineerLeverActionRifle/PrismaGrinlokWeapon", "PrimePrice": 500, "RegularPrice": 220000 }, - { "ItemType": "/Lotus/StoreItems/Weapons/Corpus/LongGuns/CrpFreezeRay/Vandal/CrpFreezeRayVandalRifle", "PrimePrice": 475, "RegularPrice": 250000 }, - { "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConJ", "PrimePrice": 75, "RegularPrice": 100000 }, - { "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConA", "PrimePrice": 75, "RegularPrice": 100000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/NezhaLeverianCape", "PrimePrice": 400, "RegularPrice": 350000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Glass/GaraAlternateSkin", "PrimePrice": 550, "RegularPrice": 100000 }, - { "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropCorpusBasilisk", "PrimePrice": 100, "RegularPrice": 100000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Hoods/JaviExecutionHood", "PrimePrice": 450, "RegularPrice": 450000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Wings/GaussSentinelWings", "PrimePrice": 400, "RegularPrice": 500000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponShotgunFactionDamageCorpusExpert", "PrimePrice": 350, "RegularPrice": 140000 }, - { "ItemType": "/Lotus/StoreItems/Types/Keys/MummyQuestKeyBlueprint", "PrimePrice": 100, "RegularPrice": 25000 }, - { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Effects/FootstepsMaple", "PrimePrice": 15, "RegularPrice": 1000 } + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/TennoCon2024GlyphAlt", + "PrimePrice": 15, + "RegularPrice": 1000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/Emotes/Tennocon2024EmoteAlt", + "PrimePrice": 15, + "RegularPrice": 1000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/HeartOfDeimosAlbumCoverPoster", + "PrimePrice": 80, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConC", + "PrimePrice": 75, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConJ", + "PrimePrice": 75, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConH", + "PrimePrice": 75, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionVoltOdonataPrimeBronze", + "PrimePrice": 125, + "RegularPrice": 55000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionVoltOdonataPrimeBronze", + "PrimePrice": 125, + "RegularPrice": 55000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionMagNovaVaultBBronze", + "PrimePrice": 125, + "RegularPrice": 55000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/SolsticeNelumboCape", + "PrimePrice": 325, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/SummerSolstice/SummerSolsticeTwinGrakatas", + "PrimePrice": 300, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Weapons/Staff/TnRibbonStaffSkin", + "PrimePrice": 350, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Grineer/Melee/GunBlade/GrnGunBlade/GrnGunblade", + "PrimePrice": 550, + "RegularPrice": 325000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Corpus/LongGuns/CrpBFG/Vandal/VandalCrpBFG", + "PrimePrice": 650, + "RegularPrice": 550000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Event/AmbulasEvent/Expert/SecondaryExplosionRadiusModExpert", + "PrimePrice": 350, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Clan/Dragon2024BadgeItem", + "PrimePrice": 55, + "RegularPrice": 45000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Archwing/Rifle/PrimedArchwingDamageOnReloadMod", + "PrimePrice": 375, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Archwing/Rifle/PrimedArchwingRifleFireIterationsMod", + "PrimePrice": 400, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageBaruukDoanStyle", + "PrimePrice": 75, + "RegularPrice": 60000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/OctaviaBobbleHead", + "PrimePrice": 50, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Skins/GaussSentinelSkin", + "PrimePrice": 500, + "RegularPrice": 425000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/PrismaLotusVinesSigil", + "PrimePrice": 55, + "RegularPrice": 60000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageExcaliburActionProto", + "PrimePrice": 75, + "RegularPrice": 60000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageIvaraAction", + "PrimePrice": 75, + "RegularPrice": 60000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/HornSkullScarf", + "PrimePrice": 325, + "RegularPrice": 350000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/RhinoDeluxeSigil", + "PrimePrice": 45, + "RegularPrice": 55000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Events/InfQuantaInfestedAladV", + "PrimePrice": 325, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/JavisExperimentsPosterD", + "PrimePrice": 90, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/JavisExperimentsPosterB", + "PrimePrice": 90, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/JavisExperimentsPosterC", + "PrimePrice": 90, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/PrimedWeaponElectricityDamageMod", + "PrimePrice": 350, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Warframe/Expert/AvatarShieldMaxModExpert", + "PrimePrice": 350, + "RegularPrice": 225000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/JavisExperimentsPosterA", + "PrimePrice": 90, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/EventSigilScarletSpear", + "PrimePrice": 45, + "RegularPrice": 45000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Grineer/LongGuns/GrnOrokinRifle/GrnOrokinRifleWeapon", + "PrimePrice": 675, + "RegularPrice": 625000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/ElixisNikana", + "PrimePrice": 375, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Wings/GaussSentinelWings", + "PrimePrice": 400, + "RegularPrice": 500000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Tails/GaussSentinelTail", + "PrimePrice": 400, + "RegularPrice": 500000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Masks/GaussSentinelMask", + "PrimePrice": 450, + "RegularPrice": 400000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropGrineerCutter", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Clan/CNY2023EmblemItem", + "PrimePrice": 55, + "RegularPrice": 45000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/WeGameNewYearFreeTigerSigil", + "PrimePrice": 55, + "RegularPrice": 45000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Clan/CNY2022EmblemItem", + "PrimePrice": 55, + "RegularPrice": 45000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/Leverian/IvaraLeverianPovisRecordsDecoration", + "PrimePrice": 75, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Hoods/HoodDuviriOperator", + "PrimePrice": 550, + "RegularPrice": 500000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Corpus/Melee/CrpTonfa/CrpPrismaTonfa", + "PrimePrice": 450, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropCleaningDroneDuviri", + "PrimePrice": 800, + "RegularPrice": 650000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Accessories/AshLevarianTiara", + "PrimePrice": 550, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Effects/BaroEphemeraB", + "PrimePrice": 250, + "RegularPrice": 350000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Promo/Warframe/PromoParis", + "PrimePrice": 315, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/ThraxSigil", + "PrimePrice": 50, + "RegularPrice": 55000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Corpus/Bow/Longbow/PrismaLenz/PrismaLenzWeapon", + "PrimePrice": 575, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/Vignettes/Warframes/ArchwingAFItem", + "PrimePrice": 100, + "RegularPrice": 330000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Game/QuartersWallpapers/LavosAlchemistWallpaper", + "PrimePrice": 275, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/GrendelOrokinDishSet", + "PrimePrice": 110, + "RegularPrice": 130000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/ColourPickerKiteerItemB", + "PrimePrice": 200, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/NezhaEtchingsTablets", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/GaussTowerOfAltraDeco", + "PrimePrice": 110, + "RegularPrice": 125000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroPlanter", + "PrimePrice": 125, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroPedestal", + "PrimePrice": 150, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Leggings/LeggingsNovaEngineer", + "PrimePrice": 300, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/BodySuits/BodySuitNovaEngineer", + "PrimePrice": 300, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Sleeves/SleevesNovaEngineer", + "PrimePrice": 300, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Hoods/HoodNovaEngineer", + "PrimePrice": 350, + "RegularPrice": 375000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BekranZaftBucketBroom", + "PrimePrice": 100, + "RegularPrice": 125000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Warfan/TnMoonWarfan/MoonWarfanWeapon", + "PrimePrice": 410, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MeleeDangles/MoonWarfanSugatraMeleeDangle", + "PrimePrice": 250, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/OstronHeadStatue", + "PrimePrice": 125, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/DomsFinalDrink", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Wisp/WispAlternateSkin", + "PrimePrice": 550, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Pacifist/BaruukImmortalSkin", + "PrimePrice": 550, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/ErraBobbleHead", + "PrimePrice": 75, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/OwlOrdisStatue", + "PrimePrice": 350, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TNWVesoBobbleHead", + "PrimePrice": 75, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TNWTeshinBobbleHead", + "PrimePrice": 75, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/CosmeticEnhancers/Peculiars/EvilSpiritMod", + "PrimePrice": 250, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmourThree/BaroArmourThreeL", + "PrimePrice": 400, + "RegularPrice": 350000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmourThree/BaroArmourThreeC", + "PrimePrice": 350, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmourThree/BaroArmourThreeA", + "PrimePrice": 400, + "RegularPrice": 350000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/BaroCape3Scarf", + "PrimePrice": 500, + "RegularPrice": 500000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/ElixisTiberon", + "PrimePrice": 315, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Effects/LotusFlowers", + "PrimePrice": 250, + "RegularPrice": 450000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/UmbraPedestal", + "PrimePrice": 0, + "RegularPrice": 1000000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Dragon/ChromaAlternateSkin", + "PrimePrice": 550, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Hoverboard/HoverboardStickerBaroB", + "PrimePrice": 75, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/ElixisLatronPistol", + "PrimePrice": 400, + "RegularPrice": 215000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/WeaponGlaiveOnKillBuffSecondary", + "PrimePrice": 300, + "RegularPrice": 115000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConA", + "PrimePrice": 75, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/WeaponGlaiveSecondaryHeadshotKillMod", + "PrimePrice": 300, + "RegularPrice": 115000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConD", + "PrimePrice": 75, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConB", + "PrimePrice": 75, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Expert/WeaponIncreaseRadialExplosionModExpert", + "PrimePrice": 350, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Archwing/Primary/ArchwingHeavyPistols/Prisma/PrismaArchHeavyPistols", + "PrimePrice": 525, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/TwinSnakesGlyph", + "PrimePrice": 80, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConF", + "PrimePrice": 75, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConE", + "PrimePrice": 75, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/WeaponGlaiveOnSixKillsBuffSecondary", + "PrimePrice": 300, + "RegularPrice": 115000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/WeGameNewYearOxSigil", + "PrimePrice": 55, + "RegularPrice": 45000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConG", + "PrimePrice": 75, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponFreezeDamageModExpert", + "PrimePrice": 350, + "RegularPrice": 125000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/GarvLatroxPoster", + "PrimePrice": 80, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Grineer/Melee/GrnBoomerang/HalikarWraithWeapon", + "PrimePrice": 450, + "RegularPrice": 350000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardTennoConI", + "PrimePrice": 75, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/NecraArmor/NecraArmorC", + "PrimePrice": 325, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/NecraArmor/NecraArmorL", + "PrimePrice": 300, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/NecraArmor/NecraArmorA", + "PrimePrice": 315, + "RegularPrice": 215000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponReloadSpeedModExpert", + "PrimePrice": 300, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Grineer/Melee/GrineerMachetteAndCleaver/PrismaMachete", + "PrimePrice": 400, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MoaPet/BaroMoaPetSkin", + "PrimePrice": 500, + "RegularPrice": 325000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/Deimos/PlushySunMonsterCommon", + "PrimePrice": 150, + "RegularPrice": 125000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/Deimos/PlushyMoonMonsterCommon", + "PrimePrice": 150, + "RegularPrice": 125000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Expert/WeaponClipMaxModExpert", + "PrimePrice": 280, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponClipMaxModExpert", + "PrimePrice": 280, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/TnShinaiSword/TnShinaiSwordSkin", + "PrimePrice": 375, + "RegularPrice": 280000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnShinaiArmor/TnShinaiArmorL", + "PrimePrice": 275, + "RegularPrice": 115000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnShinaiArmor/TnShinaiArmorC", + "PrimePrice": 300, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnShinaiArmor/TnShinaiArmorA", + "PrimePrice": 315, + "RegularPrice": 125000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Weapons/DualSword/DualRibbonKamasSkin", + "PrimePrice": 350, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Infestation/NidusAlternateSkin", + "PrimePrice": 550, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Game/ActionFigureDioramas/EmpyreanRegionADiorama", + "PrimePrice": 155, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropGrineerFlak", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropGrineerTaktis", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/AshLeverianLiosPistol", + "PrimePrice": 400, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Glass/GaraAlternateSkin", + "PrimePrice": 550, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Expert/WeaponSnipersConvertAmmoModExpert", + "PrimePrice": 400, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/EraHypnosisPoster", + "PrimePrice": 100, + "RegularPrice": 110000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/NezhaLeverianCape", + "PrimePrice": 400, + "RegularPrice": 350000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Leverian/NezhaLeverian/NezhaLeverianPolearm", + "PrimePrice": 350, + "RegularPrice": 325000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BoredTennoPoster", + "PrimePrice": 90, + "RegularPrice": 120000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropCorpusBasilisk", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropCorpusWeaver", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropCorpusHarpi", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Archwing/GrendelArchwingSkin", + "PrimePrice": 400, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Hoods/JaviExecutionHood", + "PrimePrice": 450, + "RegularPrice": 450000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/DualStat/ElectEventMeleeMod", + "PrimePrice": 300, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/DualStat/FireEventMeleeMod", + "PrimePrice": 300, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/MeleeTrees/ClawCmbTwoMeleeTree", + "PrimePrice": 385, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/FireEventRifleMod", + "PrimePrice": 300, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/MeleeTrees/AxeCmbThreeMeleeTree", + "PrimePrice": 385, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/WeaponEventSlashDamageMod", + "PrimePrice": 375, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/BowMultiShotOnHitMod", + "PrimePrice": 300, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/ElectEventShotgunMod", + "PrimePrice": 300, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/FireEventPistolMod", + "PrimePrice": 300, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/FireEventShotgunMod", + "PrimePrice": 300, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/WeaponEventPistolImpactDamageMod", + "PrimePrice": 300, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/PrimedWeaponCritDamageMod", + "PrimePrice": 400, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/Expert/WeaponMeleeFactionDamageInfestedExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/Expert/WeaponMeleeFactionDamageGrineerExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/Expert/WeaponMeleeFactionDamageCorruptedExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/Expert/WeaponMeleeFactionDamageCorpusExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/WeaponClipMaxModExpert", + "PrimePrice": 280, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponShotgunConvertAmmoModExpert", + "PrimePrice": 400, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Archwing/Rifle/Expert/ArchwingRifleDamageAmountModExpert", + "PrimePrice": 350, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Expert/WeaponRifleConvertAmmoModExpert", + "PrimePrice": 400, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Sentinels/SentinelPrecepts/PrimedRegen", + "PrimePrice": 300, + "RegularPrice": 220000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/Expert/WeaponMeleeRangeIncModExpert", + "PrimePrice": 300, + "RegularPrice": 220000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponCritDamageModExpert", + "PrimePrice": 280, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/WeaponReloadSpeedModExpert", + "PrimePrice": 375, + "RegularPrice": 120000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/Expert/WeaponMeleeDamageModExpert", + "PrimePrice": 385, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponDamageAmountModExpert", + "PrimePrice": 300, + "RegularPrice": 110000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/WeaponCritChanceModBeginnerExpert", + "PrimePrice": 400, + "RegularPrice": 220000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/WeaponPistolConvertAmmoModExpert", + "PrimePrice": 400, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Sentinel/Kubrow/Expert/KubrowPackLeaderExpertMod", + "PrimePrice": 300, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Archwing/Expert/ArchwingSuitAbilityStrengthModExpert", + "PrimePrice": 350, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/Expert/WeaponImpactDamageModExpert", + "PrimePrice": 350, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/WeaponFireDamageModExpert", + "PrimePrice": 350, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Warframe/Expert/AvatarPowerMaxModExpert", + "PrimePrice": 350, + "RegularPrice": 110000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/Expert/WeaponToxinDamageModExpert", + "PrimePrice": 350, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Expert/WeaponReloadSpeedModExpert", + "PrimePrice": 375, + "RegularPrice": 120000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/WeaponPistolFactionDamageInfestedExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/WeaponPistolFactionDamageGrineerExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/WeaponPistolFactionDamageCorruptedExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/WeaponPistolFactionDamageCorpusExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Expert/WeaponFreezeDamageModExpert", + "PrimePrice": 350, + "RegularPrice": 110000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Warframe/Expert/AvatarAbilityDurationModExpert", + "PrimePrice": 350, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponShotgunFactionDamageInfestedExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponShotgunFactionDamageGrineerExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponShotgunFactionDamageCorruptedExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponShotgunFactionDamageCorpusExpert", + "PrimePrice": 350, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponElectricityDamageModExpert", + "PrimePrice": 350, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Expert/PrimedWeaponFactionDamageInfested", + "PrimePrice": 400, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Expert/PrimedWeaponFactionDamageGrineer", + "PrimePrice": 400, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Expert/PrimedWeaponFactionDamageCorrupted", + "PrimePrice": 400, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Expert/PrimedWeaponFactionDamageCorpus", + "PrimePrice": 400, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Sentinel/SentinelLootRadarEnemyRadarExpertMod", + "PrimePrice": 300, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/MeleeTrees/GlaiveCmbTwoMeleeTree", + "PrimePrice": 385, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/WeaponEventSlashDamageMod", + "PrimePrice": 375, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/WeaponEventMeleeImpactDamageMod", + "PrimePrice": 400, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/WeaponEventRifleImpactDamageMod", + "PrimePrice": 330, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/WeaponEventSlashDamageMod", + "PrimePrice": 375, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/WeaponEventShotgunImpactDamageMod", + "PrimePrice": 365, + "RegularPrice": 220000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/ElectEventRifleMod", + "PrimePrice": 300, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/ElectEventPistolMod", + "PrimePrice": 300, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/WeaponEventSlashDamageMod", + "PrimePrice": 375, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/VoidTrader/VTDetron", + "PrimePrice": 500, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Corpus/LongGuns/CrpFreezeRay/Vandal/CrpFreezeRayVandalRifle", + "PrimePrice": 475, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/ClanTech/Chemical/FlameThrowerWraith", + "PrimePrice": 550, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Grineer/Melee/GrineerMachetteAndCleaver/WraithMacheteWeapon", + "PrimePrice": 410, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Corpus/Pistols/CrpHandRL/PrismaAngstrum", + "PrimePrice": 475, + "RegularPrice": 210000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Grineer/Melee/GrineerMachetteAndCleaver/PrismaDualCleavers", + "PrimePrice": 490, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Grineer/LongGuns/VoidTraderGorgon/VTGorgon", + "PrimePrice": 600, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/VoidTrader/PrismaGrakata", + "PrimePrice": 610, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Grineer/LongGuns/GrineerLeverActionRifle/PrismaGrinlokWeapon", + "PrimePrice": 500, + "RegularPrice": 220000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Corpus/Melee/KickAndPunch/PrismaObex", + "PrimePrice": 500, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/VoidTrader/PrismaSkana", + "PrimePrice": 510, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Corpus/LongGuns/CorpusUMP/PrismaCorpusUMP", + "PrimePrice": 400, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Grineer/Pistols/GrineerBulbousSMG/Prisma/PrismaTwinGremlinsWeapon", + "PrimePrice": 500, + "RegularPrice": 220000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Archwing/Melee/VoidTraderArchsword/VTArchSwordWeapon", + "PrimePrice": 550, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/ClanTech/Energy/VandalElectroProd", + "PrimePrice": 410, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Corpus/LongGuns/CrpShockRifle/QuantaVandal", + "PrimePrice": 450, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Corpus/LongGuns/Machinegun/SupraVandal", + "PrimePrice": 500, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Grineer/Pistols/WraithSingleViper/WraithSingleViper", + "PrimePrice": 400, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Grineer/LongGuns/GrineerSniperRifle/VulkarWraith", + "PrimePrice": 450, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Pistols/ConclaveLeverPistol/ConclaveLeverPistol", + "PrimePrice": 500, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MeleeDangles/FireMeleeDangle", + "PrimePrice": 100, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/BaroInarosPolearmSkin", + "PrimePrice": 325, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MeleeDangles/BaroInarosMeleeDangle", + "PrimePrice": 250, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MeleeDangles/InfestedMeleeDangle", + "PrimePrice": 250, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/VTHalloweenDarkSword", + "PrimePrice": 320, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/SummerSolstice/SummerSolsticeGorgon", + "PrimePrice": 300, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/SummerSolstice/SummerIgnisSkin", + "PrimePrice": 300, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/BaroArrow", + "PrimePrice": 375, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MeleeDangles/BaroMeleeDangle", + "PrimePrice": 250, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/BaroScytheMacheteSkin", + "PrimePrice": 375, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/ElixisOdonataSkin", + "PrimePrice": 350, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/ElixisBallasSword", + "PrimePrice": 350, + "RegularPrice": 350000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/PrismaArrow", + "PrimePrice": 350, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/VTRedeemerSkin", + "PrimePrice": 325, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/ElixisSonicor", + "PrimePrice": 380, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/ElixisTigris", + "PrimePrice": 300, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/VTQuanta", + "PrimePrice": 300, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/VoidTrader/ElixisOpticor", + "PrimePrice": 325, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Halloween/HalloweenDread", + "PrimePrice": 300, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/ImageBaroKiteer", + "PrimePrice": 80, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/Seasonal/AvatarImageGlyphCookieKavat", + "PrimePrice": 80, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/Seasonal/AvatarImageGlyphCookieKubrow", + "PrimePrice": 80, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/LisetScarf", + "PrimePrice": 600, + "RegularPrice": 400000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnLatronArmor/TnLatronChestArmorElixis", + "PrimePrice": 275, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnLatronArmor/TnLatronLegArmorElixis", + "PrimePrice": 300, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnLatronArmor/TnLatronArmArmorElixis", + "PrimePrice": 325, + "RegularPrice": 220000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/ColourPickerTwitchBItemA", + "PrimePrice": 220, + "RegularPrice": 220000 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/VTEosArmourBundle", + "PrimePrice": 285, + "RegularPrice": 260000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/VTEos/VTEosChestArmor", + "PrimePrice": 125, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Effects/FootstepsMaple", + "PrimePrice": 15, + "RegularPrice": 1000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Clan/BaroKavatBadgeItem", + "PrimePrice": 50, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/BaroKavatSigil", + "PrimePrice": 55, + "RegularPrice": 45000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/GrineerTurbines/WraithTurbinesChestArmor", + "PrimePrice": 300, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/WraithTurbinesScarf", + "PrimePrice": 400, + "RegularPrice": 500000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/GrineerTurbines/WraithTurbinesLegArmor", + "PrimePrice": 350, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/GrineerTurbines/WraithTurbinesArmArmor", + "PrimePrice": 350, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Pirate/HydroidAlternateSkin", + "PrimePrice": 550, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/Seasonal/Halloween2019GrendelTreat", + "PrimePrice": 80, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmour/BaroArmourC", + "PrimePrice": 150, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/ColourPickerKiteerItemA", + "PrimePrice": 150, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/KazBaroCape", + "PrimePrice": 325, + "RegularPrice": 450000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Effects/BaroEphemeraA", + "PrimePrice": 100, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmourTwo/BaroArmourTwoC", + "PrimePrice": 175, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmourTwo/BaroArmourTwoL", + "PrimePrice": 225, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmourTwo/BaroArmourTwoA", + "PrimePrice": 310, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmour/BaroArmourL", + "PrimePrice": 300, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/BaroCape2Scarf", + "PrimePrice": 400, + "RegularPrice": 350000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Clan/BaroQuantumBadgeItem", + "PrimePrice": 400, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmour/BaroArmourA", + "PrimePrice": 350, + "RegularPrice": 110000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/SolsticeBaroCape", + "PrimePrice": 425, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/BaroCape", + "PrimePrice": 500, + "RegularPrice": 500000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageBaroIcon", + "PrimePrice": 80, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/VTEos/VTEosALArmor", + "PrimePrice": 50, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/VTEos/VTEosLLArmor", + "PrimePrice": 65, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetThreeWinged/VTSetThreeLegLeftArmor", + "PrimePrice": 65, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetThreeWinged/VTSetThreeArmLeftArmor", + "PrimePrice": 65, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetTwoSamurai/VTSetTwoLegLeftArmor", + "PrimePrice": 100, + "RegularPrice": 55000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetTwoSamurai/VTSetTwoArmLeftArmor", + "PrimePrice": 100, + "RegularPrice": 55000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Magician/LimboImmortalSkin", + "PrimePrice": 550, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Cowgirl/MesaImmortallSkin", + "PrimePrice": 550, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Harlequin/MirageAlternateSkin", + "PrimePrice": 550, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Clan/BaroKubrowBadgeItem", + "PrimePrice": 50, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/BaroKubrowSigil", + "PrimePrice": 55, + "RegularPrice": 45000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/FurisArmor/PrismaFurisLArmor", + "PrimePrice": 225, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/FurisArmor/PrismaFurisCArmor", + "PrimePrice": 250, + "RegularPrice": 220000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/FurisArmor/PrismaFurisAArmor", + "PrimePrice": 300, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetThreeWinged/VTSetThreeChestArmor", + "PrimePrice": 150, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetTwoSamurai/VTSetTwoChestArmor", + "PrimePrice": 225, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/VTHornSkullScarf", + "PrimePrice": 250, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnLatronArmor/TnLatronChestArmorPrisma", + "PrimePrice": 275, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnLatronArmor/TnLatronLegArmorPrisma", + "PrimePrice": 300, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnLatronArmor/TnLatronArmArmorPrisma", + "PrimePrice": 325, + "RegularPrice": 220000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Clan/PrismaLotusEmblem", + "PrimePrice": 80, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageBaroTwoIcon", + "PrimePrice": 80, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/PrismaLotusSigil", + "PrimePrice": 55, + "RegularPrice": 45000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/Halloween2014Wings/PrismaNaberusArmArmor", + "PrimePrice": 220, + "RegularPrice": 140000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/PrimeTraderSigil", + "PrimePrice": 50, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/PrismaRazorScarf", + "PrimePrice": 350, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/VTDinoSpikeScarf", + "PrimePrice": 400, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageLowPolyKavat", + "PrimePrice": 80, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageLowPolyKubrow", + "PrimePrice": 80, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/VTEos/VTEosARArmor", + "PrimePrice": 50, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/VTEos/VTEosLRArmor", + "PrimePrice": 65, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetThreeWinged/VTSetThreeLegRightArmor", + "PrimePrice": 65, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetThreeWinged/VTSetThreeArmRightArmor", + "PrimePrice": 65, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetTwoSamurai/VTSetTwoLegRightArmor", + "PrimePrice": 100, + "RegularPrice": 55000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetTwoSamurai/VTSetTwoArmRightArmor", + "PrimePrice": 100, + "RegularPrice": 55000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Tengu/ZephyrAlternateSkin", + "PrimePrice": 550, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Game/KubrowPet/Patterns/KubrowPetPatternPrimeTraderA", + "PrimePrice": 150, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Camo/DesertDirigaSkin", + "PrimePrice": 225, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Masks/KavatPetMask", + "PrimePrice": 500, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Tails/KavatPetTail", + "PrimePrice": 400, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Wings/KavatPetWings", + "PrimePrice": 400, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Catbrows/Armor/CatbrowArmorVoidTraderA", + "PrimePrice": 500, + "RegularPrice": 275000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Kubrows/Armor/KubrowArmorBaro", + "PrimePrice": 500, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Masks/BaroPetMask", + "PrimePrice": 500, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Tails/BaroPetTail", + "PrimePrice": 400, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Wings/BaroPetWings", + "PrimePrice": 400, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/KavatColorPackNexus", + "PrimePrice": 200, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Wings/PrismaJetWings", + "PrimePrice": 300, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Tails/PrismaFishTail", + "PrimePrice": 200, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Masks/PrismaMechHeadMask", + "PrimePrice": 175, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Kubrows/Armor/KubrowArmorPrisma", + "PrimePrice": 400, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Sentinels/SentinelPowersuits/PrismaShadePowerSuit", + "PrimePrice": 500, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sentinels/Skins/DesertTaxonSkin", + "PrimePrice": 200, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Catbrows/Armor/CatbrowArmorHalloweenA", + "PrimePrice": 400, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Boosters/AffinityBooster3DayStoreItem", + "PrimePrice": 450, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Boosters/CreditBooster3DayStoreItem", + "PrimePrice": 350, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Boosters/ModDropChanceBooster3DayStoreItem", + "PrimePrice": 500, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Boosters/ResourceAmount3DayStoreItem", + "PrimePrice": 400, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionPBronze", + "PrimePrice": 50, + "RegularPrice": 45000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Recipes/Components/CorruptedBombardBallBlueprint", + "PrimePrice": 100, + "RegularPrice": 50000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Restoratives/Consumable/CorruptedHeavyGunnerBall", + "PrimePrice": 100, + "RegularPrice": 40000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/OrbiterPictureFrameBaro", + "PrimePrice": 100, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Restoratives/Consumable/AssassinBaitC", + "PrimePrice": 200, + "RegularPrice": 125000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/MiscItems/PhotoboothTileInarosTomb", + "PrimePrice": 325, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Restoratives/Consumable/BaroFireWorksCrate", + "PrimePrice": 50, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/MiscItems/PhotoboothTileOrokinExtraction", + "PrimePrice": 325, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Keys/MummyQuestKeyBlueprint", + "PrimePrice": 100, + "RegularPrice": 25000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Restoratives/Consumable/AssassinBait", + "PrimePrice": 200, + "RegularPrice": 125000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Restoratives/Consumable/AssassinBaitB", + "PrimePrice": 200, + "RegularPrice": 125000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroKiTeerDecorationB", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroKiTeerDecorationE", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropCleaningDroneBaro", + "PrimePrice": 700, + "RegularPrice": 500000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroKiTeerBobbleHead", + "PrimePrice": 70, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Hoverboard/HoverboardStickerBaroA", + "PrimePrice": 75, + "RegularPrice": 75000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/KavatBust", + "PrimePrice": 220, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/KubrowBust", + "PrimePrice": 220, + "RegularPrice": 250000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyDesertSkate", + "PrimePrice": 125, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroKiTeerDecorationD", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/ExcaliburArchwingBobbleHead", + "PrimePrice": 90, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Accessories/BaroTiara", + "PrimePrice": 525, + "RegularPrice": 375000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Accessories/EarpieceBaroC", + "PrimePrice": 500, + "RegularPrice": 400000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Accessories/BaroMouthPieceA", + "PrimePrice": 500, + "RegularPrice": 400000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Accessories/BaroVisor", + "PrimePrice": 525, + "RegularPrice": 375000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Accessories/BaroHorn", + "PrimePrice": 525, + "RegularPrice": 375000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Accessories/EarpieceBaroA", + "PrimePrice": 500, + "RegularPrice": 400000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Operator/Accessories/EarpieceBaroB", + "PrimePrice": 250, + "RegularPrice": 200000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Game/QuartersWallpapers/BaroWallpaper", + "PrimePrice": 250, + "RegularPrice": 175000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Liset/InarosLisetSkin", + "PrimePrice": 400, + "RegularPrice": 300000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroKiTeerDecorationA", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Liset/LisetInsectSkinInaros", + "PrimePrice": 425, + "RegularPrice": 320000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Liset/LisetInsectSkinPrimeTrader", + "PrimePrice": 230, + "RegularPrice": 375000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/ParazonPoster", + "PrimePrice": 100, + "RegularPrice": 125000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/KubrowKavatLowPolyPoster", + "PrimePrice": 90, + "RegularPrice": 110000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Liset/LisetSkinVoidTrader", + "PrimePrice": 120, + "RegularPrice": 150000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Liset/LisetBlueSkySkinPrimeTrader", + "PrimePrice": 210, + "RegularPrice": 450000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroKiTeerDecorationF", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Liset/LisetBlueSkySkinInaros", + "PrimePrice": 375, + "RegularPrice": 340000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroKiTeerDecorationG", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropOstRugBaro", + "PrimePrice": 225, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroKiTeerDecorationH", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Liset/Gyroscope/LisetGyroscopeSkinPrimeTrader", + "PrimePrice": 220, + "RegularPrice": 400000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroKiTeerDecorationC", + "PrimePrice": 100, + "RegularPrice": 100000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/PedistalPrime", + "PrimePrice": 0, + "RegularPrice": 1000000 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/Emotes/BaroEmote", + "PrimePrice": 0, + "RegularPrice": 1000000 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/EventSniperReloadDamageMod", + "PrimePrice": 2995, + "RegularPrice": 1000000 + } ] } ], @@ -1120,79 +2976,6 @@ "TwitchPromos": [], "ExperimentRecommended": [], "ForceLogoutVersion": 0, - "SeasonInfo": { - "Activation": { "$date": { "$numberLong": "1715796000000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "AffiliationTag": "RadioLegionIntermission12Syndicate", - "Season": 14, - "Phase": 0, - "Params": "", - "ActiveChallenges": [ - { - "_id": { "$oid": "001300010000000000000008" }, - "Daily": true, - "Activation": { "$date": { "$numberLong": "1715558400000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Challenge": "/Lotus/Types/Challenges/Seasons/Daily/SeasonDailyFeedMeMore" - }, - { - "_id": { "$oid": "001300010000000000000009" }, - "Daily": true, - "Activation": { "$date": { "$numberLong": "1715644800000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Challenge": "/Lotus/Types/Challenges/Seasons/Daily/SeasonDailyTwoForOne" - }, - { - "_id": { "$oid": "001300010000000000000010" }, - "Daily": true, - "Activation": { "$date": { "$numberLong": "1715731200000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Challenge": "/Lotus/Types/Challenges/Seasons/Daily/SeasonDailyKillEnemiesWithFinishers" - }, - { - "_id": { "$oid": "001300010000000000000001" }, - "Activation": { "$date": { "$numberLong": "1715558400000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Challenge": "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" - }, - { - "_id": { "$oid": "001300010000000000000002" }, - "Activation": { "$date": { "$numberLong": "1715558400000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Challenge": "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" - }, - { - "_id": { "$oid": "001300010000000000000003" }, - "Activation": { "$date": { "$numberLong": "1715558400000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Challenge": "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" - }, - { - "_id": { "$oid": "001300010000000000000004" }, - "Activation": { "$date": { "$numberLong": "1715558400000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Challenge": "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyOpenLockers" - }, - { - "_id": { "$oid": "001300010000000000000005" }, - "Activation": { "$date": { "$numberLong": "1715558400000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Challenge": "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyBloodthirsty" - }, - { - "_id": { "$oid": "001300010000000000000006" }, - "Activation": { "$date": { "$numberLong": "1715558400000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Challenge": "/Lotus/Types/Challenges/Seasons/WeeklyHard/SeasonWeeklyHardEliteSanctuaryOnslaught" - }, - { - "_id": { "$oid": "001300010000000000000007" }, - "Activation": { "$date": { "$numberLong": "1715558400000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Challenge": "/Lotus/Types/Challenges/Seasons/WeeklyHard/SeasonWeeklyHardCompleteSortie" - } - ] - }, "KnownCalendarSeasons": [ { "Activation": { "$date": { "$numberLong": "1733961600000" } }, diff --git a/static/webui/index.html b/static/webui/index.html index 0d4b3e95..6468d53a 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -61,6 +61,9 @@ + @@ -89,7 +92,7 @@
- +
@@ -163,10 +166,15 @@
-
+
+
@@ -179,10 +187,15 @@
-
+
+
@@ -193,10 +206,15 @@
-
+
+
@@ -279,6 +297,34 @@
+
+
+
+
+
+ + +
+ + + + +
+
+
+
+
+
@@ -299,6 +345,15 @@
+
+ + +
+
@@ -309,6 +364,13 @@
+
+ + + + + +
@@ -373,7 +435,7 @@ - + @@ -392,7 +454,7 @@
- +
@@ -411,22 +473,31 @@
-
-
-
- - -
+
+
+
+
+
+
+ + +
+ + +
+
+
-
-
-
-
-
- - - - +
+
+
+
+
+ + + +
+
@@ -517,6 +588,42 @@
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
@@ -530,26 +637,18 @@
-

- +
+ + + +
- +
-
-
- - - - - - -
@@ -561,6 +660,7 @@
+
@@ -571,19 +671,52 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/webui/script.js b/static/webui/script.js index 1c55cf4d..849062b2 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -118,6 +118,9 @@ function updateLocElements() { document.querySelectorAll("[data-loc]").forEach(elm => { elm.innerHTML = loc(elm.getAttribute("data-loc")); }); + document.querySelectorAll("[data-loc-placeholder]").forEach(elm => { + elm.placeholder = loc(elm.getAttribute("data-loc-placeholder")); + }); } function setActiveLanguage(lang) { @@ -128,8 +131,12 @@ function setActiveLanguage(lang) { document.querySelector("[data-lang=" + lang + "]").classList.add("active"); window.dictPromise = new Promise(resolve => { - const webui_lang = ["en", "ru"].indexOf(lang) == -1 ? "en" : lang; - const script = document.createElement("script"); + const webui_lang = ["en", "ru", "fr", "de", "zh"].indexOf(lang) == -1 ? "en" : lang; + let script = document.getElementById("translations"); + if (script) document.documentElement.removeChild(script); + + script = document.createElement("script"); + script.id = "translations"; script.src = "/translations/" + webui_lang + ".js"; script.onload = function () { updateLocElements(); @@ -150,6 +157,15 @@ function setLanguage(lang) { } } +const webUiModularWeapons = [ + "/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon", + "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon", + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary", + "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetPowerSuit" +]; + let uniqueLevelCaps = {}; function fetchItemList() { window.itemListPromise = new Promise(resolve => { @@ -157,8 +173,54 @@ function fetchItemList() { req.done(async data => { await dictPromise; + document.querySelectorAll('[id^="datalist-"]').forEach(datalist => { + datalist.innerHTML = ""; + }); + + const syndicateNone = document.createElement("option"); + syndicateNone.setAttribute("data-key", ""); + syndicateNone.value = loc("cheats_none"); + document.getElementById("datalist-Syndicates").appendChild(syndicateNone); + window.archonCrystalUpgrades = data.archonCrystalUpgrades; + // Add mods mising in data sources + data.mods.push({ + uniqueName: "/Lotus/Upgrades/Mods/Fusers/LegendaryModFuser", + name: loc("code_legendaryCore") + }); + data.mods.push({ + uniqueName: "/Lotus/Upgrades/CosmeticEnhancers/Peculiars/CyoteMod", + name: loc("code_traumaticPeculiar") + }); + + // Add modular weapons + data.OperatorAmps.push({ + uniqueName: "/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon", + name: loc("code_amp") + }); + data.Melee.push({ + uniqueName: "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon", + name: loc("code_zaw") + }); + data.LongGuns.push({ + uniqueName: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", + name: loc("code_kitgun") + }); + data.Pistols.push({ + uniqueName: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary", + name: loc("code_kitgun") + }); + data.MoaPets ??= []; + data.MoaPets.push({ + uniqueName: "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit", + name: loc("code_moa") + }); + data.MoaPets.push({ + uniqueName: "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetPowerSuit", + name: loc("code_zanuka") + }); + const itemMap = { // Generics for rivens "/Lotus/Weapons/Tenno/Archwing/Primary/ArchGun": { name: loc("code_archgun") }, @@ -167,26 +229,25 @@ function fetchItemList() { "/Lotus/Weapons/Tenno/Rifle/LotusRifle": { name: loc("code_rifle") }, "/Lotus/Weapons/Tenno/Shotgun/LotusShotgun": { name: loc("code_shotgun") }, // Modular weapons - "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary": { name: loc("code_kitgun") }, "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam": { name: loc("code_kitgun") }, "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryLauncher": { name: loc("code_kitgun") }, "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun": { name: loc("code_kitgun") }, "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimarySniper": { name: loc("code_kitgun") }, - "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary": { name: loc("code_kitgun") }, "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam": { name: loc("code_kitgun") }, "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun": { name: loc("code_kitgun") }, - "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon": { name: loc("code_zaw") }, "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/OperatorTrainingAmpWeapon": { name: loc("code_moteAmp") }, - "/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon": { name: loc("code_amp") }, - "/Lotus/Weapons/Operator/Pistols/DrifterPistol/DrifterPistolPlayerWeapon": { - name: loc("code_sirocco") + "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit": { name: loc("code_kDrive") }, + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit": { + name: loc("code_zanukaA") }, - "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit": { name: loc("code_kdrive") }, - // Missing in data sources - "/Lotus/Upgrades/Mods/Fusers/LegendaryModFuser": { name: loc("code_legendaryCore") }, - "/Lotus/Upgrades/CosmeticEnhancers/Peculiars/CyoteMod": { name: loc("code_traumaticPeculiar") } + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit": { + name: loc("code_zanukaB") + }, + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit": { + name: loc("code_zanukaC") + } }; for (const [type, items] of Object.entries(data)) { if (type == "archonCrystalUpgrades") { @@ -198,20 +259,62 @@ function fetchItemList() { }); } else if (type == "uniqueLevelCaps") { uniqueLevelCaps = items; - } else if (type == "Syndicates") { + } else { items.forEach(item => { - if (item.uniqueName.startsWith("RadioLegion")) item.name += " (" + item.uniqueName + ")"; - const option = document.createElement("option"); - option.value = item.uniqueName; - option.innerHTML = item.name; - document.getElementById("changeSyndicate").appendChild(option); - itemMap[item.uniqueName] = { ...item, type }; - }); - } else if (type != "badItems") { - items.forEach(item => { - if (item.uniqueName in data.badItems) { - item.name += " " + loc("code_badItem"); - } else if (item.uniqueName.substr(0, 18) != "/Lotus/Types/Game/") { + if (item.name.includes(" ")) { + item.name = item.name.replace(" ", ""); + } + if ("badReason" in item) { + if (item.badReason == "starter") { + item.name = loc("code_starter").split("|MOD|").join(item.name); + } else { + item.name += " " + loc("code_badItem"); + } + } + if (type == "Syndicates" && item.uniqueName.startsWith("RadioLegion")) { + item.name += " (" + item.uniqueName + ")"; + } + if (type == "ModularParts") { + const supportedModularParts = [ + "LWPT_HB_DECK", + "LWPT_HB_ENGINE", + "LWPT_HB_FRONT", + "LWPT_HB_JET", + "LWPT_AMP_OCULUS", + "LWPT_AMP_CORE", + "LWPT_AMP_BRACE", + "LWPT_BLADE", + "LWPT_HILT", + "LWPT_HILT_WEIGHT", + "LWPT_GUN_PRIMARY_HANDLE", + "LWPT_GUN_SECONDARY_HANDLE", + "LWPT_GUN_BARREL", + "LWPT_GUN_CLIP", + "LWPT_MOA_ENGINE", + "LWPT_MOA_PAYLOAD", + "LWPT_MOA_HEAD", + "LWPT_MOA_LEG", + "LWPT_ZANUKA_BODY", + "LWPT_ZANUKA_HEAD", + "LWPT_ZANUKA_LEG", + "LWPT_ZANUKA_TAIL" + ]; + if (supportedModularParts.includes(item.partType)) { + const option = document.createElement("option"); + option.setAttribute("data-key", item.uniqueName); + option.value = item.name; + document + .getElementById("datalist-" + type + "-" + item.partType.slice(5)) + .appendChild(option); + } else { + console.log(item.partType); + const option = document.createElement("option"); + option.setAttribute("data-key", item.uniqueName); + option.value = item.name; + document.getElementById("datalist-" + type).appendChild(option); + } + } + if (item.badReason != "notraw") { const option = document.createElement("option"); option.setAttribute("data-key", item.uniqueName); option.value = item.name; @@ -233,6 +336,24 @@ function updateInventory() { window.itemListPromise.then(itemMap => { window.didInitialInventoryUpdate = true; + const modularWeapons = [ + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam", + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryLauncher", + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun", + "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimarySniper", + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary", + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam", + "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun", + "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon", + "/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon", + "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit", + "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit" + ]; + // Populate inventory route ["RegularCredits", "PremiumCredits", "FusionPoints", "PrimeTokens"].forEach(currency => { document.getElementById(currency + "-owned").textContent = loc("currency_owned") @@ -252,7 +373,8 @@ function updateInventory() { "SentinelWeapons", "Hoverboards", "OperatorAmps", - "MechSuits" + "MechSuits", + "MoaPets" ].forEach(category => { document.getElementById(category + "-list").innerHTML = ""; data[category].forEach(item => { @@ -319,6 +441,17 @@ function updateInventory() { a.innerHTML = ``; td.appendChild(a); } + if (!(item.Features & 8) && modularWeapons.includes(item.ItemType)) { + const a = document.createElement("a"); + a.href = "#"; + a.onclick = function (event) { + event.preventDefault(); + gildEquipment(category, item.ItemId.$oid); + }; + a.title = loc("code_gild"); + a.innerHTML = ``; + td.appendChild(a); + } if (category == "Suits") { const a = document.createElement("a"); a.href = "/webui/powersuit/" + item.ItemId.$oid; @@ -356,6 +489,132 @@ function updateInventory() { }); }); + // Populate quests route + document.getElementById("QuestKeys-list").innerHTML = ""; + data.QuestKeys.forEach(item => { + const tr = document.createElement("tr"); + tr.setAttribute("data-item-type", item.ItemType); + const stage = item.Progress?.length ?? 0; + + const datalist = document.getElementById("datalist-QuestKeys"); + const optionToRemove = datalist.querySelector(`option[data-key="${item.ItemType}"]`); + if (optionToRemove) { + datalist.removeChild(optionToRemove); + } + + { + const td = document.createElement("td"); + td.textContent = itemMap[item.ItemType]?.name ?? item.ItemType; + if (!item.Completed) { + td.textContent += + " | " + loc("code_stage") + ": [" + stage + "/" + itemMap[item.ItemType].chainLength + "]"; + } else { + td.textContent += " | " + loc("code_completed"); + } + + if (data.ActiveQuest == item.ItemType) td.textContent += " | " + loc("code_active"); + tr.appendChild(td); + } + { + const td = document.createElement("td"); + td.classList = "text-end text-nowrap"; + if (data.ActiveQuest == item.ItemType && !item.Completed) { + console.log(data.ActiveQuest); + + const a = document.createElement("a"); + a.href = "#"; + a.onclick = function (event) { + event.preventDefault(); + doQuestUpdate("setInactive", item.ItemType); + }; + a.title = loc("code_setInactive"); + a.innerHTML = ``; + td.appendChild(a); + } + if (stage > 0) { + const a = document.createElement("a"); + a.href = "#"; + a.onclick = function (event) { + event.preventDefault(); + doQuestUpdate("resetKey", item.ItemType); + }; + a.title = loc("code_reset"); + a.innerHTML = ``; + td.appendChild(a); + } + if (itemMap[item.ItemType].chainLength > stage && !item.Completed) { + const a = document.createElement("a"); + a.href = "#"; + a.onclick = function (event) { + event.preventDefault(); + doQuestUpdate("completeKey", item.ItemType); + }; + a.title = loc("code_complete"); + a.innerHTML = ``; + td.appendChild(a); + } + if (stage > 0 && itemMap[item.ItemType].chainLength > 1) { + const a = document.createElement("a"); + a.href = "#"; + a.onclick = function (event) { + event.preventDefault(); + doQuestUpdate("prevStage", item.ItemType); + }; + a.title = loc("code_prevStage"); + a.innerHTML = ``; + td.appendChild(a); + } + if ( + itemMap[item.ItemType].chainLength > stage && + !item.Completed && + itemMap[item.ItemType].chainLength > 1 + ) { + const a = document.createElement("a"); + a.href = "#"; + a.onclick = function (event) { + event.preventDefault(); + doQuestUpdate("nextStage", item.ItemType); + }; + a.title = loc("code_nextStage"); + a.innerHTML = ``; + td.appendChild(a); + } + { + const a = document.createElement("a"); + a.href = "#"; + a.onclick = function (event) { + event.preventDefault(); + const option = document.createElement("option"); + option.setAttribute("data-key", item.ItemType); + option.value = itemMap[item.ItemType]?.name ?? item.ItemType; + document.getElementById("datalist-QuestKeys").appendChild(option); + doQuestUpdate("deleteKey", item.ItemType); + }; + a.title = loc("code_remove"); + a.innerHTML = ``; + td.appendChild(a); + } + tr.appendChild(td); + } + document.getElementById("QuestKeys-list").appendChild(tr); + }); + + const datalistQuestKeys = document.querySelectorAll("#datalist-QuestKeys option"); + const form = document.querySelector("form[onsubmit*=\"doAcquireEquipment('QuestKeys')\"]"); + const giveAllQuestButton = document.querySelector("button[onclick*=\"doBulkQuestUpdate('giveAll')\"]"); + + if (datalistQuestKeys.length === 0) { + form.classList.add("disabled"); + form.querySelector("input").disabled = true; + form.querySelector("button").disabled = true; + giveAllQuestButton.disabled = true; + } else { + form.classList.remove("disabled"); + form.querySelector("input").disabled = false; + form.querySelector("button").disabled = false; + giveAllQuestButton.disabled = false; + } + // Populate mods route document.getElementById("riven-list").innerHTML = ""; document.getElementById("mods-list").innerHTML = ""; @@ -386,7 +645,7 @@ function updateInventory() { " ⟳ " + - parseInt(fingerprint.rerolls) + + (fingerprint.rerolls ?? 0) + ""; tr.appendChild(td); } @@ -523,7 +782,7 @@ function updateInventory() { const uniqueUpgrades = {}; (item.ArchonCrystalUpgrades ?? []).forEach(upgrade => { - if (upgrade) { + if (upgrade && upgrade.UpgradeType) { uniqueUpgrades[upgrade.UpgradeType] ??= 0; uniqueUpgrades[upgrade.UpgradeType] += 1; } @@ -559,8 +818,10 @@ function updateInventory() { single.loadRoute("/webui/inventory"); } } - - document.getElementById("changeSyndicate").value = data.SupportedSyndicate ?? ""; + document.getElementById("changeSyndicate").value = + [...document.querySelectorAll("#datalist-Syndicates option")].find( + option => option.getAttribute("data-key") === (data.SupportedSyndicate ?? "") + )?.value ?? loc("cheats_none"); }); }); } @@ -598,6 +859,80 @@ function doAcquireEquipment(category) { }); } +function doAcquireModularEquipment(category, ItemType) { + let requiredParts; + let ModularParts = []; + switch (category) { + case "HoverBoards": + ItemType = "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit"; + requiredParts = ["HB_DECK", "HB_ENGINE", "HB_FRONT", "HB_JET"]; + break; + case "OperatorAmps": + requiredParts = ["AMP_OCULUS", "AMP_CORE", "AMP_BRACE"]; + break; + case "Melee": + requiredParts = ["BLADE", "HILT", "HILT_WEIGHT"]; + break; + case "LongGuns": + requiredParts = ["GUN_BARREL", "GUN_PRIMARY_HANDLE", "GUN_CLIP"]; + break; + case "Pistols": + requiredParts = ["GUN_BARREL", "GUN_SECONDARY_HANDLE", "GUN_CLIP"]; + break; + case "MoaPets": + if (ItemType == "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit") { + requiredParts = ["MOA_ENGINE", "MOA_PAYLOAD", "MOA_HEAD", "MOA_LEG"]; + } else { + requiredParts = ["ZANUKA_BODY", "ZANUKA_HEAD", "ZANUKA_LEG", "ZANUKA_TAIL"]; + } + break; + } + requiredParts.forEach(part => { + const partName = getKey(document.getElementById("acquire-type-" + category + "-" + part)); + if (partName) { + ModularParts.push(partName); + } + }); + if (ModularParts.length != requiredParts.length) { + let isFirstPart = true; + requiredParts.forEach(part => { + const partSelector = document.getElementById("acquire-type-" + category + "-" + part); + if (!getKey(partSelector)) { + if (isFirstPart) { + isFirstPart = false; + $("#acquire-type-" + category + "-" + part) + .addClass("is-invalid") + .focus(); + } else { + $("#acquire-type-" + category + "-" + part).addClass("is-invalid"); + } + } + }); + } else { + revalidateAuthz(() => { + const req = $.post({ + url: "/custom/addModularEquipment?" + window.authz, + contentType: "application/json", + data: JSON.stringify({ + ItemType, + ModularParts + }) + }); + req.done(() => { + const mainInput = document.getElementById("acquire-type-" + category); + if (mainInput) { + mainInput.value = ""; + document.getElementById("modular-" + category).style.display = "none"; + } + requiredParts.forEach(part => { + document.getElementById("acquire-type-" + category + "-" + part).value = ""; + }); + updateInventory(); + }); + }); + } +} + $("input[list]").on("input", function () { $(this).removeClass("is-invalid"); }); @@ -624,7 +959,9 @@ function addMissingEquipment(categories) { "#" + category + "-list [data-item-type='" + elm.getAttribute("data-key") + "']" ) ) { - requests.push({ ItemType: elm.getAttribute("data-key"), ItemCount: 1 }); + if (!webUiModularWeapons.includes(elm.getAttribute("data-key"))) { + requests.push({ ItemType: elm.getAttribute("data-key"), ItemCount: 1 }); + } } }); }); @@ -661,14 +998,12 @@ function maxRankAllEquipment(categories) { } if (category === "Suits") { if ("exalted" in itemMap[item.ItemType]) { - if (!batchData["SpecialItems"]) { - batchData["SpecialItems"] = []; - } for (const exaltedType of itemMap[item.ItemType].exalted) { const exaltedItem = data["SpecialItems"].find(x => x.ItemType == exaltedType); if (exaltedItem) { const exaltedCap = itemMap[exaltedType]?.type == "weapons" ? 800_000 : 1_600_000; if (exaltedItem.XP < exaltedCap) { + batchData["SpecialItems"] ??= []; batchData["SpecialItems"].push({ ItemId: { $oid: exaltedItem.ItemId.$oid }, XP: exaltedCap @@ -685,7 +1020,7 @@ function maxRankAllEquipment(categories) { return sendBatchGearExp(batchData); } - alert(loc("code_noEquipmentToRankUp")); + toast(loc("code_noEquipmentToRankUp")); }); }); } @@ -718,6 +1053,7 @@ function sendBatchGearExp(data) { contentType: "application/json", data: JSON.stringify(data) }).done(() => { + toast(loc("code_succRankUp")); updateInventory(); }); }); @@ -783,27 +1119,48 @@ function disposeOfItems(category, type, count) { }); } +function gildEquipment(category, oid) { + revalidateAuthz(() => { + $.post({ + url: "/custom/gildEquipment?" + window.authz, + contentType: "application/json", + data: JSON.stringify({ + ItemId: oid, + Category: category + }) + }).done(function () { + updateInventory(); + }); + }); +} + function doAcquireMiscItems() { - const data = getKey(document.getElementById("miscitem-type")); - if (!data) { + const uniqueName = getKey(document.getElementById("miscitem-type")); + if (!uniqueName) { $("#miscitem-type").addClass("is-invalid").focus(); return; } - const [category, uniqueName] = data.split(":"); - revalidateAuthz(() => { - $.post({ - url: "/custom/addItems?" + window.authz, - contentType: "application/json", - data: JSON.stringify([ - { - ItemType: uniqueName, - ItemCount: parseInt($("#miscitem-count").val()) + const count = parseInt($("#miscitem-count").val()); + if (count != 0) { + revalidateAuthz(() => { + $.post({ + url: "/custom/addItems?" + window.authz, + contentType: "application/json", + data: JSON.stringify([ + { + ItemType: uniqueName, + ItemCount: count + } + ]) + }).done(function () { + if (count > 0) { + toast(loc("code_succAdded")); + } else { + toast(loc("code_succRemoved")); } - ]) - }).done(function () { - alert(loc("code_succAdded")); + }); }); - }); + } } function doAcquireRiven() { @@ -938,6 +1295,9 @@ function doChangeSettings() { url: "/custom/config?" + window.authz, contentType: "text/plain", data: JSON.stringify(json, null, 2) + }).then(() => { + // A few cheats affect the inventory response which in turn may change what values we need to show + updateInventory(); }); }); } @@ -951,6 +1311,7 @@ single.getRoute("/webui/cheats").on("beforeload", function () { clearInterval(interval); fetch("/custom/config?" + window.authz).then(res => { if (res.status == 200) { + $("#server-settings-no-perms").addClass("d-none"); $("#server-settings").removeClass("d-none"); res.json().then(json => Object.entries(json).forEach(entry => { @@ -969,6 +1330,7 @@ single.getRoute("/webui/cheats").on("beforeload", function () { ); } else { $("#server-settings-no-perms").removeClass("d-none"); + $("#server-settings").addClass("d-none"); } }); } @@ -996,9 +1358,9 @@ function doUnlockAllFocusSchools() { await unlockFocusSchool(upgradeType); } if (Object.keys(missingFocusUpgrades).length == 0) { - alert(loc("code_focusAllUnlocked")); + toast(loc("code_focusAllUnlocked")); } else { - alert(loc("code_focusUnlocked").split("|COUNT|").join(Object.keys(missingFocusUpgrades).length)); + toast(loc("code_focusUnlocked").split("|COUNT|").join(Object.keys(missingFocusUpgrades).length)); } }); }); @@ -1032,6 +1394,12 @@ function doHelminthUnlockAll() { }); } +function doIntrinsicsUnlockAll() { + revalidateAuthz(() => { + $.get("/custom/unlockAllIntrinsics?" + window.authz); + }); +} + function doAddAllMods() { let modsAll = new Set(); for (const child of document.getElementById("datalist-mods").children) { @@ -1126,14 +1494,15 @@ function doImport() { inventory: JSON.parse($("#import-inventory").val()) }) }).then(function () { - alert(loc("code_succImport")); + toast(loc("code_succImport")); updateInventory(); }); }); } function doChangeSupportedSyndicate() { - const uniqueName = document.getElementById("changeSyndicate").value; + const uniqueName = getKey(document.getElementById("changeSyndicate")); + revalidateAuthz(() => { $.get("/api/setSupportedSyndicate.php?" + window.authz + "&syndicate=" + uniqueName).done(function () { updateInventory(); @@ -1154,7 +1523,16 @@ function doAddCurrency(currency) { }); } -function doQuestUpdate(operation) { +function doQuestUpdate(operation, itemType) { + $.post({ + url: "/custom/manageQuests?" + window.authz + "&operation=" + operation + "&itemType=" + itemType, + contentType: "application/json" + }).then(function () { + updateInventory(); + }); +} + +function doBulkQuestUpdate(operation) { $.post({ url: "/custom/manageQuests?" + window.authz + "&operation=" + operation, contentType: "application/json" @@ -1162,3 +1540,62 @@ function doQuestUpdate(operation) { updateInventory(); }); } + +function toast(text) { + const toast = document.createElement("div"); + toast.className = "toast align-items-center text-bg-primary border-0"; + const div = document.createElement("div"); + div.className = "d-flex"; + const body = document.createElement("div"); + body.className = "toast-body"; + body.textContent = text; + div.appendChild(body); + const button = document.createElement("button"); + button.className = "btn-close btn-close-white me-2 m-auto"; + button.setAttribute("data-bs-dismiss", "toast"); + div.appendChild(button); + toast.appendChild(div); + new bootstrap.Toast(document.querySelector(".toast-container").appendChild(toast)).show(); +} + +function handleModularSelection(category) { + const itemType = getKey(document.getElementById("acquire-type-" + category)); + + if (webUiModularWeapons.includes(itemType)) { + doAcquireModularEquipment(category, itemType); + } else { + doAcquireEquipment(category); + } +} +{ + const supportedModularInventoryCategory = ["OperatorAmps", "Melee", "LongGuns", "Pistols", "MoaPets"]; + supportedModularInventoryCategory.forEach(inventoryCategory => { + document.getElementById("acquire-type-" + inventoryCategory).addEventListener("input", function () { + const modularFields = document.getElementById("modular-" + inventoryCategory); + const modularFieldsZanuka = + inventoryCategory === "MoaPets" + ? document.getElementById("modular-" + inventoryCategory + "-Zanuka") + : null; + const key = getKey(this); + + if (webUiModularWeapons.includes(key)) { + if (key === "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetPowerSuit" && modularFieldsZanuka) { + modularFields.style.display = "none"; + modularFieldsZanuka.style.display = ""; + } else if (key === "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit") { + modularFields.style.display = ""; + if (modularFieldsZanuka) { + modularFieldsZanuka.style.display = "none"; + } + } else { + modularFields.style.display = ""; + } + } else { + modularFields.style.display = "none"; + if (modularFieldsZanuka) { + modularFieldsZanuka.style.display = "none"; + } + } + }); + }); +} diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js new file mode 100644 index 00000000..1fffac3a --- /dev/null +++ b/static/webui/translations/de.js @@ -0,0 +1,155 @@ +// German translation by Animan8000 +dict = { + general_inventoryUpdateNote: `Hinweis: Änderungen, die hier vorgenommen werden, werden erst im Spiel angewendet, sobald das Inventar synchronisiert wird. Die Sternenkarte zu besuchen, sollte der einfachste Weg sein, dies auszulösen.`, + general_addButton: `Hinzufügen`, + general_bulkActions: `Massenaktionen`, + code_nonValidAuthz: `Deine Anmeldedaten sind nicht mehr gültig.`, + code_changeNameConfirm: `In welchen Namen möchtest du deinen Account umbenennen?`, + code_deleteAccountConfirm: `Bist du sicher, dass du deinen Account |DISPLAYNAME| (|EMAIL|) löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.`, + code_archgun: `Arch-Gewehr`, + code_melee: `Nahkampf`, + code_pistol: `Pistole`, + code_rifle: `Gewehr`, + code_shotgun: `Schrotflinte`, + code_kitgun: `Kitgun`, + code_zaw: `Zaw`, + code_moteAmp: `Anfangsverstärker`, + code_amp: `Verstärker`, + code_kDrive: `K-Drive`, + code_legendaryCore: `Legendärer Kern`, + code_traumaticPeculiar: `Kuriose Mod: Traumatisch`, + code_starter: `|MOD| (Defekt)`, + code_badItem: `(Fälschung)`, + code_maxRank: `Max. Rang`, + code_rename: `Umbenennen`, + code_renamePrompt: `Neuen benutzerdefinierten Namen eingeben:`, + code_remove: `Entfernen`, + code_addItemsConfirm: `Bist du sicher, dass du |COUNT| Gegenstände zu deinem Account hinzufügen möchtest?`, + code_succRankUp: `Erfolgreich aufgestiegen.`, + code_noEquipmentToRankUp: `Keine Ausstattung zum Rangaufstieg verfügbar.`, + code_succAdded: `Erfolgreich hinzugefügt.`, + code_succRemoved: `Erfolgreich entfernt.`, + code_buffsNumber: `Anzahl der Buffs`, + code_cursesNumber: `Anzahl der Flüche`, + code_rerollsNumber: `Anzahl der Umrollversuche`, + code_viewStats: `Statistiken anzeigen`, + code_rank: `Rang`, + code_count: `Anzahl`, + code_focusAllUnlocked: `Alle Fokus-Schulen sind bereits freigeschaltet.`, + code_focusUnlocked: `|COUNT| neue Fokus-Schulen freigeschaltet! Ein Inventar-Update wird benötigt, damit die Änderungen im Spiel sichtbar werden. Die Sternenkarte zu besuchen, sollte der einfachste Weg sein, dies auszulösen.`, + code_addModsConfirm: `Bist du sicher, dass du |COUNT| Mods zu deinem Account hinzufügen möchtest?`, + code_succImport: `Erfolgreich importiert.`, + code_gild: `Veredeln`, + code_moa: `Moa`, + code_zanuka: `Jagdhund`, + code_zanukaA: `Jagdhund: Dorma`, + code_zanukaB: `Jagdhund: Bhaira`, + code_zanukaC: `Jagdhund: Hec`, + code_stage: `Abschnitt`, + code_complete: `Abschließen`, + code_nextStage: `Nächster Abschnitt`, + code_prevStage: `Vorheriger Abschnitt`, + code_reset: `Zurücksetzen`, + code_setInactive: `Quest inaktiv setzen`, + code_completed: `Abgeschlossen`, + code_active: `Aktiv`, + login_description: `Melde dich mit deinem OpenWF-Account an (denselben Angaben wie im Spiel, wenn du dich mit diesem Server verbindest).`, + login_emailLabel: `E-Mail-Adresse`, + login_passwordLabel: `Passwort`, + login_loginButton: `Anmelden`, + navbar_logout: `Abmelden`, + navbar_renameAccount: `Account umbenennen`, + navbar_deleteAccount: `Account löschen`, + navbar_inventory: `Inventar`, + navbar_mods: `Mods`, + navbar_quests: `Quests`, + navbar_cheats: `Cheats`, + navbar_import: `Importieren`, + inventory_addItems: `Gegenstände hinzufügen`, + inventory_suits: `Warframes`, + inventory_longGuns: `Primärwaffen`, + inventory_pistols: `Sekundärwaffen`, + inventory_melee: `Nahkampfwaffen`, + inventory_spaceSuits: `Archwings`, + inventory_spaceGuns: `Archwing Primärwaffen`, + inventory_spaceMelee: `Archwing Nahkampfwaffen`, + inventory_mechSuits: `Necramechs`, + inventory_sentinels: `Wächter`, + inventory_sentinelWeapons: `Wächter-Waffen`, + inventory_operatorAmps: `Verstärker`, + inventory_hoverboards: `K-Drives`, + inventory_moaPets: `Moa`, + inventory_bulkAddSuits: `Fehlende Warframes hinzufügen`, + inventory_bulkAddWeapons: `Fehlende Waffen hinzufügen`, + inventory_bulkAddSpaceSuits: `Fehlende Archwings hinzufügen`, + inventory_bulkAddSpaceWeapons: `Fehlende Archwing-Waffen hinzufügen`, + inventory_bulkAddSentinels: `Fehlende Wächter hinzufügen`, + inventory_bulkAddSentinelWeapons: `Fehlende Wächter-Waffen hinzufügen`, + inventory_bulkRankUpSuits: `Alle Warframes auf Max. Rang`, + inventory_bulkRankUpWeapons: `Alle Waffen auf Max. Rang`, + inventory_bulkRankUpSpaceSuits: `Alle Archwings auf Max. Rang`, + inventory_bulkRankUpSpaceWeapons: `Alle Archwing-Waffen auf Max. Rang`, + inventory_bulkRankUpSentinels: `Alle Wächter auf Max. Rang`, + inventory_bulkRankUpSentinelWeapons: `Alle Wächter-Waffen auf Max. Rang`, + + quests_list: `Quests`, + quests_completeAll: `Alle Quests abschließen`, + quests_resetAll: `Alle Quests zurücksetzen`, + quests_giveAll: `Alle Quests erhalten`, + + currency_RegularCredits: `Credits`, + currency_PremiumCredits: `Platinum`, + currency_FusionPoints: `Endo`, + currency_PrimeTokens: `Reines Aya`, + currency_owned: `Du hast |COUNT|.`, + powersuit_archonShardsLabel: `Archon-Scherben-Slots`, + powersuit_archonShardsDescription: `Du kannst diese unbegrenzten Slots nutzen, um eine Vielzahl von Verbesserungen anzuwenden.`, + mods_addRiven: `Riven hinzufügen`, + mods_fingerprint: `Fingerabdruck`, + mods_fingerprintHelp: `Benötigst du Hilfe mit dem Fingerabdruck?`, + mods_rivens: `Rivens`, + mods_mods: `Mods`, + mods_bulkAddMods: `Fehlende Mods hinzufügen`, + cheats_administratorRequirement: `Du musst Administrator sein, um diese Funktion nutzen zu können. Um Administrator zu werden, füge |DISPLAYNAME| zu administratorNames in der config.json hinzu.`, + cheats_server: `Server`, + cheats_skipTutorial: `Tutorial überspringen`, + cheats_skipAllDialogue: `Alle Dialoge überspringen`, + cheats_unlockAllScans: `Alle Scans freischalten`, + cheats_unlockAllMissions: `Alle Missionen freischalten`, + cheats_infiniteCredits: `Unendlich Credits`, + cheats_infinitePlatinum: `Unendlich Platinum`, + cheats_infiniteEndo: `Unendlich Endo`, + cheats_infiniteRegalAya: `Unendlich Reines Aya`, + cheats_infiniteHelminthMaterials: `Unendlich Helminth-Materialien`, + cheats_unlockAllShipFeatures: `Alle Schiffs-Funktionen freischalten`, + cheats_unlockAllShipDecorations: `Alle Schiffsdekorationen freischalten`, + cheats_unlockAllFlavourItems: `Alle Sammlerstücke freischalten`, + cheats_unlockAllSkins: `Alle Skins freischalten`, + cheats_unlockAllCapturaScenes: `Alle Photora-Szenen freischalten`, + cheats_universalPolarityEverywhere: `Universelle Polarität überall`, + cheats_unlockDoubleCapacityPotatoesEverywhere: `Orokin Reaktor & Beschleuniger überall`, + cheats_unlockExilusEverywhere: `Exilus-Adapter überall`, + cheats_unlockArcanesEverywhere: `Arkana-Adapter überall`, + cheats_noDailyStandingLimits: `Kein tägliches Ansehenslimit`, + cheats_noArgonCrystalDecay: `Argon-Kristalle verschwinden niemals`, + cheats_noMasteryRankUpCooldown: `Keine Wartezeit beim Meisterschaftsrangaufstieg`, + cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, + cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, + cheats_noDojoRoomBuildStage: `Kein Dojo-Raum-Bauvorgang`, + cheats_fastDojoRoomDestruction: `Schnelle Dojo-Raum-Zerstörung`, + cheats_noDojoResearchCosts: `Keine Dojo-Forschungskosten`, + cheats_noDojoResearchTime: `Keine Dojo-Forschungszeit`, + cheats_fastClanAscension: `Schneller Clan-Aufstieg`, + cheats_spoofMasteryRank: `Gefälschter Meisterschaftsrang (-1 zum deaktivieren)`, + cheats_saveSettings: `Einstellungen speichern`, + cheats_account: `Account`, + cheats_unlockAllFocusSchools: `Alle Fokus-Schulen freischalten`, + cheats_helminthUnlockAll: `Helminth vollständig aufleveln`, + cheats_intrinsicsUnlockAll: `Alle Inhärenzen auf Max. Rang`, + cheats_changeSupportedSyndicate: `Unterstütztes Syndikat`, + cheats_changeButton: `Ändern`, + cheats_none: `Keines`, + import_importNote: `Du kannst hier eine vollständige oder teilweise Inventarantwort (Client-Darstellung) einfügen. Alle Felder, die vom Importer unterstützt werden, werden in deinem Account überschrieben.`, + import_submit: `Absenden`, + prettier_sucks_ass: `` +}; diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 3251cd63..798c69db 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -14,18 +14,20 @@ dict = { code_zaw: `Zaw`, code_moteAmp: `Mote Amp`, code_amp: `Amp`, - code_sirocco: `Sirocco`, code_kDrive: `K-Drive`, code_legendaryCore: `Legendary Core`, code_traumaticPeculiar: `Traumatic Peculiar`, + code_starter: `|MOD| (Flawed)`, code_badItem: `(Imposter)`, code_maxRank: `Max Rank`, code_rename: `Rename`, code_renamePrompt: `Enter new custom name:`, code_remove: `Remove`, code_addItemsConfirm: `Are you sure you want to add |COUNT| items to your account?`, + code_succRankUp: `Successfully ranked up.`, code_noEquipmentToRankUp: `No equipment to rank up.`, code_succAdded: `Successfully added.`, + code_succRemoved: `Successfully removed.`, code_buffsNumber: `Number of buffs`, code_cursesNumber: `Number of curses`, code_rerollsNumber: `Number of rerolls`, @@ -36,6 +38,20 @@ dict = { code_focusUnlocked: `Unlocked |COUNT| new focus schools! An inventory update will be needed for the changes to be reflected in-game. Visiting the navigation should be the easiest way to trigger that.`, code_addModsConfirm: `Are you sure you want to add |COUNT| mods to your account?`, code_succImport: `Successfully imported.`, + code_gild: `Gild`, + code_moa: `Moa`, + code_zanuka: `Hound`, + code_zanukaA: `Dorma Hound`, + code_zanukaB: `Bhaira Hound`, + code_zanukaC: `Hec Hound`, + code_stage: `Stage`, + code_complete: `Complete`, + code_nextStage: `Next stage`, + code_prevStage: `Previous stage`, + code_reset: `Reset`, + code_setInactive: `Make the quest inactive`, + code_completed: `Completed`, + code_active: `Active`, login_description: `Login using your OpenWF account credentials (same as in-game when connecting to this server).`, login_emailLabel: `Email address`, login_passwordLabel: `Password`, @@ -61,6 +77,7 @@ dict = { inventory_sentinelWeapons: `Sentinel Weapons`, inventory_operatorAmps: `Amps`, inventory_hoverboards: `K-Drives`, + inventory_moaPets: `Moa`, inventory_bulkAddSuits: `Add Missing Warframes`, inventory_bulkAddWeapons: `Add Missing Weapons`, inventory_bulkAddSpaceSuits: `Add Missing Archwings`, @@ -74,6 +91,11 @@ dict = { inventory_bulkRankUpSentinels: `Max Rank All Sentinels`, inventory_bulkRankUpSentinelWeapons: `Max Rank All Sentinel Weapons`, + quests_list: `Quests`, + quests_completeAll: `Complete All Quests`, + quests_resetAll: `Reset All Quests`, + quests_giveAll: `Give All Quests`, + currency_RegularCredits: `Credits`, currency_PremiumCredits: `Platinum`, currency_FusionPoints: `Endo`, @@ -93,8 +115,6 @@ dict = { cheats_skipAllDialogue: `Skip All Dialogue`, cheats_unlockAllScans: `Unlock All Scans`, cheats_unlockAllMissions: `Unlock All Missions`, - cheats_unlockAllQuests: `Unlock All Quests`, - cheats_completeAllQuests: `Complete All Quests`, cheats_infiniteCredits: `Infinite Credits`, cheats_infinitePlatinum: `Infinite Platinum`, cheats_infiniteEndo: `Infinite Endo`, @@ -102,7 +122,7 @@ dict = { cheats_infiniteHelminthMaterials: `Infinite Helminth Materials`, cheats_unlockAllShipFeatures: `Unlock All Ship Features`, cheats_unlockAllShipDecorations: `Unlock All Ship Decorations`, - cheats_unlockAllFlavourItems: `Unlock All Flavor Items`, + cheats_unlockAllFlavourItems: `Unlock All Flavor Items`, cheats_unlockAllSkins: `Unlock All Skins`, cheats_unlockAllCapturaScenes: `Unlock All Captura Scenes`, cheats_universalPolarityEverywhere: `Universal Polarity Everywhere`, @@ -110,20 +130,25 @@ dict = { cheats_unlockExilusEverywhere: `Exilus Adapters Everywhere`, cheats_unlockArcanesEverywhere: `Arcane Adapters Everywhere`, cheats_noDailyStandingLimits: `No Daily Standing Limits`, + cheats_noArgonCrystalDecay: `No Argon Crystal Decay`, + cheats_noMasteryRankUpCooldown: `No Mastery Rank Up Cooldown`, + cheats_noVendorPurchaseLimits: `No Vendor Purchase Limits`, + cheats_instantResourceExtractorDrones: `Instant Resource Extractor Drones`, + cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, + cheats_fastDojoRoomDestruction: `Fast Dojo Room Destruction`, + cheats_noDojoResearchCosts: `No Dojo Research Costs`, + cheats_noDojoResearchTime: `No Dojo Research Time`, + cheats_fastClanAscension: `Fast Clan Ascension`, cheats_spoofMasteryRank: `Spoofed Mastery Rank (-1 to disable)`, cheats_saveSettings: `Save Settings`, cheats_account: `Account`, cheats_unlockAllFocusSchools: `Unlock All Focus Schools`, cheats_helminthUnlockAll: `Fully Level Up Helminth`, + cheats_intrinsicsUnlockAll: `Max Rank All Intrinsics`, cheats_changeSupportedSyndicate: `Supported syndicate`, cheats_changeButton: `Change`, cheats_none: `None`, - cheats_quests: `Quests`, - cheats_quests_unlockAll: `Unlock All Quests`, - cheats_quests_completeAll: `Complete All Quests`, - cheats_quests_completeAllUnlocked: `Complete All Unlocked Quests`, - cheats_quests_resetAll: `Reset All Quests`, - cheats_quests_giveAll: `Give All Quests`, import_importNote: `You can provide a full or partial inventory response (client respresentation) here. All fields that are supported by the importer will be overwritten in your account.`, - import_submit: `Submit` + import_submit: `Submit`, + prettier_sucks_ass: `` }; diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js new file mode 100644 index 00000000..eb635c5a --- /dev/null +++ b/static/webui/translations/fr.js @@ -0,0 +1,155 @@ +// French translation by Vitruvio +dict = { + general_inventoryUpdateNote: `Note : Les changements effectués ici seront appliqués lors de la syncrhonisation. Visiter la navigation appliquera les changements apportés à l'inventaire.`, + general_addButton: `Ajouter`, + general_bulkActions: `Action groupée`, + code_nonValidAuthz: `Informations de connexion invalides`, + code_changeNameConfirm: `Nouveau nom du compte :`, + code_deleteAccountConfirm: `Supprimer |DISPLAYNAME| (|EMAIL|) ? Cette action est irreversible.`, + code_archgun: `Archgun`, + code_melee: `Melee`, + code_pistol: `Pistolet`, + code_rifle: `Fusil`, + code_shotgun: `Fusil à Pompe`, + code_kitgun: `Kitgun`, + code_zaw: `Zaw`, + code_moteAmp: `Amplificateur Faible`, + code_amp: `Amplificateur`, + code_kDrive: `K-Drive`, + code_legendaryCore: `Coeur Légendaire`, + code_traumaticPeculiar: `Traumatisme Atypique`, + code_starter: `|MOD| (Défectueux)`, + code_badItem: `(Imposteur)`, + code_maxRank: `Rang Max`, + code_rename: `Renommer`, + code_renamePrompt: `Nouveau nom :`, + code_remove: `Retirer`, + code_addItemsConfirm: `Ajouter |COUNT| items à l'inventaire ?`, + code_succRankUp: `[UNTRANSLATED] Successfully ranked up.`, + code_noEquipmentToRankUp: `No equipment to rank up.`, + code_succAdded: `Ajouté.`, + code_succRemoved: `[UNTRANSLATED] Successfully removed.`, + code_buffsNumber: `Nombre de buffs`, + code_cursesNumber: `Nombre de débuffs`, + code_rerollsNumber: `Nombre de rerolls`, + code_viewStats: `Voir les stats`, + code_rank: `Rang`, + code_count: `Quantité`, + 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_addModsConfirm: `Ajouter |COUNT| mods à l'inventaire ?`, + code_succImport: `Importé.`, + code_gild: `Polir`, + code_moa: `Moa`, + code_zanuka: `Molosse`, + code_zanukaA: `Molosse Dorma`, + code_zanukaB: `Molosse Bhaira`, + code_zanukaC: `Molosse Hec`, + code_stage: `[UNTRANSLATED] Stage`, + code_complete: `[UNTRANSLATED] Complete`, + code_nextStage: `[UNTRANSLATED] Next stage`, + code_prevStage: `[UNTRANSLATED] Previous stage`, + code_reset: `[UNTRANSLATED] Reset`, + code_setInactive: `[UNTRANSLATED] Make the quest inactive`, + code_completed: `[UNTRANSLATED] Completed`, + code_active: `[UNTRANSLATED] Active`, + login_description: `Connexion avec les informations de connexion OpenWF.`, + login_emailLabel: `Email`, + login_passwordLabel: `Mot de passe`, + login_loginButton: `Connexion`, + navbar_logout: `Déconnexion`, + navbar_renameAccount: `Renommer le compte`, + navbar_deleteAccount: `Supprimer le compte`, + navbar_inventory: `Inventaire`, + navbar_mods: `Mods`, + navbar_quests: `Quêtes`, + navbar_cheats: `Cheats`, + navbar_import: `Importer`, + inventory_addItems: `Ajouter des items`, + inventory_suits: `Warframes`, + inventory_longGuns: `Armes principales`, + inventory_pistols: `Armes secondaires`, + inventory_melee: `Armes de melee`, + inventory_spaceSuits: `Archwings`, + inventory_spaceGuns: `Archguns`, + inventory_spaceMelee: `Archmelee`, + inventory_mechSuits: `Necramechs`, + inventory_sentinels: `Sentinelles`, + inventory_sentinelWeapons: `Armes de sentinelles`, + inventory_operatorAmps: `Amplificateurs`, + inventory_hoverboards: `K-Drives`, + inventory_moaPets: `Moa`, + inventory_bulkAddSuits: `Ajouter les Warframes manquantes`, + inventory_bulkAddWeapons: `Ajouter les armes manquantes`, + inventory_bulkAddSpaceSuits: `Ajouter les Archwings manquants`, + inventory_bulkAddSpaceWeapons: `Ajouter les armes d'Archwing manquantes`, + inventory_bulkAddSentinels: `Ajouter les Sentinelles manquantes`, + inventory_bulkAddSentinelWeapons: `Ajouter les armes de Sentinelles manquantes`, + inventory_bulkRankUpSuits: `Toutes les Warframes rang max`, + inventory_bulkRankUpWeapons: `Toutes les armes rang max`, + inventory_bulkRankUpSpaceSuits: `Tous les Archwings rang max`, + inventory_bulkRankUpSpaceWeapons: `Toutes les armes d'Archwing rang max`, + inventory_bulkRankUpSentinels: `Toutes les Sentinelles rang max`, + inventory_bulkRankUpSentinelWeapons: `Toutes les armes de Sentinelles rang max`, + + quests_list: `Quêtes`, + quests_completeAll: `Compléter toutes les quêtes`, + quests_resetAll: `Réinitialiser toutes les quêtes`, + quests_giveAll: `Obtenir toutes les quêtes`, + + currency_RegularCredits: `Crédits`, + currency_PremiumCredits: `Platinum`, + currency_FusionPoints: `Endo`, + currency_PrimeTokens: `Aya Raffiné`, + currency_owned: `|COUNT| possédés.`, + powersuit_archonShardsLabel: `Emplacements de fragments d'Archonte`, + powersuit_archonShardsDescription: `Slots illimités pour appliquer plusieurs améliorations.`, + mods_addRiven: `Ajouter un riven`, + mods_fingerprint: `Empreinte`, + mods_fingerprintHelp: `Besoin d'aide pour l'empreinte ?`, + mods_rivens: `Rivens`, + mods_mods: `Mods`, + mods_bulkAddMods: `Ajouter les mods manquants`, + cheats_administratorRequirement: `Rôle d'administrateur requis pour cette fonctionnalité. Ajoutez |DISPLAYNAME| à la ligne administratorNames dans le fichier config.json.`, + cheats_server: `Serveur`, + cheats_skipTutorial: `Passer le tutoriel`, + cheats_skipAllDialogue: `Passer les dialogues`, + cheats_unlockAllScans: `Débloquer tous les scans`, + cheats_unlockAllMissions: `Débloquer toutes les missions`, + cheats_infiniteCredits: `Crédits infinis`, + cheats_infinitePlatinum: `Platinum infini`, + cheats_infiniteEndo: `Endo infini`, + cheats_infiniteRegalAya: `Aya Raffiné infini`, + cheats_infiniteHelminthMaterials: `Ressources d'Helminth infinies`, + cheats_unlockAllShipFeatures: `Débloquer tous les segments du vaisseau`, + cheats_unlockAllShipDecorations: `Débloquer toutes les décorations du vaisseau`, + cheats_unlockAllFlavourItems: `Débloquer tous les Flavor Items`, + cheats_unlockAllSkins: `Débloquer tous les skins`, + cheats_unlockAllCapturaScenes: `Débloquer toutes les scènes captura`, + cheats_universalPolarityEverywhere: `Polarités universelles partout`, + cheats_unlockDoubleCapacityPotatoesEverywhere: `Réacteurs et Catalyseurs partout`, + cheats_unlockExilusEverywhere: `Adaptateurs Exilus partout`, + cheats_unlockArcanesEverywhere: `Adaptateur d'Arcanes partout`, + cheats_noDailyStandingLimits: `Pas de limite de réputation journalière`, + cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, + cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, + cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, + cheats_instantResourceExtractorDrones: `Ressources de drone d'extraction instantannées`, + cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`, + cheats_fastDojoRoomDestruction: `[UNTRANSLATED] Fast Dojo Room Destruction`, + cheats_noDojoResearchCosts: `Aucun coût de recherche (Dojo)`, + cheats_noDojoResearchTime: `Aucun temps de recherche (Dojo)`, + cheats_fastClanAscension: `[UNTRANSLATED] Fast Clan Ascension`, + cheats_spoofMasteryRank: `Spoofed Mastery Rank (-1 to disable)`, + cheats_saveSettings: `Sauvegarder les paramètres`, + cheats_account: `Compte`, + cheats_unlockAllFocusSchools: `Débloquer toutes les écoles de focus`, + cheats_helminthUnlockAll: `Helminth niveau max`, + cheats_intrinsicsUnlockAll: `[UNTRANSLATED] Max Rank All Intrinsics`, + cheats_changeSupportedSyndicate: `Allégeance`, + cheats_changeButton: `Changer`, + cheats_none: `Aucun`, + import_importNote: `Import manuel. Toutes les modifcations supportées par l'inventaire écraseront celles présentes dans la base de données.`, + import_submit: `Soumettre`, + prettier_sucks_ass: `` +}; diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 781046b9..893a864d 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -15,18 +15,20 @@ dict = { code_zaw: `Зо`, code_moteAmp: `Пылинка`, code_amp: `Усилитель`, - code_sirocco: `Сирокко`, code_kDrive: `К-Драйв`, code_legendaryCore: `Легендарное ядро`, code_traumaticPeculiar: `Травмирующая Странность`, + code_starter: `|MOD| (Повреждённый)`, code_badItem: `(Самозванец)`, code_maxRank: `Максимальный ранг`, code_rename: `Переименовать`, code_renamePrompt: `Введите новое имя:`, code_remove: `Удалить`, code_addItemsConfirm: `Вы уверены, что хотите добавить |COUNT| предметов на ваш аккаунт?`, + code_succRankUp: `Ранг успешно повышен`, code_noEquipmentToRankUp: `Нет снаряжения для повышения ранга.`, code_succAdded: `Успешно добавлено.`, + code_succRemoved: `Успешно удалено.`, code_buffsNumber: `Количество усилений`, code_cursesNumber: `Количество проклятий`, code_rerollsNumber: `Количество циклов`, @@ -37,6 +39,20 @@ dict = { code_focusUnlocked: `Разблокировано |COUNT| новых школ фокуса! Для отображения изменений в игре потребуется обновление инвентаря. Посещение навигации — самый простой способ этого добиться.`, code_addModsConfirm: `Вы уверены, что хотите добавить |COUNT| модов на ваш аккаунт?`, code_succImport: `Успешно импортировано.`, + code_gild: `Улучшить`, + code_moa: `МОА`, + code_zanuka: `Гончая`, + code_zanukaA: `Гончая: Дорма`, + code_zanukaB: `Гончая: Бхайра`, + code_zanukaC: `Гончая: Хек`, + code_stage: `Этап`, + code_complete: `Завершить`, + code_nextStage: `Cледующий этап`, + code_prevStage: `Предыдущий этап`, + code_reset: `Сбросить`, + code_setInactive: `Сделать квест неактивным`, + code_completed: `Завершено`, + code_active: `Активный`, login_description: `Войдите, используя учетные данные OpenWF (те же, что и в игре при подключении к этому серверу).`, login_emailLabel: `Адрес электронной почты`, login_passwordLabel: `Пароль`, @@ -62,6 +78,7 @@ dict = { inventory_sentinelWeapons: `Оружие стражей`, inventory_operatorAmps: `Усилители`, inventory_hoverboards: `К-Драйвы`, + inventory_moaPets: `МОА`, inventory_bulkAddSuits: `Добавить отсутствующие варфреймы`, inventory_bulkAddWeapons: `Добавить отсутствующее оружие`, inventory_bulkAddSpaceSuits: `Добавить отсутствующие арчвинги`, @@ -75,6 +92,11 @@ dict = { inventory_bulkRankUpSentinels: `Максимальный ранг всех стражей`, inventory_bulkRankUpSentinelWeapons: `Максимальный ранг всего оружия стражей`, + quests_list: `Квесты`, + quests_completeAll: `Завершить все квесты`, + quests_resetAll: `Сбросить прогресс всех квестов`, + quests_giveAll: `Выдать все квесты`, + currency_RegularCredits: `Кредиты`, currency_PremiumCredits: `Платина`, currency_FusionPoints: `Эндо`, @@ -94,13 +116,11 @@ dict = { cheats_skipAllDialogue: `Пропустить все диалоги`, cheats_unlockAllScans: `Разблокировать все сканирования`, cheats_unlockAllMissions: `Разблокировать все миссии`, - cheats_unlockAllQuests: `Разблокировать все квесты`, - cheats_completeAllQuests: `Завершить все квесты`, cheats_infiniteCredits: `Бесконечные кредиты`, cheats_infinitePlatinum: `Бесконечная платина`, cheats_infiniteEndo: `Бесконечное эндо`, cheats_infiniteRegalAya: `Бесконечная Королевская Айя`, - cheats_infiniteHelminthMaterials: `[UNTRANSLATED] Infinite Helminth Materials`, + cheats_infiniteHelminthMaterials: `Бесконечные Выделения Гельминта`, cheats_unlockAllShipFeatures: `Разблокировать все функции корабля`, cheats_unlockAllShipDecorations: `Разблокировать все украшения корабля`, cheats_unlockAllFlavourItems: `Разблокировать все уникальные предметы`, @@ -111,20 +131,25 @@ dict = { cheats_unlockExilusEverywhere: `Адаптеры Эксилус везде`, cheats_unlockArcanesEverywhere: `Адаптеры для мистификаторов везде`, cheats_noDailyStandingLimits: `Без ежедневных ограничений репутации`, + cheats_noArgonCrystalDecay: `Без распада аргоновых кристаллов`, + cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, + cheats_noVendorPurchaseLimits: `Отсутствие лимитов на покупки у вендоров`, + cheats_instantResourceExtractorDrones: `Мгновенные Экстракторы Ресурсов`, + cheats_noDojoRoomBuildStage: `Мгновенное Строительтво Комнат Додзё`, + cheats_fastDojoRoomDestruction: `Мгновенные Уничтожение Комнат Додзё`, + cheats_noDojoResearchCosts: `Бесплатные Исследование Додзё`, + cheats_noDojoResearchTime: `Мгновенные Исследование Додзё`, + cheats_fastClanAscension: `Мгновенное Вознесение Клана`, cheats_spoofMasteryRank: `Подделанный ранг мастерства (-1 для отключения)`, cheats_saveSettings: `Сохранить настройки`, cheats_account: `Аккаунт`, cheats_unlockAllFocusSchools: `Разблокировать все школы фокуса`, cheats_helminthUnlockAll: `Полностью улучшить Гельминта`, + cheats_intrinsicsUnlockAll: `Полностью улучшить Модуляры`, cheats_changeSupportedSyndicate: `Поддерживаемый синдикат`, cheats_changeButton: `Изменить`, cheats_none: `Отсутствует`, - cheats_quests: `Квесты`, - cheats_quests_unlockAll: `Разблокировать все квесты`, - cheats_quests_completeAll: `Завершить все квесты`, - cheats_quests_completeAllUnlocked: `Завершить все разблокированые квесты`, - cheats_quests_resetAll: `Сбросить прогресс всех квестов`, - cheats_quests_giveAll: `Выдать все квесты`, import_importNote: `Вы можете загрузить полный или частичный ответ инвентаря (клиентское представление) здесь. Все поддерживаемые поля будут перезаписаны в вашем аккаунте.`, - import_submit: `Отправить` + import_submit: `Отправить`, + prettier_sucks_ass: `` }; diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js new file mode 100644 index 00000000..29890fd0 --- /dev/null +++ b/static/webui/translations/zh.js @@ -0,0 +1,155 @@ +// Chinese translation by meb154 +dict = { + general_inventoryUpdateNote: `注意:此处所做的更改只有在游戏同步仓库后才会生效。您可以通过访问星图来触发仓库更新。`, + general_addButton: `添加`, + general_bulkActions: `批量操作`, + code_nonValidAuthz: `您的登录凭证已失效。`, + code_changeNameConfirm: `您想将账户名称更改为什么?`, + code_deleteAccountConfirm: `确定要删除账户 |DISPLAYNAME| (|EMAIL|) 吗?此操作不可撤销。`, + code_archgun: `空战`, + code_melee: `近战`, + code_pistol: `手枪`, + code_rifle: `步枪`, + code_shotgun: `霰弹枪`, + code_kitgun: `组合枪`, + code_zaw: `自制近战`, + code_moteAmp: `微尘增幅器`, + code_amp: `增幅器`, + code_kDrive: `K式悬浮板`, + code_legendaryCore: `传奇核心`, + code_traumaticPeculiar: `创伤怪奇`, + code_starter: `|MOD| (有瑕疵的)`, + code_badItem: `(Imposter)`, + code_maxRank: `满级`, + code_rename: `重命名`, + code_renamePrompt: `输入新的自定义名称:`, + code_remove: `移除`, + code_addItemsConfirm: `确定要向账户添加 |COUNT| 件物品吗?`, + code_succRankUp: `[UNTRANSLATED] Successfully ranked up.`, + code_noEquipmentToRankUp: `没有可升级的装备。`, + code_succAdded: `已成功添加。`, + code_succRemoved: `[UNTRANSLATED] Successfully removed.`, + code_buffsNumber: `增益数量`, + code_cursesNumber: `负面数量`, + code_rerollsNumber: `洗卡次数`, + code_viewStats: `查看属性`, + code_rank: `等级`, + code_count: `数量`, + code_focusAllUnlocked: `所有专精学派均已解锁。`, + code_focusUnlocked: `已解锁 |COUNT| 个新专精学派!需要游戏内仓库更新才能生效,您可以通过访问星图来触发仓库更新。`, + code_addModsConfirm: `确定要向账户添加 |COUNT| 张MOD吗?`, + code_succImport: `导入成功。`, + code_gild: `镀金`, + code_moa: `恐鸟`, + code_zanuka: `猎犬`, + code_zanukaA: `铎玛猎犬`, + code_zanukaB: `拜拉猎犬`, + code_zanukaC: `骸克猎犬`, + code_stage: `[UNTRANSLATED] Stage`, + code_complete: `[UNTRANSLATED] Complete`, + code_nextStage: `[UNTRANSLATED] Next stage`, + code_prevStage: `[UNTRANSLATED] Previous stage`, + code_reset: `[UNTRANSLATED] Reset`, + code_setInactive: `[UNTRANSLATED] Make the quest inactive`, + code_completed: `[UNTRANSLATED] Completed`, + code_active: `[UNTRANSLATED] Active`, + login_description: `使用您的 OpenWF 账户凭证登录(与游戏内连接本服务器时使用的昵称相同)。`, + login_emailLabel: `电子邮箱`, + login_passwordLabel: `密码`, + login_loginButton: `登录`, + navbar_logout: `退出登录`, + navbar_renameAccount: `重命名账户`, + navbar_deleteAccount: `删除账户`, + navbar_inventory: `仓库`, + navbar_mods: `Mods`, + navbar_quests: `任务`, + navbar_cheats: `作弊选项`, + navbar_import: `导入`, + inventory_addItems: `添加物品`, + inventory_suits: `战甲`, + inventory_longGuns: `主要武器`, + inventory_pistols: `次要武器`, + inventory_melee: `近战武器`, + inventory_spaceSuits: `Archwings`, + inventory_spaceGuns: `Archwing主武器`, + inventory_spaceMelee: `Archwing近战武器`, + inventory_mechSuits: `殁世机甲`, + inventory_sentinels: `守护`, + inventory_sentinelWeapons: `守护武器`, + inventory_operatorAmps: `增幅器`, + inventory_hoverboards: `K式悬浮板`, + inventory_moaPets: `恐鸟`, + inventory_bulkAddSuits: `添加缺失战甲`, + inventory_bulkAddWeapons: `添加缺失武器`, + inventory_bulkAddSpaceSuits: `添加缺失Archwing`, + inventory_bulkAddSpaceWeapons: `添加缺失Archwing武器`, + inventory_bulkAddSentinels: `添加缺失守护`, + inventory_bulkAddSentinelWeapons: `添加缺失守护武器`, + inventory_bulkRankUpSuits: `所有战甲升满级`, + inventory_bulkRankUpWeapons: `所有武器升满级`, + inventory_bulkRankUpSpaceSuits: `所有Archwing升满级`, + inventory_bulkRankUpSpaceWeapons: `所有Archwing武器升满级`, + inventory_bulkRankUpSentinels: `所有守护升满级`, + inventory_bulkRankUpSentinelWeapons: `所有守护武器升满级`, + + quests_list: `任务`, + quests_completeAll: `完成所有任务`, + quests_resetAll: `重置所有任务`, + quests_giveAll: `授予所有任务`, + + currency_RegularCredits: `现金`, + currency_PremiumCredits: `白金`, + currency_FusionPoints: `内融核心`, + currency_PrimeTokens: `御品阿耶`, + currency_owned: `当前拥有 |COUNT|。`, + powersuit_archonShardsLabel: `执刑官源力石槽位`, + powersuit_archonShardsDescription: `您可以使用这些无限插槽应用各种强化效果`, + mods_addRiven: `添加裂罅MOD`, + mods_fingerprint: `印记`, + mods_fingerprintHelp: `需要印记相关的帮助?`, + mods_rivens: `裂罅MOD`, + mods_mods: `Mods`, + mods_bulkAddMods: `添加缺失MOD`, + cheats_administratorRequirement: `您必须是管理员才能使用此功能。要成为管理员,请将 |DISPLAYNAME| 添加到 config.json 的 administratorNames 中。`, + cheats_server: `服务器`, + cheats_skipTutorial: `跳过教程`, + cheats_skipAllDialogue: `跳过所有对话`, + cheats_unlockAllScans: `解锁所有扫描`, + cheats_unlockAllMissions: `解锁所有任务`, + cheats_infiniteCredits: `无限现金`, + cheats_infinitePlatinum: `无限白金`, + cheats_infiniteEndo: `无限内融核心`, + cheats_infiniteRegalAya: `无限御品阿耶`, + cheats_infiniteHelminthMaterials: `无限Helminth材料`, + cheats_unlockAllShipFeatures: `解锁所有飞船功能`, + cheats_unlockAllShipDecorations: `解锁所有飞船装饰`, + cheats_unlockAllFlavourItems: `解锁所有装饰物品`, + cheats_unlockAllSkins: `解锁所有外观`, + cheats_unlockAllCapturaScenes: `解锁所有Captura场景`, + cheats_universalPolarityEverywhere: `全局万用极性`, + cheats_unlockDoubleCapacityPotatoesEverywhere: `全物品自带Orokin反应堆`, + cheats_unlockExilusEverywhere: `全物品自带适配器`, + cheats_unlockArcanesEverywhere: `全物品自带赋能适配器`, + cheats_noDailyStandingLimits: `无每日声望限制`, + cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, + cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, + cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, + cheats_instantResourceExtractorDrones: `即时资源采集无人机`, + cheats_noDojoRoomBuildStage: `无视道场房间建造阶段`, + cheats_fastDojoRoomDestruction: `快速拆除道场房间`, + cheats_noDojoResearchCosts: `无视道场研究消耗`, + cheats_noDojoResearchTime: `无视道场研究时间`, + cheats_fastClanAscension: `快速升级氏族`, + cheats_spoofMasteryRank: `伪造精通段位(-1为禁用)`, + cheats_saveSettings: `保存设置`, + cheats_account: `账户`, + cheats_unlockAllFocusSchools: `解锁所有专精学派`, + cheats_helminthUnlockAll: `完全升级Helminth`, + cheats_intrinsicsUnlockAll: `[UNTRANSLATED] Max Rank All Intrinsics`, + cheats_changeSupportedSyndicate: `支持的集团`, + cheats_changeButton: `更改`, + cheats_none: `无`, + import_importNote: `您可以在此处提供完整或部分库存响应(客户端表示)。支持的所有字段将被覆盖到您的账户中。`, + import_submit: `提交`, + prettier_sucks_ass: `` +}; diff --git a/tsconfig.json b/tsconfig.json index c2603f08..349cffc0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ /* Visit https://aka.ms/tsconfig to read more about this file */ /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "incremental": true /* Save .tsbuildinfo files to allow for incremental compilation of projects. */, // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */