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 f03e5269..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", - "no-case-declarations": "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/.gitattributes b/.gitattributes index 0936ecf2..b3bfce24 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ # Auto detect text files and perform LF normalization -* text=auto +* text=auto eol=lf static/webui/libs/ linguist-vendored diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ec5e2082..aed7014e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,17 +5,24 @@ on: jobs: build: runs-on: ubuntu-latest - strategy: - matrix: - version: [18, 20, 22] steps: - name: Checkout uses: actions/checkout@v4.1.2 - name: Setup Node.js environment uses: actions/setup-node@v4.0.2 with: - node-version: ${{ matrix.version }} + node-version: ">=20.6.0" - run: npm ci - run: cp config.json.example config.json - - run: npm run build - - run: npm run lint + - run: npm run verify + - run: npm run lint:ci + - run: npm run prettier + - run: npm run update-translations + - name: Fail if there are uncommitted changes + run: | + if [[ -n "$(git status --porcelain)" ]]; then + echo "Uncommitted changes detected:" + git status + git --no-pager diff + exit 1 + fi 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..940260d8 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["dbaeumer.vscode-eslint"] +} diff --git a/Dockerfile b/Dockerfile index f265957f..c36da824 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,19 +5,44 @@ ENV APP_MY_ADDRESS=localhost ENV APP_HTTP_PORT=80 ENV APP_HTTPS_PORT=443 ENV APP_AUTO_CREATE_ACCOUNT=true -ENV APP_SKIP_STORY_MODE_CHOICE=true -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 -ENV APP_UNLOCK_ALL_FLAVOUR_ITEMS=true -ENV APP_UNLOCK_ALL_SKINS=true -ENV APP_UNIVERSAL_POLARITY_EVERYWHERE=true +ENV APP_SKIP_TUTORIAL=false +ENV APP_SKIP_ALL_DIALOGUE=false +ENV APP_UNLOCK_ALL_SCANS=false +ENV APP_UNLOCK_ALL_MISSIONS=false +ENV APP_INFINITE_CREDITS=false +ENV APP_INFINITE_PLATINUM=false +ENV APP_INFINITE_ENDO=false +ENV APP_INFINITE_REGAL_AYA=false +ENV APP_INFINITE_HELMINTH_MATERIALS=false +ENV APP_CLAIMING_BLUEPRINT_REFUNDS_INGREDIENTS=false +ENV APP_DONT_SUBTRACT_VOIDTRACES=false +ENV APP_DONT_SUBTRACT_CONSUMABLES=false +ENV APP_UNLOCK_ALL_SHIP_FEATURES=false +ENV APP_UNLOCK_ALL_SHIP_DECORATIONS=false +ENV APP_UNLOCK_ALL_FLAVOUR_ITEMS=false +ENV APP_UNLOCK_ALL_SKINS=false +ENV APP_UNLOCK_ALL_CAPTURA_SCENES=false +ENV APP_UNIVERSAL_POLARITY_EVERYWHERE=false +ENV APP_UNLOCK_DOUBLE_CAPACITY_POTATOES_EVERYWHERE=false +ENV APP_UNLOCK_EXILUS_EVERYWHERE=false +ENV APP_UNLOCK_ARCANES_EVERYWHERE=false +ENV APP_NO_DAILY_FOCUS_LIMIT=false +ENV APP_NO_ARGON_CRYSTAL_DECAY=false +ENV APP_NO_MASTERY_RANK_UP_COOLDOWN=false +ENV APP_NO_VENDOR_PURCHASE_LIMITS=true +ENV APP_NO_DEATH_MARKS=false +ENV APP_NO_KIM_COOLDOWNS=false +ENV APP_SYNDICATE_MISSIONS_REPEATABLE=false +ENV APP_INSTANT_FINISH_RIVEN_CHALLENGE=false +ENV APP_INSTANT_RESOURCE_EXTRACTOR_DRONES=false +ENV APP_NO_RESOURCE_EXTRACTOR_DRONES_DAMAGE=false +ENV APP_SKIP_CLAN_KEY_CRAFTING=false +ENV APP_NO_DOJO_ROOM_BUILD_STAGE=false +ENV APP_NO_DECO_BUILD_STAGE=false +ENV APP_FAST_DOJO_ROOM_DESTRUCTION=false +ENV APP_NO_DOJO_RESEARCH_COSTS=false +ENV APP_NO_DOJO_RESEARCH_TIME=false +ENV APP_FAST_CLAN_ASCENSION=false ENV APP_SPOOF_MASTERY_RANK=-1 RUN apk add --no-cache bash sed wget jq diff --git a/README.md b/README.md index 4dcab407..e1ef957a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,16 @@ 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 +SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [config.json.example](config.json.example), which has most cheats disabled. + - `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`. - `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..983b8f8d 100644 --- a/UPDATE AND START SERVER.bat +++ b/UPDATE AND START SERVER.bat @@ -1,8 +1,8 @@ @echo off 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 +13,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 2bdc035a..d9585902 100644 --- a/config.json.example +++ b/config.json.example @@ -7,25 +7,54 @@ "myAddress": "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, - "unlockAllShipFeatures": true, - "unlockAllShipDecorations": true, - "unlockAllFlavourItems": true, - "unlockAllSkins": true, - "unlockAllCapturaScenes": true, - "universalPolarityEverywhere": true, - "unlockDoubleCapacityPotatoesEverywhere": true, - "unlockExilusEverywhere": true, - "unlockArcanesEverywhere": true, - "noDailyStandingLimits": true, - "spoofMasteryRank": -1 + "skipTutorial": false, + "skipAllDialogue": false, + "unlockAllScans": false, + "unlockAllMissions": false, + "infiniteCredits": false, + "infinitePlatinum": false, + "infiniteEndo": false, + "infiniteRegalAya": false, + "infiniteHelminthMaterials": false, + "claimingBlueprintRefundsIngredients": false, + "dontSubtractVoidTraces": false, + "dontSubtractConsumables": false, + "unlockAllShipFeatures": false, + "unlockAllShipDecorations": false, + "unlockAllFlavourItems": false, + "unlockAllSkins": false, + "unlockAllCapturaScenes": false, + "universalPolarityEverywhere": false, + "unlockDoubleCapacityPotatoesEverywhere": false, + "unlockExilusEverywhere": false, + "unlockArcanesEverywhere": false, + "noDailyStandingLimits": false, + "noDailyFocusLimit": false, + "noArgonCrystalDecay": false, + "noMasteryRankUpCooldown": false, + "noVendorPurchaseLimits": true, + "noDeathMarks": false, + "noKimCooldowns": false, + "syndicateMissionsRepeatable": false, + "instantFinishRivenChallenge": false, + "instantResourceExtractorDrones": false, + "noResourceExtractorDronesDamage": false, + "skipClanKeyCrafting": false, + "noDojoRoomBuildStage": false, + "noDecoBuildStage": 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/docker-compose.yml b/docker-compose.yml index d11206cc..544dec95 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,19 +12,44 @@ services: # APP_HTTP_PORT: 80 # APP_HTTPS_PORT: 443 # APP_AUTO_CREATE_ACCOUNT: true - # APP_SKIP_STORY_MODE_CHOICE: true - # APP_SKIP_TUTORIAL: true - # APP_SKIP_ALL_DIALOGUE: true - # APP_UNLOCK_ALL_SCANS: true - # APP_UNLOCK_ALL_MISSIONS: true - # APP_UNLOCK_ALL_QUESTS: true - # APP_COMPLETE_ALL_QUESTS: true - # APP_INFINITE_RESOURCES: true - # APP_UNLOCK_ALL_SHIP_FEATURES: true - # APP_UNLOCK_ALL_SHIP_DECORATIONS: true - # APP_UNLOCK_ALL_FLAVOUR_ITEMS: true - # APP_UNLOCK_ALL_SKINS: true - # APP_UNIVERSAL_POLARITY_EVERYWHERE: true + # APP_SKIP_TUTORIAL: false + # APP_SKIP_ALL_DIALOGUE: false + # APP_UNLOCK_ALL_SCANS: false + # APP_UNLOCK_ALL_MISSIONS: false + # APP_INFINITE_CREDITS: false + # APP_INFINITE_PLATINUM: false + # APP_INFINITE_ENDO: false + # APP_INFINITE_REGAL_AYA: false + # APP_INFINITE_HELMINTH_MATERIALS: false + # APP_CLAIMING_BLUEPRINT_REFUNDS_INGREDIENTS: false + # APP_DONT_SUBTRACT_VOIDTRACES: false + # APP_DONT_SUBTRACT_CONSUMABLES: false + # APP_UNLOCK_ALL_SHIP_FEATURES: false + # APP_UNLOCK_ALL_SHIP_DECORATIONS: false + # APP_UNLOCK_ALL_FLAVOUR_ITEMS: false + # APP_UNLOCK_ALL_SKINS: false + # APP_UNLOCK_ALL_CAPTURA_SCENES: false + # APP_UNIVERSAL_POLARITY_EVERYWHERE: false + # APP_UNLOCK_DOUBLE_CAPACITY_POTATOES_EVERYWHERE: false + # APP_UNLOCK_EXILUS_EVERYWHERE: false + # APP_UNLOCK_ARCANES_EVERYWHERE: false + # APP_NO_DAILY_FOCUS_LIMIT: false + # APP_NO_ARGON_CRYSTAL_DECAY: false + # APP_NO_MASTERY_RANK_UP_COOLDOWN: false + # APP_NO_VENDOR_PURCHASE_LIMITS: true + # APP_NO_DEATH_MARKS: false + # APP_NO_KIM_COOLDOWNS: false + # APP_SYNDICATE_MISSIONS_REPEATABLE: false + # APP_INSTANT_FINISH_RIVEN_CHALLENGE: false + # APP_INSTANT_RESOURCE_EXTRACTOR_DRONES: false + # APP_NO_RESOURCE_EXTRACTOR_DRONES_DAMAGE: false + # APP_SKIP_CLAN_KEY_CRAFTING: false + # APP_NO_DOJO_ROOM_BUILD_STAGE: false + # APP_NO_DECO_BUILD_STAGE: false + # APP_FAST_DOJO_ROOM_DESTRUCTION: false + # APP_NO_DOJO_RESEARCH_COSTS: false + # APP_NO_DOJO_RESEARCH_TIME: false + # APP_FAST_CLAN_ASCENSION: false # APP_SPOOF_MASTERY_RANK: -1 volumes: - ./docker-data/static:/app/static/data diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 9ec9086b..13e70c33 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -19,5 +19,6 @@ do mv config.tmp config.json done -npm install -exec npm run dev +npm i --omit=dev +npm run build +exec npm run start diff --git a/package-lock.json b/package-lock.json index ec52be41..59407f15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,31 +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.30", + "json-with-bigint": "^3.4.4", + "mongoose": "^8.11.0", + "morgan": "^1.10.0", + "ncp": "^2.0.0", + "typescript": "^5.5", + "warframe-public-export-plus": "^0.5.64", "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", + "@typescript-eslint/eslint-plugin": "^8.28.0", + "@typescript-eslint/parser": "^8.28.0", + "@typescript/native-preview": "^7.0.0-dev.20250523.1", + "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" - }, - "engines": { - "node": ">=18.15.0", - "npm": ">=9.5.0" + "tsconfig-paths": "^4.2.0" } }, "node_modules/@colors/colors": { @@ -70,9 +68,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.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, "license": "MIT", "dependencies": { @@ -247,9 +245,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.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.2.tgz", + "integrity": "sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==", "license": "MIT", "dependencies": { "sparse-bitfield": "^3.0.3" @@ -294,16 +292,16 @@ } }, "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.4", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz", + "integrity": "sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==", "dev": true, "license": "MIT", "engines": { "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://opencollective.com/pkgr" } }, "node_modules/@tsconfig/node10": { @@ -338,7 +336,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,30 +346,26 @@ "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": "*" } }, "node_modules/@types/express": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", - "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", - "dev": true, + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", + "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", - "@types/qs": "*", "@types/serve-static": "*" } }, "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,55 +378,48 @@ "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.15.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.16.tgz", + "integrity": "sha512-3pr+KjwpVujqWqOKT8mNR+rd09FqhBLwg+5L/4t0cNYBzm/yEiYGCxWttjaPBsLtAo+WFNoXzGJfolM1JuRXoA==", "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/qs": { "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 +430,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 +473,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.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz", + "integrity": "sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ==", "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.32.0", + "@typescript-eslint/type-utils": "8.32.0", + "@typescript-eslint/utils": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.1.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": { - "@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.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.0.tgz", + "integrity": "sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==", "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.32.0", + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/typescript-estree": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.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.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz", + "integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0" + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -569,41 +546,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.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.0.tgz", + "integrity": "sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/typescript-estree": "8.32.0", + "@typescript-eslint/utils": "8.32.0", "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.1.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.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.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz", + "integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==", "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 +584,232 @@ } }, "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.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz", + "integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==", "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.32.0", + "@typescript-eslint/visitor-keys": "8.32.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.1.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.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.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.0.tgz", + "integrity": "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.18.0", - "eslint-visitor-keys": "^3.4.3" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.32.0", + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/typescript-estree": "8.32.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.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz", + "integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.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/@typescript/native-preview": { + "version": "7.0.0-dev.20250523.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20250523.1.tgz", + "integrity": "sha512-CgdgP/gmyaMThY7Fho19nDaTVryn9QV/zD/6w1KfDCn3M4Rq4WvkSc7Ob1ohc4V1XjCSIzg6Ul+HbLEc7xvV4Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsgo": "bin/tsgo.js" + }, + "engines": { + "node": ">=20.6.0" + }, + "optionalDependencies": { + "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20250523.1", + "@typescript/native-preview-darwin-x64": "7.0.0-dev.20250523.1", + "@typescript/native-preview-linux-arm": "7.0.0-dev.20250523.1", + "@typescript/native-preview-linux-arm64": "7.0.0-dev.20250523.1", + "@typescript/native-preview-linux-x64": "7.0.0-dev.20250523.1", + "@typescript/native-preview-win32-arm64": "7.0.0-dev.20250523.1", + "@typescript/native-preview-win32-x64": "7.0.0-dev.20250523.1" + } + }, + "node_modules/@typescript/native-preview-darwin-arm64": { + "version": "7.0.0-dev.20250523.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20250523.1.tgz", + "integrity": "sha512-oWJMPD+lfH9/dvHhPSZdTv43lfyZGrn7crytefhkiQPSwP0MIUCpnDkofGP/ML1nv0xx0pwWhH+Ein88NW3LuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.6.0" + } + }, + "node_modules/@typescript/native-preview-darwin-x64": { + "version": "7.0.0-dev.20250523.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20250523.1.tgz", + "integrity": "sha512-Yk8bJEsYsRKgRqYlwPvh7DPdgBMC/oPN60X0LWeuMLci65+4kyqF8Cv6K/W3ABc005cB4tYn4iR+9T6zipvrKw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.6.0" + } + }, + "node_modules/@typescript/native-preview-linux-arm": { + "version": "7.0.0-dev.20250523.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20250523.1.tgz", + "integrity": "sha512-B+8CRIv6ebL8gzAagnJP8wml3baFV2FtFWuXYl6jlAcLGoQOh/yGdcAueZoJjJKNod4gAOl8OJoTicuC0BVIxw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.6.0" + } + }, + "node_modules/@typescript/native-preview-linux-arm64": { + "version": "7.0.0-dev.20250523.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20250523.1.tgz", + "integrity": "sha512-IErNI08z9qE6mHaJaT6tM7il8j21ryH3DNVyFP4yz5FTKnkXFj1Kb4NcI41Q8w226LTQgBR8kNErVlbUWr7ywA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.6.0" + } + }, + "node_modules/@typescript/native-preview-linux-x64": { + "version": "7.0.0-dev.20250523.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20250523.1.tgz", + "integrity": "sha512-TCZtknsLUgPRaEfX9CvBZNgrHhMRZPYYZgF1Aasdv0PONv9mB8w0Xforgxoo4UFjdF5ZzOu2icgc7sKJJeu5vw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.6.0" + } + }, + "node_modules/@typescript/native-preview-win32-arm64": { + "version": "7.0.0-dev.20250523.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20250523.1.tgz", + "integrity": "sha512-bulwrkLEkoY4Jqeuvfz24RiVOiZZ7Rr9TblFqZAgZFZOnyXuhjM1jE8F1hnJFC5AghJe2HdLD3EKfabqlffrIw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.6.0" + } + }, + "node_modules/@typescript/native-preview-win32-x64": { + "version": "7.0.0-dev.20250523.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20250523.1.tgz", + "integrity": "sha512-ztzfO0oF/rj8xO5y3SyAcigmgvgczrqobCugEWFqiYumteWZPN2MYWcNYk2k8Y5LAgg1fN1xHIg8RRSPoo6XUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.6.0" + } + }, "node_modules/@ungap/structured-clone": { - "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 +827,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 +883,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 +893,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 +933,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 +943,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 +962,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,84 +978,25 @@ } }, "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.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "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.6.3", + "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", - "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" - }, - "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" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -969,9 +1021,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 +1046,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 +1059,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 +1139,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 +1153,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 +1207,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": { @@ -1189,9 +1232,9 @@ } }, "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -1206,53 +1249,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", @@ -1308,16 +1316,6 @@ "node": ">= 0.8" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -1328,19 +1326,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 +1369,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 +1414,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 +1491,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.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.0.tgz", + "integrity": "sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA==", "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.9.1" + "synckit": "^0.11.0" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -1539,7 +1509,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": { @@ -1679,71 +1649,47 @@ } }, "node_modules/express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.0.1.tgz", - "integrity": "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.0.1", + "body-parser": "^2.2.0", "content-disposition": "^1.0.0", - "content-type": "~1.0.4", - "cookie": "0.7.1", + "content-type": "^1.0.5", + "cookie": "^0.7.1", "cookie-signature": "^1.2.1", - "debug": "4.3.6", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "^2.0.0", - "fresh": "2.0.0", - "http-errors": "2.0.0", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", - "methods": "~1.1.2", "mime-types": "^3.0.0", - "on-finished": "2.4.1", - "once": "1.4.0", - "parseurl": "~1.3.3", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "router": "^2.0.0", - "safe-buffer": "5.2.1", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", "send": "^1.1.0", - "serve-static": "^2.1.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "^2.0.0", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { "node": ">= 18" - } - }, - "node_modules/express/node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/express/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1803,9 +1749,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 +1800,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 +1849,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 +1883,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 +1910,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 +1952,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 +1986,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 +1997,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 +2022,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", @@ -2197,12 +2092,12 @@ } }, "node_modules/iconv-lite": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz", - "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" @@ -2219,9 +2114,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 +2145,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 +2212,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 +2263,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 +2304,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-with-bigint": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.4.4.tgz", + "integrity": "sha512-AhpYAAaZsPjU7smaBomDt1SOQshi9rEm6BlTbfVwsG1vNmeHKtEedJi62sHZzJTyKNtwzmNnrsd55kjwJ7054A==", + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -2568,15 +2455,6 @@ "node": ">= 8" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -2592,21 +2470,21 @@ } }, "node_modules/mime-db": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", - "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.0.tgz", - "integrity": "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "license": "MIT", "dependencies": { - "mime-db": "^1.53.0" + "mime-db": "^1.54.0" }, "engines": { "node": ">= 0.6" @@ -2642,6 +2520,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 +2539,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.16.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.16.0.tgz", + "integrity": "sha512-D1PNcdT0y4Grhou5Zi/qgipZOYeWrhLEpk33n3nm6LGtz61jvO88WlrWCK/bigMjpnOdAUKKQwsGIl0NtWMyYw==", "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 +2595,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.14.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.14.1.tgz", + "integrity": "sha512-ijd12vjqUBr5Btqqflu0c/o8Oed5JpdaE0AKO9TjGxCgywYwnzt6ynR1ySjhgxGxrYVeXC0t1P11f1zlRiE93Q==", "license": "MIT", "dependencies": { - "bson": "^6.10.1", + "bson": "^6.10.3", "kareem": "2.6.3", - "mongodb": "~6.12.0", + "mongodb": "~6.16.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", @@ -2741,7 +2620,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 +2636,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 +2645,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 +2693,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 +2711,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 +2731,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 +2758,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 +2867,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 +2899,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 +2923,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 +2951,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", @@ -3116,12 +2974,12 @@ } }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "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": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -3175,28 +3033,18 @@ "node": ">= 0.8" } }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" }, "engines": { - "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": ">= 6" } }, "node_modules/readdirp": { @@ -3212,15 +3060,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 +3092,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 +3120,19 @@ } }, "node_modules/router": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.0.0.tgz", - "integrity": "sha512-dIM5zVoG8xhC6rnSN8uoAgFARwTE7BQs8YwHEvK0VCmfxQXMaOuA1uiR1IPwsW7JyK5iTt7Od/TC9StasS2NPQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "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" + "debug": "^4.4.0", + "depd": "^2.0.0", + "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 +3195,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": { @@ -3371,19 +3208,18 @@ } }, "node_modules/send": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.1.0.tgz", - "integrity": "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "license": "MIT", "dependencies": { "debug": "^4.3.5", - "destroy": "^1.2.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", - "fresh": "^0.5.2", + "fresh": "^2.0.0", "http-errors": "^2.0.0", - "mime-types": "^2.1.35", + "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", @@ -3393,46 +3229,16 @@ "node": ">= 18" } }, - "node_modules/send/node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/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/send/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" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/serve-static": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.1.0.tgz", - "integrity": "sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", - "send": "^1.0.0" + "send": "^1.2.0" }, "engines": { "node": ">= 18" @@ -3554,16 +3360,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", @@ -3613,29 +3409,19 @@ } }, "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==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" + "safe-buffer": "~5.2.0" } }, "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,20 +3480,20 @@ } }, "node_modules/synckit": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", - "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.4.tgz", + "integrity": "sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" + "@pkgr/core": "^0.2.3", + "tslib": "^2.8.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://opencollective.com/synckit" } }, "node_modules/text-hex": { @@ -3723,52 +3509,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", @@ -3792,9 +3532,9 @@ } }, "node_modules/tr46": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", - "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "license": "MIT", "dependencies": { "punycode": "^2.3.1" @@ -3823,16 +3563,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": { @@ -4000,9 +3740,9 @@ } }, "node_modules/type-is": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.0.tgz", - "integrity": "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", "dependencies": { "content-type": "^1.0.5", @@ -4014,10 +3754,10 @@ } }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", - "dev": true, + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4027,10 +3767,9 @@ } }, "node_modules/undici-types": { - "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, + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, "node_modules/unpipe": { @@ -4042,15 +3781,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", @@ -4067,15 +3797,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -4093,9 +3814,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.30", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.30.tgz", - "integrity": "sha512-vzs+naEqp3iFZTbgIky4jiNbjNIovuR4oSimrFiuyIbrnfTlfXFzDfzT0hG2rgS8yEXBAbOcv2Zfm3fmWuZ0Kg==" + "version": "0.5.64", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.64.tgz", + "integrity": "sha512-JyHRtYumfwQ1Iog2unzlBWfQHJlZER+iUISquyFFv0Qqtv2QsNzFv2AbV7sCaqgDcE8tw6e5/YqGgfI0m403/g==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", @@ -4112,12 +3833,12 @@ } }, "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.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "license": "MIT", "dependencies": { - "tr46": "^5.0.0", + "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" }, "engines": { @@ -4194,52 +3915,6 @@ "node": ">= 12.0.0" } }, - "node_modules/winston-transport/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/winston-transport/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/winston/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/winston/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -4250,23 +3925,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 +3935,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 d26bee37..3eaff1b9 100644 --- a/package.json +++ b/package.json @@ -4,40 +4,40 @@ "description": "WF Emulator", "main": "index.ts", "scripts": { - "start": "node --import ./build/src/pathman.js build/src/index.js", + "start": "node --enable-source-maps --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:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .", "lint:fix": "eslint --fix --ext .ts .", "prettier": "prettier --write .", "update-translations": "cd scripts && node update-translations.js" }, "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.30", + "json-with-bigint": "^3.4.4", + "mongoose": "^8.11.0", + "morgan": "^1.10.0", + "ncp": "^2.0.0", + "typescript": "^5.5", + "warframe-public-export-plus": "^0.5.64", "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", + "@typescript-eslint/eslint-plugin": "^8.28.0", + "@typescript-eslint/parser": "^8.28.0", + "@typescript/native-preview": "^7.0.0-dev.20250523.1", + "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" - }, - "engines": { - "node": ">=18.15.0", - "npm": ">=9.5.0" + "tsconfig-paths": "^4.2.0" } } diff --git a/scripts/update-translations.js b/scripts/update-translations.js index 45067b25..5351afaa 100644 --- a/scripts/update-translations.js +++ b/scripts/update-translations.js @@ -1,10 +1,10 @@ -// 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"); function extractStrings(content) { - const regex = /([a-zA-Z_]+): `([^`]*)`,/g; + const regex = /([a-zA-Z0-9_]+): `([^`]*)`,/g; let matches; const strings = {}; while ((matches = regex.exec(content)) !== null) { @@ -15,7 +15,7 @@ function extractStrings(content) { const source = fs.readFileSync("../static/webui/translations/en.js", "utf8"); const sourceStrings = extractStrings(source); -const sourceLines = source.split("\n"); +const sourceLines = source.substring(0, source.length - 1).split("\n"); fs.readdirSync("../static/webui/translations").forEach(file => { if (fs.lstatSync(`../static/webui/translations/${file}`).isFile() && file !== "en.js") { @@ -36,7 +36,7 @@ fs.readdirSync("../static/webui/translations").forEach(file => { fs.writeSync(fileHandle, ` ${key}: \`[UNTRANSLATED] ${value}\`,\n`); } }); - } else if (line.length) { + } else { fs.writeSync(fileHandle, line + "\n"); } }); diff --git a/src/app.ts b/src/app.ts index 6f9bc0b9..48079166 100644 --- a/src/app.ts +++ b/src/app.ts @@ -15,14 +15,31 @@ import { webuiRouter } from "@/src/routes/webui"; const app = express(); +app.use((req, _res, next) => { + // 38.5.0 introduced "ezip" for encrypted body blobs and "e" for request verification only (encrypted body blobs with no application data). + // The bootstrapper decrypts it for us but having an unsupported Content-Encoding here would still be an issue for Express, so removing it. + if (req.headers["content-encoding"] == "ezip" || req.headers["content-encoding"] == "e") { + req.headers["content-encoding"] = undefined; + } + + // U18 uses application/x-www-form-urlencoded even tho the data is JSON which Express doesn't like. + // U17 sets no Content-Type at all, which Express also doesn't like. + if (!req.headers["content-type"] || req.headers["content-type"] == "application/x-www-form-urlencoded") { + req.headers["content-type"] = "application/octet-stream"; + } + + next(); +}); + app.use(bodyParser.raw()); app.use(express.json({ limit: "4mb" })); -app.use(bodyParser.text()); +app.use(bodyParser.text({ limit: "4mb" })); 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/constants/timeConstants.ts b/src/constants/timeConstants.ts index 32a03742..4411f556 100644 --- a/src/constants/timeConstants.ts +++ b/src/constants/timeConstants.ts @@ -2,15 +2,18 @@ const millisecondsPerSecond = 1000; const secondsPerMinute = 60; const minutesPerHour = 60; const hoursPerDay = 24; +const daysPerWeek = 7; const unixSecond = millisecondsPerSecond; const unixMinute = secondsPerMinute * millisecondsPerSecond; const unixHour = unixMinute * minutesPerHour; const unixDay = hoursPerDay * unixHour; +const unixWeek = daysPerWeek * unixDay; export const unixTimesInMs = { second: unixSecond, minute: unixMinute, hour: unixHour, - day: unixDay + day: unixDay, + week: unixWeek }; 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..b0350434 100644 --- a/src/controllers/api/activateRandomModController.ts +++ b/src/controllers/api/activateRandomModController.ts @@ -1,10 +1,16 @@ +import { toOid } from "@/src/helpers/inventoryHelpers"; +import { + createVeiledRivenFingerprint, + createUnveiledRivenFingerprint, + 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"; +import { config } from "@/src/services/configService"; export const activateRandomModController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -16,83 +22,26 @@ export const activateRandomModController: RequestHandler = async (req, res) => { ItemCount: -1 } ]); - 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 rivenType = getRandomElement(rivenRawToRealWeighted[request.ItemType])!; + const fingerprint = config.instantFinishRivenChallenge + ? createUnveiledRivenFingerprint(ExportUpgrades[rivenType]) + : 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/addFriendController.ts b/src/controllers/api/addFriendController.ts new file mode 100644 index 00000000..bbb629d9 --- /dev/null +++ b/src/controllers/api/addFriendController.ts @@ -0,0 +1,60 @@ +import { toOid } from "@/src/helpers/inventoryHelpers"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Friendship } from "@/src/models/friendModel"; +import { addAccountDataToFriendInfo, addInventoryDataToFriendInfo } from "@/src/services/friendService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IFriendInfo } from "@/src/types/friendTypes"; +import { RequestHandler } from "express"; + +export const addFriendController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const payload = getJSONfromString(String(req.body)); + const promises: Promise[] = []; + const newFriends: IFriendInfo[] = []; + if (payload.friend == "all") { + const [internalFriendships, externalFriendships] = await Promise.all([ + Friendship.find({ owner: accountId }, "friend"), + Friendship.find({ friend: accountId }, "owner") + ]); + for (const externalFriendship of externalFriendships) { + if (!internalFriendships.find(x => x.friend.equals(externalFriendship.owner))) { + promises.push( + Friendship.insertOne({ + owner: accountId, + friend: externalFriendship.owner, + Note: externalFriendship.Note // TOVERIFY: Should the note be copied when accepting a friend request? + }) as unknown as Promise + ); + newFriends.push({ + _id: toOid(externalFriendship.owner) + }); + } + } + } else { + const externalFriendship = await Friendship.findOne({ owner: payload.friend, friend: accountId }, "Note"); + if (externalFriendship) { + promises.push( + Friendship.insertOne({ + owner: accountId, + friend: payload.friend, + Note: externalFriendship.Note + }) as unknown as Promise + ); + newFriends.push({ + _id: { $oid: payload.friend } + }); + } + } + for (const newFriend of newFriends) { + promises.push(addAccountDataToFriendInfo(newFriend)); + promises.push(addInventoryDataToFriendInfo(newFriend)); + } + await Promise.all(promises); + res.json({ + Friends: newFriends + }); +}; + +interface IAddFriendRequest { + friend: string; // oid or "all" in which case all=1 is also a query parameter +} 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/addIgnoredUserController.ts b/src/controllers/api/addIgnoredUserController.ts new file mode 100644 index 00000000..8e8d4441 --- /dev/null +++ b/src/controllers/api/addIgnoredUserController.ts @@ -0,0 +1,30 @@ +import { toOid } from "@/src/helpers/inventoryHelpers"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Account, Ignore } from "@/src/models/loginModel"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IFriendInfo } from "@/src/types/friendTypes"; +import { RequestHandler } from "express"; + +export const addIgnoredUserController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const data = getJSONfromString(String(req.body)); + const ignoreeAccount = await Account.findOne( + { DisplayName: data.playerName.substring(0, data.playerName.length - 1) }, + "_id" + ); + if (ignoreeAccount) { + await Ignore.create({ ignorer: accountId, ignoree: ignoreeAccount._id }); + res.json({ + Ignored: { + _id: toOid(ignoreeAccount._id), + DisplayName: data.playerName + } satisfies IFriendInfo + }); + } else { + res.status(400).end(); + } +}; + +interface IAddIgnoredUserRequest { + playerName: string; +} diff --git a/src/controllers/api/addPendingFriendController.ts b/src/controllers/api/addPendingFriendController.ts new file mode 100644 index 00000000..0ba548f4 --- /dev/null +++ b/src/controllers/api/addPendingFriendController.ts @@ -0,0 +1,52 @@ +import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Friendship } from "@/src/models/friendModel"; +import { Account } from "@/src/models/loginModel"; +import { addInventoryDataToFriendInfo, areFriendsOfFriends } from "@/src/services/friendService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IFriendInfo } from "@/src/types/friendTypes"; +import { RequestHandler } from "express"; + +export const addPendingFriendController: RequestHandler = async (req, res) => { + const payload = getJSONfromString(String(req.body)); + + const account = await Account.findOne({ DisplayName: payload.friend }); + if (!account) { + res.status(400).end(); + return; + } + + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(account._id.toString(), "Settings"); + if ( + inventory.Settings?.FriendInvRestriction == "GIFT_MODE_NONE" || + (inventory.Settings?.FriendInvRestriction == "GIFT_MODE_FRIENDS" && + !(await areFriendsOfFriends(account._id, accountId))) + ) { + res.status(400).send("Friend Invite Restriction"); + return; + } + + await Friendship.insertOne({ + owner: accountId, + friend: account._id, + Note: payload.message + }); + + const friendInfo: IFriendInfo = { + _id: toOid(account._id), + DisplayName: account.DisplayName, + LastLogin: toMongoDate(account.LastLogin), + Note: payload.message + }; + await addInventoryDataToFriendInfo(friendInfo); + res.json({ + Friend: friendInfo + }); +}; + +interface IAddPendingFriendRequest { + friend: string; + message: string; +} diff --git a/src/controllers/api/addToAllianceController.ts b/src/controllers/api/addToAllianceController.ts new file mode 100644 index 00000000..36970a22 --- /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 { getEffectiveAvatarImageType, 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 as string, "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[getEffectiveAvatarImageType(senderInventory)].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..53f0b445 --- /dev/null +++ b/src/controllers/api/addToGuildController.ts @@ -0,0 +1,111 @@ +import { toMongoDate } from "@/src/helpers/inventoryHelpers"; +import { Guild, GuildMember } from "@/src/models/guildModel"; +import { Account } from "@/src/models/loginModel"; +import { addInventoryDataToFriendInfo, areFriends } from "@/src/services/friendService"; +import { hasGuildPermission } from "@/src/services/guildService"; +import { createMessage } from "@/src/services/inboxService"; +import { getEffectiveAvatarImageType, 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 senderAccount = await getAccountForRequest(req); + const inventory = await getInventory(account._id.toString(), "Settings"); + if ( + inventory.Settings?.GuildInvRestriction == "GIFT_MODE_NONE" || + (inventory.Settings?.GuildInvRestriction == "GIFT_MODE_FRIENDS" && + !(await areFriends(account._id, senderAccount._id))) + ) { + res.status(400).json("Invite restricted"); + return; + } + + const guild = (await Guild.findById(payload.GuildId.$oid, "Name Ranks"))!; + 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[getEffectiveAvatarImageType(senderInventory)].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, + LastLogin: toMongoDate(account.LastLogin), + Rank: 7, + Status: 2 + }; + await addInventoryDataToFriendInfo(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..deb05cde --- /dev/null +++ b/src/controllers/api/artifactTransmutationController.ts @@ -0,0 +1,168 @@ +import { fromOid, 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 { IUpgradeFromClient } from "@/src/types/inventoryTypes/inventoryTypes"; +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: fromOid(upgrade.ItemId) }); + }); + + 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; + if (fromOid(upgrade.ItemId) != "000000000000000000000000") { + inventory.Upgrades.pull({ _id: fromOid(upgrade.ItemId) }); + } else { + 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"; + } + }); + + let newModType: string | undefined; + for (const specialModSet of specialModSets) { + if (specialModSet.indexOf(payload.Consumed[0].ItemType) != -1) { + newModType = getRandomElement(specialModSet); + break; + } + } + + if (!newModType) { + // 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 }); + } + }); + + 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: IUpgradeFromClient; + LevelDiff: number; + Consumed: IUpgradeFromClient[]; + Cost: number; + FusionPointCost: number; + RivenTransmute?: boolean; +} + +const specialModSets: string[][] = [ + [ + "/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalFourMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalFiveMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalSixMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalSevenMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalEightMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalWildcardMod" + ], + [ + "/Lotus/Upgrades/Mods/Immortal/AntivirusOneMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusTwoMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusThreeMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusFourMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusFiveMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusSixMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusSevenMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusEightMod" + ], + [ + "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod", + "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod", + "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod", + "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod", + "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod" + ] +]; 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..1c7cb792 100644 --- a/src/controllers/api/changeDojoRootController.ts +++ b/src/controllers/api/changeDojoRootController.ts @@ -1,15 +1,28 @@ 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; + } + + // Example POST body: {"pivot":[0, 0, -64],"components":"{\"670429301ca0a63848ccc467\":{\"R\":[0,0,0],\"P\":[0,3,32]},\"6704254a1ca0a63848ccb33c\":{\"R\":[0,0,0],\"P\":[0,9.25,-32]},\"670429461ca0a63848ccc731\":{\"R\":[-90,0,0],\"P\":[-47.999992370605,3,16]}}"} + if (req.body) { + logger.debug(`data provided to ${req.path}: ${String(req.body)}`); + throw new Error("dojo reparent operation should not need deco repositioning"); // because we always provide SortId + } const idToNode: Record = {}; - guild.DojoComponents!.forEach(x => { + guild.DojoComponents.forEach(x => { idToNode[x._id.toString()] = { component: x, parent: undefined, @@ -18,7 +31,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); @@ -36,29 +49,19 @@ export const changeDojoRootController: RequestHandler = async (req, res) => { newRoot.component.pp = undefined; newRoot.parent = undefined; - // Don't even ask me why this is needed because I don't know either + // Set/update SortId in top-to-bottom order const stack: INode[] = [newRoot]; - let i = 0; - const idMap: Record = {}; while (stack.length != 0) { const top = stack.shift()!; - idMap[top.component._id.toString()] = new Types.ObjectId( - (++i).toString(16).padStart(8, "0") + top.component._id.toString().substr(8) - ); + top.component.SortId = new Types.ObjectId(); top.children.forEach(x => stack.push(x)); } - guild.DojoComponents!.forEach(x => { - x._id = idMap[x._id.toString()]; - if (x.pi) { - x.pi = idMap[x.pi.toString()]; - } - }); logger.debug("New tree:\n" + treeToString(newRoot)); 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 779f4587..e519d170 100644 --- a/src/controllers/api/claimCompletedRecipeController.ts +++ b/src/controllers/api/claimCompletedRecipeController.ts @@ -4,28 +4,33 @@ import { RequestHandler } from "express"; import { logger } from "@/src/utils/logger"; import { getRecipe } from "@/src/services/itemDataService"; -import { IOid } from "@/src/types/commonTypes"; +import { IOid, IOidWithLegacySupport } from "@/src/types/commonTypes"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getAccountForRequest } from "@/src/services/loginService"; import { getInventory, updateCurrency, addItem, - addMiscItems, addRecipes, - updateCurrencyByAccountId + occupySlot, + combineInventoryChanges } from "@/src/services/inventoryService"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { InventorySlot, IPendingRecipeDatabase } from "@/src/types/inventoryTypes/inventoryTypes"; +import { toOid2 } from "@/src/helpers/inventoryHelpers"; +import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; +import { IRecipe } from "warframe-public-export-plus"; +import { config } from "@/src/services/configService"; -export interface IClaimCompletedRecipeRequest { +interface IClaimCompletedRecipeRequest { RecipeIds: IOid[]; } export const claimCompletedRecipeController: RequestHandler = async (req, res) => { const claimCompletedRecipeRequest = getJSONfromString(String(req.body)); - const accountId = await getAccountIdForRequest(req); - if (!accountId) throw new Error("no account id"); - - const inventory = await getInventory(accountId); + const account = await getAccountForRequest(req); + const inventory = await getInventory(account._id.toString()); const pendingRecipe = inventory.PendingRecipes.id(claimCompletedRecipeRequest.RecipeIds[0].$oid); if (!pendingRecipe) { throw new Error(`no pending recipe found with id ${claimCompletedRecipeRequest.RecipeIds[0].$oid}`); @@ -37,7 +42,6 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = // } inventory.PendingRecipes.pull(pendingRecipe._id); - await inventory.save(); const recipe = getRecipe(pendingRecipe.ItemType); if (!recipe) { @@ -45,21 +49,15 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = } if (req.query.cancel) { - const inventory = await getInventory(accountId); - const currencyChanges = updateCurrency(inventory, recipe.buildPrice * -1, false); - addMiscItems(inventory, recipe.ingredients); + const inventoryChanges: IInventoryChanges = {}; + await refundRecipeIngredients(inventory, inventoryChanges, recipe, pendingRecipe); 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 }); + let BrandedSuits: undefined | IOidWithLegacySupport[]; if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") { - const inventory = await getInventory(accountId); inventory.PendingSpectreLoadouts ??= []; inventory.SpectreLoadouts ??= []; @@ -77,37 +75,86 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = ); inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]); inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1); - await inventory.save(); } + } else if (recipe.secretIngredientAction == "SIA_UNBRAND") { + inventory.BrandedSuits!.splice( + inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)), + 1 + ); + BrandedSuits = [toOid2(pendingRecipe.SuitToUnbrand!, account.BuildLabel)]; } - let InventoryChanges = {}; + let InventoryChanges: IInventoryChanges = {}; if (recipe.consumeOnUse) { - const recipeChanges = [ + addRecipes(inventory, [ { ItemType: pendingRecipe.ItemType, ItemCount: -1 } - ]; - - InventoryChanges = { ...InventoryChanges, Recipes: recipeChanges }; - - const inventory = await getInventory(accountId); - addRecipes(inventory, recipeChanges); - await inventory.save(); + ]); } 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, - ...(await updateCurrencyByAccountId(recipe.skipBuildTimePrice, true, accountId)) + ...updateCurrency(inventory, cost, true) }; } - const inventory = await getInventory(accountId); - InventoryChanges = { - ...InventoryChanges, - ...(await addItem(inventory, recipe.resultType, recipe.num)).InventoryChanges - }; + if (recipe.secretIngredientAction != "SIA_UNBRAND") { + InventoryChanges = { + ...InventoryChanges, + ...(await addItem( + inventory, + recipe.resultType, + recipe.num, + false, + undefined, + pendingRecipe.TargetFingerprint + )) + }; + } + if (config.claimingBlueprintRefundsIngredients) { + await refundRecipeIngredients(inventory, InventoryChanges, recipe, pendingRecipe); + } await inventory.save(); - res.json({ InventoryChanges }); + res.json({ InventoryChanges, BrandedSuits }); + } +}; + +const refundRecipeIngredients = async ( + inventory: TInventoryDatabaseDocument, + inventoryChanges: IInventoryChanges, + recipe: IRecipe, + pendingRecipe: IPendingRecipeDatabase +): Promise => { + updateCurrency(inventory, recipe.buildPrice * -1, false, inventoryChanges); + + 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) + ); + } } }; diff --git a/src/controllers/api/claimLibraryDailyTaskRewardController.ts b/src/controllers/api/claimLibraryDailyTaskRewardController.ts new file mode 100644 index 00000000..3d582e46 --- /dev/null +++ b/src/controllers/api/claimLibraryDailyTaskRewardController.ts @@ -0,0 +1,31 @@ +import { addFusionPoints, 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; + + addFusionPoints(inventory, 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/clearDialogueHistoryController.ts b/src/controllers/api/clearDialogueHistoryController.ts index 96c8e1c6..f24f360a 100644 --- a/src/controllers/api/clearDialogueHistoryController.ts +++ b/src/controllers/api/clearDialogueHistoryController.ts @@ -7,6 +7,8 @@ export const clearDialogueHistoryController: RequestHandler = async (req, res) = const inventory = await getInventory(accountId); const request = JSON.parse(String(req.body)) as IClearDialogueRequest; if (inventory.DialogueHistory && inventory.DialogueHistory.Dialogues) { + inventory.DialogueHistory.Resets ??= 0; + inventory.DialogueHistory.Resets += 1; for (const dialogueName of request.Dialogues) { const index = inventory.DialogueHistory.Dialogues.findIndex(x => x.DialogueName == dialogueName); if (index != -1) { 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/completeCalendarEventController.ts b/src/controllers/api/completeCalendarEventController.ts new file mode 100644 index 00000000..20c8abb3 --- /dev/null +++ b/src/controllers/api/completeCalendarEventController.ts @@ -0,0 +1,41 @@ +import { getCalendarProgress, getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; +import { getWorldState } from "@/src/services/worldStateService"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { RequestHandler } from "express"; + +// GET request; query parameters: CompletedEventIdx=0&Iteration=4&Version=19&Season=CST_SUMMER +export const completeCalendarEventController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const calendarProgress = getCalendarProgress(inventory); + const currentSeason = getWorldState().KnownCalendarSeasons[0]; + let inventoryChanges: IInventoryChanges = {}; + let dayIndex = 0; + for (const day of currentSeason.Days) { + if (day.events.length == 0 || day.events[0].type != "CET_CHALLENGE") { + if (dayIndex == calendarProgress.SeasonProgress.LastCompletedDayIdx) { + if (day.events.length != 0) { + const selection = day.events[parseInt(req.query.CompletedEventIdx as string)]; + if (selection.type == "CET_REWARD") { + inventoryChanges = (await handleStoreItemAcquisition(selection.reward!, inventory)) + .InventoryChanges; + } else if (selection.type == "CET_UPGRADE") { + calendarProgress.YearProgress.Upgrades.push(selection.upgrade!); + } else if (selection.type != "CET_PLOT") { + throw new Error(`unexpected selection type: ${selection.type}`); + } + } + break; + } + ++dayIndex; + } + } + calendarProgress.SeasonProgress.LastCompletedDayIdx++; + await inventory.save(); + res.json({ + InventoryChanges: inventoryChanges, + CalendarProgress: inventory.CalendarProgress + }); +}; diff --git a/src/controllers/api/completeRandomModChallengeController.ts b/src/controllers/api/completeRandomModChallengeController.ts new file mode 100644 index 00000000..a4e3cf08 --- /dev/null +++ b/src/controllers/api/completeRandomModChallengeController.ts @@ -0,0 +1,45 @@ +import { RequestHandler } from "express"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { createUnveiledRivenFingerprint } from "@/src/helpers/rivenHelper"; +import { ExportUpgrades } from "warframe-public-export-plus"; + +export const completeRandomModChallengeController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const request = getJSONfromString(String(req.body)); + let inventoryChanges: IInventoryChanges = {}; + + // Remove 20 plat or riven cipher + if ((req.query.p as string) == "1") { + inventoryChanges = { ...updateCurrency(inventory, 20, true) }; + } else { + const miscItemChanges: IMiscItem[] = [ + { + ItemType: "/Lotus/Types/Items/MiscItems/RivenIdentifier", + ItemCount: -1 + } + ]; + addMiscItems(inventory, miscItemChanges); + inventoryChanges.MiscItems = miscItemChanges; + } + + // Update riven fingerprint to a randomised unveiled state + const upgrade = inventory.Upgrades.id(request.ItemId)!; + const meta = ExportUpgrades[upgrade.ItemType]; + upgrade.UpgradeFingerprint = JSON.stringify(createUnveiledRivenFingerprint(meta)); + + await inventory.save(); + + res.json({ + InventoryChanges: inventoryChanges, + Fingerprint: upgrade.UpgradeFingerprint + }); +}; + +interface ICompleteRandomModChallengeRequest { + ItemId: string; +} 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..11deac0c --- /dev/null +++ b/src/controllers/api/confirmGuildInvitationController.ts @@ -0,0 +1,118 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Guild, GuildMember } from "@/src/models/guildModel"; +import { Account } from "@/src/models/loginModel"; +import { + deleteGuild, + getGuildClient, + giveClanKey, + hasGuildPermission, + removeDojoKeyItems +} from "@/src/services/guildService"; +import { 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); + giveClanKey(inventory, inventoryChanges); + 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)), + 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 LevelKeys Recipes"); + inventory.GuildId = new Types.ObjectId(req.query.clanId as string); + giveClanKey(inventory); + 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..865eb868 --- /dev/null +++ b/src/controllers/api/contributeGuildClassController.ts @@ -0,0 +1,67 @@ +import { toMongoDate } from "@/src/helpers/inventoryHelpers"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Guild } from "@/src/models/guildModel"; +import { checkClanAscensionHasRequiredContributors } from "@/src/services/guildService"; +import { addFusionPoints, 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)); + + await checkClanAscensionHasRequiredContributors(guild); + + await guild.save(); + + // Either way, endo is given to the contributor. + const inventory = await getInventory(accountId, "FusionPoints"); + addFusionPoints(inventory, 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..ea0e48a6 100644 --- a/src/controllers/api/createGuildController.ts +++ b/src/controllers/api/createGuildController.ts @@ -1,35 +1,42 @@ import { RequestHandler } from "express"; -import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getAccountForRequest } 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, giveClanKey } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; export const createGuildController: RequestHandler = async (req, res) => { - const accountId = await getAccountIdForRequest(req); + const account = await getAccountForRequest(req); const payload = getJSONfromString(String(req.body)); + // Remove pending applications for this account + await GuildMember.deleteMany({ accountId: account._id, 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: account._id, + 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", - ItemCount: 1 - }); + const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes"); + inventory.GuildId = guild._id; + const inventoryChanges: IInventoryChanges = {}; + giveClanKey(inventory, inventoryChanges); + await inventory.save(); - await inventory.save(); - } - - res.json(guild); + res.json({ + ...(await getGuildClient(guild, account)), + InventoryChanges: inventoryChanges + }); }; 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/crewMembersController.ts b/src/controllers/api/crewMembersController.ts new file mode 100644 index 00000000..a4f2ea2a --- /dev/null +++ b/src/controllers/api/crewMembersController.ts @@ -0,0 +1,54 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { ICrewMemberClient } from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; +import { Types } from "mongoose"; + +export const crewMembersController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "CrewMembers NemesisHistory"); + const data = getJSONfromString(String(req.body)); + if (data.crewMember.SecondInCommand) { + clearOnCall(inventory); + } + if (data.crewMember.ItemId.$oid == "000000000000000000000000") { + const convertedNemesis = inventory.NemesisHistory!.find(x => x.fp == data.crewMember.NemesisFingerprint)!; + convertedNemesis.SecondInCommand = data.crewMember.SecondInCommand; + } else { + const dbCrewMember = inventory.CrewMembers.id(data.crewMember.ItemId.$oid)!; + dbCrewMember.AssignedRole = data.crewMember.AssignedRole; + dbCrewMember.SkillEfficiency = data.crewMember.SkillEfficiency; + dbCrewMember.WeaponConfigIdx = data.crewMember.WeaponConfigIdx; + dbCrewMember.WeaponId = new Types.ObjectId(data.crewMember.WeaponId.$oid); + dbCrewMember.Configs = data.crewMember.Configs; + dbCrewMember.SecondInCommand = data.crewMember.SecondInCommand; + } + await inventory.save(); + res.json({ + crewMemberId: data.crewMember.ItemId.$oid, + NemesisFingerprint: data.crewMember.NemesisFingerprint + }); +}; + +interface ICrewMembersRequest { + crewMember: ICrewMemberClient; +} + +const clearOnCall = (inventory: TInventoryDatabaseDocument): void => { + for (const cm of inventory.CrewMembers) { + if (cm.SecondInCommand) { + cm.SecondInCommand = false; + return; + } + } + if (inventory.NemesisHistory) { + for (const cm of inventory.NemesisHistory) { + if (cm.SecondInCommand) { + cm.SecondInCommand = false; + return; + } + } + } +}; diff --git a/src/controllers/api/crewShipIdentifySalvageController.ts b/src/controllers/api/crewShipIdentifySalvageController.ts new file mode 100644 index 00000000..cc77589e --- /dev/null +++ b/src/controllers/api/crewShipIdentifySalvageController.ts @@ -0,0 +1,84 @@ +import { + addCrewShipSalvagedWeaponSkin, + addCrewShipRawSalvage, + getInventory, + addEquipment +} from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; +import { ICrewShipComponentFingerprint, IInnateDamageFingerprint } from "@/src/types/inventoryTypes/inventoryTypes"; +import { ExportCustoms, ExportRailjackWeapons, ExportUpgrades } from "warframe-public-export-plus"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { getRandomInt } from "@/src/services/rngService"; +import { IFingerprintStat } from "@/src/helpers/rivenHelper"; +import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes"; + +export const crewShipIdentifySalvageController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory( + accountId, + "CrewShipSalvagedWeaponSkins CrewShipSalvagedWeapons CrewShipRawSalvage" + ); + const payload = getJSONfromString(String(req.body)); + + const inventoryChanges: IInventoryChanges = {}; + if (payload.ItemType in ExportCustoms) { + const meta = ExportCustoms[payload.ItemType]; + let upgradeFingerprint: ICrewShipComponentFingerprint = { compat: payload.ItemType, buffs: [] }; + if (meta.subroutines) { + upgradeFingerprint = { + SubroutineIndex: getRandomInt(0, meta.subroutines.length - 1), + ...upgradeFingerprint + }; + } + for (const upgrade of meta.randomisedUpgrades!) { + upgradeFingerprint.buffs.push({ Tag: upgrade.tag, Value: Math.trunc(Math.random() * 0x40000000) }); + } + addCrewShipSalvagedWeaponSkin( + inventory, + payload.ItemType, + JSON.stringify(upgradeFingerprint), + inventoryChanges + ); + } else { + const meta = ExportRailjackWeapons[payload.ItemType]; + let defaultOverwrites: Partial | undefined; + if (meta.defaultUpgrades?.[0]) { + const upgradeType = meta.defaultUpgrades[0].ItemType; + const upgradeMeta = ExportUpgrades[upgradeType]; + const buffs: IFingerprintStat[] = []; + for (const buff of upgradeMeta.upgradeEntries!) { + buffs.push({ + Tag: buff.tag, + Value: Math.trunc(Math.random() * 0x40000000) + }); + } + defaultOverwrites = { + UpgradeType: upgradeType, + UpgradeFingerprint: JSON.stringify({ + compat: payload.ItemType, + buffs + } satisfies IInnateDamageFingerprint) + }; + } + addEquipment(inventory, "CrewShipSalvagedWeapons", payload.ItemType, defaultOverwrites, inventoryChanges); + } + + inventoryChanges.CrewShipRawSalvage = [ + { + ItemType: payload.ItemType, + ItemCount: -1 + } + ]; + addCrewShipRawSalvage(inventory, inventoryChanges.CrewShipRawSalvage); + + await inventory.save(); + res.json({ + InventoryChanges: inventoryChanges + }); +}; + +interface ICrewShipIdentifySalvageRequest { + ItemType: string; +} 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/dojoController.ts b/src/controllers/api/dojoController.ts index c14d9316..9c54b449 100644 --- a/src/controllers/api/dojoController.ts +++ b/src/controllers/api/dojoController.ts @@ -1,5 +1,11 @@ import { RequestHandler } from "express"; +// Arbiter Dojo endpoints, not really used by us as we don't provide a ContentURL. + export const dojoController: RequestHandler = (_req, res) => { res.json("-1"); // Tell client to use authorised request. }; + +export const setDojoURLController: RequestHandler = (_req, res) => { + res.end(); +}; diff --git a/src/controllers/api/dronesController.ts b/src/controllers/api/dronesController.ts index bff5086c..f319773b 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 = + !config.noResourceExtractorDronesDamage && 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/endlessXpController.ts b/src/controllers/api/endlessXpController.ts index 656ebb05..1be4bd2b 100644 --- a/src/controllers/api/endlessXpController.ts +++ b/src/controllers/api/endlessXpController.ts @@ -1,60 +1,529 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getInventory } from "@/src/services/inventoryService"; +import { combineInventoryChanges, getInventory } from "@/src/services/inventoryService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { TEndlessXpCategory } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IEndlessXpReward, IInventoryClient, TEndlessXpCategory } from "@/src/types/inventoryTypes/inventoryTypes"; +import { logger } from "@/src/utils/logger"; +import { ExportRewards, ICountedStoreItem } from "warframe-public-export-plus"; +import { getRandomElement } from "@/src/services/rngService"; +import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; export const endlessXpController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId); const payload = getJSONfromString(String(req.body)); - - inventory.EndlessXP ??= []; - const entry = inventory.EndlessXP.find(x => x.Category == payload.Category); - if (entry) { - entry.Choices = payload.Choices; - } else { - inventory.EndlessXP.push({ - Category: payload.Category, - Choices: payload.Choices - }); - } - await inventory.save(); - - res.json({ - NewProgress: { - Category: payload.Category, - Earn: 0, - Claim: 0, - BonusAvailable: { - $date: { - $numberLong: "9999999999999" - } - }, - Expiry: { - $date: { - $numberLong: "9999999999999" - } - }, - Choices: payload.Choices, - PendingRewards: [ - { - RequiredTotalXp: 190, - Rewards: [ - { - StoreItem: "/Lotus/StoreItems/Upgrades/Mods/Aura/PlayerHealthAuraMod", - ItemCount: 1 - } - ] - } - // ... - ] + if (payload.Mode == "r") { + const inventory = await getInventory(accountId, "EndlessXP"); + inventory.EndlessXP ??= []; + let entry = inventory.EndlessXP.find(x => x.Category == payload.Category); + if (!entry) { + entry = { + Category: payload.Category, + Earn: 0, + Claim: 0, + Choices: payload.Choices, + PendingRewards: [] + }; + inventory.EndlessXP.push(entry); } - }); + + const weekStart = 1734307200_000 + Math.trunc((Date.now() - 1734307200_000) / 604800000) * 604800000; + const weekEnd = weekStart + 604800000; + + entry.Earn = 0; + entry.Claim = 0; + entry.BonusAvailable = new Date(weekStart); + entry.Expiry = new Date(weekEnd); + entry.Choices = payload.Choices; + entry.PendingRewards = + payload.Category == "EXC_HARD" + ? generateHardModeRewards(payload.Choices) + : generateNormalModeRewards(payload.Choices); + + await inventory.save(); + res.json({ + NewProgress: inventory.toJSON().EndlessXP!.find(x => x.Category == payload.Category)! + }); + } else if (payload.Mode == "c") { + const inventory = await getInventory(accountId); + const entry = inventory.EndlessXP!.find(x => x.Category == payload.Category)!; + const inventoryChanges: IInventoryChanges = {}; + for (const reward of entry.PendingRewards) { + if (entry.Claim < reward.RequiredTotalXp && reward.RequiredTotalXp <= entry.Earn) { + combineInventoryChanges( + inventoryChanges, + ( + await handleStoreItemAcquisition( + reward.Rewards[0].StoreItem, + inventory, + reward.Rewards[0].ItemCount + ) + ).InventoryChanges + ); + } + } + entry.Claim = entry.Earn; + await inventory.save(); + res.json({ + InventoryChanges: inventoryChanges, + ClaimedXp: entry.Claim + }); + } else { + logger.debug(`data provided to ${req.path}: ${String(req.body)}`); + throw new Error(`unexpected endlessXp mode: ${payload.Mode}`); + } }; -interface IEndlessXpRequest { - Mode: string; // "r" - Category: TEndlessXpCategory; - Choices: string[]; -} +type IEndlessXpRequest = + | { + Mode: "r"; + Category: TEndlessXpCategory; + Choices: string[]; + } + | { + Mode: "c" | "something else"; + Category: TEndlessXpCategory; + }; + +const generateRandomRewards = (deckName: string): ICountedStoreItem[] => { + const reward = getRandomElement(ExportRewards[deckName][0])!; + return [ + { + StoreItem: reward.type, + ItemCount: reward.itemCount + } + ]; +}; + +const normalModeChosenRewards: Record = { + Excalibur: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Excalibur/RadialJavelinAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburBlueprint" + ], + Trinity: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinityHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinityChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Trinity/EnergyVampireAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinitySystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinityBlueprint" + ], + Ember: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Ember/WorldOnFireAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberBlueprint" + ], + Loki: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKIHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKIChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Loki/InvisibilityAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKISystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKIBlueprint" + ], + Mag: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Mag/CrushAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagBlueprint" + ], + Rhino: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Rhino/RhinoChargeAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoBlueprint" + ], + Ash: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Ninja/GlaiveAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshBlueprint" + ], + Frost: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Frost/IceShieldAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostBlueprint" + ], + Nyx: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Jade/SelfBulletAttractorAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxBlueprint" + ], + Saryn: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Saryn/PoisonAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynBlueprint" + ], + Vauban: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Trapper/LevTrapAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperBlueprint" + ], + Nova: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaChassisBlueprint", + "/Lotus/StoreItems/Powersuits/AntiMatter/MolecularPrimeAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaBlueprint" + ], + Nekros: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Necro/CloneTheDeadAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroBlueprint" + ], + Valkyr: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Berserker/IntimidateAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerBlueprint" + ], + Oberon: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Paladin/RegenerationAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinBlueprint" + ], + Hydroid: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Pirate/CannonBarrageAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidBlueprint" + ], + Mirage: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Harlequin/LightAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinBlueprint" + ], + Limbo: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Magician/TearInSpaceAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianBlueprint" + ], + Mesa: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Cowgirl/GunFuPvPAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerBlueprint" + ], + Chroma: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Dragon/DragonLuckAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaBlueprint" + ], + Atlas: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Brawler/BrawlerPassiveAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerBlueprint" + ], + Ivara: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Ranger/RangerStealAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerBlueprint" + ], + Inaros: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummyHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummyChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Sandman/SandmanSwarmAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummySystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummyBlueprint" + ], + Titania: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairyHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairyChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Fairy/FairyFlightAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairySystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairyBlueprint" + ], + Nidus: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Infestation/InfestPodsAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusBlueprint" + ], + Octavia: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Bard/BardCharmAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaBlueprint" + ], + Harrow: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Priest/PriestPactAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestBlueprint" + ], + Gara: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Glass/GlassFragmentAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassBlueprint" + ], + Khora: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Khora/KhoraCrackAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraBlueprint" + ], + Revenant: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Revenant/RevenantMarkAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantBlueprint" + ], + Garuda: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Garuda/GarudaUnstoppableAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaBlueprint" + ], + Baruuk: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistChassisBlueprint", + "/Lotus/StoreItems/Powersuits/Pacifist/PacifistFistAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistBlueprint" + ], + Hildryn: [ + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeHelmetBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeChassisBlueprint", + "/Lotus/StoreItems/Powersuits/IronFrame/IronFrameStripAugmentCard", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeSystemsBlueprint", + "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeBlueprint" + ] +}; + +const generateNormalModeRewards = (choices: string[]): IEndlessXpReward[] => { + const choiceRewards = normalModeChosenRewards[choices[0]]; + return [ + { + RequiredTotalXp: 190, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalSilverRewards" + ) + }, + { + RequiredTotalXp: 400, + Rewards: [ + { + StoreItem: choiceRewards[0], + ItemCount: 1 + } + ] + }, + { + RequiredTotalXp: 630, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalSilverRewards" + ) + }, + { + RequiredTotalXp: 890, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalMODRewards" + ) + }, + { + RequiredTotalXp: 1190, + Rewards: [ + { + StoreItem: choiceRewards[1], + ItemCount: 1 + } + ] + }, + { + RequiredTotalXp: 1540, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalGoldRewards" + ) + }, + { + RequiredTotalXp: 1950, + Rewards: [ + { + StoreItem: choiceRewards[2], + ItemCount: 1 + } + ] + }, + { + RequiredTotalXp: 2430, + Rewards: [ + { + StoreItem: choiceRewards[3], + ItemCount: 1 + } + ] + }, + { + RequiredTotalXp: 2990, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalArcaneRewards" + ) + }, + { + RequiredTotalXp: 3640, + Rewards: [ + { + StoreItem: choiceRewards[4], + ItemCount: 1 + } + ] + } + ]; +}; + +const hardModeChosenRewards: Record = { + Braton: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BratonIncarnonUnlocker", + Lato: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/LatoIncarnonUnlocker", + Skana: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/SkanaIncarnonUnlocker", + Paris: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/ParisIncarnonUnlocker", + Kunai: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/KunaiIncarnonUnlocker", + Boar: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BoarIncarnonUnlocker", + Gammacor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/GammacorIncarnonUnlocker", + Anku: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/AnkuIncarnonUnlocker", + Gorgon: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/GorgonIncarnonUnlocker", + Angstrum: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/AngstrumIncarnonUnlocker", + Bo: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/BoIncarnonUnlocker", + Latron: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/LatronIncarnonUnlocker", + Furis: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/FurisIncarnonUnlocker", + Furax: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/FuraxIncarnonUnlocker", + Strun: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/StrunIncarnonUnlocker", + Lex: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/LexIncarnonUnlocker", + Magistar: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/MagistarIncarnonUnlocker", + Boltor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BoltorIncarnonUnlocker", + Bronco: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/BroncoIncarnonUnlocker", + CeramicDagger: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/CeramicDaggerIncarnonUnlocker", + Torid: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/ToridIncarnonUnlocker", + DualToxocyst: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/DualToxocystIncarnonUnlocker", + DualIchor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/DualIchorIncarnonUnlocker", + Miter: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/MiterIncarnonUnlocker", + Atomos: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/AtomosIncarnonUnlocker", + AckAndBrunt: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/AckAndBruntIncarnonUnlocker", + Soma: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/SomaIncarnonUnlocker", + Vasto: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/VastoIncarnonUnlocker", + NamiSolo: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/NamiSoloIncarnonUnlocker", + Burston: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BurstonIncarnonUnlocker", + Zylok: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/ZylokIncarnonUnlocker", + Sibear: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/SibearIncarnonUnlocker", + Dread: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/DreadIncarnonUnlocker", + Despair: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/DespairIncarnonUnlocker", + Hate: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/HateIncarnonUnlocker", + Dera: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/DeraIncarnonUnlocker", + Cestra: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/CestraIncarnonUnlocker", + Okina: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/OkinaIncarnonUnlocker", + Sybaris: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/SybarisIncarnonUnlocker", + Sicarus: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/SicarusIncarnonUnlocker", + RivenPrimary: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawRifleRandomMod", + RivenSecondary: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawPistolRandomMod", + RivenMelee: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawMeleeRandomMod", + Kuva: "/Lotus/Types/Game/DuviriEndless/CircuitSteelPathBIGKuvaReward" +}; + +const generateHardModeRewards = (choices: string[]): IEndlessXpReward[] => { + return [ + { + RequiredTotalXp: 285, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards" + ) + }, + { + RequiredTotalXp: 600, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathArcaneRewards" + ) + }, + { + RequiredTotalXp: 945, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards" + ) + }, + { + RequiredTotalXp: 1335, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards" + ) + }, + { + RequiredTotalXp: 1785, + Rewards: [ + { + StoreItem: hardModeChosenRewards[choices[0]], + ItemCount: 1 + } + ] + }, + { + RequiredTotalXp: 2310, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathGoldRewards" + ) + }, + { + RequiredTotalXp: 2925, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathGoldRewards" + ) + }, + { + RequiredTotalXp: 3645, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathArcaneRewards" + ) + }, + { + RequiredTotalXp: 4485, + Rewards: generateRandomRewards( + "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSteelEssenceRewards" + ) + }, + { + RequiredTotalXp: 5460, + Rewards: [ + { + StoreItem: hardModeChosenRewards[choices[1]], + ItemCount: 1 + } + ] + } + ]; +}; diff --git a/src/controllers/api/entratiLabConquestModeController.ts b/src/controllers/api/entratiLabConquestModeController.ts new file mode 100644 index 00000000..e5b6c818 --- /dev/null +++ b/src/controllers/api/entratiLabConquestModeController.ts @@ -0,0 +1,71 @@ +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.EntratiLabConquestCacheScoreMission = 0; + inventory.EntratiLabConquestActiveFrameVariants = []; + } + if (inventory.EchoesHexConquestUnlocked) { + inventory.EchoesHexConquestUnlocked = 0; + inventory.EchoesHexConquestCacheScoreMission = 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/fishmongerController.ts b/src/controllers/api/fishmongerController.ts index 898f5e4c..d85f7a4c 100644 --- a/src/controllers/api/fishmongerController.ts +++ b/src/controllers/api/fishmongerController.ts @@ -1,10 +1,9 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper"; -import { addMiscItems, getInventory, getStandingLimit, updateStandingLimit } from "@/src/services/inventoryService"; +import { addMiscItems, addStanding, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { RequestHandler } from "express"; -import { ExportResources, ExportSyndicates } from "warframe-public-export-plus"; +import { ExportResources } from "warframe-public-export-plus"; export const fishmongerController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -31,32 +30,15 @@ export const fishmongerController: RequestHandler = async (req, res) => { miscItemChanges.push({ ItemType: fish.ItemType, ItemCount: fish.ItemCount * -1 }); } addMiscItems(inventory, miscItemChanges); - if (gainedStanding && syndicateTag) { - let syndicate = inventory.Affiliations.find(x => x.Tag == syndicateTag); - if (!syndicate) { - syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: syndicateTag, Standing: 0 }) - 1]; - } - const syndicateMeta = ExportSyndicates[syndicateTag]; - - const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0); - if (syndicate.Standing + gainedStanding > max) { - gainedStanding = max - syndicate.Standing; - } - if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) { - gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin); - } - - syndicate.Standing += gainedStanding; - - updateStandingLimit(inventory, syndicateMeta.dailyLimitBin, gainedStanding); - } + let affiliationMod; + if (gainedStanding && syndicateTag) affiliationMod = addStanding(inventory, syndicateTag, gainedStanding); await inventory.save(); res.json({ InventoryChanges: { MiscItems: miscItemChanges }, SyndicateTag: syndicateTag, - StandingChange: gainedStanding + StandingChange: affiliationMod?.Standing || 0 }); }; diff --git a/src/controllers/api/focusController.ts b/src/controllers/api/focusController.ts index 29c92bf2..90b55a2e 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,10 +54,19 @@ 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(); - res.end(); + + await Inventory.updateOne( + { + accountOwnerId: accountId + }, + { + FocusAbility: focusType + } + ); + + res.json({ + FocusUpgrade: { ItemType: focusType } + }); break; } case FocusOperation.UnlockUpgrade: { @@ -98,13 +106,15 @@ export const focusController: RequestHandler = async (req, res) => { } case FocusOperation.SentTrainingAmplifier: { const request = JSON.parse(String(req.body)) as ISentTrainingAmplifierRequest; - const parts: string[] = [ - "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingGrip", - "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingChassis", - "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingBarrel" - ]; const inventory = await getInventory(accountId); - const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, parts); + const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, { + ModularParts: [ + "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingGrip", + "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingChassis", + "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingBarrel" + ] + }); + 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/gardeningController.ts b/src/controllers/api/gardeningController.ts new file mode 100644 index 00000000..1913bd63 --- /dev/null +++ b/src/controllers/api/gardeningController.ts @@ -0,0 +1,84 @@ +import { toMongoDate } from "@/src/helpers/inventoryHelpers"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { addMiscItem, getInventory } from "@/src/services/inventoryService"; +import { toStoreItem } from "@/src/services/itemDataService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService"; +import { IMongoDate } from "@/src/types/commonTypes"; +import { IMissionReward } from "@/src/types/missionTypes"; +import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { IGardeningClient } from "@/src/types/shipTypes"; +import { RequestHandler } from "express"; +import { dict_en, ExportResources } from "warframe-public-export-plus"; + +export const gardeningController: RequestHandler = async (req, res) => { + const data = getJSONfromString(String(req.body)); + if (data.Mode != "HarvestAll") { + throw new Error(`unexpected gardening mode: ${data.Mode}`); + } + + const accountId = await getAccountIdForRequest(req); + const [inventory, personalRooms] = await Promise.all([ + getInventory(accountId, "MiscItems"), + getPersonalRooms(accountId, "Apartment") + ]); + + // Harvest plants + const inventoryChanges: IInventoryChanges = {}; + const rewards: Record = {}; + for (const planter of personalRooms.Apartment.Gardening.Planters) { + rewards[planter.Name] = []; + for (const plant of planter.Plants) { + const itemType = + "/Lotus/Types/Gameplay/Duviri/Resource/DuviriPlantItem" + + plant.PlantType.substring(plant.PlantType.length - 1); + const itemCount = Math.random() < 0.775 ? 2 : 4; + + addMiscItem(inventory, itemType, itemCount, inventoryChanges); + + rewards[planter.Name].push([ + { + StoreItem: toStoreItem(itemType), + TypeName: itemType, + ItemCount: itemCount, + DailyCooldown: false, + Rarity: itemCount == 2 ? 0.7743589743589744 : 0.22564102564102564, + TweetText: `${itemCount}x ${dict_en[ExportResources[itemType].name]} (Resource)`, + ProductCategory: "MiscItems" + } + ]); + } + } + + // Refresh garden + personalRooms.Apartment.Gardening = createGarden(); + + await Promise.all([inventory.save(), personalRooms.save()]); + + const planter = personalRooms.Apartment.Gardening.Planters[personalRooms.Apartment.Gardening.Planters.length - 1]; + const plant = planter.Plants[planter.Plants.length - 1]; + res.json({ + GardenTagName: planter.Name, + PlantType: plant.PlantType, + PlotIndex: plant.PlotIndex, + EndTime: toMongoDate(plant.EndTime), + InventoryChanges: inventoryChanges, + Gardening: personalRooms.toJSON().Apartment.Gardening, + Rewards: rewards + } satisfies IGardeningResponse); +}; + +interface IGardeningRequest { + Mode: string; +} + +interface IGardeningResponse { + GardenTagName: string; + PlantType: string; + PlotIndex: number; + EndTime: IMongoDate; + InventoryChanges: IInventoryChanges; + Gardening: IGardeningClient; + Rewards: Record; +} 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..a4ea7bcd 100644 --- a/src/controllers/api/getAllianceController.ts +++ b/src/controllers/api/getAllianceController.ts @@ -1,7 +1,26 @@ +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 }; +// POST request since U27 +/*interface IGetAllianceRequest { + memberCount: number; + clanLeaderName: string; + clanLeaderId: string; +}*/ diff --git a/src/controllers/api/getFriendsController.ts b/src/controllers/api/getFriendsController.ts index 292e107c..684e9bcc 100644 --- a/src/controllers/api/getFriendsController.ts +++ b/src/controllers/api/getFriendsController.ts @@ -1,14 +1,54 @@ -import { Request, Response } from "express"; +import { toOid } from "@/src/helpers/inventoryHelpers"; +import { Friendship } from "@/src/models/friendModel"; +import { addAccountDataToFriendInfo, addInventoryDataToFriendInfo } from "@/src/services/friendService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IFriendInfo } from "@/src/types/friendTypes"; +import { Request, RequestHandler, Response } from "express"; -const getFriendsController = (_request: Request, response: Response): void => { - response.writeHead(200, { - //Connection: "keep-alive", - //"Content-Encoding": "gzip", - "Content-Type": "text/html", - // charset: "UTF - 8", - "Content-Length": "3" - }); - response.end(Buffer.from([0x7b, 0x7d, 0x0a])); +// POST with {} instead of GET as of 38.5.0 +export const getFriendsController: RequestHandler = async (req: Request, res: Response) => { + const accountId = await getAccountIdForRequest(req); + const response: IGetFriendsResponse = { + Current: [], + IncomingFriendRequests: [], + OutgoingFriendRequests: [] + }; + const [internalFriendships, externalFriendships] = await Promise.all([ + Friendship.find({ owner: accountId }), + Friendship.find({ friend: accountId }, "owner Note") + ]); + for (const externalFriendship of externalFriendships) { + if (!internalFriendships.find(x => x.friend.equals(externalFriendship.owner))) { + response.IncomingFriendRequests.push({ + _id: toOid(externalFriendship.owner), + Note: externalFriendship.Note + }); + } + } + for (const internalFriendship of internalFriendships) { + const friendInfo: IFriendInfo = { + _id: toOid(internalFriendship.friend) + }; + if (externalFriendships.find(x => x.owner.equals(internalFriendship.friend))) { + response.Current.push(friendInfo); + } else { + response.OutgoingFriendRequests.push(friendInfo); + } + } + const promises: Promise[] = []; + for (const arr of Object.values(response)) { + for (const friendInfo of arr) { + promises.push(addAccountDataToFriendInfo(friendInfo)); + promises.push(addInventoryDataToFriendInfo(friendInfo)); + } + } + await Promise.all(promises); + res.json(response); }; -export { getFriendsController }; +// interface IGetFriendsResponse { +// Current: IFriendInfo[]; +// IncomingFriendRequests: IFriendInfo[]; +// OutgoingFriendRequests: IFriendInfo[]; +// } +type IGetFriendsResponse = Record<"Current" | "IncomingFriendRequests" | "OutgoingFriendRequests", IFriendInfo[]>; 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..f99b8d1c 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 { getAccountForRequest } from "@/src/services/loginService"; +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) => { - const accountId = await getAccountIdForRequest(req); - const inventory = await Inventory.findOne({ accountOwnerId: accountId }); - if (!inventory) { - res.status(400).json({ error: "inventory was undefined" }); - return; - } +export const getGuildController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + const inventory = await getInventory(account._id.toString(), "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, account)); return; } } - res.json({}); + res.end(); }; - -export { getGuildController }; diff --git a/src/controllers/api/getGuildDojoController.ts b/src/controllers/api/getGuildDojoController.ts index 9d7ed93f..7c5b9412 100644 --- a/src/controllers/api/getGuildDojoController.ts +++ b/src/controllers/api/getGuildDojoController.ts @@ -2,28 +2,34 @@ import { RequestHandler } from "express"; import { Types } from "mongoose"; import { Guild } from "@/src/models/guildModel"; import { getDojoClient } from "@/src/services/guildService"; +import { Account } from "@/src/models/loginModel"; 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) : {}; + const account = await Account.findById(req.query.accountId as string); + res.json(await getDojoClient(guild, 0, payload.ComponentId, account?.BuildLabel)); }; + +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/getIgnoredUsersController.ts b/src/controllers/api/getIgnoredUsersController.ts index 97127fba..b3a6cf22 100644 --- a/src/controllers/api/getIgnoredUsersController.ts +++ b/src/controllers/api/getIgnoredUsersController.ts @@ -1,16 +1,20 @@ +import { toOid } from "@/src/helpers/inventoryHelpers"; +import { Account, Ignore } from "@/src/models/loginModel"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IFriendInfo } from "@/src/types/friendTypes"; +import { parallelForeach } from "@/src/utils/async-utils"; import { RequestHandler } from "express"; -const getIgnoredUsersController: RequestHandler = (_req, res) => { - res.writeHead(200, { - "Content-Type": "text/html", - "Content-Length": "3" +export const getIgnoredUsersController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const ignores = await Ignore.find({ ignorer: accountId }); + const ignoredUsers: IFriendInfo[] = []; + await parallelForeach(ignores, async ignore => { + const ignoreeAccount = (await Account.findById(ignore.ignoree, "DisplayName"))!; + ignoredUsers.push({ + _id: toOid(ignore.ignoree), + DisplayName: ignoreeAccount.DisplayName + "" + }); }); - res.end( - Buffer.from([ - 0x7b, 0x22, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x22, 0x3a, 0x38, 0x33, 0x30, 0x34, 0x30, 0x37, 0x37, 0x32, 0x32, - 0x34, 0x30, 0x32, 0x32, 0x32, 0x36, 0x31, 0x35, 0x30, 0x31, 0x7d - ]) - ); + res.json({ IgnoredUsers: ignoredUsers }); }; - -export { getIgnoredUsersController }; diff --git a/src/controllers/api/getNewRewardSeedController.ts b/src/controllers/api/getNewRewardSeedController.ts index 163e3c6e..cb9e1f82 100644 --- a/src/controllers/api/getNewRewardSeedController.ts +++ b/src/controllers/api/getNewRewardSeedController.ts @@ -1,13 +1,19 @@ +import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; +import { generateRewardSeed } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; import { RequestHandler } from "express"; -const getNewRewardSeedController: RequestHandler = (_req, res) => { - res.json({ rewardSeed: generateRewardSeed() }); +export const getNewRewardSeedController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + + const rewardSeed = generateRewardSeed(); + await Inventory.updateOne( + { + accountOwnerId: accountId + }, + { + RewardSeed: rewardSeed + } + ); + res.json({ rewardSeed: rewardSeed }); }; - -function generateRewardSeed(): number { - const min = -Number.MAX_SAFE_INTEGER; - const max = Number.MAX_SAFE_INTEGER; - return Math.floor(Math.random() * (max - min + 1)) + min; -} - -export { getNewRewardSeedController }; diff --git a/src/controllers/api/getShipController.ts b/src/controllers/api/getShipController.ts index 10f6e25f..ca22ec3d 100644 --- a/src/controllers/api/getShipController.ts +++ b/src/controllers/api/getShipController.ts @@ -2,19 +2,24 @@ import { RequestHandler } from "express"; import { config } from "@/src/services/configService"; import allShipFeatures from "@/static/fixed_responses/allShipFeatures.json"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getPersonalRooms } from "@/src/services/personalRoomsService"; -import { getShip } from "@/src/services/shipService"; +import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService"; import { toOid } from "@/src/helpers/inventoryHelpers"; import { IGetShipResponse } from "@/src/types/shipTypes"; -import { IPersonalRooms } from "@/src/types/personalRoomsTypes"; +import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes"; import { getLoadout } from "@/src/services/loadoutService"; export const getShipController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const personalRoomsDb = await getPersonalRooms(accountId); - const personalRooms = personalRoomsDb.toJSON(); + + // Setup gardening if it's missing. Maybe should be done as part of some quest completion in the future. + if (personalRoomsDb.Apartment.Gardening.Planters.length == 0) { + personalRoomsDb.Apartment.Gardening = createGarden(); + await personalRoomsDb.save(); + } + + const personalRooms = personalRoomsDb.toJSON(); const loadout = await getLoadout(accountId); - const ship = await getShip(personalRoomsDb.activeShipId, "ShipAttachments SkinFlavourItem"); const getShipResponse: IGetShipResponse = { ShipOwnerId: accountId, @@ -24,9 +29,12 @@ export const getShipController: RequestHandler = async (req, res) => { ShipId: toOid(personalRoomsDb.activeShipId), ShipInterior: { Colors: personalRooms.ShipInteriorColors, - ShipAttachments: ship.ShipAttachments, - SkinFlavourItem: ship.SkinFlavourItem - } + ShipAttachments: { HOOD_ORNAMENT: "" }, + SkinFlavourItem: "" + }, + FavouriteLoadoutId: personalRooms.Ship.FavouriteLoadoutId + ? toOid(personalRooms.Ship.FavouriteLoadoutId) + : undefined }, Apartment: personalRooms.Apartment, TailorShop: personalRooms.TailorShop diff --git a/src/controllers/api/getVendorInfoController.ts b/src/controllers/api/getVendorInfoController.ts index b161176e..5f9d3292 100644 --- a/src/controllers/api/getVendorInfoController.ts +++ b/src/controllers/api/getVendorInfoController.ts @@ -1,14 +1,20 @@ import { RequestHandler } from "express"; -import { getVendorManifestByTypeName } from "@/src/services/serversideVendorsService"; +import { applyStandingToVendorManifest, getVendorManifestByTypeName } from "@/src/services/serversideVendorsService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; -export const getVendorInfoController: RequestHandler = (req, res) => { - if (typeof req.query.vendor == "string") { - const manifest = getVendorManifestByTypeName(req.query.vendor); - if (!manifest) { - throw new Error(`Unknown vendor: ${req.query.vendor}`); - } - res.json(manifest); - } else { - res.status(400).end(); +export const getVendorInfoController: RequestHandler = async (req, res) => { + let manifest = getVendorManifestByTypeName(req.query.vendor as string); + if (!manifest) { + throw new Error(`Unknown vendor: ${req.query.vendor as string}`); } + + // For testing purposes, authenticating with this endpoint is optional here, but would be required on live. + if (req.query.accountId) { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + manifest = applyStandingToVendorManifest(inventory, manifest); + } + + res.json(manifest); }; diff --git a/src/controllers/api/getVoidProjectionRewardsController.ts b/src/controllers/api/getVoidProjectionRewardsController.ts index 9a577cf5..3a09f4ba 100644 --- a/src/controllers/api/getVoidProjectionRewardsController.ts +++ b/src/controllers/api/getVoidProjectionRewardsController.ts @@ -1,77 +1,31 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { addMiscItems, getInventory } from "@/src/services/inventoryService"; +import { crackRelic } from "@/src/helpers/relicHelper"; +import { getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; -import { getRandomWeightedReward2 } from "@/src/services/rngService"; -import { ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; -import { logger } from "@/src/utils/logger"; +import { IVoidTearParticipantInfo } from "@/src/types/requestTypes"; import { RequestHandler } from "express"; -import { ExportRelics, ExportRewards, TRarity } from "warframe-public-export-plus"; export const getVoidProjectionRewardsController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const data = getJSONfromString(String(req.body)); + + if (data.ParticipantInfo.QualifiesForReward && !data.ParticipantInfo.HaveRewardResponse) { + const inventory = await getInventory(accountId); + await crackRelic(inventory, data.ParticipantInfo); + await inventory.save(); + } + const response: IVoidProjectionRewardResponse = { CurrentWave: data.CurrentWave, ParticipantInfo: data.ParticipantInfo, DifficultyTier: data.DifficultyTier }; - if (data.ParticipantInfo.QualifiesForReward) { - const relic = ExportRelics[data.ParticipantInfo.VoidProjection]; - const weights = refinementToWeights[relic.quality]; - logger.debug(`opening a relic of quality ${relic.quality}; rarity weights are`, weights); - const reward = getRandomWeightedReward2( - ExportRewards[relic.rewardManifest][0] as { type: string; itemCount: number; rarity: TRarity }[], // rarity is nullable in PE+ typings, but always present for relics - weights - )!; - logger.debug(`relic rolled`, reward); - response.ParticipantInfo.Reward = reward.type; - - const inventory = await getInventory(accountId); - // Remove relic - addMiscItems(inventory, [ - { - ItemType: data.ParticipantInfo.VoidProjection, - ItemCount: -1 - } - ]); - // Give reward - await handleStoreItemAcquisition(reward.type, inventory, reward.itemCount); - await inventory.save(); - } res.json(response); }; -const refinementToWeights = { - VPQ_BRONZE: { - COMMON: 0.76, - UNCOMMON: 0.22, - RARE: 0.02, - LEGENDARY: 0 - }, - VPQ_SILVER: { - COMMON: 0.7, - UNCOMMON: 0.26, - RARE: 0.04, - LEGENDARY: 0 - }, - VPQ_GOLD: { - COMMON: 0.6, - UNCOMMON: 0.34, - RARE: 0.06, - LEGENDARY: 0 - }, - VPQ_PLATINUM: { - COMMON: 0.5, - UNCOMMON: 0.4, - RARE: 0.1, - LEGENDARY: 0 - } -}; - interface IVoidProjectionRewardRequest { CurrentWave: number; - ParticipantInfo: IParticipantInfo; + ParticipantInfo: IVoidTearParticipantInfo; VoidTier: string; DifficultyTier: number; VoidProjectionRemovalHash: string; @@ -79,20 +33,6 @@ interface IVoidProjectionRewardRequest { interface IVoidProjectionRewardResponse { CurrentWave: number; - ParticipantInfo: IParticipantInfo; + ParticipantInfo: IVoidTearParticipantInfo; DifficultyTier: number; } - -interface IParticipantInfo { - AccountId: string; - Name: string; - ChosenRewardOwner: string; - MissionHash: string; - VoidProjection: string; - Reward: string; - QualifiesForReward: boolean; - HaveRewardResponse: boolean; - RewardsMultiplier: number; - RewardProjection: string; - HardModeReward: ITypeCount; -} diff --git a/src/controllers/api/giftingController.ts b/src/controllers/api/giftingController.ts new file mode 100644 index 00000000..55865cee --- /dev/null +++ b/src/controllers/api/giftingController.ts @@ -0,0 +1,113 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Account } from "@/src/models/loginModel"; +import { areFriends } from "@/src/services/friendService"; +import { createMessage } from "@/src/services/inboxService"; +import { + combineInventoryChanges, + getEffectiveAvatarImageType, + getInventory, + updateCurrency +} from "@/src/services/inventoryService"; +import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; +import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; +import { IOid } from "@/src/types/commonTypes"; +import { IInventoryChanges, IPurchaseParams } from "@/src/types/purchaseTypes"; +import { RequestHandler } from "express"; +import { ExportBundles, 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. + const senderAccount = await getAccountForRequest(req); + if ( + inventory.Settings?.GiftMode == "GIFT_MODE_NONE" || + (inventory.Settings?.GiftMode == "GIFT_MODE_FRIENDS" && !(await areFriends(account._id, senderAccount._id))) + ) { + 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 senderInventory = await getInventory(senderAccount._id.toString()); + + if (senderInventory.GiftsRemaining == 0) { + res.status(400).send("10").end(); + return; + } + senderInventory.GiftsRemaining -= 1; + + const inventoryChanges: IInventoryChanges = updateCurrency( + senderInventory, + data.PurchaseParams.ExpectedPrice, + true + ); + if (data.PurchaseParams.StoreItem in ExportBundles) { + const bundle = ExportBundles[data.PurchaseParams.StoreItem]; + if (bundle.giftingBonus) { + combineInventoryChanges( + inventoryChanges, + (await handleStoreItemAcquisition(bundle.giftingBonus, senderInventory)).InventoryChanges + ); + } + } + 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[getEffectiveAvatarImageType(senderInventory)].icon, + gifts: [ + { + GiftType: data.PurchaseParams.StoreItem + } + ] + } + ]); + + res.json({ + InventoryChanges: inventoryChanges + }); +}; + +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..fac741fc 100644 --- a/src/controllers/api/gildWeaponController.ts +++ b/src/controllers/api/gildWeaponController.ts @@ -1,37 +1,26 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { getInventory } from "@/src/services/inventoryService"; -import { WeaponTypeInternal } from "@/src/services/itemDataService"; -import { ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes"; - -const modularWeaponCategory: (WeaponTypeInternal | "Hoverboards")[] = [ - "LongGuns", - "Pistols", - "Melee", - "OperatorAmps", - "Hoverboards" // Not sure about hoverboards just coppied from modual crafting -]; +import { addMiscItems, getInventory } from "@/src/services/inventoryService"; +import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; +import { ArtifactPolarity, EquipmentFeatures, IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { ExportRecipes } from "warframe-public-export-plus"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; 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"; + Category: TEquipmentKey; } -// 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)); data.ItemId = String(req.query.ItemId); - if (!modularWeaponCategory.includes(req.query.Category as WeaponTypeInternal | "Hoverboards")) { - throw new Error(`Unknown modular weapon Category: ${String(req.query.Category)}`); - } - data.Category = req.query.Category as WeaponTypeInternal | "Hoverboards"; + data.Category = req.query.Category as TEquipmentKey; const inventory = await getInventory(accountId); const weaponIndex = inventory[data.Category].findIndex(x => String(x._id) === data.ItemId); @@ -40,9 +29,12 @@ 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.ItemName = data.ItemName; - weapon.XP = 0; + weapon.Features ??= 0; + weapon.Features |= EquipmentFeatures.GILDED; + if (data.Recipe != "webui") { + weapon.ItemName = data.ItemName; + weapon.XP = 0; + } if (data.Category != "OperatorAmps" && data.PolarizeSlot && data.PolarizeValue) { weapon.Polarity = [ { @@ -52,11 +44,32 @@ export const gildWeaponController: RequestHandler = async (req, res) => { ]; } inventory[data.Category][weaponIndex] = weapon; - await inventory.save(); + const inventoryChanges: IInventoryChanges = {}; + inventoryChanges[data.Category] = [weapon.toJSON()]; - res.json({ - InventoryChanges: { - [data.Category]: [weapon] + const affiliationMods = []; + + if (data.Recipe != "webui") { + const recipe = ExportRecipes[data.Recipe]; + inventoryChanges.MiscItems = recipe.secretIngredients!.map(ingredient => ({ + ItemType: ingredient.ItemType, + ItemCount: ingredient.ItemCount * -1 + })); + addMiscItems(inventory, inventoryChanges.MiscItems); + + 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: inventoryChanges, + AffiliationMods: affiliationMods }); }; diff --git a/src/controllers/api/giveKeyChainTriggeredItemsController.ts b/src/controllers/api/giveKeyChainTriggeredItemsController.ts index ef1e4700..df8e8a80 100644 --- a/src/controllers/api/giveKeyChainTriggeredItemsController.ts +++ b/src/controllers/api/giveKeyChainTriggeredItemsController.ts @@ -1,38 +1,17 @@ import { RequestHandler } from "express"; -import { isEmptyObject, parseString } from "@/src/helpers/general"; +import { parseString } from "@/src/helpers/general"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { addKeyChainItems, getInventory } from "@/src/services/inventoryService"; -import { IGroup } from "@/src/types/loginTypes"; -import { updateQuestStage } from "@/src/services/questService"; +import { getInventory } from "@/src/services/inventoryService"; +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); const keyChainInfo = getJSONfromString((req.body as string).toString()); const inventory = await getInventory(accountId); - const inventoryChanges = await addKeyChainItems(inventory, keyChainInfo); - - if (isEmptyObject(inventoryChanges)) { - throw new Error("inventory changes was empty after getting keychain items: should not happen"); - } - // items were added: update quest stage's i (item was given) - updateQuestStage(inventory, keyChainInfo, { i: true }); - + const inventoryChanges = await giveKeyChainItem(inventory, keyChainInfo); await inventory.save(); + res.send(inventoryChanges); - - //TODO: Check whether Wishlist is used to track items which should exist uniquely in the inventory - /* - some items are added or removed (not sure) to the wishlist, in that case a - WishlistChanges: ["/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem"], - is added to the response, need to determine for which items this is the case and what purpose this has. - */ - //{"KeyChain":"/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain","ChainStage":0} - //{"WishlistChanges":["/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem"],"MiscItems":[{"ItemType":"/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem","ItemCount":1}]} }; - -export interface IKeyChainRequest { - KeyChain: string; - ChainStage: number; - Groups?: IGroup[]; -} diff --git a/src/controllers/api/giveKeyChainTriggeredMessageController.ts b/src/controllers/api/giveKeyChainTriggeredMessageController.ts index 0f699d42..3bc41c21 100644 --- a/src/controllers/api/giveKeyChainTriggeredMessageController.ts +++ b/src/controllers/api/giveKeyChainTriggeredMessageController.ts @@ -1,34 +1,15 @@ -import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; -import { IMessage } from "@/src/models/inboxModel"; -import { createMessage } from "@/src/services/inboxService"; import { getInventory } from "@/src/services/inventoryService"; -import { getKeyChainMessage } from "@/src/services/itemDataService"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { updateQuestStage } from "@/src/services/questService"; +import { giveKeyChainMessage } from "@/src/services/questService"; +import { IKeyChainRequest } from "@/src/types/requestTypes"; import { RequestHandler } from "express"; export const giveKeyChainTriggeredMessageController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const keyChainInfo = JSON.parse((req.body as Buffer).toString()) as IKeyChainRequest; - 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]); - const inventory = await getInventory(accountId, "QuestKeys"); - updateQuestStage(inventory, keyChainInfo, { m: true }); + await giveKeyChainMessage(inventory, accountId, keyChainInfo); await inventory.save(); res.send(1); diff --git a/src/controllers/api/giveQuestKey.ts b/src/controllers/api/giveQuestKeyRewardController.ts similarity index 89% rename from src/controllers/api/giveQuestKey.ts rename to src/controllers/api/giveQuestKeyRewardController.ts index 070dea75..d74d56bf 100644 --- a/src/controllers/api/giveQuestKey.ts +++ b/src/controllers/api/giveQuestKeyRewardController.ts @@ -16,15 +16,15 @@ export const giveQuestKeyRewardController: RequestHandler = async (req, res) => const inventory = await getInventory(accountId); const inventoryChanges = await addItem(inventory, reward.ItemType, reward.Amount); await inventory.save(); - res.json(inventoryChanges.InventoryChanges); + res.json(inventoryChanges); //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/giveShipDecoAndLoreFragmentController.ts b/src/controllers/api/giveShipDecoAndLoreFragmentController.ts new file mode 100644 index 00000000..08385cbf --- /dev/null +++ b/src/controllers/api/giveShipDecoAndLoreFragmentController.ts @@ -0,0 +1,20 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { addLoreFragmentScans, addShipDecorations, getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { ILoreFragmentScan, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; + +export const giveShipDecoAndLoreFragmentController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "LoreFragmentScans ShipDecorations"); + const data = getJSONfromString(String(req.body)); + addLoreFragmentScans(inventory, data.LoreFragmentScans); + addShipDecorations(inventory, data.ShipDecorations); + await inventory.save(); + res.end(); +}; + +interface IGiveShipDecoAndLoreFragmentRequest { + LoreFragmentScans: ILoreFragmentScan[]; + ShipDecorations: ITypeCount[]; +} diff --git a/src/controllers/api/giveStartingGearController.ts b/src/controllers/api/giveStartingGearController.ts new file mode 100644 index 00000000..6556de93 --- /dev/null +++ b/src/controllers/api/giveStartingGearController.ts @@ -0,0 +1,16 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { addStartingGear, getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { TPartialStartingGear } from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; + +export const giveStartingGearController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const startingGear = getJSONfromString(String(req.body)); + const inventory = await getInventory(accountId); + + const inventoryChanges = await addStartingGear(inventory, startingGear); + await inventory.save(); + + res.send(inventoryChanges); +}; diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index 34022699..49d96b70 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -1,119 +1,407 @@ 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 { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; +import { + addCrewShipWeaponSkin, + addEquipment, + addItem, + addMiscItems, + addRecipes, + combineInventoryChanges, + getInventory, + occupySlot, + updateCurrency +} from "@/src/services/inventoryService"; +import { IMiscItem, InventorySlot } 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, toOid } from "@/src/helpers/inventoryHelpers"; +import { logger } from "@/src/utils/logger"; +import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; 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!]; - 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 + if (data.Action == "Sync") { + let needSave = false; + const techProjects: ITechProjectClient[] = []; + const guild = await getGuildForRequestEx(req, inventory); + 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 (data.Mode == "Guild") { + const guild = await getGuildForRequestEx(req, inventory); + 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)) { + 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 { + const recipe = ExportDojoRecipes.research[data.RecipeType]; + if (data.TechProductCategory) { + if ( + data.TechProductCategory != "CrewShipWeapons" && + data.TechProductCategory != "CrewShipWeaponSkins" + ) { + throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`); + } + if (!inventory[getSalvageCategory(data.TechProductCategory)].id(data.CategoryItemId!)) { + throw new Error( + `no item with id ${data.CategoryItemId} in ${getSalvageCategory(data.TechProductCategory)} array` + ); + } + } + const techProject = + inventory.PersonalTechProjects[ + inventory.PersonalTechProjects.push({ + State: 0, + ReqCredits: recipe.price, + ItemType: data.RecipeType, + ProductCategory: data.TechProductCategory, + CategoryItemId: data.CategoryItemId, + ReqItems: recipe.ingredients + }) - 1 + ]; + await inventory.save(); + res.json({ + isPersonal: true, + action: "Start", + personalTech: techProject.toJSON() }); } + } else if (data.Action == "Contribute") { + if ((req.query.guildId as string) == "000000000000000000000000") { + const techProject = inventory.PersonalTechProjects.id(data.ResearchId)!; + + techProject.ReqCredits -= data.RegularCredits; + const inventoryChanges: IInventoryChanges = updateCurrency(inventory, data.RegularCredits, false); + + const miscItemChanges = []; + for (const miscItem of data.MiscItems) { + const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); + if (reqItem) { + if (miscItem.ItemCount > reqItem.ItemCount) { + miscItem.ItemCount = reqItem.ItemCount; + } + reqItem.ItemCount -= miscItem.ItemCount; + miscItemChanges.push({ + ItemType: miscItem.ItemType, + ItemCount: miscItem.ItemCount * -1 + }); + } + } + addMiscItems(inventory, miscItemChanges); + inventoryChanges.MiscItems = miscItemChanges; + + techProject.HasContributions = true; + + if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) { + techProject.State = 1; + const recipe = ExportDojoRecipes.research[techProject.ItemType]; + techProject.CompletionDate = new Date(Date.now() + recipe.time * 1000); + } + + await inventory.save(); + res.json({ + InventoryChanges: inventoryChanges, + PersonalResearch: { $oid: data.ResearchId }, + PersonalResearchDate: techProject.CompletionDate ? toMongoDate(techProject.CompletionDate) : undefined + }); + } else { + if (!hasAccessToDojo(inventory)) { + res.status(400).send("-1").end(); + return; + } + + const guild = await getGuildForRequestEx(req, inventory); + const guildMember = (await GuildMember.findOne( + { accountId, guildId: guild._id }, + "RegularCreditsContributed MiscItemsContributed" + ))!; + + const techProject = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!; + + if (data.VaultCredits) { + if (data.VaultCredits > techProject.ReqCredits) { + data.VaultCredits = techProject.ReqCredits; + } + techProject.ReqCredits -= data.VaultCredits; + guild.VaultRegularCredits! -= data.VaultCredits; + } + + if (data.RegularCredits > techProject.ReqCredits) { + data.RegularCredits = techProject.ReqCredits; + } + techProject.ReqCredits -= data.RegularCredits; + + guildMember.RegularCreditsContributed ??= 0; + guildMember.RegularCreditsContributed += data.RegularCredits; + + if (data.VaultMiscItems.length) { + for (const miscItem of data.VaultMiscItems) { + const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); + if (reqItem) { + if (miscItem.ItemCount > reqItem.ItemCount) { + miscItem.ItemCount = reqItem.ItemCount; + } + reqItem.ItemCount -= miscItem.ItemCount; + + const vaultMiscItem = guild.VaultMiscItems!.find(x => x.ItemType == miscItem.ItemType)!; + vaultMiscItem.ItemCount -= miscItem.ItemCount; + } + } + } + + const miscItemChanges = []; + for (const miscItem of data.MiscItems) { + const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); + if (reqItem) { + if (miscItem.ItemCount > reqItem.ItemCount) { + miscItem.ItemCount = reqItem.ItemCount; + } + reqItem.ItemCount -= miscItem.ItemCount; + miscItemChanges.push({ + ItemType: miscItem.ItemType, + ItemCount: miscItem.ItemCount * -1 + }); + + addGuildMemberMiscItemContribution(guildMember, miscItem); + } + } + addMiscItems(inventory, miscItemChanges); + const inventoryChanges: IInventoryChanges = updateCurrency(inventory, data.RegularCredits, false); + inventoryChanges.MiscItems = miscItemChanges; + + // Check if research is fully funded now. + await processGuildTechProjectContributionsUpdate(guild, techProject); + + await Promise.all([guild.save(), inventory.save(), guildMember.save()]); + res.json({ + InventoryChanges: inventoryChanges, + Vault: getGuildVault(guild) + }); + } + } else if (data.Action.split(",")[0] == "Buy") { + const purchase = data as IGuildTechBuyRequest; + if (purchase.Mode == "Guild") { + const guild = await getGuildForRequestEx(req, inventory); + if ( + !hasAccessToDojo(inventory) || + !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator)) + ) { + res.status(400).send("-1").end(); + return; + } + const quantity = parseInt(data.Action.split(",")[1]); + const recipeChanges = [ + { + ItemType: purchase.RecipeType, + ItemCount: quantity + } + ]; + addRecipes(inventory, recipeChanges); + const currencyChanges = updateCurrency( + inventory, + ExportDojoRecipes.research[purchase.RecipeType].replicatePrice, + false + ); + await inventory.save(); + // Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`. + res.json({ + inventoryChanges: { + ...currencyChanges, + Recipes: recipeChanges + } + }); + } else { + const inventoryChanges = claimSalvagedComponent(inventory, purchase.CategoryItemId!); + await inventory.save(); + res.json({ + inventoryChanges: inventoryChanges + }); + } + } else if (data.Action == "Fabricate") { + const guild = await getGuildForRequestEx(req, inventory); + 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") { + const guild = await getGuildForRequestEx(req, inventory); + 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") { + const guild = await getGuildForRequestEx(req, inventory); + 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 if (action == "Contribute") { - const contributions = data as IGuildTechContributeFields; - const techProject = guild.TechProjects!.find(x => x.ItemType == contributions.RecipeType)!; - if (contributions.RegularCredits > techProject.ReqCredits) { - contributions.RegularCredits = techProject.ReqCredits; - } - techProject.ReqCredits -= contributions.RegularCredits; - const miscItemChanges = []; - for (const miscItem of contributions.MiscItems) { - const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); + } else if (data.Action == "Cancel" && data.CategoryItemId) { + const personalTechProjectIndex = inventory.PersonalTechProjects.findIndex(x => + x.CategoryItemId?.equals(data.CategoryItemId) + ); + const personalTechProject = inventory.PersonalTechProjects[personalTechProjectIndex]; + inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1); + + const meta = ExportDojoRecipes.research[personalTechProject.ItemType]; + const contributedCredits = meta.price - personalTechProject.ReqCredits; + const inventoryChanges = updateCurrency(inventory, contributedCredits * -1, false); + inventoryChanges.MiscItems = []; + for (const ingredient of meta.ingredients) { + const reqItem = personalTechProject.ReqItems.find(x => x.ItemType == ingredient.ItemType); if (reqItem) { - if (miscItem.ItemCount > reqItem.ItemCount) { - miscItem.ItemCount = reqItem.ItemCount; - } - reqItem.ItemCount -= miscItem.ItemCount; - miscItemChanges.push({ - ItemType: miscItem.ItemType, - ItemCount: miscItem.ItemCount * -1 + const contributedItems = ingredient.ItemCount - reqItem.ItemCount; + inventoryChanges.MiscItems.push({ + ItemType: ingredient.ItemType, + ItemCount: contributedItems }); } } - addMiscItems(inventory, miscItemChanges); - const inventoryChanges: IInventoryChanges = { - ...updateCurrency(inventory, contributions.RegularCredits, false), - MiscItems: miscItemChanges - }; + addMiscItems(inventory, inventoryChanges.MiscItems); - 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); - } - - await guild.save(); await inventory.save(); res.json({ - InventoryChanges: inventoryChanges + action: "Cancel", + isPersonal: true, + inventoryChanges: inventoryChanges, + personalTech: { + ItemId: toOid(personalTechProject._id) + } }); - } else if (action == "Buy") { - const purchase = data as IGuildTechBuyFields; - const quantity = parseInt(data.Action.split(",")[1]); - const inventory = await getInventory(accountId); - const recipeChanges = [ + } else if (data.Action == "Rush" && data.CategoryItemId) { + const inventoryChanges: IInventoryChanges = { + ...updateCurrency(inventory, 20, true), + ...claimSalvagedComponent(inventory, data.CategoryItemId) + }; + await inventory.save(); + res.json({ + inventoryChanges: inventoryChanges + }); + } else if (data.Action == "InstantFinish") { + if (data.TechProductCategory != "CrewShipWeapons" && data.TechProductCategory != "CrewShipWeaponSkins") { + throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`); + } + const inventoryChanges = finishComponentRepair(inventory, data.TechProductCategory, data.CategoryItemId!); + inventoryChanges.MiscItems = [ { - ItemType: purchase.RecipeType, - ItemCount: quantity + ItemType: "/Lotus/Types/Items/MiscItems/InstantSalvageRepairItem", + ItemCount: -1 } ]; - addRecipes(inventory, recipeChanges); - const currencyChanges = updateCurrency( - inventory, - ExportDojoRecipes.research[purchase.RecipeType].replicatePrice, - false - ); + addMiscItems(inventory, inventoryChanges.MiscItems); await inventory.save(); - // Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`. res.json({ - inventoryChanges: { - ...currencyChanges, - Recipes: recipeChanges - } + inventoryChanges: inventoryChanges }); } else { - throw new Error(`unknown guildTech action: ${data.Action}`); + logger.debug(`data provided to ${req.path}: ${String(req.body)}`); + throw new Error(`unhandled guildTech request`); } }; -type TGuildTechRequest = { - Action: string; -} & Partial & - Partial; +type TGuildTechRequest = + | { Action: "Sync" | "SomethingElseThatWeMightNotKnowAbout" } + | IGuildTechBasicRequest + | IGuildTechContributeRequest; -interface IGuildTechStartFields { - Mode: "Guild"; +interface IGuildTechBasicRequest { + Action: "Start" | "Fabricate" | "Pause" | "Unpause" | "Cancel" | "Rush" | "InstantFinish"; + Mode: "Guild" | "Personal"; RecipeType: string; + TechProductCategory?: string; + CategoryItemId?: string; } -type IGuildTechBuyFields = IGuildTechStartFields; +interface IGuildTechBuyRequest extends Omit { + Action: string; +} -interface IGuildTechContributeFields { - ResearchId: ""; +interface IGuildTechContributeRequest { + Action: "Contribute"; + ResearchId: string; RecipeType: string; RegularCredits: number; MiscItems: IMiscItem[]; @@ -121,7 +409,49 @@ interface IGuildTechContributeFields { 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)); +const getSalvageCategory = ( + category: "CrewShipWeapons" | "CrewShipWeaponSkins" +): "CrewShipSalvagedWeapons" | "CrewShipSalvagedWeaponSkins" => { + return category == "CrewShipWeapons" ? "CrewShipSalvagedWeapons" : "CrewShipSalvagedWeaponSkins"; +}; + +const claimSalvagedComponent = (inventory: TInventoryDatabaseDocument, itemId: string): IInventoryChanges => { + // delete personal tech project + const personalTechProjectIndex = inventory.PersonalTechProjects.findIndex(x => x.CategoryItemId?.equals(itemId)); + const personalTechProject = inventory.PersonalTechProjects[personalTechProjectIndex]; + inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1); + + const category = personalTechProject.ProductCategory! as "CrewShipWeapons" | "CrewShipWeaponSkins"; + return finishComponentRepair(inventory, category, itemId); +}; + +const finishComponentRepair = ( + inventory: TInventoryDatabaseDocument, + category: "CrewShipWeapons" | "CrewShipWeaponSkins", + itemId: string +): IInventoryChanges => { + const salvageCategory = getSalvageCategory(category); + + // find salved part & delete it + const salvageIndex = inventory[salvageCategory].findIndex(x => x._id.equals(itemId)); + const salvageItem = inventory[salvageCategory][salvageIndex]; + inventory[salvageCategory].splice(salvageIndex, 1); + + // add final item + const inventoryChanges = { + ...(category == "CrewShipWeaponSkins" + ? addCrewShipWeaponSkin(inventory, salvageItem.ItemType, salvageItem.UpgradeFingerprint) + : addEquipment(inventory, category, salvageItem.ItemType, { + UpgradeFingerprint: salvageItem.UpgradeFingerprint + })), + ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false) + }; + + inventoryChanges.RemovedIdItems = [ + { + ItemId: { $oid: itemId } + } + ]; + + return inventoryChanges; }; diff --git a/src/controllers/api/hostSessionController.ts b/src/controllers/api/hostSessionController.ts index 1745d994..52c7a6a2 100644 --- a/src/controllers/api/hostSessionController.ts +++ b/src/controllers/api/hostSessionController.ts @@ -1,17 +1,24 @@ import { RequestHandler } from "express"; -import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getAccountForRequest } from "@/src/services/loginService"; import { createNewSession } from "@/src/managers/sessionManager"; import { logger } from "@/src/utils/logger"; import { ISession } from "@/src/types/session"; +import { JSONParse } from "json-with-bigint"; +import { toOid2, version_compare } from "@/src/helpers/inventoryHelpers"; const hostSessionController: RequestHandler = async (req, res) => { - const accountId = await getAccountIdForRequest(req); - const hostSessionRequest = JSON.parse(req.body as string) as ISession; + const account = await getAccountForRequest(req); + const hostSessionRequest = JSONParse(String(req.body)) as ISession; logger.debug("HostSession Request", { hostSessionRequest }); - const session = createNewSession(hostSessionRequest, accountId); + const session = createNewSession(hostSessionRequest, account._id); logger.debug(`New Session Created`, { session }); - res.json({ sessionId: { $oid: session.sessionId }, rewardSeed: 99999999 }); + if (account.BuildLabel && version_compare(account.BuildLabel, "2015.03.21.08.17") < 0) { + // U15 or below + res.send(session.sessionId.toString()); + } else { + res.json({ sessionId: toOid2(session.sessionId, account.BuildLabel), rewardSeed: 99999999 }); + } }; export { hostSessionController }; 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..8e775c10 100644 --- a/src/controllers/api/inboxController.ts +++ b/src/controllers/api/inboxController.ts @@ -1,21 +1,31 @@ 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, + getEffectiveAvatarImageType, + getInventory +} from "@/src/services/inventoryService"; import { logger } from "@/src/utils/logger"; -import { ExportGear } from "warframe-public-export-plus"; +import { ExportFlavour } from "warframe-public-export-plus"; +import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; +import { fromStoreItem, isStoreItem } from "@/src/services/itemDataService"; +import { IOid } from "@/src/types/commonTypes"; 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") { @@ -24,17 +34,17 @@ export const inboxController: RequestHandler = async (req, res) => { return; } - await deleteMessageRead(deleteId as string); + await deleteMessageRead(parseOid(deleteId as string)); res.status(200).end(); } else if (messageId) { - const message = await getMessage(messageId as string); + const message = await getMessage(parseOid(messageId as string)); message.r = true; - const attachmentItems = message.att; - const attachmentCountedItems = message.countedAtt; + await message.save(); - if (!attachmentItems && !attachmentCountedItems) { - await message.save(); + const attachmentItems = message.attVisualOnly ? undefined : message.att; + const attachmentCountedItems = message.attVisualOnly ? undefined : message.countedAtt; + if (!attachmentItems && !attachmentCountedItems && !message.gifts) { res.status(200).end(); return; } @@ -45,8 +55,8 @@ export const inboxController: RequestHandler = async (req, res) => { await addItems( inventory, attachmentItems.map(attItem => ({ - ItemType: attItem, - ItemCount: attItem in ExportGear ? (ExportGear[attItem].purchaseQuantity ?? 1) : 1 + ItemType: isStoreItem(attItem) ? fromStoreItem(attItem) : attItem, + ItemCount: 1 })), inventoryChanges ); @@ -54,15 +64,49 @@ 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[getEffectiveAvatarImageType(inventory)].icon, + highPriority: true + } + ]); + } + } + } await inventory.save(); - await message.save(); - res.json({ InventoryChanges: inventoryChanges }); } else if (latestClientMessageId) { await createNewEventMessages(req); const messages = await Inbox.find({ ownerId: accountId }).sort({ date: 1 }); - const latestClientMessage = messages.find(m => m._id.toString() === latestClientMessageId); + const latestClientMessage = messages.find(m => m._id.toString() === parseOid(latestClientMessageId as string)); if (!latestClientMessage) { logger.debug(`this should only happen after DeleteAllRead `); @@ -85,3 +129,11 @@ export const inboxController: RequestHandler = async (req, res) => { res.json({ Inbox: inbox }); } }; + +// 33.6.0 has query arguments like lastMessage={"$oid":"68112baebf192e786d1502bb"} instead of lastMessage=68112baebf192e786d1502bb +const parseOid = (oid: string): string => { + if (oid[0] == "{") { + return (JSON.parse(oid) as IOid).$oid; + } + return oid; +}; diff --git a/src/controllers/api/infestedFoundryController.ts b/src/controllers/api/infestedFoundryController.ts index 5e27f1c9..4cc21061 100644 --- a/src/controllers/api/infestedFoundryController.ts +++ b/src/controllers/api/infestedFoundryController.ts @@ -1,30 +1,35 @@ import { RequestHandler } from "express"; -import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getAccountForRequest } 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, - 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 { toMongoDate, version_compare } 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); + const account = await getAccountForRequest(req); switch (req.query.mode) { case "s": { // 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 inventory = await getInventory(account._id.toString()); + const suit = inventory.Suits.id(request.SuitId.$oid)!; if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) { suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}]; } @@ -51,36 +56,52 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { case "x": { // 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 inventory = await getInventory(account._id.toString()); + 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); + + // consume resources + if (!config.infiniteHelminthMaterials) { + let type: string; + let count: number; + if (account.BuildLabel && version_compare(account.BuildLabel, "2025.05.20.10.18") < 0) { + // < 38.6.0 + type = "/Lotus/Types/Items/InfestedFoundry/HelminthBile"; + count = 300; + } else { + // >= 38.6.0 + type = + archonCrystalRemovalResource[ + suit.ArchonCrystalUpgrades![request.Slot].Color!.replace("_MYTHIC", "") + ]; + count = suit.ArchonCrystalUpgrades![request.Slot].Color!.indexOf("_MYTHIC") != -1 ? 300 : 150; + } + inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == type)!.Count -= count; } - ]; - addMiscItems(inventory, miscItemChanges); + } // remove from suit suit.ArchonCrystalUpgrades![request.Slot] = {}; - // remove bile - const bile = inventory.InfestedFoundry!.Resources!.find( - x => x.ItemType == "/Lotus/Types/Items/InfestedFoundry/HelminthBile" - )!; - bile.Count -= 300; - await inventory.save(); + const infestedFoundry = inventory.toJSON().InfestedFoundry!; + applyCheatsToInfestedFoundry(infestedFoundry); res.json({ InventoryChanges: { MiscItems: miscItemChanges, - InfestedFoundry: inventory.toJSON().InfestedFoundry + InfestedFoundry: infestedFoundry } }); break; @@ -89,7 +110,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { case "n": { // name the beast const request = getJSONfromString(String(req.body)); - const inventory = await getInventory(accountId); + const inventory = await getInventory(account._id.toString()); inventory.InfestedFoundry ??= {}; inventory.InfestedFoundry.Name = request.newName; await inventory.save(); @@ -105,15 +126,21 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { case "c": { // consume items + + if (config.infiniteHelminthMaterials) { + res.status(400).end(); + return; + } + const request = getJSONfromString(String(req.body)); - const inventory = await getInventory(accountId); + const inventory = await getInventory(account._id.toString()); inventory.InfestedFoundry ??= {}; inventory.InfestedFoundry.Resources ??= []; 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]; @@ -202,7 +229,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { case "o": { // offerings update const request = getJSONfromString(String(req.body)); - const inventory = await getInventory(accountId); + const inventory = await getInventory(account._id.toString()); inventory.InfestedFoundry ??= {}; inventory.InfestedFoundry.InvigorationIndex = request.OfferingsIndex; inventory.InfestedFoundry.InvigorationSuitOfferings = request.SuitTypes; @@ -210,9 +237,11 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { inventory.InfestedFoundry.InvigorationsApplied = 0; } await inventory.save(); + const infestedFoundry = inventory.toJSON().InfestedFoundry!; + applyCheatsToInfestedFoundry(infestedFoundry); res.json({ InventoryChanges: { - InfestedFoundry: inventory.toJSON().InfestedFoundry + InfestedFoundry: infestedFoundry } }); break; @@ -221,18 +250,20 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { case "a": { // subsume warframe const request = getJSONfromString(String(req.body)); - const inventory = await getInventory(accountId); + const inventory = await getInventory(account._id.toString()); const recipe = getRecipe(request.Recipe)!; - for (const ingredient of recipe.secretIngredients!) { - const resource = inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == ingredient.ItemType); - if (resource) { - resource.Count -= ingredient.ItemCount; + if (!config.infiniteHelminthMaterials) { + for (const ingredient of recipe.secretIngredients!) { + const resource = inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == ingredient.ItemType); + if (resource) { + resource.Count -= ingredient.ItemCount; + } } } const suit = inventory.Suits.id(request.SuitId.$oid)!; inventory.Suits.pull(suit); const consumedSuit: IConsumedSuit = { s: suit.ItemType }; - if (suit.Configs && suit.Configs[0] && suit.Configs[0].pricol) { + if (suit.Configs[0] && suit.Configs[0].pricol) { consumedSuit.c = suit.Configs[0].pricol; } if ((inventory.InfestedFoundry!.XP ?? 0) < 73125_00) { @@ -241,12 +272,13 @@ 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); res.json({ InventoryChanges: { Recipes: recipeChanges, @@ -260,7 +292,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { platinum: 0, Slots: 1 }, - InfestedFoundry: inventory.toJSON().InfestedFoundry + InfestedFoundry: infestedFoundry } }); break; @@ -268,15 +300,17 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { case "r": { // rush subsume - const inventory = await getInventory(accountId); + const inventory = await getInventory(account._id.toString()); const currencyChanges = updateCurrency(inventory, 50, true); const recipeChanges = handleSubsumeCompletion(inventory); await inventory.save(); + const infestedFoundry = inventory.toJSON().InfestedFoundry!; + applyCheatsToInfestedFoundry(infestedFoundry); res.json({ InventoryChanges: { ...currencyChanges, Recipes: recipeChanges, - InfestedFoundry: inventory.toJSON().InfestedFoundry + InfestedFoundry: infestedFoundry } }); break; @@ -284,21 +318,25 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { case "u": { const request = getJSONfromString(String(req.body)); - const inventory = await getInventory(accountId); + const inventory = await getInventory(account._id.toString()); 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; const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry!, 4800_00); addRecipes(inventory, recipeChanges); - for (let i = 0; i != request.ResourceTypes.length; ++i) { - inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == request.ResourceTypes[i])!.Count -= - request.ResourceCosts[i]; + if (!config.infiniteHelminthMaterials) { + for (let i = 0; i != request.ResourceTypes.length; ++i) { + inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == request.ResourceTypes[i])!.Count -= + request.ResourceCosts[i]; + } } inventory.InfestedFoundry!.InvigorationsApplied ??= 0; inventory.InfestedFoundry!.InvigorationsApplied += 1; await inventory.save(); + const infestedFoundry = inventory.toJSON().InfestedFoundry!; + applyCheatsToInfestedFoundry(infestedFoundry); res.json({ SuitId: request.SuitId, OffensiveUpgrade: request.OffensiveUpgradeType, @@ -306,14 +344,14 @@ export const infestedFoundryController: RequestHandler = async (req, res) => { UpgradesExpiry: toMongoDate(upgradesExpiry), InventoryChanges: { Recipes: recipeChanges, - InfestedFoundry: inventory.toJSON().InfestedFoundry + InfestedFoundry: infestedFoundry } }); break; } case "custom_unlockall": { - const inventory = await getInventory(accountId); + const inventory = await getInventory(account._id.toString()); inventory.InfestedFoundry ??= {}; inventory.InfestedFoundry.XP ??= 0; if (151875_00 > inventory.InfestedFoundry.XP) { @@ -329,6 +367,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)}`); } }; @@ -356,103 +395,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; -}; - interface IHelminthOfferingsUpdate { OfferingsIndex: number; SuitTypes: string[]; @@ -503,3 +450,12 @@ const apetiteModel = (x: number): number => { } return 3; }; + +const archonCrystalRemovalResource: Record = { + ACC_RED: "/Lotus/Types/Items/InfestedFoundry/HelminthOxides", + ACC_YELLOW: "/Lotus/Types/Items/InfestedFoundry/HelminthBile", + ACC_BLUE: "/Lotus/Types/Items/InfestedFoundry/HelminthSynthetics", + ACC_GREEN: "/Lotus/Types/Items/InfestedFoundry/HelminthBiotics", + ACC_ORANGE: "/Lotus/Types/Items/InfestedFoundry/HelminthPheromones", + ACC_PURPLE: "/Lotus/Types/Items/InfestedFoundry/HelminthCalx" +}; diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index df7cebcf..97da2c65 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -13,13 +13,27 @@ import { ExportResources, ExportVirtuals } from "warframe-public-export-plus"; -import { handleSubsumeCompletion } from "./infestedFoundryController"; -import { allDailyAffiliationKeys } from "@/src/services/inventoryService"; +import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService"; +import { + addMiscItems, + allDailyAffiliationKeys, + cleanupInventory, + createLibraryDailyTask, + generateRewardSeed +} from "@/src/services/inventoryService"; +import { logger } from "@/src/utils/logger"; +import { catBreadHash } from "@/src/helpers/stringHelpers"; +import { Types } from "mongoose"; +import { getNemesisManifest } from "@/src/helpers/nemesisHelpers"; +import { getPersonalRooms } from "@/src/services/personalRoomsService"; +import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes"; +import { Ship } from "@/src/models/shipModel"; +import { toLegacyOid, version_compare } from "@/src/helpers/inventoryHelpers"; export const inventoryController: RequestHandler = async (request, response) => { const account = await getAccountForRequest(request); - const inventory = await Inventory.findOne({ accountOwnerId: account._id.toString() }); + const inventory = await Inventory.findOne({ accountOwnerId: account._id }); if (!inventory) { response.status(400).json({ error: "inventory was undefined" }); @@ -27,16 +41,60 @@ 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; - await inventory.save(); + 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; + } + } + } + + cleanupInventory(inventory); + + inventory.NextRefill = new Date((Math.trunc(Date.now() / 86400000) + 1) * 86400000); + //await inventory.save(); } if ( @@ -45,29 +103,49 @@ export const inventoryController: RequestHandler = async (request, response) => new Date() >= inventory.InfestedFoundry.AbilityOverrideUnlockCooldown ) { handleSubsumeCompletion(inventory); - await inventory.save(); + //await inventory.save(); } - response.json(await getInventoryResponse(inventory, "xpBasedLevelCapDisabled" in request.query)); + if (inventory.LastInventorySync) { + const lastSyncDuviriMood = Math.trunc(inventory.LastInventorySync.getTimestamp().getTime() / 7200000); + const currentDuviriMood = Math.trunc(Date.now() / 7200000); + if (lastSyncDuviriMood != currentDuviriMood) { + logger.debug(`refreshing duviri seed`); + if (!inventory.DuviriInfo) { + inventory.DuviriInfo = { + Seed: generateRewardSeed(), + NumCompletions: 0 + }; + } else { + inventory.DuviriInfo.Seed = generateRewardSeed(); + } + } + } + inventory.LastInventorySync = new Types.ObjectId(); + await inventory.save(); + + response.json( + await getInventoryResponse(inventory, "xpBasedLevelCapDisabled" in request.query, account.BuildLabel) + ); }; export const getInventoryResponse = async ( inventory: TInventoryDatabaseDocument, - xpBasedLevelCapDisabled: boolean + xpBasedLevelCapDisabled: boolean, + buildLabel: string | undefined ): Promise => { - const inventoryWithLoadOutPresets = await inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>( - "LoadOutPresets" - ); - const inventoryWithLoadOutPresetsAndShips = await inventoryWithLoadOutPresets.populate<{ Ships: IShipInventory }>( - "Ships" - ); - const inventoryResponse = inventoryWithLoadOutPresetsAndShips.toJSON(); + const [inventoryWithLoadOutPresets, ships] = await Promise.all([ + inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>("LoadOutPresets"), + Ship.find({ ShipOwnerId: inventory.accountOwnerId }) + ]); + const inventoryResponse = inventoryWithLoadOutPresets.toJSON(); + inventoryResponse.Ships = ships.map(x => x.toJSON()); if (config.infiniteCredits) { inventoryResponse.RegularCredits = 999999999; } if (config.infinitePlatinum) { - inventoryResponse.PremiumCreditsFree = 999999999; + inventoryResponse.PremiumCreditsFree = 0; inventoryResponse.PremiumCredits = 999999999; } if (config.infiniteEndo) { @@ -105,7 +183,7 @@ export const getInventoryResponse = async ( inventoryResponse.ShipDecorations = []; for (const [uniqueName, item] of Object.entries(ExportResources)) { if (item.productCategory == "ShipDecorations") { - inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 1 }); + inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 999_999 }); } } } @@ -158,7 +236,8 @@ export const getInventoryResponse = async ( if (config.universalPolarityEverywhere) { const Polarity: IPolarity[] = []; - for (let i = 0; i != 12; ++i) { + // 12 is needed for necramechs. 15 is needed for plexus/crewshipharness. + for (let i = 0; i != 15; ++i) { Polarity.push({ Slot: i, Value: ArtifactPolarity.Any @@ -207,25 +286,64 @@ 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; } } - // Fix for #380 - inventoryResponse.NextRefill = { $date: { $numberLong: "9999999999999" } }; + if (config.noDailyFocusLimit) { + inventoryResponse.DailyFocus = Math.max(999_999, 250000 + inventoryResponse.PlayerLevel * 5000); + } - // This determines if the "void fissures" tab is shown in navigation. - inventoryResponse.HasOwnedVoidProjectionsPreviously = true; + if (inventoryResponse.InfestedFoundry) { + applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry); + } // Omitting this field so opening the navigation resyncs the inventory which is more desirable for typical usage. - //inventoryResponse.LastInventorySync = toOid(new Types.ObjectId()); + inventoryResponse.LastInventorySync = undefined; + + // Set 2FA enabled so trading post can be used + inventoryResponse.HWIDProtectEnabled = true; + + if (buildLabel) { + // Fix nemesis for older versions + if ( + inventoryResponse.Nemesis && + version_compare(getNemesisManifest(inventoryResponse.Nemesis.manifest).minBuild, buildLabel) < 0 + ) { + inventoryResponse.Nemesis = undefined; + } + + if (version_compare(buildLabel, "2018.02.22.14.34") < 0) { + const personalRoomsDb = await getPersonalRooms(inventory.accountOwnerId.toString()); + const personalRooms = personalRoomsDb.toJSON(); + inventoryResponse.Ship = personalRooms.Ship; + + if (version_compare(buildLabel, "2016.12.21.19.13") <= 0) { + // U19.5 and below use $id instead of $oid + for (const category of equipmentKeys) { + for (const item of inventoryResponse[category]) { + toLegacyOid(item.ItemId); + } + } + for (const upgrade of inventoryResponse.Upgrades) { + toLegacyOid(upgrade.ItemId); + } + if (inventoryResponse.BrandedSuits) { + for (const id of inventoryResponse.BrandedSuits) { + toLegacyOid(id); + } + } + } + } + } return inventoryResponse; }; -export const addString = (arr: string[], str: string): void => { - if (!arr.find(x => x == str)) { +const addString = (arr: string[], str: string): void => { + if (arr.indexOf(str) == -1) { arr.push(str); } }; @@ -251,15 +369,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 00f1380c..3d510f26 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -6,8 +6,8 @@ import { buildConfig } from "@/src/services/buildConfigService"; import { Account } from "@/src/models/loginModel"; import { createAccount, isCorrectPassword, isNameTaken } from "@/src/services/loginService"; import { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "@/src/types/loginTypes"; -import { DTLS, groups, HUB, platformCDNs } from "@/static/fixed_responses/login_static"; import { logger } from "@/src/utils/logger"; +import { version_compare } from "@/src/helpers/inventoryHelpers"; export const loginController: RequestHandler = async (request, response) => { const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object @@ -20,10 +20,30 @@ export const loginController: RequestHandler = async (request, response) => { ? request.query.buildLabel.split(" ").join("+") : buildConfig.buildLabel; - if (!account && config.autoCreateAccount && loginRequest.ClientType != "webui") { + let myAddress: string; + let myUrlBase: string = request.protocol + "://"; + if (request.host.indexOf("warframe.com") == -1) { + // Client request was redirected cleanly, so we know it can reach us how it's reaching us now. + myAddress = request.hostname; + myUrlBase += request.host; + } else { + // Don't know how the client reached us, hoping the config does. + myAddress = config.myAddress; + myUrlBase += myAddress; + const port: number = request.protocol == "http" ? config.httpPort || 80 : config.httpsPort || 443; + if (port != (request.protocol == "http" ? 80 : 443)) { + myUrlBase += ":" + port; + } + } + + if ( + !account && + ((config.autoCreateAccount && loginRequest.ClientType != "webui") || + loginRequest.ClientType == "webui-register") + ) { 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 { @@ -35,17 +55,18 @@ export const loginController: RequestHandler = async (request, response) => { email: loginRequest.email, password: loginRequest.password, DisplayName: name, - CountryCode: loginRequest.lang.toUpperCase(), - ClientType: loginRequest.ClientType, + CountryCode: loginRequest.lang?.toUpperCase() ?? "EN", + ClientType: loginRequest.ClientType == "webui-register" ? "webui" : loginRequest.ClientType, CrossPlatformAllowed: true, ForceLogoutVersion: 0, ConsentNeeded: false, TrackedSettings: [], Nonce: nonce, - LatestEventMessageDate: new Date(0) + BuildLabel: buildLabel, + LastLogin: new Date() }); logger.debug("created new account"); - response.json(createLoginResponse(newAccount, buildLabel)); + response.json(createLoginResponse(myAddress, myUrlBase, newAccount, buildLabel)); return; } catch (error: unknown) { if (error instanceof Error) { @@ -54,43 +75,94 @@ 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 (loginRequest.ClientType == "webui-register") { + response.status(400).json({ error: "account already exists" }); + 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) { + // U17 seems to handle "nonce still set" like a login failure. + if (version_compare(buildLabel, "2015.12.05.18.07") >= 0) { + response.status(400).send({ error: "nonce still set" }); + return; + } + } + + account.ClientType = loginRequest.ClientType; account.Nonce = nonce; - } - if (loginRequest.ClientType != "webui") { - account.CountryCode = loginRequest.lang.toUpperCase(); + account.CountryCode = loginRequest.lang?.toUpperCase() ?? "EN"; + account.BuildLabel = buildLabel; + account.LastLogin = new Date(); } await account.save(); - response.json(createLoginResponse(account.toJSON(), buildLabel)); + response.json(createLoginResponse(myAddress, myUrlBase, account.toJSON(), buildLabel)); }; -const createLoginResponse = (account: IDatabaseAccountJson, buildLabel: string): ILoginResponse => { - return { +const createLoginResponse = ( + myAddress: string, + myUrlBase: string, + account: IDatabaseAccountJson, + buildLabel: string +): ILoginResponse => { + const resp: ILoginResponse = { id: account.id, DisplayName: account.DisplayName, CountryCode: account.CountryCode, - ClientType: account.ClientType, - CrossPlatformAllowed: account.CrossPlatformAllowed, - ForceLogoutVersion: account.ForceLogoutVersion, AmazonAuthToken: account.AmazonAuthToken, AmazonRefreshToken: account.AmazonRefreshToken, - ConsentNeeded: account.ConsentNeeded, - TrackedSettings: account.TrackedSettings, Nonce: account.Nonce, - Groups: groups, - platformCDNs: platformCDNs, - NRS: [config.myAddress], - DTLS: DTLS, - IRC: config.myIrcAddresses ?? [config.myAddress], - HUB: HUB, - BuildLabel: buildLabel, - MatchmakingBuildId: buildConfig.matchmakingBuildId + BuildLabel: buildLabel }; + if (version_compare(buildLabel, "2015.02.13.10.41") >= 0) { + resp.NRS = config.NRS; + } + if (version_compare(buildLabel, "2015.05.14.16.29") >= 0) { + // U17 and up + resp.IRC = config.myIrcAddresses ?? [myAddress]; + } + if (version_compare(buildLabel, "2018.11.08.14.45") >= 0) { + // U24 and up + resp.ConsentNeeded = account.ConsentNeeded; + resp.TrackedSettings = account.TrackedSettings; + } + if (version_compare(buildLabel, "2019.08.29.20.01") >= 0) { + // U25.7 and up + resp.ForceLogoutVersion = account.ForceLogoutVersion; + } + if (version_compare(buildLabel, "2019.10.31.22.42") >= 0) { + // U26 and up + resp.Groups = []; + } + if (version_compare(buildLabel, "2021.04.13.19.58") >= 0) { + resp.DTLS = 99; + } + if (version_compare(buildLabel, "2022.04.29.12.53") >= 0) { + resp.ClientType = account.ClientType; + } + if (version_compare(buildLabel, "2022.09.06.19.24") >= 0) { + resp.CrossPlatformAllowed = account.CrossPlatformAllowed; + resp.HUB = `${myUrlBase}/api/`; + resp.MatchmakingBuildId = buildConfig.matchmakingBuildId; + } + if (version_compare(buildLabel, "2023.04.25.23.40") >= 0) { + resp.platformCDNs = [`${myUrlBase}/`]; + } + return resp; }; 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..290a13f8 --- /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.indexOf(body.ChosenReward) == -1) { + 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..3b5009c2 100644 --- a/src/controllers/api/missionInventoryUpdateController.ts +++ b/src/controllers/api/missionInventoryUpdateController.ts @@ -1,11 +1,12 @@ import { RequestHandler } from "express"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getAccountForRequest } from "@/src/services/loginService"; import { IMissionInventoryUpdateRequest } from "@/src/types/requestTypes"; import { addMissionInventoryUpdates, addMissionRewards } from "@/src/services/missionInventoryUpdateService"; -import { getInventory } from "@/src/services/inventoryService"; +import { generateRewardSeed, getInventory } from "@/src/services/inventoryService"; import { getInventoryResponse } from "./inventoryController"; import { logger } from "@/src/utils/logger"; +import { IMissionInventoryUpdateResponse } from "@/src/types/missionTypes"; /* **** INPUT **** @@ -47,18 +48,30 @@ 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 account = await getAccountForRequest(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 inventory = await getInventory(account._id.toString()); + const firstCompletion = missionReport.SortieId + ? inventory.CompletedSorties.indexOf(missionReport.SortieId) == -1 + : false; + const inventoryUpdates = await addMissionInventoryUpdates(account, inventory, missionReport); - if (missionReport.MissionStatus !== "GS_SUCCESS") { + if ( + missionReport.MissionStatus !== "GS_SUCCESS" && + !( + missionReport.RewardInfo?.jobId || + missionReport.RewardInfo?.challengeMissionId || + missionReport.RewardInfo?.T + ) + ) { + if (missionReport.EndOfMatchUpload) { + inventory.RewardSeed = generateRewardSeed(); + } await inventory.save(); - const inventoryResponse = await getInventoryResponse(inventory, true); + const inventoryResponse = await getInventoryResponse(inventory, true, account.BuildLabel); res.json({ InventoryJson: JSON.stringify(inventoryResponse), MissionRewards: [] @@ -66,10 +79,20 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res) return; } - const { MissionRewards, inventoryChanges, credits } = await addMissionRewards(inventory, missionReport); + const { + MissionRewards, + inventoryChanges, + credits, + AffiliationMods, + SyndicateXPItemReward, + ConquestCompletedMissionsCount + } = await addMissionRewards(inventory, missionReport, firstCompletion); + if (missionReport.EndOfMatchUpload) { + inventory.RewardSeed = generateRewardSeed(); + } await inventory.save(); - const inventoryResponse = await getInventoryResponse(inventory, true); + const inventoryResponse = await getInventoryResponse(inventory, true, account.BuildLabel); //TODO: figure out when to send inventory. it is needed for many cases. res.json({ @@ -78,8 +101,11 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res) MissionRewards, ...credits, ...inventoryUpdates, - FusionPoints: inventoryChanges?.FusionPoints - }); + //FusionPoints: inventoryChanges?.FusionPoints, // This in combination with InventoryJson or InventoryChanges seems to just double the number of endo shown, so unsure when this is needed. + SyndicateXPItemReward, + AffiliationMods, + ConquestCompletedMissionsCount + } satisfies IMissionInventoryUpdateResponse); }; /* diff --git a/src/controllers/api/modularWeaponCraftingController.ts b/src/controllers/api/modularWeaponCraftingController.ts index d70f48ca..6034132c 100644 --- a/src/controllers/api/modularWeaponCraftingController.ts +++ b/src/controllers/api/modularWeaponCraftingController.ts @@ -1,30 +1,29 @@ 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 } from "@/src/services/inventoryService"; - -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 { + getInventory, + updateCurrency, + addEquipment, + addMiscItems, + applyDefaultUpgrades, + occupySlot, + productCategoryToInventoryBin, + combineInventoryChanges, + addSpecialItem +} from "@/src/services/inventoryService"; +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, ExportWeapons, IDefaultUpgrade } from "warframe-public-export-plus"; +import { Status } from "@/src/types/inventoryTypes/inventoryTypes"; interface IModularCraftRequest { WeaponType: string; Parts: string[]; + isWebUi?: boolean; } export const modularWeaponCraftingController: RequestHandler = async (req, res) => { @@ -36,30 +35,163 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res) const category = modularWeaponTypes[data.WeaponType]; const inventory = await getInventory(accountId); - // Give weapon - const weapon = addEquipment(inventory, category, data.WeaponType, data.Parts); + let defaultUpgrades: IDefaultUpgrade[] | undefined; + const defaultOverwrites: Partial = { + ModularParts: data.Parts + }; + 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]; + + 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]]; + + const meta = ExportSentinels[data.WeaponType]; + + for (const specialItem of meta.exalted!) { + addSpecialItem(inventory, specialItem, inventoryChanges); + } + + defaultUpgrades = meta.defaultUpgrades; + } else { + defaultUpgrades = getDefaultUpgrades(data.Parts); + } + + if (category == "MoaPets") { + const weapon = ExportSentinels[data.WeaponType].defaultWeapon; + if (weapon) { + const category = ExportWeapons[weapon].productCategory; + addEquipment(inventory, category, weapon, undefined, inventoryChanges); + combineInventoryChanges( + inventoryChanges, + occupySlot(inventory, productCategoryToInventoryBin(category)!, !!data.isWebUi) + ); + } + } + defaultOverwrites.Configs = applyDefaultUpgrades(inventory, defaultUpgrades); + addEquipment(inventory, category, data.WeaponType, defaultOverwrites, inventoryChanges); + combineInventoryChanges( + inventoryChanges, + occupySlot(inventory, productCategoryToInventoryBin(category)!, !!data.isWebUi) + ); + if (defaultUpgrades) { + inventoryChanges.RawUpgrades = defaultUpgrades.map(x => ({ ItemType: x.ItemType, ItemCount: 1 })); + } // Remove credits & parts const miscItemChanges = []; - for (const part of data.Parts) { - miscItemChanges.push({ - ItemType: part, - ItemCount: -1 - }); + let currencyChanges = {}; + if (!data.isWebUi) { + for (const part of data.Parts) { + miscItemChanges.push({ + ItemType: part, + ItemCount: -1 + }); + } + currencyChanges = updateCurrency( + inventory, + category == "Hoverboards" || + category == "MoaPets" || + category == "LongGuns" || + category == "Pistols" || + category == "KubrowPets" + ? 5000 + : 4000, // Definitely correct for Melee & OperatorAmps + false + ); + addMiscItems(inventory, miscItemChanges); } - const currencyChanges = updateCurrency( - inventory, - category == "Hoverboards" || category == "MoaPets" ? 5000 : 4000, - false - ); - addMiscItems(inventory, miscItemChanges); - await inventory.save(); + await inventory.save(); // Tell client what we did res.json({ InventoryChanges: { + ...inventoryChanges, ...currencyChanges, - [category]: [weapon], MiscItems: miscItemChanges } }); diff --git a/src/controllers/api/modularWeaponSaleController.ts b/src/controllers/api/modularWeaponSaleController.ts index fac479f3..767d1a94 100644 --- a/src/controllers/api/modularWeaponSaleController.ts +++ b/src/controllers/api/modularWeaponSaleController.ts @@ -1,8 +1,183 @@ 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 { SRng } 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 && + !data.excludeFromCodex // exclude pvp variants + ) { + 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, { + Features: EquipmentFeatures.DOUBLE_CAPACITY | EquipmentFeatures.GILDED, + ItemName: payload.ItemName, + Configs: configs, + ModularParts: weaponInfo.ModularParts, + 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 SRng(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..9c305103 --- /dev/null +++ b/src/controllers/api/nemesisController.ts @@ -0,0 +1,325 @@ +import { version_compare } from "@/src/helpers/inventoryHelpers"; +import { + consumeModCharge, + encodeNemesisGuess, + getInfNodes, + getKnifeUpgrade, + getNemesisManifest, + getNemesisPasscode, + getNemesisPasscodeModTypes, + IKnifeResponse +} from "@/src/helpers/nemesisHelpers"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Loadout } from "@/src/models/inventoryModels/loadoutModel"; +import { freeUpSlot, getInventory } from "@/src/services/inventoryService"; +import { getAccountForRequest } from "@/src/services/loginService"; +import { SRng } from "@/src/services/rngService"; +import { IMongoDate, IOid } from "@/src/types/commonTypes"; +import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { + IInnateDamageFingerprint, + IInventoryClient, + INemesisClient, + InventorySlot, + IUpgradeClient, + IWeaponSkinClient, + LoadoutIndex, + TEquipmentKey, + TNemesisFaction +} from "@/src/types/inventoryTypes/inventoryTypes"; +import { logger } from "@/src/utils/logger"; +import { RequestHandler } from "express"; + +export const nemesisController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + if ((req.query.mode as string) == "f") { + const body = getJSONfromString(String(req.body)); + const inventory = await getInventory(account._id.toString(), 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(account._id.toString(), "Nemesis"); + const body = getJSONfromString(String(req.body)); + const passcode = getNemesisPasscode(inventory.Nemesis!); + 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 == "r") { + const inventory = await getInventory( + account._id.toString(), + "Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades" + ); + const body = getJSONfromString(String(req.body)); + if (inventory.Nemesis!.Faction == "FC_INFESTATION") { + const guess: number[] = [body.guess & 0xf, (body.guess >> 4) & 0xf, (body.guess >> 8) & 0xf]; + const passcode = getNemesisPasscode(inventory.Nemesis!)[0]; + + // Add to GuessHistory + const result1 = passcode == guess[0] ? 0 : 1; + const result2 = passcode == guess[1] ? 0 : 1; + const result3 = passcode == guess[2] ? 0 : 1; + inventory.Nemesis!.GuessHistory.push( + encodeNemesisGuess(guess[0], result1, guess[1], result2, guess[2], result3) + ); + + // Increase antivirus if correct antivirus mod is installed + const response: IKnifeResponse = {}; + if (result1 == 0 || result2 == 0 || result3 == 0) { + let antivirusGain = 5; + const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!; + const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid); + const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0; + const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!; + for (const upgrade of body.knife!.AttachedUpgrades) { + switch (upgrade.ItemType) { + case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod": + antivirusGain += 10; + consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); + break; + case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod": + antivirusGain += 10; + consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); + break; + case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod": // Instant Secure + antivirusGain += 15; + consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); + break; + case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod": // Immuno Shield + antivirusGain += 15; + consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); + break; + case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod": + antivirusGain += 10; + consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); + break; + } + } + inventory.Nemesis!.HenchmenKilled += antivirusGain; + } + + if (inventory.Nemesis!.HenchmenKilled >= 100) { + inventory.Nemesis!.HenchmenKilled = 100; + } + inventory.Nemesis!.InfNodes = getInfNodes(getNemesisManifest(inventory.Nemesis!.manifest), 0); + + await inventory.save(); + res.json(response); + } else { + const passcode = getNemesisPasscode(inventory.Nemesis!); + if (passcode[body.position] != body.guess) { + res.end(); + } else { + inventory.Nemesis!.Rank += 1; + inventory.Nemesis!.InfNodes = getInfNodes( + getNemesisManifest(inventory.Nemesis!.manifest), + inventory.Nemesis!.Rank + ); + await inventory.save(); + res.json({ RankIncrease: 1 }); + } + } + } else if ((req.query.mode as string) == "rs") { + // report spawn; POST but no application data in body + const inventory = await getInventory(account._id.toString(), "Nemesis"); + inventory.Nemesis!.LastEnc = inventory.Nemesis!.MissionCount; + await inventory.save(); + res.json({ LastEnc: inventory.Nemesis!.LastEnc }); + } else if ((req.query.mode as string) == "s") { + const inventory = await getInventory(account._id.toString(), "Nemesis"); + const body = getJSONfromString(String(req.body)); + body.target.fp = BigInt(body.target.fp); + + const manifest = getNemesisManifest(body.target.manifest); + if (account.BuildLabel && version_compare(manifest.minBuild, account.BuildLabel) < 0) { + logger.warn( + `client on version ${account.BuildLabel} provided nemesis manifest ${body.target.manifest} which was expected to require ${manifest.minBuild} or above. please file a bug report.` + ); + } + + let weaponIdx = -1; + if (body.target.Faction != "FC_INFESTATION") { + const weapons: readonly string[] = manifest.weapons; + const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1); + weaponIdx = initialWeaponIdx; + do { + const weapon = weapons[weaponIdx]; + if (body.target.DisallowedWeapons.indexOf(weapon) == -1) { + break; + } + weaponIdx = (weaponIdx + 1) % weapons.length; + } while (weaponIdx != initialWeaponIdx); + } + + 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(manifest, 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 if ((req.query.mode as string) == "w") { + const inventory = await getInventory( + account._id.toString(), + "Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades" + ); + //const body = getJSONfromString(String(req.body)); + + inventory.Nemesis!.InfNodes = [ + { + Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode, + Influence: 1 + } + ]; + inventory.Nemesis!.Weakened = true; + + const response: IKnifeResponse & { target: INemesisClient } = { + target: inventory.toJSON().Nemesis! + }; + + // Consume charge of the correct requiem mod(s) + const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!; + const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid); + const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0; + const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!; + const modTypes = getNemesisPasscodeModTypes(inventory.Nemesis!); + for (const modType of modTypes) { + const upgrade = getKnifeUpgrade(inventory, dataknifeUpgrades, modType); + consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); + } + + await inventory.save(); + res.json(response); + } 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: TNemesisFaction; + 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[]; +} + +interface INemesisRequiemRequest { + guess: number; // grn/crp: 4 bits | coda: 3x 4 bits + position: number; // grn/crp: 0-2 | coda: 0 + // knife field provided for coda only + knife?: IKnife; +} + +// interface INemesisWeakenRequest { +// target: INemesisClient; +// knife: IKnife; +// } + +interface IKnife { + Item: IEquipmentClient; + Skins: IWeaponSkinClient[]; + ModSlot: number; + CustSlot: number; + AttachedUpgrades: IUpgradeClient[]; + HiddenWhenHolstered: boolean; +} diff --git a/src/controllers/api/placeDecoInComponentController.ts b/src/controllers/api/placeDecoInComponentController.ts new file mode 100644 index 00000000..a45806a8 --- /dev/null +++ b/src/controllers/api/placeDecoInComponentController.ts @@ -0,0 +1,124 @@ +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"; +import { config } from "@/src/services/configService"; + +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; + deco.Scale = request.Scale; + } else { + const deco = + component.Decos[ + component.Decos.push({ + _id: new Types.ObjectId(), + Type: request.Type, + Pos: request.Pos, + Rot: request.Rot, + Scale: request.Scale, + 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 (deco.Type != "/Lotus/Objects/Tenno/Props/TnoPaintBotDojoDeco") { + if (!meta || (meta.price == 0 && meta.ingredients.length == 0) || config.noDojoDecoBuildStage) { + deco.CompletionTime = new Date(); + if (meta) { + processDojoBuildMaterialsGathered(guild, meta); + } + } 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[]; + Scale?: number; + Name?: string; + Sockets?: number; + MoveId?: string; + ShipDeco?: boolean; + VaultDeco?: boolean; +} diff --git a/src/controllers/api/playedParkourTutorialController.ts b/src/controllers/api/playedParkourTutorialController.ts new file mode 100644 index 00000000..a37689f2 --- /dev/null +++ b/src/controllers/api/playedParkourTutorialController.ts @@ -0,0 +1,9 @@ +import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const playedParkourTutorialController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + await Inventory.updateOne({ accountOwnerId: accountId }, { PlayedParkourTutorial: true }); + res.end(); +}; 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/projectionManagerController.ts b/src/controllers/api/projectionManagerController.ts index 90c07f45..cd209942 100644 --- a/src/controllers/api/projectionManagerController.ts +++ b/src/controllers/api/projectionManagerController.ts @@ -2,13 +2,16 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { addMiscItems, getInventory } from "@/src/services/inventoryService"; import { ExportRelics, IRelic } from "warframe-public-export-plus"; +import { config } from "@/src/services/configService"; export const projectionManagerController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId); const request = JSON.parse(String(req.body)) as IProjectionUpgradeRequest; const [era, category, currentQuality] = parseProjection(request.projectionType); - const upgradeCost = (request.qualityTag - qualityKeywordToNumber[currentQuality]) * 25; + const upgradeCost = config.dontSubtractVoidTraces + ? 0 + : (request.qualityTag - qualityKeywordToNumber[currentQuality]) * 25; const newProjectionType = findProjection(era, category, qualityNumberToKeyword[request.qualityTag]); addMiscItems(inventory, [ { @@ -50,6 +53,7 @@ const qualityKeywordToNumber: Record = { // e.g. "/Lotus/Types/Game/Projections/T2VoidProjectionProteaPrimeDBronze" -> ["Lith", "W5", "VPQ_BRONZE"] const parseProjection = (typeName: string): [string, string, VoidProjectionQuality] => { const relic: IRelic | undefined = ExportRelics[typeName]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!relic) { throw new Error(`Unknown projection ${typeName}`); } diff --git a/src/controllers/api/questControlController.ts b/src/controllers/api/questControlController.ts new file mode 100644 index 00000000..0a6e0781 --- /dev/null +++ b/src/controllers/api/questControlController.ts @@ -0,0 +1,25 @@ +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +// Basic shim handling action=sync to login on U21 +export const questControlController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const quests: IQuestState[] = []; + for (const quest of inventory.QuestKeys) { + quests.push({ + quest: quest.ItemType, + state: 3 // COMPLETE + }); + } + res.json({ + QuestState: quests + }); +}; + +interface IQuestState { + quest: string; + state: number; + task?: string; +} 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..5e1d792b --- /dev/null +++ b/src/controllers/api/releasePetController.ts @@ -0,0 +1,27 @@ +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, + payload.recipeName == "/Lotus/Types/Game/KubrowPet/ReleasePetRecipe" ? 25000 : 0, + 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" | "webui"; + petId: string; +} diff --git a/src/controllers/api/removeFriendController.ts b/src/controllers/api/removeFriendController.ts new file mode 100644 index 00000000..24b39c2e --- /dev/null +++ b/src/controllers/api/removeFriendController.ts @@ -0,0 +1,99 @@ +import { toOid } from "@/src/helpers/inventoryHelpers"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Friendship } from "@/src/models/friendModel"; +import { Account } from "@/src/models/loginModel"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IOid } from "@/src/types/commonTypes"; +import { parallelForeach } from "@/src/utils/async-utils"; +import { RequestHandler } from "express"; +import { Types } from "mongoose"; + +export const removeFriendGetController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + if (req.query.all) { + const [internalFriendships, externalFriendships] = await Promise.all([ + Friendship.find({ owner: accountId }, "friend"), + Friendship.find({ friend: accountId }, "owner") + ]); + const promises: Promise[] = []; + const friends: IOid[] = []; + for (const externalFriendship of externalFriendships) { + if (!internalFriendships.find(x => x.friend.equals(externalFriendship.owner))) { + promises.push(Friendship.deleteOne({ _id: externalFriendship._id }) as unknown as Promise); + friends.push(toOid(externalFriendship.owner)); + } + } + await Promise.all(promises); + res.json({ + Friends: friends + } satisfies IRemoveFriendsResponse); + } else { + const friendId = req.query.friendId as string; + await Promise.all([ + Friendship.deleteOne({ owner: accountId, friend: friendId }), + Friendship.deleteOne({ owner: friendId, friend: accountId }) + ]); + res.json({ + Friends: [{ $oid: friendId }] + } satisfies IRemoveFriendsResponse); + } +}; + +export const removeFriendPostController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const data = getJSONfromString(String(req.body)); + const friends = new Set((await Friendship.find({ owner: accountId }, "friend")).map(x => x.friend)); + // TOVERIFY: Should pending friendships also be kept? + + // Keep friends that have been online within threshold + await parallelForeach([...friends], async friend => { + const account = (await Account.findById(friend, "LastLogin"))!; + const daysLoggedOut = (Date.now() - account.LastLogin.getTime()) / 86400_000; + if (daysLoggedOut < data.DaysLoggedOut) { + friends.delete(friend); + } + }); + + if (data.SkipClanmates) { + const inventory = await getInventory(accountId, "GuildId"); + if (inventory.GuildId) { + await parallelForeach([...friends], async friend => { + const friendInventory = await getInventory(friend.toString(), "GuildId"); + if (friendInventory.GuildId?.equals(inventory.GuildId)) { + friends.delete(friend); + } + }); + } + } + + // Remove all remaining friends that aren't in SkipFriendIds & give response. + const promises = []; + const response: IOid[] = []; + for (const friend of friends) { + if (!data.SkipFriendIds.find(skipFriendId => checkFriendId(skipFriendId, friend))) { + promises.push(Friendship.deleteOne({ owner: accountId, friend: friend })); + promises.push(Friendship.deleteOne({ owner: friend, friend: accountId })); + response.push(toOid(friend)); + } + } + await Promise.all(promises); + res.json({ + Friends: response + } satisfies IRemoveFriendsResponse); +}; + +// The friend ids format is a bit weird, e.g. when 6633b81e9dba0b714f28ff02 (A) is friends with 67cdac105ef1f4b49741c267 (B), A's friend id for B is 808000105ef1f40560ca079e and B's friend id for A is 8000b81e9dba0b06408a8075. +const checkFriendId = (friendId: string, b: Types.ObjectId): boolean => { + return friendId.substring(6, 6 + 8) == b.toString().substring(6, 6 + 8); +}; + +interface IBatchRemoveFriendsRequest { + DaysLoggedOut: number; + SkipClanmates: boolean; + SkipFriendIds: string[]; +} + +interface IRemoveFriendsResponse { + Friends: IOid[]; +} 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/removeIgnoredUserController.ts b/src/controllers/api/removeIgnoredUserController.ts new file mode 100644 index 00000000..73613ce6 --- /dev/null +++ b/src/controllers/api/removeIgnoredUserController.ts @@ -0,0 +1,21 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Account, Ignore } from "@/src/models/loginModel"; +import { getAccountForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const removeIgnoredUserController: RequestHandler = async (req, res) => { + const accountId = await getAccountForRequest(req); + const data = getJSONfromString(String(req.body)); + const ignoreeAccount = await Account.findOne( + { DisplayName: data.playerName.substring(0, data.playerName.length - 1) }, + "_id" + ); + if (ignoreeAccount) { + await Ignore.deleteOne({ ignorer: accountId, ignoree: ignoreeAccount._id }); + } + res.end(); +}; + +interface IRemoveIgnoredUserRequest { + playerName: string; +} diff --git a/src/controllers/api/rerollRandomModController.ts b/src/controllers/api/rerollRandomModController.ts index 71bc8f1c..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 { createUnveiledRivenFingerprint, randomiseRivenStats, RivenFingerprint } from "@/src/helpers/rivenHelper"; import { ExportUpgrades } from "warframe-public-export-plus"; -import { getRandomElement } from "@/src/services/rngService"; +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); - - randomiseStats(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"); @@ -52,28 +63,6 @@ export const rerollRandomModController: RequestHandler = async (req, res) => { } }; -const randomiseStats = (randomModType: string, fingerprint: IUnveiledRivenFingerprint): void => { - const meta = ExportUpgrades[randomModType]; - - 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) }); - } -}; - type RerollRandomModRequest = LetsGoGamblingRequest | AwDangitRequest; interface LetsGoGamblingRequest { @@ -85,20 +74,10 @@ interface AwDangitRequest { CommitReroll: boolean; } -interface IUnveiledRivenFingerprint { - compat: string; - lim: number; - lvl: number; - lvlReq: 0; - rerolls?: number; - pol: string; - buffs: IRivenStat[]; - curses: IRivenStat[]; -} - -interface IRivenStat { - Tag: string; - Value: number; +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..171538a7 100644 --- a/src/controllers/api/saveDialogueController.ts +++ b/src/controllers/api/saveDialogueController.ts @@ -1,53 +1,40 @@ -import { getInventory } from "@/src/services/inventoryService"; +import { config } from "@/src/services/configService"; +import { addEmailItem, getDialogue, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { ICompletedDialogue } from "@/src/types/inventoryTypes/inventoryTypes"; -import { logger } from "@/src/utils/logger"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { RequestHandler } from "express"; export const saveDialogueController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const request = JSON.parse(String(req.body)) as SaveDialogueRequest; if ("YearIteration" in request) { - const inventory = await getInventory(accountId); - if (inventory.DialogueHistory) { - inventory.DialogueHistory.YearIteration = request.YearIteration; - } else { - inventory.DialogueHistory = { YearIteration: request.YearIteration }; - } + const inventory = await getInventory(accountId, "DialogueHistory"); + inventory.DialogueHistory ??= {}; + inventory.DialogueHistory.YearIteration = request.YearIteration; await inventory.save(); res.end(); } else { const inventory = await getInventory(accountId); - if (!inventory.DialogueHistory) { - throw new Error("bad inventory state"); - } - if (request.QueuedDialogues.length != 0 || request.OtherDialogueInfos.length != 0) { - logger.error(`saveDialogue request not fully handled: ${String(req.body)}`); - } + const inventoryChanges: IInventoryChanges = {}; + const tomorrowAt0Utc = config.noKimCooldowns + ? Date.now() + : (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000; + inventory.DialogueHistory ??= {}; inventory.DialogueHistory.Dialogues ??= []; - let dialogue = inventory.DialogueHistory.Dialogues.find(x => x.DialogueName == request.DialogueName); - if (!dialogue) { - dialogue = - inventory.DialogueHistory.Dialogues[ - inventory.DialogueHistory.Dialogues.push({ - Rank: 0, - Chemistry: 0, - AvailableDate: new Date(0), - AvailableGiftDate: new Date(0), - RankUpExpiry: new Date(0), - BountyChemExpiry: new Date(0), - Gifts: [], - Booleans: [], - Completed: [], - DialogueName: request.DialogueName - }) - 1 - ]; - } + const dialogue = getDialogue(inventory, request.DialogueName); dialogue.Rank = request.Rank; dialogue.Chemistry = request.Chemistry; - //dialogue.QueuedDialogues = request.QueuedDialogues; + dialogue.QueuedDialogues = request.QueuedDialogues; for (const bool of request.Booleans) { dialogue.Booleans.push(bool); + if (bool == "LizzieShawzin") { + await addEmailItem( + inventory, + "/Lotus/Types/Items/EmailItems/LizzieShawzinSkinEmailItem", + inventoryChanges + ); + } } for (const bool of request.ResetBooleans) { const index = dialogue.Booleans.findIndex(x => x == bool); @@ -55,14 +42,38 @@ export const saveDialogueController: RequestHandler = async (req, res) => { dialogue.Booleans.splice(index, 1); } } - dialogue.Completed.push(request.Data); - const tomorrowAt0Utc = (Math.trunc(Date.now() / (86400 * 1000)) + 1) * 86400 * 1000; - dialogue.AvailableDate = new Date(tomorrowAt0Utc); - await inventory.save(); - res.json({ - InventoryChanges: [], - AvailableDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } } - }); + for (const info of request.OtherDialogueInfos) { + const otherDialogue = getDialogue(inventory, info.Dialogue); + if (info.Tag != "") { + otherDialogue.QueuedDialogues.push(info.Tag); + } + otherDialogue.Chemistry += info.Value; // unsure + } + if (request.Data) { + dialogue.Completed.push(request.Data); + dialogue.AvailableDate = new Date(tomorrowAt0Utc); + await inventory.save(); + res.json({ + InventoryChanges: inventoryChanges, + AvailableDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } } + }); + } else if (request.Gift) { + const inventoryChanges = updateCurrency(inventory, request.Gift.Cost, false); + const gift = dialogue.Gifts.find(x => x.Item == request.Gift!.Item); + if (gift) { + gift.GiftedQuantity += 1; + } else { + dialogue.Gifts.push({ Item: request.Gift.Item, GiftedQuantity: 1 }); + } + dialogue.AvailableGiftDate = new Date(tomorrowAt0Utc); + await inventory.save(); + res.json({ + InventoryChanges: inventoryChanges, + AvailableGiftDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } } + }); + } else { + res.end(); + } } }; @@ -77,9 +88,21 @@ interface SaveCompletedDialogueRequest { Rank: number; Chemistry: number; CompletionType: number; - QueuedDialogues: string[]; // unsure + QueuedDialogues: string[]; + Gift?: { + Item: string; + GainedChemistry: number; + Cost: number; + GiftedQuantity: number; + }; Booleans: string[]; ResetBooleans: string[]; - Data: ICompletedDialogue; - OtherDialogueInfos: string[]; // unsure + Data?: ICompletedDialogue; + OtherDialogueInfos: IOtherDialogueInfo[]; +} + +interface IOtherDialogueInfo { + Dialogue: string; + Tag: string; + Value: number; } diff --git a/src/controllers/api/saveLoadout.ts b/src/controllers/api/saveLoadout.ts deleted file mode 100644 index 38ea5559..00000000 --- a/src/controllers/api/saveLoadout.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { RequestHandler } from "express"; -import { ISaveLoadoutRequest } from "@/src/types/saveLoadoutTypes"; -import { handleInventoryItemConfigChange } from "@/src/services/saveLoadoutService"; -import { getAccountIdForRequest } from "@/src/services/loginService"; -import { logger } from "@/src/utils/logger"; - -export const saveLoadoutController: RequestHandler = async (req, res) => { - //validate here - const accountId = await getAccountIdForRequest(req); - - try { - const body: ISaveLoadoutRequest = JSON.parse(req.body as string) as ISaveLoadoutRequest; - // console.log(util.inspect(body, { showHidden: false, depth: null, colors: true })); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { UpgradeVer, ...equipmentChanges } = body; - const newLoadoutId = await handleInventoryItemConfigChange(equipmentChanges, accountId); - - //send back new loadout id, if new loadout was added - if (newLoadoutId) { - res.send(newLoadoutId); - } - res.status(200).end(); - } catch (error: unknown) { - if (error instanceof Error) { - logger.error(`error in saveLoadoutController: ${error.message}`); - res.status(400).json({ error: error.message }); - } else { - res.status(400).json({ error: "unknown error" }); - } - } -}; diff --git a/src/controllers/api/saveLoadoutController.ts b/src/controllers/api/saveLoadoutController.ts new file mode 100644 index 00000000..c5383e1c --- /dev/null +++ b/src/controllers/api/saveLoadoutController.ts @@ -0,0 +1,22 @@ +import { RequestHandler } from "express"; +import { ISaveLoadoutRequest } from "@/src/types/saveLoadoutTypes"; +import { handleInventoryItemConfigChange } from "@/src/services/saveLoadoutService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; + +export const saveLoadoutController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + + const body: ISaveLoadoutRequest = getJSONfromString(String(req.body)); + // console.log(util.inspect(body, { showHidden: false, depth: null, colors: true })); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { UpgradeVer, ...equipmentChanges } = body; + const newLoadoutId = await handleInventoryItemConfigChange(equipmentChanges, accountId); + + //send back new loadout id, if new loadout was added + if (newLoadoutId) { + res.send(newLoadoutId); + } + res.end(); +}; 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..fdfb3a82 100644 --- a/src/controllers/api/sellController.ts +++ b/src/controllers/api/sellController.ts @@ -1,17 +1,79 @@ 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, + combineInventoryChanges, + addCrewShipRawSalvage, + addFusionPoints +} from "@/src/services/inventoryService"; +import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; +import { ExportDojoRecipes } from "warframe-public-export-plus"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; 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 as keyof TInventoryDatabaseDocument); + } + 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.MechSuits) { + requiredFields.add(InventorySlot.MECHSUITS); + } + if (payload.Items.Sentinels || payload.Items.SentinelWeapons || payload.Items.MoaPets) { + requiredFields.add(InventorySlot.SENTINELS); + } + if (payload.Items.OperatorAmps) { + requiredFields.add(InventorySlot.AMPS); + } + if (payload.Items.Hoverboards) { + requiredFields.add(InventorySlot.SPACESUITS); + } + if (payload.Items.CrewShipWeapons || payload.Items.CrewShipWeaponSkins) { + requiredFields.add(InventorySlot.RJ_COMPONENT_AND_ARMAMENTS); + requiredFields.add("CrewShipRawSalvage"); + if (payload.Items.CrewShipWeapons) { + requiredFields.add("CrewShipSalvagedWeapons"); + } + if (payload.Items.CrewShipWeaponSkins) { + requiredFields.add("CrewShipSalvagedWeaponSkins"); + } + } + const inventory = await getInventory(accountId, Array.from(requiredFields).join(" ")); // Give currency if (payload.SellCurrency == "SC_RegularCredits") { inventory.RegularCredits += payload.SellPrice; } else if (payload.SellCurrency == "SC_FusionPoints") { - inventory.FusionPoints += payload.SellPrice; + addFusionPoints(inventory, payload.SellPrice); } else if (payload.SellCurrency == "SC_PrimeBucks") { addMiscItems(inventory, [ { @@ -26,64 +88,146 @@ export const sellController: RequestHandler = async (req, res) => { ItemCount: payload.SellPrice } ]); + } else if (payload.SellCurrency == "SC_Resources") { + // Will add appropriate MiscItems from CrewShipWeapons or CrewShipWeaponSkins } else { throw new Error("Unknown SellCurrency: " + payload.SellCurrency); } + const inventoryChanges: IInventoryChanges = {}; + // Remove item(s) 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.MechSuits) { + payload.Items.MechSuits.forEach(sellItem => { + inventory.MechSuits.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.MECHSUITS); }); } 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.MoaPets) { + payload.Items.MoaPets.forEach(sellItem => { + inventory.MoaPets.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.CrewShipWeapons) { + payload.Items.CrewShipWeapons.forEach(sellItem => { + if (sellItem.String[0] == "/") { + addCrewShipRawSalvage(inventory, [ + { + ItemType: sellItem.String, + ItemCount: sellItem.Count * -1 + } + ]); + } else { + const index = inventory.CrewShipWeapons.findIndex(x => x._id.equals(sellItem.String)); + if (index != -1) { + if (payload.SellCurrency == "SC_Resources") { + refundPartialBuildCosts(inventory, inventory.CrewShipWeapons[index].ItemType, inventoryChanges); + } + inventory.CrewShipWeapons.splice(index, 1); + freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS); + } else { + inventory.CrewShipSalvagedWeapons.pull({ _id: sellItem.String }); + } + } + }); + } + if (payload.Items.CrewShipWeaponSkins) { + payload.Items.CrewShipWeaponSkins.forEach(sellItem => { + if (sellItem.String[0] == "/") { + addCrewShipRawSalvage(inventory, [ + { + ItemType: sellItem.String, + ItemCount: sellItem.Count * -1 + } + ]); + } else { + const index = inventory.CrewShipWeaponSkins.findIndex(x => x._id.equals(sellItem.String)); + if (index != -1) { + if (payload.SellCurrency == "SC_Resources") { + refundPartialBuildCosts( + inventory, + inventory.CrewShipWeaponSkins[index].ItemType, + inventoryChanges + ); + } + inventory.CrewShipWeaponSkins.splice(index, 1); + freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS); + } else { + inventory.CrewShipSalvagedWeaponSkins.pull({ _id: sellItem.String }); + } + } }); } if (payload.Items.Consumables) { @@ -132,7 +276,9 @@ export const sellController: RequestHandler = async (req, res) => { } await inventory.save(); - res.json({}); + res.json({ + inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges" + }); }; interface ISellRequest { @@ -148,10 +294,15 @@ interface ISellRequest { SpaceSuits?: ISellItem[]; SpaceGuns?: ISellItem[]; SpaceMelee?: ISellItem[]; + MechSuits?: ISellItem[]; Sentinels?: ISellItem[]; SentinelWeapons?: ISellItem[]; + MoaPets?: ISellItem[]; OperatorAmps?: ISellItem[]; Hoverboards?: ISellItem[]; + Drones?: ISellItem[]; + CrewShipWeapons?: ISellItem[]; + CrewShipWeaponSkins?: ISellItem[]; }; SellPrice: number; SellCurrency: @@ -168,3 +319,33 @@ interface ISellItem { String: string; // oid or uniqueName Count: number; } + +const refundPartialBuildCosts = ( + inventory: TInventoryDatabaseDocument, + itemType: string, + inventoryChanges: IInventoryChanges +): void => { + // House versions + const research = Object.values(ExportDojoRecipes.research).find(x => x.resultType == itemType); + if (research) { + const miscItemChanges = research.ingredients.map(x => ({ + ItemType: x.ItemType, + ItemCount: Math.trunc(x.ItemCount * 0.8) + })); + addMiscItems(inventory, miscItemChanges); + combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges }); + return; + } + + // Sigma versions + const recipe = Object.values(ExportDojoRecipes.fabrications).find(x => x.resultType == itemType); + if (recipe) { + const miscItemChanges = recipe.ingredients.map(x => ({ + ItemType: x.ItemType, + ItemCount: Math.trunc(x.ItemCount * 0.8) + })); + addMiscItems(inventory, miscItemChanges); + combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges }); + return; + } +}; diff --git a/src/controllers/api/sendMsgToInBoxController.ts b/src/controllers/api/sendMsgToInBoxController.ts new file mode 100644 index 00000000..7cad8c15 --- /dev/null +++ b/src/controllers/api/sendMsgToInBoxController.ts @@ -0,0 +1,31 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { createMessage } from "@/src/services/inboxService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const sendMsgToInBoxController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const data = getJSONfromString(String(req.body)); + await createMessage(accountId, [ + { + sub: data.title, + msg: data.message, + sndr: data.sender ?? "/Lotus/Language/Bosses/Ordis", + icon: data.senderIcon, + highPriority: data.highPriority, + transmission: data.transmission, + att: data.attachments + } + ]); + res.end(); +}; + +interface ISendMsgToInBoxRequest { + title: string; + message: string; + sender?: string; + senderIcon?: string; + highPriority?: boolean; + transmission?: string; + attachments?: string[]; +} 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/setDojoComponentColorsController.ts b/src/controllers/api/setDojoComponentColorsController.ts new file mode 100644 index 00000000..ac069242 --- /dev/null +++ b/src/controllers/api/setDojoComponentColorsController.ts @@ -0,0 +1,34 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +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 setDojoComponentColorsController: 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 data = getJSONfromString(String(req.body)); + const component = guild.DojoComponents.id(data.ComponentId)!; + //const deco = component.Decos!.find(x => x._id.equals(data.DecoId))!; + //deco.Pending = true; + //component.PaintBot = new Types.ObjectId(data.DecoId); + if ("lights" in req.query) { + component.PendingLights = data.Colours; + } else { + component.PendingColors = data.Colours; + } + await guild.save(); + res.json(await getDojoClient(guild, 0, component._id)); +}; + +interface ISetDojoComponentColorsRequest { + ComponentId: string; + DecoId: string; + Colours: number[]; +} 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/setDojoComponentSettingsController.ts b/src/controllers/api/setDojoComponentSettingsController.ts new file mode 100644 index 00000000..1286ba25 --- /dev/null +++ b/src/controllers/api/setDojoComponentSettingsController.ts @@ -0,0 +1,25 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +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 setDojoComponentSettingsController: 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 component = guild.DojoComponents.id(req.query.componentId as string)!; + const data = getJSONfromString(String(req.body)); + component.Settings = data.Settings; + await guild.save(); + res.json(await getDojoClient(guild, 0, component._id)); +}; + +interface ISetDojoComponentSettingsRequest { + Settings: 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/setFriendNoteController.ts b/src/controllers/api/setFriendNoteController.ts new file mode 100644 index 00000000..c12543da --- /dev/null +++ b/src/controllers/api/setFriendNoteController.ts @@ -0,0 +1,30 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Friendship } from "@/src/models/friendModel"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const setFriendNoteController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const payload = getJSONfromString(String(req.body)); + const friendship = await Friendship.findOne({ owner: accountId, friend: payload.FriendId }, "Note Favorite"); + if (friendship) { + if ("Note" in payload) { + friendship.Note = payload.Note; + } else { + friendship.Favorite = payload.Favorite; + } + await friendship.save(); + } + res.json({ + Id: payload.FriendId, + SetNote: "Note" in payload, + Note: friendship?.Note, + Favorite: friendship?.Favorite + }); +}; + +interface ISetFriendNoteRequest { + FriendId: string; + Note?: string; + Favorite?: boolean; +} diff --git a/src/controllers/api/setGuildMotdController.ts b/src/controllers/api/setGuildMotdController.ts new file mode 100644 index 00000000..1e09ab28 --- /dev/null +++ b/src/controllers/api/setGuildMotdController.ts @@ -0,0 +1,64 @@ +import { version_compare } from "@/src/helpers/inventoryHelpers"; +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(); + } + + if (!account.BuildLabel || version_compare(account.BuildLabel, "2020.03.24.20.24") > 0) { + res.json({ IsLongMOTD, MOTD }); + } else { + res.send(MOTD).end(); + } +}; diff --git a/src/controllers/api/setHubNpcCustomizationsController.ts b/src/controllers/api/setHubNpcCustomizationsController.ts new file mode 100644 index 00000000..6e199933 --- /dev/null +++ b/src/controllers/api/setHubNpcCustomizationsController.ts @@ -0,0 +1,21 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IHubNpcCustomization } from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; + +export const setHubNpcCustomizationsController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "HubNpcCustomizations"); + const upload = getJSONfromString(String(req.body)); + inventory.HubNpcCustomizations ??= []; + const cust = inventory.HubNpcCustomizations.find(x => x.Tag == upload.Tag); + if (cust) { + cust.Colors = upload.Colors; + cust.Pattern = upload.Pattern; + } else { + inventory.HubNpcCustomizations.push(upload); + } + await inventory.save(); + res.end(); +}; diff --git a/src/controllers/api/setPlacedDecoInfoController.ts b/src/controllers/api/setPlacedDecoInfoController.ts index 56b9afe7..19f76061 100644 --- a/src/controllers/api/setPlacedDecoInfoController.ts +++ b/src/controllers/api/setPlacedDecoInfoController.ts @@ -1,5 +1,5 @@ import { getAccountIdForRequest } from "@/src/services/loginService"; -import { ISetPlacedDecoInfoRequest } from "@/src/types/shipTypes"; +import { IPictureFrameInfo, ISetPlacedDecoInfoRequest } from "@/src/types/shipTypes"; import { RequestHandler } from "express"; import { handleSetPlacedDecoInfo } from "@/src/services/shipCustomizationsService"; @@ -7,5 +7,17 @@ export const setPlacedDecoInfoController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const payload = JSON.parse(req.body as string) as ISetPlacedDecoInfoRequest; await handleSetPlacedDecoInfo(accountId, payload); - res.end(); + res.json({ + DecoId: payload.DecoId, + IsPicture: true, + PictureFrameInfo: payload.PictureFrameInfo, + BootLocation: payload.BootLocation + } satisfies ISetPlacedDecoInfoResponse); }; + +interface ISetPlacedDecoInfoResponse { + DecoId: string; + IsPicture: boolean; + PictureFrameInfo?: IPictureFrameInfo; + BootLocation?: string; +} diff --git a/src/controllers/api/setShipFavouriteLoadoutController.ts b/src/controllers/api/setShipFavouriteLoadoutController.ts index d798e0ed..a7df934f 100644 --- a/src/controllers/api/setShipFavouriteLoadoutController.ts +++ b/src/controllers/api/setShipFavouriteLoadoutController.ts @@ -3,29 +3,40 @@ import { RequestHandler } from "express"; import { getPersonalRooms } from "@/src/services/personalRoomsService"; import { IOid } from "@/src/types/commonTypes"; import { Types } from "mongoose"; +import { IFavouriteLoadoutDatabase, TBootLocation } from "@/src/types/shipTypes"; export const setShipFavouriteLoadoutController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const personalRooms = await getPersonalRooms(accountId); const body = JSON.parse(String(req.body)) as ISetShipFavouriteLoadoutRequest; - if (body.BootLocation != "SHOP") { + if (body.BootLocation == "LISET") { + personalRooms.Ship.FavouriteLoadoutId = new Types.ObjectId(body.FavouriteLoadoutId.$oid); + } else if (body.BootLocation == "APARTMENT") { + updateTaggedDisplay(personalRooms.Apartment.FavouriteLoadouts, body); + } else if (body.BootLocation == "SHOP") { + updateTaggedDisplay(personalRooms.TailorShop.FavouriteLoadouts, body); + } else { + console.log(body); throw new Error(`unexpected BootLocation: ${body.BootLocation}`); } - const display = personalRooms.TailorShop.FavouriteLoadouts.find(x => x.Tag == body.TagName); - if (display) { - display.LoadoutId = new Types.ObjectId(body.FavouriteLoadoutId.$oid); - } else { - personalRooms.TailorShop.FavouriteLoadouts.push({ - Tag: body.TagName, - LoadoutId: new Types.ObjectId(body.FavouriteLoadoutId.$oid) - }); - } await personalRooms.save(); - res.json({}); + res.json(body); }; interface ISetShipFavouriteLoadoutRequest { - BootLocation: string; + BootLocation: TBootLocation; FavouriteLoadoutId: IOid; - TagName: string; + TagName?: string; } + +const updateTaggedDisplay = (arr: IFavouriteLoadoutDatabase[], body: ISetShipFavouriteLoadoutRequest): void => { + const display = arr.find(x => x.Tag == body.TagName!); + if (display) { + display.LoadoutId = new Types.ObjectId(body.FavouriteLoadoutId.$oid); + } else { + arr.push({ + Tag: body.TagName!, + LoadoutId: new Types.ObjectId(body.FavouriteLoadoutId.$oid) + }); + } +}; diff --git a/src/controllers/api/setShipVignetteController.ts b/src/controllers/api/setShipVignetteController.ts new file mode 100644 index 00000000..a1d991da --- /dev/null +++ b/src/controllers/api/setShipVignetteController.ts @@ -0,0 +1,48 @@ +import { addMiscItems, combineInventoryChanges, getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getPersonalRooms } from "@/src/services/personalRoomsService"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { logger } from "@/src/utils/logger"; +import { RequestHandler } from "express"; + +export const setShipVignetteController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "MiscItems"); + const personalRooms = await getPersonalRooms(accountId); + const body = JSON.parse(String(req.body)) as ISetShipVignetteRequest; + personalRooms.Ship.Wallpaper = body.Wallpaper; + personalRooms.Ship.Vignette = body.Vignette; + personalRooms.Ship.VignetteFish ??= []; + const inventoryChanges: IInventoryChanges = {}; + for (let i = 0; i != body.Fish.length; ++i) { + if (body.Fish[i] && !personalRooms.Ship.VignetteFish[i]) { + logger.debug(`moving ${body.Fish[i]} from inventory to vignette slot ${i}`); + const miscItemsDelta = [{ ItemType: body.Fish[i], ItemCount: -1 }]; + addMiscItems(inventory, miscItemsDelta); + combineInventoryChanges(inventoryChanges, { MiscItems: miscItemsDelta }); + } else if (personalRooms.Ship.VignetteFish[i] && !body.Fish[i]) { + logger.debug(`moving ${personalRooms.Ship.VignetteFish[i]} from vignette slot ${i} to inventory`); + const miscItemsDelta = [{ ItemType: personalRooms.Ship.VignetteFish[i], ItemCount: +1 }]; + addMiscItems(inventory, miscItemsDelta); + combineInventoryChanges(inventoryChanges, { MiscItems: miscItemsDelta }); + } + } + personalRooms.Ship.VignetteFish = body.Fish; + if (body.VignetteDecos.length) { + logger.error(`setShipVignette request not fully handled:`, body); + } + await Promise.all([inventory.save(), personalRooms.save()]); + res.json({ + Wallpaper: body.Wallpaper, + Vignette: body.Vignette, + VignetteFish: body.Fish, + InventoryChanges: inventoryChanges + }); +}; + +interface ISetShipVignetteRequest { + Wallpaper: string; + Vignette: string; + Fish: string[]; + VignetteDecos: unknown[]; +} 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..0773f252 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 { getAccountForRequest } 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 account = await getAccountForRequest(req); + const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys"); + const guild = await getGuildForRequestEx(req, inventory); + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, account._id, 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, undefined, account.BuildLabel)); }; 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..78adf1fc 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] && startRecipeRequest.Ids[i][0] != "/") { + const category = ExportWeapons[recipe.ingredients[i].ItemType].productCategory; + if (category != "LongGuns" && category != "Pistols" && category != "Melee") { + throw new Error(`unexpected equipment ingredient type: ${category}`); + } + 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..8b1a9c7a 100644 --- a/src/controllers/api/syndicateSacrificeController.ts +++ b/src/controllers/api/syndicateSacrificeController.ts @@ -5,6 +5,8 @@ import { ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-pl import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; +import { toStoreItem } from "@/src/services/itemDataService"; +import { logger } from "@/src/utils/logger"; export const syndicateSacrificeController: RequestHandler = async (request, response) => { const accountId = await getAccountIdForRequest(request); @@ -22,7 +24,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]; @@ -50,13 +52,6 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp syndicate.Title ??= 0; syndicate.Title += 1; - if (syndicate.Title > 0 && manifest.favours.length != 0) { - syndicate.FreeFavorsEarned ??= []; - if (!syndicate.FreeFavorsEarned.includes(syndicate.Title)) { - syndicate.FreeFavorsEarned.push(syndicate.Title); - } - } - if (reward) { combineInventoryChanges( res.InventoryChanges, @@ -64,6 +59,39 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp ); } + // Quacks like a nightwave syndicate? + if (manifest.dailyChallenges) { + const title = manifest.titles!.find(x => x.level == syndicate.Title); + if (title) { + res.NewEpisodeReward = true; + let rewardType: string; + let rewardCount: number; + if (title.storeItemReward) { + rewardType = title.storeItemReward; + rewardCount = 1; + } else { + rewardType = toStoreItem(title.reward!.ItemType); + rewardCount = title.reward!.ItemCount; + } + const rewardInventoryChanges = (await handleStoreItemAcquisition(rewardType, inventory, rewardCount)) + .InventoryChanges; + if (Object.keys(rewardInventoryChanges).length == 0) { + logger.debug(`nightwave rank up reward did not seem to get added, giving 50 creds instead`); + const nightwaveCredsItemType = manifest.titles![0].reward!.ItemType; + rewardInventoryChanges.MiscItems = [{ ItemType: nightwaveCredsItemType, ItemCount: 50 }]; + addMiscItems(inventory, rewardInventoryChanges.MiscItems); + } + combineInventoryChanges(res.InventoryChanges, rewardInventoryChanges); + } + } else { + if (syndicate.Title > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == syndicate.Title)) { + syndicate.FreeFavorsEarned ??= []; + if (!syndicate.FreeFavorsEarned.includes(syndicate.Title)) { + syndicate.FreeFavorsEarned.push(syndicate.Title); + } + } + } + await inventory.save(); response.json(res); diff --git a/src/controllers/api/syndicateStandingBonusController.ts b/src/controllers/api/syndicateStandingBonusController.ts index 6899ee3e..3170bc93 100644 --- a/src/controllers/api/syndicateStandingBonusController.ts +++ b/src/controllers/api/syndicateStandingBonusController.ts @@ -1,10 +1,12 @@ 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, addStanding, freeUpSlot, getInventory } 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 { getMaxStanding } from "@/src/helpers/syndicateStandingHelper"; +import { ExportSyndicates, ExportWeapons } from "warframe-public-export-plus"; +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 +14,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,44 +24,43 @@ 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; - let syndicate = inventory.Affiliations.find(x => x.Tag == request.Operation.AffiliationTag); - if (!syndicate) { - syndicate = - inventory.Affiliations[ - inventory.Affiliations.push({ Tag: request.Operation.AffiliationTag, Standing: 0 }) - 1 - ]; - } - - const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0); - if (syndicate.Standing + gainedStanding > max) { - gainedStanding = max - syndicate.Standing; - } - - if (syndicateMeta.medallionsCappedByDailyLimit) { - if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) { - gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin); + // 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`); } - updateStandingLimit(inventory, syndicateMeta.dailyLimitBin, gainedStanding); + 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 }; } - syndicate.Standing += gainedStanding; + const affiliationMod = addStanding(inventory, request.Operation.AffiliationTag, gainedStanding, true); await inventory.save(); res.json({ - InventoryChanges: { - MiscItems: request.Operation.Items - }, - AffiliationMods: [ - { - Tag: request.Operation.AffiliationTag, - Standing: gainedStanding - } - ] + InventoryChanges: inventoryChanges, + AffiliationMods: [affiliationMod] }); }; @@ -67,6 +69,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..b948bb79 100644 --- a/src/controllers/api/updateChallengeProgressController.ts +++ b/src/controllers/api/updateChallengeProgressController.ts @@ -1,16 +1,46 @@ 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 { getAccountForRequest } from "@/src/services/loginService"; +import { addChallenges, getInventory } from "@/src/services/inventoryService"; +import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IAffiliationMods } from "@/src/types/purchaseTypes"; -const updateChallengeProgressController: RequestHandler = async (req, res) => { - const payload = getJSONfromString(String(req.body)); - const accountId = await getAccountIdForRequest(req); +export const updateChallengeProgressController: RequestHandler = async (req, res) => { + const challenges = getJSONfromString(String(req.body)); + const account = await getAccountForRequest(req); - await updateChallengeProgress(payload, accountId); + const inventory = await getInventory( + account._id.toString(), + "ChallengeProgress SeasonChallengeHistory Affiliations" + ); + let affiliationMods: IAffiliationMods[] = []; + if (challenges.ChallengeProgress) { + affiliationMods = addChallenges( + account, + inventory, + challenges.ChallengeProgress, + challenges.SeasonChallengeCompletions + ); + } + if (challenges.SeasonChallengeHistory) { + challenges.SeasonChallengeHistory.forEach(({ challenge, id }) => { + const itemIndex = inventory.SeasonChallengeHistory.findIndex(i => i.challenge === challenge); + if (itemIndex !== -1) { + inventory.SeasonChallengeHistory[itemIndex].id = id; + } else { + inventory.SeasonChallengeHistory.push({ challenge, id }); + } + }); + } + await inventory.save(); - res.status(200).end(); + 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 d295a462..2a376b4a 100644 --- a/src/controllers/api/upgradesController.ts +++ b/src/controllers/api/upgradesController.ts @@ -11,7 +11,8 @@ 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) => { const accountId = await getAccountIdForRequest(req); @@ -24,7 +25,13 @@ export const upgradesController: RequestHandler = async (req, res) => { operation.UpgradeRequirement == "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker" ) { updateCurrency(inventory, 10, true); - } else { + } else if ( + operation.OperationType != "UOT_SWAP_POLARITY" && + operation.OperationType != "UOT_ABILITY_OVERRIDE" + ) { + if (!operation.UpgradeRequirement) { + throw new Error(`${operation.OperationType} operation should be free?`); + } addMiscItems(inventory, [ { ItemType: operation.UpgradeRequirement, @@ -48,8 +55,10 @@ export const upgradesController: RequestHandler = async (req, res) => { const recipe = getRecipeByResult(operation.UpgradeRequirement)!; for (const ingredient of recipe.ingredients) { totalPercentagePointsConsumed += ingredient.ItemCount / 10; - inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == ingredient.ItemType)!.Count -= - ingredient.ItemCount; + if (!config.infiniteHelminthMaterials) { + inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == ingredient.ItemType)!.Count -= + ingredient.ItemCount; + } } } @@ -63,95 +72,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/addCurrencyController.ts b/src/controllers/custom/addCurrencyController.ts index 63dedb6f..be14b8a3 100644 --- a/src/controllers/custom/addCurrencyController.ts +++ b/src/controllers/custom/addCurrencyController.ts @@ -1,12 +1,16 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getInventory } from "@/src/services/inventoryService"; +import { addFusionPoints, getInventory } from "@/src/services/inventoryService"; export const addCurrencyController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId); const request = req.body as IAddCurrencyRequest; - inventory[request.currency] += request.delta; + const inventory = await getInventory(accountId, request.currency); + if (request.currency == "FusionPoints") { + addFusionPoints(inventory, request.delta); + } else { + inventory[request.currency] += request.delta; + } await inventory.save(); res.end(); }; diff --git a/src/controllers/custom/addItemsController.ts b/src/controllers/custom/addItemsController.ts index 1eb50ed6..dc39ef64 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, undefined, undefined, true); } await inventory.save(); res.end(); diff --git a/src/controllers/custom/addMissingMaxRankModsController.ts b/src/controllers/custom/addMissingMaxRankModsController.ts new file mode 100644 index 00000000..99cd09ec --- /dev/null +++ b/src/controllers/custom/addMissingMaxRankModsController.ts @@ -0,0 +1,44 @@ +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; +import { ExportArcanes, ExportUpgrades } from "warframe-public-export-plus"; + +export const addMissingMaxRankModsController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "Upgrades"); + + const maxOwnedRanks: Record = {}; + for (const upgrade of inventory.Upgrades) { + const fingerprint = JSON.parse(upgrade.UpgradeFingerprint ?? "{}") as { lvl?: number }; + if (fingerprint.lvl) { + maxOwnedRanks[upgrade.ItemType] ??= 0; + if (fingerprint.lvl > maxOwnedRanks[upgrade.ItemType]) { + maxOwnedRanks[upgrade.ItemType] = fingerprint.lvl; + } + } + } + + for (const [uniqueName, data] of Object.entries(ExportUpgrades)) { + if (data.fusionLimit != 0 && data.type != "PARAZON" && maxOwnedRanks[uniqueName] != data.fusionLimit) { + inventory.Upgrades.push({ + ItemType: uniqueName, + UpgradeFingerprint: JSON.stringify({ lvl: data.fusionLimit }) + }); + } + } + + for (const [uniqueName, data] of Object.entries(ExportArcanes)) { + if ( + data.name != "/Lotus/Language/Items/GenericCosmeticEnhancerName" && + maxOwnedRanks[uniqueName] != data.fusionLimit + ) { + inventory.Upgrades.push({ + ItemType: uniqueName, + UpgradeFingerprint: JSON.stringify({ lvl: data.fusionLimit }) + }); + } + } + + await inventory.save(); + res.end(); +}; diff --git a/src/controllers/custom/addXpController.ts b/src/controllers/custom/addXpController.ts index 7cb284fe..7e42deb3 100644 --- a/src/controllers/custom/addXpController.ts +++ b/src/controllers/custom/addXpController.ts @@ -1,5 +1,6 @@ -import { addGearExpByCategory, getInventory } from "@/src/services/inventoryService"; +import { applyClientEquipmentUpdates, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IOid } from "@/src/types/commonTypes"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { RequestHandler } from "express"; @@ -11,7 +12,7 @@ export const addXpController: RequestHandler = async (req, res) => { const request = req.body as IAddXpRequest; for (const [category, gear] of Object.entries(request)) { for (const clientItem of gear) { - const dbItem = inventory[category as TEquipmentKey].id(clientItem.ItemId.$oid); + const dbItem = inventory[category as TEquipmentKey].id((clientItem.ItemId as IOid).$oid); if (dbItem) { if (dbItem.ItemType in ExportMisc.uniqueLevelCaps) { if ((dbItem.Polarized ?? 0) < 5) { @@ -20,7 +21,7 @@ export const addXpController: RequestHandler = async (req, res) => { } } } - addGearExpByCategory(inventory, gear, category as TEquipmentKey); + applyClientEquipmentUpdates(inventory, gear, category as TEquipmentKey); } await inventory.save(); res.end(); diff --git a/src/controllers/custom/deleteAccountController.ts b/src/controllers/custom/deleteAccountController.ts index fb8ca399..449a44c4 100644 --- a/src/controllers/custom/deleteAccountController.ts +++ b/src/controllers/custom/deleteAccountController.ts @@ -1,22 +1,39 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { Account } from "@/src/models/loginModel"; +import { Account, Ignore } from "@/src/models/loginModel"; import { Inbox } from "@/src/models/inboxModel"; import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; 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"; +import { Friendship } from "@/src/models/friendModel"; 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 }), + Friendship.deleteMany({ owner: accountId }), + Friendship.deleteMany({ friend: accountId }), + GuildMember.deleteMany({ accountId: accountId }), + Ignore.deleteMany({ ignorer: accountId }), + Ignore.deleteMany({ ignoree: 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..668aebf4 --- /dev/null +++ b/src/controllers/custom/getAccountInfoController.ts @@ -0,0 +1,44 @@ +import { AllianceMember, Guild, GuildMember } from "@/src/models/guildModel"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountForRequest, isAdministrator } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const getAccountInfoController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + const inventory = await getInventory(account._id.toString(), "QuestKeys"); + const info: IAccountInfo = { + DisplayName: account.DisplayName, + IsAdministrator: isAdministrator(account), + CompletedVorsPrize: !!inventory.QuestKeys.find( + x => x.ItemType == "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain" + )?.Completed + }; + 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; + CompletedVorsPrize: 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 38add971..716104a5 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -3,47 +3,99 @@ import { getDict, getItemName, getString } from "@/src/services/itemDataService" import { ExportArcanes, ExportAvionics, + ExportCustoms, + 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"; +import allIncarnons from "@/static/fixed_responses/allIncarnonList.json"; interface ListedItem { uniqueName: string; name: string; fusionLimit?: number; exalted?: string[]; + badReason?: "starter" | "frivolous" | "notraw"; + partType?: string; + chainLength?: number; + parazon?: boolean; } +interface ItemLists { + archonCrystalUpgrades: Record; + uniqueLevelCaps: Record; + Suits: ListedItem[]; + LongGuns: ListedItem[]; + Melee: ListedItem[]; + ModularParts: ListedItem[]; + Pistols: ListedItem[]; + Sentinels: ListedItem[]; + SentinelWeapons: ListedItem[]; + SpaceGuns: ListedItem[]; + SpaceMelee: ListedItem[]; + SpaceSuits: ListedItem[]; + MechSuits: ListedItem[]; + miscitems: ListedItem[]; + Syndicates: ListedItem[]; + OperatorAmps: ListedItem[]; + QuestKeys: ListedItem[]; + KubrowPets: ListedItem[]; + EvolutionProgress: ListedItem[]; + mods: ListedItem[]; +} + +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 = {}; - res.Suits = []; - res.LongGuns = []; - res.Melee = []; - res.ModularParts = []; - res.Pistols = []; - res.Sentinels = []; - res.SentinelWeapons = []; - res.SpaceGuns = []; - res.SpaceMelee = []; - res.SpaceSuits = []; - res.MechSuits = []; - res.miscitems = []; - res.Syndicates = []; + const res: ItemLists = { + archonCrystalUpgrades, + uniqueLevelCaps: ExportMisc.uniqueLevelCaps, + Suits: [], + LongGuns: [], + Melee: [], + ModularParts: [], + Pistols: [], + Sentinels: [], + SentinelWeapons: [], + SpaceGuns: [], + SpaceMelee: [], + SpaceSuits: [], + MechSuits: [], + miscitems: [], + Syndicates: [], + OperatorAmps: [], + QuestKeys: [], + KubrowPets: [], + EvolutionProgress: [], + mods: [] + }; for (const [uniqueName, item] of Object.entries(ExportWarframes)) { - if ( - item.productCategory == "Suits" || - item.productCategory == "SpaceSuits" || - item.productCategory == "MechSuits" - ) { + res[item.productCategory].push({ + uniqueName, + name: getString(item.name, lang), + exalted: item.exalted + }); + } + for (const [uniqueName, item] of Object.entries(ExportSentinels)) { + if (item.productCategory == "Sentinels" || item.productCategory == "KubrowPets") { res[item.productCategory].push({ uniqueName, name: getString(item.name, lang), @@ -51,29 +103,18 @@ const getItemListsController: RequestHandler = (req, response) => { }); } } - for (const [uniqueName, item] of Object.entries(ExportSentinels)) { - if (item.productCategory == "Sentinels") { - res[item.productCategory].push({ - uniqueName, - name: getString(item.name, lang) - }); - } - } 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" - ) { - res.ModularParts.push({ - uniqueName, - name: getString(item.name, lang) - }); + if (item.partType) { + if (!uniqueName.startsWith("/Lotus/Types/Items/Deimos/")) { + res.ModularParts.push({ + uniqueName, + 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) }); } @@ -84,7 +125,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, @@ -93,7 +135,7 @@ const getItemListsController: RequestHandler = (req, response) => { } } else if (!item.excludeFromCodex) { res.miscitems.push({ - uniqueName: "MiscItems:" + uniqueName, + uniqueName: uniqueName, name: getString(item.name, lang) }); } @@ -102,22 +144,55 @@ const getItemListsController: RequestHandler = (req, response) => { let name = getString(item.name, lang); if ("dissectionParts" in item) { name = getString("/Lotus/Language/Fish/FishDisplayName", lang).split("|FISH_NAME|").join(name); - if (uniqueName.indexOf("Large") != -1) { - name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeLargeAbbrev", lang)); - } else if (uniqueName.indexOf("Medium") != -1) { - name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeMediumAbbrev", lang)); + if (item.syndicateTag == "CetusSyndicate") { + if (uniqueName.indexOf("Large") != -1) { + name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeLargeAbbrev", lang)); + } else if (uniqueName.indexOf("Medium") != -1) { + name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeMediumAbbrev", lang)); + } else { + name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeSmallAbbrev", lang)); + } } else { - name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeSmallAbbrev", lang)); + if (uniqueName.indexOf("Large") != -1) { + name = name + .split("|FISH_SIZE|") + .join(getString("/Lotus/Language/SolarisVenus/RobofishAgeCategoryElderAbbrev", lang)); + } else if (uniqueName.indexOf("Medium") != -1) { + name = name + .split("|FISH_SIZE|") + .join(getString("/Lotus/Language/SolarisVenus/RobofishAgeCategoryMatureAbbrev", lang)); + } else { + name = name + .split("|FISH_SIZE|") + .join(getString("/Lotus/Language/SolarisVenus/RobofishAgeCategoryYoungAbbrev", 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) }); } @@ -126,25 +201,51 @@ const getItemListsController: RequestHandler = (req, response) => { if (!item.hidden) { const resultName = getItemName(item.resultType); if (resultName) { + let itemName = getString(resultName, lang); + if (item.num > 1) itemName = `${itemName} X ${item.num}`; res.miscitems.push({ - uniqueName: "Recipes:" + uniqueName, - name: recipeNameTemplate.replace("|ITEM|", getString(resultName, lang)) + uniqueName: uniqueName, + name: recipeNameTemplate.replace("|ITEM|", itemName) }); } } } + 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) + }); + } + for (const [uniqueName, item] of Object.entries(ExportCustoms)) { + 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"; } + if (upgrade.type == "PARAZON") { + mod.parazon = true; + } + res.mods.push(mod); } for (const [uniqueName, upgrade] of Object.entries(ExportAvionics)) { res.mods.push({ @@ -154,12 +255,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)) { @@ -168,13 +272,28 @@ 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 + }); + } else if (key.name) { + res.miscitems.push({ + uniqueName, + name: getString(key.name, lang) + }); + } + } + for (const uniqueName of allIncarnons) { + res.EvolutionProgress.push({ + uniqueName, + name: getString(getItemName(uniqueName) || "", lang) + }); + } - response.json({ - badItems, - archonCrystalUpgrades, - uniqueLevelCaps: ExportMisc.uniqueLevelCaps, - ...res - }); + response.json(res); }; export { getItemListsController }; 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/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 504b4951..b951c754 100644 --- a/src/controllers/custom/manageQuestsController.ts +++ b/src/controllers/custom/manageQuestsController.ts @@ -1,8 +1,11 @@ -import { addString } from "@/src/controllers/api/inventoryController"; import { getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { addQuestKey, IUpdateQuestRequest, updateQuestKey } from "@/src/services/questService"; -import { IQuestStage } from "@/src/types/inventoryTypes/inventoryTypes"; +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"; @@ -10,12 +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"; - 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)) { @@ -23,56 +31,128 @@ export const manageQuestsController: RequestHandler = async (req, res) => { allQuestKeys.push(k); } } - const inventory = await getInventory(accountId, "QuestKeys NodeIntrosCompleted"); + 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) { - const chainStageTotal = ExportKeys[questKey].chainStages?.length ?? 0; - const Progress = Array(chainStageTotal).fill({ c: 0, i: true, m: true, b: [] } satisfies IQuestStage); - const inventoryQuestKey = inventory.QuestKeys.find(qk => qk.ItemType === questKey); - if (inventoryQuestKey) { - inventoryQuestKey.Completed = true; - inventoryQuestKey.Progress = Progress; - continue; - } - addQuestKey(inventory, { ItemType: questKey, Completed: true, unlock: true, Progress: Progress }); + for (const questKey of inventory.QuestKeys) { + await completeQuest(inventory, questKey.ItemType); } - inventory.ArchwingEnabled = true; - inventory.ActiveQuest = ""; - - // Skip "Watch The Maker" - addString(inventory.NodeIntrosCompleted, "/Lotus/Levels/Cinematics/NewWarIntro/NewWarStageTwo.level"); break; } - case "ResetAll": { - logger.info("resetting all quests.."); + case "resetAll": { for (const questKey of inventory.QuestKeys) { questKey.Completed = false; questKey.Progress = []; + questKey.CompletionDate = undefined; + } + inventory.ActiveQuest = ""; + break; + } + case "giveAll": { + allQuestKeys.forEach(questKey => addQuestKey(inventory, { ItemType: questKey })); + break; + } + case "deleteKey": { + 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 "completeAllUnlocked": { - logger.info("completing all unlocked quests.."); - for (const questKey of inventory.QuestKeys) { - //if (!questKey.unlock) { continue; } - questKey.Completed = true; + 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); + } 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 + }); + } + } + } + break; + } + 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/setEvolutionProgressController.ts b/src/controllers/custom/setEvolutionProgressController.ts new file mode 100644 index 00000000..45c7c68b --- /dev/null +++ b/src/controllers/custom/setEvolutionProgressController.ts @@ -0,0 +1,33 @@ +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const setEvolutionProgressController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const payload = req.body as ISetEvolutionProgressRequest; + + inventory.EvolutionProgress ??= []; + payload.forEach(element => { + const entry = inventory.EvolutionProgress!.find(entry => entry.ItemType === element.ItemType); + + if (entry) { + entry.Progress = 0; + entry.Rank = element.Rank; + } else { + inventory.EvolutionProgress!.push({ + Progress: 0, + Rank: element.Rank, + ItemType: element.ItemType + }); + } + }); + + await inventory.save(); + res.end(); +}; + +type ISetEvolutionProgressRequest = { + ItemType: string; + Rank: number; +}[]; 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..bc12c6d6 --- /dev/null +++ b/src/controllers/dynamic/getProfileViewingDataController.ts @@ -0,0 +1,341 @@ +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, getJSONfromString } from "@/src/helpers/stringHelpers"; +import { ExportCustoms, ExportDojoRecipes } from "warframe-public-export-plus"; +import { IStatsClient } from "@/src/types/statTypes"; +import { toStoreItem } from "@/src/services/itemDataService"; +import { FlattenMaps } from "mongoose"; + +const getProfileViewingDataByPlayerIdImpl = async (playerId: string): Promise => { + const account = await Account.findById(playerId, "DisplayName"); + if (!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; + + return { + Results: [result], + TechProjects: [], + XpComponents: [], + //XpCacheExpiryDate, some IMongoDate in the future, no clue what it's for + Stats: stats + }; +}; + +export const getProfileViewingDataGetController: RequestHandler = async (req, res) => { + if (req.query.playerId) { + const data = await getProfileViewingDataByPlayerIdImpl(req.query.playerId as string); + if (data) { + res.json(data); + } else { + res.status(409).send("Could not find requested account"); + } + } else if (req.query.guildId) { + const guild = await Guild.findById( + req.query.guildId as string, + "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); + } +}; + +// For old versions, this was an authenticated POST request. +interface IGetProfileViewingDataRequest { + AccountId: string; +} +export const getProfileViewingDataPostController: RequestHandler = async (req, res) => { + const payload = getJSONfromString(String(req.body)); + const data = await getProfileViewingDataByPlayerIdImpl(payload.AccountId); + if (data) { + res.json(data); + } else { + res.status(409).send("Could not find requested account"); + } +}; + +interface IProfileViewingData { + Results: IPlayerProfileViewingDataResult[]; + TechProjects: []; + XpComponents: []; + //XpCacheExpiryDate, some IMongoDate in the future, no clue what it's for + Stats: FlattenMaps>; +} + +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..89335497 100644 --- a/src/controllers/dynamic/worldStateController.ts +++ b/src/controllers/dynamic/worldStateController.ts @@ -1,180 +1,6 @@ import { RequestHandler } from "express"; -import staticWorldState from "@/static/fixed_responses/worldState/worldState.json"; -import static1999FallDays from "@/static/fixed_responses/worldState/1999_fall_days.json"; -import static1999SpringDays from "@/static/fixed_responses/worldState/1999_spring_days.json"; -import static1999SummerDays from "@/static/fixed_responses/worldState/1999_summer_days.json"; -import static1999WinterDays from "@/static/fixed_responses/worldState/1999_winter_days.json"; -import { buildConfig } from "@/src/services/buildConfigService"; -import { IMongoDate, IOid } from "@/src/types/commonTypes"; -import { unixTimesInMs } from "@/src/constants/timeConstants"; +import { getWorldState } from "@/src/services/worldStateService"; export const worldStateController: RequestHandler = (req, res) => { - const worldState: IWorldState = { - BuildLabel: - typeof req.query.buildLabel == "string" - ? req.query.buildLabel.split(" ").join("+") - : buildConfig.buildLabel, - Time: Math.round(Date.now() / 1000), - EndlessXpChoices: [], - ...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; - - // 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 bountyCycleStart = bountyCycle * 9000000; - const bountyCycleEnd = bountyCycleStart + 9000000; - worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "ZarimanSyndicate")] = { - _id: { $oid: bountyCycleStart.toString(16) + "0000000000000029" }, - Activation: { $date: { $numberLong: bountyCycleStart.toString() } }, - Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } }, - Tag: "ZarimanSyndicate", - Seed: bountyCycle, - Nodes: [] - }; - worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "EntratiLabSyndicate")] = { - _id: { $oid: bountyCycleStart.toString(16) + "0000000000000004" }, - Activation: { $date: { $numberLong: bountyCycleStart.toString() } }, - Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } }, - Tag: "EntratiLabSyndicate", - Seed: bountyCycle, - Nodes: [] - }; - worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "HexSyndicate")] = { - _id: { $oid: bountyCycleStart.toString(16) + "0000000000000006" }, - Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, - Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, - Tag: "HexSyndicate", - Seed: bountyCycle, - Nodes: [] - }; - - // Circuit choices cycling every week - worldState.EndlessXpChoices.push({ - Category: "EXC_NORMAL", - Choices: [ - ["Nidus", "Octavia", "Harrow"], - ["Gara", "Khora", "Revenant"], - ["Garuda", "Baruuk", "Hildryn"], - ["Excalibur", "Trinity", "Ember"], - ["Loki", "Mag", "Rhino"], - ["Ash", "Frost", "Nyx"], - ["Saryn", "Vauban", "Nova"], - ["Nekros", "Valkyr", "Oberon"], - ["Hydroid", "Mirage", "Limbo"], - ["Mesa", "Chroma", "Atlas"], - ["Ivara", "Inaros", "Titania"] - ][week % 12] - }); - worldState.EndlessXpChoices.push({ - Category: "EXC_HARD", - Choices: [ - ["Boar", "Gammacor", "Angstrum", "Gorgon", "Anku"], - ["Bo", "Latron", "Furis", "Furax", "Strun"], - ["Lex", "Magistar", "Boltor", "Bronco", "CeramicDagger"], - ["Torid", "DualToxocyst", "DualIchor", "Miter", "Atomos"], - ["AckAndBrunt", "Soma", "Vasto", "NamiSolo", "Burston"], - ["Zylok", "Sibear", "Dread", "Despair", "Hate"], - ["Dera", "Sybaris", "Cestra", "Sicarus", "Okina"], - ["Braton", "Lato", "Skana", "Paris", "Kunai"] - ][week % 8] - }); - - // 1999 Calendar Season cycling every week + YearIteration every 4 weeks - worldState.KnownCalendarSeasons[0].Activation = { $date: { $numberLong: weekStart.toString() } }; - worldState.KnownCalendarSeasons[0].Expiry = { $date: { $numberLong: weekEnd.toString() } }; - worldState.KnownCalendarSeasons[0].Season = ["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"][week % 4]; - worldState.KnownCalendarSeasons[0].Days = [ - static1999WinterDays, - static1999SpringDays, - static1999SummerDays, - static1999FallDays - ][week % 4]; - 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 tmp = { - cavabegin: "1690761600", - PurchasePlatformLockEnabled: true, - tcsn: true, - pgr: { - ts: "1732572900", - en: "CUSTOM DECALS @ ZEVILA", - fr: "DECALS CUSTOM @ ZEVILA", - it: "DECALCOMANIE PERSONALIZZATE @ ZEVILA", - de: "AUFKLEBER NACH WUNSCH @ ZEVILA", - es: "CALCOMANÍAS PERSONALIZADAS @ ZEVILA", - pt: "DECALQUES PERSONALIZADOS NA ZEVILA", - ru: "ПОЛЬЗОВАТЕЛЬСКИЕ НАКЛЕЙКИ @ ЗеВиЛа", - pl: "NOWE NAKLEJKI @ ZEVILA", - uk: "КОРИСТУВАЦЬКІ ДЕКОЛІ @ ЗІВІЛА", - tr: "ÖZEL ÇIKARTMALAR @ ZEVILA", - ja: "カスタムデカール @ ゼビラ", - zh: "定制贴花认准泽威拉", - ko: "커스텀 데칼 @ ZEVILA", - tc: "自訂貼花 @ ZEVILA", - th: "รูปลอกสั่งทำที่ ZEVILA" - }, - ennnd: true, - mbrt: true, - sfn: [550, 553, 554, 555][halfHour % 4] - }; - worldState.Tmp = JSON.stringify(tmp); - - res.json(worldState); + res.json(getWorldState(req.query.buildLabel as string | undefined)); }; - -interface IWorldState { - BuildLabel: string; - Time: number; - SyndicateMissions: ISyndicateMission[]; - NodeOverrides: INodeOverride[]; - EndlessXpChoices: IEndlessXpChoice[]; - KnownCalendarSeasons: ICalendarSeason[]; - Tmp?: string; -} - -interface ISyndicateMission { - _id: IOid; - Activation: IMongoDate; - Expiry: IMongoDate; - Tag: string; - Seed: number; - Nodes: string[]; -} - -interface INodeOverride { - _id: IOid; - Activation?: IMongoDate; - Expiry?: IMongoDate; - Node: string; - Hide?: boolean; - Seed?: number; - LevelOverride?: string; - Faction?: string; - CustomNpcEncounters?: string; -} - -interface IEndlessXpChoice { - Category: string; - Choices: string[]; -} - -interface ICalendarSeason { - Activation: IMongoDate; - Expiry: IMongoDate; - Season: string; // "CST_UNDEFINED" | "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL" - Days: { - day: number; - }[]; - YearIteration: number; -} 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..3ff5a848 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 }); } @@ -27,7 +27,15 @@ const viewController: RequestHandler = async (req, res) => { for (const type of Object.keys(ExportEnemies.avatars)) { if (!scans.has(type)) scans.add(type); } - responseJson.Scans ??= []; + + // Take any existing scans and also set them to 9999 + if (responseJson.Scans) { + for (const scan of responseJson.Scans) { + scans.add(scan.type); + } + } + responseJson.Scans = []; + for (const type of scans) { responseJson.Scans.push({ type: type, scans: 9999 }); } diff --git a/src/helpers/customHelpers/customHelpers.ts b/src/helpers/customHelpers/customHelpers.ts index e1173d1f..02defc8c 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: "", @@ -49,8 +49,8 @@ const toDatabaseAccount = (createAccount: IAccountCreation): IDatabaseAccount => ForceLogoutVersion: 0, TrackedSettings: [], Nonce: 0, - LatestEventMessageDate: new Date(0) - } satisfies IDatabaseAccount; + LastLogin: new Date() + } satisfies IDatabaseAccountRequiredFields; }; export { toDatabaseAccount, toAccountCreation as toCreateAccount }; diff --git a/src/helpers/inventoryHelpers.ts b/src/helpers/inventoryHelpers.ts index d7fa7b25..6ced29e9 100644 --- a/src/helpers/inventoryHelpers.ts +++ b/src/helpers/inventoryHelpers.ts @@ -1,10 +1,193 @@ -import { IMongoDate, IOid } from "@/src/types/commonTypes"; +import { IMongoDate, IOid, IOidWithLegacySupport } from "@/src/types/commonTypes"; import { Types } from "mongoose"; +import { TRarity } from "warframe-public-export-plus"; + +export const version_compare = (a: string, b: string): number => { + const a_digits = a + .split("/")[0] + .split(".") + .map(x => parseInt(x)); + const b_digits = b + .split("/")[0] + .split(".") + .map(x => parseInt(x)); + for (let i = 0; i != a_digits.length; ++i) { + if (a_digits[i] != b_digits[i]) { + return a_digits[i] > b_digits[i] ? 1 : -1; + } + } + return 0; +}; export const toOid = (objectId: Types.ObjectId): IOid => { - return { $oid: objectId.toString() } satisfies IOid; + return { $oid: objectId.toString() }; +}; + +export function toOid2(objectId: Types.ObjectId, buildLabel: undefined): IOid; +export function toOid2(objectId: Types.ObjectId, buildLabel: string | undefined): IOidWithLegacySupport; +export function toOid2(objectId: Types.ObjectId, buildLabel: string | undefined): IOidWithLegacySupport { + if (buildLabel && version_compare(buildLabel, "2016.12.21.19.13") <= 0) { + return { $id: objectId.toString() }; + } + return { $oid: objectId.toString() }; +} + +export const toLegacyOid = (oid: IOidWithLegacySupport): void => { + if (!("$id" in oid)) { + oid.$id = oid.$oid; + delete oid.$oid; + } +}; + +export const fromOid = (oid: IOidWithLegacySupport): string => { + return (oid.$oid ?? oid.$id)!; }; export const toMongoDate = (date: Date): IMongoDate => { return { $date: { $numberLong: date.getTime().toString() } }; }; + +export const fromMongoDate = (date: IMongoDate): Date => { + return new Date(parseInt(date.$date.$numberLong)); +}; + +export const kubrowWeights: Record = { + COMMON: 6, + UNCOMMON: 4, + RARE: 2, + LEGENDARY: 1 +}; + +export const kubrowFurPatternsWeights: Record = { + COMMON: 6, + UNCOMMON: 5, + RARE: 2, + LEGENDARY: 1 +}; + +export const catbrowDetails = { + Colors: [ + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseA", rarity: "COMMON" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseB", rarity: "COMMON" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseC", rarity: "COMMON" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseD", rarity: "COMMON" as TRarity }, + + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryA", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryB", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryC", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryD", rarity: "UNCOMMON" as TRarity }, + + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryA", rarity: "RARE" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryB", rarity: "RARE" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryC", rarity: "RARE" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryD", rarity: "RARE" as TRarity }, + + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsA", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsB", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsC", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsD", rarity: "LEGENDARY" as TRarity } + ], + + EyeColors: [ + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesA", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesB", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesC", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesD", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesE", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesF", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesG", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesH", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesI", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesJ", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesK", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesL", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesM", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesN", rarity: "LEGENDARY" as TRarity } + ], + + FurPatterns: [{ type: "/Lotus/Types/Game/CatbrowPet/Patterns/CatbrowPetPatternA", rarity: "COMMON" as TRarity }], + + BodyTypes: [ + { type: "/Lotus/Types/Game/CatbrowPet/BodyTypes/CatbrowPetRegularBodyType", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/BodyTypes/CatbrowPetRegularBodyType", rarity: "LEGENDARY" as TRarity } + ], + + Heads: [ + { type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadA", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadB", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadC", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadD", rarity: "LEGENDARY" as TRarity } + ], + + Tails: [ + { type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailA", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailB", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailC", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailD", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailE", rarity: "LEGENDARY" as TRarity } + ] +}; + +export const kubrowDetails = { + Colors: [ + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneA", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneB", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneC", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneD", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneE", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneF", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneG", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneH", rarity: "UNCOMMON" as TRarity }, + + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidA", rarity: "RARE" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidB", rarity: "RARE" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidC", rarity: "RARE" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidD", rarity: "RARE" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidE", rarity: "RARE" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidF", rarity: "RARE" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidG", rarity: "RARE" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidH", rarity: "RARE" as TRarity }, + + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantA", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantB", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantC", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantD", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantE", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantF", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantG", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantH", rarity: "LEGENDARY" as TRarity } + ], + + EyeColors: [ + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesA", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesB", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesC", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesD", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesE", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesF", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesG", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesH", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesI", rarity: "LEGENDARY" as TRarity } + ], + + FurPatterns: [ + { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternB", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternA", rarity: "UNCOMMON" as TRarity }, + + { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternC", rarity: "RARE" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternD", rarity: "RARE" as TRarity }, + + { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternE", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternF", rarity: "LEGENDARY" as TRarity } + ], + + BodyTypes: [ + { type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetRegularBodyType", rarity: "UNCOMMON" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetHeavyBodyType", rarity: "LEGENDARY" as TRarity }, + { type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetThinBodyType", rarity: "LEGENDARY" as TRarity } + ], + + Heads: [], + + Tails: [] +}; 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..9d6a7a2b --- /dev/null +++ b/src/helpers/nemesisHelpers.ts @@ -0,0 +1,491 @@ +import { ExportRegions, ExportWarframes } from "warframe-public-export-plus"; +import { IInfNode, TNemesisFaction } from "@/src/types/inventoryTypes/inventoryTypes"; +import { getRewardAtPercentage, SRng } from "@/src/services/rngService"; +import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel"; +import { logger } from "../utils/logger"; +import { IOid } from "../types/commonTypes"; +import { Types } from "mongoose"; +import { addMods, generateRewardSeed } from "../services/inventoryService"; +import { isArchwingMission } from "../services/worldStateService"; + +type TInnateDamageTag = + | "InnateElectricityDamage" + | "InnateHeatDamage" + | "InnateFreezeDamage" + | "InnateToxinDamage" + | "InnateMagDamage" + | "InnateRadDamage" + | "InnateImpactDamage"; + +export interface INemesisManifest { + weapons: readonly string[]; + systemIndexes: readonly number[]; + showdownNode: string; + ephemeraChance: number; + ephemeraTypes?: Record; + firstKillReward: string; + firstConvertReward: string; + messageTitle: string; + messageBody: string; + minBuild: string; +} + +class KuvaLichManifest implements INemesisManifest { + weapons = [ + "/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" + ]; + systemIndexes = [2, 3, 9, 11, 18]; + showdownNode = "CrewBattleNode557"; + ephemeraChance = 0.05; + ephemeraTypes = { + InnateElectricityDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaLightningEphemera", + InnateHeatDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaFireEphemera", + InnateFreezeDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaIceEphemera", + InnateToxinDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaToxinEphemera", + InnateMagDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaMagneticEphemera", + InnateRadDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaTricksterEphemera", + InnateImpactDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaImpactEphemera" + }; + firstKillReward = "/Lotus/StoreItems/Upgrades/Skins/Clan/LichKillerBadgeItem"; + firstConvertReward = "/Lotus/StoreItems/Upgrades/Skins/Sigils/KuvaLichSigil"; + messageTitle = "/Lotus/Language/Inbox/VanquishKuvaMsgTitle"; + messageBody = "/Lotus/Language/Inbox/VanquishLichMsgBody"; + minBuild = "2019.10.31.22.42"; // 26.0.0 +} + +class KuvaLichManifestVersionTwo extends KuvaLichManifest { + constructor() { + super(); + this.ephemeraChance = 0.1; + this.minBuild = "2020.03.05.16.06"; // Unsure about this one, so using the same value as in version three. + } +} + +class KuvaLichManifestVersionThree extends KuvaLichManifestVersionTwo { + constructor() { + super(); + this.weapons.push("/Lotus/Weapons/Grineer/Bows/GrnBow/GrnBowWeapon"); + this.weapons.push("/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hind/KuvaHind"); + this.weapons.push("/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Nukor/KuvaNukor"); + this.ephemeraChance = 0.2; + this.minBuild = "2020.03.05.16.06"; // This is 27.2.0, tho 27.1.0 should also recognise this. + } +} + +class KuvaLichManifestVersionFour extends KuvaLichManifestVersionThree { + constructor() { + super(); + this.minBuild = "2021.07.05.17.03"; // Unsure about this one, so using the same value as in version five. + } +} + +class KuvaLichManifestVersionFive extends KuvaLichManifestVersionFour { + constructor() { + super(); + this.weapons.push("/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hek/KuvaHekWeapon"); + this.weapons.push("/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Zarr/KuvaZarr"); + this.weapons.push("/Lotus/Weapons/Grineer/KuvaLich/HeavyWeapons/Grattler/KuvaGrattler"); + this.minBuild = "2021.07.05.17.03"; // 30.5.0 + } +} + +class KuvaLichManifestVersionSix extends KuvaLichManifestVersionFive { + constructor() { + super(); + this.weapons.push("/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Sobek/KuvaSobek"); + this.minBuild = "2024.05.15.11.07"; // 35.6.0 + } +} + +class LawyerManifest implements INemesisManifest { + weapons = [ + "/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" + ]; + systemIndexes = [1, 15, 4, 7, 8]; + showdownNode = "CrewBattleNode558"; + ephemeraChance = 0.2; + ephemeraTypes = { + InnateElectricityDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraA", + InnateHeatDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraB", + InnateFreezeDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraC", + InnateToxinDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraD", + InnateMagDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraE", + InnateRadDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraF", + InnateImpactDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraG" + }; + firstKillReward = "/Lotus/StoreItems/Upgrades/Skins/Clan/CorpusLichBadgeItem"; + firstConvertReward = "/Lotus/StoreItems/Upgrades/Skins/Sigils/CorpusLichSigil"; + messageTitle = "/Lotus/Language/Inbox/VanquishLawyerMsgTitle"; + messageBody = "/Lotus/Language/Inbox/VanquishLichMsgBody"; + minBuild = "2021.07.05.17.03"; // 30.5.0 +} + +class LawyerManifestVersionTwo extends LawyerManifest { + constructor() { + super(); + this.weapons.push("/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEPlinx/CrpBEPlinxWeapon"); + this.minBuild = "2022.11.30.08.13"; // 32.2.0 + } +} + +class LawyerManifestVersionThree extends LawyerManifestVersionTwo { + constructor() { + super(); + this.weapons.push("/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEGlaxion/CrpBEGlaxion"); + this.minBuild = "2024.05.15.11.07"; // 35.6.0 + } +} + +class LawyerManifestVersionFour extends LawyerManifestVersionThree { + constructor() { + super(); + this.minBuild = "2024.10.01.11.03"; // 37.0.0 + } +} + +class InfestedLichManfest implements INemesisManifest { + weapons = []; + systemIndexes = [23]; + showdownNode = "CrewBattleNode559"; + ephemeraChance = 0; + firstKillReward = "/Lotus/StoreItems/Upgrades/Skins/Sigils/InfLichVanquishedSigil"; + firstConvertReward = "/Lotus/StoreItems/Upgrades/Skins/Sigils/InfLichConvertedSigil"; + messageTitle = "/Lotus/Language/Inbox/VanquishBandMsgTitle"; + messageBody = "/Lotus/Language/Inbox/VanquishBandMsgBody"; + minBuild = "2025.03.18.09.51"; // 38.5.0 +} + +const nemesisManifests: Record = { + "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifest": new KuvaLichManifest(), + "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionTwo": new KuvaLichManifestVersionTwo(), + "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionThree": new KuvaLichManifestVersionThree(), + "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionFour": new KuvaLichManifestVersionFour(), + "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionFive": new KuvaLichManifestVersionFive(), + "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix": new KuvaLichManifestVersionSix(), + "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifest": new LawyerManifest(), + "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionTwo": new LawyerManifestVersionTwo(), + "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree": new LawyerManifestVersionThree(), + "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour": new LawyerManifestVersionFour(), + "/Lotus/Types/Enemies/InfestedLich/InfestedLichManifest": new InfestedLichManfest() +}; + +export const getNemesisManifest = (manifest: string): INemesisManifest => { + if (manifest in nemesisManifests) { + return nemesisManifests[manifest]; + } + throw new Error(`unknown nemesis manifest: ${manifest}`); +}; + +export const getInfNodes = (manifest: INemesisManifest, rank: number): IInfNode[] => { + const infNodes = []; + const systemIndex = manifest.systemIndexes[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 + !isArchwingMission(value) + ) { + //console.log(dict_en[value.name]); + infNodes.push({ Node: key, Influence: 1 }); + } + } + return infNodes; +}; + +// Get a parazon 'passcode' based on the nemesis fingerprint so it's always the same for the same nemesis. +export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: TNemesisFaction }): number[] => { + const rng = new SRng(nemesis.fp); + const choices = [0, 1, 2, 3, 5, 6, 7]; + let choiceIndex = rng.randomInt(0, choices.length - 1); + const passcode = [choices[choiceIndex]]; + if (nemesis.Faction != "FC_INFESTATION") { + choices.splice(choiceIndex, 1); + choiceIndex = rng.randomInt(0, choices.length - 1); + passcode.push(choices[choiceIndex]); + + choices.splice(choiceIndex, 1); + choiceIndex = rng.randomInt(0, choices.length - 1); + passcode.push(choices[choiceIndex]); + } + return passcode; +}; + +const reqiuemMods: readonly string[] = [ + "/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalFourMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalFiveMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalSixMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalSevenMod", + "/Lotus/Upgrades/Mods/Immortal/ImmortalEightMod" +]; + +const antivirusMods: readonly string[] = [ + "/Lotus/Upgrades/Mods/Immortal/AntivirusOneMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusTwoMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusThreeMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusFourMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusFiveMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusSixMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusSevenMod", + "/Lotus/Upgrades/Mods/Immortal/AntivirusEightMod" +]; + +export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: TNemesisFaction }): string[] => { + const passcode = getNemesisPasscode(nemesis); + return nemesis.Faction == "FC_INFESTATION" + ? passcode.map(i => antivirusMods[i]) + : passcode.map(i => reqiuemMods[i]); +}; + +export const encodeNemesisGuess = ( + symbol1: number, + result1: number, + symbol2: number, + result2: number, + symbol3: number, + result3: number +): number => { + return ( + (symbol1 & 0xf) | + ((result1 & 3) << 12) | + ((symbol2 << 4) & 0xff) | + ((result2 << 14) & 0xffff) | + ((symbol3 & 0xf) << 8) | + ((result3 & 3) << 16) + ); +}; + +export const decodeNemesisGuess = (val: number): number[] => { + return [val & 0xf, (val >> 12) & 3, (val & 0xff) >> 4, (val & 0xffff) >> 14, (val >> 8) & 0xf, (val >> 16) & 3]; +}; + +export interface IKnifeResponse { + UpgradeIds?: string[]; + UpgradeTypes?: string[]; + UpgradeFingerprints?: { lvl: number }[]; + UpgradeNew?: boolean[]; + HasKnife?: boolean; +} + +export const getKnifeUpgrade = ( + inventory: TInventoryDatabaseDocument, + dataknifeUpgrades: string[], + type: string +): { ItemId: IOid; ItemType: string } => { + if (dataknifeUpgrades.indexOf(type) != -1) { + return { + ItemId: { $oid: "000000000000000000000000" }, + ItemType: type + }; + } + for (const upgradeId of dataknifeUpgrades) { + if (upgradeId.length == 24) { + const upgrade = inventory.Upgrades.id(upgradeId); + if (upgrade && upgrade.ItemType == type) { + return { + ItemId: { $oid: upgradeId }, + ItemType: type + }; + } + } + } + throw new Error(`${type} does not seem to be installed on parazon?!`); +}; + +export const consumeModCharge = ( + response: IKnifeResponse, + inventory: TInventoryDatabaseDocument, + upgrade: { ItemId: IOid; ItemType: string }, + dataknifeUpgrades: string[] +): void => { + response.UpgradeIds ??= []; + response.UpgradeTypes ??= []; + response.UpgradeFingerprints ??= []; + response.UpgradeNew ??= []; + response.HasKnife = true; + + if (upgrade.ItemId.$oid != "000000000000000000000000") { + const dbUpgrade = inventory.Upgrades.id(upgrade.ItemId.$oid)!; + const fingerprint = JSON.parse(dbUpgrade.UpgradeFingerprint!) as { lvl: number }; + fingerprint.lvl += 1; + dbUpgrade.UpgradeFingerprint = JSON.stringify(fingerprint); + + response.UpgradeIds.push(upgrade.ItemId.$oid); + response.UpgradeTypes.push(upgrade.ItemType); + response.UpgradeFingerprints.push(fingerprint); + response.UpgradeNew.push(false); + } else { + const id = new Types.ObjectId(); + inventory.Upgrades.push({ + _id: id, + ItemType: upgrade.ItemType, + UpgradeFingerprint: `{"lvl":1}` + }); + + addMods(inventory, [ + { + ItemType: upgrade.ItemType, + ItemCount: -1 + } + ]); + + const dataknifeRawUpgradeIndex = dataknifeUpgrades.indexOf(upgrade.ItemType); + if (dataknifeRawUpgradeIndex != -1) { + dataknifeUpgrades[dataknifeRawUpgradeIndex] = id.toString(); + } else { + logger.warn(`${upgrade.ItemType} not found in dataknife config`); + } + + response.UpgradeIds.push(id.toString()); + response.UpgradeTypes.push(upgrade.ItemType); + response.UpgradeFingerprints.push({ lvl: 1 }); + response.UpgradeNew.push(true); + } +}; + +export const getInnateDamageTag = (KillingSuit: string): TInnateDamageTag => { + return ExportWarframes[KillingSuit].nemesisUpgradeTag!; +}; + +const petHeads = [ + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC" +] as const; + +export interface INemesisProfile { + innateDamageTag: TInnateDamageTag; + innateDamageValue: number; + ephemera?: string; + petHead?: (typeof petHeads)[number]; + petBody?: string; + petLegs?: string; + petTail?: string; +} + +export const generateNemesisProfile = ( + fp: bigint = generateRewardSeed(), + manifest: INemesisManifest = new LawyerManifest(), + killingSuit: string = "/Lotus/Powersuits/Ember/Ember" +): INemesisProfile => { + const rng = new SRng(fp); + rng.randomFloat(); // used for the weapon index + const WeaponUpgradeValueAttenuationExponent = 2.25; + let value = Math.pow(rng.randomFloat(), WeaponUpgradeValueAttenuationExponent); + if (value >= 0.941428) { + value = 1; + } + const profile: INemesisProfile = { + innateDamageTag: getInnateDamageTag(killingSuit), + innateDamageValue: Math.trunc(value * 0x40000000) // TODO: For -1399275245665749231n, the value should be 75306944, but we're off by 59 with 75307003. + }; + if (rng.randomFloat() <= manifest.ephemeraChance && manifest.ephemeraTypes) { + profile.ephemera = manifest.ephemeraTypes[profile.innateDamageTag]; + } + rng.randomFloat(); // something related to sentinel agent maybe + if (manifest instanceof LawyerManifest) { + profile.petHead = rng.randomElement(petHeads)!; + profile.petBody = rng.randomElement([ + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyA", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyB", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyC" + ])!; + profile.petLegs = rng.randomElement([ + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsA", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsB", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsC" + ])!; + profile.petTail = rng.randomElement([ + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailA", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailB", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailC" + ])!; + } + return profile; +}; + +export const getKillTokenRewardCount = (fp: bigint): number => { + const rng = new SRng(fp); + return rng.randomInt(10, 15); +}; + +// /Lotus/Types/Enemies/InfestedLich/InfestedLichRewardManifest +const infestedLichRotA = [ + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyDJRomHuman", probability: 0.046 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyDJRomInfested", probability: 0.045 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyDrillbitHuman", probability: 0.046 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyDrillbitInfested", probability: 0.045 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyHarddriveHuman", probability: 0.046 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyHarddriveInfested", probability: 0.045 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyPacketHuman", probability: 0.046 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyPacketInfested", probability: 0.045 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyZekeHuman", probability: 0.046 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyZekeInfested", probability: 0.045 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandBillboardPosterA", probability: 0.045 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandBillboardPosterB", probability: 0.046 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandDespairPoster", probability: 0.045 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandGridPoster", probability: 0.046 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandHuddlePoster", probability: 0.045 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandJumpPoster", probability: 0.046 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandLimoPoster", probability: 0.045 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandLookingDownPosterDay", probability: 0.046 }, + { + type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandLookingDownPosterNight", + probability: 0.045 + }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandSillyPoster", probability: 0.046 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandWhiteBluePoster", probability: 0.045 }, + { type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandWhitePinkPoster", probability: 0.045 } +]; +const infestedLichRotB = [ + { type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraA", probability: 0.072 }, + { type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraB", probability: 0.071 }, + { type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraC", probability: 0.072 }, + { type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraD", probability: 0.071 }, + { type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraE", probability: 0.072 }, + { type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraF", probability: 0.071 }, + { type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraG", probability: 0.071 }, + { type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraH", probability: 0.072 }, + { type: "/Lotus/StoreItems/Types/Items/Emotes/DanceDJRomHype", probability: 0.071 }, + { type: "/Lotus/StoreItems/Types/Items/Emotes/DancePacketWindmillShuffle", probability: 0.072 }, + { type: "/Lotus/StoreItems/Types/Items/Emotes/DanceHarddrivePony", probability: 0.071 }, + { type: "/Lotus/StoreItems/Types/Items/Emotes/DanceDrillbitCrisscross", probability: 0.072 }, + { type: "/Lotus/StoreItems/Types/Items/Emotes/DanceZekeCanthavethis", probability: 0.071 }, + { type: "/Lotus/StoreItems/Types/Items/PhotoBooth/PhotoboothTileRJLasXStadiumBossArena", probability: 0.071 } +]; +export const getInfestedLichItemRewards = (fp: bigint): string[] => { + const rng = new SRng(fp); + const rotAReward = getRewardAtPercentage(infestedLichRotA, rng.randomFloat())!.type; + rng.randomFloat(); // unused afaict + const rotBReward = getRewardAtPercentage(infestedLichRotB, rng.randomFloat())!.type; + return [rotAReward, rotBReward]; +}; diff --git a/src/helpers/pathHelper.ts b/src/helpers/pathHelper.ts new file mode 100644 index 00000000..94a0f242 --- /dev/null +++ b/src/helpers/pathHelper.ts @@ -0,0 +1,5 @@ +import path from "path"; + +export const rootDir = path.join(__dirname, "../.."); +export const isDev = path.basename(rootDir) != "build"; +export const repoDir = isDev ? rootDir : path.join(rootDir, ".."); diff --git a/src/helpers/relicHelper.ts b/src/helpers/relicHelper.ts new file mode 100644 index 00000000..13d7d7d4 --- /dev/null +++ b/src/helpers/relicHelper.ts @@ -0,0 +1,69 @@ +import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; +import { IVoidTearParticipantInfo } from "@/src/types/requestTypes"; +import { ExportRelics, ExportRewards, TRarity } from "warframe-public-export-plus"; +import { getRandomWeightedReward, IRngResult } from "@/src/services/rngService"; +import { logger } from "@/src/utils/logger"; +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, + 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 = getRandomWeightedReward( + ExportRewards[relic.rewardManifest][0] as { type: string; itemCount: number; rarity: TRarity }[], // rarity is nullable in PE+ typings, but always present for relics + weights + )!; + logger.debug(`relic rolled`, reward); + participant.Reward = reward.type; + + // Remove relic + const miscItemChanges = [ + { + ItemType: participant.VoidProjection, + ItemCount: -1 + } + ]; + addMiscItems(inventory, miscItemChanges); + combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges }); + + // Give reward + combineInventoryChanges( + inventoryChanges, + (await handleStoreItemAcquisition(reward.type, inventory, reward.itemCount)).InventoryChanges + ); + + return reward; +}; + +const refinementToWeights = { + VPQ_BRONZE: { + COMMON: 0.76, + UNCOMMON: 0.22, + RARE: 0.02, + LEGENDARY: 0 + }, + VPQ_SILVER: { + COMMON: 0.7, + UNCOMMON: 0.26, + RARE: 0.04, + LEGENDARY: 0 + }, + VPQ_GOLD: { + COMMON: 0.6, + UNCOMMON: 0.34, + RARE: 0.06, + LEGENDARY: 0 + }, + VPQ_PLATINUM: { + COMMON: 0.5, + UNCOMMON: 0.4, + RARE: 0.1, + LEGENDARY: 0 + } +}; diff --git a/src/helpers/rivenHelper.ts b/src/helpers/rivenHelper.ts new file mode 100644 index 00000000..34a7babc --- /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..06d2ac28 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) as T; }; 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 acf55bdb..80adc252 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,40 +1,33 @@ -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..."); -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"; +// Proceed with normal startup: bring up config watcher service, validate config, connect to MongoDB, and finally start listening for HTTP. import mongoose from "mongoose"; +import { JSONStringify } from "json-with-bigint"; +import { startWebServer } from "./services/webService"; + +import { validateConfig } from "@/src/services/configWatcherService"; + +// Patch JSON.stringify to work flawlessly with Bigints. +JSON.stringify = JSONStringify; -registerLogFileCreationListener(); validateConfig(); mongoose .connect(config.mongodbUrl) .then(() => { logger.info("Connected to MongoDB"); - - const httpPort = config.httpPort || 80; - const httpsPort = config.httpsPort || 443; - const options = { - key: fs.readFileSync("static/certs/key.pem"), - cert: fs.readFileSync("static/certs/cert.pem") - }; - - http.createServer(app).listen(httpPort, () => { - logger.info("HTTP server started on port " + httpPort); - https.createServer(options, app).listen(httpsPort, () => { - logger.info("HTTPS server started on port " + httpsPort); - - logger.info( - "Access the WebUI in your browser at http://localhost" + (httpPort == 80 ? "" : ":" + httpPort) - ); - }); - }); + startWebServer(); }) .catch(error => { if (error instanceof Error) { diff --git a/src/managers/sessionManager.ts b/src/managers/sessionManager.ts index 98bcc912..ed397823 100644 --- a/src/managers/sessionManager.ts +++ b/src/managers/sessionManager.ts @@ -1,10 +1,12 @@ import { ISession, IFindSessionRequest } from "@/src/types/session"; import { logger } from "@/src/utils/logger"; +import { JSONParse } from "json-with-bigint"; +import { Types } from "mongoose"; const sessions: ISession[] = []; -function createNewSession(sessionData: ISession, Creator: string): ISession { - const sessionId = getNewSessionID(); +function createNewSession(sessionData: ISession, Creator: Types.ObjectId): ISession { + const sessionId = new Types.ObjectId(); const newSession: ISession = { sessionId, creatorId: Creator, @@ -25,7 +27,7 @@ function createNewSession(sessionData: ISession, Creator: string): ISession { customSettings: sessionData.customSettings || "", rewardSeed: sessionData.rewardSeed || -1, guildId: sessionData.guildId || "", - buildId: sessionData.buildId || 4920386201513015989, + buildId: sessionData.buildId || 4920386201513015989n, platform: sessionData.platform || 0, xplatform: sessionData.xplatform || true, freePublic: sessionData.freePublic || 3, @@ -40,13 +42,15 @@ function getAllSessions(): ISession[] { return sessions; } -function getSessionByID(sessionId: string): ISession | undefined { - return sessions.find(session => session.sessionId === sessionId); +function getSessionByID(sessionId: string | Types.ObjectId): ISession | undefined { + return sessions.find(session => session.sessionId.equals(sessionId)); } -function getSession(sessionIdOrRequest: string | IFindSessionRequest): any[] { - if (typeof sessionIdOrRequest === "string") { - const session = sessions.find(session => session.sessionId === sessionIdOrRequest); +function getSession( + sessionIdOrRequest: string | Types.ObjectId | IFindSessionRequest +): { createdBy: Types.ObjectId; id: Types.ObjectId }[] { + if (typeof sessionIdOrRequest === "string" || sessionIdOrRequest instanceof Types.ObjectId) { + const session = sessions.find(session => session.sessionId.equals(sessionIdOrRequest)); if (session) { logger.debug("Found Sessions:", { session }); return [ @@ -79,36 +83,15 @@ function getSession(sessionIdOrRequest: string | IFindSessionRequest): any[] { })); } -function getSessionByCreatorID(creatorId: string): ISession | undefined { - return sessions.find(session => session.creatorId === creatorId); +function getSessionByCreatorID(creatorId: string | Types.ObjectId): ISession | undefined { + return sessions.find(session => session.creatorId.equals(creatorId)); } -function getNewSessionID(): string { - const characters = "0123456789abcdef"; - const maxAttempts = 100; - let sessionId = ""; - - for (let attempt = 0; attempt < maxAttempts; attempt++) { - sessionId = "64"; - for (let i = 0; i < 22; i++) { - const randomIndex = Math.floor(Math.random() * characters.length); - sessionId += characters[randomIndex]; - } - - if (!sessions.some(session => session.sessionId === sessionId)) { - return sessionId; - } - } - - throw new Error("Failed to generate a unique session ID"); -} - -function updateSession(sessionId: string, sessionData: string): boolean { - const session = sessions.find(session => session.sessionId === sessionId); +function updateSession(sessionId: string | Types.ObjectId, sessionData: string): boolean { + const session = sessions.find(session => session.sessionId.equals(sessionId)); if (!session) return false; try { - const updatedData = JSON.parse(sessionData); - Object.assign(session, updatedData); + Object.assign(session, JSONParse(sessionData)); return true; } catch (error) { console.error("Invalid JSON string for session update."); @@ -116,8 +99,8 @@ function updateSession(sessionId: string, sessionData: string): boolean { } } -function deleteSession(sessionId: string): boolean { - const index = sessions.findIndex(session => session.sessionId === sessionId); +function deleteSession(sessionId: string | Types.ObjectId): boolean { + const index = sessions.findIndex(session => session.sessionId.equals(sessionId)); if (index !== -1) { sessions.splice(index, 1); return true; @@ -130,7 +113,6 @@ export { getAllSessions, getSessionByID, getSessionByCreatorID, - getNewSessionID, updateSession, deleteSession, getSession diff --git a/src/models/friendModel.ts b/src/models/friendModel.ts new file mode 100644 index 00000000..f253101a --- /dev/null +++ b/src/models/friendModel.ts @@ -0,0 +1,15 @@ +import { IFriendship } from "@/src/types/friendTypes"; +import { model, Schema } from "mongoose"; + +const friendshipSchema = new Schema({ + owner: { type: Schema.Types.ObjectId, required: true }, + friend: { type: Schema.Types.ObjectId, required: true }, + Note: String, + Favorite: Boolean +}); + +friendshipSchema.index({ owner: 1 }); +friendshipSchema.index({ friend: 1 }); +friendshipSchema.index({ owner: 1, friend: 1 }, { unique: true }); + +export const Friendship = model("Friendship", friendshipSchema); diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 0582dc64..c393a38d 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -2,13 +2,50 @@ import { IGuildDatabase, IDojoComponentDatabase, ITechProjectDatabase, - ITechProjectClient + IDojoDecoDatabase, + ILongMOTD, + IGuildMemberDatabase, + IGuildLogEntryNumber, + IGuildRank, + IGuildLogRoomChange, + IGuildLogEntryRoster, + IGuildLogEntryContributable, + IDojoLeaderboardEntry, + IGuildAdDatabase, + IAllianceDatabase, + IAllianceMemberDatabase, + GuildPermission } 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], + Scale: Number, + Name: String, + Sockets: Number, + RegularCredits: Number, + MiscItems: { type: [typeCountSchema], default: undefined }, + CompletionTime: Date, + RushPlatinum: Number, + PictureFrameInfo: pictureFrameInfoSchema, + Pending: Boolean +}); + +const dojoLeaderboardEntrySchema = new Schema( + { + s: Number, + r: Number, + n: String + }, + { _id: false } +); const dojoComponentSchema = new Schema({ + SortId: Schema.Types.ObjectId, pf: { type: String, required: true }, ppf: String, pi: Schema.Types.ObjectId, @@ -16,7 +53,21 @@ 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, + PaintBot: Schema.Types.ObjectId, + PendingColors: { type: [Number], default: undefined }, + Colors: { type: [Number], default: undefined }, + PendingLights: { type: [Number], default: undefined }, + Lights: { type: [Number], default: undefined }, + Settings: String, + Leaderboard: { type: [dojoLeaderboardEntrySchema], default: undefined } }); const techProjectSchema = new Schema( @@ -30,26 +81,217 @@ 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: GuildPermission.Host | 4318 + }, + { + Name: "/Lotus/Language/Game/Rank_Officer", + Permissions: GuildPermission.Host | 4314 + }, + { + Name: "/Lotus/Language/Game/Rank_Leader", + Permissions: GuildPermission.Host | 4106 + }, + { + Name: "/Lotus/Language/Game/Rank_Sage", + Permissions: GuildPermission.Host | 4304 + }, + { + Name: "/Lotus/Language/Game/Rank_Soldier", + Permissions: GuildPermission.Host | 4098 + }, + { + Name: "/Lotus/Language/Game/Rank_Initiate", + Permissions: GuildPermission.Host | GuildPermission.Fabricator + }, + { + Name: "/Lotus/Language/Game/Rank_Utility", + Permissions: GuildPermission.Host | GuildPermission.Fabricator } -}); +]; + +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 }, + VaultDecoRecipes: { type: [typeCountSchema], 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..37a7fc5e 100644 --- a/src/models/inboxModel.ts +++ b/src/models/inboxModel.ts @@ -4,7 +4,8 @@ import { typeCountSchema } from "@/src/models/inventoryModels/inventoryModel"; import { IMongoDate, IOid } from "@/src/types/commonTypes"; import { ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; -export interface IMessageClient extends Omit { +export interface IMessageClient + extends Omit { _id?: IOid; date: IMongoDate; startDate?: IMongoDate; @@ -15,6 +16,8 @@ export interface IMessageClient extends Omit( + { + GiftType: String + }, + { _id: false } +); + const messageSchema = new Schema( { ownerId: Schema.Types.ObjectId, @@ -89,18 +109,24 @@ const messageSchema = new Schema( endDate: Date, r: Boolean, att: { type: [String], default: undefined }, + gifts: { type: [giftSchema], default: undefined }, countedAtt: { type: [typeCountSchema], default: undefined }, + attVisualOnly: Boolean, 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 } ); @@ -112,13 +138,14 @@ messageSchema.virtual("messageId").get(function (this: IMessageDatabase) { messageSchema.set("toJSON", { virtuals: true, transform(_document, returnedObject) { - delete returnedObject.ownerId; - const messageDatabase = returnedObject as IMessageDatabase; const messageClient = returnedObject as IMessageClient; delete returnedObject._id; delete returnedObject.__v; + delete returnedObject.ownerId; + delete returnedObject.attVisualOnly; + delete returnedObject.expiry; messageClient.date = toMongoDate(messageDatabase.date); @@ -130,4 +157,7 @@ messageSchema.set("toJSON", { } }); +messageSchema.index({ ownerId: 1 }); +messageSchema.index({ expiry: 1 }, { expireAfterSeconds: 0 }); + export const Inbox = model("Inbox", messageSchema, "inbox"); diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 5848600e..27caf691 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, @@ -38,11 +38,11 @@ import { IPeriodicMissionCompletionResponse, ILoreFragmentScan, IEvolutionProgress, - IEndlessXpProgress, - ICrewShipPortGuns, + IEndlessXpProgressDatabase, + IEndlessXpProgressClient, ICrewShipCustomization, ICrewShipWeapon, - ICrewShipPilotWeapon, + ICrewShipWeaponEmplacements, IShipExterior, IHelminthFoodRecord, ICrewShipMembersDatabase, @@ -64,7 +64,42 @@ import { IKubrowPetEggClient, ICustomMarkers, IMarkerInfo, - IMarker + IMarker, + ICalendarProgress, + IPendingCouponDatabase, + IPendingCouponClient, + ILibraryDailyTaskInfo, + IDroneDatabase, + IDroneClient, + IAlignment, + ICollectibleEntry, + IIncentiveState, + ISongChallenge, + ILibraryPersonalProgress, + IRecentVendorPurchaseDatabase, + IVendorPurchaseHistoryEntryDatabase, + IVendorPurchaseHistoryEntryClient, + INemesisDatabase, + INemesisClient, + IInfNode, + IDiscoveredMarker, + IWeeklyMission, + ILockedWeaponGroupDatabase, + IPersonalTechProjectDatabase, + IPersonalTechProjectClient, + ILastSortieRewardDatabase, + ILastSortieRewardClient, + ICrewMemberSkill, + ICrewMemberSkillEfficiency, + ICrewMemberDatabase, + ICrewMemberClient, + ISortieRewardAttenuation, + IInvasionProgressDatabase, + IInvasionProgressClient, + IAccolades, + IHubNpcCustomization, + ILotusCustomization, + IEndlessXpReward } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -78,10 +113,21 @@ import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; -import { EquipmentSelectionSchema } from "./loadoutModel"; +import { EquipmentSelectionSchema, oidSchema } from "./loadoutModel"; +import { ICountedStoreItem } from "warframe-public-export-plus"; 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, @@ -102,29 +148,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, @@ -283,6 +306,55 @@ upgradeSchema.set("toJSON", { } }); +const crewMemberSkillSchema = new Schema( + { + Assigned: Number + }, + { _id: false } +); + +const crewMemberSkillEfficiencySchema = new Schema( + { + PILOTING: crewMemberSkillSchema, + GUNNERY: crewMemberSkillSchema, + ENGINEERING: crewMemberSkillSchema, + COMBAT: crewMemberSkillSchema, + SURVIVABILITY: crewMemberSkillSchema + }, + { _id: false } +); + +const crewMemberSchema = new Schema( + { + ItemType: { type: String, required: true }, + NemesisFingerprint: { type: BigInt, default: 0n }, + Seed: { type: BigInt, default: 0n }, + AssignedRole: Number, + SkillEfficiency: crewMemberSkillEfficiencySchema, + WeaponConfigIdx: Number, + WeaponId: { type: Schema.Types.ObjectId, default: "000000000000000000000000" }, + XP: { type: Number, default: 0 }, + PowersuitType: { type: String, required: true }, + Configs: [ItemConfigSchema], + SecondInCommand: { type: Boolean, default: false } + }, + { id: false } +); + +crewMemberSchema.set("toJSON", { + virtuals: true, + transform(_doc, obj) { + const db = obj as ICrewMemberDatabase; + const client = obj as ICrewMemberClient; + + client.WeaponId = toOid(db.WeaponId); + client.ItemId = toOid(db._id); + + delete obj._id; + delete obj.__v; + } +}); + const slotsBinSchema = new Schema( { Slots: Number, @@ -322,8 +394,8 @@ MailboxSchema.set("toJSON", { const DuviriInfoSchema = new Schema( { - Seed: Number, - NumCompletions: Number + Seed: { type: BigInt, required: true }, + NumCompletions: { type: Number, required: true } }, { _id: false, @@ -345,6 +417,52 @@ const TypeXPItemSchema = new Schema( { _id: false } ); +const droneSchema = new Schema( + { + ItemType: String, + CurrentHP: Number, + 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 } +); +droneSchema.set("toJSON", { + virtuals: true, + transform(_document, obj) { + const client = obj as IDroneClient; + 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, @@ -404,6 +522,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, @@ -411,6 +541,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 } @@ -432,7 +563,39 @@ const seasonChallengeHistorySchema = new Schema( { _id: false } ); -//TODO: check whether this is complete +const personalTechProjectSchema = new Schema({ + State: Number, + ReqCredits: Number, + ItemType: String, + ProductCategory: String, + CategoryItemId: Schema.Types.ObjectId, + ReqItems: { type: [typeCountSchema], default: undefined }, + HasContributions: Boolean, + CompletionDate: Date +}); + +personalTechProjectSchema.virtual("ItemId").get(function () { + return { $oid: this._id.toString() }; +}); + +personalTechProjectSchema.set("toJSON", { + virtuals: true, + transform(_doc, ret, _options) { + delete ret._id; + delete ret.__v; + + const db = ret as IPersonalTechProjectDatabase; + const client = ret as IPersonalTechProjectClient; + + if (db.CategoryItemId) { + client.CategoryItemId = toOid(db.CategoryItemId); + } + if (db.CompletionDate) { + client.CompletionDate = toMongoDate(db.CompletionDate); + } + } +}); + const playerSkillsSchema = new Schema( { LPP_SPACE: { type: Number, default: 0 }, @@ -455,7 +618,8 @@ const settingsSchema = new Schema({ GiftMode: String, GuildInvRestriction: String, ShowFriendInvNotifications: Boolean, - TradingRulesConfirmed: Boolean + TradingRulesConfirmed: Boolean, + SubscribedToSurveys: Boolean }); const consumedSchuitsSchema = new Schema( @@ -504,7 +668,7 @@ const questProgressSchema = new Schema( const questKeysSchema = new Schema( { - Progress: { type: [questProgressSchema], default: undefined }, + Progress: { type: [questProgressSchema], default: [] }, unlock: Boolean, Completed: Boolean, CustomData: String, @@ -526,7 +690,28 @@ questKeysSchema.set("toJSON", { } }); -const fusionTreasuresSchema = new Schema().add(typeCountSchema).add({ Sockets: Number }); +export const fusionTreasuresSchema = new Schema().add(typeCountSchema).add({ Sockets: Number }); + +const invasionProgressSchema = new Schema( + { + invasionId: Schema.Types.ObjectId, + Delta: Number, + AttackerScore: Number, + DefenderScore: Number + }, + { _id: false } +); + +invasionProgressSchema.set("toJSON", { + transform(_doc, obj) { + const db = obj as IInvasionProgressDatabase; + const client = obj as IInvasionProgressClient; + + client._id = toOid(db.invasionId); + delete obj.invasionId; + delete obj.__v; + } +}); const spectreLoadoutsSchema = new Schema( { @@ -544,7 +729,9 @@ const spectreLoadoutsSchema = new Schema( const weaponSkinsSchema = new Schema( { - ItemType: String + ItemType: String, + Favorite: Boolean, + IsNew: Boolean }, { id: false } ); @@ -597,6 +784,26 @@ const loreFragmentScansSchema = new Schema( { _id: false } ); +// const lotusCustomizationSchema = new Schema().add(ItemConfigSchema).add({ +// Persona: String +// }); + +// Laxer schema for cleanupInventory +const lotusCustomizationSchema = new Schema( + { + Skins: [String], + pricol: colorSchema, + attcol: Schema.Types.Mixed, + sigcol: Schema.Types.Mixed, + eyecol: Schema.Types.Mixed, + facial: Schema.Types.Mixed, + cloth: Schema.Types.Mixed, + syancol: Schema.Types.Mixed, + Persona: String + }, + { _id: false } +); + const evolutionProgressSchema = new Schema( { Progress: Number, @@ -606,33 +813,65 @@ const evolutionProgressSchema = new Schema( { _id: false } ); -const endlessXpProgressSchema = new Schema( +const countedStoreItemSchema = new Schema( { - Category: String, - Choices: [String] + StoreItem: String, + ItemCount: Number }, { _id: false } ); -const crewShipPilotWeaponSchema = new Schema( +const endlessXpRewardSchema = new Schema( + { + RequiredTotalXp: Number, + Rewards: [countedStoreItemSchema] + }, + { _id: false } +); + +const endlessXpProgressSchema = new Schema( + { + Category: { type: String, required: true }, + Earn: { type: Number, default: 0 }, + Claim: { type: Number, default: 0 }, + BonusAvailable: Date, + Expiry: Date, + Choices: { type: [String], required: true }, + PendingRewards: { type: [endlessXpRewardSchema], default: [] } + }, + { _id: false } +); + +endlessXpProgressSchema.set("toJSON", { + transform(_doc, ret) { + const db = ret as IEndlessXpProgressDatabase; + const client = ret as IEndlessXpProgressClient; + + if (db.BonusAvailable) { + client.BonusAvailable = toMongoDate(db.BonusAvailable); + } + if (db.Expiry) { + client.Expiry = toMongoDate(db.Expiry); + } + } +}); +const crewShipWeaponEmplacementsSchema = new Schema( { PRIMARY_A: EquipmentSelectionSchema, - SECONDARY_A: EquipmentSelectionSchema - }, - { _id: false } -); - -const crewShipPortGunsSchema = new Schema( - { - PRIMARY_A: EquipmentSelectionSchema + PRIMARY_B: EquipmentSelectionSchema, + SECONDARY_A: EquipmentSelectionSchema, + SECONDARY_B: EquipmentSelectionSchema }, { _id: false } ); const crewShipWeaponSchema = new Schema( { - PILOT: crewShipPilotWeaponSchema, - PORT_GUNS: crewShipPortGunsSchema + PILOT: crewShipWeaponEmplacementsSchema, + PORT_GUNS: crewShipWeaponEmplacementsSchema, + STARBOARD_GUNS: crewShipWeaponEmplacementsSchema, + ARTILLERY: crewShipWeaponEmplacementsSchema, + SCANNER: crewShipWeaponEmplacementsSchema }, { _id: false } ); @@ -656,7 +895,7 @@ const crewShipCustomizationSchema = new Schema( const crewShipMemberSchema = new Schema( { ItemId: { type: Schema.Types.ObjectId, required: false }, - NemesisFingerprint: { type: Number, required: false } + NemesisFingerprint: { type: BigInt, required: false } }, { _id: false } ); @@ -705,7 +944,7 @@ const dialogueSchema = new Schema( AvailableGiftDate: Date, RankUpExpiry: Date, BountyChemExpiry: Date, - //QueuedDialogues: ??? + QueuedDialogues: { type: [String], default: [] }, Gifts: { type: [dialogueGiftSchema], default: [] }, Booleans: { type: [String], default: [] }, Completed: { type: [completedDialogueSchema], default: [] }, @@ -728,7 +967,8 @@ dialogueSchema.set("toJSON", { const dialogueHistorySchema = new Schema( { - YearIteration: { type: Number, required: true }, + YearIteration: Number, + Resets: Number, Dialogues: { type: [dialogueSchema], required: false } }, { _id: false } @@ -773,14 +1013,16 @@ 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); + } } }); const EquipmentSchema = new Schema( { ItemType: String, - Configs: [ItemConfigSchema], + Configs: { type: [ItemConfigSchema], default: [] }, UpgradeVer: { type: Number, default: 101 }, XP: { type: Number, default: 0 }, Features: Number, @@ -802,12 +1044,15 @@ const EquipmentSchema = new Schema( OffensiveUpgrade: String, DefensiveUpgrade: String, UpgradesExpiry: Date, + UmbraDate: Date, ArchonCrystalUpgrades: { type: [ArchonCrystalUpgradeSchema], default: undefined }, Weapon: crewShipWeaponSchema, Customization: crewShipCustomizationSchema, RailjackImage: FlavourItemSchema, CrewMembers: crewShipMembersSchema, - Details: detailsSchema + Details: detailsSchema, + Favorite: Boolean, + IsNew: Boolean }, { id: false } ); @@ -828,6 +1073,12 @@ EquipmentSchema.set("toJSON", { if (db.InfestationDate) { client.InfestationDate = toMongoDate(db.InfestationDate); } + if (db.UpgradesExpiry) { + client.UpgradesExpiry = toMongoDate(db.UpgradesExpiry); + } + if (db.UmbraDate) { + client.UmbraDate = toMongoDate(db.UmbraDate); + } } }); @@ -837,6 +1088,46 @@ equipmentKeys.forEach(key => { equipmentFields[key] = { type: [EquipmentSchema] }; }); +const pendingRecipeSchema = new Schema( + { + ItemType: String, + CompletionDate: Date, + TargetItemId: String, + TargetFingerprint: String, + 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 accoladesSchema = new Schema( + { + Heirloom: Boolean + }, + { _id: false } +); + const infestedFoundrySchema = new Schema( { Name: String, @@ -891,19 +1182,234 @@ const CustomMarkersSchema = new Schema( { _id: false } ); +const calenderProgressSchema = new Schema( + { + Version: { type: Number, default: 19 }, + Iteration: { type: Number, required: true }, + YearProgress: { + Upgrades: { type: [String], default: [] } + }, + SeasonProgress: { + SeasonType: { type: String, required: true }, + LastCompletedDayIdx: { type: Number, default: 0 }, + LastCompletedChallengeDayIdx: { type: Number, default: 0 }, + ActivatedChallenges: { type: [String], default: [] } + } + }, + { _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) }, + Discount: { type: Number, default: 0 } + }, + { _id: false } +); + +pendingCouponSchema.set("toJSON", { + transform(_doc, ret, _options) { + (ret as IPendingCouponClient).Expiry = toMongoDate((ret as IPendingCouponDatabase).Expiry); + } +}); + +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, + RewardStanding: Number + }, + { _id: false } +); + +const infNodeSchema = 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: { type: [infNodeSchema], default: undefined }, + HenchmenKilled: Number, + HintProgress: Number, + Hints: { type: [Number], default: [] }, + GuessHistory: { type: [Number], default: undefined }, + 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 lastSortieRewardSchema = new Schema( + { + SortieId: Schema.Types.ObjectId, + StoreItem: String, + Manifest: String + }, + { _id: false } +); + +lastSortieRewardSchema.set("toJSON", { + virtuals: true, + transform(_doc, obj) { + const db = obj as ILastSortieRewardDatabase; + const client = obj as ILastSortieRewardClient; + + client.SortieId = toOid(db.SortieId); + + delete obj._id; + delete obj.__v; + } +}); + +const sortieRewardAttenutationSchema = new Schema( + { + Tag: String, + Atten: 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 hubNpcCustomizationSchema = new Schema( + { + Colors: colorSchema, + Pattern: String, + Tag: String + }, + { _id: false } +); + const inventorySchema = new Schema( { accountOwnerId: Schema.Types.ObjectId, - SubscribedToEmails: Number, - Created: Date, - RewardSeed: Number, + SubscribedToEmails: { type: Number, default: 0 }, + SubscribedToEmailsPersonalized: { type: Number, default: 0 }, + RewardSeed: BigInt, //Credit RegularCredits: { type: Number, default: 0 }, //Platinum - PremiumCredits: { type: Number, default: 50 }, + PremiumCredits: { type: Number, default: 0 }, //Gift Platinum(Non trade) - PremiumCreditsFree: { type: Number, default: 50 }, + PremiumCreditsFree: { type: Number, default: 0 }, //Endo FusionPoints: { type: Number, default: 0 }, //Regal Aya @@ -911,7 +1417,7 @@ const inventorySchema = new Schema( //Slots SuitBin: { type: slotsBinSchema, default: { Slots: 3 } }, - WeaponBin: { type: slotsBinSchema, default: { Slots: 10 } }, + WeaponBin: { type: slotsBinSchema, default: { Slots: 11 } }, SentinelBin: { type: slotsBinSchema, default: { Slots: 10 } }, SpaceSuitBin: { type: slotsBinSchema, default: { Slots: 4 } }, SpaceWeaponBin: { type: slotsBinSchema, default: { Slots: 4 } }, @@ -930,7 +1436,7 @@ const inventorySchema = new Schema( //How many Gift do you have left*(gift spends the trade) GiftsRemaining: { type: Number, default: 8 }, //Curent trade info Giving or Getting items - PendingTrades: [Schema.Types.Mixed], + //PendingTrades: [Schema.Types.Mixed], //Syndicate currently being pledged to. SupportedSyndicate: String, @@ -967,7 +1473,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], @@ -979,7 +1486,7 @@ const inventorySchema = new Schema( KubrowPetEggs: [kubrowPetEggSchema], //Prints Cat(3 Prints)\Kubrow(2 Prints) Pets - KubrowPetPrints: [Schema.Types.Mixed], + //KubrowPetPrints: [Schema.Types.Mixed], //Item for EquippedGear example:Scaner,LoadoutTechSummon etc Consumables: [typeCountSchema], @@ -991,6 +1498,7 @@ const inventorySchema = new Schema( ReceivedStartingGear: Boolean, ArchwingEnabled: Boolean, + HasOwnedVoidProjectionsPreviously: Boolean, //Use Operator\Drifter UseAdultOperatorLoadout: Boolean, @@ -1011,22 +1519,20 @@ const inventorySchema = new Schema( //Default RailJack CrewShipAmmo: [typeCountSchema], - CrewShipWeapons: [Schema.Types.Mixed], - CrewShipWeaponSkins: [Schema.Types.Mixed], + CrewShipWeaponSkins: [upgradeSchema], + CrewShipSalvagedWeaponSkins: [upgradeSchema], - //NPC Crew and weapon - CrewMembers: [Schema.Types.Mixed], - CrewShipSalvagedWeaponSkins: [Schema.Types.Mixed], - CrewShipSalvagedWeapons: [Schema.Types.Mixed], + //RailJack Crew + CrewMembers: [crewMemberSchema], //Complete Mission\Quests Missions: [missionSchema], QuestKeys: [questKeysSchema], - ActiveQuest: { type: String, default: "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain" }, //TODO: check after mission starting gear + ActiveQuest: { type: String, default: "" }, //item like DojoKey or Boss missions key - LevelKeys: [Schema.Types.Mixed], + LevelKeys: [typeCountSchema], //Active quests - Quests: [Schema.Types.Mixed], + //Quests: [Schema.Types.Mixed], //Cosmetics like profile glyphs\Kavasa Prime Kubrow Collar\Game Theme etc FlavourItems: [FlavourItemSchema], @@ -1038,6 +1544,16 @@ const inventorySchema = new Schema( //Mastery Rank next availability TrainingDate: { type: Date, default: new Date(0) }, + //Accolades + Staff: Boolean, + Founder: Number, + Guide: Number, + Moderator: Boolean, + Partner: Boolean, + Accolades: accoladesSchema, + //Not an accolade but unlocks an extra chat + Counselor: Boolean, + //you saw last played Region when you opened the star map LastRegionPlayed: String, @@ -1055,7 +1571,7 @@ const inventorySchema = new Schema( TauntHistory: { type: [tauntSchema], default: undefined }, //noShow2FA,VisitPrimeVault etc - WebFlags: Schema.Types.Mixed, + //WebFlags: Schema.Types.Mixed, //Id CompletedAlerts CompletedAlerts: [String], @@ -1075,9 +1591,9 @@ const inventorySchema = new Schema( //the color your clan requests like Items/Research/DojoColors/DojoColorPlainsB ActiveDojoColorResearch: String, - SentientSpawnChanceBoosters: Schema.Types.Mixed, + //SentientSpawnChanceBoosters: Schema.Types.Mixed, - QualifyingInvasions: [Schema.Types.Mixed], + QualifyingInvasions: [invasionProgressSchema], FactionScores: [Number], // https://warframe.fandom.com/wiki/Specter_(Tenno) @@ -1085,33 +1601,35 @@ 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], - LastSortieReward: [Schema.Types.Mixed], + LastSortieReward: { type: [lastSortieRewardSchema], default: undefined }, + LastLiteSortieReward: { type: [lastSortieRewardSchema], default: undefined }, + SortieRewardAttenuation: { type: [sortieRewardAttenutationSchema], default: undefined }, - //Resource_Drone[Uselees stuff] - Drones: [Schema.Types.Mixed], + // Resource Extractor Drones + Drones: [droneSchema], //Active profile ico ActiveAvatarImageType: String, // open location store like EidolonPlainsDiscoverable or OrbVallisCaveDiscoverable - DiscoveredMarkers: [Schema.Types.Mixed], + DiscoveredMarkers: [discoveredMarkerSchema], //Open location mission like "JobId" + "StageCompletions" - CompletedJobs: [Schema.Types.Mixed], + //CompletedJobs: [Schema.Types.Mixed], //Game mission\ivent score example "Tag": "WaterFight", "Best": 170, "Count": 1258, - PersonalGoalProgress: [Schema.Types.Mixed], + //PersonalGoalProgress: [Schema.Types.Mixed], //Setting interface Style ThemeStyle: String, @@ -1119,7 +1637,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], @@ -1129,47 +1647,48 @@ const inventorySchema = new Schema( //https://warframe.fandom.com/wiki/Heist //ProfitTaker(1-4) Example:"LocationTag": "EudicoHeists", "Jobs":Mission name - CompletedJobChains: [completedJobChainsSchema], + CompletedJobChains: { type: [completedJobChainsSchema], default: undefined }, //Night Wave Challenge SeasonChallengeHistory: [seasonChallengeHistorySchema], 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: Schema.Types.Mixed, + LibraryAvailableDailyTaskInfo: libraryDailyTaskInfoSchema, + LibraryActiveDailyTaskInfo: libraryDailyTaskInfoSchema, //https://warframe.fandom.com/wiki/Invasion - InvasionChainProgress: [Schema.Types.Mixed], + //InvasionChainProgress: [Schema.Types.Mixed], //CorpusLich or GrineerLich - NemesisAbandonedRewards: [String], - //CorpusLich\KuvaLich - NemesisHistory: [Schema.Types.Mixed], - LastNemesisAllySpawnTime: Schema.Types.Mixed, + NemesisAbandonedRewards: { type: [String], default: [] }, + Nemesis: nemesisSchema, + NemesisHistory: { type: [nemesisSchema], default: undefined }, + //LastNemesisAllySpawnTime: Schema.Types.Mixed, //TradingRulesConfirmed,ShowFriendInvNotifications(Option->Social) Settings: settingsSchema, //Railjack craft //https://warframe.fandom.com/wiki/Rising_Tide - PersonalTechProjects: [Schema.Types.Mixed], + PersonalTechProjects: { type: [personalTechProjectSchema], default: [] }, //Modulars lvl and exp(Railjack|Duviri) //https://warframe.fandom.com/wiki/Intrinsics PlayerSkills: { type: playerSkillsSchema, default: {} }, //TradeBannedUntil data - TradeBannedUntil: Schema.Types.Mixed, + //TradeBannedUntil: Schema.Types.Mixed, //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 - LotusCustomization: Schema.Types.Mixed, + LotusCustomization: { type: lotusCustomizationSchema, default: undefined }, //Progress+Rank+ItemType(ZarimanPumpShotgun) //https://warframe.fandom.com/wiki/Incarnon @@ -1180,31 +1699,31 @@ const inventorySchema = new Schema( //Unknown and system DuviriInfo: DuviriInfoSchema, + LastInventorySync: Schema.Types.ObjectId, Mailbox: MailboxSchema, HandlerPoints: Number, - ChallengesFixVersion: Number, + ChallengesFixVersion: { type: Number, default: 6 }, PlayedParkourTutorial: Boolean, - SubscribedToEmailsPersonalized: Number, - ActiveLandscapeTraps: [Schema.Types.Mixed], - RepVotes: [Schema.Types.Mixed], - LeagueTickets: [Schema.Types.Mixed], + //ActiveLandscapeTraps: [Schema.Types.Mixed], + //RepVotes: [Schema.Types.Mixed], + //LeagueTickets: [Schema.Types.Mixed], HasContributedToDojo: Boolean, HWIDProtectEnabled: Boolean, LoadOutPresets: { type: Schema.Types.ObjectId, ref: "Loadout" }, - CurrentLoadOutIds: [Schema.Types.Mixed], + CurrentLoadOutIds: [oidSchema], RandomUpgradesIdentified: Number, BountyScore: Number, - ChallengeInstanceStates: [Schema.Types.Mixed], - RecentVendorPurchases: [Schema.Types.Mixed], - Robotics: [Schema.Types.Mixed], - UsedDailyDeals: [Schema.Types.Mixed], - CollectibleSeries: [Schema.Types.Mixed], + //ChallengeInstanceStates: [Schema.Types.Mixed], + RecentVendorPurchases: { type: [recentVendorPurchaseSchema], default: undefined }, + //Robotics: [Schema.Types.Mixed], + //UsedDailyDeals: [Schema.Types.Mixed], + CollectibleSeries: { type: [collectibleEntrySchema], default: undefined }, HasResetAccount: { type: Boolean, default: false }, //Discount Coupon - PendingCoupon: Schema.Types.Mixed, + PendingCoupon: pendingCouponSchema, //Like BossAladV,BossCaptainVor come for you on missions % chance - DeathMarks: [String], + DeathMarks: { type: [String], default: [] }, //Zanuka Harvestable: Boolean, //Grustag three @@ -1212,7 +1731,29 @@ const inventorySchema = new Schema( EndlessXP: { type: [endlessXpProgressSchema], default: undefined }, - DialogueHistory: dialogueHistorySchema + DialogueHistory: dialogueHistorySchema, + 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 }, + + HubNpcCustomizations: { type: [hubNpcCustomizationSchema], default: undefined } }, { timestamps: { createdAt: "Created", updatedAt: false } } ); @@ -1223,20 +1764,47 @@ 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 (inventoryResponse.BlessingCooldown) { + 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 + }; + } + if (inventoryDatabase.LastInventorySync) { + inventoryResponse.LastInventorySync = toOid(inventoryDatabase.LastInventorySync); + } } }); +inventorySchema.index({ accountOwnerId: 1 }, { unique: true }); + // type overwrites for subdocuments/subdocument arrays export type InventoryDocumentProps = { FlavourItems: Types.DocumentArray; @@ -1249,14 +1817,20 @@ export type InventoryDocumentProps = { KahlLoadOuts: Types.DocumentArray; PendingRecipes: Types.DocumentArray; WeaponSkins: Types.DocumentArray; + QuestKeys: Types.DocumentArray; + Drones: Types.DocumentArray; + CrewShipWeaponSkins: Types.DocumentArray; + CrewShipSalvagedWeaponSkins: Types.DocumentArray; + PersonalTechProjects: Types.DocumentArray; + CrewMembers: 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..208b9e17 100644 --- a/src/models/inventoryModels/loadoutModel.ts +++ b/src/models/inventoryModels/loadoutModel.ts @@ -1,9 +1,9 @@ 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( +export const oidSchema = new Schema( { $oid: String }, @@ -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..0f83c0cb 100644 --- a/src/models/loginModel.ts +++ b/src/models/loginModel.ts @@ -1,4 +1,4 @@ -import { IDatabaseAccountJson } from "@/src/types/loginTypes"; +import { IDatabaseAccountJson, IIgnore } from "@/src/types/loginTypes"; import { model, Schema, SchemaOptions } from "mongoose"; const opts = { @@ -20,8 +20,12 @@ 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 } + BuildLabel: String, + Dropped: Boolean, + LastLogin: { type: Date, default: 0 }, + LatestEventMessageDate: { type: Date, default: 0 }, + LastLoginRewardDate: { type: Number, default: 0 }, + LoginDays: { type: Number, default: 1 } }, opts ); @@ -35,3 +39,13 @@ databaseAccountSchema.set("toJSON", { }); export const Account = model("Account", databaseAccountSchema); + +const ignoreSchema = new Schema({ + ignorer: Schema.Types.ObjectId, + ignoree: Schema.Types.ObjectId +}); + +ignoreSchema.index({ ignorer: 1 }); +ignoreSchema.index({ ignorer: 1, ignoree: 1 }, { unique: true }); + +export const Ignore = model("Ignore", ignoreSchema); diff --git a/src/models/personalRoomsModel.ts b/src/models/personalRoomsModel.ts index e54d1b1c..9a19d06b 100644 --- a/src/models/personalRoomsModel.ts +++ b/src/models/personalRoomsModel.ts @@ -1,18 +1,21 @@ -import { toOid } from "@/src/helpers/inventoryHelpers"; +import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; import { colorSchema } from "@/src/models/inventoryModels/inventoryModel"; import { IOrbiter, IPersonalRoomsDatabase, PersonalRoomsModelType } from "@/src/types/personalRoomsTypes"; import { - IApartment, IFavouriteLoadoutDatabase, - IGardening, + IGardeningDatabase, IPlacedDecosDatabase, IPictureFrameInfo, IRoom, - ITailorShopDatabase + ITailorShopDatabase, + IApartmentDatabase, + IPlanterDatabase, + IPlantDatabase, + IPlantClient } from "@/src/types/shipTypes"; -import { Schema, model } from "mongoose"; +import { Schema, Types, model } from "mongoose"; -const pictureFrameInfoSchema = new Schema( +export const pictureFrameInfoSchema = new Schema( { Image: String, Filter: String, @@ -57,56 +60,11 @@ const roomSchema = new Schema( { Name: String, MaxCapacity: Number, - PlacedDecos: { type: [placedDecosSchema], default: undefined } + PlacedDecos: { type: [placedDecosSchema], default: [] } }, { _id: false } ); -const gardeningSchema = new Schema({ - Planters: [Schema.Types.Mixed] //TODO: add when implementing gardening -}); - -const apartmentSchema = new Schema( - { - Rooms: [roomSchema], - FavouriteLoadouts: [Schema.Types.Mixed], - Gardening: gardeningSchema // TODO: ensure this is correct - }, - { _id: false } -); -const apartmentDefault: IApartment = { - Rooms: [ - { Name: "ElevatorLanding", MaxCapacity: 1600 }, - { Name: "ApartmentRoomA", MaxCapacity: 1000 }, - { Name: "ApartmentRoomB", MaxCapacity: 1600 }, - { Name: "ApartmentRoomC", MaxCapacity: 1600 }, - { Name: "DuviriHallway", MaxCapacity: 1600 } - ], - FavouriteLoadouts: [], - Gardening: {} -}; - -const orbiterSchema = new Schema( - { - Features: [String], - Rooms: [roomSchema], - ContentUrlSignature: { type: String, required: false }, - BootLocation: String - }, - { _id: false } -); -const orbiterDefault: IOrbiter = { - Features: ["/Lotus/Types/Items/ShipFeatureItems/EarthNavigationFeatureItem"], //TODO: potentially remove after missionstarting gear - Rooms: [ - { Name: "AlchemyRoom", MaxCapacity: 1600 }, - { Name: "BridgeRoom", MaxCapacity: 1600 }, - { Name: "LisetRoom", MaxCapacity: 1000 }, - { Name: "OperatorChamberRoom", MaxCapacity: 1600 }, - { Name: "OutsideRoom", MaxCapacity: 1600 }, - { Name: "PersonalQuartersRoom", MaxCapacity: 1600 } - ] -}; - const favouriteLoadoutSchema = new Schema( { Tag: String, @@ -122,6 +80,98 @@ favouriteLoadoutSchema.set("toJSON", { } }); +const plantSchema = new Schema( + { + PlantType: String, + EndTime: Date, + PlotIndex: Number + }, + { _id: false } +); + +plantSchema.set("toJSON", { + virtuals: true, + transform(_doc, obj) { + const client = obj as IPlantClient; + const db = obj as IPlantDatabase; + + client.EndTime = toMongoDate(db.EndTime); + } +}); + +const planterSchema = new Schema( + { + Name: { type: String, required: true }, + Plants: { type: [plantSchema], default: [] } + }, + { _id: false } +); + +const gardeningSchema = new Schema( + { + Planters: { type: [planterSchema], default: [] } + }, + { _id: false } +); + +const apartmentSchema = new Schema( + { + Rooms: [roomSchema], + FavouriteLoadouts: [favouriteLoadoutSchema], + Gardening: gardeningSchema + }, + { _id: false } +); +const apartmentDefault: IApartmentDatabase = { + Rooms: [ + { Name: "ElevatorLanding", MaxCapacity: 1600 }, + { Name: "ApartmentRoomA", MaxCapacity: 1000 }, + { Name: "ApartmentRoomB", MaxCapacity: 1600 }, + { Name: "ApartmentRoomC", MaxCapacity: 1600 }, + { Name: "DuviriHallway", MaxCapacity: 1600 } + ], + FavouriteLoadouts: [], + Gardening: { + Planters: [] + } +}; + +const orbiterSchema = new Schema( + { + Features: [String], + Rooms: [roomSchema], + VignetteFish: { type: [String], default: undefined }, + FavouriteLoadoutId: Schema.Types.ObjectId, + Wallpaper: String, + Vignette: String, + ContentUrlSignature: { type: String, required: false }, + BootLocation: String + }, + { _id: false } +); +const orbiterDefault: IOrbiter = { + Features: ["/Lotus/Types/Items/ShipFeatureItems/EarthNavigationFeatureItem"], //TODO: potentially remove after missionstarting gear + Rooms: [ + { Name: "AlchemyRoom", MaxCapacity: 1600 }, + { + Name: "BridgeRoom", + MaxCapacity: 1600, + PlacedDecos: [ + { + Type: "/Lotus/Objects/Tenno/Props/Ships/LandCraftPlayerProps/ConclaveConsolePlayerShipDeco", + Pos: [-30.082, -3.95954, -16.7913], + Rot: [-135, 0, 0], + _id: undefined as unknown as Types.ObjectId + } + ] + }, + { Name: "LisetRoom", MaxCapacity: 1000 }, + { Name: "OperatorChamberRoom", MaxCapacity: 1600 }, + { Name: "OutsideRoom", MaxCapacity: 1600 }, + { Name: "PersonalQuartersRoom", MaxCapacity: 1600 } + ] +}; + const tailorShopSchema = new Schema( { FavouriteLoadouts: [favouriteLoadoutSchema], @@ -152,6 +202,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..a1cd1457 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"; @@ -28,17 +28,15 @@ shipSchema.set("toJSON", { delete returnedObject._id; delete returnedObject.__v; delete returnedObject.ShipOwnerId; - if (shipDatabase.ShipExteriorColors) { - shipResponse.ShipExterior = { - Colors: shipDatabase.ShipExteriorColors, - ShipAttachments: shipDatabase.ShipAttachments, - SkinFlavourItem: shipDatabase.SkinFlavourItem - }; - delete shipDatabase.ShipExteriorColors; - delete shipDatabase.ShipAttachments; - delete shipDatabase.SkinFlavourItem; - } + shipResponse.ShipExterior = { + Colors: shipDatabase.ShipExteriorColors, + ShipAttachments: shipDatabase.ShipAttachments, + SkinFlavourItem: shipDatabase.SkinFlavourItem + }; + delete shipDatabase.ShipExteriorColors; + delete shipDatabase.ShipAttachments; + delete shipDatabase.SkinFlavourItem; } }); @@ -47,3 +45,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 7ad9e5a6..82b42842 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -1,42 +1,80 @@ 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 { addFriendController } from "@/src/controllers/api/addFriendController"; import { addFriendImageController } from "@/src/controllers/api/addFriendImageController"; +import { addIgnoredUserController } from "@/src/controllers/api/addIgnoredUserController"; +import { addPendingFriendController } from "@/src/controllers/api/addPendingFriendController"; +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 "../controllers/api/artifactsController"; -import { changeDojoRootController } from "../controllers/api/changeDojoRootController"; +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 { completeCalendarEventController } from "@/src/controllers/api/completeCalendarEventController"; +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 { crewMembersController } from "@/src/controllers/api/crewMembersController"; +import { crewShipIdentifySalvageController } from "@/src/controllers/api/crewShipIdentifySalvageController"; +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 { dojoController } from "@/src/controllers/api/dojoController"; +import { destroyDojoDecoController } from "@/src/controllers/api/destroyDojoDecoController"; +import { divvyAllianceVaultController } from "@/src/controllers/api/divvyAllianceVaultController"; +import { dojoComponentRushController } from "@/src/controllers/api/dojoComponentRushController"; +import { dojoController, setDojoURLController } 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"; import { focusController } from "@/src/controllers/api/focusController"; import { fusionTreasuresController } from "@/src/controllers/api/fusionTreasuresController"; +import { gardeningController } from "@/src/controllers/api/gardeningController"; import { genericUpdateController } from "@/src/controllers/api/genericUpdateController"; 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 "../controllers/api/getGuildLogController"; +import { getGuildLogController } from "@/src/controllers/api/getGuildLogController"; import { getIgnoredUsersController } from "@/src/controllers/api/getIgnoredUsersController"; import { getNewRewardSeedController } from "@/src/controllers/api/getNewRewardSeedController"; +import { getProfileViewingDataPostController } from "@/src/controllers/dynamic/getProfileViewingDataController"; 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"; -import { giveQuestKeyRewardController } from "@/src/controllers/api/giveQuestKey"; -import { guildTechController } from "../controllers/api/guildTechController"; +import { giveQuestKeyRewardController } from "@/src/controllers/api/giveQuestKeyRewardController"; +import { giveShipDecoAndLoreFragmentController } from "@/src/controllers/api/giveShipDecoAndLoreFragmentController"; +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"; @@ -46,64 +84,110 @@ 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 { playedParkourTutorialController } from "@/src/controllers/api/playedParkourTutorialController"; import { playerSkillsController } from "@/src/controllers/api/playerSkillsController"; -import { projectionManagerController } from "../controllers/api/projectionManagerController"; +import { postGuildAdvertisementController } from "@/src/controllers/api/postGuildAdvertisementController"; +import { projectionManagerController } from "@/src/controllers/api/projectionManagerController"; import { purchaseController } from "@/src/controllers/api/purchaseController"; +import { questControlController } from "@/src/controllers/api/questControlController"; import { queueDojoComponentDestructionController } from "@/src/controllers/api/queueDojoComponentDestructionController"; +import { redeemPromoCodeController } from "@/src/controllers/api/redeemPromoCodeController"; +import { releasePetController } from "@/src/controllers/api/releasePetController"; +import { removeFriendGetController, removeFriendPostController } from "@/src/controllers/api/removeFriendController"; +import { removeFromAllianceController } from "@/src/controllers/api/removeFromAllianceController"; +import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController"; +import { removeIgnoredUserController } from "@/src/controllers/api/removeIgnoredUserController"; 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 { saveLoadoutController } from "@/src/controllers/api/saveLoadoutController"; +import { saveSettingsController } from "@/src/controllers/api/saveSettingsController"; +import { saveVaultAutoContributeController } from "@/src/controllers/api/saveVaultAutoContributeController"; import { sellController } from "@/src/controllers/api/sellController"; +import { sendMsgToInBoxController } from "@/src/controllers/api/sendMsgToInBoxController"; 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 { setDojoComponentColorsController } from "@/src/controllers/api/setDojoComponentColorsController"; import { setDojoComponentMessageController } from "@/src/controllers/api/setDojoComponentMessageController"; +import { setDojoComponentSettingsController } from "@/src/controllers/api/setDojoComponentSettingsController"; import { setEquippedInstrumentController } from "@/src/controllers/api/setEquippedInstrumentController"; +import { setFriendNoteController } from "@/src/controllers/api/setFriendNoteController"; +import { setGuildMotdController } from "@/src/controllers/api/setGuildMotdController"; +import { setHubNpcCustomizationsController } from "@/src/controllers/api/setHubNpcCustomizationsController"; import { setPlacedDecoInfoController } from "@/src/controllers/api/setPlacedDecoInfoController"; import { setShipCustomizationsController } from "@/src/controllers/api/setShipCustomizationsController"; import { setShipFavouriteLoadoutController } from "@/src/controllers/api/setShipFavouriteLoadoutController"; +import { setShipVignetteController } from "@/src/controllers/api/setShipVignetteController"; import { setSupportedSyndicateController } from "@/src/controllers/api/setSupportedSyndicateController"; -import { setWeaponSkillTreeController } from "../controllers/api/setWeaponSkillTreeController"; +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"; import { surveysController } from "@/src/controllers/api/surveysController"; -import { syndicateSacrificeController } from "../controllers/api/syndicateSacrificeController"; -import { syndicateStandingBonusController } from "../controllers/api/syndicateStandingBonusController"; +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 { updateThemeController } from "../controllers/api/updateThemeController"; +import { updateSongChallengeController } from "@/src/controllers/api/updateSongChallengeController"; +import { updateThemeController } from "@/src/controllers/api/updateThemeController"; import { upgradesController } from "@/src/controllers/api/upgradesController"; -import { saveSettingsController } from "../controllers/api/saveSettingsController"; +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("/changeDojoRoot.php", changeDojoRootController); +apiRouter.get("/changeGuildRank.php", changeGuildRankController); apiRouter.get("/checkDailyMissionBonus.php", checkDailyMissionBonusController); +apiRouter.get("/claimLibraryDailyTaskReward.php", claimLibraryDailyTaskRewardController); +apiRouter.get("/completeCalendarEvent.php", completeCalendarEventController); +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("/getAlliance.php", getAllianceController); 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); +apiRouter.get("/getMessages.php", inboxController); // unsure if this is correct, but needed for U17 apiRouter.get("/getNewRewardSeed.php", getNewRewardSeedController); apiRouter.get("/getShip.php", getShipController); +apiRouter.get("/getShipDecos.php", (_req, res) => { res.end(); }); // needed to log in on U22.8 apiRouter.get("/getVendorInfo.php", getVendorInfoController); apiRouter.get("/hub", hubController); apiRouter.get("/hubInstances", hubInstancesController); @@ -114,61 +198,124 @@ apiRouter.get("/logout.php", logoutController); apiRouter.get("/marketRecommendations.php", marketRecommendationsController); apiRouter.get("/marketSearchRecommendations.php", marketRecommendationsController); apiRouter.get("/modularWeaponSale.php", modularWeaponSaleController); +apiRouter.get("/playedParkourTutorial.php", playedParkourTutorialController); +apiRouter.get("/questControl.php", questControlController); apiRouter.get("/queueDojoComponentDestruction.php", queueDojoComponentDestructionController); +apiRouter.get("/removeFriend.php", removeFriendGetController); +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("/setDojoURL", setDojoURLController); +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("/addFriend.php", addFriendController); apiRouter.post("/addFriendImage.php", addFriendImageController); +apiRouter.post("/addIgnoredUser.php", addIgnoredUserController); +apiRouter.post("/addPendingFriend.php", addPendingFriendController); +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("/commitStoryModeDecision.php", (_req, res) => { res.end(); }); // U14 (maybe wanna actually unlock the ship features?) +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("/crewMembers.php", crewMembersController); +apiRouter.post("/crewShipIdentifySalvage.php", crewShipIdentifySalvageController); +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); apiRouter.post("/focus.php", focusController); apiRouter.post("/fusionTreasures.php", fusionTreasuresController); +apiRouter.post("/gardening.php", gardeningController); apiRouter.post("/genericUpdate.php", genericUpdateController); apiRouter.post("/getAlliance.php", getAllianceController); +apiRouter.post("/getFriends.php", getFriendsController); +apiRouter.post("/getGuildDojo.php", getGuildDojoController); +apiRouter.post("/getProfileViewingData.php", getProfileViewingDataPostController); 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); apiRouter.post("/giveQuestKeyReward.php", giveQuestKeyRewardController); +apiRouter.post("/giveShipDecoAndLoreFragment.php", giveShipDecoAndLoreFragmentController); +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("/questControl.php", questControlController); // U17 +apiRouter.post("/redeemPromoCode.php", redeemPromoCodeController); +apiRouter.post("/releasePet.php", releasePetController); +apiRouter.post("/removeFriend.php", removeFriendPostController); +apiRouter.post("/removeFromGuild.php", removeFromGuildController); +apiRouter.post("/removeIgnoredUser.php", removeIgnoredUserController); 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("/sendMsgToInBox.php", sendMsgToInBoxController); +apiRouter.post("/setDojoComponentColors.php", setDojoComponentColorsController); apiRouter.post("/setDojoComponentMessage.php", setDojoComponentMessageController); +apiRouter.post("/setDojoComponentSettings.php", setDojoComponentSettingsController); apiRouter.post("/setEquippedInstrument.php", setEquippedInstrumentController); +apiRouter.post("/setFriendNote.php", setFriendNoteController); +apiRouter.post("/setGuildMotd.php", setGuildMotdController); +apiRouter.post("/setHubNpcCustomizations.php", setHubNpcCustomizationsController); apiRouter.post("/setPlacedDecoInfo.php", setPlacedDecoInfoController); apiRouter.post("/setShipCustomizations.php", setShipCustomizationsController); apiRouter.post("/setShipFavouriteLoadout.php", setShipFavouriteLoadoutController); +apiRouter.post("/setShipVignette.php", setShipVignetteController); 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); @@ -177,12 +324,16 @@ 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("/updateInventory.php", missionInventoryUpdateController); // U26 and below 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("/saveSettings.php", saveSettingsController); +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..8411d996 100644 --- a/src/routes/custom.ts +++ b/src/routes/custom.ts @@ -5,18 +5,24 @@ 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 { addMissingMaxRankModsController } from "@/src/controllers/custom/addMissingMaxRankModsController"; 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 { addXpController } from "@/src/controllers/custom/addXpController"; import { importController } from "@/src/controllers/custom/importController"; +import { manageQuestsController } from "@/src/controllers/custom/manageQuestsController"; +import { setEvolutionProgressController } from "@/src/controllers/custom/setEvolutionProgressController"; import { getConfigDataController } from "@/src/controllers/custom/getConfigDataController"; import { updateConfigDataController } from "@/src/controllers/custom/updateConfigDataController"; -import { manageQuestsController } from "@/src/controllers/custom/manageQuestsController"; const customRouter = express.Router(); @@ -25,7 +31,12 @@ 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.get("/addMissingMaxRankMods", addMissingMaxRankModsController); customRouter.post("/createAccount", createAccountController); customRouter.post("/createMessage", createMessageController); @@ -34,6 +45,7 @@ customRouter.post("/addItems", addItemsController); customRouter.post("/addXp", addXpController); customRouter.post("/import", importController); customRouter.post("/manageQuests", manageQuestsController); +customRouter.post("/setEvolutionProgress", setEvolutionProgressController); customRouter.get("/config", getConfigDataController); customRouter.post("/config", updateConfigDataController); diff --git a/src/routes/dynamic.ts b/src/routes/dynamic.ts index 0e808d48..c6950a16 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 { getProfileViewingDataGetController } 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", getProfileViewingDataGetController); +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 2312727b..6a42481b 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -1,39 +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[]; - administratorNames?: string[] | string; + NRS?: string[]; + administratorNames?: string[]; autoCreateAccount?: boolean; skipTutorial?: boolean; skipAllDialogue?: boolean; @@ -43,34 +23,65 @@ interface IConfig { infinitePlatinum?: boolean; infiniteEndo?: boolean; infiniteRegalAya?: boolean; + infiniteHelminthMaterials?: boolean; + claimingBlueprintRefundsIngredients?: boolean; + dontSubtractVoidTraces?: boolean; + dontSubtractConsumables?: boolean; unlockAllShipFeatures?: boolean; unlockAllShipDecorations?: boolean; unlockAllFlavourItems?: boolean; unlockAllSkins?: boolean; unlockAllCapturaScenes?: boolean; + unlockAllDecoRecipes?: boolean; universalPolarityEverywhere?: boolean; unlockDoubleCapacityPotatoesEverywhere?: boolean; unlockExilusEverywhere?: boolean; unlockArcanesEverywhere?: boolean; noDailyStandingLimits?: boolean; + noDailyFocusLimit?: boolean; + noArgonCrystalDecay?: boolean; + noMasteryRankUpCooldown?: boolean; + noVendorPurchaseLimits?: boolean; + noDeathMarks?: boolean; + noKimCooldowns?: boolean; + syndicateMissionsRepeatable?: boolean; + instantFinishRivenChallenge?: boolean; + instantResourceExtractorDrones?: boolean; + noResourceExtractorDronesDamage?: boolean; + skipClanKeyCrafting?: boolean; + noDojoRoomBuildStage?: boolean; + noDojoDecoBuildStage?: 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..46c236ea --- /dev/null +++ b/src/services/configWatcherService.ts @@ -0,0 +1,46 @@ +import fs from "fs"; +import fsPromises from "fs/promises"; +import { logger } from "../utils/logger"; +import { config, configPath, loadConfig } from "./configService"; +import { getWebPorts, startWebServer, stopWebServer } from "./webService"; + +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(); + + const webPorts = getWebPorts(); + if (config.httpPort != webPorts.http || config.httpsPort != webPorts.https) { + logger.info(`Restarting web server to apply port changes.`); + void stopWebServer().then(startWebServer); + } + } +}); + +export const validateConfig = (): void => { + 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/friendService.ts b/src/services/friendService.ts new file mode 100644 index 00000000..7affefec --- /dev/null +++ b/src/services/friendService.ts @@ -0,0 +1,47 @@ +import { IFriendInfo } from "../types/friendTypes"; +import { getInventory } from "./inventoryService"; +import { config } from "./configService"; +import { Account } from "../models/loginModel"; +import { Types } from "mongoose"; +import { Friendship } from "../models/friendModel"; +import { fromOid, toMongoDate } from "../helpers/inventoryHelpers"; + +export const addAccountDataToFriendInfo = async (info: IFriendInfo): Promise => { + const account = (await Account.findById(fromOid(info._id), "DisplayName LastLogin"))!; + info.DisplayName = account.DisplayName; + info.LastLogin = toMongoDate(account.LastLogin); +}; + +export const addInventoryDataToFriendInfo = async (info: IFriendInfo): Promise => { + const inventory = await getInventory(fromOid(info._id), "PlayerLevel ActiveAvatarImageType"); + info.PlayerLevel = config.spoofMasteryRank == -1 ? inventory.PlayerLevel : config.spoofMasteryRank; + info.ActiveAvatarImageType = inventory.ActiveAvatarImageType; +}; + +export const areFriends = async (a: Types.ObjectId | string, b: Types.ObjectId | string): Promise => { + const [aAddedB, bAddedA] = await Promise.all([ + Friendship.exists({ owner: a, friend: b }), + Friendship.exists({ owner: b, friend: a }) + ]); + return Boolean(aAddedB && bAddedA); +}; + +export const areFriendsOfFriends = async (a: Types.ObjectId | string, b: Types.ObjectId | string): Promise => { + const [aInternalFriends, bInternalFriends] = await Promise.all([ + Friendship.find({ owner: a }), + Friendship.find({ owner: b }) + ]); + for (const aInternalFriend of aInternalFriends) { + if (bInternalFriends.find(x => x.friend.equals(aInternalFriend.friend))) { + const c = aInternalFriend.friend; + const [cAcceptedA, cAcceptedB] = await Promise.all([ + Friendship.exists({ owner: c, friend: a }), + Friendship.exists({ owner: c, friend: b }) + ]); + if (cAcceptedA && cAcceptedB) { + return true; + } + } + } + return false; +}; diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 210bc1a6..5486c17f 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -1,11 +1,37 @@ import { Request } from "express"; -import { getAccountIdForRequest } from "@/src/services/loginService"; -import { getInventory } from "@/src/services/inventoryService"; -import { Guild } from "@/src/models/guildModel"; +import { getAccountIdForRequest, TAccountDocument } from "@/src/services/loginService"; +import { addLevelKeys, addRecipes, combineInventoryChanges, getInventory } from "@/src/services/inventoryService"; +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 { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; +import { + GuildPermission, + IAllianceClient, + IAllianceDatabase, + IAllianceMemberClient, + IDojoClient, + IDojoComponentClient, + IDojoComponentDatabase, + IDojoContributable, + IDojoDecoClient, + IGuildClient, + IGuildMemberClient, + IGuildMemberDatabase, + IGuildVault, + ITechProjectDatabase +} from "@/src/types/guildTypes"; +import { toMongoDate, toOid, toOid2 } 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 { 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"; +import allDecoRecipes from "@/static/fixed_responses/allDecoRecipes.json"; +import { createMessage } from "./inboxService"; +import { addAccountDataToFriendInfo, addInventoryDataToFriendInfo } from "./friendService"; export const getGuildForRequest = async (req: Request): Promise => { const accountId = await getAccountIdForRequest(req); @@ -21,53 +47,741 @@ 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 => { - const dojo: IDojoClient = { - _id: { $oid: guild._id.toString() }, +export const getGuildClient = async ( + guild: TGuildDatabaseDocument, + account: TAccountDocument +): Promise => { + const guildMembers = await GuildMember.find({ guildId: guild._id }); + + const members: IGuildMemberClient[] = []; + let missingEntry = true; + const dataFillInPromises: Promise[] = []; + for (const guildMember of guildMembers) { + const member: IGuildMemberClient = { + _id: toOid2(guildMember.accountId, account.BuildLabel), + Rank: guildMember.rank, + Status: guildMember.status, + Note: guildMember.RequestMsg, + RequestExpiry: guildMember.RequestExpiry ? toMongoDate(guildMember.RequestExpiry) : undefined + }; + if (guildMember.accountId.equals(account._id)) { + missingEntry = false; + } else { + dataFillInPromises.push(addAccountDataToFriendInfo(member)); + dataFillInPromises.push(addInventoryDataToFriendInfo(member)); + } + members.push(member); + } + if (missingEntry) { + // Handle clans created prior to creation of the GuildMember model. + await GuildMember.insertOne({ + accountId: account._id, + guildId: guild._id, + status: 0, + rank: 0 + }); + members.push({ + _id: toOid2(account._id, account.BuildLabel), + Status: 0, + Rank: 0 + }); + } + + await Promise.all(dataFillInPromises); + + return { + _id: toOid2(guild._id, account.BuildLabel), Name: guild.Name, - Tier: 1, + MOTD: guild.MOTD, + LongMOTD: guild.LongMOTD, + Members: members, + Ranks: guild.Ranks, + Tier: guild.Tier, + Emblem: guild.Emblem, + Vault: getGuildVault(guild), + ActiveDojoColorResearch: guild.ActiveDojoColorResearch, + Class: guild.Class, + XP: guild.XP, + IsContributor: !!guild.CeremonyContributors?.find(x => x.equals(account._id)), + NumContributors: guild.CeremonyContributors?.length ?? 0, + CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined, + AutoContributeFromVault: guild.AutoContributeFromVault, + AllianceId: guild.AllianceId ? toOid2(guild.AllianceId, account.BuildLabel) : undefined + }; +}; + +export const getGuildVault = (guild: TGuildDatabaseDocument): IGuildVault => { + return { + DojoRefundRegularCredits: guild.VaultRegularCredits, + DojoRefundMiscItems: guild.VaultMiscItems, + DojoRefundPremiumCredits: guild.VaultPremiumCredits, + ShipDecorations: guild.VaultShipDecorations, + FusionTreasures: guild.VaultFusionTreasures, + DecoRecipes: config.unlockAllDecoRecipes + ? allDecoRecipes.map(recipe => ({ ItemType: recipe, ItemCount: 1 })) + : guild.VaultDecoRecipes + }; +}; + +export const getDojoClient = async ( + guild: TGuildDatabaseDocument, + status: number, + componentId?: Types.ObjectId | string, + buildLabel?: string +): Promise => { + const dojo: IDojoClient = { + _id: toOid2(guild._id, buildLabel), + Name: guild.Name, + 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[] = []; + const decosToRemoveNoRefund: { componentId: Types.ObjectId; decoId: Types.ObjectId }[] = []; + let needSave = false; + for (const dojoComponent of guild.DojoComponents) { + if (!componentId || dojoComponent._id.equals(componentId)) { + const clientComponent: IDojoComponentClient = { + id: toOid2(dojoComponent._id, buildLabel), + SortId: toOid2(dojoComponent.SortId ?? dojoComponent._id, buildLabel), // always providing a SortId so decos don't need repositioning to reparent + pf: dojoComponent.pf, + ppf: dojoComponent.ppf, + Name: dojoComponent.Name, + Message: dojoComponent.Message, + DecoCapacity: dojoComponent.DecoCapacity ?? 600, + Settings: dojoComponent.Settings + }; + if (dojoComponent.pi) { + clientComponent.pi = toOid2(dojoComponent.pi, buildLabel); + clientComponent.op = dojoComponent.op!; + clientComponent.pp = dojoComponent.pp!; + } + if (dojoComponent.CompletionTime) { + clientComponent.CompletionTime = toMongoDate(dojoComponent.CompletionTime); + clientComponent.TimeRemaining = Math.trunc( + (dojoComponent.CompletionTime.getTime() - Date.now()) / 1000 + ); + 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); + clientComponent.DestructionTimeRemaining = Math.trunc( + (dojoComponent.DestructionTime.getTime() - Date.now()) / 1000 + ); + } + } else { + clientComponent.RegularCredits = dojoComponent.RegularCredits; + clientComponent.MiscItems = dojoComponent.MiscItems; + } + if (dojoComponent.Decos) { + clientComponent.Decos = []; + for (const deco of dojoComponent.Decos) { + const clientDeco: IDojoDecoClient = { + id: toOid2(deco._id, buildLabel), + Type: deco.Type, + Pos: deco.Pos, + Rot: deco.Rot, + Scale: deco.Scale, + Name: deco.Name, + Sockets: deco.Sockets, + PictureFrameInfo: deco.PictureFrameInfo + }; + if (deco.CompletionTime) { + if ( + deco.Type == "/Lotus/Objects/Tenno/Props/TnoPaintBotDojoDeco" && + Date.now() >= deco.CompletionTime.getTime() + ) { + if (dojoComponent.PendingColors) { + dojoComponent.Colors = dojoComponent.PendingColors; + dojoComponent.PendingColors = undefined; + } + if (dojoComponent.PendingLights) { + dojoComponent.Lights = dojoComponent.PendingLights; + dojoComponent.PendingLights = undefined; + } + decosToRemoveNoRefund.push({ componentId: dojoComponent._id, decoId: deco._id }); + continue; + } + clientDeco.CompletionTime = toMongoDate(deco.CompletionTime); + clientDeco.TimeRemaining = Math.trunc((deco.CompletionTime.getTime() - Date.now()) / 1000); + } else { + clientDeco.RegularCredits = deco.RegularCredits; + clientDeco.MiscItems = deco.MiscItems; + } + clientComponent.Decos.push(clientDeco); + } + } + clientComponent.PendingColors = dojoComponent.PendingColors; + clientComponent.Colors = dojoComponent.Colors; + clientComponent.PendingLights = dojoComponent.PendingLights; + clientComponent.Lights = dojoComponent.Lights; + 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; + } + for (const deco of decosToRemoveNoRefund) { + logger.debug(`removing polychrome`, deco); + const component = guild.DojoComponents.id(deco.componentId)!; + component.Decos!.splice( + component.Decos!.findIndex(x => x._id.equals(deco.decoId)), + 1 + ); + 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.indexOf(build.resultType) == -1) { + 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 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"); + await parallelForeach(members, async member => { + 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 +): 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); + } + } + if (guild.CeremonyContributors) { + await checkClanAscensionHasRequiredContributors(guild); + } +}; + +export const checkClanAscensionHasRequiredContributors = async (guild: TGuildDatabaseDocument): Promise => { + const requiredContributors = [1, 5, 15, 30, 50][guild.Tier - 1]; + // Once required contributor count is hit, the class is committed and there's 72 hours to claim endo. + if (guild.CeremonyContributors!.length >= requiredContributors) { + guild.Class = guild.CeremonyClass!; + guild.CeremonyClass = undefined; + guild.CeremonyResetDate = new Date(Date.now() + (config.fastClanAscension ? 5_000 : 72 * 3600_000)); + if (!config.fastClanAscension) { + // Send message to all active guild members + const members = await GuildMember.find({ guildId: guild._id, status: 0 }, "accountId"); + await parallelForeach(members, async member => { + // somewhat unfaithful as on live the "msg" is not a loctag, but since we don't have the string, we'll let the client fill it in with "arg". + await createMessage(member.accountId, [ + { + sndr: guild.Name, + msg: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgressDetails", + arg: [ + { + Key: "RESETDATE", + Tag: + guild.CeremonyResetDate!.getUTCMonth() + + "/" + + guild.CeremonyResetDate!.getUTCDate() + + "/" + + (guild.CeremonyResetDate!.getUTCFullYear() % 100) + + " " + + guild.CeremonyResetDate!.getUTCHours().toString().padStart(2, "0") + + ":" + + guild.CeremonyResetDate!.getUTCMinutes().toString().padStart(2, "0") + } + ], + sub: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgress", + icon: "/Lotus/Interface/Graphics/ClanTileImages/ClanEnterDojo.png", + highPriority: true + } + ]); + }); + } + } +}; + +export const giveClanKey = (inventory: TInventoryDatabaseDocument, inventoryChanges?: IInventoryChanges): void => { + if (config.skipClanKeyCrafting) { + const levelKeyChanges = [ + { + ItemType: "/Lotus/Types/Keys/DojoKey", + ItemCount: 1 + } + ]; + addLevelKeys(inventory, levelKeyChanges); + if (inventoryChanges) { + combineInventoryChanges(inventoryChanges, { LevelKeys: levelKeyChanges }); + } + } else { + const recipeChanges = [ + { + ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint", + ItemCount: 1 + } + ]; + addRecipes(inventory, recipeChanges); + if (inventoryChanges) { + combineInventoryChanges(inventoryChanges, { Recipes: recipeChanges }); + } + } +}; + +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 94fa1ef0..3f7f0051 100644 --- a/src/services/importService.ts +++ b/src/services/importService.ts @@ -2,6 +2,7 @@ import { Types } from "mongoose"; import { IEquipmentClient, IEquipmentDatabase, + IItemConfig, IOperatorConfigClient, IOperatorConfigDatabase } from "../types/inventoryTypes/commonInventoryTypes"; @@ -23,6 +24,12 @@ import { IKubrowPetDetailsDatabase, ILoadoutConfigClient, ILoadOutPresets, + INemesisClient, + INemesisDatabase, + IPendingRecipeClient, + IPendingRecipeDatabase, + IQuestKeyClient, + IQuestKeyDatabase, ISlots, IUpgradeClient, IUpgradeDatabase, @@ -31,6 +38,7 @@ import { } from "../types/inventoryTypes/inventoryTypes"; import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel"; import { ILoadoutConfigDatabase, ILoadoutDatabase } from "../types/saveLoadoutTypes"; +import { slotNames } from "../types/purchaseTypes"; const convertDate = (value: IMongoDate): Date => { return new Date(parseInt(value.$date.$numberLong)); @@ -48,8 +56,10 @@ const convertEquipment = (client: IEquipmentClient): IEquipmentDatabase => { InfestationDate: convertOptionalDate(client.InfestationDate), Expiry: convertOptionalDate(client.Expiry), UpgradesExpiry: convertOptionalDate(client.UpgradesExpiry), + UmbraDate: convertOptionalDate(client.UmbraDate), CrewMembers: client.CrewMembers ? convertCrewShipMembers(client.CrewMembers) : undefined, Details: client.Details ? convertKubrowDetails(client.Details) : undefined, + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition Configs: client.Configs ? client.Configs.map(obj => Object.fromEntries( @@ -96,18 +106,18 @@ const replaceSlots = (db: ISlots, client: ISlots): void => { db.Slots = client.Slots; }; -const convertCrewShipMember = (client: ICrewShipMemberClient): ICrewShipMemberDatabase => { - return { - ...client, - ItemId: client.ItemId ? new Types.ObjectId(client.ItemId.$oid) : undefined - }; +export const importCrewMemberId = (crewMemberId: ICrewShipMemberClient): ICrewShipMemberDatabase => { + if (crewMemberId.ItemId) { + return { ItemId: new Types.ObjectId(crewMemberId.ItemId.$oid) }; + } + return { NemesisFingerprint: BigInt(crewMemberId.NemesisFingerprint ?? 0) }; }; const convertCrewShipMembers = (client: ICrewShipMembersClient): ICrewShipMembersDatabase => { return { - SLOT_A: client.SLOT_A ? convertCrewShipMember(client.SLOT_A) : undefined, - SLOT_B: client.SLOT_B ? convertCrewShipMember(client.SLOT_B) : undefined, - SLOT_C: client.SLOT_C ? convertCrewShipMember(client.SLOT_C) : undefined + SLOT_A: client.SLOT_A ? importCrewMemberId(client.SLOT_A) : undefined, + SLOT_B: client.SLOT_B ? importCrewMemberId(client.SLOT_B) : undefined, + SLOT_C: client.SLOT_C ? importCrewMemberId(client.SLOT_C) : undefined }; }; @@ -143,6 +153,42 @@ 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, + fp: BigInt(client.fp), + d: convertDate(client.d) + }; +}; + +// Empty objects from live may have been encoded as empty arrays because of PHP. +const convertItemConfig = (client: T): T => { + return { + ...client, + pricol: Array.isArray(client.pricol) ? {} : client.pricol, + attcol: Array.isArray(client.attcol) ? {} : client.attcol, + sigcol: Array.isArray(client.sigcol) ? {} : client.sigcol, + eyecol: Array.isArray(client.eyecol) ? {} : client.eyecol, + facial: Array.isArray(client.facial) ? {} : client.facial, + cloth: Array.isArray(client.cloth) ? {} : client.cloth, + syancol: Array.isArray(client.syancol) ? {} : client.syancol + }; +}; + export const importInventory = (db: TInventoryDatabaseDocument, client: Partial): void => { for (const key of equipmentKeys) { if (client[key] !== undefined) { @@ -152,10 +198,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 => { @@ -171,45 +229,69 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< replaceArray(db[key], client[key].map(convertOperatorConfig)); } } - for (const key of [ - "SuitBin", - "WeaponBin", - "SentinelBin", - "SpaceSuitBin", - "SpaceWeaponBin", - "PvpBonusLoadoutBin", - "PveBonusLoadoutBin", - "RandomModBin", - "MechBin", - "CrewMemberBin", - "OperatorAmpBin", - "CrewShipSalvageBin" - ] as const) { + for (const key of slotNames) { if (client[key] !== undefined) { replaceSlots(db[key], client[key]); } } - if (client.UseAdultOperatorLoadout !== undefined) { - db.UseAdultOperatorLoadout = client.UseAdultOperatorLoadout; + // boolean + for (const key of [ + "UseAdultOperatorLoadout", + "HasOwnedVoidProjectionsPreviously", + "ReceivedStartingGear", + "ArchwingEnabled", + "PlayedParkourTutorial", + "Staff", + "Moderator", + "Partner", + "Counselor" + ] as const) { + if (client[key] !== undefined) { + db[key] = client[key]; + } } + // number for (const key of [ "PlayerLevel", "RegularCredits", "PremiumCredits", "PremiumCreditsFree", "FusionPoints", - "PrimeTokens" + "PrimeTokens", + "TradesRemaining", + "GiftsRemaining", + "ChallengesFixVersion", + "Founder", + "Guide" ] as const) { if (client[key] !== undefined) { db[key] = client[key]; } } - for (const key of ["ThemeStyle", "ThemeBackground", "ThemeSounds", "EquippedInstrument", "FocusAbility"] as const) { + // string + 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) { + // string[] + for (const key of [ + "EquippedGear", + "EquippedEmotes", + "NodeIntrosCompleted", + "DeathMarks", + "Wishlist", + "NemesisAbandonedRewards" + ] as const) { if (client[key] !== undefined) { db[key] = client[key]; } @@ -238,6 +320,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 = convertItemConfig(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.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 + }); + }); + } + if (client.Accolades !== undefined) { + db.Accolades = client.Accolades; + } }; const convertLoadOutConfig = (client: ILoadoutConfigClient): ILoadoutConfigDatabase => { diff --git a/src/services/inboxService.ts b/src/services/inboxService.ts index 11768054..0c2d698d 100644 --- a/src/services/inboxService.ts +++ b/src/services/inboxService.ts @@ -1,8 +1,8 @@ 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 messages from "@/static/fixed_responses/messages.json"; +import eventMessages from "@/static/fixed_responses/eventMessages.json"; import { logger } from "@/src/utils/logger"; export const getAllMessagesSorted = async (accountId: string): Promise[]> => { @@ -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}`); @@ -27,19 +27,19 @@ export const deleteAllMessagesRead = async (accountId: string): Promise => await Inbox.deleteMany({ ownerId: accountId, r: true }); }; -export const createNewEventMessages = async (req: Request) => { +export const createNewEventMessages = async (req: Request): Promise => { const account = await getAccountForRequest(req); const latestEventMessageDate = account.LatestEventMessageDate; //TODO: is baroo there? create these kind of messages too (periodical messages) - const newEventMessages = messages.Messages.filter(m => new Date(m.eventMessageDate) > latestEventMessageDate); + const newEventMessages = eventMessages.Messages.filter(m => new Date(m.eventMessageDate) > latestEventMessageDate); if (newEventMessages.length === 0) { logger.debug(`No new event messages. Latest event message date: ${latestEventMessageDate.toISOString()}`); 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) => { 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 65ccd2d0..4ccd9c5c 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, IAffiliationMods } from "@/src/types/purchaseTypes"; import { IChallengeProgress, - IConsumable, IFlavourItem, IMiscItem, IMission, @@ -20,37 +15,77 @@ import { TEquipmentKey, IFusionTreasure, IDailyAffiliations, - IInventoryDatabase, IKubrowPetEggDatabase, - IKubrowPetEggClient + IKubrowPetEggClient, + ILibraryDailyTaskInfo, + IDroneClient, + IUpgradeClient, + TPartialStartingGear, + ILoreFragmentScan, + ICrewMemberClient, + Status, + IKubrowPetDetailsDatabase, + ITraits, + ICalendarProgress, + INemesisWeaponTargetFingerprint, + INemesisPetTargetFingerprint, + IDialogueDatabase } 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, 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, + ExportChallenges, ExportCustoms, + ExportDrones, + ExportEmailItems, + ExportEnemies, ExportFlavour, + ExportFusionBundles, ExportGear, ExportKeys, + ExportMisc, + ExportRailjackWeapons, ExportRecipes, ExportResources, ExportSentinels, ExportSyndicates, ExportUpgrades, + ExportWarframes, ExportWeapons, + IDefaultUpgrade, + IPowersuit, + ISentinel, TStandingLimitBin } from "warframe-public-export-plus"; import { createShip } from "./shipService"; -import { creditBundles, fusionBundles } from "@/src/services/missionInventoryUpdateService"; -import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; -import { toOid } from "../helpers/inventoryHelpers"; +import { + catbrowDetails, + fromMongoDate, + fromOid, + kubrowDetails, + kubrowFurPatternsWeights, + kubrowWeights, + toOid +} from "../helpers/inventoryHelpers"; +import { addQuestKey, completeQuest } from "@/src/services/questService"; +import { handleBundleAcqusition } from "./purchaseService"; +import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; +import { getRandomElement, getRandomInt, getRandomWeightedReward, SRng } from "./rngService"; +import { createMessage } from "./inboxService"; +import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper"; +import { getNightwaveSyndicateTag, getWorldState } from "./worldStateService"; +import { generateNemesisProfile, INemesisProfile } from "../helpers/nemesisHelpers"; +import { TAccountDocument } from "./loginService"; export const createInventory = async ( accountOwnerId: Types.ObjectId, @@ -60,70 +95,21 @@ export const createInventory = async ( const inventory = new Inventory({ accountOwnerId: accountOwnerId, LoadOutPresets: defaultItemReferences.loadOutPresetId, - Ships: [defaultItemReferences.ship], - PlayedParkourTutorial: config.skipTutorial, - ReceivedStartingGear: config.skipTutorial + Ships: [defaultItemReferences.ship] }); + inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask(); + inventory.RewardSeed = generateRewardSeed(); + inventory.DuviriInfo = { + Seed: generateRewardSeed(), + NumCompletions: 0 + }; + await addItem(inventory, "/Lotus/Types/Friendly/PlayerControllable/Weapons/DuviriDualSwords"); + if (config.skipTutorial) { - const defaultEquipment = [ - // Awakening rewards - { ItemCount: 1, ItemType: "/Lotus/Powersuits/Excalibur/Excalibur" }, - { ItemCount: 1, ItemType: "/Lotus/Weapons/Tenno/Melee/LongSword/LongSword" }, - { ItemCount: 1, ItemType: "/Lotus/Weapons/Tenno/Pistol/Pistol" }, - { ItemCount: 1, ItemType: "/Lotus/Weapons/Tenno/Rifle/Rifle" }, - { ItemCount: 1, ItemType: "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem1" }, - { ItemCount: 1, ItemType: "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem2" }, - { ItemCount: 1, ItemType: "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem3" }, - { ItemCount: 1, ItemType: "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem4" }, - { ItemCount: 1, ItemType: "/Lotus/Types/Restoratives/LisetAutoHack" } - ]; - - // const vorsPrizeRewards = [ - // // Vor's Prize rewards - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarHealthMaxMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarShieldMaxMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarAbilityRangeMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarAbilityStrengthMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarAbilityDurationMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarPickupBonusMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarPowerMaxMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarEnemyRadarMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Melee/WeaponFireRateMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Melee/WeaponMeleeDamageMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Rifle/WeaponFactionDamageCorpus" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Rifle/WeaponFactionDamageGrineer" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Rifle/WeaponDamageAmountMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Pistol/WeaponFireDamageMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Pistol/WeaponElectricityDamageMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Pistol/WeaponDamageAmountMod" }, - // { ItemCount: 1, ItemType: "/Lotus/Types/Recipes/Weapons/BurstonRifleBlueprint" }, - // { ItemCount: 1, ItemType: "/Lotus/Types/Items/MiscItems/Morphic" }, - // { ItemCount: 400, ItemType: "/Lotus/Types/Items/MiscItems/PolymerBundle" }, - // { ItemCount: 150, ItemType: "/Lotus/Types/Items/MiscItems/AlloyPlate" } - // ]; - for (const equipment of defaultEquipment) { - await addItem(inventory, equipment.ItemType, equipment.ItemCount); - } - - // Missing in Public Export - inventory.Horses.push({ - ItemType: "/Lotus/Types/NeutralCreatures/ErsatzHorse/ErsatzHorsePowerSuit" - }); - inventory.DataKnives.push({ - ItemType: "/Lotus/Weapons/Tenno/HackingDevices/TnHackingDevice/TnHackingDeviceWeapon", - XP: 450000 - }); - inventory.Scoops.push({ - ItemType: "/Lotus/Weapons/Tenno/Speedball/SpeedballWeaponTest" - }); - inventory.DrifterMelee.push({ - ItemType: "/Lotus/Types/Friendly/PlayerControllable/Weapons/DuviriDualSwords" - }); - - inventory.QuestKeys.push({ - ItemType: "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain" - }); + inventory.PlayedParkourTutorial = true; + await addStartingGear(inventory); + await completeQuest(inventory, "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain"); const completedMissions = ["SolNode27", "SolNode89", "SolNode63", "SolNode85", "SolNode15", "SolNode79"]; @@ -133,40 +119,132 @@ export const createInventory = async ( Tag: tag })) ); - - inventory.RegularCredits = 25000; - inventory.FusionPoints = 180; } await inventory.save(); } catch (error) { - throw new Error(`Error creating inventory: ${error instanceof Error ? error.message : "Unknown error"}`); + throw new Error(`Error creating inventory: ${error instanceof Error ? error.message : "Unknown error type"}`); } }; +export const generateRewardSeed = (): bigint => { + const hiDword = getRandomInt(0, 0x7fffffff); + const loDword = getRandomInt(0, 0xffffffff); + let seed = (BigInt(hiDword) << 32n) | BigInt(loDword); + if (Math.random() < 0.5) { + seed *= -1n; + seed -= 1n; + } + return seed; +}; + +//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 +): Promise => { + if (inventory.ReceivedStartingGear) { + throw new Error(`account has already received starting gear`); + } + inventory.ReceivedStartingGear = true; + + 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, { IsNew: false }, inventoryChanges); + addEquipment(inventory, "Pistols", Pistols[0].ItemType, { IsNew: false }, inventoryChanges); + addEquipment(inventory, "Melee", Melee[0].ItemType, { IsNew: false }, inventoryChanges); + await addPowerSuit(inventory, Suits[0].ItemType, { IsNew: false }, inventoryChanges); + addEquipment( + inventory, + "DataKnives", + "/Lotus/Weapons/Tenno/HackingDevices/TnHackingDevice/TnHackingDeviceWeapon", + { XP: 450_000, IsNew: false }, + inventoryChanges + ); + addEquipment( + inventory, + "Scoops", + "/Lotus/Weapons/Tenno/Speedball/SpeedballWeaponTest", + { IsNew: false }, + 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); + } + + return inventoryChanges; +}; + /** * Combines two inventory changes objects into one. * * @param InventoryChanges - will hold the combined changes * @param delta - inventory changes to be added */ +//TODO: this fails silently when providing an incorrect object to delta export const combineInventoryChanges = (InventoryChanges: IInventoryChanges, delta: IInventoryChanges): void => { for (const key in delta) { if (!(key in InventoryChanges)) { InventoryChanges[key] = delta[key]; + } else if (key == "MiscItems") { + for (const deltaItem of delta[key]!) { + const existing = InventoryChanges[key]!.find(x => x.ItemType == deltaItem.ItemType); + if (existing) { + existing.ItemCount += deltaItem.ItemCount; + } else { + InventoryChanges[key]!.push(deltaItem); + } + } } 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; @@ -182,7 +260,7 @@ export const combineInventoryChanges = (InventoryChanges: IInventoryChanges, del export const getInventory = async ( accountOwnerId: string, - projection: string | undefined = undefined + projection?: string ): Promise => { const inventory = await Inventory.findOne({ accountOwnerId: accountOwnerId }, projection); @@ -193,11 +271,78 @@ 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, + targetFingerprint?: string, + exactQuantity: boolean = false +): 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 await handleBundleAcqusition(typeName, inventory, quantity); + } + // Strict typing if (typeName in ExportRecipes) { const recipeChanges = [ @@ -208,9 +353,7 @@ export const addItem = async ( ]; addRecipes(inventory, recipeChanges); return { - InventoryChanges: { - Recipes: recipeChanges - } + Recipes: recipeChanges }; } if (typeName in ExportResources) { @@ -223,33 +366,40 @@ 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 }, - 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 ...(!inventory.CrewShipHarnesses?.length ? addCrewShipHarness(inventory, "/Lotus/Types/Game/CrewShip/RailJack/DefaultHarness") : {}) }; - - return { InventoryChanges: inventoryChanges }; } else if (ExportResources[typeName].productCategory == "ShipDecorations") { const changes = [ { @@ -259,12 +409,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: got ${quantity}, expected 0..100`); + } for (let i = 0; i != quantity; ++i) { const egg: IKubrowPetEggDatabase = { ItemType: "/Lotus/Types/Game/KubrowPet/Eggs/KubrowEgg", @@ -275,23 +426,57 @@ export const addItem = async ( changes.push({ ItemType: egg.ItemType, ExpirationDate: { $date: { $numberLong: "2000000000000" } }, - ItemId: toOid(egg._id) + ItemId: toOid(egg._id) // TODO: Pass on buildLabel from purchaseService }); } 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") { + if (meta.subroutines || meta.randomisedUpgrades) { + // House versions need to be identified to get stats so put them into raw salvage first. + const rawSalvageChanges = [ + { + ItemType: typeName, + ItemCount: quantity + } + ]; + addCrewShipRawSalvage(inventory, rawSalvageChanges); + inventoryChanges = { CrewShipRawSalvage: rawSalvageChanges }; + } else { + // Sigma versions can be added directly. + if (quantity != 1) { + throw new Error( + `unexpected acquisition quantity of CrewShipWeaponSkin: got ${quantity}, expected 1` + ); + } + inventoryChanges = { + ...addCrewShipWeaponSkin(inventory, typeName, undefined), + ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, premiumPurchase) + }; + } + } else { + if (quantity != 1) { + throw new Error(`unexpected acquisition quantity of WeaponSkins: got ${quantity}, expected 1`); + } + 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 = [ @@ -302,35 +487,104 @@ export const addItem = async ( ]; addMods(inventory, changes); return { - InventoryChanges: { - RawUpgrades: changes - } + RawUpgrades: changes }; } if (typeName in ExportGear) { + // Multipling by purchase quantity for gear because: + // - The Saya's Vigil scanner message has it as a non-counted attachment. + // - Blueprints for Ancient Protector Specter, Shield Osprey Specter, etc. have num=1 despite giving their purchaseQuantity. + if (!exactQuantity) { + quantity *= ExportGear[typeName].purchaseQuantity ?? 1; + } const consumablesChanges = [ { 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); - updateSlots(inventory, InventorySlot.WEAPONS, 0, 1); - return { - InventoryChanges: { - ...inventoryChanges, - WeaponBin: { count: 1, platinum: 0, Slots: -1 } + 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) + } + ] + }); + } + if (targetFingerprint) { + const targetFingerprintObj = JSON.parse(targetFingerprint) as INemesisWeaponTargetFingerprint; + defaultOverwrites.UpgradeType = targetFingerprintObj.ItemType; + defaultOverwrites.UpgradeFingerprint = JSON.stringify(targetFingerprintObj.UpgradeFingerprint); + defaultOverwrites.ItemName = targetFingerprintObj.Name; + } + const inventoryChanges = addEquipment(inventory, weapon.productCategory, typeName, defaultOverwrites); + if (weapon.additionalItems) { + for (const item of weapon.additionalItems) { + combineInventoryChanges(inventoryChanges, await addItem(inventory, item, 1)); + } + } + return { + ...inventoryChanges, + ...occupySlot( + inventory, + productCategoryToInventoryBin(weapon.productCategory) ?? InventorySlot.WEAPONS, + premiumPurchase + ) + }; + } else if (targetFingerprint) { + // Sister's Hound + const targetFingerprintObj = JSON.parse(targetFingerprint) as INemesisPetTargetFingerprint; + const head = targetFingerprintObj.Parts[0]; + const defaultOverwrites: Partial = { + ModularParts: targetFingerprintObj.Parts, + ItemName: targetFingerprintObj.Name, + Configs: applyDefaultUpgrades(inventory, ExportWeapons[head].defaultUpgrades) + }; + const 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" + }[head] as string; + return { + ...addEquipment(inventory, "MoaPets", itemType, defaultOverwrites), + ...occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase) }; } else { // Modular weapon parts @@ -342,42 +596,75 @@ export const addItem = async ( ]; addMiscItems(inventory, miscItemChanges); return { - InventoryChanges: { - MiscItems: miscItemChanges - } + MiscItems: miscItemChanges }; } } - if (typeName in creditBundles) { - const creditsTotal = creditBundles[typeName] * quantity; + if (typeName in ExportRailjackWeapons) { + const meta = ExportRailjackWeapons[typeName]; + if (meta.defaultUpgrades?.length) { + // House versions need to be identified to get stats so put them into raw salvage first. + const rawSalvageChanges = [ + { + ItemType: typeName, + ItemCount: quantity + } + ]; + addCrewShipRawSalvage(inventory, rawSalvageChanges); + return { CrewShipRawSalvage: rawSalvageChanges }; + } else { + // Sigma versions can be added directly. + if (quantity != 1) { + throw new Error(`unexpected acquisition quantity of CrewShipWeapon: got ${quantity}, expected 1`); + } + return { + ...addEquipment(inventory, meta.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 fusionBundles) { - const fusionPointsTotal = fusionBundles[typeName] * quantity; - inventory.FusionPoints += fusionPointsTotal; + if (typeName in ExportFusionBundles) { + const fusionPointsTotal = ExportFusionBundles[typeName].fusionPoints * quantity; + addFusionPoints(inventory, fusionPointsTotal); return { - InventoryChanges: { - FusionPoints: fusionPointsTotal - } + FusionPoints: fusionPointsTotal }; } if (typeName in ExportKeys) { - // Note: "/Lotus/Types/Keys/" contains some EmailItems and ShipFeatureItems - inventory.QuestKeys.push({ ItemType: typeName }); - return { - InventoryChanges: { - QuestKeys: [ - { - ItemType: typeName - } - ] - } - }; + // Note: "/Lotus/Types/Keys/" contains some EmailItems + const key = ExportKeys[typeName]; + + if (key.chainStages) { + const key = addQuestKey(inventory, { ItemType: typeName }); + if (!key) return {}; + return { QuestKeys: [key] }; + } else { + const levelKeyChanges = [{ ItemType: typeName, ItemCount: quantity }]; + addLevelKeys(inventory, levelKeyChanges); + return { LevelKeys: levelKeyChanges }; + } + } + if (typeName in ExportDrones) { + // Can only get 1 at a time from crafting, but for convenience's sake, allow up 100 to via the WebUI. + if (quantity < 0 || quantity > 100) { + throw new Error(`unexpected acquisition quantity of Drones: got ${quantity}, expected 0..100`); + } + for (let i = 0; i != quantity; ++i) { + return addDrone(inventory, typeName); + } + } + if (typeName in ExportEmailItems) { + if (quantity != 1) { + throw new Error(`unexpected acquisition quantity of EmailItems: got ${quantity}, expected 1`); + } + return await addEmailItem(inventory, typeName); } // Path-based duck typing @@ -385,45 +672,34 @@ 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, { + Features: 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) }; } } @@ -432,50 +708,55 @@ export const addItem = async ( switch (typeName.substr(1).split("/")[2]) { case "Mods": // Legendary Core case "CosmeticEnhancers": // Traumatic Peculiar - const changes = [ - { - ItemType: typeName, - ItemCount: quantity - } - ]; - addMods(inventory, changes); - return { - InventoryChanges: { + { + const changes = [ + { + ItemType: typeName, + ItemCount: quantity + } + ]; + addMods(inventory, changes); + return { 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") { @@ -487,41 +768,48 @@ export const addItem = async ( } satisfies IMiscItem ]; addMiscItems(inventory, miscItemChanges); + inventory.HasOwnedVoidProjectionsPreviously = true; return { - InventoryChanges: { - MiscItems: miscItemChanges - } + MiscItems: miscItemChanges }; + } else if ( + typeName.substr(1).split("/")[3] == "CatbrowPet" || + typeName.substr(1).split("/")[3] == "KubrowPet" + ) { + return addKubrowPet(inventory, typeName, undefined, premiumPurchase); + } else if (typeName.startsWith("/Lotus/Types/Game/CrewShip/CrewMember/")) { + if (!seed) { + throw new Error(`Expected crew member to have a seed`); + } + seed |= 0x33b81en << 32n; + return { + ...addCrewMember(inventory, typeName, seed), + ...occupySlot(inventory, InventorySlot.CREWMEMBERS, premiumPurchase) + }; + } else if (typeName == "/Lotus/Types/Game/CrewShip/RailJack/DefaultHarness") { + return addCrewShipHarness(inventory, typeName); } break; } case "NeutralCreatures": { + if (inventory.Horses.length != 0) { + logger.warn("refusing to add Horse because account already has one"); + return {}; + } 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 addMotorcycle(inventory, typeName); + } + break; } break; } - const errorMessage = `unable to add item: ${typeName}`; - logger.error(errorMessage); - throw new Error(errorMessage); + throw new Error(`unable to add item: ${typeName}`); }; export const addItems = async ( @@ -532,32 +820,29 @@ 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; }; -//TODO: maybe genericMethod for all the add methods, they share a lot of logic -export const addSentinel = ( +export const applyDefaultUpgrades = ( inventory: TInventoryDatabaseDocument, - sentinelName: string, - inventoryChanges: IInventoryChanges = {} -): IInventoryChanges => { - if (ExportSentinels[sentinelName]?.defaultWeapon) { - addSentinelWeapon(inventory, ExportSentinels[sentinelName].defaultWeapon, inventoryChanges); - } - + defaultUpgrades: IDefaultUpgrade[] | undefined +): IItemConfig[] => { const modsToGive: IRawUpgrade[] = []; const configs: IItemConfig[] = []; - if (ExportSentinels[sentinelName]?.defaultUpgrades) { + if (defaultUpgrades) { const upgrades = []; - for (const defaultUpgrade of ExportSentinels[sentinelName].defaultUpgrades) { + 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; } } @@ -565,60 +850,125 @@ export const addSentinel = ( configs.push({ Upgrades: upgrades }); } } - addMods(inventory, modsToGive); - const sentinelIndex = inventory.Sentinels.push({ ItemType: sentinelName, Configs: configs, XP: 0 }) - 1; + return configs; +}; + +//TODO: maybe genericMethod for all the add methods, they share a lot of logic +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, 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, + Features: premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined, + IsNew: inventory.Sentinels.find(x => x.ItemType == sentinelName) ? undefined : true + }) - 1; inventoryChanges.Sentinels ??= []; - (inventoryChanges.Sentinels as IEquipmentClient[]).push( - inventory.Sentinels[sentinelIndex].toJSON() - ); + 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 as IEquipmentClient[]).push( - inventory.SentinelWeapons[index].toJSON() - ); + inventoryChanges.SentinelWeapons.push(inventory.SentinelWeapons[index].toJSON()); }; -export const addPowerSuit = ( +export const addPowerSuit = async ( inventory: TInventoryDatabaseDocument, powersuitName: string, + defaultOverwrites?: Partial, inventoryChanges: IInventoryChanges = {} -): IInventoryChanges => { - const specialItems = getExalted(powersuitName); - if (specialItems) { - for (const specialItem of specialItems) { - addSpecialItem(inventory, specialItem, inventoryChanges); +): 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 suit: Omit = Object.assign( + { + ItemType: powersuitName, + Configs: [], + UpgradeVer: 101, + XP: 0, + IsNew: true + }, + defaultOverwrites + ); + if (suit.IsNew) { + suit.IsNew = !inventory.Suits.find(x => x.ItemType == powersuitName); + } + if (!suit.IsNew) { + suit.IsNew = undefined; + } + const suitIndex = inventory.Suits.push(suit) - 1; inventoryChanges.Suits ??= []; - (inventoryChanges.Suits as IEquipmentClient[]).push(inventory.Suits[suitIndex].toJSON()); + 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 +): 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: inventory.MechSuits.find(x => x.ItemType == mechsuitName) ? undefined : true + }) - 1; inventoryChanges.MechSuits ??= []; - (inventoryChanges.MechSuits as IEquipmentClient[]).push(inventory.MechSuits[suitIndex].toJSON()); + inventoryChanges.MechSuits.push(inventory.MechSuits[suitIndex].toJSON()); return inventoryChanges; }; @@ -639,21 +989,109 @@ export const addSpecialItem = ( XP: 0 }) - 1; inventoryChanges.SpecialItems ??= []; - (inventoryChanges.SpecialItems as IEquipmentClient[]).push( - inventory.SpecialItems[specialItemIndex].toJSON() - ); + inventoryChanges.SpecialItems.push(inventory.SpecialItems[specialItemIndex].toJSON()); }; export const addSpaceSuit = ( inventory: TInventoryDatabaseDocument, spacesuitName: string, + inventoryChanges: IInventoryChanges = {}, + features?: number +): IInventoryChanges => { + const suitIndex = + inventory.SpaceSuits.push({ + ItemType: spacesuitName, + Configs: [], + UpgradeVer: 101, + XP: 0, + Features: features, + IsNew: inventory.SpaceSuits.find(x => x.ItemType == spacesuitName) ? undefined : true + }) - 1; + inventoryChanges.SpaceSuits ??= []; + inventoryChanges.SpaceSuits.push(inventory.SpaceSuits[suitIndex].toJSON()); + return inventoryChanges; +}; + +export const addKubrowPet = ( + inventory: TInventoryDatabaseDocument, + kubrowPetName: string, + details: IKubrowPetDetailsDatabase | undefined, + premiumPurchase: boolean, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { - const suitIndex = inventory.SpaceSuits.push({ ItemType: spacesuitName, Configs: [], UpgradeVer: 101, XP: 0 }) - 1; - inventoryChanges.SpaceSuits ??= []; - (inventoryChanges.SpaceSuits as IEquipmentClient[]).push( - inventory.SpaceSuits[suitIndex].toJSON() - ); + combineInventoryChanges(inventoryChanges, occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase)); + + const kubrowPet = ExportSentinels[kubrowPetName] as ISentinel | undefined; + const exalted = kubrowPet?.exalted ?? []; + for (const specialItem of exalted) { + addSpecialItem(inventory, specialItem, inventoryChanges); + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const configs: IItemConfig[] = applyDefaultUpgrades(inventory, kubrowPet?.defaultUpgrades); + + if (!details) { + let traits: ITraits; + + if (kubrowPetName == "/Lotus/Types/Game/CatbrowPet/VampireCatbrowPetPowerSuit") { + traits = { + BaseColor: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseVampire", + SecondaryColor: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryVampire", + TertiaryColor: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryVampire", + AccentColor: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsVampire", + EyeColor: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseA", + FurPattern: "/Lotus/Types/Game/CatbrowPet/Patterns/CatbrowPetPatternVampire", + Personality: kubrowPetName, + BodyType: "/Lotus/Types/Game/CatbrowPet/BodyTypes/CatbrowPetVampireBodyType", + Head: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadVampire", + Tail: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailVampire" + }; + } else { + const isCatbrow = [ + "/Lotus/Types/Game/CatbrowPet/MirrorCatbrowPetPowerSuit", + "/Lotus/Types/Game/CatbrowPet/CheshireCatbrowPetPowerSuit" + ].includes(kubrowPetName); + const traitsPool = isCatbrow ? catbrowDetails : kubrowDetails; + + traits = { + BaseColor: getRandomWeightedReward(traitsPool.Colors, kubrowWeights)!.type, + SecondaryColor: getRandomWeightedReward(traitsPool.Colors, kubrowWeights)!.type, + TertiaryColor: getRandomWeightedReward(traitsPool.Colors, kubrowWeights)!.type, + AccentColor: getRandomWeightedReward(traitsPool.Colors, kubrowWeights)!.type, + EyeColor: getRandomWeightedReward(traitsPool.EyeColors, kubrowWeights)!.type, + FurPattern: getRandomWeightedReward(traitsPool.FurPatterns, kubrowFurPatternsWeights)!.type, + Personality: kubrowPetName, + BodyType: getRandomWeightedReward(traitsPool.BodyTypes, kubrowWeights)!.type, + Head: isCatbrow ? getRandomWeightedReward(traitsPool.Heads, kubrowWeights)!.type : undefined, + Tail: isCatbrow ? getRandomWeightedReward(traitsPool.Tails, kubrowWeights)!.type : undefined + }; + } + + 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 + }; + } + + const kubrowPetIndex = + inventory.KubrowPets.push({ + ItemType: kubrowPetName, + Configs: configs, + XP: 0, + Details: details, + IsNew: inventory.KubrowPets.find(x => x.ItemType == kubrowPetName) ? undefined : true + }) - 1; + inventoryChanges.KubrowPets ??= []; + inventoryChanges.KubrowPets.push(inventory.KubrowPets[kubrowPetIndex].toJSON()); + return inventoryChanges; }; @@ -664,9 +1102,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; } }; @@ -678,38 +1115,38 @@ const isCurrencyTracked = (usePremium: boolean): boolean => { export const updateCurrency = ( inventory: TInventoryDatabaseDocument, price: number, - usePremium: boolean -): ICurrencyChanges => { - const currencyChanges: ICurrencyChanges = {}; + usePremium: boolean, + inventoryChanges: IInventoryChanges = {} +): IInventoryChanges => { if (price != 0 && isCurrencyTracked(usePremium)) { if (usePremium) { if (inventory.PremiumCreditsFree > 0) { - currencyChanges.PremiumCreditsFree = Math.min(price, inventory.PremiumCreditsFree) * -1; - inventory.PremiumCreditsFree += currencyChanges.PremiumCreditsFree; + const premiumCreditsFreeDelta = Math.min(price, inventory.PremiumCreditsFree) * -1; + inventoryChanges.PremiumCreditsFree ??= 0; + inventoryChanges.PremiumCreditsFree += premiumCreditsFreeDelta; + inventory.PremiumCreditsFree += premiumCreditsFreeDelta; } - currencyChanges.PremiumCredits = -price; - inventory.PremiumCredits += currencyChanges.PremiumCredits; + inventoryChanges.PremiumCredits ??= 0; + inventoryChanges.PremiumCredits -= price; + inventory.PremiumCredits -= price; + logger.debug(`currency changes `, { PremiumCredits: -price }); } else { - currencyChanges.RegularCredits = -price; - inventory.RegularCredits += currencyChanges.RegularCredits; + inventoryChanges.RegularCredits ??= 0; + inventoryChanges.RegularCredits -= price; + inventory.RegularCredits -= price; + logger.debug(`currency changes `, { RegularCredits: -price }); } - logger.debug(`currency changes `, currencyChanges); } - return currencyChanges; + return inventoryChanges; }; -export const updateCurrencyByAccountId = async ( - price: number, - usePremium: boolean, - accountId: string -): Promise => { - if (!isCurrencyTracked(usePremium)) { - return {}; +export const addFusionPoints = (inventory: TInventoryDatabaseDocument, add: number): number => { + if (inventory.FusionPoints + add > 2147483647) { + logger.warn(`capping FusionPoints balance at 2147483647`); + add = 2147483647 - inventory.FusionPoints; } - const inventory = await getInventory(accountId); - const currencyChanges = updateCurrency(inventory, price, usePremium); - await inventory.save(); - return currencyChanges; + inventory.FusionPoints += add; + return add; }; const standingLimitBinToInventoryKey: Record< @@ -734,32 +1171,76 @@ const standingLimitBinToInventoryKey: Record< export const allDailyAffiliationKeys: (keyof IDailyAffiliations)[] = Object.values(standingLimitBinToInventoryKey); -export const getStandingLimit = (inventory: IDailyAffiliations, bin: TStandingLimitBin): number => { +const getStandingLimit = (inventory: IDailyAffiliations, bin: TStandingLimitBin): number => { if (bin == "STANDING_LIMIT_BIN_NONE" || config.noDailyStandingLimits) { return Number.MAX_SAFE_INTEGER; } return inventory[standingLimitBinToInventoryKey[bin]]; }; -export const updateStandingLimit = ( - inventory: IDailyAffiliations, - bin: TStandingLimitBin, - subtrahend: number -): void => { +const updateStandingLimit = (inventory: IDailyAffiliations, bin: TStandingLimitBin, subtrahend: number): void => { if (bin != "STANDING_LIMIT_BIN_NONE" && !config.noDailyStandingLimits) { inventory[standingLimitBinToInventoryKey[bin]] -= subtrahend; } }; +export const addStanding = ( + inventory: TInventoryDatabaseDocument, + syndicateTag: string, + gainedStanding: number, + isMedallion: boolean = false +): IAffiliationMods => { + let syndicate = inventory.Affiliations.find(x => x.Tag == syndicateTag); + const syndicateMeta = ExportSyndicates[syndicateTag]; + + if (!syndicate) { + syndicate = + inventory.Affiliations[inventory.Affiliations.push({ Tag: syndicateTag, Standing: 0, Title: 0 }) - 1]; + } + + const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0); + if (syndicate.Standing + gainedStanding > max) gainedStanding = max - syndicate.Standing; + + if (!isMedallion || syndicateMeta.medallionsCappedByDailyLimit) { + if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) { + gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin); + } + updateStandingLimit(inventory, syndicateMeta.dailyLimitBin, gainedStanding); + } + + syndicate.Standing += gainedStanding; + return { + Tag: syndicateTag, + Standing: gainedStanding + }; +}; + // 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); + } else if (node == "BeatCaliberChicks") { + await addEmailItem(inventory, "/Lotus/Types/Items/EmailItems/BeatCaliberChicksEmailItem", inventoryChanges); + } else if (node == "ClearedFiveLoops") { + await addEmailItem(inventory, "/Lotus/Types/Items/EmailItems/ClearedFiveLoopsEmailItem", inventoryChanges); + } + } + // Combine the two arrays into one. data.NodeIntrosCompleted = inventory.NodeIntrosCompleted.concat(data.NodeIntrosCompleted); @@ -768,34 +1249,39 @@ 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 = ( inventory: TInventoryDatabaseDocument, category: TEquipmentKey, type: string, - modularParts: string[] | undefined = undefined, + defaultOverwrites?: Partial, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { - const index = - inventory[category].push({ + const equipment: Omit = Object.assign( + { ItemType: type, Configs: [], XP: 0, - ModularParts: modularParts - }) - 1; + IsNew: category != "CrewShipWeapons" && category != "CrewShipSalvagedWeapons" + }, + defaultOverwrites + ); + if (equipment.IsNew) { + equipment.IsNew = !inventory[category].find(x => x.ItemType == type); + } + if (!equipment.IsNew) { + equipment.IsNew = undefined; + } + const index = inventory[category].push(equipment) - 1; inventoryChanges[category] ??= []; - (inventoryChanges[category] as IEquipmentClient[]).push(inventory[category][index].toJSON()); + inventoryChanges[category].push(inventory[category][index].toJSON()); return inventoryChanges; }; @@ -804,11 +1290,14 @@ export const addCustomization = ( customizationName: string, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { - const flavourItemIndex = inventory.FlavourItems.push({ ItemType: customizationName }) - 1; - inventoryChanges.FlavourItems ??= []; - (inventoryChanges.FlavourItems as IFlavourItem[]).push( - inventory.FlavourItems[flavourItemIndex].toJSON() - ); + if (!inventory.FlavourItems.find(x => x.ItemType == customizationName)) { + 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() + ); + } return inventoryChanges; }; @@ -817,10 +1306,47 @@ export const addSkin = ( typeName: string, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { - const index = inventory.WeaponSkins.push({ ItemType: typeName }) - 1; - inventoryChanges.WeaponSkins ??= []; - (inventoryChanges.WeaponSkins as IWeaponSkinClient[]).push( - inventory.WeaponSkins[index].toJSON() + if (inventory.WeaponSkins.find(x => x.ItemType == typeName)) { + logger.debug(`refusing to add WeaponSkin ${typeName} because account already owns it`); + } else { + 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() + ); + } + return inventoryChanges; +}; + +export const addCrewShipWeaponSkin = ( + inventory: TInventoryDatabaseDocument, + typeName: string, + upgradeFingerprint: string | undefined, + inventoryChanges: IInventoryChanges = {} +): IInventoryChanges => { + const index = + inventory.CrewShipWeaponSkins.push({ ItemType: typeName, UpgradeFingerprint: upgradeFingerprint }) - 1; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + inventoryChanges.CrewShipWeaponSkins ??= []; + (inventoryChanges.CrewShipWeaponSkins as IUpgradeClient[]).push( + inventory.CrewShipWeaponSkins[index].toJSON() + ); + return inventoryChanges; +}; + +export const addCrewShipSalvagedWeaponSkin = ( + inventory: TInventoryDatabaseDocument, + typeName: string, + upgradeFingerprint: string | undefined, + inventoryChanges: IInventoryChanges = {} +): IInventoryChanges => { + const index = + inventory.CrewShipSalvagedWeaponSkins.push({ ItemType: typeName, UpgradeFingerprint: upgradeFingerprint }) - 1; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + inventoryChanges.CrewShipSalvagedWeaponSkins ??= []; + (inventoryChanges.CrewShipSalvagedWeaponSkins as IUpgradeClient[]).push( + inventory.CrewShipSalvagedWeaponSkins[index].toJSON() ); return inventoryChanges; }; @@ -830,9 +1356,13 @@ const addCrewShip = ( typeName: string, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { - const index = inventory.CrewShips.push({ ItemType: typeName }) - 1; - inventoryChanges.CrewShips ??= []; - (inventoryChanges.CrewShips as object[]).push(inventory.CrewShips[index].toJSON()); + if (inventory.CrewShips.length != 0) { + 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()); + } return inventoryChanges; }; @@ -841,27 +1371,150 @@ const addCrewShipHarness = ( typeName: string, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { - const index = inventory.CrewShipHarnesses.push({ ItemType: typeName }) - 1; - inventoryChanges.CrewShipHarnesses ??= []; - (inventoryChanges.CrewShipHarnesses as object[]).push(inventory.CrewShipHarnesses[index].toJSON()); + 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()); + } return inventoryChanges; }; -//TODO: wrong id is not erroring -export const addGearExpByCategory = ( +const addMotorcycle = ( inventory: TInventoryDatabaseDocument, - gearArray: IEquipmentClient[] | undefined, + typeName: string, + inventoryChanges: IInventoryChanges = {} +): IInventoryChanges => { + if (inventory.Motorcycles.length != 0) { + 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()); + } + return inventoryChanges; +}; + +const addDrone = ( + inventory: TInventoryDatabaseDocument, + typeName: string, + inventoryChanges: IInventoryChanges = {} +): IInventoryChanges => { + const index = inventory.Drones.push({ ItemType: typeName, CurrentHP: ExportDrones[typeName].durability }) - 1; + inventoryChanges.Drones ??= []; + inventoryChanges.Drones.push(inventory.Drones[index].toJSON()); + return inventoryChanges; +}; + +/*const getCrewMemberSkills = (seed: bigint, skillPointsToAssign: number): Record => { + const rng = new SRng(seed); + + const skills = ["PILOTING", "GUNNERY", "ENGINEERING", "COMBAT", "SURVIVABILITY"]; + for (let i = 1; i != 5; ++i) { + const swapIndex = rng.randomInt(0, i); + if (swapIndex != i) { + const tmp = skills[i]; + skills[i] = skills[swapIndex]; + skills[swapIndex] = tmp; + } + } + + rng.randomFloat(); // unused afaict + + const skillAssignments = [0, 0, 0, 0, 0]; + for (let skill = 0; skillPointsToAssign; skill = (skill + 1) % 5) { + const maxIncrease = Math.min(5 - skillAssignments[skill], skillPointsToAssign); + const increase = rng.randomInt(0, maxIncrease); + skillAssignments[skill] += increase; + skillPointsToAssign -= increase; + } + + skillAssignments.sort((a, b) => b - a); + + const combined: Record = {}; + for (let i = 0; i != 5; ++i) { + combined[skills[i]] = skillAssignments[i]; + } + return combined; +};*/ + +const addCrewMember = ( + inventory: TInventoryDatabaseDocument, + itemType: string, + seed: bigint, + inventoryChanges: IInventoryChanges = {} +): IInventoryChanges => { + // SkillEfficiency is additional to the base stats, so we don't need to compute this + //const skillPointsToAssign = itemType.endsWith("Strong") ? 12 : itemType.indexOf("Medium") != -1 ? 10 : 8; + //const skills = getCrewMemberSkills(seed, skillPointsToAssign); + + // Arbiters = male + // CephalonSuda = female + // NewLoka = female + // Perrin = male + // RedVeil = male + // SteelMeridian = female + const powersuitType = + itemType.indexOf("Arbiters") != -1 || itemType.indexOf("Perrin") != -1 || itemType.indexOf("RedVeil") != -1 + ? "/Lotus/Powersuits/NpcPowersuits/CrewMemberMaleSuit" + : "/Lotus/Powersuits/NpcPowersuits/CrewMemberFemaleSuit"; + + const index = + inventory.CrewMembers.push({ + ItemType: itemType, + NemesisFingerprint: 0n, + Seed: seed, + SkillEfficiency: { + PILOTING: { Assigned: 0 }, + GUNNERY: { Assigned: 0 }, + ENGINEERING: { Assigned: 0 }, + COMBAT: { Assigned: 0 }, + SURVIVABILITY: { Assigned: 0 } + }, + PowersuitType: powersuitType + }) - 1; + inventoryChanges.CrewMembers ??= []; + inventoryChanges.CrewMembers.push(inventory.CrewMembers[index].toJSON()); + 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; +}; + +export const applyClientEquipmentUpdates = ( + inventory: TInventoryDatabaseDocument, + gearArray: IEquipmentClient[], categoryName: TEquipmentKey ): void => { const category = inventory[categoryName]; - gearArray?.forEach(({ ItemId, XP }) => { - if (!XP) { - return; + gearArray.forEach(({ ItemId, XP, InfestationDate }) => { + const item = category.id(fromOid(ItemId)); + if (!item) { + throw new Error(`No item with id ${fromOid(ItemId)} in ${categoryName}`); } - const item = category.id(ItemId.$oid); - if (item) { + if (XP) { item.XP ??= 0; item.XP += XP; @@ -876,13 +1529,35 @@ export const addGearExpByCategory = ( }); } } + + if (InfestationDate) { + // 2147483647000 means cured, otherwise became infected + item.InfestationDate = + InfestationDate.$date.$numberLong == "2147483647000" ? new Date(0) : fromMongoDate(InfestationDate); + } }); }; -export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray: IMiscItem[] | undefined): void => { +export const addMiscItem = ( + inventory: TInventoryDatabaseDocument, + type: string, + count: number, + inventoryChanges: IInventoryChanges +): void => { + const miscItemChanges: IMiscItem[] = [ + { + ItemType: type, + ItemCount: count + } + ]; + addMiscItems(inventory, miscItemChanges); + combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges }); +}; + +export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray: IMiscItem[]): void => { const { MiscItems } = inventory; - itemsArray?.forEach(({ ItemCount, ItemType }) => { + itemsArray.forEach(({ ItemCount, ItemType }) => { if (ItemCount == 0) { return; } @@ -893,94 +1568,81 @@ 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) { - logger.warn(`account now owns a negative amount of ${ItemType}`); + logger.warn(`inventory.MiscItems has a negative count for ${ItemType}`); } }); }; -export const addShipDecorations = ( +const applyArrayChanges = ( inventory: TInventoryDatabaseDocument, - itemsArray: IConsumable[] | undefined + key: "ShipDecorations" | "Consumables" | "CrewShipRawSalvage" | "CrewShipAmmo" | "Recipes" | "LevelKeys", + changes: ITypeCount[] ): void => { - const { ShipDecorations } = inventory; + const arr: ITypeCount[] = inventory[key]; + 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(`inventory.${key} has a negative count for ${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 addLevelKeys = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { + applyArrayChanges(inventory, "LevelKeys", itemsArray); +}; + +export const addMods = (inventory: TInventoryDatabaseDocument, itemsArray: IRawUpgrade[]): void => { const { RawUpgrades } = inventory; - itemsArray?.forEach(({ ItemType, ItemCount }) => { + itemsArray.forEach(({ ItemType, ItemCount }) => { if (ItemCount == 0) { return; } @@ -994,28 +1656,30 @@ export const addMods = (inventory: TInventoryDatabaseDocument, itemsArray: IRawU if (RawUpgrades[itemIndex].ItemCount == 0) { RawUpgrades.splice(itemIndex, 1); } else if (RawUpgrades[itemIndex].ItemCount <= 0) { - logger.warn(`account now owns a negative amount of ${ItemType}`); + logger.warn(`inventory.RawUpgrades has a negative count for ${ItemType}`); } }); }; -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(`inventory.FusionTreasures has a negative count for ${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, @@ -1029,60 +1693,82 @@ 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]; + + if (!config.noDailyFocusLimit) { + inventory.DailyFocus -= focusXpPlus.reduce((a, b) => a + b, 0); } }; -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(); -}; - -export const addSeasonalChallengeHistory = ( - inventory: TInventoryDatabaseDocument, - itemsArray: ISeasonChallenge[] | undefined -): void => { - const category = inventory.SeasonChallengeHistory; - - itemsArray?.forEach(({ challenge, id }) => { - const itemIndex = category.findIndex(i => i.challenge === challenge); - - if (itemIndex !== -1) { - category[itemIndex].id = id; +export const addLoreFragmentScans = (inventory: TInventoryDatabaseDocument, arr: ILoreFragmentScan[]): void => { + arr.forEach(clientFragment => { + const fragment = inventory.LoreFragmentScans.find(x => x.ItemType == clientFragment.ItemType); + if (fragment) { + fragment.Progress += clientFragment.Progress; } else { - category.push({ challenge, id }); + inventory.LoreFragmentScans.push(clientFragment); } }); }; export const addChallenges = ( + account: TAccountDocument, inventory: TInventoryDatabaseDocument, - itemsArray: IChallengeProgress[] | undefined -): void => { - const category = inventory.ChallengeProgress; - - itemsArray?.forEach(({ Name, Progress }) => { - const itemIndex = category.findIndex(i => i.Name === Name); + ChallengeProgress: IChallengeProgress[], + SeasonChallengeCompletions: ISeasonChallenge[] | undefined +): IAffiliationMods[] => { + ChallengeProgress.forEach(({ Name, Progress }) => { + const itemIndex = inventory.ChallengeProgress.findIndex(i => i.Name === Name); if (itemIndex !== -1) { - category[itemIndex].Progress += Progress; + inventory.ChallengeProgress[itemIndex].Progress = Progress; } else { - category.push({ Name, Progress }); + inventory.ChallengeProgress.push({ Name, Progress }); } }); + + const affiliationMods: IAffiliationMods[] = []; + if (SeasonChallengeCompletions) { + for (const challenge of SeasonChallengeCompletions) { + // Ignore challenges that weren't completed just now + if (!ChallengeProgress.find(x => challenge.challenge.indexOf(x.Name) != -1)) { + continue; + } + + const meta = ExportChallenges[challenge.challenge]; + const nightwaveSyndicateTag = getNightwaveSyndicateTag(account.BuildLabel); + logger.debug("Completed season challenge", { + uniqueName: challenge.challenge, + syndicateTag: nightwaveSyndicateTag, + ...meta + }); + if (nightwaveSyndicateTag) { + let affiliation = inventory.Affiliations.find(x => x.Tag == nightwaveSyndicateTag); + if (!affiliation) { + affiliation = + inventory.Affiliations[ + inventory.Affiliations.push({ + Tag: nightwaveSyndicateTag, + Standing: 0 + }) - 1 + ]; + } + affiliation.Standing += meta.standing!; + + if (affiliationMods.length == 0) { + affiliationMods.push({ Tag: nightwaveSyndicateTag }); + } + affiliationMods[0].Standing ??= 0; + affiliationMods[0].Standing += meta.standing!; + } + } + } + return affiliationMods; }; export const addMissionComplete = (inventory: TInventoryDatabaseDocument, { Tag, Completes }: IMission): void => { @@ -1097,7 +1783,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; @@ -1112,7 +1798,7 @@ export const addBooster = (ItemType: string, time: number, inventory: TInventory }; export const updateSyndicate = ( - inventory: HydratedDocument, + inventory: TInventoryDatabaseDocument, syndicateUpdate: IMissionInventoryUpdateRequest["AffiliationChanges"] ): void => { syndicateUpdate?.forEach(affiliation => { @@ -1146,17 +1832,208 @@ 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); } - await addItems(inventory, nonStoreItems); - return inventoryChanges; }; + +export const createLibraryDailyTask = (): ILibraryDailyTaskInfo => { + const enemyTypes = getRandomElement(libraryDailyTasks)!; + const enemyAvatar = ExportEnemies.avatars[enemyTypes[0]]; + const scansRequired = getRandomInt(2, 4); + return { + 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 + }; +}; + +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" + }); +}; + +export const cleanupInventory = (inventory: TInventoryDatabaseDocument): void => { + let index = inventory.MiscItems.findIndex(x => x.ItemType == ""); + if (index != -1) { + inventory.MiscItems.splice(index, 1); + } + + index = inventory.Affiliations.findIndex(x => x.Tag == "KahlSyndicate"); + if (index != -1 && !inventory.Affiliations[index].WeeklyMissions) { + logger.debug(`KahlSyndicate seems broken, removing it and setting up again`); + inventory.Affiliations.splice(index, 1); + setupKahlSyndicate(inventory); + } + + const LibrarySyndicate = inventory.Affiliations.find(x => x.Tag == "LibrarySyndicate"); + if (LibrarySyndicate && LibrarySyndicate.FreeFavorsEarned) { + logger.debug(`removing FreeFavorsEarned from LibrarySyndicate`); + LibrarySyndicate.FreeFavorsEarned = undefined; + } + + if (inventory.LotusCustomization) { + if ( + Array.isArray(inventory.LotusCustomization.attcol) || + Array.isArray(inventory.LotusCustomization.sigcol) || + Array.isArray(inventory.LotusCustomization.eyecol) || + Array.isArray(inventory.LotusCustomization.facial) || + Array.isArray(inventory.LotusCustomization.cloth) || + Array.isArray(inventory.LotusCustomization.syancol) + ) { + logger.debug(`fixing empty objects represented as empty arrays in LotusCustomization`); + inventory.LotusCustomization.attcol = {}; + inventory.LotusCustomization.sigcol = {}; + inventory.LotusCustomization.eyecol = {}; + inventory.LotusCustomization.facial = {}; + inventory.LotusCustomization.cloth = {}; + inventory.LotusCustomization.syancol = {}; + } + } +}; + +export const getDialogue = (inventory: TInventoryDatabaseDocument, dialogueName: string): IDialogueDatabase => { + let dialogue = inventory.DialogueHistory!.Dialogues!.find(x => x.DialogueName == dialogueName); + if (!dialogue) { + dialogue = + inventory.DialogueHistory!.Dialogues![ + inventory.DialogueHistory!.Dialogues!.push({ + Rank: 0, + Chemistry: 0, + AvailableDate: new Date(0), + AvailableGiftDate: new Date(0), + RankUpExpiry: new Date(0), + BountyChemExpiry: new Date(0), + QueuedDialogues: [], + Gifts: [], + Booleans: [], + Completed: [], + DialogueName: dialogueName + }) - 1 + ]; + } + return dialogue; +}; + +export const getCalendarProgress = (inventory: TInventoryDatabaseDocument): ICalendarProgress => { + const currentSeason = getWorldState().KnownCalendarSeasons[0]; + + if (!inventory.CalendarProgress) { + inventory.CalendarProgress = { + Version: 19, + Iteration: currentSeason.YearIteration, + YearProgress: { + Upgrades: [] + }, + SeasonProgress: { + SeasonType: currentSeason.Season, + LastCompletedDayIdx: 0, + LastCompletedChallengeDayIdx: 0, + ActivatedChallenges: [] + } + }; + } + + const yearRolledOver = inventory.CalendarProgress.Iteration != currentSeason.YearIteration; + if (yearRolledOver) { + inventory.CalendarProgress.Iteration = currentSeason.YearIteration; + inventory.CalendarProgress.YearProgress.Upgrades = []; + } + if (yearRolledOver || inventory.CalendarProgress.SeasonProgress.SeasonType != currentSeason.Season) { + inventory.CalendarProgress.SeasonProgress.SeasonType = currentSeason.Season; + inventory.CalendarProgress.SeasonProgress.LastCompletedDayIdx = -1; + inventory.CalendarProgress.SeasonProgress.LastCompletedChallengeDayIdx = -1; + inventory.CalendarProgress.SeasonProgress.ActivatedChallenges = []; + } + + return inventory.CalendarProgress; +}; + +export const giveNemesisWeaponRecipe = ( + inventory: TInventoryDatabaseDocument, + weaponType: string, + nemesisName: string = "AGOR ROK", + weaponLoc?: string, + profile: INemesisProfile = generateNemesisProfile() +): void => { + if (!weaponLoc) { + weaponLoc = ExportWeapons[weaponType].name; + } + const recipeType = Object.entries(ExportRecipes).find(arr => arr[1].resultType == weaponType)![0]; + addRecipes(inventory, [ + { + ItemType: recipeType, + ItemCount: 1 + } + ]); + inventory.PendingRecipes.push({ + CompletionDate: new Date(), + ItemType: recipeType, + TargetFingerprint: JSON.stringify({ + ItemType: "/Lotus/Weapons/Grineer/KuvaLich/Upgrades/InnateDamageRandomMod", + UpgradeFingerprint: { + compat: weaponType, + buffs: [ + { + Tag: profile.innateDamageTag, + Value: profile.innateDamageValue + } + ] + }, + Name: weaponLoc + "|" + nemesisName + } satisfies INemesisWeaponTargetFingerprint) + }); +}; + +export const giveNemesisPetRecipe = ( + inventory: TInventoryDatabaseDocument, + nemesisName: string = "AGOR ROK", + profile: INemesisProfile = generateNemesisProfile() +): void => { + const head = profile.petHead!; + const body = profile.petBody!; + const legs = profile.petLegs!; + const tail = profile.petTail!; + const recipeType = Object.entries(ExportRecipes).find(arr => arr[1].resultType == head)![0]; + addRecipes(inventory, [ + { + ItemType: recipeType, + ItemCount: 1 + } + ]); + inventory.PendingRecipes.push({ + CompletionDate: new Date(), + ItemType: recipeType, + TargetFingerprint: JSON.stringify({ + Parts: [head, body, legs, tail], + Name: "/Lotus/Language/Pets/ZanukaPetName|" + nemesisName + } satisfies INemesisPetTargetFingerprint) + }); +}; + +export const getEffectiveAvatarImageType = (inventory: TInventoryDatabaseDocument): string => { + return inventory.ActiveAvatarImageType ?? "/Lotus/Types/StoreItems/AvatarImages/AvatarImageDefault"; +}; diff --git a/src/services/itemDataService.ts b/src/services/itemDataService.ts index d68ef666..3b3e997c 100644 --- a/src/services/itemDataService.ts +++ b/src/services/itemDataService.ts @@ -1,7 +1,5 @@ -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"; import { dict_de, dict_en, @@ -19,20 +17,23 @@ import { dict_uk, dict_zh, ExportArcanes, + ExportBoosters, 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 +53,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 +69,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 +76,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 +145,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,60 +159,99 @@ export const getKeyChainItems = ({ KeyChain, ChainStage }: IKeyChainRequest): st return keyChainStage.itemsToGiveWhenTriggered; }; -export const getLevelKeyRewards = (levelKey: string) => { - const levelKeyData = ExportKeys[levelKey]; - if (!levelKeyData) { - const error = `LevelKey ${levelKey} not found`; - logger.error(error); - throw new Error(error); +export const getLevelKeyRewards = ( + levelKey: string +): { levelKeyRewards?: IMissionReward; levelKeyRewards2?: TReward[] } => { + if (!(levelKey in ExportKeys)) { + throw new Error(`LevelKey ${levelKey} not found`); } - if (!levelKeyData.rewards) { - const error = `LevelKey ${levelKey} does not contain rewards`; - logger.error(error); - throw new Error(error); + 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`); } - return levelKeyData.rewards; + return { + levelKeyRewards, + levelKeyRewards2 + }; }; -export const getNode = (nodeName: string): IRegion => { - const node = ExportRegions[nodeName]; - if (!node) { - throw new Error(`Node ${nodeName} not found`); - } - - 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` - ); - } - 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]; - if (!keyChainStage) { - throw new Error(`KeyChainStage ${ChainStage} not found`); + let i = ChainStage; + let chainStageMessage = chainStages[i].messageToSendWhenTriggered; + while (!chainStageMessage) { + if (++i >= chainStages.length) { + break; + } + chainStageMessage = chainStages[i].messageToSendWhenTriggered; } - const chainStageMessage = keyChainStage.messageToSendWhenTriggered; - if (!chainStageMessage) { throw new Error( `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/") || type in ExportBoosters; +}; + +export const toStoreItem = (type: string): string => { + if (type.startsWith("/Lotus/Types/StoreItems/Boosters/")) { + const boosterEntry = Object.entries(ExportBoosters).find(arr => arr[1].typeName == type); + if (boosterEntry) { + return boosterEntry[0]; + } + throw new Error(`could not convert ${type} to a store item`); + } + return "/Lotus/StoreItems/" + type.substring("/Lotus/".length); +}; + +export const fromStoreItem = (type: string): string => { + if (type.startsWith("/Lotus/StoreItems/")) { + return "/Lotus/" + type.substring("/Lotus/StoreItems/".length); + } + + if (type in ExportBoosters) { + return ExportBoosters[type].typeName; + } + + throw new Error(`${type} is not a store item`); +}; + +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..2ef908f7 --- /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 | undefined +): 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 | undefined, + guildId: string | undefined, + guildTier: number | undefined +): 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..c8a213ea --- /dev/null +++ b/src/services/loginRewardService.ts @@ -0,0 +1,167 @@ +import randomRewards from "@/static/fixed_responses/loginRewards/randomRewards.json"; +import { IInventoryChanges } from "../types/purchaseTypes"; +import { TAccountDocument } from "./loginService"; +import { mixSeeds, SRng } from "./rngService"; +import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel"; +import { addBooster, updateCurrency } from "./inventoryService"; +import { handleStoreItemAcquisition } from "./purchaseService"; +import { + ExportBoosterPacks, + 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 SRng(mixSeeds(accountSeed, account.LoginDays)); + return rng.randomFloat() < 0.25; +}; + +// 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 SRng(mixSeeds(accountSeed, account.LoginDays)); + const pick_a_door = rng.randomFloat() < 0.25; + 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: SRng, day: number, inventory: TInventoryDatabaseDocument): ILoginReward => { + const reward = rng.randomReward(randomRewards)!; + //const reward = randomRewards.find(x => x.RewardType == "RT_BOOSTER")!; + let storeItemType: string = reward.StoreItemType; + if (reward.RewardType == "RT_RANDOM_RECIPE") { + 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 (!recipe.excludeFromMarket && unmasteredItems.has(recipe.resultType)) { + eligibleRecipes.push(uniqueName); + } + } + if (eligibleRecipes.length == 0) { + // This account has all applicable warframes and weapons already mastered (filthy cheater), need a different reward. + return getRandomLoginReward(rng, day, inventory); + } + storeItemType = toStoreItem(rng.randomElement(eligibleRecipes)!); + } else if (reward.StoreItemType == "/Lotus/StoreItems/Types/BoosterPacks/LoginRewardRandomProjection") { + storeItemType = toStoreItem( + rng.randomElement(ExportBoosterPacks["/Lotus/Types/BoosterPacks/LoginRewardRandomProjection"].components)! + .Item + ); + } + return { + //_id: toOid(new Types.ObjectId()), + RewardType: reward.RewardType, + //CouponType: "CPT_PLATINUM", + Icon: reward.Icon ?? "", + //ItemType: "", + StoreItemType: 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 0432c33b..ccf5b958 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(); @@ -47,65 +48,59 @@ export const createPersonalRooms = async (accountId: Types.ObjectId, shipId: Typ activeShipId: shipId }); if (config.skipTutorial) { - // // Vor's Prize rewards - // const defaultFeatures = [ - // "/Lotus/Types/Items/ShipFeatureItems/EarthNavigationFeatureItem", - // "/Lotus/Types/Items/ShipFeatureItems/MercuryNavigationFeatureItem", - // "/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem", - // "/Lotus/Types/Items/ShipFeatureItems/SocialMenuFeatureItem", - // "/Lotus/Types/Items/ShipFeatureItems/FoundryFeatureItem", - // "/Lotus/Types/Items/ShipFeatureItems/ModsFeatureItem" - // ]; - // personalRooms.Ship.Features.push(...defaultFeatures); + // unlocked during Vor's Prize + const defaultFeatures = [ + "/Lotus/Types/Items/ShipFeatureItems/MercuryNavigationFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/SocialMenuFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/FoundryFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/ModsFeatureItem" + ]; + personalRooms.Ship.Features.push(...defaultFeatures); } 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 as string); + 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?.indexOf(account.DisplayName) != -1; +}; + +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 0cf0a0c7..908be883 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -1,36 +1,117 @@ -import { ExportRegions, ExportRewards, IReward } from "warframe-public-export-plus"; +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, SRng, getRandomElement, getRandomReward } from "@/src/services/rngService"; +import { equipmentKeys, IMission, ITypeCount, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { + addBooster, addChallenges, addConsumables, addCrewShipAmmo, addCrewShipRawSalvage, + addEmailItem, addFocusXpIncreases, + addFusionPoints, addFusionTreasures, - addGearExpByCategory, addItem, + addLevelKeys, + addLoreFragmentScans, addMiscItems, addMissionComplete, addMods, addRecipes, + addShipDecorations, + addSkin, + addStanding, + applyClientEquipmentUpdates, combineInventoryChanges, + generateRewardSeed, + getCalendarProgress, + getDialogue, + giveNemesisPetRecipe, + giveNemesisWeaponRecipe, + updateCurrency, updateSyndicate } from "@/src/services/inventoryService"; import { updateQuestKey } from "@/src/services/questService"; -import { HydratedDocument } from "mongoose"; -import { IInventoryChanges } from "@/src/types/purchaseTypes"; -import { getLevelKeyRewards, getNode } from "@/src/services/itemDataService"; -import { InventoryDocumentProps, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; +import { Types } from "mongoose"; +import { IAffiliationMods, IInventoryChanges } from "@/src/types/purchaseTypes"; +import { fromStoreItem, 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 { IMissionCredits, 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 { + generateNemesisProfile, + getInfestedLichItemRewards, + getInfNodes, + getKillTokenRewardCount, + getNemesisManifest, + getNemesisPasscode +} from "@/src/helpers/nemesisHelpers"; +import { Loadout } from "../models/inventoryModels/loadoutModel"; +import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes"; +import { + getLiteSortie, + getSortie, + getWorldState, + idToBountyCycle, + idToDay, + idToWeek, + pushClassicBounties +} from "./worldStateService"; +import { config } from "./configService"; +import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; +import { ISyndicateMissionInfo } from "../types/worldStateTypes"; +import { fromOid } from "../helpers/inventoryHelpers"; +import { TAccountDocument } from "./loginService"; -const getRotations = (rotationCount: number): number[] => { - if (rotationCount === 0) return [0]; +const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] => { + // For Spy missions, e.g. 3 vaults cracked = A, B, C + if (rewardInfo.VaultsCracked) { + const rotations: number[] = []; + for (let i = 0; i != rewardInfo.VaultsCracked; ++i) { + rotations.push(i); + } + return rotations; + } - const rotationPattern = [0, 0, 1, 2]; // A, A, B, C + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const missionIndex: number | undefined = ExportRegions[rewardInfo.node]?.missionIndex; + + // For Rescue missions + if (missionIndex == 3 && rewardInfo.rewardTier) { + return [rewardInfo.rewardTier]; + } + + const rotationCount = rewardInfo.rewardQualifications?.length || 0; + + // Empty or absent rewardQualifications should not give rewards when: + // - Completing only 1 zone of (E)SO (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1823) + // - Aborting a railjack mission (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1741) + if (rotationCount == 0 && missionIndex != 30 && missionIndex != 32) { + return [0]; + } + + const rotationPattern = + tierOverride === undefined + ? [0, 0, 1, 2] // A, A, B, C + : [tierOverride]; const rotatedValues = []; for (let i = 0; i < rotationCount; i++) { @@ -40,60 +121,114 @@ const getRotations = (rotationCount: number): number[] => { return rotatedValues; }; -const getRandomRewardByChance = (pool: IReward[]): IRngResult | undefined => { +const getRandomRewardByChance = (pool: IReward[], rng?: SRng): IRngResult | undefined => { + if (rng) { + const res = rng.randomReward(pool as IRngResult[]); + rng.randomFloat(); // something related to rewards multiplier + return res; + } return getRandomReward(pool as IRngResult[]); }; -export const creditBundles: Record = { - "/Lotus/Types/PickUps/Credits/1500Credits": 1500, - "/Lotus/Types/PickUps/Credits/2000Credits": 2000, - "/Lotus/Types/PickUps/Credits/2500Credits": 2500, - "/Lotus/Types/PickUps/Credits/3000Credits": 3000, - "/Lotus/Types/PickUps/Credits/4000Credits": 4000, - "/Lotus/Types/PickUps/Credits/5000Credits": 5000, - "/Lotus/Types/PickUps/Credits/7500Credits": 7500, - "/Lotus/Types/PickUps/Credits/10000Credits": 10000, - "/Lotus/Types/PickUps/Credits/5000Hollars": 5000, - "/Lotus/Types/PickUps/Credits/7500Hollars": 7500, - "/Lotus/Types/PickUps/Credits/10000Hollars": 10000, - "/Lotus/Types/PickUps/Credits/CorpusArenaCreditRewards/CorpusArenaRewardOneHard": 105000, - "/Lotus/Types/PickUps/Credits/CorpusArenaCreditRewards/CorpusArenaRewardTwoHard": 175000, - "/Lotus/Types/PickUps/Credits/CorpusArenaCreditRewards/CorpusArenaRewardThreeHard": 250000, - "/Lotus/Types/StoreItems/CreditBundles/Zariman/TableACreditsCommon": 15000, - "/Lotus/Types/StoreItems/CreditBundles/Zariman/TableACreditsUncommon": 30000 -}; - -export const fusionBundles: Record = { - "/Lotus/Upgrades/Mods/FusionBundles/CommonFusionBundle": 15, - "/Lotus/Upgrades/Mods/FusionBundles/UncommonFusionBundle": 50, - "/Lotus/Upgrades/Mods/FusionBundles/RareFusionBundle": 80 -}; - //type TMissionInventoryUpdateKeys = keyof IMissionInventoryUpdateRequest; //const ignoredInventoryUpdateKeys = ["FpsAvg", "FpsMax", "FpsMin", "FpsSamples"] satisfies TMissionInventoryUpdateKeys[]; // for keys with no meaning for this server //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 ( + account: TAccountDocument, + inventory: TInventoryDatabaseDocument, inventoryUpdates: IMissionInventoryUpdateRequest -) => { - //TODO: type this properly - const inventoryChanges: Partial = {}; - if (inventoryUpdates.MissionFailed === true) { - return; +): Promise => { + const inventoryChanges: IInventoryChanges = {}; + if (inventoryUpdates.EndOfMatchUpload) { + if (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.KeyToRemove) { + if (!inventoryUpdates.KeyOwner || inventory.accountOwnerId.equals(inventoryUpdates.KeyOwner)) { + addLevelKeys(inventory, [ + { + ItemType: inventoryUpdates.KeyToRemove, + 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.RewardInfo.NemesisHenchmenKills && inventory.Nemesis) { + inventory.Nemesis.HenchmenKilled += inventoryUpdates.RewardInfo.NemesisHenchmenKills; + } + if (inventoryUpdates.RewardInfo.NemesisHintProgress && inventory.Nemesis) { + inventory.Nemesis.HintProgress += inventoryUpdates.RewardInfo.NemesisHintProgress; + if (inventory.Nemesis.Faction != "FC_INFESTATION" && inventory.Nemesis.Hints.length != 3) { + const progressNeeded = [35, 60, 100][inventory.Nemesis.Hints.length]; + if (inventory.Nemesis.HintProgress >= progressNeeded) { + inventory.Nemesis.HintProgress -= progressNeeded; + const passcode = getNemesisPasscode(inventory.Nemesis); + inventory.Nemesis.Hints.push(passcode[inventory.Nemesis.Hints.length]); + } + } + } + if (inventoryUpdates.MissionStatus == "GS_SUCCESS" && inventoryUpdates.RewardInfo.jobId) { + // e.g. for Profit-Taker Phase 1: + // JobTier: -6, + // jobId: '/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyOne_-6_SolarisUnitedHub1_663a71c80000000000000025_EudicoHeists', + // This is sent multiple times, with JobStage starting at 0 and incrementing each time, but only the final upload has GS_SUCCESS. + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [bounty, tier, hub, id, tag] = inventoryUpdates.RewardInfo.jobId.split("_"); + if (tag == "EudicoHeists") { + inventory.CompletedJobChains ??= []; + let chain = inventory.CompletedJobChains.find(x => x.LocationTag == tag); + if (!chain) { + chain = + inventory.CompletedJobChains[ + inventory.CompletedJobChains.push({ LocationTag: tag, Jobs: [] }) - 1 + ]; + } + if (!chain.Jobs.includes(bounty)) { + chain.Jobs.push(bounty); + if (bounty == "/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyThree") { + await createMessage(inventory.accountOwnerId, [ + { + sub: "/Lotus/Language/SolarisHeists/HeavyCatalystInboxTitle", + sndr: "/Lotus/Language/Bosses/Ordis", + msg: "/Lotus/Language/SolarisHeists/HeavyCatalystInboxMessage", + icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", + att: ["/Lotus/Types/Restoratives/HeavyWeaponSummon"], + highPriority: true + } + ]); + await addItem(inventory, "/Lotus/Types/Items/MiscItems/HeavyWeaponCatalyst", 1); + } + } + } } } for (const [key, value] of getEntriesUnsafe(inventoryUpdates)) { @@ -106,7 +241,7 @@ export const addMissionInventoryUpdates = ( inventory.RegularCredits += value; break; case "QuestKeys": - updateQuestKey(inventory, value); + await updateQuestKey(inventory, value); break; case "AffiliationChanges": updateSyndicate(inventory, value); @@ -141,13 +276,20 @@ export const addMissionInventoryUpdates = ( addMiscItems(inventory, value); break; case "Consumables": - addConsumables(inventory, value); + if (config.dontSubtractConsumables) { + addConsumables( + inventory, + value.filter(x => x.ItemCount > 0) + ); + } else { + addConsumables(inventory, value); + } break; case "Recipes": addRecipes(inventory, value); break; case "ChallengeProgress": - addChallenges(inventory, value); + addChallenges(account, inventory, value, inventoryUpdates.SeasonChallengeCompletions); break; case "FusionTreasures": addFusionTreasures(inventory, value); @@ -158,14 +300,25 @@ 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; + let fusionPointsDelta = 0; for (const fusionBundle of value) { - const fusionPointsTotal = fusionBundles[fusionBundle.ItemType] * fusionBundle.ItemCount; - inventory.FusionPoints += fusionPointsTotal; - fusionPoints += fusionPointsTotal; + fusionPointsDelta += addFusionPoints( + inventory, + ExportFusionBundles[fusionBundle.ItemType].fusionPoints * fusionBundle.ItemCount + ); + } + inventoryChanges.FusionPoints = fusionPointsDelta; + break; + } + case "EmailItems": { + for (const tc of value) { + await addEmailItem(inventory, tc.ItemType); } - inventoryChanges.FusionPoints = fusionPoints; break; } case "FocusXpIncreases": { @@ -191,26 +344,421 @@ export const addMissionInventoryUpdates = ( }); break; } + case "LoreFragmentScans": + addLoreFragmentScans(inventory, value); + break; + case "LibraryScans": + value.forEach(scan => { + let synthesisIgnored = true; + if (inventory.LibraryPersonalTarget) { + const taskAvatar = libraryPersonalTargetToAvatar[inventory.LibraryPersonalTarget]; + const taskAvatars = libraryDailyTasks.find(x => x.indexOf(taskAvatar) != -1)!; + if (taskAvatars.indexOf(scan.EnemyType) != -1) { + 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; + inventory.LibraryPersonalTarget = undefined; + } + logger.debug(`synthesis of ${scan.EnemyType} added to personal target progress`); + synthesisIgnored = false; + } + } + if ( + inventory.LibraryActiveDailyTaskInfo && + inventory.LibraryActiveDailyTaskInfo.EnemyTypes.indexOf(scan.EnemyType) != -1 + ) { + 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 id = fromOid(clientUpgrade.ItemId); + if (id == "") { + // U19 does not provide RawUpgrades and instead interleaves them with riven progress here + addMods(inventory, [clientUpgrade]); + } else { + const upgrade = inventory.Upgrades.id(id)!; + upgrade.UpgradeFingerprint = clientUpgrade.UpgradeFingerprint; // primitive way to copy over the riven challenge progress + } + }); + break; + case "WeaponSkins": + for (const item of value) { + addSkin(inventory, item.ItemType); + } + break; + case "Boosters": + value.forEach(booster => { + addBooster(booster.ItemType, booster.ExpiryDate, inventory); + }); + break; case "SyndicateId": { - inventory.CompletedSyndicates.push(value); + if (!config.syndicateMissionsRepeatable) { + inventory.CompletedSyndicates.push(value); + } break; } case "SortieId": { - inventory.CompletedSorties.push(value); + if (inventory.CompletedSorties.indexOf(value) == -1) { + inventory.CompletedSorties.push(value); + } break; } - case "SeasonChallengeCompletions": + case "SeasonChallengeCompletions": { const processedCompletions = value.map(({ challenge, id }) => ({ challenge: challenge.substring(challenge.lastIndexOf("/") + 1), id })); - inventory.SeasonChallengeHistory.push(...processedCompletions); break; + } + case "DeathMarks": { + if (!config.noDeathMarks) { + for (const bossName of value) { + if (inventory.DeathMarks.indexOf(bossName) == -1) { + // It's a new death mark; we have to say the line. + await createMessage(inventory.accountOwnerId, [ + { + sub: bossName, + sndr: "/Lotus/Language/G1Quests/DeathMarkSender", + msg: "/Lotus/Language/G1Quests/DeathMarkMessage", + icon: "/Lotus/Interface/Icons/Npcs/Stalker_d.png", + highPriority: true, + expiry: new Date(Date.now() + 86400_000) // TOVERIFY: This type of inbox message seems to automatically delete itself. We'll just delete it after 24 hours, but it's clear if this is correct. + } + ]); + } + } + 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 "KubrowPetEggs": { + for (const egg of value) { + inventory.KubrowPetEggs ??= []; + inventory.KubrowPetEggs.push({ + ItemType: egg.ItemType, + _id: new Types.ObjectId() + }); + } + 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 "BrandedSuits": { + inventory.BrandedSuits ??= []; + if (!inventory.BrandedSuits.find(x => x.equals(value.$oid))) { + inventory.BrandedSuits.push(new Types.ObjectId(value.$oid)); + + 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. + } + ]); + } + 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 + }; + inventory.Harvestable = false; + break; + } + case "UnlockWeapons": { + inventory.LockedWeaponGroup = undefined; + break; + } + case "IncHarvester": { + inventory.Harvestable = true; + 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; + } + case "InvasionProgress": { + for (const clientProgress of value) { + const dbProgress = inventory.QualifyingInvasions.find(x => + x.invasionId.equals(clientProgress._id.$oid) + ); + if (dbProgress) { + dbProgress.Delta += clientProgress.Delta; + dbProgress.AttackerScore += clientProgress.AttackerScore; + dbProgress.DefenderScore += clientProgress.DefenderScore; + } else { + inventory.QualifyingInvasions.push({ + invasionId: new Types.ObjectId(clientProgress._id.$oid), + Delta: clientProgress.Delta, + AttackerScore: clientProgress.AttackerScore, + DefenderScore: clientProgress.DefenderScore + }); + } + } + break; + } + case "CalendarProgress": { + const calendarProgress = getCalendarProgress(inventory); + for (const progress of value) { + const challengeName = progress.challenge.substring(progress.challenge.lastIndexOf("/") + 1); + calendarProgress.SeasonProgress.LastCompletedChallengeDayIdx++; + calendarProgress.SeasonProgress.ActivatedChallenges.push(challengeName); + } + break; + } + case "duviriCaveOffers": { + // Duviri cave offers (generated with the duviri seed) change after completing one of its game modes (not when aborting). + if (inventoryUpdates.MissionStatus != "GS_QUIT") { + inventory.DuviriInfo!.Seed = generateRewardSeed(); + } + break; + } + case "NemesisKillConvert": + if (inventory.Nemesis) { + inventory.NemesisHistory ??= []; + inventory.NemesisHistory.push({ + // Copy over all 'base' values + fp: inventory.Nemesis.fp, + d: inventory.Nemesis.d, + manifest: inventory.Nemesis.manifest, + KillingSuit: inventory.Nemesis.KillingSuit, + killingDamageType: inventory.Nemesis.killingDamageType, + ShoulderHelmet: inventory.Nemesis.ShoulderHelmet, + WeaponIdx: inventory.Nemesis.WeaponIdx, + AgentIdx: inventory.Nemesis.AgentIdx, + BirthNode: inventory.Nemesis.BirthNode, + Faction: inventory.Nemesis.Faction, + Rank: inventory.Nemesis.Rank, + Traded: inventory.Nemesis.Traded, + PrevOwners: inventory.Nemesis.PrevOwners, + SecondInCommand: false, + Weakened: inventory.Nemesis.Weakened, + // And set killed flag + k: value.killed + }); + + const manifest = getNemesisManifest(inventory.Nemesis.manifest); + const profile = generateNemesisProfile( + inventory.Nemesis.fp, + manifest, + inventory.Nemesis.KillingSuit + ); + const att: string[] = []; + let countedAtt: ITypeCount[] | undefined; + + if (value.killed) { + if ( + value.weaponLoc && + inventory.Nemesis.Faction != "FC_INFESTATION" // weaponLoc is "/Lotus/Language/Weapons/DerelictCernosName" for these for some reason + ) { + const weaponType = manifest.weapons[inventory.Nemesis.WeaponIdx]; + giveNemesisWeaponRecipe(inventory, weaponType, value.nemesisName, value.weaponLoc, profile); + att.push(weaponType); + } + //if (value.petLoc) { + if (profile.petHead) { + giveNemesisPetRecipe(inventory, value.nemesisName, profile); + att.push( + { + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA": + "/Lotus/Types/Recipes/ZanukaPet/ZanukaPetCompleteHeadABlueprint", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB": + "/Lotus/Types/Recipes/ZanukaPet/ZanukaPetCompleteHeadBBlueprint", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC": + "/Lotus/Types/Recipes/ZanukaPet/ZanukaPetCompleteHeadCBlueprint" + }[profile.petHead] + ); + } + } + + // "Players will receive a Lich's Ephemera regardless of whether they Vanquish or Convert them." + if (profile.ephemera) { + addSkin(inventory, profile.ephemera); + att.push(profile.ephemera); + } + + const skinRewardStoreItem = value.killed ? manifest.firstKillReward : manifest.firstConvertReward; + if (Object.keys(addSkin(inventory, fromStoreItem(skinRewardStoreItem))).length != 0) { + att.push(skinRewardStoreItem); + } + + if (inventory.Nemesis.Faction == "FC_INFESTATION") { + const [rotARewardStoreItem, rotBRewardStoreItem] = getInfestedLichItemRewards( + inventory.Nemesis.fp + ); + const rotAReward = fromStoreItem(rotARewardStoreItem); + const rotBReward = fromStoreItem(rotBRewardStoreItem); + await addItem(inventory, rotAReward); + await addItem(inventory, rotBReward); + att.push(rotAReward); + att.push(rotBReward); + + if (value.killed) { + countedAtt = [ + { + ItemType: "/Lotus/Types/Items/MiscItems/CodaWeaponBucks", + ItemCount: getKillTokenRewardCount(inventory.Nemesis.fp) + } + ]; + addMiscItems(inventory, countedAtt); + } + } + + if (value.killed) { + await createMessage(inventory.accountOwnerId, [ + { + sndr: "/Lotus/Language/Bosses/Ordis", + msg: manifest.messageBody, + arg: [ + { + Key: "LICH_NAME", + Tag: value.nemesisName + } + ], + att: att, + countedAtt: countedAtt, + attVisualOnly: true, + sub: manifest.messageTitle, + icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", + highPriority: true + } + ]); + } + + inventory.Nemesis = undefined; + } + break; default: - // Equipment XP updates if (equipmentKeys.includes(key as TEquipmentKey)) { - addGearExpByCategory(inventory, value as IEquipmentClient[], key as TEquipmentKey); + applyClientEquipmentUpdates(inventory, value as IEquipmentClient[], key as TEquipmentKey); } break; // if ( @@ -226,16 +774,168 @@ export const addMissionInventoryUpdates = ( return inventoryChanges; }; +interface AddMissionRewardsReturnType { + MissionRewards: IMissionReward[]; + inventoryChanges?: IInventoryChanges; + credits?: IMissionCredits; + AffiliationMods?: IAffiliationMods[]; + SyndicateXPItemReward?: number; + ConquestCompletedMissionsCount?: number; +} + +interface IConquestReward { + at: number; + pool: IRngResult[]; +} + +const labConquestRewards: IConquestReward[] = [ + { + at: 5, + pool: ExportRewards[ + "/Lotus/Types/Game/MissionDecks/EntratiLabConquestRewards/EntratiLabConquestSilverRewards" + ][0] as IRngResult[] + }, + { + at: 10, + pool: ExportRewards[ + "/Lotus/Types/Game/MissionDecks/EntratiLabConquestRewards/EntratiLabConquestSilverRewards" + ][0] as IRngResult[] + }, + { + at: 15, + pool: [ + { + type: "/Lotus/StoreItems/Types/Gameplay/EntratiLab/Resources/EntratiLanthornBundle", + itemCount: 3, + probability: 1 + } + ] + }, + { + at: 20, + pool: ExportRewards[ + "/Lotus/Types/Game/MissionDecks/EntratiLabConquestRewards/EntratiLabConquestGoldRewards" + ][0] as IRngResult[] + }, + { + at: 28, + pool: [ + { + type: "/Lotus/StoreItems/Types/Items/MiscItems/DistillPoints", + itemCount: 20, + probability: 1 + } + ] + }, + { + at: 31, + pool: ExportRewards[ + "/Lotus/Types/Game/MissionDecks/EntratiLabConquestRewards/EntratiLabConquestGoldRewards" + ][0] as IRngResult[] + }, + { + at: 34, + pool: ExportRewards[ + "/Lotus/Types/Game/MissionDecks/EntratiLabConquestRewards/EntratiLabConquestArcaneRewards" + ][0] as IRngResult[] + }, + { + at: 37, + pool: [ + { + type: "/Lotus/StoreItems/Types/Items/MiscItems/DistillPoints", + itemCount: 50, + probability: 1 + } + ] + } +]; + +const hexConquestRewards: IConquestReward[] = [ + { + at: 5, + pool: ExportRewards[ + "/Lotus/Types/Game/MissionDecks/1999ConquestRewards/1999ConquestSilverRewards" + ][0] as IRngResult[] + }, + { + at: 10, + pool: ExportRewards[ + "/Lotus/Types/Game/MissionDecks/1999ConquestRewards/1999ConquestSilverRewards" + ][0] as IRngResult[] + }, + { + at: 15, + pool: [ + { + type: "/Lotus/StoreItems/Types/BoosterPacks/1999StickersPackEchoesArchimedea", + itemCount: 1, + probability: 1 + } + ] + }, + { + at: 20, + pool: ExportRewards[ + "/Lotus/Types/Game/MissionDecks/1999ConquestRewards/1999ConquestGoldRewards" + ][0] as IRngResult[] + }, + { + at: 28, + pool: [ + { + type: "/Lotus/StoreItems/Types/Items/MiscItems/1999ConquestBucks", + itemCount: 6, + probability: 1 + } + ] + }, + { + at: 31, + pool: ExportRewards[ + "/Lotus/Types/Game/MissionDecks/1999ConquestRewards/1999ConquestGoldRewards" + ][0] as IRngResult[] + }, + { + at: 34, + pool: ExportRewards[ + "/Lotus/Types/Game/MissionDecks/1999ConquestRewards/1999ConquestArcaneRewards" + ][0] as IRngResult[] + }, + { + at: 37, + pool: [ + { + type: "/Lotus/StoreItems/Types/Items/MiscItems/1999ConquestBucks", + itemCount: 9, + probability: 1 + } + ] + } +]; + +const droptableAliases: Record = { + "/Lotus/Types/DropTables/ManInTheWall/MITWGruzzlingArcanesDropTable": + "/Lotus/Types/DropTables/EntratiLabDropTables/DoppelgangerDropTable", + "/Lotus/Types/DropTables/WF1999DropTables/LasrianTankSteelPathDropTable": + "/Lotus/Types/DropTables/WF1999DropTables/LasrianTankHardModeDropTable" +}; + //TODO: return type of partial missioninventoryupdate response export const addMissionRewards = async ( inventory: TInventoryDatabaseDocument, { + wagerTier: wagerTier, + Nemesis: nemesis, RewardInfo: rewardInfo, LevelKeyName: levelKeyName, Missions: missions, - RegularCredits: creditDrops - }: IMissionInventoryUpdateRequest -) => { + RegularCredits: creditDrops, + VoidTearParticipantsCurrWave: voidTearWave, + StrippedItems: strippedItems + }: IMissionInventoryUpdateRequest, + firstCompletion: boolean +): 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 `); @@ -243,76 +943,399 @@ export const addMissionRewards = async ( } //TODO: check double reward merging - const MissionRewards = getRandomMissionDrops(rewardInfo).map(drop => { - return { StoreItem: drop.type, ItemCount: drop.itemCount }; - }); + const MissionRewards: IMissionReward[] = getRandomMissionDrops( + inventory, + rewardInfo, + missions, + wagerTier, + firstCompletion + ); logger.debug("random mission drops:", MissionRewards); const inventoryChanges: IInventoryChanges = {}; + const AffiliationMods: IAffiliationMods[] = []; + let SyndicateXPItemReward; + let ConquestCompletedMissionsCount; let missionCompletionCredits = 0; //inventory change is what the client has not rewarded itself, also the client needs to know the credit changes for display if (levelKeyName) { const fixedLevelRewards = getLevelKeyRewards(levelKeyName); //logger.debug(`fixedLevelRewards ${fixedLevelRewards}`); - for (const reward of fixedLevelRewards) { - //quest stage completion credit rewards - if (reward.rewardType == "RT_CREDITS") { - inventory.RegularCredits += reward.amount; - missionCompletionCredits += reward.amount; - continue; + if (fixedLevelRewards.levelKeyRewards) { + addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, inventory, MissionRewards, rewardInfo); + } + if (fixedLevelRewards.levelKeyRewards2) { + for (const reward of fixedLevelRewards.levelKeyRewards2) { + //quest stage completion credit rewards + if (reward.rewardType == "RT_CREDITS") { + missionCompletionCredits += reward.amount; // will be added to inventory in addCredits + continue; + } + MissionRewards.push({ + StoreItem: reward.itemType, + ItemCount: reward.rewardType === "RT_RESOURCE" ? reward.amount : 1 + }); } - MissionRewards.push({ - StoreItem: reward.itemType, - ItemCount: reward.rewardType === "RT_RESOURCE" ? reward.amount : 1 - }); } } + // 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 != 23 && // junction + node.missionIndex != 28 && // open world + missions.Tag != "SolNode761" && // the index + missions.Tag != "SolNode762" && // the index + missions.Tag != "SolNode763" && // the index + missions.Tag != "CrewBattleNode556" && // free flight + getRotations(rewardInfo).length > 0 // (E)SO should not give credits for only completing zone 1, in which case it has no rewardQualifications (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1823) + ) { + const levelCreditReward = getLevelCreditRewards(node); + missionCompletionCredits += levelCreditReward; + inventory.RegularCredits += levelCreditReward; + logger.debug(`levelCreditReward ${levelCreditReward}`); + } + + if (node.missionReward) { + missionCompletionCredits += addFixedLevelRewards(node.missionReward, inventory, MissionRewards, rewardInfo); + } + + if (rewardInfo.sortieTag == "Mission1") { + missionCompletionCredits += 20_000; + } else if (rewardInfo.sortieTag == "Mission2") { + missionCompletionCredits += 30_000; + } else if (rewardInfo.sortieTag == "Final") { + missionCompletionCredits += 50_000; + } + + if (missions.Tag == "PlutoToErisJunction") { + await createMessage(inventory.accountOwnerId, [ + { + sndr: "/Lotus/Language/G1Quests/GolemQuestJordasName", + msg: "/Lotus/Language/G1Quests/GolemQuestIntroBody", + att: ["/Lotus/Types/Keys/GolemQuest/GolemQuestKeyChainItem"], + sub: "/Lotus/Language/G1Quests/GolemQuestIntroTitle", + icon: "/Lotus/Interface/Icons/Npcs/JordasPortrait.png", + highPriority: true + } + ]); + } + } + + if (rewardInfo.useVaultManifest) { + MissionRewards.push({ + StoreItem: getRandomElement(corruptedMods)!, + ItemCount: 1 + }); + } + + if (rewardInfo.periodicMissionTag == "EliteAlert" || rewardInfo.periodicMissionTag == "EliteAlertB") { + missionCompletionCredits += 50_000; + MissionRewards.push({ + StoreItem: "/Lotus/StoreItems/Types/Items/MiscItems/Elitium", + ItemCount: 1 + }); + } + + if (rewardInfo.ConquestCompleted !== undefined) { + let score = 1; + if (rewardInfo.ConquestHardModeActive === 1) score += 3; + + if (rewardInfo.ConquestPersonalModifiersActive !== undefined) + score += rewardInfo.ConquestPersonalModifiersActive; + if (rewardInfo.ConquestEquipmentSuggestionsFulfilled !== undefined) + score += rewardInfo.ConquestEquipmentSuggestionsFulfilled; + + score *= rewardInfo.ConquestCompleted + 1; + + if (rewardInfo.ConquestCompleted == 2 && rewardInfo.ConquestHardModeActive === 1) score += 1; + + logger.debug(`completed conquest mission ${rewardInfo.ConquestCompleted + 1} for a score of ${score}`); + + const conquestType = rewardInfo.ConquestType; + const conquestNode = + conquestType == "HexConquest" ? "EchoesHexConquestHardModeUnlocked" : "EntratiLabConquestHardModeUnlocked"; + if (score >= 25 && inventory.NodeIntrosCompleted.indexOf(conquestNode) == -1) + inventory.NodeIntrosCompleted.push(conquestNode); + + if (conquestType == "HexConquest") { + inventory.EchoesHexConquestCacheScoreMission ??= 0; + if (score > inventory.EchoesHexConquestCacheScoreMission) { + for (const reward of hexConquestRewards) { + if (score >= reward.at && inventory.EchoesHexConquestCacheScoreMission < reward.at) { + const rolled = getRandomReward(reward.pool)!; + logger.debug(`rolled hex conquest reward for reaching ${reward.at} points`, rolled); + MissionRewards.push({ + StoreItem: rolled.type, + ItemCount: rolled.itemCount + }); + } + } + inventory.EchoesHexConquestCacheScoreMission = score; + } + } else { + inventory.EntratiLabConquestCacheScoreMission ??= 0; + if (score > inventory.EntratiLabConquestCacheScoreMission) { + for (const reward of labConquestRewards) { + if (score >= reward.at && inventory.EntratiLabConquestCacheScoreMission < reward.at) { + const rolled = getRandomReward(reward.pool)!; + logger.debug(`rolled lab conquest reward for reaching ${reward.at} points`, rolled); + MissionRewards.push({ + StoreItem: rolled.type, + ItemCount: rolled.itemCount + }); + } + } + inventory.EntratiLabConquestCacheScoreMission = score; + } + } + + ConquestCompletedMissionsCount = rewardInfo.ConquestCompleted == 2 ? 0 : rewardInfo.ConquestCompleted + 1; + } + for (const reward of MissionRewards) { - //TODO: additem should take in storeItems - const inventoryChange = await addItem(inventory, reward.StoreItem.replace("StoreItems/", ""), 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? combineInventoryChanges(inventoryChanges, inventoryChange.InventoryChanges); } - //node based credit rewards for mission completion - if (missions) { - const node = getNode(missions.Tag); - - if (node.missionIndex !== 28) { - const levelCreditReward = getLevelCreditRewards(missions?.Tag); - missionCompletionCredits += levelCreditReward; - inventory.RegularCredits += levelCreditReward; - logger.debug(`levelCreditReward ${levelCreditReward}`); - } - } - - //creditBonus is not correct for mirage mission 3 const credits = addCredits(inventory, { missionCompletionCredits, missionDropCredits: creditDrops ?? 0, rngRewardCredits: inventoryChanges.RegularCredits ?? 0 }); - return { inventoryChanges, MissionRewards, credits }; + if ( + voidTearWave && + voidTearWave.Participants[0].QualifiesForReward && + !voidTearWave.Participants[0].HaveRewardResponse + ) { + const reward = await crackRelic(inventory, voidTearWave.Participants[0], inventoryChanges); + MissionRewards.push({ StoreItem: reward.type, ItemCount: reward.itemCount }); + } + + if (strippedItems) { + for (const si of strippedItems) { + if (si.DropTable in droptableAliases) { + logger.debug(`rewriting ${si.DropTable} to ${droptableAliases[si.DropTable]}`); + si.DropTable = droptableAliases[si.DropTable]; + } + const droptables = ExportEnemies.droptables[si.DropTable] ?? []; + if (si.DROP_MOD) { + const modDroptable = droptables.find(x => x.type == "mod"); + if (modDroptable) { + for (let i = 0; i != si.DROP_MOD.length; ++i) { + const reward = getRandomReward(modDroptable.items)!; + logger.debug(`stripped droptable (mods pool) rolled`, reward); + await addItem(inventory, reward.type); + MissionRewards.push({ + StoreItem: toStoreItem(reward.type), + ItemCount: 1, + FromEnemyCache: true // to show "identified" + }); + } + } else { + logger.error(`unknown droptable ${si.DropTable} for DROP_MOD`); + } + } + if (si.DROP_BLUEPRINT) { + const blueprintDroptable = droptables.find(x => x.type == "blueprint"); + if (blueprintDroptable) { + for (let i = 0; i != si.DROP_BLUEPRINT.length; ++i) { + const reward = getRandomReward(blueprintDroptable.items)!; + logger.debug(`stripped droptable (blueprints pool) rolled`, reward); + await addItem(inventory, reward.type); + MissionRewards.push({ + StoreItem: toStoreItem(reward.type), + ItemCount: 1, + FromEnemyCache: true // to show "identified" + }); + } + } else { + logger.error(`unknown droptable ${si.DropTable} for DROP_BLUEPRINT`); + } + } + } + } + + 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( + getNemesisManifest(inventory.Nemesis.manifest), + inventory.Nemesis.Rank + ); + } + + if (inventory.Nemesis.Faction == "FC_INFESTATION") { + inventory.Nemesis.MissionCount += 1; + + inventoryChanges.Nemesis.MissionCount ??= 0; + inventoryChanges.Nemesis.MissionCount += 1; + } + + inventoryChanges.Nemesis.InfNodes = inventory.Nemesis.InfNodes; + } + } + + if (rewardInfo.JobStage != undefined && rewardInfo.jobId) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [jobType, unkIndex, hubNode, syndicateMissionId, locationTag] = rewardInfo.jobId.split("_"); + const syndicateMissions: ISyndicateMissionInfo[] = []; + pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId)); + const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId); + if (syndicateEntry && syndicateEntry.Jobs) { + let currentJob = syndicateEntry.Jobs[rewardInfo.JobTier!]; + if (syndicateEntry.Tag === "EntratiSyndicate") { + const vault = syndicateEntry.Jobs.find(j => j.locationTag === locationTag); + if (vault) currentJob = vault; + let medallionAmount = Math.floor(currentJob.xpAmounts[rewardInfo.JobStage] / (rewardInfo.Q ? 0.8 : 1)); + + if ( + ["DeimosEndlessAreaDefenseBounty", "DeimosEndlessExcavateBounty", "DeimosEndlessPurifyBounty"].some( + ending => jobType.endsWith(ending) + ) + ) { + const endlessJob = syndicateEntry.Jobs.find(j => j.endless); + if (endlessJob) { + const index = rewardInfo.JobStage % endlessJob.xpAmounts.length; + const excess = Math.floor(rewardInfo.JobStage / (endlessJob.xpAmounts.length - 1)); + medallionAmount = Math.floor(endlessJob.xpAmounts[index] * (1 + 0.15000001 * excess)); + } + } + await addItem(inventory, "/Lotus/Types/Items/Deimos/EntratiFragmentUncommonB", medallionAmount); + MissionRewards.push({ + StoreItem: "/Lotus/StoreItems/Types/Items/Deimos/EntratiFragmentUncommonB", + ItemCount: medallionAmount + }); + SyndicateXPItemReward = medallionAmount; + } else { + if (rewardInfo.JobTier! >= 0) { + AffiliationMods.push( + addStanding( + inventory, + syndicateEntry.Tag, + Math.floor(currentJob.xpAmounts[rewardInfo.JobStage] / (rewardInfo.Q ? 0.8 : 1)) + ) + ); + } else { + if (jobType.endsWith("Heists/HeistProfitTakerBountyOne") && rewardInfo.JobStage === 2) { + AffiliationMods.push(addStanding(inventory, syndicateEntry.Tag, 1000)); + } + if (jobType.endsWith("Hunts/AllTeralystsHunt") && rewardInfo.JobStage === 2) { + AffiliationMods.push(addStanding(inventory, syndicateEntry.Tag, 5000)); + } + if ( + [ + "Hunts/TeralystHunt", + "Heists/HeistProfitTakerBountyTwo", + "Heists/HeistProfitTakerBountyThree", + "Heists/HeistProfitTakerBountyFour", + "Heists/HeistExploiterBountyOne" + ].some(ending => jobType.endsWith(ending)) + ) { + AffiliationMods.push(addStanding(inventory, syndicateEntry.Tag, 1000)); + } + } + } + } + } + + if (rewardInfo.challengeMissionId) { + const [syndicateTag, tierStr, chemistryStr] = rewardInfo.challengeMissionId.split("_"); + const tier = Number(tierStr); + const chemistry = Number(chemistryStr); + const isSteelPath = missions?.Tier; + if (syndicateTag === "ZarimanSyndicate") { + let medallionAmount = tier + 1; + if (isSteelPath) medallionAmount = Math.round(medallionAmount * 1.5); + await addItem(inventory, "/Lotus/Types/Gameplay/Zariman/Resources/ZarimanDogTagBounty", medallionAmount); + MissionRewards.push({ + StoreItem: "/Lotus/StoreItems/Types/Gameplay/Zariman/Resources/ZarimanDogTagBounty", + ItemCount: medallionAmount + }); + SyndicateXPItemReward = medallionAmount; + } else { + let standingAmount = (tier + 1) * 1000; + if (tier > 5) standingAmount = 7500; // InfestedLichBounty + if (isSteelPath) standingAmount *= 1.5; + AffiliationMods.push(addStanding(inventory, syndicateTag, standingAmount)); + } + if (syndicateTag == "HexSyndicate" && chemistry && tier < 6) { + const seed = getWorldState().SyndicateMissions.find(x => x.Tag == "HexSyndicate")!.Seed; + const { nodes, buddies } = getHexBounties(seed); + const buddy = buddies[tier]; + logger.debug(`Hex seed is ${seed}, giving chemistry for ${buddy}`); + if (missions?.Tag != nodes[tier]) { + logger.warn( + `Uh-oh, tier ${tier} bounty should've been on ${nodes[tier]} but you were just on ${missions?.Tag}` + ); + } + const tomorrowAt0Utc = config.noKimCooldowns + ? Date.now() + : (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000; + const dialogue = getDialogue(inventory, buddy); + dialogue.Chemistry += chemistry; + dialogue.BountyChemExpiry = new Date(tomorrowAt0Utc); + } + if (isSteelPath) { + await addItem(inventory, "/Lotus/Types/Items/MiscItems/SteelEssence", 1); + MissionRewards.push({ + StoreItem: "/Lotus/StoreItems/Types/Items/MiscItems/SteelEssence", + ItemCount: 1 + }); + } + } + + return { + inventoryChanges, + MissionRewards, + credits, + AffiliationMods, + SyndicateXPItemReward, + ConquestCompletedMissionsCount + }; }; -//slightly inaccurate compared to official +//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] @@ -331,48 +1354,404 @@ export const addCredits = ( return { ...finalCredits, DailyMissionBonus: true }; }; -function getLevelCreditRewards(nodeName: string): number { - const minEnemyLevel = getNode(nodeName).minEnemyLevel; +export const addFixedLevelRewards = ( + rewards: IMissionRewardExternal, + inventory: TInventoryDatabaseDocument, + MissionRewards: IMissionReward[], + rewardInfo?: IRewardInfo +): number => { + let missionBonusCredits = 0; + if (rewards.credits) { + missionBonusCredits += rewards.credits; + inventory.RegularCredits += rewards.credits; + } + if (rewards.items) { + for (const item of rewards.items) { + MissionRewards.push({ + StoreItem: item, + ItemCount: 1 + }); + } + } + if (rewards.countedItems) { + for (const item of rewards.countedItems) { + MissionRewards.push({ + StoreItem: `/Lotus/StoreItems${item.ItemType.substring("Lotus/".length)}`, + ItemCount: item.ItemCount + }); + } + } + if (rewards.countedStoreItems) { + for (const item of rewards.countedStoreItems) { + MissionRewards.push(item); + } + } + if (rewards.droptable) { + if (rewards.droptable in ExportRewards) { + const rotations: number[] = rewardInfo ? getRotations(rewardInfo) : [0]; + logger.debug(`rolling ${rewards.droptable} for level key rewards`, { rotations }); + for (const tier of rotations) { + const reward = getRandomRewardByChance(ExportRewards[rewards.droptable][tier]); + if (reward) { + MissionRewards.push({ + StoreItem: reward.type, + ItemCount: reward.itemCount + }); + } + } + } else { + logger.error(`unknown droptable ${rewards.droptable}`); + } + } + return missionBonusCredits; +}; + +function getLevelCreditRewards(node: IRegion): number { + const minEnemyLevel = node.minEnemyLevel; return 1000 + (minEnemyLevel - 1) * 100; //TODO: get dark sektor fixed credit rewards and railjack bonus } -function getRandomMissionDrops(RewardInfo: IRewardInfo): IRngResult[] { - const drops: IRngResult[] = []; +function getRandomMissionDrops( + inventory: TInventoryDatabaseDocument, + RewardInfo: IRewardInfo, + mission: IMission | undefined, + tierOverride: number | undefined, + firstCompletion: boolean +): IMissionReward[] { + const drops: IMissionReward[] = []; + if (RewardInfo.sortieTag == "Final" && firstCompletion) { + const arr = RewardInfo.sortieId!.split("_"); + let sortieId = arr[1]; + if (sortieId == "Lite") { + sortieId = arr[2]; + + const boss = getLiteSortie(idToWeek(sortieId)).Boss; + let crystalType = { + SORTIE_BOSS_AMAR: "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalAmar", + SORTIE_BOSS_NIRA: "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalNira", + SORTIE_BOSS_BOREAL: "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalBoreal" + }[boss]; + const attenTag = { + SORTIE_BOSS_AMAR: "NarmerSortieAmarCrystalRewards", + SORTIE_BOSS_NIRA: "NarmerSortieNiraCrystalRewards", + SORTIE_BOSS_BOREAL: "NarmerSortieBorealCrystalRewards" + }[boss]; + const attenIndex = inventory.SortieRewardAttenuation?.findIndex(x => x.Tag == attenTag) ?? -1; + const mythicProbability = + 0.2 + (inventory.SortieRewardAttenuation?.find(x => x.Tag == attenTag)?.Atten ?? 0); + if (Math.random() < mythicProbability) { + crystalType += "Mythic"; + if (attenIndex != -1) { + inventory.SortieRewardAttenuation!.splice(attenIndex, 1); + } + } else { + if (attenIndex == -1) { + inventory.SortieRewardAttenuation ??= []; + inventory.SortieRewardAttenuation.push({ + Tag: attenTag, + Atten: 0.2 + }); + } else { + inventory.SortieRewardAttenuation![attenIndex].Atten += 0.2; + } + } + + drops.push({ StoreItem: crystalType, ItemCount: 1 }); + + const drop = getRandomRewardByChance( + ExportRewards["/Lotus/Types/Game/MissionDecks/ArchonSortieRewards"][0] + )!; + drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); + inventory.LastLiteSortieReward = [ + { + SortieId: new Types.ObjectId(sortieId), + StoreItem: drop.type, + Manifest: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards" + } + ]; + } else { + const drop = getRandomRewardByChance(ExportRewards["/Lotus/Types/Game/MissionDecks/SortieRewards"][0])!; + drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); + inventory.LastSortieReward = [ + { + SortieId: new Types.ObjectId(sortieId), + StoreItem: drop.type, + Manifest: "/Lotus/Types/Game/MissionDecks/SortieRewards" + } + ]; + } + } + if (RewardInfo.periodicMissionTag?.startsWith("HardDaily")) { + drops.push({ + StoreItem: "/Lotus/StoreItems/Types/Items/MiscItems/SteelEssence", + ItemCount: 5 + }); + } if (RewardInfo.node in ExportRegions) { const region = ExportRegions[RewardInfo.node]; - const rewardManifests = region.rewardManifests ?? []; + let rewardManifests: string[]; + if (RewardInfo.periodicMissionTag == "EliteAlert" || RewardInfo.periodicMissionTag == "EliteAlertB") { + rewardManifests = ["/Lotus/Types/Game/MissionDecks/EliteAlertMissionRewards/EliteAlertMissionRewards"]; + } else if (RewardInfo.invasionId && region.missionIndex == 0) { + // Invasion assassination has Phorid has the boss who should drop Nyx parts + // TODO: Check that the invasion faction is indeed FC_INFESTATION once the Invasions in worldState are more dynamic + rewardManifests = ["/Lotus/Types/Game/MissionDecks/BossMissionRewards/NyxRewards"]; + } else if (RewardInfo.sortieId) { + // Sortie mission types differ from the underlying node and hence also don't give rewards from the underlying nodes. + // Assassinations in non-lite sorties are an exception to this. + if (region.missionIndex == 0) { + const arr = RewardInfo.sortieId.split("_"); + let giveNodeReward = false; + if (arr[1] != "Lite") { + const sortie = getSortie(idToDay(arr[1])); + giveNodeReward = sortie.Variants.find(x => x.node == arr[0])!.missionType == "MT_ASSASSINATION"; + } + rewardManifests = giveNodeReward ? region.rewardManifests : []; + } else { + rewardManifests = []; + } + } else if (RewardInfo.T == 13) { + // Undercroft extra/side portal (normal mode), gives 1 Pathos Clamp + Duviri Arcane. + drops.push({ + StoreItem: "/Lotus/StoreItems/Types/Gameplay/Duviri/Resource/DuviriDragonDropItem", + ItemCount: 1 + }); + rewardManifests = [ + "/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriStaticUndercroftResourceRewards" + ]; + } else if (RewardInfo.T == 14) { + // Undercroft extra/side portal (steel path), gives 3 Pathos Clamps + Eidolon Arcane. + drops.push({ + StoreItem: "/Lotus/StoreItems/Types/Gameplay/Duviri/Resource/DuviriDragonDropItem", + ItemCount: 3 + }); + rewardManifests = [ + "/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriSteelPathStaticUndercroftResourceRewards" + ]; + } else if (RewardInfo.T == 15) { + rewardManifests = [ + mission?.Tier == 1 + ? "/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriKullervoSteelPathRNGRewards" + : "/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriKullervoNormalRNGRewards" + ]; + } else if (RewardInfo.T == 70) { + // Orowyrm chest, gives 10 Pathos Clamps, or 15 on Steel Path. + drops.push({ + StoreItem: "/Lotus/StoreItems/Types/Gameplay/Duviri/Resource/DuviriDragonDropItem", + ItemCount: mission?.Tier == 1 ? 15 : 10 + }); + rewardManifests = []; + } else { + rewardManifests = region.rewardManifests; + } let rotations: number[] = []; - if (RewardInfo.VaultsCracked) { - // For Spy missions, e.g. 3 vaults cracked = A, B, C - for (let i = 0; i != RewardInfo.VaultsCracked; ++i) { - rotations.push(i); - } - } else { - const rotationCount = RewardInfo.rewardQualifications?.length || 0; - rotations = getRotations(rotationCount); - } - rewardManifests - .map(name => ExportRewards[name]) - .forEach(table => { - for (const rotation of rotations) { - const rotationRewards = table[rotation]; - const drop = getRandomRewardByChance(rotationRewards); - if (drop) { - drops.push(drop); + if (RewardInfo.jobId) { + if (RewardInfo.JobStage! >= 0) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [jobType, unkIndex, hubNode, syndicateMissionId, locationTag] = RewardInfo.jobId.split("_"); + let isEndlessJob = false; + if (syndicateMissionId) { + const syndicateMissions: ISyndicateMissionInfo[] = []; + pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId)); + const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId); + if (syndicateEntry && syndicateEntry.Jobs) { + let job = syndicateEntry.Jobs[RewardInfo.JobTier!]; + + if (syndicateEntry.Tag === "EntratiSyndicate") { + const vault = syndicateEntry.Jobs.find(j => j.locationTag === locationTag); + if (vault && locationTag) job = vault; + // if ( + // [ + // "DeimosRuinsExterminateBounty", + // "DeimosRuinsEscortBounty", + // "DeimosRuinsMistBounty", + // "DeimosRuinsPurifyBounty", + // "DeimosRuinsSacBounty" + // ].some(ending => jobType.endsWith(ending)) + // ) { + // job.rewards = "TODO"; // Droptable for Arcana Isolation Vault + // } + if ( + [ + "DeimosEndlessAreaDefenseBounty", + "DeimosEndlessExcavateBounty", + "DeimosEndlessPurifyBounty" + ].some(ending => jobType.endsWith(ending)) + ) { + const endlessJob = syndicateEntry.Jobs.find(j => j.endless); + if (endlessJob) { + isEndlessJob = true; + job = endlessJob; + const excess = Math.floor(RewardInfo.JobStage! / (job.xpAmounts.length - 1)); + + const rotationIndexes = [0, 0, 1, 2]; + const rotationIndex = rotationIndexes[excess % rotationIndexes.length]; + const dropTable = [ + "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTableARewards", + "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTableBRewards", + "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTableCRewards" + ]; + job.rewards = dropTable[rotationIndex]; + } + } + } else if (syndicateEntry.Tag === "SolarisSyndicate") { + if (jobType.endsWith("Heists/HeistProfitTakerBountyOne") && RewardInfo.JobStage == 2) { + job = { + rewards: + "/Lotus/Types/Game/MissionDecks/HeistJobMissionRewards/HeistTierATableARewards", + masteryReq: 0, + minEnemyLevel: 40, + maxEnemyLevel: 60, + xpAmounts: [1000] + }; + RewardInfo.Q = false; // Just in case + } else { + const tierMap = { + Two: "B", + Three: "C", + Four: "D" + }; + + for (const [key, tier] of Object.entries(tierMap)) { + if (jobType.endsWith(`Heists/HeistProfitTakerBounty${key}`)) { + job = { + rewards: `/Lotus/Types/Game/MissionDecks/HeistJobMissionRewards/HeistTier${tier}TableARewards`, + masteryReq: 0, + minEnemyLevel: 40, + maxEnemyLevel: 60, + xpAmounts: [1000] + }; + RewardInfo.Q = false; // Just in case + break; + } + } + } + } + rewardManifests = [job.rewards]; + if (job.xpAmounts.length > 1) { + rotations = [RewardInfo.JobStage! % (job.xpAmounts.length - 1)]; + } else { + rotations = [0]; + } + if ( + RewardInfo.Q && + (RewardInfo.JobStage === job.xpAmounts.length - 1 || job.isVault) && + !isEndlessJob + ) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (ExportRewards[job.rewards]) { + rewardManifests.push(job.rewards); + rotations.push(ExportRewards[job.rewards].length - 1); + } + } } } - }); + } + } else if (RewardInfo.challengeMissionId) { + const rewardTables: Record = { + EntratiLabSyndicate: [ + "/Lotus/Types/Game/MissionDecks/EntratiLabJobMissionReward/TierATableRewards", + "/Lotus/Types/Game/MissionDecks/EntratiLabJobMissionReward/TierBTableRewards", + "/Lotus/Types/Game/MissionDecks/EntratiLabJobMissionReward/TierCTableRewards", + "/Lotus/Types/Game/MissionDecks/EntratiLabJobMissionReward/TierDTableRewards", + "/Lotus/Types/Game/MissionDecks/EntratiLabJobMissionReward/TierETableRewards" + ], + ZarimanSyndicate: [ + "/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierATableRewards", + "/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierBTableRewards", + "/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierCTableARewards", // [sic] + "/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierDTableRewards", + "/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierETableRewards" + ], + HexSyndicate: [ + "/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierABountyRewards", + "/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierBBountyRewards", + "/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierCBountyRewards", + "/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierDBountyRewards", + "/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierEBountyRewards", + "/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierFBountyRewards", + "/Lotus/Types/Game/MissionDecks/1999MissionRewards/InfestedLichBountyRewards" + ] + }; + + const [syndicateTag, tierStr] = RewardInfo.challengeMissionId.split("_"); + const tier = Number(tierStr); + + const rewardTable = rewardTables[syndicateTag][tier]; + + if (rewardTable) { + rewardManifests = [rewardTable]; + rotations = [0]; + } else { + logger.error(`Unknown syndicate or tier: ${RewardInfo.challengeMissionId}`); + } + } else { + if (RewardInfo.node == "SolNode238") { + // The Circuit + const category = mission?.Tier == 1 ? "EXC_HARD" : "EXC_NORMAL"; + const progress = inventory.EndlessXP?.find(x => x.Category == category); + if (progress) { + // https://wiki.warframe.com/w/The%20Circuit#Tiers_and_Weekly_Rewards + const roundsCompleted = RewardInfo.rewardQualifications?.length || 0; + if (roundsCompleted >= 1) { + progress.Earn += 100; + } + if (roundsCompleted >= 2) { + progress.Earn += 110; + } + if (roundsCompleted >= 3) { + progress.Earn += 125; + } + if (roundsCompleted >= 4) { + progress.Earn += 145; + if (progress.BonusAvailable && progress.BonusAvailable.getTime() <= Date.now()) { + progress.Earn += 50; + progress.BonusAvailable = new Date(Date.now() + 24 * 3600_000); // TOVERIFY + } + } + if (roundsCompleted >= 5) { + progress.Earn += (roundsCompleted - 4) * 170; + } + } + tierOverride = 0; + } + rotations = getRotations(RewardInfo, tierOverride); + } + if (rewardManifests.length != 0) { + logger.debug(`generating random mission rewards`, { rewardManifests, rotations }); + } + if (RewardInfo.rewardSeed) { + if (RewardInfo.rewardSeed != inventory.RewardSeed) { + logger.warn(`RewardSeed mismatch:`, { client: RewardInfo.rewardSeed, database: inventory.RewardSeed }); + } + } + const rng = new SRng(BigInt(RewardInfo.rewardSeed ?? generateRewardSeed()) ^ 0xffffffffffffffffn); + rewardManifests.forEach(name => { + const table = ExportRewards[name]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!table) { + logger.error(`unknown droptable: ${name}`); + return; + } + for (const rotation of rotations) { + const rotationRewards = table[rotation]; + const drop = getRandomRewardByChance(rotationRewards, rng); + if (drop) { + drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); + } + } + }); if (region.cacheRewardManifest && RewardInfo.EnemyCachesFound) { const deck = ExportRewards[region.cacheRewardManifest]; for (let rotation = 0; rotation != RewardInfo.EnemyCachesFound; ++rotation) { const drop = getRandomRewardByChance(deck[rotation]); if (drop) { - drops.push(drop); + drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount, FromEnemyCache: true }); } } } @@ -391,9 +1770,147 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo): IRngResult[] { const drop = getRandomRewardByChance(deck[rotation]); if (drop) { - drops.push(drop); + drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); } } + + if (RewardInfo.PurgatoryRewardQualifications) { + for (const encodedQualification of RewardInfo.PurgatoryRewardQualifications) { + const qualification = parseInt(encodedQualification) - 1; + if (qualification < 0 || qualification > 8) { + logger.error(`unexpected purgatory reward qualification: ${qualification}`); + } else { + const drop = getRandomRewardByChance( + ExportRewards[ + [ + "/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryBlueTokenRewards", + "/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryGoldTokenRewards", + "/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryBlackTokenRewards" + ][Math.trunc(qualification / 3)] + ][qualification % 3] + ); + if (drop) { + drops.push({ + StoreItem: drop.type, + ItemCount: drop.itemCount, + FromEnemyCache: true // to show "identified" + }); + } + } + } + } + + if (RewardInfo.periodicMissionTag?.startsWith("KuvaMission")) { + const drop = getRandomRewardByChance( + ExportRewards[ + RewardInfo.periodicMissionTag == "KuvaMission6" || RewardInfo.periodicMissionTag == "KuvaMission12" + ? "/Lotus/Types/Game/MissionDecks/KuvaMissionRewards/KuvaSiphonFloodRewards" + : "/Lotus/Types/Game/MissionDecks/KuvaMissionRewards/KuvaSiphonRewards" + ][0] + )!; + drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); + } } 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" +}; + +const node_excluded_buddies: Record = { + SolNode856: "/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue", + SolNode852: "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue", + SolNode851: "/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue", + SolNode850: "/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue", + SolNode853: "/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue", + SolNode854: "/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue" +}; + +const getHexBounties = (seed: number): { nodes: string[]; buddies: string[] } => { + // We're gonna shuffle these arrays, so they're not truly 'const'. + const nodes: string[] = [ + "SolNode850", + "SolNode851", + "SolNode852", + "SolNode853", + "SolNode854", + "SolNode856", + "SolNode858" + ]; + const excludable_nodes: string[] = ["SolNode851", "SolNode852", "SolNode853", "SolNode854"]; + const buddies: string[] = [ + "/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue", + "/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue", + "/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue", + "/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue", + "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue", + "/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue" + ]; + + const rng = new SRng(seed); + rng.shuffleArray(nodes); + rng.shuffleArray(excludable_nodes); + while (nodes.length > buddies.length) { + nodes.splice( + nodes.findIndex(x => x == excludable_nodes[0]), + 1 + ); + excludable_nodes.splice(0, 1); + } + rng.shuffleArray(buddies); + for (let i = 0; i != 6; ++i) { + if (buddies[i] == node_excluded_buddies[nodes[i]]) { + const swapIdx = (i + 1) % buddies.length; + const tmp = buddies[swapIdx]; + buddies[swapIdx] = buddies[i]; + buddies[i] = tmp; + } + } + return { nodes, buddies }; +}; diff --git a/src/services/personalRoomsService.ts b/src/services/personalRoomsService.ts index 5325af1c..8cf49ec1 100644 --- a/src/services/personalRoomsService.ts +++ b/src/services/personalRoomsService.ts @@ -1,8 +1,14 @@ import { PersonalRooms } from "@/src/models/personalRoomsModel"; import { addItem, getInventory } from "@/src/services/inventoryService"; +import { TPersonalRoomsDatabaseDocument } from "../types/personalRoomsTypes"; +import { IGardeningDatabase } from "../types/shipTypes"; +import { getRandomElement } from "./rngService"; -export const getPersonalRooms = async (accountId: string) => { - const personalRooms = await PersonalRooms.findOne({ personalRoomsOwnerId: accountId }); +export const getPersonalRooms = async ( + accountId: string, + projection?: string +): Promise => { + const personalRooms = await PersonalRooms.findOne({ personalRoomsOwnerId: accountId }, projection); if (!personalRooms) { throw new Error(`personal rooms not found for account ${accountId}`); @@ -10,7 +16,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)) { @@ -24,3 +30,64 @@ export const updateShipFeature = async (accountId: string, shipFeature: string) await addItem(inventory, shipFeature, -1); await inventory.save(); }; + +export const createGarden = (): IGardeningDatabase => { + const plantTypes = [ + "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantA", + "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantB", + "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantC", + "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantD", + "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantE", + "/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantF" + ]; + const endTime = new Date((Math.trunc(Date.now() / 1000) + 79200) * 1000); // Plants will take 22 hours to grow + return { + Planters: [ + { + Name: "Garden0", + Plants: [ + { + PlantType: getRandomElement(plantTypes)!, + EndTime: endTime, + PlotIndex: 0 + }, + { + PlantType: getRandomElement(plantTypes)!, + EndTime: endTime, + PlotIndex: 1 + } + ] + }, + { + Name: "Garden1", + Plants: [ + { + PlantType: getRandomElement(plantTypes)!, + EndTime: endTime, + PlotIndex: 0 + }, + { + PlantType: getRandomElement(plantTypes)!, + EndTime: endTime, + PlotIndex: 1 + } + ] + }, + { + Name: "Garden2", + Plants: [ + { + PlantType: getRandomElement(plantTypes)!, + EndTime: endTime, + PlotIndex: 0 + }, + { + PlantType: getRandomElement(plantTypes)!, + EndTime: endTime, + PlotIndex: 1 + } + ] + } + ] + }; +}; diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index bc2ef924..59a431c3 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -8,16 +8,18 @@ import { updateCurrency, updateSlots } from "@/src/services/inventoryService"; -import { getRandomWeightedReward } from "@/src/services/rngService"; -import { getVendorManifestByOid } from "@/src/services/serversideVendorsService"; +import { getRandomWeightedRewardUc } from "@/src/services/rngService"; +import { applyStandingToVendorManifest, getVendorManifestByOid } from "@/src/services/serversideVendorsService"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IPurchaseRequest, IPurchaseResponse, SlotPurchase, IInventoryChanges } from "@/src/types/purchaseTypes"; import { logger } from "@/src/utils/logger"; import worldState from "@/static/fixed_responses/worldState/worldState.json"; import { ExportBoosterPacks, + ExportBoosters, ExportBundles, ExportGear, + ExportMisc, ExportResources, ExportSyndicates, ExportVendors, @@ -25,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/"); @@ -47,38 +50,115 @@ 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!); + let 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); + manifest = applyStandingToVendorManifest(inventory, manifest); + 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.RegularPrice) { + combineInventoryChanges( + prePurchaseInventoryChanges, + updateCurrency(inventory, offer.RegularPrice[0], false) + ); + } + if (offer.PremiumPrice) { + combineInventoryChanges( + prePurchaseInventoryChanges, + updateCurrency(inventory, offer.PremiumPrice[0], true) + ); } 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, + false, + purchaseRequest.PurchaseParams.UsePremium, + seed ); - combineInventoryChanges(purchaseResponse.InventoryChanges, inventoryChanges); - - if (!purchaseResponse) throw new Error("purchase response was undefined"); + combineInventoryChanges(purchaseResponse.InventoryChanges, prePurchaseInventoryChanges); const currencyChanges = updateCurrency( inventory, @@ -91,6 +171,32 @@ 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) + ); + if (purchaseRequest.PurchaseParams.ExpectedPrice) { + throw new Error(`vendor purchase should not have an expected price`); + } + + 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!; @@ -107,6 +213,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 @@ -117,10 +224,10 @@ export const handlePurchase = async ( purchaseResponse.Standing = [ { Tag: syndicateTag, - Standing: favour.standingCost + Standing: favour.standingCost * purchaseRequest.PurchaseParams.Quantity } ]; - affiliation.Standing -= favour.standingCost; + affiliation.Standing -= favour.standingCost * purchaseRequest.PurchaseParams.Quantity; } } } @@ -131,15 +238,32 @@ 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 (typeof offer.credits == "number") { + combineInventoryChanges( + purchaseResponse.InventoryChanges, + updateCurrency(inventory, offer.credits, false) + ); + } + if (typeof offer.platinum == "number") { + combineInventoryChanges( + purchaseResponse.InventoryChanges, + updateCurrency(inventory, offer.platinum, true) + ); + } + if (offer.itemPrices) { + handleItemPrices( + inventory, + offer.itemPrices, + purchaseRequest.PurchaseParams.Quantity, + purchaseResponse.InventoryChanges + ); + } } } + if (purchaseRequest.PurchaseParams.ExpectedPrice) { + throw new Error(`vendor purchase should not have an expected price`); + } break; case 18: { if (purchaseRequest.PurchaseParams.SourceId! != worldState.PrimeVaultTraders[0]._id.$oid) { @@ -162,9 +286,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; @@ -189,46 +317,58 @@ 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); } } }; +export const handleBundleAcqusition = async ( + storeItemName: string, + inventory: TInventoryDatabaseDocument, + quantity: number = 1, + inventoryChanges: IInventoryChanges = {} +): Promise => { + const bundle = ExportBundles[storeItemName]; + logger.debug("acquiring bundle", bundle); + for (const component of bundle.components) { + combineInventoryChanges( + inventoryChanges, + ( + await handleStoreItemAcquisition( + component.typeName, + inventory, + component.purchaseQuantity * quantity, + component.durability, + true + ) + ).InventoryChanges + ); + } + return inventoryChanges; +}; + export const handleStoreItemAcquisition = async ( storeItemName: string, inventory: TInventoryDatabaseDocument, quantity: number = 1, durability: TRarity = "COMMON", - ignorePurchaseQuantity: boolean = false + ignorePurchaseQuantity: boolean = false, + premiumPurchase: boolean = true, + seed?: bigint ): Promise => { let purchaseResponse = { InventoryChanges: {} }; logger.debug(`handling acquision of ${storeItemName}`); if (storeItemName in ExportBundles) { - const bundle = ExportBundles[storeItemName]; - logger.debug("acquiring bundle", bundle); - for (const component of bundle.components) { - combineInventoryChanges( - purchaseResponse.InventoryChanges, - ( - await handleStoreItemAcquisition( - component.typeName, - inventory, - component.purchaseQuantity * quantity, - component.durability, - true - ) - ).InventoryChanges - ); - } + 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) { @@ -239,14 +379,23 @@ export const handleStoreItemAcquisition = async ( } switch (storeCategory) { default: { - purchaseResponse = await addItem(inventory, internalName, quantity); + purchaseResponse = { + InventoryChanges: await addItem(inventory, internalName, quantity, premiumPurchase, seed) + }; break; } case "Types": - purchaseResponse = await handleTypesPurchase(internalName, inventory, quantity); + purchaseResponse = await handleTypesPurchase( + internalName, + inventory, + quantity, + ignorePurchaseQuantity, + premiumPurchase, + seed + ); break; case "Boosters": - purchaseResponse = handleBoostersPurchase(internalName, inventory, durability); + purchaseResponse = handleBoostersPurchase(storeItemName, inventory, durability); break; } } @@ -254,16 +403,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) @@ -273,7 +422,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( @@ -282,22 +432,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 ( @@ -306,6 +457,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}`); } @@ -313,67 +465,85 @@ 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; }; +const handleCreditBundlePurchase = async ( + typeName: string, + inventory: TInventoryDatabaseDocument +): Promise => { + if (typeName && typeName in ExportMisc.creditBundles) { + const creditsAmount = ExportMisc.creditBundles[typeName]; + + inventory.RegularCredits += creditsAmount; + await inventory.save(); + + return { InventoryChanges: { RegularCredits: creditsAmount } }; + } else { + throw new Error(`unknown credit bundle: ${typeName}`); + } +}; + //TODO: change to getInventory, apply changes then save at the end const handleTypesPurchase = async ( typesName: string, inventory: TInventoryDatabaseDocument, - quantity: number + quantity: number, + ignorePurchaseQuantity: boolean, + premiumPurchase: boolean = true, + seed?: bigint ): 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, premiumPurchase, seed) }; 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); } }; -const boosterCollection = [ - "/Lotus/Types/Boosters/ResourceAmountBooster", - "/Lotus/Types/Boosters/AffinityBooster", - "/Lotus/Types/Boosters/ResourceDropChanceBooster", - "/Lotus/Types/Boosters/CreditBooster" -]; - -const boosterDuration: Record = { - COMMON: 3 * 86400, - UNCOMMON: 7 * 86400, - RARE: 30 * 86400, - LEGENDARY: 90 * 86400 -}; - const handleBoostersPurchase = ( boosterStoreName: string, inventory: TInventoryDatabaseDocument, durability: TRarity ): { InventoryChanges: IInventoryChanges } => { - const ItemType = boosterStoreName.replace("StoreItem", ""); - if (!boosterCollection.find(x => x == ItemType)) { - logger.error(`unknown booster type: ${ItemType}`); + if (!(boosterStoreName in ExportBoosters)) { + logger.error(`unknown booster type: ${boosterStoreName}`); return { InventoryChanges: {} }; } - const ExpiryDate = boosterDuration[durability]; + const ItemType = ExportBoosters[boosterStoreName].typeName; + const ExpiryDate = ExportMisc.boosterDurations[durability]; addBooster(ItemType, ExpiryDate, inventory); diff --git a/src/services/questService.ts b/src/services/questService.ts index 75316763..053f6eb4 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -1,8 +1,16 @@ -import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; +import { IKeyChainRequest } from "@/src/types/requestTypes"; +import { isEmptyObject } from "@/src/helpers/general"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; -import { IInventoryDatabase, IQuestKeyDatabase, IQuestStage } from "@/src/types/inventoryTypes/inventoryTypes"; +import { createMessage } from "@/src/services/inboxService"; +import { addItem, addItems, addKeyChainItems, setupKahlSyndicate } from "@/src/services/inventoryService"; +import { fromStoreItem, getKeyChainMessage, getLevelKeyRewards } from "@/src/services/itemDataService"; +import { IQuestKeyClient, IQuestKeyDatabase, IQuestStage, ITypeCount } 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"; +import questCompletionItems from "@/static/fixed_responses/questCompletionRewards.json"; export interface IUpdateQuestRequest { QuestKeys: Omit[]; @@ -13,10 +21,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"); @@ -28,11 +36,16 @@ export const updateQuestKey = ( throw new Error(`quest key ${questKeyUpdate[0].ItemType} not found`); } - inventory.QuestKeys[questKeyIndex] = questKeyUpdate[0]; + inventory.QuestKeys[questKeyIndex].overwrite(questKeyUpdate[0]); + const inventoryChanges: IInventoryChanges = {}; if (questKeyUpdate[0].Completed) { inventory.QuestKeys[questKeyIndex].CompletionDate = new Date(); + + const questKey = questKeyUpdate[0].ItemType; + await handleQuestCompletion(inventory, questKey, inventoryChanges); } + return inventoryChanges; }; export const updateQuestStage = ( @@ -52,6 +65,7 @@ export const updateQuestStage = ( const questStage = quest.Progress[ChainStage]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!questStage) { const questStageIndex = quest.Progress.push(questStageUpdate) - 1; if (questStageIndex !== ChainStage) { @@ -63,10 +77,300 @@ export const updateQuestStage = ( Object.assign(questStage, questStageUpdate); }; -export const addQuestKey = (inventory: TInventoryDatabaseDocument, questKey: IQuestKeyDatabase): void => { +export const addQuestKey = ( + inventory: TInventoryDatabaseDocument, + questKey: IQuestKeyDatabase +): IQuestKeyClient | undefined => { if (inventory.QuestKeys.some(q => q.ItemType === questKey.ItemType)) { - logger.error(`quest key ${questKey.ItemType} already exists`); + logger.warn(`Quest key ${questKey.ItemType} already exists. It will not be added`); return; } - inventory.QuestKeys.push(questKey); + + 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): Promise => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const chainStages = ExportKeys[questKey]?.chainStages; + + if (!chainStages) { + throw new Error(`Quest ${questKey} does not contain chain stages`); + } + + 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; + } + if (existingQuestKey) { + 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 = startingStage; i < chainStageTotal; i++) { + await giveKeyChainStageTriggered(inventory, { KeyChain: questKey, ChainStage: i }); + + await giveKeyChainMissionReward(inventory, { KeyChain: questKey, ChainStage: i }); + } + + await handleQuestCompletion(inventory, questKey); +}; + +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`); + + 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; +}; + +// Checks that `questKey` is in `requirements`, and if so, that all other quests in `requirements` are also already completed. +const doesQuestCompletionFinishSet = ( + inventory: TInventoryDatabaseDocument, + questKey: string, + requirements: string[] +): boolean => { + let holds = false; + for (const requirement of requirements) { + if (questKey == requirement) { + holds = true; + } else { + if (!inventory.QuestKeys.find(x => x.ItemType == requirement)?.Completed) { + return false; + } + } + } + return holds; +}; + +const handleQuestCompletion = async ( + inventory: TInventoryDatabaseDocument, + questKey: string, + inventoryChanges: IInventoryChanges = {} +): Promise => { + logger.debug(`completed quest ${questKey}`); + + if (questKey == "/Lotus/Types/Keys/OrokinMoonQuest/OrokinMoonQuestKeyChain") { + await createMessage(inventory.accountOwnerId, [ + { + sndr: "/Lotus/Language/Bosses/Ordis", + msg: "/Lotus/Language/G1Quests/SecondDreamFinishInboxMessage", + att: [ + "/Lotus/Weapons/Tenno/Melee/Swords/StalkerTwo/StalkerTwoSmallSword", + "/Lotus/Upgrades/Skins/Sigils/ScarSigil" + ], + sub: "/Lotus/Language/G1Quests/SecondDreamFinishInboxTitle", + icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", + highPriority: true + } + ]); + } else if (questKey == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain") { + setupKahlSyndicate(inventory); + } + + // Whispers in the Walls is unlocked once The New + Heart of Deimos are completed. + if ( + doesQuestCompletionFinishSet(inventory, questKey, [ + "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain", + "/Lotus/Types/Keys/InfestedMicroplanetQuest/InfestedMicroplanetQuestKeyChain" + ]) + ) { + await createMessage(inventory.accountOwnerId, [ + { + sndr: "/Lotus/Language/Bosses/Loid", + msg: "/Lotus/Language/EntratiLab/EntratiQuest/WiTWQuestRecievedInboxBody", + att: ["/Lotus/Types/Keys/EntratiLab/EntratiQuestKeyChain"], + sub: "/Lotus/Language/EntratiLab/EntratiQuest/WiTWQuestRecievedInboxTitle", + icon: "/Lotus/Interface/Icons/Npcs/Entrati/Loid.png", + highPriority: true + } + ]); + } + + // The Hex (Quest) is unlocked once The Lotus Eaters + The Duviri Paradox are completed. + if ( + doesQuestCompletionFinishSet(inventory, questKey, [ + "/Lotus/Types/Keys/1999PrologueQuest/1999PrologueQuestKeyChain", + "/Lotus/Types/Keys/DuviriQuest/DuviriQuestKeyChain" + ]) + ) { + await createMessage(inventory.accountOwnerId, [ + { + sndr: "/Lotus/Language/NewWar/P3M1ChooseMara", + msg: "/Lotus/Language/1999Quest/1999QuestInboxBody", + att: ["/Lotus/Types/Keys/1999Quest/1999QuestKeyChain"], + sub: "/Lotus/Language/1999Quest/1999QuestInboxSubject", + icon: "/Lotus/Interface/Icons/Npcs/Operator.png", + highPriority: true + } + ]); + } + + const questCompletionItems = getQuestCompletionItems(questKey); + logger.debug(`quest completion items`, questCompletionItems); + if (questCompletionItems) { + await addItems(inventory, questCompletionItems, inventoryChanges); + } + + if (inventory.ActiveQuest == questKey) inventory.ActiveQuest = ""; +}; + +export const giveKeyChainItem = async ( + inventory: TInventoryDatabaseDocument, + keyChainInfo: IKeyChainRequest +): Promise => { + const inventoryChanges = await addKeyChainItems(inventory, keyChainInfo); + + if (isEmptyObject(inventoryChanges)) { + logger.warn("inventory changes was empty after getting keychain items: should not happen"); + } + // items were added: update quest stage's i (item was given) + updateQuestStage(inventory, keyChainInfo, { i: true }); + + return inventoryChanges; + + //TODO: Check whether Wishlist is used to track items which should exist uniquely in the inventory + /* + some items are added or removed (not sure) to the wishlist, in that case a + WishlistChanges: ["/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem"], + is added to the response, need to determine for which items this is the case and what purpose this has. + */ + //{"KeyChain":"/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain","ChainStage":0} + //{"WishlistChanges":["/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem"],"MiscItems":[{"ItemType":"/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem","ItemCount":1}]} +}; + +export const giveKeyChainMessage = async ( + inventory: TInventoryDatabaseDocument, + accountId: string | Types.ObjectId, + keyChainInfo: IKeyChainRequest +): Promise => { + const keyChainMessage = getKeyChainMessage(keyChainInfo); + + 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..72379ab0 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -6,7 +6,7 @@ export interface IRngResult { probability: number; } -export const getRandomElement = (arr: T[]): T => { +export const getRandomElement = (arr: readonly T[]): T | undefined => { return arr[Math.floor(Math.random() * arr.length)]; }; @@ -18,11 +18,14 @@ export const getRandomInt = (min: number, max: number): number => { return Math.floor(Math.random() * (max - min + 1)) + min; }; -export const getRandomReward = (pool: IRngResult[]): IRngResult | undefined => { +export 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) { @@ -31,43 +34,100 @@ export const getRandomReward = (pool: IRngResult[]): IRngResult | undefined => { return item; } } - throw new Error("What the fuck?"); + return pool[pool.length - 1]; }; -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 with identical results to the game client. Based on work by Donald Knuth. +export class SRng { + state: bigint; + + constructor(seed: bigint | number) { + this.state = BigInt(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: readonly T[]): T | undefined { + 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; + } + + randomReward(pool: T[]): T | undefined { + return getRewardAtPercentage(pool, this.randomFloat()); + } + + churnSeed(its: number): void { + while (its--) { + this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn; + } + } + + shuffleArray(arr: T[]): void { + for (let lastIdx = arr.length - 1; lastIdx >= 1; --lastIdx) { + const swapIdx = this.randomInt(0, lastIdx); + const tmp = arr[swapIdx]; + arr[swapIdx] = arr[lastIdx]; + arr[lastIdx] = tmp; + } + } +} diff --git a/src/services/saveLoadoutService.ts b/src/services/saveLoadoutService.ts index 188b06ae..0676d113 100644 --- a/src/services/saveLoadoutService.ts +++ b/src/services/saveLoadoutService.ts @@ -13,6 +13,8 @@ import { Types } from "mongoose"; import { isEmptyObject } from "@/src/helpers/general"; import { logger } from "@/src/utils/logger"; import { equipmentKeys, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IItemConfig } from "../types/inventoryTypes/commonInventoryTypes"; +import { importCrewMemberId } from "./importService"; //TODO: setup default items on account creation or like originally in giveStartingItems.php @@ -28,8 +30,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 +86,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 +139,33 @@ 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)) { + if (itemId.startsWith("ca70ca70ca70ca70")) { + logger.warn( + `unlockAllSkins does not work with favoriting items because you don't actually own it` + ); + } else { + const inventoryItem = inventory.WeaponSkins.id(itemId); + if (!inventoryItem) { + throw new Error(`inventory item WeaponSkins not found with id ${itemId}`); + } + if ("Favorite" in itemConfigEntries) { + inventoryItem.Favorite = itemConfigEntries.Favorite; + } + if ("IsNew" in itemConfigEntries) { + inventoryItem.IsNew = itemConfigEntries.IsNew; + } + } + } + break; + } + case "LotusCustomization": { + logger.debug(`saved LotusCustomization`, equipmentChanges.LotusCustomization); + inventory.LotusCustomization = equipmentChanges.LotusCustomization; + break; + } default: { if (equipmentKeys.includes(equipmentName as TEquipmentKey) && equipmentName != "ValidNewLoadoutId") { logger.debug(`general Item config saved of type ${equipmentName}`, { @@ -148,14 +174,42 @@ export const handleInventoryItemConfigChange = async ( const itemEntries = equipment as IItemEntry; for (const [itemId, itemConfigEntries] of Object.entries(itemEntries)) { - const inventoryItem = inventory[equipmentName].find(item => item._id?.toString() === itemId); + const inventoryItem = inventory[equipmentName].id(itemId); if (!inventoryItem) { throw new Error(`inventory item ${equipmentName} not found with id ${itemId}`); } for (const [configId, config] of Object.entries(itemConfigEntries)) { - inventoryItem.Configs[parseInt(configId)] = config; + if (/^[0-9]+$/.test(configId)) { + inventoryItem.Configs[parseInt(configId)] = config as IItemConfig; + } + } + if ("Favorite" in itemConfigEntries) { + inventoryItem.Favorite = itemConfigEntries.Favorite; + } + if ("IsNew" in itemConfigEntries) { + inventoryItem.IsNew = itemConfigEntries.IsNew; + } + + if ("ItemName" in itemConfigEntries) { + inventoryItem.ItemName = itemConfigEntries.ItemName; + } + if ("RailjackImage" in itemConfigEntries) { + inventoryItem.RailjackImage = itemConfigEntries.RailjackImage; + } + if ("Customization" in itemConfigEntries) { + inventoryItem.Customization = itemConfigEntries.Customization; + } + if ("Weapon" in itemConfigEntries) { + inventoryItem.Weapon = itemConfigEntries.Weapon; + } + if (itemConfigEntries.CrewMembers) { + inventoryItem.CrewMembers = { + SLOT_A: importCrewMemberId(itemConfigEntries.CrewMembers.SLOT_A ?? {}), + SLOT_B: importCrewMemberId(itemConfigEntries.CrewMembers.SLOT_B ?? {}), + SLOT_C: importCrewMemberId(itemConfigEntries.CrewMembers.SLOT_C ?? {}) + }; } } break; diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index b06f6ae7..89154147 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -1,8 +1,15 @@ -import { IMongoDate, IOid } from "@/src/types/commonTypes"; +import { unixTimesInMs } from "@/src/constants/timeConstants"; +import { isDev } from "@/src/helpers/pathHelper"; +import { catBreadHash } from "@/src/helpers/stringHelpers"; +import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; +import { mixSeeds, SRng } from "@/src/services/rngService"; +import { IMongoDate } from "@/src/types/commonTypes"; +import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes"; +import { logger } from "@/src/utils/logger"; +import { ExportVendors, IRange, IVendor } 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"; @@ -15,43 +22,20 @@ import DuviriAcrithisVendorManifest from "@/static/fixed_responses/getVendorInfo 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 Nova1999ConquestShopManifest from "@/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.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 Temple1999VendorManifest from "@/static/fixed_responses/getVendorInfo/Temple1999VendorManifest.json"; import TeshinHardModeVendorManifest from "@/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json"; import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVendorInfo/ZarimanCommisionsManifestArchimedean.json"; -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[] = [ +const rawVendorManifests: IVendorManifest[] = [ ArchimedeanVendorManifest, DeimosEntratiFragmentVendorProductsManifest, - DeimosFishmongerVendorManifest, DeimosHivemindCommisionsManifestFishmonger, DeimosHivemindCommisionsManifestPetVendor, DeimosHivemindCommisionsManifestProspector, @@ -63,36 +47,345 @@ const vendorManifests: IVendorManifest[] = [ DuviriAcrithisVendorManifest, EntratiLabsEntratiLabsCommisionsManifest, EntratiLabsEntratiLabVendorManifest, - HubsIronwakeDondaVendorManifest, - HubsPerrinSequenceWeaponVendorManifest, + HubsIronwakeDondaVendorManifest, // uses preprocessing HubsRailjackCrewMemberVendorManifest, MaskSalesmanManifest, - OstronFishmongerVendorManifest, + Nova1999ConquestShopManifest, OstronPetVendorManifest, OstronProspectorVendorManifest, - RadioLegionIntermission12VendorManifest, - SolarisDebtTokenVendorManifest, SolarisDebtTokenVendorRepossessionsManifest, - SolarisFishmongerVendorManifest, SolarisProspectorVendorManifest, - TeshinHardModeVendorManifest, + Temple1999VendorManifest, + TeshinHardModeVendorManifest, // uses preprocessing ZarimanCommisionsManifestArchimedean ]; -export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => { - for (const vendorManifest of vendorManifests) { - if (vendorManifest.VendorInfo.TypeName == typeName) { - return vendorManifest; +interface IGeneratableVendorInfo extends Omit { + cycleOffset?: number; + cycleDuration: number; +} + +const generatableVendors: IGeneratableVendorInfo[] = [ + { + _id: { $oid: "67dadc30e4b6e0e5979c8d84" }, + TypeName: "/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest", + RandomSeedType: "VRST_WEAPON", + RequiredGoalTag: "", + WeaponUpgradeValueAttenuationExponent: 2.25, + cycleOffset: 1740960000_000, + cycleDuration: 4 * unixTimesInMs.day + }, + { + _id: { $oid: "60ad3b6ec96976e97d227e19" }, + TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/PerrinSequenceWeaponVendorManifest", + RandomSeedType: "VRST_WEAPON", + WeaponUpgradeValueAttenuationExponent: 2.25, + cycleOffset: 1744934400_000, + cycleDuration: 4 * unixTimesInMs.day + } + // { + // _id: { $oid: "5dbb4c41e966f7886c3ce939" }, + // TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest" + // } +]; + +const getVendorOid = (typeName: string): string => { + return "5be4a159b144f3cd" + catBreadHash(typeName).toString(16).padStart(8, "0"); +}; + +// https://stackoverflow.com/a/17445304 +const gcd = (a: number, b: number): number => { + return b ? gcd(b, a % b) : a; +}; + +const getCycleDuration = (manifest: IVendor): number => { + let dur = 0; + for (const item of manifest.items) { + if (typeof item.durationHours != "number") { + dur = 1; + break; } + if (dur != item.durationHours) { + dur = gcd(dur, item.durationHours); + } + } + return dur * unixTimesInMs.hour; +}; + +export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => { + for (const vendorManifest of rawVendorManifests) { + if (vendorManifest.VendorInfo.TypeName == typeName) { + return preprocessVendorManifest(vendorManifest); + } + } + for (const vendorInfo of generatableVendors) { + if (vendorInfo.TypeName == typeName) { + return generateVendorManifest(vendorInfo); + } + } + if (typeName in ExportVendors) { + const manifest = ExportVendors[typeName]; + return generateVendorManifest({ + _id: { $oid: getVendorOid(typeName) }, + TypeName: typeName, + RandomSeedType: manifest.randomSeedType, + cycleDuration: getCycleDuration(manifest) + }); } return undefined; }; export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined => { - for (const vendorManifest of vendorManifests) { + for (const vendorManifest of rawVendorManifests) { if (vendorManifest.VendorInfo._id.$oid == oid) { - return vendorManifest; + return preprocessVendorManifest(vendorManifest); + } + } + for (const vendorInfo of generatableVendors) { + if (vendorInfo._id.$oid == oid) { + return generateVendorManifest(vendorInfo); + } + } + for (const [typeName, manifest] of Object.entries(ExportVendors)) { + const typeNameOid = getVendorOid(typeName); + if (typeNameOid == oid) { + return generateVendorManifest({ + _id: { $oid: typeNameOid }, + TypeName: typeName, + RandomSeedType: manifest.randomSeedType, + cycleDuration: getCycleDuration(manifest) + }); } } return undefined; }; + +export const applyStandingToVendorManifest = ( + inventory: TInventoryDatabaseDocument, + vendorManifest: IVendorManifest +): IVendorManifest => { + return { + VendorInfo: { + ...vendorManifest.VendorInfo, + ItemManifest: [...vendorManifest.VendorInfo.ItemManifest].map(offer => { + if (offer.Affiliation && offer.ReductionPerPositiveRank && offer.IncreasePerNegativeRank) { + const title: number = inventory.Affiliations.find(x => x.Tag == offer.Affiliation)?.Title ?? 0; + const factor = + 1 + (title < 0 ? offer.IncreasePerNegativeRank : offer.ReductionPerPositiveRank) * title * -1; + //console.log(offer.Affiliation, title, factor); + if (factor) { + offer = { ...offer }; + if (offer.RegularPrice) { + offer.RegularPriceBeforeDiscount = offer.RegularPrice; + offer.RegularPrice = [ + Math.trunc(offer.RegularPriceBeforeDiscount[0] * factor), + Math.trunc(offer.RegularPriceBeforeDiscount[1] * factor) + ]; + } + if (offer.ItemPrices) { + offer.ItemPricesBeforeDiscount = offer.ItemPrices; + offer.ItemPrices = []; + for (const item of offer.ItemPricesBeforeDiscount) { + offer.ItemPrices.push({ ...item, ItemCount: Math.trunc(item.ItemCount * factor) }); + } + } + } + } + return offer; + }) + } + }; +}; + +const preprocessVendorManifest = (originalManifest: IVendorManifest): IVendorManifest => { + 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) { + refreshExpiry(offer.Expiry); + } + return manifest; + } + return originalManifest; +}; + +const refreshExpiry = (expiry: IMongoDate): void => { + const period = parseInt(expiry.$date.$numberLong); + if (Date.now() >= period) { + const epoch = 1734307200_000; // 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(); + } +}; + +const toRange = (value: IRange | number): IRange => { + if (typeof value == "number") { + return { minValue: value, maxValue: value }; + } + return value; +}; + +const vendorManifestCache: Record = {}; + +const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifest => { + if (!(vendorInfo.TypeName in vendorManifestCache)) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { cycleOffset, cycleDuration, ...clientVendorInfo } = vendorInfo; + vendorManifestCache[vendorInfo.TypeName] = { + VendorInfo: { + ...clientVendorInfo, + ItemManifest: [], + Expiry: { $date: { $numberLong: "0" } } + } + }; + } + const cacheEntry = vendorManifestCache[vendorInfo.TypeName]; + const info = cacheEntry.VendorInfo; + if (Date.now() >= parseInt(info.Expiry.$date.$numberLong)) { + // Remove expired offers + for (let i = 0; i != info.ItemManifest.length; ) { + if (Date.now() >= parseInt(info.ItemManifest[i].Expiry.$date.$numberLong)) { + info.ItemManifest.splice(i, 1); + } else { + ++i; + } + } + + // Add new offers + const vendorSeed = parseInt(vendorInfo._id.$oid.substring(16), 16); + const cycleOffset = vendorInfo.cycleOffset ?? 1734307200_000; + const cycleDuration = vendorInfo.cycleDuration; + const cycleIndex = Math.trunc((Date.now() - cycleOffset) / cycleDuration); + const rng = new SRng(mixSeeds(vendorSeed, cycleIndex)); + const manifest = ExportVendors[vendorInfo.TypeName]; + const offersToAdd = []; + if ( + manifest.numItems && + (manifest.numItems.minValue != manifest.numItems.maxValue || + manifest.items.length != manifest.numItems.minValue) && + !manifest.isOneBinPerCycle + ) { + const remainingItemCapacity: Record = {}; + for (const item of manifest.items) { + remainingItemCapacity[item.storeItem] = 1 + item.duplicates; + } + for (const offer of info.ItemManifest) { + remainingItemCapacity[offer.StoreItem] -= 1; + } + const numItemsTarget = rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue); + while (info.ItemManifest.length + offersToAdd.length < numItemsTarget) { + // TODO: Consider per-bin item limits + // TODO: Consider item probability weightings + const item = rng.randomElement(manifest.items)!; + if (remainingItemCapacity[item.storeItem] != 0) { + remainingItemCapacity[item.storeItem] -= 1; + offersToAdd.push(item); + } + } + } else { + let binThisCycle; + if (manifest.isOneBinPerCycle) { + binThisCycle = cycleIndex % 2; // Note: May want to auto-compute the bin size, but this is only used for coda weapons right now. + } + for (const rawItem of manifest.items) { + if (!manifest.isOneBinPerCycle || rawItem.bin == binThisCycle) { + offersToAdd.push(rawItem); + } + } + + // For most vendors, the offers seem to roughly be in reverse order from the manifest. Coda weapons are an odd exception. + if (!manifest.isOneBinPerCycle) { + offersToAdd.reverse(); + } + } + const cycleStart = cycleOffset + cycleIndex * cycleDuration; + for (const rawItem of offersToAdd) { + const durationHoursRange = toRange(rawItem.durationHours); + const expiry = + cycleStart + + rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour; + const item: IItemManifest = { + StoreItem: rawItem.storeItem, + ItemPrices: rawItem.itemPrices?.map(itemPrice => ({ ...itemPrice, ProductCategory: "MiscItems" })), + Bin: "BIN_" + rawItem.bin, + QuantityMultiplier: 1, + Expiry: { $date: { $numberLong: expiry.toString() } }, + AllowMultipurchase: false, + Id: { + $oid: + ((cycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + + vendorInfo._id.$oid.substring(8, 16) + + rng.randomInt(0, 0xffff_ffff).toString(16).padStart(8, "0") + } + }; + if (rawItem.numRandomItemPrices) { + item.ItemPrices = []; + for (let i = 0; i != rawItem.numRandomItemPrices; ++i) { + let itemPrice: { type: string; count: IRange }; + do { + itemPrice = rng.randomElement(manifest.randomItemPricesPerBin![rawItem.bin])!; + } while (item.ItemPrices.find(x => x.ItemType == itemPrice.type)); + item.ItemPrices.push({ + ItemType: itemPrice.type, + ItemCount: rng.randomInt(itemPrice.count.minValue, itemPrice.count.maxValue), + ProductCategory: "MiscItems" + }); + } + } + if (rawItem.credits) { + const value = + typeof rawItem.credits == "number" + ? rawItem.credits + : rng.randomInt( + rawItem.credits.minValue / rawItem.credits.step, + rawItem.credits.maxValue / rawItem.credits.step + ) * rawItem.credits.step; + item.RegularPrice = [value, value]; + } + if (rawItem.platinum) { + const value = + typeof rawItem.platinum == "number" + ? rawItem.platinum + : rng.randomInt(rawItem.platinum.minValue, rawItem.platinum.maxValue); + item.PremiumPrice = [value, value]; + } + if (vendorInfo.RandomSeedType) { + item.LocTagRandSeed = rng.randomInt(0, 0xffff_ffff); + if (vendorInfo.RandomSeedType == "VRST_WEAPON") { + const highDword = rng.randomInt(0, 0xffff_ffff); + item.LocTagRandSeed = (BigInt(highDword) << 32n) | (BigInt(item.LocTagRandSeed) & 0xffffffffn); + } + } + info.ItemManifest.push(item); + } + + // Update vendor expiry + let soonestOfferExpiry: number = Number.MAX_SAFE_INTEGER; + for (const offer of info.ItemManifest) { + const offerExpiry = parseInt(offer.Expiry.$date.$numberLong); + if (soonestOfferExpiry > offerExpiry) { + soonestOfferExpiry = offerExpiry; + } + } + info.Expiry.$date.$numberLong = soonestOfferExpiry.toString(); + } + return cacheEntry; +}; + +if (isDev) { + const ads = getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest")! + .VendorInfo.ItemManifest; + if ( + ads.length != 5 || + ads[0].Bin != "BIN_4" || + ads[1].Bin != "BIN_3" || + ads[2].Bin != "BIN_2" || + ads[3].Bin != "BIN_1" || + ads[4].Bin != "BIN_0" + ) { + logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest`); + } +} 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 d520e842..a074b637 100644 --- a/src/services/statsService.ts +++ b/src/services/statsService.ts @@ -1,6 +1,5 @@ import { Stats, TStatsDatabaseDocument } from "@/src/models/statsModel"; import { - IEnemy, IStatsAdd, IStatsMax, IStatsSet, @@ -9,7 +8,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 +26,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) { @@ -58,6 +60,7 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: break; default: if (!ignoredCategories.includes(category)) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!unknownCategories[action]) { unknownCategories[action] = []; } @@ -81,7 +84,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 }); @@ -94,7 +96,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,13 +107,15 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: case "HIT_ENTITY_ITEM": case "HEADSHOT_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); @@ -126,29 +129,45 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: } } break; + } case "KILL_ENEMY": case "EXECUTE_ENEMY": case "HEADSHOT": + case "KILL_ASSIST": { playerStats.Enemies ??= []; - const enemyStatKey = { - KILL_ENEMY: "kills", - EXECUTE_ENEMY: "executions", - HEADSHOT: "headshots" - }[category] as "kills" | "executions" | "headshots"; + const enemyStatKey = ( + { + KILL_ENEMY: "kills", + EXECUTE_ENEMY: "executions", + HEADSHOT: "headshots", + KILL_ASSIST: "assists" + } as const + )[category]; for (const [type, count] of Object.entries(data as IUploadEntry)) { - const enemy = playerStats.Enemies.find(element => element.type === type); - if (enemy) { + let enemy = playerStats.Enemies.find(element => element.type === type); + if (!enemy) { + enemy = { type: type }; + playerStats.Enemies.push(enemy); + } + 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; - playerStats.Enemies.push(newEnemy); } } break; + } case "DIE": playerStats.Enemies ??= []; @@ -179,21 +198,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; @@ -229,6 +246,7 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload: default: if (!ignoredCategories.includes(category)) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!unknownCategories[action]) { unknownCategories[action] = []; } @@ -267,6 +285,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; @@ -283,15 +310,81 @@ 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, + payload.guildId + ); + break; + default: if (!ignoredCategories.includes(category)) { - if (!unknownCategories[action]) { - unknownCategories[action] = []; - } + unknownCategories[action] ??= []; unknownCategories[action].push(category); } break; @@ -303,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; @@ -394,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/services/webService.ts b/src/services/webService.ts new file mode 100644 index 00000000..77fe01fe --- /dev/null +++ b/src/services/webService.ts @@ -0,0 +1,65 @@ +import http from "http"; +import https from "https"; +import fs from "node:fs"; +import { config } from "./configService"; +import { logger } from "../utils/logger"; +import { app } from "../app"; +import { AddressInfo } from "node:net"; + +let httpServer: http.Server | undefined; +let httpsServer: https.Server | undefined; + +const tlsOptions = { + key: fs.readFileSync("static/certs/key.pem"), + cert: fs.readFileSync("static/certs/cert.pem") +}; + +export const startWebServer = (): void => { + const httpPort = config.httpPort || 80; + const httpsPort = config.httpsPort || 443; + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + httpServer = http.createServer(app); + httpServer.listen(httpPort, () => { + logger.info("HTTP server started on port " + httpPort); + // eslint-disable-next-line @typescript-eslint/no-misused-promises + httpsServer = https.createServer(tlsOptions, app); + httpsServer.listen(httpsPort, () => { + logger.info("HTTPS server started on port " + httpsPort); + + logger.info( + "Access the WebUI in your browser at http://localhost" + (httpPort == 80 ? "" : ":" + httpPort) + ); + }); + }); +}; + +export const getWebPorts = (): Record<"http" | "https", number | undefined> => { + return { + http: (httpServer?.address() as AddressInfo | undefined)?.port, + https: (httpsServer?.address() as AddressInfo | undefined)?.port + }; +}; + +export const stopWebServer = async (): Promise => { + const promises: Promise[] = []; + if (httpServer) { + promises.push( + new Promise(resolve => { + httpServer!.close(() => { + resolve(); + }); + }) + ); + } + if (httpsServer) { + promises.push( + new Promise(resolve => { + httpsServer!.close(() => { + resolve(); + }); + }) + ); + } + await Promise.all(promises); +}; diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts new file mode 100644 index 00000000..7d91e027 --- /dev/null +++ b/src/services/worldStateService.ts @@ -0,0 +1,1273 @@ +import staticWorldState from "@/static/fixed_responses/worldState/worldState.json"; +import sortieTilesets from "@/static/fixed_responses/worldState/sortieTilesets.json"; +import sortieTilesetMissions from "@/static/fixed_responses/worldState/sortieTilesetMissions.json"; +import syndicateMissions from "@/static/fixed_responses/worldState/syndicateMissions.json"; +import { buildConfig } from "@/src/services/buildConfigService"; +import { unixTimesInMs } from "@/src/constants/timeConstants"; +import { config } from "@/src/services/configService"; +import { SRng } from "@/src/services/rngService"; +import { ExportRegions, ExportSyndicates, IRegion } from "warframe-public-export-plus"; +import { + ICalendarDay, + ICalendarEvent, + ICalendarSeason, + ILiteSortie, + ISeasonChallenge, + ISortie, + ISortieMission, + ISyndicateMissionInfo, + IWorldState +} from "../types/worldStateTypes"; +import { version_compare } from "../helpers/inventoryHelpers"; + +const sortieBosses = [ + "SORTIE_BOSS_HYENA", + "SORTIE_BOSS_KELA", + "SORTIE_BOSS_VOR", + "SORTIE_BOSS_RUK", + "SORTIE_BOSS_HEK", + "SORTIE_BOSS_KRIL", + "SORTIE_BOSS_TYL", + "SORTIE_BOSS_JACKAL", + "SORTIE_BOSS_ALAD", + "SORTIE_BOSS_AMBULAS", + "SORTIE_BOSS_NEF", + "SORTIE_BOSS_RAPTOR", + "SORTIE_BOSS_PHORID", + "SORTIE_BOSS_LEPHANTIS", + "SORTIE_BOSS_INFALAD", + "SORTIE_BOSS_CORRUPTED_VOR" +] as const; + +type TSortieBoss = (typeof sortieBosses)[number]; + +const sortieBossToFaction: Record = { + SORTIE_BOSS_HYENA: "FC_CORPUS", + SORTIE_BOSS_KELA: "FC_GRINEER", + SORTIE_BOSS_VOR: "FC_GRINEER", + SORTIE_BOSS_RUK: "FC_GRINEER", + SORTIE_BOSS_HEK: "FC_GRINEER", + SORTIE_BOSS_KRIL: "FC_GRINEER", + SORTIE_BOSS_TYL: "FC_GRINEER", + SORTIE_BOSS_JACKAL: "FC_CORPUS", + SORTIE_BOSS_ALAD: "FC_CORPUS", + SORTIE_BOSS_AMBULAS: "FC_CORPUS", + SORTIE_BOSS_NEF: "FC_CORPUS", + SORTIE_BOSS_RAPTOR: "FC_CORPUS", + SORTIE_BOSS_PHORID: "FC_INFESTATION", + SORTIE_BOSS_LEPHANTIS: "FC_INFESTATION", + SORTIE_BOSS_INFALAD: "FC_INFESTATION", + SORTIE_BOSS_CORRUPTED_VOR: "FC_OROKIN" +}; + +const sortieFactionToSystemIndexes: Record = { + FC_GRINEER: [0, 2, 3, 5, 6, 9, 11, 18], + FC_CORPUS: [1, 4, 7, 8, 12, 15], + FC_INFESTATION: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15], + FC_OROKIN: [14] +}; + +const sortieFactionToFactionIndexes: Record = { + FC_GRINEER: [0], + FC_CORPUS: [1], + FC_INFESTATION: [0, 1, 2], + FC_OROKIN: [3] +}; + +const sortieBossNode: Record, string> = { + SORTIE_BOSS_ALAD: "SolNode53", + SORTIE_BOSS_AMBULAS: "SolNode51", + SORTIE_BOSS_HEK: "SolNode24", + SORTIE_BOSS_HYENA: "SolNode127", + SORTIE_BOSS_INFALAD: "SolNode166", + SORTIE_BOSS_JACKAL: "SolNode104", + SORTIE_BOSS_KELA: "SolNode193", + SORTIE_BOSS_KRIL: "SolNode99", + SORTIE_BOSS_LEPHANTIS: "SolNode712", + SORTIE_BOSS_NEF: "SettlementNode20", + SORTIE_BOSS_PHORID: "SolNode171", + SORTIE_BOSS_RAPTOR: "SolNode210", + SORTIE_BOSS_RUK: "SolNode32", + SORTIE_BOSS_TYL: "SolNode105", + SORTIE_BOSS_VOR: "SolNode108" +}; + +const eidolonJobs = [ + "/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyAss", + "/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyCap", + "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountySab", + "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyLib", + "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyCap", + "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyExt", + "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCap", + "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyTheft", + "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCache", + "/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapOne", + "/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapTwo", + "/Lotus/Types/Gameplay/Eidolon/Jobs/SabotageBountySab", + "/Lotus/Types/Gameplay/Eidolon/Jobs/RescueBountyResc" +]; + +const eidolonNarmerJobs = [ + "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AssassinateBountyAss", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AttritionBountyExt", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/ReclamationBountyTheft", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AttritionBountyLib" +]; + +const venusJobs = [ + "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobAmbush", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobExcavation", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobRecovery", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobAssassinate", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobExcavation", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobAssassinate", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobExterminate", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobResource", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobRecovery", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobResource", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobSpy", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusSpyJobSpy", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobAmbush", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobExcavation", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusTheftJobResource", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobCaches", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobResource", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobSpy", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobDefense", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobRecovery", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusPreservationJobResource", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusWetworkJobAssassinate", + "/Lotus/Types/Gameplay/Venus/Jobs/VenusWetworkJobSpy" +]; + +const venusNarmerJobs = [ + "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobAssassinate", + "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobExterminate", + "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusPreservationJobDefense", + "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusTheftJobExcavation" +]; + +const microplanetJobs = [ + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAreaDefenseBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAssassinateBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosCrpSurvivorBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosGrnSurvivorBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosKeyPiecesBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosExcavateBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosPurifyBounty" +]; + +const microplanetEndlessJobs = [ + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessAreaDefenseBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessExcavateBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessPurifyBounty" +]; + +const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0 + +const isBeforeNextExpectedWorldStateRefresh = (date: number): boolean => { + return Date.now() + 300_000 > date; +}; + +const getSortieTime = (day: number): number => { + const dayStart = EPOCH + day * 86400000; + const date = new Date(dayStart); + date.setUTCHours(12); + const isDst = new Intl.DateTimeFormat("en-US", { + timeZone: "America/Toronto", + timeZoneName: "short" + }) + .formatToParts(date) + .find(part => part.type === "timeZoneName")! + .value.includes("DT"); + return dayStart + (isDst ? 16 : 17) * 3600000; +}; + +const pushSyndicateMissions = ( + worldState: IWorldState, + day: number, + seed: number, + idSuffix: string, + syndicateTag: string +): void => { + const nodeOptions: string[] = [...syndicateMissions]; + + const rng = new SRng(seed); + const nodes: string[] = []; + for (let i = 0; i != 6; ++i) { + const index = rng.randomInt(0, nodeOptions.length - 1); + nodes.push(nodeOptions[index]); + nodeOptions.splice(index, 1); + } + + const dayStart = getSortieTime(day); + const dayEnd = getSortieTime(day + 1); + worldState.SyndicateMissions.push({ + _id: { $oid: ((dayStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + idSuffix }, + Activation: { $date: { $numberLong: dayStart.toString() } }, + Expiry: { $date: { $numberLong: dayEnd.toString() } }, + Tag: syndicateTag, + Seed: seed, + Nodes: nodes + }); +}; + +type TSortieTileset = keyof typeof sortieTilesetMissions; + +const pushTilesetModifiers = (modifiers: string[], tileset: TSortieTileset): void => { + switch (tileset) { + case "GrineerForestTileset": + modifiers.push("SORTIE_MODIFIER_HAZARD_FOG"); + break; + case "CorpusShipTileset": + case "GrineerGalleonTileset": + case "InfestedCorpusShipTileset": + modifiers.push("SORTIE_MODIFIER_HAZARD_MAGNETIC"); + modifiers.push("SORTIE_MODIFIER_HAZARD_FIRE"); + modifiers.push("SORTIE_MODIFIER_HAZARD_ICE"); + break; + case "CorpusIcePlanetTileset": + case "CorpusIcePlanetTilesetCaves": + modifiers.push("SORTIE_MODIFIER_HAZARD_COLD"); + break; + } +}; + +export const getSortie = (day: number): ISortie => { + const seed = new SRng(day).randomInt(0, 100_000); + const rng = new SRng(seed); + + const boss = rng.randomElement(sortieBosses)!; + + const nodes: string[] = []; + for (const [key, value] of Object.entries(ExportRegions)) { + if ( + sortieFactionToSystemIndexes[sortieBossToFaction[boss]].includes(value.systemIndex) && + sortieFactionToFactionIndexes[sortieBossToFaction[boss]].includes(value.factionIndex!) && + key in sortieTilesets + ) { + nodes.push(key); + } + } + + const selectedNodes: ISortieMission[] = []; + const missionTypes = new Set(); + + for (let i = 0; i < 3; i++) { + const randomIndex = rng.randomInt(0, nodes.length - 1); + const node = nodes[randomIndex]; + + const modifiers = [ + "SORTIE_MODIFIER_LOW_ENERGY", + "SORTIE_MODIFIER_IMPACT", + "SORTIE_MODIFIER_SLASH", + "SORTIE_MODIFIER_PUNCTURE", + "SORTIE_MODIFIER_EXIMUS", + "SORTIE_MODIFIER_MAGNETIC", + "SORTIE_MODIFIER_CORROSIVE", + "SORTIE_MODIFIER_VIRAL", + "SORTIE_MODIFIER_ELECTRICITY", + "SORTIE_MODIFIER_RADIATION", + "SORTIE_MODIFIER_FIRE", + "SORTIE_MODIFIER_EXPLOSION", + "SORTIE_MODIFIER_FREEZE", + "SORTIE_MODIFIER_POISON", + "SORTIE_MODIFIER_SECONDARY_ONLY", + "SORTIE_MODIFIER_SHOTGUN_ONLY", + "SORTIE_MODIFIER_SNIPER_ONLY", + "SORTIE_MODIFIER_RIFLE_ONLY", + "SORTIE_MODIFIER_BOW_ONLY" + ]; + + if (i == 2 && boss != "SORTIE_BOSS_CORRUPTED_VOR" && rng.randomInt(0, 2) == 2) { + const tileset = sortieTilesets[sortieBossNode[boss] as keyof typeof sortieTilesets] as TSortieTileset; + pushTilesetModifiers(modifiers, tileset); + + const modifierType = rng.randomElement(modifiers)!; + + selectedNodes.push({ + missionType: "MT_ASSASSINATION", + modifierType, + node: sortieBossNode[boss], + tileset + }); + continue; + } + + const tileset = sortieTilesets[node as keyof typeof sortieTilesets] as TSortieTileset; + pushTilesetModifiers(modifiers, tileset); + + const missionType = rng.randomElement(sortieTilesetMissions[tileset])!; + + if (missionTypes.has(missionType) || missionType == "MT_ASSASSINATION") { + i--; + continue; + } + + modifiers.push("SORTIE_MODIFIER_MELEE_ONLY"); // not an assassination mission, can now push this + + if (missionType != "MT_TERRITORY") { + modifiers.push("SORTIE_MODIFIER_HAZARD_RADIATION"); + } + + if (ExportRegions[node].factionIndex == 0) { + // Grineer + modifiers.push("SORTIE_MODIFIER_ARMOR"); + } else if (ExportRegions[node].factionIndex == 1) { + // Corpus + modifiers.push("SORTIE_MODIFIER_SHIELDS"); + } + + const modifierType = rng.randomElement(modifiers)!; + + selectedNodes.push({ + missionType, + modifierType, + node, + tileset + }); + nodes.splice(randomIndex, 1); + missionTypes.add(missionType); + } + + const dayStart = getSortieTime(day); + const dayEnd = getSortieTime(day + 1); + return { + _id: { $oid: ((dayStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "d4d932c97c0a3acd" }, + Activation: { $date: { $numberLong: dayStart.toString() } }, + Expiry: { $date: { $numberLong: dayEnd.toString() } }, + Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards", + Seed: seed, + Boss: boss, + Variants: selectedNodes + }; +}; + +interface IRotatingSeasonChallengePools { + daily: string[]; + weekly: string[]; + hardWeekly: string[]; +} + +const getSeasonChallengePools = (syndicateTag: string): IRotatingSeasonChallengePools => { + const syndicate = ExportSyndicates[syndicateTag]; + return { + daily: syndicate.dailyChallenges!, + weekly: syndicate.weeklyChallenges!.filter( + x => + x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/") && + !x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent") + ), + hardWeekly: syndicate.weeklyChallenges!.filter(x => x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/")) + }; +}; + +const getSeasonDailyChallenge = (pools: IRotatingSeasonChallengePools, day: number): ISeasonChallenge => { + const dayStart = EPOCH + day * 86400000; + const dayEnd = EPOCH + (day + 3) * 86400000; + const rng = new SRng(new SRng(day).randomInt(0, 100_000)); + return { + _id: { $oid: "67e1b5ca9d00cb47" + day.toString().padStart(8, "0") }, + Daily: true, + Activation: { $date: { $numberLong: dayStart.toString() } }, + Expiry: { $date: { $numberLong: dayEnd.toString() } }, + Challenge: rng.randomElement(pools.daily)! + }; +}; + +const getSeasonWeeklyChallenge = (pools: IRotatingSeasonChallengePools, week: number, id: number): ISeasonChallenge => { + const weekStart = EPOCH + week * 604800000; + const weekEnd = weekStart + 604800000; + const challengeId = week * 7 + id; + const rng = new SRng(new SRng(challengeId).randomInt(0, 100_000)); + return { + _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: rng.randomElement(pools.weekly)! + }; +}; + +const getSeasonWeeklyHardChallenge = ( + pools: IRotatingSeasonChallengePools, + week: number, + id: number +): ISeasonChallenge => { + const weekStart = EPOCH + week * 604800000; + const weekEnd = weekStart + 604800000; + const challengeId = week * 7 + id; + const rng = new SRng(new SRng(challengeId).randomInt(0, 100_000)); + return { + _id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: rng.randomElement(pools.hardWeekly)! + }; +}; + +const pushWeeklyActs = ( + activeChallenges: ISeasonChallenge[], + pools: IRotatingSeasonChallengePools, + week: number +): void => { + const weekStart = EPOCH + week * 604800000; + const weekEnd = weekStart + 604800000; + + activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 0)); + activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 1)); + activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 2)); + activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 3)); + activeChallenges.push({ + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" + }); + activeChallenges.push({ + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" + }); + activeChallenges.push({ + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" + }); +}; + +export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[], bountyCycle: number): void => { + const table = String.fromCharCode(65 + (bountyCycle % 3)); + const vaultTable = String.fromCharCode(65 + ((bountyCycle + 1) % 3)); + const deimosDTable = String.fromCharCode(65 + (bountyCycle % 2)); + + // TODO: xpAmounts need to be calculated based on the jobType somehow? + + const seed = new SRng(bountyCycle).randomInt(0, 100_000); + const bountyCycleStart = bountyCycle * 9000000; + const bountyCycleEnd = bountyCycleStart + 9000000; + + { + const rng = new SRng(seed); + syndicateMissions.push({ + _id: { + $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000008" + }, + Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, + Tag: "CetusSyndicate", + Seed: seed, + Nodes: [], + Jobs: [ + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierATable${table}Rewards`, + masteryReq: 0, + minEnemyLevel: 5, + maxEnemyLevel: 15, + xpAmounts: [430, 430, 430] + }, + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierBTable${table}Rewards`, + masteryReq: 1, + minEnemyLevel: 10, + maxEnemyLevel: 30, + xpAmounts: [620, 620, 620] + }, + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierCTable${table}Rewards`, + masteryReq: 2, + minEnemyLevel: 20, + maxEnemyLevel: 40, + xpAmounts: [670, 670, 670, 990] + }, + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierDTable${table}Rewards`, + masteryReq: 3, + minEnemyLevel: 30, + maxEnemyLevel: 50, + xpAmounts: [570, 570, 570, 570, 1110] + }, + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETable${table}Rewards`, + masteryReq: 5, + minEnemyLevel: 40, + maxEnemyLevel: 60, + xpAmounts: [740, 740, 740, 740, 1450] + }, + { + jobType: rng.randomElement(eidolonJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETable${table}Rewards`, + masteryReq: 10, + minEnemyLevel: 100, + maxEnemyLevel: 100, + xpAmounts: [840, 840, 840, 840, 1660] + }, + { + jobType: rng.randomElement(eidolonNarmerJobs), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTable${table}Rewards`, + masteryReq: 0, + minEnemyLevel: 50, + maxEnemyLevel: 70, + xpAmounts: [840, 840, 840, 840, 1650] + } + ] + }); + } + + { + const rng = new SRng(seed); + syndicateMissions.push({ + _id: { + $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000025" + }, + Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, + Tag: "SolarisSyndicate", + Seed: seed, + Nodes: [], + Jobs: [ + { + jobType: rng.randomElement(venusJobs), + rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierATable${table}Rewards`, + masteryReq: 0, + minEnemyLevel: 5, + maxEnemyLevel: 15, + xpAmounts: [340, 340, 340] + }, + { + jobType: rng.randomElement(venusJobs), + rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierBTable${table}Rewards`, + masteryReq: 1, + minEnemyLevel: 10, + maxEnemyLevel: 30, + xpAmounts: [660, 660, 660] + }, + { + jobType: rng.randomElement(venusJobs), + rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierCTable${table}Rewards`, + masteryReq: 2, + minEnemyLevel: 20, + maxEnemyLevel: 40, + xpAmounts: [610, 610, 610, 900] + }, + { + jobType: rng.randomElement(venusJobs), + rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierDTable${table}Rewards`, + masteryReq: 3, + minEnemyLevel: 30, + maxEnemyLevel: 50, + xpAmounts: [600, 600, 600, 600, 1170] + }, + { + jobType: rng.randomElement(venusJobs), + rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable${table}Rewards`, + masteryReq: 5, + minEnemyLevel: 40, + maxEnemyLevel: 60, + xpAmounts: [690, 690, 690, 690, 1350] + }, + { + jobType: rng.randomElement(venusJobs), + rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable${table}Rewards`, + masteryReq: 10, + minEnemyLevel: 100, + maxEnemyLevel: 100, + xpAmounts: [840, 840, 840, 840, 1660] + }, + { + jobType: rng.randomElement(venusNarmerJobs), + rewards: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTableBRewards", + masteryReq: 0, + minEnemyLevel: 50, + maxEnemyLevel: 70, + xpAmounts: [780, 780, 780, 780, 1540] + } + ] + }); + } + + { + const rng = new SRng(seed); + syndicateMissions.push({ + _id: { + $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000002" + }, + Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, + Tag: "EntratiSyndicate", + Seed: seed, + Nodes: [], + Jobs: [ + { + jobType: rng.randomElement(microplanetJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierATable${table}Rewards`, + masteryReq: 0, + minEnemyLevel: 5, + maxEnemyLevel: 15, + xpAmounts: [5, 5, 5] + }, + { + jobType: rng.randomElement(microplanetJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierCTable${table}Rewards`, + masteryReq: 1, + minEnemyLevel: 15, + maxEnemyLevel: 25, + xpAmounts: [12, 12, 12] + }, + { + jobType: rng.randomElement(microplanetEndlessJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTable${table}Rewards`, + masteryReq: 5, + minEnemyLevel: 25, + maxEnemyLevel: 30, + endless: true, + xpAmounts: [14, 14, 14] + }, + { + jobType: rng.randomElement(microplanetJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierDTable${deimosDTable}Rewards`, + masteryReq: 2, + minEnemyLevel: 30, + maxEnemyLevel: 40, + xpAmounts: [17, 17, 17, 25] + }, + { + jobType: rng.randomElement(microplanetJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards`, + masteryReq: 3, + minEnemyLevel: 40, + maxEnemyLevel: 60, + xpAmounts: [22, 22, 22, 22, 43] + }, + { + jobType: rng.randomElement(microplanetJobs), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards`, + masteryReq: 10, + minEnemyLevel: 100, + maxEnemyLevel: 100, + xpAmounts: [25, 25, 25, 25, 50] + }, + { + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierATable${vaultTable}Rewards`, + masteryReq: 5, + minEnemyLevel: 30, + maxEnemyLevel: 40, + xpAmounts: [2, 2, 2, 4], + locationTag: "ChamberB", + isVault: true + }, + { + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierBTable${vaultTable}Rewards`, + masteryReq: 5, + minEnemyLevel: 40, + maxEnemyLevel: 50, + xpAmounts: [4, 4, 4, 5], + locationTag: "ChamberA", + isVault: true + }, + { + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierCTable${vaultTable}Rewards`, + masteryReq: 5, + minEnemyLevel: 50, + maxEnemyLevel: 60, + xpAmounts: [5, 5, 5, 7], + locationTag: "ChamberC", + isVault: true + } + ] + }); + } +}; + +const birthdays: number[] = [ + 1, // Kaya + 45, // Lettie + 74, // Minerva (MinervaVelemirDialogue_rom.dialogue) + 143, // Amir + 166, // Flare + 191, // Aoi + 306, // Eleanor + 307, // Arthur + 338, // Quincy + 355 // Velimir (MinervaVelemirDialogue_rom.dialogue) +]; + +const getCalendarSeason = (week: number): ICalendarSeason => { + const seasonIndex = week % 4; + const seasonDay1 = [1, 91, 182, 274][seasonIndex]; + const seasonDay91 = seasonDay1 + 90; + const eventDays: ICalendarDay[] = []; + for (const day of birthdays) { + if (day < seasonDay1) { + continue; + } + if (day >= seasonDay91) { + break; + } + //logger.debug(`birthday on day ${day}`); + eventDays.push({ day, events: [] }); // This is how CET_PLOT looks in worldState as of around 38.5.0 + } + const rng = new SRng(new SRng(week).randomInt(0, 100_000)); + const challenges = [ + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesEasy", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesMedium", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesHard", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithMeleeEasy", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithMeleeMedium", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithMeleeHard", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithAbilitiesEasy", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithAbilitiesMedium", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithAbilitiesHard", + "/Lotus/Types/Challenges/Calendar1999/CalendarDestroyPropsEasy", + "/Lotus/Types/Challenges/Calendar1999/CalendarDestroyPropsMedium", + "/Lotus/Types/Challenges/Calendar1999/CalendarDestroyPropsHard", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEximusEasy", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEximusMedium", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillEximusHard", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesEasy", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesMedium", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesHard", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithAbilitiesEasy", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithAbilitiesMedium", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithAbilitiesHard", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillTankHard", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithMeleeEasy", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithMeleeMedium", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithMeleeHard", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesEasy", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesMedium", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesHard", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithAbilitiesEasy", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithAbilitiesMedium", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithAbilitiesHard", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithMeleeEasy", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithMeleeMedium", + "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithMeleeHard" + ]; + const rewardRanges: number[] = []; + const upgradeRanges: number[] = []; + for (let i = 0; i != 6; ++i) { + const chunkDay1 = seasonDay1 + i * 15; + const chunkDay13 = chunkDay1 - 1 + 13; + let challengeDay: number; + do { + challengeDay = rng.randomInt(chunkDay1, chunkDay13); + } while (birthdays.indexOf(challengeDay) != -1); + + let challengeIndex; + let challenge; + do { + challengeIndex = rng.randomInt(0, challenges.length - 1); + challenge = challenges[challengeIndex]; + } while (i < 2 && !challenge.endsWith("Easy")); // First 2 challenges should be easy + challenges.splice(challengeIndex, 1); + + //logger.debug(`challenge on day ${challengeDay}`); + eventDays.push({ + day: challengeDay, + events: [{ type: "CET_CHALLENGE", challenge }] + }); + + rewardRanges.push(challengeDay); + if (i == 0 || i == 3 || i == 5) { + upgradeRanges.push(challengeDay); + } + } + rewardRanges.push(seasonDay91); + upgradeRanges.push(seasonDay91); + + const rewards = [ + "/Lotus/StoreItems/Types/Items/MiscItems/UtilityUnlocker", + "/Lotus/StoreItems/Types/Recipes/Components/FormaAuraBlueprint", + "/Lotus/StoreItems/Types/Recipes/Components/FormaBlueprint", + "/Lotus/StoreItems/Types/Recipes/Components/WeaponUtilityUnlockerBlueprint", + "/Lotus/StoreItems/Types/Items/MiscItems/WeaponMeleeArcaneUnlocker", + "/Lotus/StoreItems/Types/Items/MiscItems/WeaponSecondaryArcaneUnlocker", + "/Lotus/StoreItems/Types/Items/MiscItems/WeaponPrimaryArcaneUnlocker", + "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/CircuitSilverSteelPathFusionBundle", + "/Lotus/StoreItems/Types/BoosterPacks/CalendarRivenPack", + "/Lotus/Types/StoreItems/Packages/Calendar/CalendarKuvaBundleSmall", + "/Lotus/Types/StoreItems/Packages/Calendar/CalendarKuvaBundleLarge", + "/Lotus/StoreItems/Types/BoosterPacks/CalendarArtifactPack", + "/Lotus/StoreItems/Types/BoosterPacks/CalendarMajorArtifactPack", + "/Lotus/Types/StoreItems/Boosters/AffinityBooster3DayStoreItem", + "/Lotus/Types/StoreItems/Boosters/ModDropChanceBooster3DayStoreItem", + "/Lotus/Types/StoreItems/Boosters/ResourceDropChance3DayStoreItem", + "/Lotus/StoreItems/Types/Items/MiscItems/Forma", + "/Lotus/StoreItems/Types/Recipes/Components/OrokinCatalystBlueprint", + "/Lotus/StoreItems/Types/Recipes/Components/OrokinReactorBlueprint", + "/Lotus/StoreItems/Types/Items/MiscItems/WeaponUtilityUnlocker", + "/Lotus/Types/StoreItems/Packages/Calendar/CalendarVosforPack", + "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalOrange", + "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalNira", + "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalGreen", + "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalBoreal", + "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalAmar", + "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalViolet" + ]; + for (let i = 0; i != rewardRanges.length - 1; ++i) { + const events: ICalendarEvent[] = []; + for (let j = 0; j != 2; ++j) { + const rewardIndex = rng.randomInt(0, rewards.length - 1); + events.push({ type: "CET_REWARD", reward: rewards[rewardIndex] }); + rewards.splice(rewardIndex, 1); + } + + //logger.debug(`trying to fit rewards between day ${rewardRanges[i]} and ${rewardRanges[i + 1]}`); + let day: number; + do { + day = rng.randomInt(rewardRanges[i] + 1, rewardRanges[i + 1] - 1); + } while (eventDays.find(x => x.day == day)); + eventDays.push({ day, events }); + } + + const upgradesByHexMember = [ + [ + "/Lotus/Upgrades/Calendar/AttackAndMovementSpeedOnCritMelee", + "/Lotus/Upgrades/Calendar/ElectricalDamageOnBulletJump", + "/Lotus/Upgrades/Calendar/ElectricDamagePerDistance", + "/Lotus/Upgrades/Calendar/ElectricStatusDamageAndChance", + "/Lotus/Upgrades/Calendar/OvershieldCap", + "/Lotus/Upgrades/Calendar/SpeedBuffsWhenAirborne" + ], + [ + "/Lotus/Upgrades/Calendar/AbilityStrength", + "/Lotus/Upgrades/Calendar/EnergyOrbToAbilityRange", + "/Lotus/Upgrades/Calendar/MagnetStatusPull", + "/Lotus/Upgrades/Calendar/MagnitizeWithinRangeEveryXCasts", + "/Lotus/Upgrades/Calendar/PowerStrengthAndEfficiencyPerEnergySpent", + "/Lotus/Upgrades/Calendar/SharedFreeAbilityEveryXCasts" + ], + [ + "/Lotus/Upgrades/Calendar/EnergyWavesOnCombo", + "/Lotus/Upgrades/Calendar/FinisherChancePerComboMultiplier", + "/Lotus/Upgrades/Calendar/MeleeAttackSpeed", + "/Lotus/Upgrades/Calendar/MeleeCritChance", + "/Lotus/Upgrades/Calendar/MeleeSlideFowardMomentumOnEnemyHit", + "/Lotus/Upgrades/Calendar/RadialJavelinOnHeavy" + ], + [ + "/Lotus/Upgrades/Calendar/Armor", + "/Lotus/Upgrades/Calendar/CloneActiveCompanionForEnergySpent", + "/Lotus/Upgrades/Calendar/CompanionDamage", + "/Lotus/Upgrades/Calendar/CompanionsBuffNearbyPlayer", + "/Lotus/Upgrades/Calendar/CompanionsRadiationChance", + "/Lotus/Upgrades/Calendar/RadiationProcOnTakeDamage", + "/Lotus/Upgrades/Calendar/ReviveEnemyAsSpectreOnKill" + ], + [ + "/Lotus/Upgrades/Calendar/EnergyOrbsGrantShield", + "/Lotus/Upgrades/Calendar/EnergyRestoration", + "/Lotus/Upgrades/Calendar/ExplodingHealthOrbs", + "/Lotus/Upgrades/Calendar/GenerateOmniOrbsOnWeakKill", + "/Lotus/Upgrades/Calendar/HealingEffects", + "/Lotus/Upgrades/Calendar/OrbsDuplicateOnPickup" + ], + [ + "/Lotus/Upgrades/Calendar/BlastEveryXShots", + "/Lotus/Upgrades/Calendar/GasChanceToPrimaryAndSecondary", + "/Lotus/Upgrades/Calendar/GuidingMissilesChance", + "/Lotus/Upgrades/Calendar/MagazineCapacity", + "/Lotus/Upgrades/Calendar/PunchToPrimary", + "/Lotus/Upgrades/Calendar/RefundBulletOnStatusProc", + "/Lotus/Upgrades/Calendar/StatusChancePerAmmoSpent" + ] + ]; + for (let i = 0; i != upgradeRanges.length - 1; ++i) { + // Pick 3 unique hex members + const hexMembersPickedForThisDay: number[] = []; + for (let j = 0; j != 3; ++j) { + let hexMemberIndex: number; + do { + hexMemberIndex = rng.randomInt(0, upgradesByHexMember.length - 1); + } while (hexMembersPickedForThisDay.indexOf(hexMemberIndex) != -1); + hexMembersPickedForThisDay.push(hexMemberIndex); + } + hexMembersPickedForThisDay.sort(); // Always present them in the same order + + // For each hex member, pick an upgrade that was not yet picked this season. + const events: ICalendarEvent[] = []; + for (const hexMemberIndex of hexMembersPickedForThisDay) { + const upgrades = upgradesByHexMember[hexMemberIndex]; + const upgradeIndex = rng.randomInt(0, upgrades.length - 1); + events.push({ type: "CET_UPGRADE", upgrade: upgrades[upgradeIndex] }); + upgrades.splice(upgradeIndex, 1); + } + + //logger.debug(`trying to fit upgrades between day ${upgradeRanges[i]} and ${upgradeRanges[i + 1]}`); + let day: number; + do { + day = rng.randomInt(upgradeRanges[i] + 1, upgradeRanges[i + 1] - 1); + } while (eventDays.find(x => x.day == day)); + eventDays.push({ day, events }); + } + + eventDays.sort((a, b) => a.day - b.day); + + const weekStart = EPOCH + week * 604800000; + const weekEnd = weekStart + 604800000; + return { + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Days: eventDays, + Season: (["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"] as const)[seasonIndex], + YearIteration: Math.trunc(week / 4), + Version: 19, + UpgradeAvaliabilityRequirements: ["/Lotus/Upgrades/Calendar/1999UpgradeApplicationRequirement"] + }; +}; + +export const getWorldState = (buildLabel?: string): IWorldState => { + 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 buildLabel == "string" ? buildLabel.split(" ").join("+") : buildConfig.buildLabel, + Time: config.worldState?.lockTime || Math.round(Date.now() / 1000), + Goals: [], + Alerts: [], + Sorties: [], + LiteSorties: [], + GlobalUpgrades: [], + EndlessXpChoices: [], + KnownCalendarSeasons: [], + ...staticWorldState, + SyndicateMissions: [...staticWorldState.SyndicateMissions] + }; + + // Omit void fissures for versions prior to Dante Unbound to avoid script errors. + if (buildLabel && version_compare(buildLabel, "2024.03.24.20.00") < 0) { + worldState.ActiveMissions = []; + if (version_compare(buildLabel, "2017.10.12.17.04") < 0) { + // Old versions seem to really get hung up on not being able to load these. + worldState.PVPChallengeInstances = []; + } + } + + 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" + }); + } + + // Nightwave Challenges + const nightwaveSyndicateTag = getNightwaveSyndicateTag(buildLabel); + if (nightwaveSyndicateTag) { + worldState.SeasonInfo = { + Activation: { $date: { $numberLong: "1715796000000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + AffiliationTag: nightwaveSyndicateTag, + Season: nightwaveTagToSeason[nightwaveSyndicateTag], + Phase: 0, + Params: "", + ActiveChallenges: [] + }; + const pools = getSeasonChallengePools(nightwaveSyndicateTag); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 2)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 1)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 0)); + if (isBeforeNextExpectedWorldStateRefresh(EPOCH + (day + 1) * 86400000)) { + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day + 1)); + } + pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week); + if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) { + pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week + 1); + } + } + + // Elite Sanctuary Onslaught cycling every week + worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = new SRng(week).randomInt(0, 0xff_ffff); + + // Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation + let bountyCycle = Math.trunc(Date.now() / 9000000); + let bountyCycleEnd: number | undefined; + do { + const bountyCycleStart = bountyCycle * 9000000; + bountyCycleEnd = bountyCycleStart + 9000000; + worldState.SyndicateMissions.push({ + _id: { $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000029" }, + Activation: { $date: { $numberLong: bountyCycleStart.toString() } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } }, + Tag: "ZarimanSyndicate", + Seed: bountyCycle, + Nodes: [] + }); + worldState.SyndicateMissions.push({ + _id: { $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000004" }, + Activation: { $date: { $numberLong: bountyCycleStart.toString() } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } }, + Tag: "EntratiLabSyndicate", + Seed: bountyCycle, + Nodes: [] + }); + worldState.SyndicateMissions.push({ + _id: { $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000006" }, + Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } }, + Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } }, + Tag: "HexSyndicate", + Seed: bountyCycle, + Nodes: [] + }); + + pushClassicBounties(worldState.SyndicateMissions, bountyCycle); + } while (isBeforeNextExpectedWorldStateRefresh(bountyCycleEnd) && ++bountyCycle); + + 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: "" + }); + } + + // Sortie & syndicate missions cycling every day (at 16:00 or 17:00 UTC depending on if London, OT is observing DST) + { + const rollover = getSortieTime(day); + + if (Date.now() < rollover) { + worldState.Sorties.push(getSortie(day - 1)); + } + if (isBeforeNextExpectedWorldStateRefresh(rollover)) { + worldState.Sorties.push(getSortie(day)); + } + + // The client does not seem to respect activation for classic syndicate missions, so only pushing current ones. + const sdy = Date.now() >= rollover ? day : day - 1; + const rng = new SRng(sdy); + pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48049", "ArbitersSyndicate"); + pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4804a", "CephalonSudaSyndicate"); + pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4804e", "NewLokaSyndicate"); + pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48050", "PerrinSyndicate"); + pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4805e", "RedVeilSyndicate"); + pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48061", "SteelMeridianSyndicate"); + } + + // Archon Hunt cycling every week + worldState.LiteSorties.push(getLiteSortie(week)); + if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) { + worldState.LiteSorties.push(getLiteSortie(week + 1)); + } + + // Circuit choices cycling every week + worldState.EndlessXpChoices.push({ + Category: "EXC_NORMAL", + Choices: [ + ["Nidus", "Octavia", "Harrow"], + ["Gara", "Khora", "Revenant"], + ["Garuda", "Baruuk", "Hildryn"], + ["Excalibur", "Trinity", "Ember"], + ["Loki", "Mag", "Rhino"], + ["Ash", "Frost", "Nyx"], + ["Saryn", "Vauban", "Nova"], + ["Nekros", "Valkyr", "Oberon"], + ["Hydroid", "Mirage", "Limbo"], + ["Mesa", "Chroma", "Atlas"], + ["Ivara", "Inaros", "Titania"] + ][week % 12] + }); + worldState.EndlessXpChoices.push({ + Category: "EXC_HARD", + Choices: [ + ["Boar", "Gammacor", "Angstrum", "Gorgon", "Anku"], + ["Bo", "Latron", "Furis", "Furax", "Strun"], + ["Lex", "Magistar", "Boltor", "Bronco", "CeramicDagger"], + ["Torid", "DualToxocyst", "DualIchor", "Miter", "Atomos"], + ["AckAndBrunt", "Soma", "Vasto", "NamiSolo", "Burston"], + ["Zylok", "Sibear", "Dread", "Despair", "Hate"], + ["Dera", "Sybaris", "Cestra", "Sicarus", "Okina"], + ["Braton", "Lato", "Skana", "Paris", "Kunai"] + ][week % 8] + }); + + // 1999 Calendar Season cycling every week + YearIteration every 4 weeks + worldState.KnownCalendarSeasons.push(getCalendarSeason(week)); + if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) { + worldState.KnownCalendarSeasons.push(getCalendarSeason(week + 1)); + } + + // Sentient Anomaly cycling every 30 minutes + const halfHour = Math.trunc(Date.now() / (unixTimesInMs.hour / 2)); + const tmp = { + cavabegin: "1690761600", + PurchasePlatformLockEnabled: true, + tcsn: true, + pgr: { + ts: "1732572900", + en: "CUSTOM DECALS @ ZEVILA", + fr: "DECALS CUSTOM @ ZEVILA", + it: "DECALCOMANIE PERSONALIZZATE @ ZEVILA", + de: "AUFKLEBER NACH WUNSCH @ ZEVILA", + es: "CALCOMANÍAS PERSONALIZADAS @ ZEVILA", + pt: "DECALQUES PERSONALIZADOS NA ZEVILA", + ru: "ПОЛЬЗОВАТЕЛЬСКИЕ НАКЛЕЙКИ @ ЗеВиЛа", + pl: "NOWE NAKLEJKI @ ZEVILA", + uk: "КОРИСТУВАЦЬКІ ДЕКОЛІ @ ЗІВІЛА", + tr: "ÖZEL ÇIKARTMALAR @ ZEVILA", + ja: "カスタムデカール @ ゼビラ", + zh: "定制贴花认准泽威拉", + ko: "커스텀 데칼 @ ZEVILA", + tc: "自訂貼花 @ ZEVILA", + th: "รูปลอกสั่งทำที่ ZEVILA" + }, + ennnd: true, + mbrt: true, + sfn: [550, 553, 554, 555][halfHour % 4] + }; + worldState.Tmp = JSON.stringify(tmp); + + return worldState; +}; + +export const idToBountyCycle = (id: string): number => { + return Math.trunc((parseInt(id.substring(0, 8), 16) * 1000) / 9000_000); +}; + +export const idToDay = (id: string): number => { + return Math.trunc((parseInt(id.substring(0, 8), 16) * 1000 - EPOCH) / 86400_000); +}; + +export const idToWeek = (id: string): number => { + return Math.trunc((parseInt(id.substring(0, 8), 16) * 1000 - EPOCH) / 604800_000); +}; + +export const getLiteSortie = (week: number): ILiteSortie => { + const boss = (["SORTIE_BOSS_AMAR", "SORTIE_BOSS_NIRA", "SORTIE_BOSS_BOREAL"] as const)[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 && + !isArchwingMission(value) && + value.missionIndex != 0 && // Exclude MT_ASSASSINATION + value.missionIndex != 23 && // Exclude junctions + value.missionIndex != 28 && // Exclude open worlds + value.missionIndex != 32 // Exclude railjack + ) { + nodes.push(key); + } + } + + const seed = new SRng(week).randomInt(0, 100_000); + const rng = new SRng(seed); + const firstNodeIndex = rng.randomInt(0, nodes.length - 1); + const firstNode = nodes[firstNodeIndex]; + nodes.splice(firstNodeIndex, 1); + + const weekStart = EPOCH + week * 604800000; + const weekEnd = weekStart + 604800000; + return { + _id: { + $oid: ((weekStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "5e23a244740a190c" + }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards", + Seed: seed, + 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 + } + ] + }; +}; + +export const isArchwingMission = (node: IRegion): boolean => { + if (node.name.indexOf("Archwing") != -1) { + return true; + } + // SettlementNode10 + if (node.missionIndex == 25) { + return true; + } + return false; +}; + +export const getNightwaveSyndicateTag = (buildLabel: string | undefined): string | undefined => { + if (!buildLabel || version_compare(buildLabel, "2025.05.20.10.18") >= 0) { + return "RadioLegionIntermission13Syndicate"; + } + if (version_compare(buildLabel, "2025.02.05.11.19") >= 0) { + return "RadioLegionIntermission12Syndicate"; + } + return undefined; +}; + +const nightwaveTagToSeason: Record = { + RadioLegionIntermission13Syndicate: 15, + RadioLegionIntermission12Syndicate: 14 +}; diff --git a/src/types/commonTypes.ts b/src/types/commonTypes.ts index eebd9410..a9335fff 100644 --- a/src/types/commonTypes.ts +++ b/src/types/commonTypes.ts @@ -1,9 +1,23 @@ +import { ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; + export interface IOid { $oid: string; } +export interface IOidWithLegacySupport { + $oid?: string; + $id?: string; +} + export interface IMongoDate { $date: { $numberLong: string; }; } + +export interface IReward { + items: ITypeCount[]; + credits: number; +} + +export type IJunctionRewards = Record; diff --git a/src/types/friendTypes.ts b/src/types/friendTypes.ts new file mode 100644 index 00000000..f4799201 --- /dev/null +++ b/src/types/friendTypes.ts @@ -0,0 +1,24 @@ +import { Types } from "mongoose"; +import { IMongoDate, IOidWithLegacySupport } from "./commonTypes"; + +export interface IFriendInfo { + _id: IOidWithLegacySupport; + DisplayName?: string; + PlatformNames?: string[]; + PlatformAccountId?: string; + Status?: number; + ActiveAvatarImageType?: string; + LastLogin?: IMongoDate; + PlayerLevel?: number; + Suffix?: number; + Note?: string; + Favorite?: boolean; + NewRequest?: boolean; +} + +export interface IFriendship { + owner: Types.ObjectId; + friend: Types.ObjectId; + Note?: string; + Favorite?: boolean; +} 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..7913c5fe 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -1,52 +1,223 @@ import { Types } from "mongoose"; -import { IOid, IMongoDate } from "@/src/types/commonTypes"; -import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IOid, IMongoDate, IOidWithLegacySupport } from "@/src/types/commonTypes"; +import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IPictureFrameInfo } from "./shipTypes"; +import { IFriendInfo } from "./friendTypes"; -export interface IGuild { +export interface IGuildClient { + _id: IOidWithLegacySupport; Name: string; + MOTD: string; + LongMOTD?: ILongMOTD; + Members: IGuildMemberClient[]; + Ranks: IGuildRank[]; + Tier: number; + Emblem?: boolean; + Vault: IGuildVault; + ActiveDojoColorResearch: string; + Class: number; + XP: number; + IsContributor: boolean; + NumContributors: number; + CeremonyResetDate?: IMongoDate; + CrossPlatformEnabled?: boolean; + AutoContributeFromVault?: boolean; + AllianceId?: IOidWithLegacySupport; } -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[]; + VaultDecoRecipes?: ITypeCount[]; + 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; +} + +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 + Host = 32, // No longer used in modern versions + 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[]; +} + +// 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 + _id: IOidWithLegacySupport; // ID of the guild Name: string; Tier: number; + TradeTax?: number; FixedContributions: boolean; DojoRevision: number; + AllianceId?: IOidWithLegacySupport; + 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 { - id: IOid; + id: IOidWithLegacySupport; + SortId?: IOidWithLegacySupport; pf: string; // Prefab (.level) ppf: string; - pi?: IOid; // Parent ID. N/A to root. + pi?: IOidWithLegacySupport; // Parent ID. N/A to root. op?: string; // Name of the door within this room that leads to its parent. N/A to root. pp?: string; // Name of the door within the parent that leads to this room. N/A to root. Name?: string; Message?: string; RegularCredits?: number; // "Collecting Materials" state: Number of credits that were donated. MiscItems?: IMiscItem[]; // "Collecting Materials" state: Resources that were donated. - CompletionTime?: IMongoDate; + CompletionTime?: IMongoDate; // new versions + TimeRemaining?: number; // old versions + RushPlatinum?: number; + DestructionTime?: IMongoDate; // new versions + DestructionTimeRemaining?: number; // old versions + Decos?: IDojoDecoClient[]; DecoCapacity?: number; + PaintBot?: IOidWithLegacySupport; + PendingColors?: number[]; + Colors?: number[]; + PendingLights?: number[]; + Lights?: number[]; + Settings?: string; } export interface IDojoComponentDatabase - extends Omit { + extends Omit< + IDojoComponentClient, + "id" | "SortId" | "pi" | "CompletionTime" | "DestructionTime" | "Decos" | "PaintBot" + > { _id: Types.ObjectId; + SortId?: Types.ObjectId; pi?: Types.ObjectId; CompletionTime?: Date; + CompletionLogPending?: boolean; + DestructionTime?: Date; + Decos?: IDojoDecoDatabase[]; + PaintBot?: Types.ObjectId; + Leaderboard?: IDojoLeaderboardEntry[]; +} + +export interface IDojoDecoClient { + id: IOidWithLegacySupport; + Type: string; + Pos: number[]; + Rot: number[]; + Scale?: number; + Name?: string; // for teleporters + Sockets?: number; + RegularCredits?: number; + MiscItems?: IMiscItem[]; + CompletionTime?: IMongoDate; // new versions + TimeRemaining?: number; // old versions + RushPlatinum?: number; + PictureFrameInfo?: IPictureFrameInfo; + Pending?: boolean; +} + +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 +231,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: IOidWithLegacySupport; + 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: IOidWithLegacySupport; + Name: string; + Tier: number; + Pending: boolean; + Emblem?: boolean; + Permissions: number; + MemberCount: number; + ClanLeader?: string; + ClanLeaderId?: IOidWithLegacySupport; + 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..8cc0d56f 100644 --- a/src/types/inventoryTypes/commonInventoryTypes.ts +++ b/src/types/inventoryTypes/commonInventoryTypes.ts @@ -1,4 +1,4 @@ -import { IMongoDate, IOid } from "@/src/types/commonTypes"; +import { IMongoDate, IOid, IOidWithLegacySupport } from "@/src/types/commonTypes"; import { Types } from "mongoose"; import { ICrewShipCustomization, @@ -90,12 +90,13 @@ export interface IEquipmentSelection { export interface IEquipmentClient extends Omit< IEquipmentDatabase, - "_id" | "InfestationDate" | "Expiry" | "UpgradesExpiry" | "CrewMembers" | "Details" + "_id" | "InfestationDate" | "Expiry" | "UpgradesExpiry" | "UmbraDate" | "CrewMembers" | "Details" > { - ItemId: IOid; + ItemId: IOidWithLegacySupport; InfestationDate?: IMongoDate; Expiry?: IMongoDate; UpgradesExpiry?: IMongoDate; + UmbraDate?: IMongoDate; CrewMembers?: ICrewShipMembersClient; Details?: IKubrowPetDetailsClient; } @@ -106,7 +107,8 @@ export enum EquipmentFeatures { GRAVIMAG_INSTALLED = 4, GILDED = 8, ARCANE_SLOT = 32, - INCARNON_GENESIS = 512 + INCARNON_GENESIS = 512, + VALENCE_SWAP = 1024 } export interface IEquipmentDatabase { @@ -133,12 +135,15 @@ export interface IEquipmentDatabase { OffensiveUpgrade?: string; DefensiveUpgrade?: string; UpgradesExpiry?: Date; + UmbraDate?: Date; // related to scrapped "echoes of umbra" feature ArchonCrystalUpgrades?: IArchonCrystalUpgrade[]; Weapon?: ICrewShipWeapon; Customization?: ICrewShipCustomization; RailjackImage?: IFlavourItem; CrewMembers?: ICrewShipMembersDatabase; Details?: IKubrowPetDetailsDatabase; + Favorite?: boolean; + IsNew?: boolean; _id: Types.ObjectId; } diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index e27df725..32cac658 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -1,8 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Types } from "mongoose"; -import { IOid, IMongoDate } from "../commonTypes"; +import { IOid, IMongoDate, IOidWithLegacySupport } from "../commonTypes"; import { - ArtifactPolarity, IColor, IItemConfig, IOperatorConfigClient, @@ -11,75 +10,91 @@ import { IEquipmentClient, IOperatorConfigDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes"; +import { IFingerprintStat, RivenFingerprint } from "@/src/helpers/rivenHelper"; +import { IOrbiter } from "../personalRoomsTypes"; +import { ICountedStoreItem } from "warframe-public-export-plus"; + +export type InventoryDatabaseEquipment = { + [_ in TEquipmentKey]: IEquipmentDatabase[]; +}; export interface IInventoryDatabase extends Omit< - IInventoryClient, - | "TrainingDate" - | "LoadOutPresets" - | "Mailbox" - | "GuildId" - | "PendingRecipes" - | "Created" - | "QuestKeys" - | "BlessingCooldown" - | "Ships" - | "WeaponSkins" - | "Upgrades" - | "CrewShipSalvagedWeaponSkins" - | "CrewShipWeaponSkins" - | "AdultOperatorLoadOuts" - | "OperatorLoadOuts" - | "KahlLoadOuts" - | "InfestedFoundry" - | "DialogueHistory" - | "KubrowPetEggs" - | TEquipmentKey - > { + IInventoryClient, + | "TrainingDate" + | "LoadOutPresets" + | "Mailbox" + | "GuildId" + | "PendingRecipes" + | "Created" + | "QuestKeys" + | "BlessingCooldown" + | "Ships" + | "WeaponSkins" + | "Upgrades" + | "CrewShipWeaponSkins" + | "CrewShipSalvagedWeaponSkins" + | "AdultOperatorLoadOuts" + | "OperatorLoadOuts" + | "KahlLoadOuts" + | "InfestedFoundry" + | "DialogueHistory" + | "KubrowPetEggs" + | "PendingCoupon" + | "Drones" + | "RecentVendorPurchases" + | "NextRefill" + | "Nemesis" + | "NemesisHistory" + | "EntratiVaultCountResetDate" + | "BrandedSuits" + | "LockedWeaponGroup" + | "PersonalTechProjects" + | "LastSortieReward" + | "LastLiteSortieReward" + | "CrewMembers" + | "QualifyingInvasions" + | "LastInventorySync" + | "EndlessXP" + | TEquipmentKey + >, + InventoryDatabaseEquipment { accountOwnerId: Types.ObjectId; Created: Date; TrainingDate: Date; 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; + BlessingCooldown?: Date; Ships: Types.ObjectId[]; WeaponSkins: IWeaponSkinDatabase[]; Upgrades: IUpgradeDatabase[]; - CrewShipSalvagedWeaponSkins: IUpgradeDatabase[]; CrewShipWeaponSkins: IUpgradeDatabase[]; + CrewShipSalvagedWeaponSkins: IUpgradeDatabase[]; AdultOperatorLoadOuts: IOperatorConfigDatabase[]; OperatorLoadOuts: IOperatorConfigDatabase[]; KahlLoadOuts: IOperatorConfigDatabase[]; InfestedFoundry?: IInfestedFoundryDatabase; DialogueHistory?: IDialogueHistoryDatabase; KubrowPetEggs?: IKubrowPetEggDatabase[]; - - Suits: IEquipmentDatabase[]; - LongGuns: IEquipmentDatabase[]; - Pistols: IEquipmentDatabase[]; - Melee: IEquipmentDatabase[]; - SpecialItems: IEquipmentDatabase[]; - Sentinels: IEquipmentDatabase[]; - SentinelWeapons: IEquipmentDatabase[]; - SpaceSuits: IEquipmentDatabase[]; - SpaceGuns: IEquipmentDatabase[]; - SpaceMelee: IEquipmentDatabase[]; - Hoverboards: IEquipmentDatabase[]; - OperatorAmps: IEquipmentDatabase[]; - MoaPets: IEquipmentDatabase[]; - Scoops: IEquipmentDatabase[]; - Horses: IEquipmentDatabase[]; - DrifterGuns: IEquipmentDatabase[]; - DrifterMelee: IEquipmentDatabase[]; - Motorcycles: IEquipmentDatabase[]; - CrewShips: IEquipmentDatabase[]; - DataKnives: IEquipmentDatabase[]; - MechSuits: IEquipmentDatabase[]; - CrewShipHarnesses: IEquipmentDatabase[]; - KubrowPets: IEquipmentDatabase[]; + PendingCoupon?: IPendingCouponDatabase; + Drones: IDroneDatabase[]; + RecentVendorPurchases?: IRecentVendorPurchaseDatabase[]; + NextRefill?: Date; + Nemesis?: INemesisDatabase; + NemesisHistory?: INemesisBaseDatabase[]; + EntratiVaultCountResetDate?: Date; + BrandedSuits?: Types.ObjectId[]; + LockedWeaponGroup?: ILockedWeaponGroupDatabase; + PersonalTechProjects: IPersonalTechProjectDatabase[]; + LastSortieReward?: ILastSortieRewardDatabase[]; + LastLiteSortieReward?: ILastSortieRewardDatabase[]; + CrewMembers: ICrewMemberDatabase[]; + QualifyingInvasions: IInvasionProgressDatabase[]; + LastInventorySync?: Types.ObjectId; + EndlessXP?: IEndlessXpProgressDatabase[]; } export interface IQuestKeyDatabase { @@ -119,13 +134,15 @@ export const equipmentKeys = [ "DataKnives", "MechSuits", "CrewShipHarnesses", - "KubrowPets" + "KubrowPets", + "CrewShipWeapons", + "CrewShipSalvagedWeapons" ] as const; export type TEquipmentKey = (typeof equipmentKeys)[number]; export interface IDuviriInfo { - Seed: number; + Seed: bigint; NumCompletions: number; } @@ -158,8 +175,9 @@ export type TSolarMapRegion = //TODO: perhaps split response and database into their own files -export interface IPendingRecipeResponse extends Omit { - CompletionDate: IMongoDate; +export enum LoadoutIndex { + NORMAL = 0, + DATAKNIFE = 7 } export interface IDailyAffiliations { @@ -179,39 +197,20 @@ export interface IDailyAffiliations { DailyAffiliationHex: number; } -export interface IInventoryClient extends IDailyAffiliations { - Suits: IEquipmentClient[]; - LongGuns: IEquipmentClient[]; - Pistols: IEquipmentClient[]; - Melee: IEquipmentClient[]; - SpecialItems: IEquipmentClient[]; - Sentinels: IEquipmentClient[]; - SentinelWeapons: IEquipmentClient[]; - SpaceSuits: IEquipmentClient[]; - SpaceGuns: IEquipmentClient[]; - SpaceMelee: IEquipmentClient[]; - Hoverboards: IEquipmentClient[]; - OperatorAmps: IEquipmentClient[]; - MoaPets: IEquipmentClient[]; - Scoops: IEquipmentClient[]; - Horses: IEquipmentClient[]; - DrifterGuns: IEquipmentClient[]; - DrifterMelee: IEquipmentClient[]; - Motorcycles: IEquipmentClient[]; - CrewShips: IEquipmentClient[]; - DataKnives: IEquipmentClient[]; - MechSuits: IEquipmentClient[]; - CrewShipHarnesses: IEquipmentClient[]; - KubrowPets: IEquipmentClient[]; +export type InventoryClientEquipment = { + [_ in TEquipmentKey]: IEquipmentClient[]; +}; + +export interface IInventoryClient extends IDailyAffiliations, InventoryClientEquipment { AdultOperatorLoadOuts: IOperatorConfigClient[]; OperatorLoadOuts: IOperatorConfigClient[]; KahlLoadOuts: IOperatorConfigClient[]; - DuviriInfo: IDuviriInfo; + DuviriInfo?: IDuviriInfo; Mailbox?: IMailboxClient; SubscribedToEmails: number; Created: IMongoDate; - RewardSeed: number; + RewardSeed: bigint; RegularCredits: number; PremiumCredits: number; PremiumCreditsFree: number; @@ -244,14 +243,14 @@ export interface IInventoryClient extends IDailyAffiliations { ActiveQuest: string; FlavourItems: IFlavourItem[]; LoadOutPresets: ILoadOutPresets; - CurrentLoadOutIds: IOid[]; //TODO: we store it in the database using this representation as well :/ + CurrentLoadOutIds: IOid[]; // we store it in the database using this representation as well :/ Missions: IMission[]; RandomUpgradesIdentified?: number; LastRegionPlayed: TSolarMapRegion; XPInfo: ITypeXPItem[]; Recipes: ITypeCount[]; WeaponSkins: IWeaponSkinClient[]; - PendingRecipes: IPendingRecipeResponse[]; + PendingRecipes: IPendingRecipeClient[]; TrainingDate: IMongoDate; PlayerLevel: number; Staff?: boolean; @@ -259,108 +258,126 @@ export interface IInventoryClient extends IDailyAffiliations { Guide?: number; Moderator?: boolean; Partner?: boolean; - Accolades?: { - Heirloom?: boolean; - }; + Accolades?: IAccolades; + Counselor?: boolean; Upgrades: IUpgradeClient[]; EquippedGear: string[]; DeathMarks: string[]; FusionTreasures: IFusionTreasure[]; - WebFlags: IWebFlags; + //WebFlags: IWebFlags; CompletedAlerts: string[]; - Consumables: IConsumable[]; - LevelKeys: IConsumable[]; + Consumables: ITypeCount[]; + LevelKeys: ITypeCount[]; TauntHistory?: ITaunt[]; StoryModeChoice: string; PeriodicMissionCompletions: IPeriodicMissionCompletionDatabase[]; KubrowPetEggs?: IKubrowPetEggClient[]; LoreFragmentScans: ILoreFragmentScan[]; EquippedEmotes: string[]; - PendingTrades: IPendingTrade[]; + //PendingTrades: IPendingTrade[]; Boosters: IBooster[]; ActiveDojoColorResearch: string; - SentientSpawnChanceBoosters: ISentientSpawnChanceBoosters; + //SentientSpawnChanceBoosters: ISentientSpawnChanceBoosters; SupportedSyndicate?: string; Affiliations: IAffiliation[]; - QualifyingInvasions: any[]; + QualifyingInvasions: IInvasionProgressClient[]; 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: IDrone[]; + LastSortieReward?: ILastSortieRewardClient[]; + LastLiteSortieReward?: ILastSortieRewardClient[]; + SortieRewardAttenuation?: ISortieRewardAttenuation[]; + Drones: IDroneClient[]; StepSequencers: IStepSequencer[]; - ActiveAvatarImageType: string; - ShipDecorations: IConsumable[]; + ActiveAvatarImageType?: string; + ShipDecorations: ITypeCount[]; DiscoveredMarkers: IDiscoveredMarker[]; - CompletedJobs: ICompletedJob[]; + //CompletedJobs: ICompletedJob[]; FocusAbility?: string; FocusUpgrades: IFocusUpgrade[]; HasContributedToDojo?: boolean; HWIDProtectEnabled?: boolean; - KubrowPetPrints: IKubrowPetPrint[]; - AlignmentReplay: IAlignment; - PersonalGoalProgress: IPersonalGoalProgress[]; + //KubrowPetPrints: IKubrowPetPrint[]; + AlignmentReplay?: IAlignment; + //PersonalGoalProgress: IPersonalGoalProgress[]; ThemeStyle: string; ThemeBackground: string; ThemeSounds: string; BountyScore: number; - ChallengeInstanceStates: IChallengeInstanceState[]; + //ChallengeInstanceStates: IChallengeInstanceState[]; LoginMilestoneRewards: string[]; - RecentVendorPurchases: Array; + RecentVendorPurchases?: IRecentVendorPurchaseClient[]; NodeIntrosCompleted: string[]; GuildId?: IOid; - CompletedJobChains: ICompletedJobChain[]; + CompletedJobChains?: ICompletedJobChain[]; SeasonChallengeHistory: ISeasonChallenge[]; EquippedInstrument?: string; - InvasionChainProgress: IInvasionChainProgress[]; - NemesisHistory: INemesisHistory[]; - LastNemesisAllySpawnTime?: IMongoDate; - Settings: ISettings; - PersonalTechProjects: IPersonalTechProject[]; + //InvasionChainProgress: IInvasionChainProgress[]; + Nemesis?: INemesisClient; + NemesisHistory?: INemesisBaseClient[]; + //LastNemesisAllySpawnTime?: IMongoDate; + Settings?: ISettings; + PersonalTechProjects: IPersonalTechProjectClient[]; PlayerSkills: IPlayerSkills; - CrewShipAmmo: IConsumable[]; - CrewShipSalvagedWeaponSkins: IUpgradeClient[]; - CrewShipWeapons: ICrewShipWeapon[]; - CrewShipSalvagedWeapons: ICrewShipWeapon[]; + CrewShipAmmo: ITypeCount[]; CrewShipWeaponSkins: IUpgradeClient[]; - TradeBannedUntil?: IMongoDate; + CrewShipSalvagedWeaponSkins: IUpgradeClient[]; + //TradeBannedUntil?: IMongoDate; PlayedParkourTutorial: boolean; SubscribedToEmailsPersonalized: number; InfestedFoundry?: IInfestedFoundryClient; - BlessingCooldown: IMongoDate; - CrewShipRawSalvage: IConsumable[]; - CrewMembers: ICrewMember[]; - LotusCustomization: ILotusCustomization; + BlessingCooldown?: IMongoDate; + CrewShipRawSalvage: ITypeCount[]; + CrewMembers: ICrewMemberClient[]; + LotusCustomization?: ILotusCustomization; UseAdultOperatorLoadout?: boolean; NemesisAbandonedRewards: string[]; - LastInventorySync: IOid; - NextRefill: IMongoDate; // Next time argon crystals will have a decay tick + LastInventorySync?: IOid; + NextRefill?: IMongoDate; FoundToday?: IMiscItem[]; // for Argon Crystals - CustomMarkers: ICustomMarkers[]; - ActiveLandscapeTraps: any[]; + CustomMarkers?: ICustomMarkers[]; + //ActiveLandscapeTraps: any[]; EvolutionProgress?: IEvolutionProgress[]; - RepVotes: any[]; - LeagueTickets: any[]; - Quests: any[]; - Robotics: any[]; - UsedDailyDeals: any[]; - LibraryPersonalTarget: string; + //RepVotes: any[]; + //LeagueTickets: any[]; + //Quests: any[]; + //Robotics: any[]; + //UsedDailyDeals: any[]; + LibraryPersonalTarget?: string; LibraryPersonalProgress: ILibraryPersonalProgress[]; - CollectibleSeries: ICollectibleSery[]; - LibraryAvailableDailyTaskInfo: ILibraryAvailableDailyTaskInfo; + CollectibleSeries?: ICollectibleEntry[]; + LibraryAvailableDailyTaskInfo?: ILibraryDailyTaskInfo; + LibraryActiveDailyTaskInfo?: ILibraryDailyTaskInfo; HasResetAccount: boolean; - PendingCoupon: IPendingCoupon; + PendingCoupon?: IPendingCouponClient; Harvestable: boolean; DeathSquadable: boolean; - EndlessXP?: IEndlessXpProgress[]; + EndlessXP?: IEndlessXpProgressClient[]; 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?: IOidWithLegacySupport[]; + LockedWeaponGroup?: ILockedWeaponGroupClient; + HubNpcCustomizations?: IHubNpcCustomization[]; + Ship?: IOrbiter; // U22 and below, response only } export interface IAffiliation { @@ -369,9 +386,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; @@ -380,6 +407,7 @@ export interface IAlignment { export interface IBooster { ExpiryDate: number; ItemType: string; + UsesRemaining?: number; } export interface IChallengeInstanceState { @@ -394,13 +422,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; @@ -424,50 +474,53 @@ export interface ICompletedJob { StageCompletions: number[]; } -export interface IConsumable { - ItemCount: number; - ItemType: string; +export interface ICrewMemberSkill { + Assigned: number; } -export interface ICrewMember { +export interface ICrewMemberSkillEfficiency { + PILOTING: ICrewMemberSkill; + GUNNERY: ICrewMemberSkill; + ENGINEERING: ICrewMemberSkill; + COMBAT: ICrewMemberSkill; + SURVIVABILITY: ICrewMemberSkill; +} + +export interface ICrewMemberClient { ItemType: string; - NemesisFingerprint: number; - Seed: number; - HireDate: IMongoDate; - AssignedRole: number; - SkillEfficiency: ISkillEfficiency; + NemesisFingerprint: bigint; + Seed: bigint; + AssignedRole?: number; + SkillEfficiency: ICrewMemberSkillEfficiency; WeaponConfigIdx: number; WeaponId: IOid; XP: number; PowersuitType: string; Configs: IItemConfig[]; - SecondInCommand: boolean; + SecondInCommand: boolean; // on call ItemId: IOid; } -export interface ISkillEfficiency { - PILOTING: ICombat; - GUNNERY: ICombat; - ENGINEERING: ICombat; - COMBAT: ICombat; - SURVIVABILITY: ICombat; -} - -export interface ICombat { - Assigned: number; +export interface ICrewMemberDatabase extends Omit { + WeaponId: Types.ObjectId; + _id: Types.ObjectId; } 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; } @@ -482,6 +535,16 @@ export interface IUpgradeDatabase extends Omit { _id: Types.ObjectId; } +export interface IUpgradeFromClient { + ItemType: string; + ItemId: IOidWithLegacySupport; + FromSKU?: boolean; + UpgradeFingerprint: string; + PendingRerollFingerprint: string; + ItemCount: number; + LastAdded: IOidWithLegacySupport; +} + export interface ICrewShipMembersClient { SLOT_A?: ICrewShipMemberClient; SLOT_B?: ICrewShipMemberClient; @@ -496,12 +559,12 @@ export interface ICrewShipMembersDatabase { export interface ICrewShipMemberClient { ItemId?: IOid; - NemesisFingerprint?: number; + NemesisFingerprint?: number | bigint; } export interface ICrewShipMemberDatabase { ItemId?: Types.ObjectId; - NemesisFingerprint?: number; + NemesisFingerprint?: bigint; } export interface ICrewShipCustomization { @@ -510,7 +573,7 @@ export interface ICrewShipCustomization { export interface IShipExterior { SkinFlavourItem?: string; - Colors: IColor; + Colors?: IColor; ShipAttachments?: IShipAttachments; } @@ -524,18 +587,20 @@ export interface IFlavourItem { export type IMiscItem = ITypeCount; +// inventory.CrewShips[0].Weapon export interface ICrewShipWeapon { - PILOT: ICrewShipPilotWeapon; - PORT_GUNS: ICrewShipPortGuns; + PILOT?: ICrewShipWeaponEmplacements; + PORT_GUNS?: ICrewShipWeaponEmplacements; + STARBOARD_GUNS?: ICrewShipWeaponEmplacements; + ARTILLERY?: ICrewShipWeaponEmplacements; + SCANNER?: ICrewShipWeaponEmplacements; } -export interface ICrewShipPilotWeapon { - PRIMARY_A: IEquipmentSelection; - SECONDARY_A: IEquipmentSelection; -} - -export interface ICrewShipPortGuns { - PRIMARY_A: IEquipmentSelection; +export interface ICrewShipWeaponEmplacements { + PRIMARY_A?: IEquipmentSelection; + PRIMARY_B?: IEquipmentSelection; + SECONDARY_A?: IEquipmentSelection; + SECONDARY_B?: IEquipmentSelection; } export interface IDiscoveredMarker { @@ -543,13 +608,27 @@ export interface IDiscoveredMarker { discoveryState: number[]; } -export interface IDrone { +export interface IDroneClient { ItemType: string; CurrentHP: number; ItemId: IOid; RepairStart?: IMongoDate; } +export interface IDroneDatabase { + ItemType: string; + CurrentHP: number; + _id: Types.ObjectId; + RepairStart?: Date; + + DeployTime?: Date; + System?: number; + DamageTime?: Date; + PendingDamage?: number; + ResourceType?: string; + ResourceCount?: number; +} + export interface ITypeXPItem { ItemType: string; XP: number; @@ -617,6 +696,17 @@ export interface IInvasionChainProgress { count: number; } +export interface IInvasionProgressClient { + _id: IOid; + Delta: number; + AttackerScore: number; + DefenderScore: number; +} + +export interface IInvasionProgressDatabase extends Omit { + invasionId: Types.ObjectId; +} + export interface IKubrowPetEggClient { ItemType: string; ExpirationDate: IMongoDate; // seems to be set to 7 days ahead @ 0 UTC @@ -657,12 +747,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; @@ -678,16 +768,26 @@ export enum Status { StatusStasis = "STATUS_STASIS" } -export interface ILastSortieReward { +export interface ILastSortieRewardClient { SortieId: IOid; StoreItem: string; Manifest: string; } -export interface ILibraryAvailableDailyTaskInfo { +export interface ILastSortieRewardDatabase extends Omit { + SortieId: Types.ObjectId; +} + +export interface ISortieRewardAttenuation { + Tag: string; + Atten: number; +} + +export interface ILibraryDailyTaskInfo { EnemyTypes: string[]; EnemyLocTag: string; EnemyIcon: string; + Scans?: number; ScansRequired: number; RewardStoreItem: string; RewardQuantity: number; @@ -763,51 +863,81 @@ export interface IMission extends IMissionDatabase { RewardsCooldownTime?: IMongoDate; } -export interface INemesisHistory { - fp: number; - manifest: Manifest; +export type TNemesisFaction = "FC_GRINEER" | "FC_CORPUS" | "FC_INFESTATION"; + +export interface INemesisBaseClient { + fp: bigint | number; + manifest: string; KillingSuit: string; killingDamageType: number; ShoulderHelmet: string; + WeaponIdx: number; AgentIdx: number; - BirthNode: BirthNode; + BirthNode: string; + Faction: TNemesisFaction; 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 { + fp: bigint; + 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 IPendingCoupon { +export interface INemesisDatabase extends Omit { + fp: bigint; + d: Date; +} + +export interface IInfNode { + Node: string; + Influence: number; +} + +export interface IPendingCouponDatabase { + Expiry: Date; + Discount: number; +} + +export interface IPendingCouponClient { Expiry: IMongoDate; 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 + TargetItemId?: string; // unsure what this is for + TargetFingerprint?: string; + LongGuns?: IEquipmentDatabase[]; + Pistols?: IEquipmentDatabase[]; + Melee?: IEquipmentDatabase[]; + SuitToUnbrand?: Types.ObjectId; +} + +export interface IPendingRecipeClient + extends Omit { + CompletionDate: IMongoDate; +} + +export interface IAccolades { + Heirloom?: boolean; } export interface IPendingTrade { @@ -828,23 +958,29 @@ 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[]; + buffs: IFingerprintStat[]; } -export interface IBuff { - Tag: string; - Value: number; +export interface ICrewShipComponentFingerprint extends IInnateDamageFingerprint { + SubroutineIndex?: number; +} + +export interface INemesisWeaponTargetFingerprint { + ItemType: string; + UpgradeFingerprint: IInnateDamageFingerprint; + Name: string; +} + +export interface INemesisPetTargetFingerprint { + Parts: string[]; + Name: string; } export enum GettingSlotOrderInfo { @@ -854,7 +990,7 @@ export enum GettingSlotOrderInfo { } export interface IGiving { - RawUpgrades: IConsumable[]; + RawUpgrades: ITypeCount[]; _SlotOrderInfo: GivingSlotOrderInfo[]; } @@ -883,16 +1019,22 @@ export interface IPersonalGoalProgress { ReceivedClanReward1?: boolean; } -export interface IPersonalTechProject { +export interface IPersonalTechProjectDatabase { State: number; ReqCredits: number; ItemType: string; - ReqItems: IConsumable[]; + ProductCategory?: string; + CategoryItemId?: Types.ObjectId; + ReqItems: ITypeCount[]; + HasContributions?: boolean; + CompletionDate?: Date; +} + +export interface IPersonalTechProjectClient + extends Omit { + CategoryItemId?: IOid; CompletionDate?: IMongoDate; ItemId: IOid; - ProductCategory?: string; - CategoryItemId?: IOid; - HasContributions?: boolean; } export interface IPlayerSkills { @@ -923,7 +1065,7 @@ export interface IQuestStage { export interface IRawUpgrade { ItemType: string; ItemCount: number; - LastAdded?: IOid; + LastAdded?: IOidWithLegacySupport; } export interface ISeasonChallenge { @@ -936,11 +1078,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 { @@ -981,6 +1124,8 @@ export interface ITaunt { export interface IWeaponSkinDatabase { ItemType: string; + Favorite?: boolean; + IsNew?: boolean; _id: Types.ObjectId; } @@ -1013,18 +1158,35 @@ export interface IEvolutionProgress { export type TEndlessXpCategory = "EXC_NORMAL" | "EXC_HARD"; -export interface IEndlessXpProgress { +export interface IEndlessXpProgressDatabase { Category: TEndlessXpCategory; + Earn: number; + Claim: number; + BonusAvailable?: Date; + Expiry?: Date; Choices: string[]; + PendingRewards: IEndlessXpReward[]; +} + +export interface IEndlessXpProgressClient extends Omit { + BonusAvailable?: IMongoDate; + Expiry?: IMongoDate; +} + +export interface IEndlessXpReward { + RequiredTotalXp: number; + Rewards: ICountedStoreItem[]; } export interface IDialogueHistoryClient { - YearIteration: number; + YearIteration?: number; + Resets?: number; // added in 38.5.0 Dialogues?: IDialogueClient[]; } export interface IDialogueHistoryDatabase { - YearIteration: number; + YearIteration?: number; + Resets?: number; Dialogues?: IDialogueDatabase[]; } @@ -1035,7 +1197,7 @@ export interface IDialogueClient { AvailableGiftDate: IMongoDate; RankUpExpiry: IMongoDate; BountyChemExpiry: IMongoDate; - //QueuedDialogues: any[]; + QueuedDialogues: string[]; Gifts: IDialogueGift[]; Booleans: string[]; Completed: ICompletedDialogue[]; @@ -1080,3 +1242,46 @@ export interface IMarker { z: number; showInHud: boolean; } + +export interface ISeasonProgress { + SeasonType: "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL"; + LastCompletedDayIdx: number; + LastCompletedChallengeDayIdx: number; + ActivatedChallenges: string[]; +} + +export interface ICalendarProgress { + Version: number; + Iteration: number; + YearProgress: { Upgrades: string[] }; + 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; + +export interface IHubNpcCustomization { + Colors?: IColor; + Pattern: string; + Tag: string; +} 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 728fde52..159d39e9 100644 --- a/src/types/loginTypes.ts +++ b/src/types/loginTypes.ts @@ -1,21 +1,30 @@ +import { Types } from "mongoose"; + export interface IAccountAndLoginResponseCommons { DisplayName: string; CountryCode: string; - ClientType: string; - CrossPlatformAllowed: boolean; - ForceLogoutVersion: number; + ClientType?: string; + CrossPlatformAllowed?: boolean; + ForceLogoutVersion?: number; AmazonAuthToken?: string; AmazonRefreshToken?: string; - ConsentNeeded: boolean; - TrackedSettings: string[]; + ConsentNeeded?: boolean; + TrackedSettings?: string[]; Nonce: number; } -export interface IDatabaseAccount extends IAccountAndLoginResponseCommons { +export interface IDatabaseAccountRequiredFields extends IAccountAndLoginResponseCommons { email: string; password: string; - LastLoginDay?: number; + BuildLabel?: string; + LastLogin: Date; +} + +export interface IDatabaseAccount extends IDatabaseAccountRequiredFields { + Dropped?: boolean; LatestEventMessageDate: Date; + LastLoginRewardDate: number; + LoginDays: number; } // Includes virtual ID @@ -27,26 +36,32 @@ export interface ILoginRequest { email: string; password: string; time: number; - s: string; - lang: string; + s?: string; + lang?: string; date: number; - ClientType: string; - PS: string; + ClientType?: string; + PS?: string; + kick?: boolean; } export interface ILoginResponse extends IAccountAndLoginResponseCommons { id: string; - Groups: IGroup[]; + Groups?: IGroup[]; BuildLabel: string; - MatchmakingBuildId: string; - platformCDNs: string[]; - NRS: string[]; - DTLS: number; - IRC: string[]; - HUB: string; + MatchmakingBuildId?: string; + platformCDNs?: string[]; + NRS?: string[]; + DTLS?: number; + IRC?: string[]; + HUB?: string; } export interface IGroup { experiment: string; experimentGroup: string; } + +export interface IIgnore { + ignorer: Types.ObjectId; + ignoree: Types.ObjectId; +} diff --git a/src/types/missionTypes.ts b/src/types/missionTypes.ts index 8e79e8d8..3de75318 100644 --- a/src/types/missionTypes.ts +++ b/src/types/missionTypes.ts @@ -1,3 +1,5 @@ +import { IAffiliationMods, IInventoryChanges } from "./purchaseTypes"; + export const inventoryFields = ["RawUpgrades", "MiscItems", "Consumables", "Recipes"] as const; export type IInventoryFieldType = (typeof inventoryFields)[number]; @@ -6,6 +8,27 @@ export interface IMissionReward { TypeName?: string; UpgradeLevel?: number; ItemCount: number; + DailyCooldown?: boolean; + Rarity?: number; TweetText?: string; ProductCategory?: string; + FromEnemyCache?: boolean; + IsStrippedItem?: boolean; +} + +export interface IMissionCredits { + MissionCredits: number[]; + CreditBonus: number[]; + TotalCredits: number[]; + DailyMissionBonus?: boolean; +} + +export interface IMissionInventoryUpdateResponse extends Partial { + ConquestCompletedMissionsCount?: number; + InventoryJson?: string; + MissionRewards?: IMissionReward[]; + InventoryChanges?: IInventoryChanges; + FusionPoints?: number; + SyndicateXPItemReward?: number; + AffiliationMods?: IAffiliationMods[]; } diff --git a/src/types/personalRoomsTypes.ts b/src/types/personalRoomsTypes.ts index f91a5b88..325ab9e4 100644 --- a/src/types/personalRoomsTypes.ts +++ b/src/types/personalRoomsTypes.ts @@ -1,25 +1,30 @@ import { IColor } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { - IApartment, IRoom, IPlacedDecosDatabase, ITailorShop, ITailorShopDatabase, - TBootLocation + TBootLocation, + IApartmentDatabase, + IApartmentClient } from "@/src/types/shipTypes"; -import { Model, Types } from "mongoose"; +import { Document, Model, Types } from "mongoose"; export interface IOrbiter { Features: string[]; Rooms: IRoom[]; + VignetteFish?: string[]; + FavouriteLoadoutId?: Types.ObjectId; + Wallpaper?: string; + Vignette?: string; ContentUrlSignature?: string; BootLocation?: TBootLocation; } -export interface IPersonalRooms { +export interface IPersonalRoomsClient { ShipInteriorColors: IColor; Ship: IOrbiter; - Apartment: IApartment; + Apartment: IApartmentClient; TailorShop: ITailorShop; } @@ -28,7 +33,7 @@ export interface IPersonalRoomsDatabase { personalRoomsOwnerId: Types.ObjectId; activeShipId: Types.ObjectId; Ship: IOrbiter; - Apartment: IApartment; + Apartment: IApartmentDatabase; TailorShop: ITailorShopDatabase; } @@ -38,7 +43,7 @@ export type PersonalRoomsDocumentProps = { Ship: Omit & { Rooms: RoomsType[]; }; - Apartment: Omit & { + Apartment: Omit & { Rooms: RoomsType[]; }; TailorShop: Omit & { @@ -46,5 +51,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 94e1d197..8cb92ccc 100644 --- a/src/types/purchaseTypes.ts +++ b/src/types/purchaseTypes.ts @@ -1,4 +1,14 @@ -import { IInfestedFoundryClient } from "./inventoryTypes/inventoryTypes"; +import { IEquipmentClient } from "./inventoryTypes/commonInventoryTypes"; +import { + IDroneClient, + IInfestedFoundryClient, + IMiscItem, + INemesisClient, + ITypeCount, + IRecentVendorPurchaseClient, + TEquipmentKey, + ICrewMemberClient +} from "./inventoryTypes/inventoryTypes"; export interface IPurchaseRequest { PurchaseParams: IPurchaseParams; @@ -7,7 +17,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; @@ -21,17 +31,39 @@ export interface IPurchaseParams { IsWeekly?: boolean; // for Source 7 } -export interface ICurrencyChanges { +export type IInventoryChanges = { + [_ in SlotNames]?: IBinChanges; +} & { + [_ in TEquipmentKey]?: IEquipmentClient[]; +} & { RegularCredits?: number; PremiumCredits?: number; PremiumCreditsFree?: number; -} - -export type IInventoryChanges = { - [_ in SlotNames]?: IBinChanges; -} & ICurrencyChanges & { InfestedFoundry?: IInfestedFoundryClient } & Record< - string, - IBinChanges | number | object[] | IInfestedFoundryClient + FusionPoints?: number; + PrimeTokens?: number; + InfestedFoundry?: IInfestedFoundryClient; + Drones?: IDroneClient[]; + MiscItems?: IMiscItem[]; + EmailItems?: ITypeCount[]; + CrewShipRawSalvage?: ITypeCount[]; + Nemesis?: Partial; + NewVendorPurchase?: IRecentVendorPurchaseClient; // >= 38.5.0 + RecentVendorPurchases?: IRecentVendorPurchaseClient; // < 38.5.0 + CrewMembers?: ICrewMemberClient[]; +} & Record< + Exclude< + string, + | SlotNames + | TEquipmentKey + | "RegularCredits" + | "PremiumCredits" + | "PremiumCreditsFree" + | "InfestedFoundry" + | "Drones" + | "MiscItems" + | "EmailItems" + >, + number | object[] >; export interface IAffiliationMods { @@ -48,8 +80,8 @@ export interface IPurchaseResponse { } export type IBinChanges = { - count: number; - platinum: number; + count?: number; + platinum?: number; Slots: number; Extra?: number; }; @@ -66,19 +98,23 @@ 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", + "PvpBonusLoadoutBin", + "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 01302bdc..6fdd0c09 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -13,14 +13,19 @@ import { IFusionTreasure, ICustomMarkers, IPlayerSkills, - IQuestKeyDatabase + IQuestKeyDatabase, + ILoreFragmentScan, + IUpgradeFromClient, + ICollectibleEntry, + IDiscoveredMarker, + ILockedWeaponGroupClient, + ILoadOutPresets, + IInvasionProgressClient, + IWeaponSkinClient, + IKubrowPetEggClient, + INemesisClient } from "./inventoryTypes/inventoryTypes"; - -export interface IThemeUpdateRequest { - Style?: string; - Background?: string; - Sounds?: string; -} +import { IGroup } from "./loginTypes"; export interface IAffiliationChange { Tag: string; @@ -28,12 +33,6 @@ export interface IAffiliationChange { Title: number; } -export interface IUpdateChallengeProgressRequest { - ChallengeProgress: IChallengeProgress[]; - SeasonChallengeHistory: ISeasonChallenge[]; - SeasonChallengeCompletions: ISeasonChallenge[]; -} - export type IMissionInventoryUpdateRequest = { MiscItems?: ITypeCount[]; Recipes?: ITypeCount[]; @@ -43,15 +42,21 @@ export type IMissionInventoryUpdateRequest = { CrewShipRawSalvage?: ITypeCount[]; CrewShipAmmo?: ITypeCount[]; BonusMiscItems?: ITypeCount[]; + EmailItems?: ITypeCount[]; + ShipDecorations?: ITypeCount[]; SyndicateId?: string; SortieId?: string; + CalendarProgress?: { challenge: string }[]; SeasonChallengeCompletions?: ISeasonChallenge[]; AffiliationChanges?: IAffiliationChange[]; crossPlaySetting?: string; rewardsMultiplier?: number; GoalTag: string; LevelKeyName: string; + KeyOwner?: string; + KeyRemovalHash?: string; + KeyToRemove?: string; ActiveBoosters?: IBooster[]; RawUpgrades?: IRawUpgrade[]; FusionTreasures?: IFusionTreasure[]; @@ -70,6 +75,14 @@ export type IMissionInventoryUpdateRequest = { PS: string; ActiveDojoColorResearch: string; RewardInfo?: IRewardInfo; + NemesisKillConvert?: { + nemesisName: string; + weaponLoc: string; + petLoc: "" | "/Lotus/Language/Pets/ZanukaPetName"; + fingerprint: bigint | number; + killed: boolean; + }; + target?: INemesisClient; ReceivedCeremonyMsg: boolean; LastCeremonyResetDate: number; MissionPTS: number; @@ -85,12 +98,69 @@ export type IMissionInventoryUpdateRequest = { FocusXpIncreases?: number[]; PlayerSkillGains: IPlayerSkills; CustomMarkers?: ICustomMarkers[]; + LoreFragmentScans?: ILoreFragmentScan[]; + VoidTearParticipantsCurrWave?: { + Wave: number; + IsFinalWave: boolean; + Participants: IVoidTearParticipantInfo[]; + }; + LibraryScans?: { + EnemyType: string; + Count: number; + CodexScanCount: number; + Standing: number; + }[]; + CollectibleScans?: ICollectibleEntry[]; + Upgrades?: IUpgradeFromClient[]; // riven challenge progress + WeaponSkins?: IWeaponSkinClient[]; + StrippedItems?: { + DropTable: string; + DROP_MOD?: number[]; + DROP_BLUEPRINT?: number[]; + }[]; + DeathMarks?: string[]; + Nemesis?: number; + Boosters?: IBooster[]; + CapturedAnimals?: { + AnimalType: string; + CaptureRating: number; + NumTags: number; + NumExtraRewards: number; + Count: number; + }[]; + KubrowPetEggs?: IKubrowPetEggClient[]; + DiscoveredMarkers?: IDiscoveredMarker[]; + BrandedSuits?: IOid; // sent when captured by g3 + 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 + InvasionProgress?: IInvasionProgressClient[]; + ConquestMissionsCompleted?: number; + duviriSuitSelection?: string; + duviriPistolSelection?: string; + duviriLongGunSelection?: string; + duviriMeleeSelection?: string; + duviriCaveOffers?: { + Seed: number | bigint; + Warframes: string[]; + Weapons: string[]; + }; } & { [K in TEquipmentKey]?: IEquipmentClient[]; }; export interface IRewardInfo { node: string; + invasionId?: string; + invasionAllyFaction?: "FC_GRINEER" | "FC_CORPUS"; + sortieId?: string; + sortieTag?: "Mission1" | "Mission2" | "Final"; + sortiePrereqs?: string[]; VaultsCracked?: number; // for Spy missions rewardTier?: number; nightmareMode?: boolean; @@ -99,22 +169,32 @@ export interface IRewardInfo { toxinOk?: boolean; lostTargetWave?: number; defenseTargetCount?: number; + NemesisAbandonedRewards?: string[]; + NemesisHenchmenKills?: number; + NemesisHintProgress?: number; EOM_AFK?: number; rewardQualifications?: string; // did a Survival for 5 minutes and this was "1" PurgatoryRewardQualifications?: string; - rewardSeed?: number; + rewardSeed?: number | bigint; periodicMissionTag?: string; + T?: number; // Duviri + ConquestType?: string; + ConquestCompleted?: number; + ConquestEquipmentSuggestionsFulfilled?: number; + ConquestPersonalModifiersActive?: number; + ConquestStickersActive?: number; + ConquestHardModeActive?: number; + // for bounties, only EOM_AFK and node are given from above, plus: + JobTier?: number; + jobId?: string; + JobStage?: number; + Q?: boolean; // likely indicates that the bonus objective for this stage was completed + CheckpointCounter?: number; // starts at 1, is incremented with each job stage upload, and does not reset when starting a new job + challengeMissionId?: string; } 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; @@ -134,3 +214,23 @@ export interface IUnlockShipFeatureRequest { KeyChain: string; ChainStage: number; } + +export interface IVoidTearParticipantInfo { + AccountId: string; + Name: string; + ChosenRewardOwner: string; + MissionHash: string; + VoidProjection: string; + Reward: string; + QualifiesForReward: boolean; + HaveRewardResponse: boolean; + RewardsMultiplier: number; + 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..18d692c1 100644 --- a/src/types/saveLoadoutTypes.ts +++ b/src/types/saveLoadoutTypes.ts @@ -1,7 +1,14 @@ import { IOid } from "@/src/types/commonTypes"; import { IItemConfig, IOperatorConfigClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { Types } from "mongoose"; -import { ILoadoutConfigClient } from "./inventoryTypes/inventoryTypes"; +import { + ICrewShipCustomization, + ICrewShipMembersClient, + ICrewShipWeapon, + IFlavourItem, + ILoadoutConfigClient, + ILotusCustomization +} from "./inventoryTypes/inventoryTypes"; export interface ISaveLoadoutRequest { LoadOuts: ILoadoutClient; @@ -36,9 +43,11 @@ export interface ISaveLoadoutRequest { EquippedGear: string[]; EquippedEmotes: string[]; UseAdultOperatorLoadout: boolean; + WeaponSkins: IItemEntry; + LotusCustomization: ILotusCustomization; } -export interface ISaveLoadoutRequestNoUpgradeVer extends Omit {} +export type ISaveLoadoutRequestNoUpgradeVer = Omit; export interface IOperatorConfigEntry { [configId: string]: IOperatorConfigClient; @@ -48,11 +57,20 @@ export interface IItemEntry { [itemId: string]: IConfigEntry; } -export interface IConfigEntry { - [configId: string]: IItemConfig; -} +export type IConfigEntry = { + [configId in "0" | "1" | "2" | "3" | "4" | "5"]: IItemConfig; +} & { + Favorite?: boolean; + IsNew?: boolean; + // Railjack + ItemName?: string; + RailjackImage?: IFlavourItem; + Customization?: ICrewShipCustomization; + Weapon?: ICrewShipWeapon; + CrewMembers?: ICrewShipMembersClient; +}; -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..65007172 100644 --- a/src/types/session.ts +++ b/src/types/session.ts @@ -1,29 +1,31 @@ +import { Types } from "mongoose"; + 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; + sessionId: Types.ObjectId; + creatorId: Types.ObjectId; + 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 | bigint; + 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..74eaaeae 100644 --- a/src/types/shipTypes.ts +++ b/src/types/shipTypes.ts @@ -1,12 +1,12 @@ import { Types } from "mongoose"; -import { IOid } from "@/src/types/commonTypes"; +import { IMongoDate, IOid } from "@/src/types/commonTypes"; import { IColor } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { ILoadoutClient } from "./saveLoadoutTypes"; export interface IGetShipResponse { ShipOwnerId: string; Ship: IShip; - Apartment: IApartment; + Apartment: IApartmentClient; TailorShop: ITailorShop; LoadOutInventory: { LoadOutPresets: ILoadoutClient }; } @@ -28,8 +28,12 @@ export interface IShip { ShipId: IOid; ShipInterior: IShipInterior; Rooms: IRoom[]; - ContentUrlSignature?: string; + VignetteFish?: string[]; + FavouriteLoadoutId?: IOid; + Wallpaper?: string; + Vignette?: string; BootLocation?: TBootLocation; + ContentUrlSignature?: string; } export interface IShipDatabase { @@ -47,23 +51,44 @@ export interface IRoom { PlacedDecos?: IPlacedDecosDatabase[]; } -export interface IPlants { +export interface IPlantClient { PlantType: string; - EndTime: IOid; + EndTime: IMongoDate; PlotIndex: number; } -export interface IPlanters { - Name: string; - Plants: IPlants[]; + +export interface IPlantDatabase extends Omit { + EndTime: Date; } -export interface IGardening { - Planters?: IPlanters[]; +export interface IPlanterClient { + Name: string; + Plants: IPlantClient[]; } -export interface IApartment { - Gardening: IGardening; + +export interface IPlanterDatabase { + Name: string; + Plants: IPlantDatabase[]; +} + +export interface IGardeningClient { + Planters: IPlanterClient[]; +} + +export interface IGardeningDatabase { + Planters: IPlanterDatabase[]; +} + +export interface IApartmentClient { + Gardening: IGardeningClient; Rooms: IRoom[]; - FavouriteLoadouts: string[]; + FavouriteLoadouts: IFavouriteLoadout[]; +} + +export interface IApartmentDatabase { + Gardening: IGardeningDatabase; + Rooms: IRoom[]; + FavouriteLoadouts: IFavouriteLoadoutDatabase[]; } export interface IPlacedDecosDatabase { @@ -127,7 +152,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..9249a9a0 --- /dev/null +++ b/src/types/vendorTypes.ts @@ -0,0 +1,43 @@ +import { IMongoDate, IOid } from "./commonTypes"; + +export interface IItemPrice { + ItemType: string; + ItemCount: number; + ProductCategory: string; +} + +export interface IItemManifest { + StoreItem: string; + ItemPrices?: IItemPrice[]; + RegularPrice?: number[]; + PremiumPrice?: number[]; + Bin: string; + QuantityMultiplier: number; + Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing. + PurchaseQuantityLimit?: number; + Affiliation?: string; + MinAffiliationRank?: number; + ReductionPerPositiveRank?: number; + IncreasePerNegativeRank?: number; + RotatedWeekly?: boolean; + AllowMultipurchase: boolean; + LocTagRandSeed?: number | bigint; + Id: IOid; + RegularPriceBeforeDiscount?: number[]; + ItemPricesBeforeDiscount?: IItemPrice[]; +} + +export interface IVendorInfo { + _id: IOid; + TypeName: string; + ItemManifest: IItemManifest[]; + PropertyTextHash?: string; + RandomSeedType?: string; + RequiredGoalTag?: string; + WeaponUpgradeValueAttenuationExponent?: number; + Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing. +} + +export interface IVendorManifest { + VendorInfo: IVendorInfo; +} diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts new file mode 100644 index 00000000..78c1f330 --- /dev/null +++ b/src/types/worldStateTypes.ts @@ -0,0 +1,184 @@ +import { IMongoDate, IOid } from "./commonTypes"; + +export interface IWorldState { + Version: number; // for goals + BuildLabel: string; + Time: number; + Goals: IGoal[]; + Alerts: []; + Sorties: ISortie[]; + LiteSorties: ILiteSortie[]; + SyndicateMissions: ISyndicateMissionInfo[]; + GlobalUpgrades: IGlobalUpgrade[]; + ActiveMissions: IFissure[]; + NodeOverrides: INodeOverride[]; + PVPChallengeInstances: IPVPChallengeInstance[]; + EndlessXpChoices: IEndlessXpChoice[]; + SeasonInfo?: { + Activation: IMongoDate; + Expiry: IMongoDate; + AffiliationTag: string; + Season: number; + Phase: number; + Params: string; + ActiveChallenges: ISeasonChallenge[]; + }; + KnownCalendarSeasons: ICalendarSeason[]; + Tmp?: string; +} + +export 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; +} + +export interface ISyndicateMissionInfo { + _id: IOid; + Activation: IMongoDate; + Expiry: IMongoDate; + Tag: string; + Seed: number; + Nodes: string[]; + Jobs?: { + jobType?: string; + rewards: string; + masteryReq: number; + minEnemyLevel: number; + maxEnemyLevel: number; + xpAmounts: number[]; + endless?: boolean; + locationTag?: string; + isVault?: boolean; + }[]; +} + +export interface IGlobalUpgrade { + _id: IOid; + Activation: IMongoDate; + ExpiryDate: IMongoDate; + UpgradeType: string; + OperationType: string; + Value: number; + LocalizeTag: string; + LocalizeDescTag: string; +} + +export interface IFissure { + _id: IOid; + Region: number; + Seed: number; + Activation: IMongoDate; + Expiry: IMongoDate; + Node: string; + MissionType: string; + Modifier: string; + Hard?: boolean; +} + +export interface INodeOverride { + _id: IOid; + Activation?: IMongoDate; + Expiry?: IMongoDate; + Node: string; + Hide?: boolean; + Seed?: number; + LevelOverride?: string; + Faction?: string; + CustomNpcEncounters?: string; +} + +export interface ISortie { + _id: IOid; + Activation: IMongoDate; + Expiry: IMongoDate; + Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards"; + Seed: number; + Boss: string; + Variants: { + missionType: string; + modifierType: string; + node: string; + }[]; +} + +export interface ISortieMission { + missionType: string; + modifierType: string; + node: string; + tileset: string; +} + +export interface ILiteSortie { + _id: IOid; + Activation: IMongoDate; + Expiry: IMongoDate; + Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards"; + Seed: number; + Boss: "SORTIE_BOSS_AMAR" | "SORTIE_BOSS_NIRA" | "SORTIE_BOSS_BOREAL"; + Missions: { + missionType: string; + node: string; + }[]; +} + +export interface IPVPChallengeInstance { + _id: IOid; + challengeTypeRefID: string; + startDate: IMongoDate; + endDate: IMongoDate; + params: { + n: string; // "ScriptParamValue"; + v: number; + }[]; + isGenerated: boolean; + PVPMode: string; + subChallenges: IOid[]; + Category: string; // "PVPChallengeTypeCategory_WEEKLY" | "PVPChallengeTypeCategory_WEEKLY_ROOT" | "PVPChallengeTypeCategory_DAILY"; +} + +export interface IEndlessXpChoice { + Category: string; + Choices: string[]; +} + +export interface ISeasonChallenge { + _id: IOid; + Daily?: boolean; + Activation: IMongoDate; + Expiry: IMongoDate; + Challenge: string; +} + +export interface ICalendarSeason { + Activation: IMongoDate; + Expiry: IMongoDate; + Season: "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL"; + Days: ICalendarDay[]; + YearIteration: number; + Version: number; + UpgradeAvaliabilityRequirements: string[]; +} + +export interface ICalendarDay { + day: number; + events: ICalendarEvent[]; +} + +export interface ICalendarEvent { + type: string; + challenge?: string; + reward?: string; + upgrade?: string; + dialogueName?: string; + dialogueConvo?: string; +} diff --git a/src/utils/async-utils.ts b/src/utils/async-utils.ts new file mode 100644 index 00000000..f8825612 --- /dev/null +++ b/src/utils/async-utils.ts @@ -0,0 +1,8 @@ +// Misnomer: We have concurrency, not parallelism - oh well! +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 52000727..f02c0db4 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -33,9 +33,9 @@ const consolelogFormat = format.printf(info => { colors: true }); - return `${info.timestamp} [${info.version}] ${info.level}: ${info.message} ${metadataString}`; + return `${info.timestamp as string} [${info.version as string}] ${info.level}: ${info.message as string} ${metadataString}`; } - return `${info.timestamp} [${info.version}] ${info.level}: ${info.message}`; + return `${info.timestamp as string} [${info.version as string}] ${info.level}: ${info.message as string}`; }); const fileFormat = format.combine( @@ -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..4bce415b 100644 --- a/static/certs/cert.pem +++ b/static/certs/cert.pem @@ -1,38 +1,38 @@ -----BEGIN CERTIFICATE----- -MIIGLjCCBRagAwIBAgIRAPeLmReXnv+ALT/3Tm2Vts4wDQYJKoZIhvcNAQELBQAw -gY8xCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO -BgNVBAcTB1NhbGZvcmQxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDE3MDUGA1UE -AxMuU2VjdGlnbyBSU0EgRG9tYWluIFZhbGlkYXRpb24gU2VjdXJlIFNlcnZlciBD -QTAeFw0yNDA0MTUwMDAwMDBaFw0yNTA0MTUyMzU5NTlaMBcxFTATBgNVBAMMDCou -cDJwdGxzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKoxU6lW -K5iAXZfLrKOY5lcy7z+mML2cYZkW0XXJeC6jYDyYSGAPJogeIgd3JsJWjZvHxnj7 -8KJGjO5j8B8kz4CVcV6aEx4ExJvtFUSzkgXHhlvSo2p0TTtWxC+ib3vWv+5kBSzb -4mdKKHiaz9shcLNKB77305xSBnKjAPGElgaZRwjwMqUSbPyjx4KrehyPQZDOU0aR -TKUbQNDbKYbeEmmUku0FTpao35GNsJrwzKKFIgzWAGKY+QiywIMeOGf0dTqX60GQ -MeXkKbueibuFKA12foV8RGojdT+bPIdRQyyEyntUkbu+UMknJ9bsPbKTEyQgv5nY -62O+A2lYG89Ub7MCAwEAAaOCAvowggL2MB8GA1UdIwQYMBaAFI2MXsRUrYrhd+mb -+ZsF4bgBjWHhMB0GA1UdDgQWBBQgFEQlEKO9vXkpBU7pQjbMU8MZvTAOBgNVHQ8B +MIIGMDCCBRigAwIBAgIQX4800cgswlDH/QexMSnnnjANBgkqhkiG9w0BAQsFADCB +jzELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQD +Ey5TZWN0aWdvIFJTQSBEb21haW4gVmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENB +MB4XDTI1MDMwNjAwMDAwMFoXDTI2MDMwNjIzNTk1OVowGDEWMBQGA1UEAwwNKi5m +YWtldGxzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMe42XWK +HJuR7doFTX79zrEKfTlD2hjRIif3dHKJNTJNvZa52mIoHelP7RVUuFOhp7aZCNLh +IEzDyZObl8vwO6L2PVu5tbBEEoNixbpfhc8ZICEBuVo2UAhnJFcMJtuvtrCq+7ye +oczM/k/nh8FBz2WnLzWs4CZt1sa5knZXFmBmsHJQtQIC6vx7QzVcKGOlAosIEHSK +X4nIz5fLgWSzor1Gay56j31PTk+qRvlPQM2aKiLWnlLfRED4zHJqLe94itu8llPX +b6g+cLxxRKUpMqtG/15cDdBZwv40Dja7bmNfe1u4w2QCVLjvHVaVpNXbcRay/Mhn +M1w5LzDZmV58b18CAwEAAaOCAvwwggL4MB8GA1UdIwQYMBaAFI2MXsRUrYrhd+mb ++ZsF4bgBjWHhMB0GA1UdDgQWBBS6/x/N38wMJrQq/cE1oIcRERMonTAOBgNVHQ8B 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= +aHR0cDovL29jc3Auc2VjdGlnby5jb20wJQYDVR0RBB4wHIINKi5mYWtldGxzLmNv +bYILZmFrZXRscy5jb20wggF+BgorBgEEAdZ5AgQCBIIBbgSCAWoBaAB2AJaXZL9V +WJet90OHaDcIQnfp8DrV9qTzNm5GpD8PyqnGAAABlWsz5fgAAAQDAEcwRQIgTN7Y +/mDqiD3RbGVLEOQK2wvXsboBolBRwGJFuFEsDScCIQCQ0qfb/0V8qqSxrkx/PiVS +1lSn5gBEnQUiQOkefcnW0gB2ABmG1Mcoqm/+ugNveCpNAZGqzi1yMQ+uzl1wQS0l +TMfUAAABlWsz5dAAAAQDAEcwRQIhAJnQJyrSCWWdi9Kyoa7XuMGyDKt183jJMY0E +71abTuBOAiBC+WnK1esG6xr8aVGHRcc+1U/I7LiaG3LCRMYtCKrTGwB2AMs49xWJ +fIShRF9bwd37yW7ymlnNRwppBYWwyxTDFFjnAAABlWsz5f4AAAQDAEcwRQIhAJUs +4PWDwyQJnCxCyEwFlFUY2uYQkGrQPA9f9Sw5Xk1fAiB63eQtZQGjvzvhOghy6z9a +8oGYbDfDQ/zfisMYO7rM6zANBgkqhkiG9w0BAQsFAAOCAQEAEHnSoeBbWiK3CS3a +px0BL+YXxRxdUcTMHgn5o+LlI9sWlpf+JLXmn7Z4QA6fAwT4k/Ue7xsmIq0OraDk +/pEVXWm1HO/9wUkGQg0DBi77BpfHircd7OWIMdt250Q8UAmZkOyhVgnwBcScqMwq +2T5CPaYvYGgYWx/qkIBv7JqhVbrP82rnF9b9ZUZ8GIE31chBmtMva9AsnAN5dmRw +81bVvPWXUfX30CYu5sxeWL06Zpy9nfJumxZri1SWXNTBjSvud2jsZ8tSCUAWLL/4 +ui3Vien9m2oMOpaA8xbS88ZTk9Alm/o5febEKJZUPlytQzij8gQpiovFw2v+Cdei ++tFXKw== -----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..6135769a 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= +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDHuNl1ihybke3a +BU1+/c6xCn05Q9oY0SIn93RyiTUyTb2WudpiKB3pT+0VVLhToae2mQjS4SBMw8mT +m5fL8Dui9j1bubWwRBKDYsW6X4XPGSAhAblaNlAIZyRXDCbbr7awqvu8nqHMzP5P +54fBQc9lpy81rOAmbdbGuZJ2VxZgZrByULUCAur8e0M1XChjpQKLCBB0il+JyM+X +y4Fks6K9Rmsueo99T05Pqkb5T0DNmioi1p5S30RA+Mxyai3veIrbvJZT12+oPnC8 +cUSlKTKrRv9eXA3QWcL+NA42u25jX3tbuMNkAlS47x1WlaTV23EWsvzIZzNcOS8w +2ZlefG9fAgMBAAECggEAT1Tti/LASks8300b60WFxG0WMJjzGMh5eMaiSpyVtNWM +aUKJrFOjDfnhgoeUcCPWKoG/L4Sc/+EFQMydDzTte120IasysEFZ2TZytAUdcZXZ +XUMCDQNl5vCRTsJU7Q5u0t4YAGRCgMcsfTDKi8lISGiQKBHzN1CJ74Xm13rgOInd +lAc0wd5S89sL6RYmRTj1LvuZ95EHXHqQGdv0fIFEyP3pF1iPwcoTuIVEeICqnEvW +vd8CVO68eH3HFIwioqjp4qW3pxPZMhVq4161805uAMkoQlE+7MtEVenmP++1u1gM +FjvAs3j9CZqOHZKcLlOtcGSwDlD++fCMMT4slLgLgQKBgQDy58E5nuYXdxlFQQk4 +QccUKpyJ2aVXyp9xvTFBot/5Pik1SkuDzv2XU1OTxdxf3EongLy91nMJ2/6/39Je +lf0/2MjzCtJ/lSzZ/zpJAu86UkBkWBAA5loGIof6OKedbEIgqpJqtK59S+j3ExO9 +eqa+uFrtt1UfaJG4A7TT+dIvIwKBgQDSfSOdSM5Dh3KsQHVnIWcIkzwTtlJlO+rG +6rDEADxw6Kp8VIL/dq4Foe8yW4VqLVrWUuZsU6jzC9GdnyYi6VaqZ/iSUtGkBMOT +WTTYhqXlURaQ13jhqdwCZJRbVI72JbXn2OGEv8DgXnk//QKED/8VdKqAzCSr1t1f +3yfwei0AlQKBgD19KU66yKg7/+umEP1quUiDmOjUbaSRqFcUe3mQD356m9ffnMob +BdrevxNzTNv/Wc4yKpUryic+x3gu4oQLF/annAbaQHsHejkdANYmpgRvedls6XAw +360Z5K4U1WlmVD8Mrs/QOTOCmdChxad7euZgqLPwat3ujKS2W3oljW1dAoGBAM4/ +AB6lsDZLCfnuTxt2h1bHrh5CkAnR5AJ1BC+Ja6/WyvZ4eMOIroumWJKnStr3BgLr +yAxtDSbZddNUljGvIdRnfBEkRXbJlDlVN4rSpMtF4S6bcz7rCUDu/M9g05Qs70j2 +IkPJAFzZNUWVzFlKs096uXbqkSQvrUq7ho8DqAThAoGBAL7Nrbr5LWcBgvwEhEla +VRfYb0FUrDwLIrVWntJjW566/pVQQ4BmatsblLjlQYWk9MCIYXWZbnB+2fRx9yjQ +Adggez7Dws/Mrh/wVudKgayHCy5Lgd8rYjNgC+VZf8XGrWX3QXMJ6UWAyQLTeoO7 +hToW9o9CQMIhaR43G8di1kjF -----END PRIVATE KEY----- diff --git a/static/fixed_responses/allDecoRecipes.json b/static/fixed_responses/allDecoRecipes.json new file mode 100644 index 00000000..4ed19d5b --- /dev/null +++ b/static/fixed_responses/allDecoRecipes.json @@ -0,0 +1,92 @@ +[ + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/AmbulasEventBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/AmbulasEventGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/AmbulasEventSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/AmbulasEventTerracottaTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/AridFearBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/AridFearGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/AridFearSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/BreedingGroundsBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/BreedingGroundsGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/BreedingGroundsSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/CiceroCrisisBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/CiceroCrisisGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/CiceroCrisisSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DisruptionEventBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DisruptionEventGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DisruptionEventSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DisruptionEventTerracottaTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DojoRemasterTrophyBronzeARecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DojoRemasterTrophyGoldARecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DojoRemasterTrophyPlatinumARecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DojoRemasterTrophySilverARecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EntratiEventBaseTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EntratiEventBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EntratiEventGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EntratiEventSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EntratiEventTerracottaTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EvacuationEventBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EvacuationEventGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EvacuationEventSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EvacuationEventTerracottaTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/EyesOfBlightTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/FalseProfitBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/FalseProfitClayTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/FalseProfitGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/FalseProfitSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/FusionMoaTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/GradivusDilemmaCorpusBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/GradivusDilemmaCorpusGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/GradivusDilemmaCorpusSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/GradivusDilemmaGrineerBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/GradivusDilemmaGrineerGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/GradivusDilemmaGrineerSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/IcePlanetTrophyBronzeRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/IcePlanetTrophyGoldRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/IcePlanetTrophySilverRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventBaseTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventPewterTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MechEventTrophyBronzeRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MechEventTrophyGoldRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MechEventTrophySilverRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MechEventTrophyTerracottaRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MutalistIncursionBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MutalistIncursionGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MutalistIncursionSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/OrokinMusicBoxRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/OrokinSabotageTrophyBronzeRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/OrokinSabotageTrophyGoldRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/OrokinSabotageTrophySilverRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ProjectSinisterBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ProjectSinisterClayTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ProjectSinisterGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ProjectSinisterSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/RailjackResearchTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/RathuumBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/RathuumClayTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/RathuumGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/RathuumSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ShipyardsEventBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ShipyardsEventGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ShipyardsEventSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/SlingStoneTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/SpyDroneTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/SurvivalEventBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/SurvivalEventGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/SurvivalEventSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/TennoConDojoGhostTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/TennoConDojoMoonTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/TennoConDojoMountainTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/TennoConDojoShadowTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/TennoConDojoStormTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ThumperTrophyBronzeRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ThumperTrophyCrystalRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ThumperTrophyGoldRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ThumperTrophySilverRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/CorpusPlaceables/GasTurbineConeRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/NaturalPlaceables/CoralChunkARecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/TennoPlaceables/TnoBeaconEmitterRecipe" +] diff --git a/static/fixed_responses/allIncarnonList.json b/static/fixed_responses/allIncarnonList.json new file mode 100644 index 00000000..3d85db63 --- /dev/null +++ b/static/fixed_responses/allIncarnonList.json @@ -0,0 +1,49 @@ +[ + "/Lotus/Weapons/ClanTech/Bio/BioWeapon", + "/Lotus/Weapons/ClanTech/Energy/EnergyRifle", + "/Lotus/Weapons/Corpus/Pistols/CorpusMinigun/CorpusMinigun", + "/Lotus/Weapons/Corpus/Pistols/CrpHandRL/CorpusHandRocketLauncher", + "/Lotus/Weapons/Grineer/LongGuns/GrineerSawbladeGun/SawBladeGun", + "/Lotus/Weapons/Grineer/Melee/GrineerTylAxeAndBoar/RegorAxeShield", + "/Lotus/Weapons/Grineer/Pistols/HeatGun/GrnHeatGun", + "/Lotus/Weapons/Infested/Pistols/InfVomitGun/InfVomitGunWep", + "/Lotus/Weapons/Syndicates/CephalonSuda/Pistols/CSDroidArray", + "/Lotus/Weapons/Tenno/Bows/HuntingBow", + "/Lotus/Weapons/Tenno/Bows/StalkerBow", + "/Lotus/Weapons/Tenno/LongGuns/TnoLeverAction/TnoLeverActionRifle", + "/Lotus/Weapons/Tenno/Melee/Axe/DualInfestedAxesWeapon", + "/Lotus/Weapons/Tenno/Melee/Dagger/CeramicDagger", + "/Lotus/Weapons/Tenno/Melee/Fist/Fist", + "/Lotus/Weapons/Tenno/Melee/Hammer/IceHammer/IceHammer", + "/Lotus/Weapons/Tenno/Melee/LongSword/LongSword", + "/Lotus/Weapons/Tenno/Melee/Maces/PaladinMace/PaladinMaceWeapon", + "/Lotus/Weapons/Tenno/Melee/Scythe/StalkerScytheWeapon", + "/Lotus/Weapons/Tenno/Melee/Scythe/ParisScythe/ParisScythe", + "/Lotus/Weapons/Tenno/Melee/Staff/Staff", + "/Lotus/Weapons/Tenno/Melee/Swords/CutlassAndPoignard/TennoCutlass", + "/Lotus/Weapons/Tenno/Melee/Swords/TennoSai/TennoSais", + "/Lotus/Weapons/Tenno/Pistol/AutoPistol", + "/Lotus/Weapons/Tenno/Pistol/BurstPistol", + "/Lotus/Weapons/Tenno/Pistol/HandShotGun", + "/Lotus/Weapons/Tenno/Pistol/HeavyPistol", + "/Lotus/Weapons/Tenno/Pistol/Pistol", + "/Lotus/Weapons/Tenno/Pistol/RevolverPistol", + "/Lotus/Weapons/Tenno/Pistols/ConclaveLeverPistol/ConclaveLeverPistol", + "/Lotus/Weapons/Tenno/Rifle/BoltoRifle", + "/Lotus/Weapons/Tenno/Rifle/BurstRifle", + "/Lotus/Weapons/Tenno/Rifle/HeavyRifle", + "/Lotus/Weapons/Tenno/Rifle/Rifle", + "/Lotus/Weapons/Tenno/Rifle/SemiAutoRifle", + "/Lotus/Weapons/Tenno/Rifle/TennoAR", + "/Lotus/Weapons/Tenno/Shotgun/FullAutoShotgun", + "/Lotus/Weapons/Tenno/Shotgun/Shotgun", + "/Lotus/Weapons/Tenno/ThrowingWeapons/Kunai", + "/Lotus/Weapons/Tenno/ThrowingWeapons/StalkerKunai", + "/Lotus/Weapons/Tenno/Zariman/LongGuns/PumpShotgun/ZarimanPumpShotgun", + "/Lotus/Weapons/Tenno/Zariman/LongGuns/SemiAutoRifle/ZarimanSemiAutoRifle", + "/Lotus/Weapons/Tenno/Zariman/Melee/Dagger/ZarimanDaggerWeapon", + "/Lotus/Weapons/Tenno/Zariman/Melee/Tonfas/ZarimanTonfaWeapon", + "/Lotus/Weapons/Tenno/Zariman/Pistols/HeavyPistol/ZarimanHeavyPistol", + "/Lotus/Weapons/Thanotech/EntFistIncarnon/EntFistIncarnon", + "/Lotus/Weapons/Thanotech/EntratiWristGun/EntratiWristGunWeapon" +] diff --git a/static/fixed_responses/allScans.json b/static/fixed_responses/allScans.json index 2263ad95..f4b43062 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,13 @@ "/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", + "/Lotus/Objects/Orokin/Props/CollectibleSeriesOne", + "/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLamp", + "/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLampLarge", + "/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLampSmall", + "/Lotus/Types/LevelObjects/InfestedPumpkinExplosiveTotem" ] 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 new file mode 100644 index 00000000..62cb477a --- /dev/null +++ b/static/fixed_responses/eventMessages.json @@ -0,0 +1,12 @@ +{ + "Messages": [ + { + "sub": "Welcome to Space Ninja Server", + "sndr": "/Lotus/Language/Bosses/Ordis", + "msg": "Enjoy your Space Ninja Experience", + "icon": "/Lotus/Interface/Icons/Npcs/Ordis.png", + "eventMessageDate": "2025-01-30T13:00:00.000Z", + "r": false + } + ] +} diff --git a/static/fixed_responses/getVendorInfo/DeimosFishmongerVendorManifest.json b/static/fixed_responses/getVendorInfo/DeimosFishmongerVendorManifest.json deleted file mode 100644 index ac6c0951..00000000 --- a/static/fixed_responses/getVendorInfo/DeimosFishmongerVendorManifest.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "VendorInfo": { - "_id": { - "$oid": "5f456e01c96976e97d6b8016" - }, - "TypeName": "/Lotus/Types/Game/VendorManifests/Deimos/FishmongerVendorManifest", - "ItemManifest": [ - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Deimos/FishParts/DeimosOrokinFishAPartItem", - "PremiumPrice": [9, 9], - "Bin": "BIN_1", - "QuantityMultiplier": 10, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e91b9" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Deimos/FishParts/DeimosInfestedFishDPartItem", - "PremiumPrice": [17, 17], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e91ba" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Deimos/FishParts/DeimosInfestedFishCPartItem", - "PremiumPrice": [10, 10], - "Bin": "BIN_1", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e91bb" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Deimos/FishParts/DeimosInfestedFishBPartItem", - "PremiumPrice": [6, 6], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e91bc" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Deimos/FishParts/DeimosInfestedFishAPartItem", - "PremiumPrice": [5, 5], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e91bd" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Deimos/FishParts/DeimosGenericSharedFishPartItem", - "PremiumPrice": [7, 7], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e91be" - } - } - ], - "PropertyTextHash": "6DF13A7FB573C25B4B4F989CBEFFC615", - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - } - } -} 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/HubsPerrinSequenceWeaponVendorManifest.json b/static/fixed_responses/getVendorInfo/HubsPerrinSequenceWeaponVendorManifest.json deleted file mode 100644 index 1cae38e4..00000000 --- a/static/fixed_responses/getVendorInfo/HubsPerrinSequenceWeaponVendorManifest.json +++ /dev/null @@ -1,133 +0,0 @@ -{ - "VendorInfo": { - "_id": { - "$oid": "60ad3b6ec96976e97d227e19" - }, - "TypeName": "/Lotus/Types/Game/VendorManifests/Hubs/PerrinSequenceWeaponVendorManifest", - "ItemManifest": [ - { - "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/BoardExec/Primary/CrpBEFerrox/CrpBEFerrox", - "ItemPrices": [ - { - "ItemCount": 40, - "ItemType": "/Lotus/Types/Items/MiscItems/GranumBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 4383829823946960400, - "Id": { - "$oid": "66fd60b20ba592c4c95e9488" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/Melee/CrpBriefcaseScythe/CrpBriefcaseScythe", - "ItemPrices": [ - { - "ItemCount": 40, - "ItemType": "/Lotus/Types/Items/MiscItems/GranumBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 7952272124248276000, - "Id": { - "$oid": "66fd60b20ba592c4c95e9489" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/Melee/CrpBriefcase2HKatana/CrpBriefcase2HKatana", - "ItemPrices": [ - { - "ItemCount": 40, - "ItemType": "/Lotus/Types/Items/MiscItems/GranumBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 465952672558014140, - "Id": { - "$oid": "66fd60b20ba592c4c95e948a" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/CrpBigSlash/CrpBigSlash", - "ItemPrices": [ - { - "ItemCount": 40, - "ItemType": "/Lotus/Types/Items/MiscItems/GranumBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 8342430883077507000, - "Id": { - "$oid": "66fd60b20ba592c4c95e948b" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/Melee/ShieldAndSword/CrpHammerShield/CrpHammerShield", - "ItemPrices": [ - { - "ItemCount": 40, - "ItemType": "/Lotus/Types/Items/MiscItems/GranumBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 7441523153174502000, - "Id": { - "$oid": "66fd60b20ba592c4c95e948c" - } - } - ], - "PropertyTextHash": "34F8CF1DFF745F0D67433A5EF0A03E70", - "RandomSeedType": "VRST_WEAPON", - "WeaponUpgradeValueAttenuationExponent": 2.25, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - } - } -} 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/OstronFishmongerVendorManifest.json b/static/fixed_responses/getVendorInfo/OstronFishmongerVendorManifest.json deleted file mode 100644 index c6a3670b..00000000 --- a/static/fixed_responses/getVendorInfo/OstronFishmongerVendorManifest.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "VendorInfo": { - "_id": { - "$oid": "59d6e27ebcc718474eb17115" - }, - "TypeName": "/Lotus/Types/Game/VendorManifests/Ostron/FishmongerVendorManifest", - "ItemManifest": [ - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Eidolon/FishParts/DayUncommonFishAPartItem", - "PremiumPrice": [14, 14], - "Bin": "BIN_1", - "QuantityMultiplier": 10, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e9808" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Eidolon/FishParts/BothUncommonFishBPartItem", - "PremiumPrice": [12, 12], - "Bin": "BIN_1", - "QuantityMultiplier": 10, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e9809" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Eidolon/FishParts/DayCommonFishCPartItem", - "PremiumPrice": [8, 8], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e980a" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Eidolon/FishParts/DayCommonFishBPartItem", - "PremiumPrice": [7, 7], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e980b" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Eidolon/FishParts/DayCommonFishAPartItem", - "PremiumPrice": [10, 10], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e980c" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Eidolon/FishParts/BothCommonFishBPartItem", - "PremiumPrice": [8, 8], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e980d" - } - } - ], - "PropertyTextHash": "CC3B9DAFB38F412998E90A41421A8986", - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - } - } -} diff --git a/static/fixed_responses/getVendorInfo/RadioLegionIntermission12VendorManifest.json b/static/fixed_responses/getVendorInfo/RadioLegionIntermission12VendorManifest.json deleted file mode 100644 index 33198a22..00000000 --- a/static/fixed_responses/getVendorInfo/RadioLegionIntermission12VendorManifest.json +++ /dev/null @@ -1,1192 +0,0 @@ -{ - "VendorInfo": { - "_id": { - "$oid": "67a04d500000000000000000" - }, - "TypeName": "/Lotus/Types/Game/VendorManifests/Events/RadioLegionIntermission12VendorManifest", - "ItemManifest": [ - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperChassisBlueprint", - "ItemPrices": [ - { - "ItemCount": 25, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000049" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperSystemsBlueprint", - "ItemPrices": [ - { - "ItemCount": 25, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000050" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 25, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000051" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/OrokinCatalyst", - "ItemPrices": [ - { - "ItemCount": 75, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000052" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/OrokinReactor", - "ItemPrices": [ - { - "ItemCount": 75, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000053" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/Alertium", - "ItemPrices": [ - { - "ItemCount": 15, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 5, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000054" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/Kuva", - "ItemPrices": [ - { - "ItemCount": 50, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_3", - "QuantityMultiplier": 10000, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000055" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Nightwave/GlassmakerShipDeco", - "ItemPrices": [ - { - "ItemCount": 60, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "Id": { - "$oid": "001500150000000000000056" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Skins/Hoverboard/HoverboardStickerWolf", - "ItemPrices": [ - { - "ItemCount": 30, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000057" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/StatlessV2MagAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000058" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/StatlessRhinoAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000059" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/NekrosShroudHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000060" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/StatlessAshAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000061" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/XakuAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000062" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/StatlessVaubanAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000063" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/StatlessV2TrinityAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000064" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/HildrynAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000065" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/StatlessFrostAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000066" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/OperatorArmour/Hood/RealOperatorWolfHoodBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000067" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/StatlessV2SarynAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000068" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/NidusAltTwoHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000069" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/OberonAltBHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000070" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/StyanaxAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000071" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/ExcaliburMordredHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000072" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/RangerAltBHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000073" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/StatlessV2EmberAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000074" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/AnimaAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000075" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/JadeAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000076" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/NekrosAraknidHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000077" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/ProteaAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000078" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/PriestAlt2HelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000079" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/Skins/AtomosSolsticeSkinBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000080" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/DanteAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000081" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/RevenantAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000082" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Helmets/SandmanAltHelmetBlueprint", - "ItemPrices": [ - { - "ItemCount": 35, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000083" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Aura/PlayerLootRadarAuraMod", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000084" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Aura/PlayerRifleDamageAuraMod", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000085" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Aura/PlayerShellAmmoAuraMod", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000086" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Nightwave/BroncoNightwaveMod", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000087" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Aura/PlayerHealthAuraMod", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000088" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Nightwave/MagnusNightwaveMod", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000089" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Event/Nightwave/NightwaveBattacorAugmentMod", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000090" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/Skins/GrnAxeBlueprint", - "ItemPrices": [ - { - "ItemCount": 30, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_2", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000091" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/Skins/DesertGrinlokSkinBlueprint", - "ItemPrices": [ - { - "ItemCount": 30, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_2", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000092" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/HeatDaggerBlueprint", - "ItemPrices": [ - { - "ItemCount": 50, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_3", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000093" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/GlaiveBlueprint", - "ItemPrices": [ - { - "ItemCount": 50, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_3", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000094" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/DarkDaggerBlueprint", - "ItemPrices": [ - { - "ItemCount": 50, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_3", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000095" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Skins/Sigils/NoraSeasonTwoSigil", - "ItemPrices": [ - { - "ItemCount": 30, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_3", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000096" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/DarkSwordBlueprint", - "ItemPrices": [ - { - "ItemCount": 50, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_3", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000097" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/PlasmaSwordBlueprint", - "ItemPrices": [ - { - "ItemCount": 50, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_3", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000098" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/Skins/ShockExergisSkinBlueprint", - "ItemPrices": [ - { - "ItemCount": 30, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_3", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000099" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyThumperMediumDirty", - "ItemPrices": [ - { - "ItemCount": 40, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_3", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000100" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/PvPMods/Rifle/TetraFasterProjAiming", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_4", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000101" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Powersuits/Jade/SelfBulletAttractorPvPAugmentCard", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_4", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000102" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/PvPMods/Rifle/SupraHigherAccuracyAiming", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_4", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000103" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/PvPMods/Rifle/RubicoLowZoom", - "ItemPrices": [ - { - "ItemCount": 20, - "ItemType": "/Lotus/Types/Items/MiscItems/NoraIntermissionTwelveCreds", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_4", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "001500150000000000000104" - } - } - ], - "Expiry": { - "$date": { - "$numberLong": "2051240400000" - } - } - } -} diff --git a/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorManifest.json b/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorManifest.json deleted file mode 100644 index 3a4fa0ac..00000000 --- a/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorManifest.json +++ /dev/null @@ -1,248 +0,0 @@ -{ - "VendorInfo": { - "_id": { - "$oid": "5be4a159b144f3cdf1c22efa" - }, - "TypeName": "/Lotus/Types/Game/VendorManifests/Solaris/DebtTokenVendorManifest", - "ItemManifest": [ - { - "StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleUncommonD", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Gameplay/Venus/Resources/VenusCoconutItem", - "ItemCount": 5, - "ProductCategory": "MiscItems" - }, - { - "ItemType": "/Lotus/Types/Items/MiscItems/Circuits", - "ItemCount": 3664, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [87300, 87300], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": true, - "LocTagRandSeed": 1881404827, - "Id": { - "$oid": "670daf92d21f34757a5e73b4" - } - }, - { - "StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleRareC", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Items/MiscItems/NeuralSensor", - "ItemCount": 1, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [53300, 53300], - "Bin": "BIN_2", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": true, - "LocTagRandSeed": 1943984533, - "Id": { - "$oid": "6710b5029e1a3080a65e73a7" - } - }, - { - "StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleCommonG", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Items/MiscItems/Salvage", - "ItemCount": 11540, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [27300, 27300], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": true, - "LocTagRandSeed": 744199559, - "Id": { - "$oid": "67112582cc115756985e73a4" - } - }, - { - "StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleUncommonB", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Items/Fish/Solaris/FishParts/CorpusFishThermalLaserItem", - "ItemCount": 9, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [75800, 75800], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": true, - "LocTagRandSeed": 3744711432, - "Id": { - "$oid": "670de7d28a6ec82cd25e73a2" - } - }, - { - "StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleUncommonB", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Items/MiscItems/Rubedo", - "ItemCount": 3343, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [52200, 52200], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": true, - "LocTagRandSeed": 1579000687, - "Id": { - "$oid": "670e58526171148e125e73ad" - } - }, - { - "StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleCommonA", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Gameplay/Venus/Resources/CoolantItem", - "ItemCount": 9, - "ProductCategory": "MiscItems" - }, - { - "ItemType": "/Lotus/Types/Items/Fish/Solaris/FishParts/CorpusFishAnoscopicSensorItem", - "ItemCount": 5, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [12400, 12400], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": true, - "LocTagRandSeed": 3589081466, - "Id": { - "$oid": "67112582cc115756985e73a5" - } - }, - { - "StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleUncommonC", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Items/Gems/Solaris/SolarisCommonOreBAlloyItem", - "ItemCount": 13, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [77500, 77500], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": true, - "LocTagRandSeed": 1510234814, - "Id": { - "$oid": "670f0f21250ad046c35e73ee" - } - }, - { - "StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleUncommonD", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Items/Fish/Solaris/FishParts/CorpusFishParralelBiodeItem", - "ItemCount": 7, - "ProductCategory": "MiscItems" - }, - { - "ItemType": "/Lotus/Types/Items/Gems/Solaris/SolarisCommonGemBCutItem", - "ItemCount": 12, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [94600, 94600], - "Bin": "BIN_1", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": true, - "LocTagRandSeed": 4222095721, - "Id": { - "$oid": "670f63827be40254f95e739d" - } - }, - { - "StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleCommonJ", - "ItemPrices": [ - { - "ItemType": "/Lotus/Types/Items/MiscItems/Nanospores", - "ItemCount": 14830, - "ProductCategory": "MiscItems" - } - ], - "RegularPrice": [25600, 25600], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": true, - "LocTagRandSeed": 2694388669, - "Id": { - "$oid": "67112582cc115756985e73a6" - } - } - ], - "PropertyTextHash": "A39621049CA3CA13761028CD21C239EF", - "RandomSeedType": "VRST_FLAVOUR_TEXT", - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - } - } -} diff --git a/static/fixed_responses/getVendorInfo/SolarisFishmongerVendorManifest.json b/static/fixed_responses/getVendorInfo/SolarisFishmongerVendorManifest.json deleted file mode 100644 index 4a4dcb64..00000000 --- a/static/fixed_responses/getVendorInfo/SolarisFishmongerVendorManifest.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "VendorInfo": { - "_id": { - "$oid": "5b0de8556df82a56ea9bae82" - }, - "TypeName": "/Lotus/Types/Game/VendorManifests/Solaris/FishmongerVendorManifest", - "ItemManifest": [ - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Solaris/FishParts/CorpusFishThermalLaserItem", - "PremiumPrice": [15, 15], - "Bin": "BIN_1", - "QuantityMultiplier": 10, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e9515" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Solaris/FishParts/CorpusFishVenedoCaseItem", - "PremiumPrice": [8, 8], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e9516" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Solaris/FishParts/SolarisFishDissipatorCoilItem", - "PremiumPrice": [18, 18], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e9517" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Solaris/FishParts/CorpusFishExaBrainItem", - "PremiumPrice": [5, 5], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e9518" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Solaris/FishParts/CorpusFishAnoscopicSensorItem", - "PremiumPrice": [5, 5], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e9519" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Solaris/FishParts/GenericFishScrapItem", - "PremiumPrice": [5, 5], - "Bin": "BIN_0", - "QuantityMultiplier": 20, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "AllowMultipurchase": true, - "Id": { - "$oid": "66fd60b20ba592c4c95e951a" - } - } - ], - "PropertyTextHash": "946131D0CF5CDF7C2C03BB967DE0DF49", - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - } - } -} diff --git a/static/fixed_responses/getVendorInfo/Temple1999VendorManifest.json b/static/fixed_responses/getVendorInfo/Temple1999VendorManifest.json new file mode 100644 index 00000000..6309363f --- /dev/null +++ b/static/fixed_responses/getVendorInfo/Temple1999VendorManifest.json @@ -0,0 +1,459 @@ +{ + "VendorInfo": { + "_id": { + "$oid": "67dadc30e4b6e0e5979c8d56" + }, + "TypeName": "/Lotus/Types/Game/VendorManifests/TheHex/Temple1999VendorManifest", + "ItemManifest": [ + { + "StoreItem": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TempleBlueprint", + "ItemPrices": [ + { + "ItemCount": 195, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c18c" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TempleSystemsBlueprint", + "ItemPrices": [ + { + "ItemCount": 65, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c18d" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TempleChassisBlueprint", + "ItemPrices": [ + { + "ItemCount": 65, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c18e" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TempleHelmetBlueprint", + "ItemPrices": [ + { + "ItemCount": 65, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c18f" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/1999EntHybridPistolBlueprint", + "ItemPrices": [ + { + "ItemCount": 120, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c190" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/WeaponParts/1999EntHybridPistolBarrelBlueprint", + "ItemPrices": [ + { + "ItemCount": 60, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c191" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/WeaponParts/1999EntHybridPistolReceiverBlueprint", + "ItemPrices": [ + { + "ItemCount": 60, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c192" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/WeaponParts/1999EntHybridPistolStockBlueprint", + "ItemPrices": [ + { + "ItemCount": 60, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c193" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumDrumCoreKitA", + "ItemPrices": [ + { + "ItemCount": 30, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c194" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumDrumCymbalA", + "ItemPrices": [ + { + "ItemCount": 30, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c195" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumDrumFloorTomA", + "ItemPrices": [ + { + "ItemCount": 30, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c196" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumDrumSnareA", + "ItemPrices": [ + { + "ItemCount": 30, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c197" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumEquipmentCaseA", + "ItemPrices": [ + { + "ItemCount": 30, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c198" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumEquipmentCaseB", + "ItemPrices": [ + { + "ItemCount": 30, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c199" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumEquipmentCaseC", + "ItemPrices": [ + { + "ItemCount": 30, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c19a" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumEquipmentCaseD", + "ItemPrices": [ + { + "ItemCount": 30, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c19b" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumEquipmentCaseE", + "ItemPrices": [ + { + "ItemCount": 30, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c19c" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumEquipmentCaseF", + "ItemPrices": [ + { + "ItemCount": 30, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c19d" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumSynthKeyboardA", + "ItemPrices": [ + { + "ItemCount": 30, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c19e" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/PhotoBooth/Vania/PhotoboothTileVaniaObjTempleDefense", + "ItemPrices": [ + { + "ItemCount": 100, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 1, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "AllowMultipurchase": false, + "Id": { + "$oid": "67dadc30641da66dc5c1c19f" + } + }, + { + "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/Kuva", + "ItemPrices": [ + { + "ItemCount": 110, + "ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense", + "ProductCategory": "MiscItems" + } + ], + "Bin": "BIN_0", + "QuantityMultiplier": 6000, + "Expiry": { + "$date": { + "$numberLong": "2051240400000" + } + }, + "PurchaseQuantityLimit": 7, + "AllowMultipurchase": true, + "Id": { + "$oid": "67dadc30641da66dc5c1c1a5" + } + } + ], + "PropertyTextHash": "20B13D9EB78FEC80EA32D0687F5BA1AE", + "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/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/login_static.ts b/static/fixed_responses/login_static.ts deleted file mode 100644 index d5e5ea1a..00000000 --- a/static/fixed_responses/login_static.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { IGroup } from "@/src/types/loginTypes"; - -export const groups: IGroup[] = [ - { - experiment: "InitiatePage", - experimentGroup: "initiate_page_no_video" - }, - { experiment: "ChatQAChannel", experimentGroup: "control" }, - { - experiment: "MarketSearchRecommendations", - experimentGroup: "premium_credit_purchases_14_days" - }, - { experiment: "SurveyLocation", experimentGroup: "EXIT" }, - { experiment: "GamesightAB", experimentGroup: "a" } -]; - -export const platformCDNs = [ - "https://content.warframe.com/", - "https://content-xb1.warframe.com/", - "https://content-ps4.warframe.com/", - "https://content-swi.warframe.com/", - "https://content-mob.warframe.com/" -]; - -export const DTLS = 99; - -export const HUB = "https://arbiter.warframe.com/api/"; diff --git a/static/fixed_responses/messages.json b/static/fixed_responses/messages.json deleted file mode 100644 index a67069c7..00000000 --- a/static/fixed_responses/messages.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "Messages": [ - { - "sub": "Welcome to Space Ninja Server", - "sndr": "/Lotus/Language/Bosses/Ordis", - "msg": "Enjoy your Space Ninja Experience", - "icon": "/Lotus/Interface/Icons/Npcs/Darvo.png", - "eventMessageDate": "2025-01-30T13:00:00.000Z", - "r": false - }, - { - "sub": "/Lotus/Language/Inbox/DarvoWeaponCraftingMessageBTitle", - "sndr": "/Lotus/Language/Bosses/Darvo", - "msg": "/Lotus/Language/Inbox/DarvoWeaponCraftingMessageBDesc", - "icon": "/Lotus/Interface/Icons/Npcs/Darvo.png", - "countedAtt": [ - { - "ItemCount": 1, - "ItemType": "/Lotus/Types/Recipes/Weapons/BurstonRifleBlueprint" - }, - { - "ItemCount": 1, - "ItemType": "/Lotus/Types/Items/MiscItems/Morphic" - }, - { - "ItemCount": 400, - "ItemType": "/Lotus/Types/Items/MiscItems/PolymerBundle" - }, - { - "ItemCount": 150, - "ItemType": "/Lotus/Types/Items/MiscItems/AlloyPlate" - } - ], - "highPriority": true, - "eventMessageDate": "2023-10-01T17:00:00.000Z", - "r": false - }, - { - "sub": "/Lotus/Language/G1Quests/Beginner_Growth_Inbox_Title", - "sndr": "/Lotus/Language/Menu/Mailbox_WarframeSender", - "msg": "/Lotus/Language/G1Quests/Beginner_Growth_Inbox_Desc", - "icon": "/Lotus/Interface/Icons/Npcs/Lotus_d.png", - "transmission": "/Lotus/Sounds/Dialog/VorsPrize/DLisetPostAssassinate110Lotus", - "highPriority": true, - "eventMessageDate": "2023-09-01T17:00:00.000Z", - "r": false - } - ] -} 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..1e584e72 100644 --- a/static/fixed_responses/questCompletionRewards.json +++ b/static/fixed_responses/questCompletionRewards.json @@ -1,13 +1,21 @@ { "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain": [ - { - "ItemType": "/Lotus/Types/Keys/DuviriQuest/DuviriQuestKeyChain", - "ItemCount": 1 - }, { "ItemType": "/Lotus/Types/NeutralCreatures/ErsatzHorse/ErsatzHorsePowerSuit", "ItemCount": 1 } ], - "/Lotus/Types/Keys/InfestedMicroplanetQuest/InfestedMicroplanetQuestKeyChain": [{ "ItemType": "/Lotus/Types/Recipes/WarframeRecipes/BrokenFrameBlueprint", "ItemCount": 1 }] + "/Lotus/Types/Keys/InfestedMicroplanetQuest/InfestedMicroplanetQuestKeyChain": [{ "ItemType": "/Lotus/Types/Recipes/WarframeRecipes/BrokenFrameBlueprint", "ItemCount": 1 }], + "/Lotus/Types/Keys/OrokinMoonQuest/OrokinMoonQuestKeyChain": [ + { + "ItemType": "/Lotus/Types/Keys/RailJackBuildQuest/RailjackBuildQuestEmailItem", + "ItemCount": 1 + } + ], + "/Lotus/Types/Keys/EntratiLab/EntratiQuestKeyChain": [ + { + "ItemType": "/Lotus/Types/Keys/1999PrologueQuest/1999PrologueQuestKeyChain", + "ItemCount": 1 + } + ] } diff --git a/static/fixed_responses/worldState/1999_fall_days.json b/static/fixed_responses/worldState/1999_fall_days.json deleted file mode 100644 index 3bcd30eb..00000000 --- a/static/fixed_responses/worldState/1999_fall_days.json +++ /dev/null @@ -1,77 +0,0 @@ -[ - { "day": 276, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesEasy" }] }, - { - "day": 283, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/CompanionDamage" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/GasChanceToPrimaryAndSecondary" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/ElectricStatusDamageAndChance" } - ] - }, - { - "day": 289, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponUtilityUnlocker" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarMajorArtifactPack" } - ] - }, - { "day": 295, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithMeleeEasy" }] }, - { - "day": 302, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarArtifactPack" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponSecondaryArcaneUnlocker" } - ] - }, - { "day": 305, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillEximusMedium" }] }, - { "day": 306, "events": [{ "type": "CET_PLOT", "dialogueName": "/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue", "dialogueConvo": "EleanorBirthdayConvo" }] }, - { "day": 307, "events": [{ "type": "CET_PLOT", "dialogueName": "/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue", "dialogueConvo": "ArthurBirthdayConvo" }] }, - { - "day": 309, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/Forma" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalBoreal" } - ] - }, - { - "day": 314, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/PowerStrengthAndEfficiencyPerEnergySpent" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/ElectricalDamageOnBulletJump" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/MeleeSlideFowardMomentumOnEnemyHit" } - ] - }, - { "day": 322, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesMedium" }] }, - { - "day": 328, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponSecondaryArcaneUnlocker" }, - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Packages/Calendar/CalendarKuvaBundleSmall" } - ] - }, - { "day": 337, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithAbilitiesHard" }] }, - { "day": 338, "events": [{ "type": "CET_PLOT", "dialogueName": "/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue", "dialogueConvo": "QuincyBirthdayConvo" }] }, - { - "day": 340, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/MeleeCritChance" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/RadiationProcOnTakeDamage" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/AbilityStrength" } - ] - }, - { - "day": 343, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponPrimaryArcaneUnlocker" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/FormaAura" } - ] - }, - { "day": 352, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillTankHard" }] }, - { - "day": 364, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarMajorArtifactPack" }, - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Boosters/ModDropChanceBooster3DayStoreItem" } - ] - } -] diff --git a/static/fixed_responses/worldState/1999_spring_days.json b/static/fixed_responses/worldState/1999_spring_days.json deleted file mode 100644 index 4386f2a4..00000000 --- a/static/fixed_responses/worldState/1999_spring_days.json +++ /dev/null @@ -1,75 +0,0 @@ -[ - { "day": 100, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesEasy" }] }, - { - "day": 101, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/EnergyOrbToAbilityRange" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/ElectricStatusDamageAndChance" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/EnergyRestoration" } - ] - }, - { - "day": 102, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalBoreal" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarMajorArtifactPack" } - ] - }, - { "day": 106, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesEasy" }] }, - { - "day": 107, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarArtifactPack" }, - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Packages/Calendar/CalendarKuvaBundleSmall" } - ] - }, - { "day": 122, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithMeleeMedium" }] }, - { - "day": 127, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/Forma" }, - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Packages/Calendar/CalendarVosforPack" } - ] - }, - { "day": 129, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithAbilitiesMedium" }] }, - { - "day": 135, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarArtifactPack" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponMeleeArcaneUnlocker" } - ] - }, - { - "day": 142, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/BlastEveryXShots" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/MagnitizeWithinRangeEveryXCasts" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/GenerateOmniOrbsOnWeakKill" } - ] - }, - { "day": 143, "events": [{ "type": "CET_PLOT", "dialogueName": "/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue", "dialogueConvo": "AmirBirthdayConvo" }] }, - { "day": 161, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithAbilitiesHard" }] }, - { - "day": 165, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/Forma" }, - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Boosters/ModDropChanceBooster3DayStoreItem" } - ] - }, - { "day": 169, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarDestroyPropsHard" }] }, - { - "day": 171, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/GasChanceToPrimaryAndSecondary" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/AbilityStrength" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/MeleeCritChance" } - ] - }, - { - "day": 176, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarArtifactPack" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Recipes/Components/WeaponUtilityUnlockerBlueprint" } - ] - } -] diff --git a/static/fixed_responses/worldState/1999_summer_days.json b/static/fixed_responses/worldState/1999_summer_days.json deleted file mode 100644 index 99beee4a..00000000 --- a/static/fixed_responses/worldState/1999_summer_days.json +++ /dev/null @@ -1,75 +0,0 @@ -[ - { "day": 186, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesEasy" }] }, - { "day": 191, "events": [{ "type": "CET_PLOT", "dialogueName": "/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue", "dialogueConvo": "AoiBirthdayConvo" }] }, - { - "day": 193, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalAmar" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarMajorArtifactPack" } - ] - }, - { - "day": 197, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/MeleeAttackSpeed" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/AbilityStrength" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/CompanionDamage" } - ] - }, - { "day": 199, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithMeleeMedium" }] }, - { - "day": 210, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarArtifactPack" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/CircuitSilverSteelPathFusionBundle" } - ] - }, - { "day": 215, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesWithMeleeEasy" }] }, - { - "day": 228, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Recipes/Components/WeaponUtilityUnlockerBlueprint" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarRivenPack" } - ] - }, - { "day": 236, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarDestroyPropsMedium" }] }, - { - "day": 237, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Packages/Calendar/CalendarKuvaBundleLarge" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarMajorArtifactPack" } - ] - }, - { - "day": 240, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/RadialJavelinOnHeavy" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/SharedFreeAbilityEveryXCasts" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/CompanionsRadiationChance" } - ] - }, - { "day": 245, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithAbilitiesHard" }] }, - { - "day": 250, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Boosters/AffinityBooster3DayStoreItem" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Recipes/Components/OrokinReactorBlueprint" } - ] - }, - { "day": 254, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillTankHard" }] }, - { - "day": 267, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarArtifactPack" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponSecondaryArcaneUnlocker" } - ] - }, - { - "day": 270, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/EnergyOrbToAbilityRange" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/PunchToPrimary" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/OvershieldCap" } - ] - } -] diff --git a/static/fixed_responses/worldState/1999_winter_days.json b/static/fixed_responses/worldState/1999_winter_days.json deleted file mode 100644 index 700866d3..00000000 --- a/static/fixed_responses/worldState/1999_winter_days.json +++ /dev/null @@ -1,75 +0,0 @@ -[ - { "day": 6, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillEximusEasy" }] }, - { - "day": 15, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/MagazineCapacity" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/Armor" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/EnergyRestoration" } - ] - }, - { "day": 21, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesEasy" }] }, - { - "day": 25, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarMajorArtifactPack" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalGreen" } - ] - }, - { - "day": 31, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Recipes/Components/WeaponUtilityUnlockerBlueprint" }, - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Packages/Calendar/CalendarKuvaBundleSmall" } - ] - }, - { "day": 43, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithAbilitiesMedium" }] }, - { "day": 45, "events": [{ "type": "CET_PLOT", "dialogueName": "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue", "dialogueConvo": "LettieBirthdayConvo" }] }, - { - "day": 47, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Boosters/AffinityBooster3DayStoreItem" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarMajorArtifactPack" } - ] - }, - { "day": 48, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithMeleeMedium" }] }, - { - "day": 54, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/CompanionsBuffNearbyPlayer" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/OrbsDuplicateOnPickup" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/FinisherChancePerComboMultiplier" } - ] - }, - { - "day": 56, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarArtifactPack" }, - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Packages/Calendar/CalendarKuvaBundleSmall" } - ] - }, - { "day": 71, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesHard" }] }, - { - "day": 77, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponSecondaryArcaneUnlocker" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/CircuitSilverSteelPathFusionBundle" } - ] - }, - { "day": 80, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarDestroyPropsMedium" }] }, - { - "day": 83, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Recipes/Components/OrokinReactorBlueprint" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponUtilityUnlocker" } - ] - }, - { - "day": 87, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/EnergyOrbToAbilityRange" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/MeleeAttackSpeed" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/CompanionDamage" } - ] - } -] diff --git a/static/fixed_responses/worldState/sortieTilesetMissions.json b/static/fixed_responses/worldState/sortieTilesetMissions.json new file mode 100644 index 00000000..36d5e194 --- /dev/null +++ b/static/fixed_responses/worldState/sortieTilesetMissions.json @@ -0,0 +1,45 @@ +{ + "CorpusGasCityTileset": ["MT_ARTIFACT", "MT_ASSASSINATION", "MT_DEFENSE", "MT_EXTERMINATION", "MT_INTEL", "MT_MOBILE_DEFENSE", "MT_RESCUE", "MT_SABOTAGE", "MT_SURVIVAL", "MT_TERRITORY"], + "CorpusIcePlanetTileset": ["MT_ASSASSINATION", "MT_DEFENSE", "MT_EXCAVATE", "MT_INTEL", "MT_MOBILE_DEFENSE", "MT_RESCUE", "MT_RETRIEVAL", "MT_TERRITORY"], + "CorpusIcePlanetTilesetCaves": ["MT_EXTERMINATION", "MT_INTEL"], + "CorpusOutpostTileset": [ + "MT_ARTIFACT", + "MT_ASSASSINATION", + "MT_DEFENSE", + "MT_EXCAVATE", + "MT_EXTERMINATION", + "MT_INTEL", + "MT_MOBILE_DEFENSE", + "MT_RESCUE", + "MT_SABOTAGE", + "MT_SURVIVAL", + "MT_TERRITORY" + ], + "CorpusShipTileset": ["MT_ARTIFACT", "MT_ASSASSINATION", "MT_DEFENSE", "MT_EXTERMINATION", "MT_INTEL", "MT_MOBILE_DEFENSE", "MT_RESCUE", "MT_SABOTAGE", "MT_SURVIVAL", "MT_TERRITORY"], + "EidolonTileset": ["MT_LANDSCAPE"], + "GrineerAsteroidTileset": ["MT_ASSASSINATION", "MT_DEFENSE", "MT_EVACUATION", "MT_EXTERMINATION", "MT_INTEL", "MT_MOBILE_DEFENSE", "MT_RESCUE", "MT_SURVIVAL", "MT_TERRITORY"], + "GrineerForestTileset": ["MT_ASSASSINATION", "MT_EXTERMINATION", "MT_INTEL", "MT_MOBILE_DEFENSE", "MT_TERRITORY"], + "GrineerFortressTileset": ["MT_ARTIFACT", "MT_ASSAULT", "MT_INTEL", "MT_MOBILE_DEFENSE", "MT_RESCUE", "MT_SURVIVAL"], + "GrineerGalleonTileset": [ + "MT_ARTIFACT", + "MT_ASSASSINATION", + "MT_DEFENSE", + "MT_EVACUATION", + "MT_EXTERMINATION", + "MT_INTEL", + "MT_MOBILE_DEFENSE", + "MT_RESCUE", + "MT_SABOTAGE", + "MT_SURVIVAL", + "MT_TERRITORY" + ], + "GrineerOceanTileset": ["MT_DEFENSE", "MT_EXTERMINATION", "MT_INTEL", "MT_MOBILE_DEFENSE", "MT_RESCUE", "MT_SABOTAGE", "MT_SURVIVAL", "MT_TERRITORY"], + "GrineerOceanTilesetAnywhere": ["MT_ASSASSINATION"], + "GrineerSettlementTileset": ["MT_ASSASSINATION", "MT_DEFENSE", "MT_EXTERMINATION", "MT_INTEL", "MT_MOBILE_DEFENSE", "MT_RESCUE", "MT_SABOTAGE", "MT_TERRITORY"], + "GrineerShipyardsTileset": ["MT_DEFENSE", "MT_EXTERMINATION", "MT_INTEL", "MT_MOBILE_DEFENSE", "MT_RESCUE", "MT_RETRIEVAL", "MT_TERRITORY"], + "InfestedCorpusShipTileset": ["MT_ASSASSINATION", "MT_HIVE", "MT_MOBILE_DEFENSE", "MT_RESCUE"], + "OrokinDerelictTileset": ["MT_ARTIFACT", "MT_ASSASSINATION", "MT_DEFENSE", "MT_EXTERMINATION", "MT_MOBILE_DEFENSE", "MT_SABOTAGE", "MT_SURVIVAL"], + "OrokinMoonTilesetCorpus": ["MT_ARTIFACT", "MT_DEFENSE", "MT_EXTERMINATION", "MT_MOBILE_DEFENSE", "MT_RESCUE", "MT_SURVIVAL"], + "OrokinMoonTilesetGrineer": ["MT_DEFENSE", "MT_EXTERMINATION", "MT_MOBILE_DEFENSE", "MT_RESCUE", "MT_SURVIVAL"], + "OrokinVoidTileset": ["MT_DEFENSE", "MT_EXTERMINATION", "MT_MOBILE_DEFENSE", "MT_SABOTAGE", "MT_SURVIVAL", "MT_TERRITORY"] +} diff --git a/static/fixed_responses/worldState/sortieTilesets.json b/static/fixed_responses/worldState/sortieTilesets.json new file mode 100644 index 00000000..d2965c09 --- /dev/null +++ b/static/fixed_responses/worldState/sortieTilesets.json @@ -0,0 +1,175 @@ +{ + "SettlementNode1": "CorpusShipTileset", + "SettlementNode11": "CorpusShipTileset", + "SettlementNode12": "CorpusShipTileset", + "SettlementNode14": "CorpusShipTileset", + "SettlementNode15": "CorpusShipTileset", + "SettlementNode2": "CorpusShipTileset", + "SettlementNode20": "CorpusShipTileset", + "SettlementNode3": "CorpusShipTileset", + "SolNode1": "CorpusOutpostTileset", + "SolNode10": "CorpusGasCityTileset", + "SolNode100": "CorpusGasCityTileset", + "SolNode101": "CorpusOutpostTileset", + "SolNode102": "CorpusShipTileset", + "SolNode103": "GrineerAsteroidTileset", + "SolNode104": "CorpusShipTileset", + "SolNode105": "GrineerOceanTilesetAnywhere", + "SolNode106": "GrineerSettlementTileset", + "SolNode107": "CorpusOutpostTileset", + "SolNode108": "GrineerAsteroidTileset", + "SolNode109": "CorpusOutpostTileset", + "SolNode11": "GrineerSettlementTileset", + "SolNode113": "GrineerSettlementTileset", + "SolNode118": "CorpusShipTileset", + "SolNode119": "GrineerAsteroidTileset", + "SolNode12": "GrineerAsteroidTileset", + "SolNode121": "CorpusGasCityTileset", + "SolNode122": "GrineerOceanTileset", + "SolNode123": "CorpusShipTileset", + "SolNode125": "CorpusGasCityTileset", + "SolNode126": "CorpusGasCityTileset", + "SolNode127": "CorpusShipTileset", + "SolNode128": "CorpusOutpostTileset", + "SolNode130": "GrineerAsteroidTileset", + "SolNode131": "GrineerShipyardsTileset", + "SolNode132": "GrineerShipyardsTileset", + "SolNode135": "GrineerGalleonTileset", + "SolNode137": "GrineerShipyardsTileset", + "SolNode138": "GrineerShipyardsTileset", + "SolNode139": "GrineerShipyardsTileset", + "SolNode14": "CorpusIcePlanetTilesetCaves", + "SolNode140": "GrineerShipyardsTileset", + "SolNode141": "GrineerShipyardsTileset", + "SolNode144": "GrineerShipyardsTileset", + "SolNode146": "GrineerAsteroidTileset", + "SolNode147": "GrineerShipyardsTileset", + "SolNode149": "GrineerShipyardsTileset", + "SolNode15": "GrineerGalleonTileset", + "SolNode16": "GrineerSettlementTileset", + "SolNode162": "InfestedCorpusShipTileset", + "SolNode164": "InfestedCorpusShipTileset", + "SolNode166": "InfestedCorpusShipTileset", + "SolNode17": "CorpusShipTileset", + "SolNode171": "InfestedCorpusShipTileset", + "SolNode172": "CorpusShipTileset", + "SolNode173": "InfestedCorpusShipTileset", + "SolNode175": "InfestedCorpusShipTileset", + "SolNode177": "GrineerGalleonTileset", + "SolNode18": "GrineerAsteroidTileset", + "SolNode181": "GrineerAsteroidTileset", + "SolNode184": "GrineerGalleonTileset", + "SolNode185": "GrineerGalleonTileset", + "SolNode187": "GrineerAsteroidTileset", + "SolNode188": "GrineerGalleonTileset", + "SolNode189": "GrineerGalleonTileset", + "SolNode19": "GrineerAsteroidTileset", + "SolNode191": "GrineerShipyardsTileset", + "SolNode193": "GrineerAsteroidTileset", + "SolNode195": "GrineerGalleonTileset", + "SolNode196": "GrineerGalleonTileset", + "SolNode2": "CorpusOutpostTileset", + "SolNode20": "GrineerGalleonTileset", + "SolNode203": "CorpusIcePlanetTileset", + "SolNode205": "CorpusIcePlanetTileset", + "SolNode209": "CorpusIcePlanetTileset", + "SolNode21": "CorpusOutpostTileset", + "SolNode210": "CorpusIcePlanetTileset", + "SolNode211": "CorpusIcePlanetTileset", + "SolNode212": "CorpusIcePlanetTileset", + "SolNode214": "CorpusIcePlanetTileset", + "SolNode215": "CorpusShipTileset", + "SolNode216": "CorpusIcePlanetTileset", + "SolNode217": "CorpusIcePlanetTileset", + "SolNode22": "CorpusOutpostTileset", + "SolNode220": "CorpusIcePlanetTileset", + "SolNode223": "GrineerAsteroidTileset", + "SolNode224": "GrineerGalleonTileset", + "SolNode225": "GrineerGalleonTileset", + "SolNode226": "GrineerGalleonTileset", + "SolNode228": "EidolonTileset", + "SolNode23": "CorpusShipTileset", + "SolNode24": "GrineerForestTileset", + "SolNode25": "CorpusGasCityTileset", + "SolNode26": "GrineerForestTileset", + "SolNode30": "GrineerSettlementTileset", + "SolNode300": "OrokinMoonTilesetGrineer", + "SolNode301": "OrokinMoonTilesetGrineer", + "SolNode302": "OrokinMoonTilesetCorpus", + "SolNode304": "OrokinMoonTilesetCorpus", + "SolNode305": "OrokinMoonTilesetGrineer", + "SolNode306": "OrokinMoonTilesetCorpus", + "SolNode307": "OrokinMoonTilesetCorpus", + "SolNode308": "OrokinMoonTilesetCorpus", + "SolNode31": "GrineerGalleonTileset", + "SolNode32": "GrineerGalleonTileset", + "SolNode36": "GrineerSettlementTileset", + "SolNode38": "CorpusOutpostTileset", + "SolNode39": "GrineerForestTileset", + "SolNode4": "CorpusShipTileset", + "SolNode400": "OrokinVoidTileset", + "SolNode401": "OrokinVoidTileset", + "SolNode402": "OrokinVoidTileset", + "SolNode403": "OrokinVoidTileset", + "SolNode404": "OrokinVoidTileset", + "SolNode405": "OrokinVoidTileset", + "SolNode406": "OrokinVoidTileset", + "SolNode407": "OrokinVoidTileset", + "SolNode408": "OrokinVoidTileset", + "SolNode409": "OrokinVoidTileset", + "SolNode41": "GrineerSettlementTileset", + "SolNode410": "OrokinVoidTileset", + "SolNode412": "OrokinVoidTileset", + "SolNode42": "GrineerGalleonTileset", + "SolNode43": "CorpusOutpostTileset", + "SolNode45": "GrineerSettlementTileset", + "SolNode46": "GrineerSettlementTileset", + "SolNode48": "CorpusOutpostTileset", + "SolNode49": "CorpusShipTileset", + "SolNode50": "GrineerAsteroidTileset", + "SolNode51": "CorpusOutpostTileset", + "SolNode53": "CorpusGasCityTileset", + "SolNode56": "CorpusShipTileset", + "SolNode57": "CorpusOutpostTileset", + "SolNode58": "GrineerSettlementTileset", + "SolNode59": "GrineerForestTileset", + "SolNode6": "CorpusOutpostTileset", + "SolNode61": "CorpusShipTileset", + "SolNode62": "CorpusIcePlanetTilesetCaves", + "SolNode64": "GrineerOceanTileset", + "SolNode66": "CorpusOutpostTileset", + "SolNode67": "GrineerAsteroidTileset", + "SolNode68": "GrineerGalleonTileset", + "SolNode70": "GrineerGalleonTileset", + "SolNode706": "OrokinDerelictTileset", + "SolNode707": "OrokinDerelictTileset", + "SolNode708": "OrokinDerelictTileset", + "SolNode709": "OrokinDerelictTileset", + "SolNode710": "OrokinDerelictTileset", + "SolNode711": "OrokinDerelictTileset", + "SolNode712": "OrokinDerelictTileset", + "SolNode713": "OrokinDerelictTileset", + "SolNode72": "CorpusOutpostTileset", + "SolNode73": "CorpusGasCityTileset", + "SolNode74": "CorpusGasCityTileset", + "SolNode741": "GrineerFortressTileset", + "SolNode742": "GrineerFortressTileset", + "SolNode743": "GrineerFortressTileset", + "SolNode744": "GrineerFortressTileset", + "SolNode745": "GrineerFortressTileset", + "SolNode746": "GrineerFortressTileset", + "SolNode747": "GrineerFortressTileset", + "SolNode748": "GrineerFortressTileset", + "SolNode75": "GrineerForestTileset", + "SolNode76": "CorpusShipTileset", + "SolNode78": "CorpusShipTileset", + "SolNode79": "GrineerForestTileset", + "SolNode81": "CorpusShipTileset", + "SolNode82": "GrineerGalleonTileset", + "SolNode84": "CorpusIcePlanetTilesetCaves", + "SolNode88": "CorpusShipTileset", + "SolNode93": "GrineerAsteroidTileset", + "SolNode96": "GrineerGalleonTileset", + "SolNode97": "CorpusGasCityTileset", + "SolNode99": "GrineerSettlementTileset" +} diff --git a/static/fixed_responses/worldState/syndicateMissions.json b/static/fixed_responses/worldState/syndicateMissions.json new file mode 100644 index 00000000..4c5b9233 --- /dev/null +++ b/static/fixed_responses/worldState/syndicateMissions.json @@ -0,0 +1,157 @@ +[ + "SettlementNode1", + "SettlementNode11", + "SettlementNode12", + "SettlementNode14", + "SettlementNode15", + "SettlementNode2", + "SettlementNode3", + "SolNode1", + "SolNode10", + "SolNode100", + "SolNode101", + "SolNode102", + "SolNode103", + "SolNode106", + "SolNode107", + "SolNode109", + "SolNode11", + "SolNode113", + "SolNode118", + "SolNode119", + "SolNode12", + "SolNode121", + "SolNode122", + "SolNode123", + "SolNode125", + "SolNode126", + "SolNode128", + "SolNode130", + "SolNode131", + "SolNode132", + "SolNode135", + "SolNode137", + "SolNode138", + "SolNode139", + "SolNode14", + "SolNode140", + "SolNode141", + "SolNode146", + "SolNode147", + "SolNode149", + "SolNode15", + "SolNode153", + "SolNode16", + "SolNode162", + "SolNode164", + "SolNode166", + "SolNode167", + "SolNode17", + "SolNode171", + "SolNode172", + "SolNode173", + "SolNode175", + "SolNode177", + "SolNode18", + "SolNode181", + "SolNode184", + "SolNode185", + "SolNode187", + "SolNode188", + "SolNode189", + "SolNode19", + "SolNode191", + "SolNode195", + "SolNode196", + "SolNode2", + "SolNode20", + "SolNode203", + "SolNode204", + "SolNode205", + "SolNode209", + "SolNode21", + "SolNode211", + "SolNode212", + "SolNode214", + "SolNode215", + "SolNode216", + "SolNode217", + "SolNode22", + "SolNode220", + "SolNode223", + "SolNode224", + "SolNode225", + "SolNode226", + "SolNode23", + "SolNode25", + "SolNode26", + "SolNode27", + "SolNode30", + "SolNode31", + "SolNode36", + "SolNode38", + "SolNode39", + "SolNode4", + "SolNode400", + "SolNode401", + "SolNode402", + "SolNode403", + "SolNode404", + "SolNode405", + "SolNode406", + "SolNode407", + "SolNode408", + "SolNode409", + "SolNode41", + "SolNode410", + "SolNode412", + "SolNode42", + "SolNode43", + "SolNode45", + "SolNode46", + "SolNode48", + "SolNode49", + "SolNode50", + "SolNode56", + "SolNode57", + "SolNode58", + "SolNode59", + "SolNode6", + "SolNode61", + "SolNode62", + "SolNode63", + "SolNode64", + "SolNode66", + "SolNode67", + "SolNode68", + "SolNode70", + "SolNode706", + "SolNode707", + "SolNode708", + "SolNode709", + "SolNode710", + "SolNode711", + "SolNode72", + "SolNode73", + "SolNode74", + "SolNode741", + "SolNode742", + "SolNode743", + "SolNode744", + "SolNode745", + "SolNode746", + "SolNode748", + "SolNode75", + "SolNode76", + "SolNode78", + "SolNode79", + "SolNode81", + "SolNode82", + "SolNode84", + "SolNode85", + "SolNode88", + "SolNode89", + "SolNode93", + "SolNode96", + "SolNode97" +] diff --git a/static/fixed_responses/worldState/worldState.json b/static/fixed_responses/worldState/worldState.json index eef85698..95b5fde2 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,55 +22,142 @@ "Icon": "/Lotus/Interface/Icons/DiscordIconNoBacker.png" } ], - "Sorties": [ - { - "_id": { "$oid": "663a4c7d4d932c97c0a3acd7" }, - "Activation": { "$date": { "$numberLong": "1715097600000" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Reward": "/Lotus/Types/Game/MissionDecks/SortieRewards", - "Seed": 24491, - "Boss": "SORTIE_BOSS_TYL", - "ExtraDrops": [], - "Variants": [ - { "missionType": "MT_TERRITORY", "modifierType": "SORTIE_MODIFIER_ARMOR", "node": "SolNode122", "tileset": "GrineerOceanTileset" }, - { "missionType": "MT_MOBILE_DEFENSE", "modifierType": "SORTIE_MODIFIER_LOW_ENERGY", "node": "SolNode184", "tileset": "GrineerGalleonTileset" }, - { "missionType": "MT_LANDSCAPE", "modifierType": "SORTIE_MODIFIER_EXIMUS", "node": "SolNode228", "tileset": "EidolonTileset" } - ], - "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" } + "InGameMarket": { + "LandingPage": { + "Categories": [ + { + "CategoryName": "NEW_PLAYER", + "Name": "/Lotus/Language/Store/NewPlayerCategoryTitle", + "Icon": "newplayer", + "AddToMenu": true, + "Items": [ + "/Lotus/Types/StoreItems/Packages/2024Bundles/WeaponStarterPack", + "/Lotus/StoreItems/Powersuits/MonkeyKing/MonkeyKing", + "/Lotus/StoreItems/Weapons/Tenno/Melee/SwordsAndBoards/MeleeContestWinnerOne/TennoSwordShield", + "/Lotus/StoreItems/Upgrades/Skins/Effects/WerewolfEphemera", + "/Lotus/StoreItems/Types/StoreItems/SlotItems/TwoWeaponSlotItem", + "/Lotus/StoreItems/Powersuits/Wisp/Wisp", + "/Lotus/StoreItems/Weapons/Tenno/Shotgun/Shotgun", + "/Lotus/StoreItems/Weapons/Corpus/Pistols/CrpAirPistol/CrpAirPistolArray", + "/Lotus/StoreItems/Upgrades/Skins/Scarves/FlameScarf", + "/Lotus/Types/StoreItems/Boosters/AffinityBooster3DayStoreItem" + ] + }, + { + "CategoryName": "POPULAR", + "Name": "/Lotus/Language/Menu/StorePopular", + "Icon": "popular", + "AddToMenu": true, + "Items": [ + "/Lotus/Types/StoreItems/Packages/2025Bundles/TC2025DigitalPack", + "/Lotus/Types/StoreItems/Packages/2025Bundles/EncoreCompSupPack", + "/Lotus/Types/StoreItems/Packages/2025Bundles/EncoreGeminiSupPack", + "/Lotus/Types/StoreItems/Packages/WarframeBundles/TempleItemsBundle", + "/Lotus/Types/StoreItems/Packages/FormaPack", + "/Lotus/StoreItems/Upgrades/Skins/Saryn/WF1999SarynSkin", + "/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/DaxDuviriKatana/DaxDuviriKatanaWeapon", + "/Lotus/StoreItems/Upgrades/Skins/Jade/WF1999NyxSkin", + "/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/NinjaColourPickerItem", + "/Lotus/StoreItems/Upgrades/Skins/Mag/WF1999MagSkin", + "/Lotus/StoreItems/Upgrades/Skins/Frost/WF1999FrostSkin", + "/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/DaxDuviriTwoHandedKatana/DaxDuviriTwoHandedKatanaWeapon", + "/Lotus/StoreItems/Upgrades/Skins/Harlequin/MirageDeluxeSkin", + "/Lotus/StoreItems/Weapons/Tenno/Melee/Hammer/DaxDuviriHammer/DaxDuviriHammerWeapon" + ] + }, + { + "CategoryName": "HEIRLOOM", + "Name": "/Lotus/Language/Store/HeirloomCategoryTitle", + "Icon": "heirloom", + "AddToMenu": true, + "Items": [ + "/Lotus/Types/StoreItems/Packages/2025Bundles/RhinoHeirloomPack", + "/Lotus/Types/StoreItems/Packages/HeirloomPackRhino", + "/Lotus/StoreItems/Upgrades/Skins/Rhino/RhinoHeirloomSkin", + "/Lotus/StoreItems/Upgrades/Skins/Crowns/HeirloomRhinoCrown", + "/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/ColourPickerRhinoHeirloom", + "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardRhinoHeirloom", + "/Lotus/StoreItems/Types/StoreItems/AvatarImages/HeirloomRhinoGlyph", + "/Lotus/StoreItems/Upgrades/Skins/Sigils/HeirloomRhinoSigil", + "/Lotus/Types/StoreItems/Packages/HeirloomPackEmber", + "/Lotus/StoreItems/Upgrades/Skins/Ember/EmberHeirloomSkin", + "/Lotus/StoreItems/Upgrades/Skins/Crowns/HeirloomEmberCrown", + "/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/ColourPickerEmberHeirloom", + "/Lotus/StoreItems/Types/Items/ShipDecos/TarotCardEmberHeirloom", + "/Lotus/StoreItems/Types/StoreItems/AvatarImages/HeirloomEmberGlyph", + "/Lotus/StoreItems/Upgrades/Skins/Sigils/HeirloomEmberSigil" + ] + }, + { + "CategoryName": "TENNOGEN", + "Name": "/Lotus/Language/Menu/Store_Tennogen", + "Icon": "tennogen", + "AddToMenu": true, + "Items": [ + "/Lotus/StoreItems/Upgrades/Skins/Armor/SWEndocitosShoulderArmor/SWEndocitosShoulderArmorA", + "/Lotus/StoreItems/Upgrades/Skins/Scarves/SWLunariusSyandana", + "/Lotus/StoreItems/Upgrades/Skins/Scarves/SWRauSyandana", + "/Lotus/StoreItems/Upgrades/Skins/Hoplite/SWStyanaxHuzarrSkin", + "/Lotus/StoreItems/Upgrades/Skins/Werewolf/VorunaDemionnaSkin" + ] + }, + { + "CategoryName": "SALE", + "Name": "/Lotus/Language/Menu/Store_Sale", + "Icon": "sale", + "AddToMenu": true, + "Items": [] + }, + { + "CategoryName": "WISH_LIST", + "Name": "/Lotus/Language/Menu/Store_Wishlist", + "Icon": "wishlist", + "Items": [] + } ] } + }, + "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" + } + } + } ], "SyndicateMissions": [ - { - "_id": { "$oid": "663a4fc5ba6f84724fa48049" }, - "Activation": { "$date": { "$numberLong": "1715097541439" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "ArbitersSyndicate", - "Seed": 24491, - "Nodes": ["SolNode223", "SolNode89", "SolNode146", "SolNode212", "SolNode167", "SolNode48", "SolNode78"] - }, - { - "_id": { "$oid": "663a4fc5ba6f84724fa4804a" }, - "Activation": { "$date": { "$numberLong": "1715097541439" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "CephalonSudaSyndicate", - "Seed": 12770, - "Nodes": ["SolNode36", "SolNode59", "SettlementNode12", "SolNode61", "SolNode12", "SolNode138", "SolNode72"] - }, { "_id": { "$oid": "663a4fc5ba6f84724fa4804c" }, "Activation": { "$date": { "$numberLong": "1715097541439" } }, @@ -94,14 +182,6 @@ "Seed": 50102, "Nodes": [] }, - { - "_id": { "$oid": "663a4fc5ba6f84724fa4804e" }, - "Activation": { "$date": { "$numberLong": "1715097541439" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "NewLokaSyndicate", - "Seed": 16064, - "Nodes": ["SolNode101", "SolNode224", "SolNode205", "SettlementNode2", "SolNode171", "SolNode188", "SolNode75"] - }, { "_id": { "$oid": "663a4fc5ba6f84724fa4804f" }, "Activation": { "$date": { "$numberLong": "1715097541439" } }, @@ -110,14 +190,6 @@ "Seed": 77721, "Nodes": [] }, - { - "_id": { "$oid": "663a4fc5ba6f84724fa48050" }, - "Activation": { "$date": { "$numberLong": "1715097541439" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "PerrinSyndicate", - "Seed": 9940, - "Nodes": ["SolNode39", "SolNode14", "SolNode203", "SolNode100", "SolNode130", "SolNode64", "SettlementNode15"] - }, { "_id": { "$oid": "663a4fc5ba6f84724fa48052" }, "Activation": { "$date": { "$numberLong": "1715097541439" } }, @@ -246,14 +318,6 @@ "Seed": 67257, "Nodes": [] }, - { - "_id": { "$oid": "663a4fc5ba6f84724fa4805e" }, - "Activation": { "$date": { "$numberLong": "1715097541439" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "RedVeilSyndicate", - "Seed": 46649, - "Nodes": ["SolNode226", "SolNode79", "SolNode216", "SettlementNode11", "SolNode56", "SolNode41", "SolNode23"] - }, { "_id": { "$oid": "663a4fc5ba6f84724fa48060" }, "Activation": { "$date": { "$numberLong": "1715097541439" } }, @@ -261,256 +325,6 @@ "Tag": "VoxSyndicate", "Seed": 77972, "Nodes": [] - }, - { - "_id": { "$oid": "663a4fc5ba6f84724fa48061" }, - "Activation": { "$date": { "$numberLong": "1715097541439" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "SteelMeridianSyndicate", - "Seed": 42366, - "Nodes": ["SolNode27", "SolNode107", "SolNode214", "SettlementNode1", "SolNode177", "SolNode141", "SolNode408"] - }, - { - "_id": { "$oid": "663a71c80000000000000002" }, - "Activation": { "$date": { "$numberLong": "1715106248403" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "EntratiSyndicate", - "Seed": 99561, - "Nodes": [], - "Jobs": [ - { - "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosGrnSurvivorBounty", - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierATableBRewards", - "masteryReq": 0, - "minEnemyLevel": 5, - "maxEnemyLevel": 15, - "xpAmounts": [5, 5, 5] - }, - { - "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAreaDefenseBounty", - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierCTableBRewards", - "masteryReq": 1, - "minEnemyLevel": 15, - "maxEnemyLevel": 25, - "xpAmounts": [12, 12, 12] - }, - { - "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessExcavateBounty", - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTableARewards", - "masteryReq": 5, - "minEnemyLevel": 25, - "maxEnemyLevel": 30, - "endless": true, - "xpAmounts": [14, 14, 14] - }, - { - "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAssassinateBounty", - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierDTableBRewards", - "masteryReq": 2, - "minEnemyLevel": 30, - "maxEnemyLevel": 40, - "xpAmounts": [17, 17, 17, 25] - }, - { - "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosKeyPiecesBounty", - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards", - "masteryReq": 3, - "minEnemyLevel": 40, - "maxEnemyLevel": 60, - "xpAmounts": [22, 22, 22, 22, 43] - }, - { - "jobType": "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosExcavateBounty", - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards", - "masteryReq": 10, - "minEnemyLevel": 100, - "maxEnemyLevel": 100, - "xpAmounts": [25, 25, 25, 25, 50] - }, - { - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierATableCRewards", - "masteryReq": 5, - "minEnemyLevel": 30, - "maxEnemyLevel": 40, - "xpAmounts": [2, 2, 2, 4], - "locationTag": "ChamberB", - "isVault": true - }, - { - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierBTableCRewards", - "masteryReq": 5, - "minEnemyLevel": 40, - "maxEnemyLevel": 50, - "xpAmounts": [4, 4, 4, 5], - "locationTag": "ChamberA", - "isVault": true - }, - { - "rewards": "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierCTableCRewards", - "masteryReq": 5, - "minEnemyLevel": 50, - "maxEnemyLevel": 60, - "xpAmounts": [5, 5, 5, 7], - "locationTag": "ChamberC", - "isVault": true - } - ] - }, - { - "_id": { "$oid": "663a71c80000000000000004" }, - "Activation": { "$date": { "$numberLong": "1715106248403" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "EntratiLabSyndicate", - "Seed": 99562, - "Nodes": [] - }, - { - "_id": { "$oid": "663a71c80000000000000008" }, - "Activation": { "$date": { "$numberLong": "1715106248403" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "CetusSyndicate", - "Seed": 99561, - "Nodes": [], - "Jobs": [ - { - "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyCap", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierATableBRewards", - "masteryReq": 0, - "minEnemyLevel": 5, - "maxEnemyLevel": 15, - "xpAmounts": [430, 430, 430] - }, - { - "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyLib", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierBTableBRewards", - "masteryReq": 1, - "minEnemyLevel": 10, - "maxEnemyLevel": 30, - "xpAmounts": [620, 620, 620] - }, - { - "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/RescueBountyResc", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierCTableBRewards", - "masteryReq": 2, - "minEnemyLevel": 20, - "maxEnemyLevel": 40, - "xpAmounts": [670, 670, 670, 990] - }, - { - "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/CaptureBountyCapTwo", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierDTableBRewards", - "masteryReq": 3, - "minEnemyLevel": 30, - "maxEnemyLevel": 50, - "xpAmounts": [570, 570, 570, 570, 1110] - }, - { - "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/ReclamationBountyCache", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETableBRewards", - "masteryReq": 5, - "minEnemyLevel": 40, - "maxEnemyLevel": 60, - "xpAmounts": [740, 740, 740, 740, 1450] - }, - { - "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountyCap", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETableBRewards", - "masteryReq": 10, - "minEnemyLevel": 100, - "maxEnemyLevel": 100, - "xpAmounts": [840, 840, 840, 840, 1660] - }, - { - "jobType": "/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AssassinateBountyAss", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTableBRewards", - "masteryReq": 0, - "minEnemyLevel": 50, - "maxEnemyLevel": 70, - "xpAmounts": [840, 840, 840, 840, 1650] - } - ] - }, - { - "_id": { "$oid": "663a71c80000000000000025" }, - "Activation": { "$date": { "$numberLong": "1715106248403" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "SolarisSyndicate", - "Seed": 99561, - "Nodes": [], - "Jobs": [ - { - "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobSpy", - "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierATableBRewards", - "masteryReq": 0, - "minEnemyLevel": 5, - "maxEnemyLevel": 15, - "xpAmounts": [340, 340, 340] - }, - { - "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusCullJobResource", - "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierBTableBRewards", - "masteryReq": 1, - "minEnemyLevel": 10, - "maxEnemyLevel": 30, - "xpAmounts": [660, 660, 660] - }, - { - "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusIntelJobRecovery", - "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierCTableBRewards", - "masteryReq": 2, - "minEnemyLevel": 20, - "maxEnemyLevel": 40, - "xpAmounts": [610, 610, 610, 900] - }, - { - "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusHelpingJobCaches", - "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierDTableBRewards", - "masteryReq": 3, - "minEnemyLevel": 30, - "maxEnemyLevel": 50, - "xpAmounts": [600, 600, 600, 600, 1170] - }, - { - "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobAmbush", - "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETableBRewards", - "masteryReq": 5, - "minEnemyLevel": 40, - "maxEnemyLevel": 60, - "xpAmounts": [690, 690, 690, 690, 1350] - }, - { - "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/VenusChaosJobExcavation", - "rewards": "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETableBRewards", - "masteryReq": 10, - "minEnemyLevel": 100, - "maxEnemyLevel": 100, - "xpAmounts": [840, 840, 840, 840, 1660] - }, - { - "jobType": "/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobExterminate", - "rewards": "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTableBRewards", - "masteryReq": 0, - "minEnemyLevel": 50, - "maxEnemyLevel": 70, - "xpAmounts": [780, 780, 780, 780, 1540] - } - ] - }, - { - "_id": { "$oid": "663a71c80000000000000029" }, - "Activation": { "$date": { "$numberLong": "1715106248403" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "ZarimanSyndicate", - "Seed": 99562, - "Nodes": [] - }, - { - "_id": { "$oid": "676b8d340000000000000006" }, - "Activation": { "$date": { "$numberLong": "1735101748215" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Tag": "HexSyndicate", - "Seed": 33872, - "Nodes": [] } ], "ActiveMissions": [ @@ -730,31 +544,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 + } ] } ], @@ -1119,163 +2763,5 @@ "ConstructionProjects": [], "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" } }, - "Expiry": { "$date": { "$numberLong": "2000000000000" } }, - "Days": [ - { "day": 6, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillEximusEasy" }] }, - { - "day": 15, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/MagazineCapacity" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/Armor" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/EnergyRestoration" } - ] - }, - { "day": 21, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesEasy" }] }, - { - "day": 25, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarMajorArtifactPack" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalGreen" } - ] - }, - { - "day": 31, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Recipes/Components/WeaponUtilityUnlockerBlueprint" }, - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Packages/Calendar/CalendarKuvaBundleSmall" } - ] - }, - { "day": 43, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesWithAbilitiesMedium" }] }, - { "day": 45, "events": [{ "type": "CET_PLOT", "dialogueName": "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue", "dialogueConvo": "LettieBirthdayConvo" }] }, - { - "day": 47, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Boosters/AffinityBooster3DayStoreItem" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarMajorArtifactPack" } - ] - }, - { "day": 48, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillScaldraEnemiesWithMeleeMedium" }] }, - { - "day": 54, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/CompanionsBuffNearbyPlayer" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/OrbsDuplicateOnPickup" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/FinisherChancePerComboMultiplier" } - ] - }, - { - "day": 56, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/BoosterPacks/CalendarArtifactPack" }, - { "type": "CET_REWARD", "reward": "/Lotus/Types/StoreItems/Packages/Calendar/CalendarKuvaBundleSmall" } - ] - }, - { "day": 71, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarKillTechrotEnemiesHard" }] }, - { - "day": 77, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponSecondaryArcaneUnlocker" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/CircuitSilverSteelPathFusionBundle" } - ] - }, - { "day": 80, "events": [{ "type": "CET_CHALLENGE", "challenge": "/Lotus/Types/Challenges/Calendar1999/CalendarDestroyPropsMedium" }] }, - { - "day": 83, - "events": [ - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Recipes/Components/OrokinReactorBlueprint" }, - { "type": "CET_REWARD", "reward": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponUtilityUnlocker" } - ] - }, - { - "day": 87, - "events": [ - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/EnergyOrbToAbilityRange" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/MeleeAttackSpeed" }, - { "type": "CET_UPGRADE", "upgrade": "/Lotus/Upgrades/Calendar/CompanionDamage" } - ] - } - ], - "Season": "CST_WINTER", - "YearIteration": 0, - "Version": 17, - "UpgradeAvaliabilityRequirements": ["/Lotus/Upgrades/Calendar/1999UpgradeApplicationRequirement"] - } - ] + "ForceLogoutVersion": 0 } diff --git a/static/webui/index.html b/static/webui/index.html index 6af03e3c..1b42793e 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -7,9 +7,9 @@ -