commit 0b6f9bb0266d2ba600451cfb4e9d088df2b23593 Author: 1921703654 <1921703654@qq.com> Date: Wed Sep 3 02:27:44 2025 +0800 Initial commit 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/.dockerignore b/.dockerignore new file mode 100644 index 00000000..a9a423e6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +**/.dockerignore +**/.git +Dockerfile* +.* +docker-data/ +node_modules/ +static/data/ +logs/ diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..d21f4aa7 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,47 @@ +{ + "plugins": ["@typescript-eslint", "prettier", "import"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "plugin:import/recommended", + "plugin:import/typescript" + ], + "env": { + "browser": true, + "es6": true, + "node": true + }, + "rules": { + "@typescript-eslint/consistent-type-imports": "error", + "@typescript-eslint/explicit-function-return-type": "error", + "@typescript-eslint/restrict-template-expressions": "error", + "@typescript-eslint/restrict-plus-operands": "error", + "@typescript-eslint/no-unsafe-member-access": "error", + "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "caughtErrors": "none" }], + "@typescript-eslint/no-unsafe-argument": "error", + "@typescript-eslint/no-unsafe-call": "error", + "@typescript-eslint/no-unsafe-assignment": "error", + "@typescript-eslint/no-explicit-any": "off", + "no-loss-of-precision": "error", + "@typescript-eslint/no-unnecessary-condition": "error", + "@typescript-eslint/no-base-to-string": "off", + "no-case-declarations": "error", + "prettier/prettier": "error", + "no-mixed-spaces-and-tabs": "error", + "@typescript-eslint/require-await": "error", + "import/no-named-as-default-member": "off", + "import/no-cycle": "warn" + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "./tsconfig.json" + }, + "settings": { + "import/extensions": [ ".ts" ], + "import/resolver": { + "typescript": true, + "node": true + } + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..b3bfce24 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Auto detect text files and perform LF normalization +* text=auto eol=lf + +static/webui/libs/ linguist-vendored diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..55e09ee1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..10fe1c52 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,29 @@ +name: Build +on: + push: + branches: ["main"] + pull_request: {} +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4.1.2 + - name: Setup Node.js environment + uses: actions/setup-node@v4.0.2 + with: + node-version: ">=20.18.1" + - run: npm ci + - run: cp config-vanilla.json config.json + - 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 new file mode 100644 index 00000000..4a97729c --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,44 @@ +name: Build Docker image +on: + push: + branches: + - main +jobs: + docker-amd64: + if: github.repository == 'OpenWF/SpaceNinjaServer' + runs-on: amd64 + steps: + - name: Set up Docker buildx + uses: docker/setup-buildx-action@v3 + - name: Log in to container registry + uses: docker/login-action@v3 + with: + username: openwf + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64 + push: true + tags: | + openwf/spaceninjaserver:latest + openwf/spaceninjaserver:${{ github.sha }} + docker-arm64: + if: github.repository == 'OpenWF/SpaceNinjaServer' + runs-on: arm64 + steps: + - name: Set up Docker buildx + uses: docker/setup-buildx-action@v3 + - name: Log in to container registry + uses: docker/login-action@v3 + with: + username: openwf + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v6 + with: + platforms: linux/arm64 + push: true + tags: | + openwf/spaceninjaserver:latest-arm64 + openwf/spaceninjaserver:${{ github.sha }}-arm64 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..2661a52a --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +/node_modules +/build + +/.env +/config.json +/static/data/** +!/static/data/.gitkeep +yarn.lock +/tmp + +# JetBrains/webstorm configs +.idea/ + +# logs +/logs + +# MongoDB VSCode extension playground scripts +/database_scripts + +# Default Docker directory +/docker-data diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..4fd02195 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..9de22568 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +lts/iron diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..e1ab4819 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +src/routes/api.ts +static/webui/libs/ +*.html +*.md +config-vanilla.json diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..47912fcd --- /dev/null +++ b/.prettierrc @@ -0,0 +1,27 @@ +{ + "tabWidth": 4, + "useTabs": false, + "endOfLine": "auto", + "trailingComma": "none", + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "jsxSingleQuote": false, + "printWidth": 120, + "proseWrap": "never", + "quoteProps": "as-needed", + "requirePragma": false, + "semi": true, + "singleQuote": false, + "vueIndentScriptAndStyle": true, + "arrowParens": "avoid", + "bracketSpacing": true, + "overrides": [ + { + "files": "*.json", + "options": { + "tabWidth": 2, + "printWidth": 200 + } + } + ] +} 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/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..fbeaa0be --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +// Use IntelliSense to learn about possible attributes. +// Hover to view descriptions of existing attributes. +// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug and Watch", + "args": ["${workspaceFolder}/scripts/dev.js"], + "console": "integratedTerminal" + } + ] +} + +//can use "console": "internalConsole" for VS Code's Debug Console. For that, forceConsole in logger.ts is needed to be true +//"internalConsoleOptions": "openOnSessionStart" can be useful then diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..e3cf3d5a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.preferences.preferTypeOnlyAutoImports": true +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..7a1b6292 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,17 @@ +## In General + +### Prerequisites + +Use `npm i` or `npm ci` to install all dependencies. + +### Testing + +Use `npm run verify` to verify that your changes pass TypeScript's checks. + +### Formatting + +Use `npm run prettier` to ensure your formatting matches the expected format. Failing to do so will cause CI failure. + +## WebUI Specific + +The translation system is designed around additions being made to `static/webui/translations/en.js`. They are copied over for translation via `npm run update-translations`. DO NOT produce non-English strings; we want them to be translated by humans who can understand the full context. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..d4d7b1a0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM node:24-alpine3.21 + +RUN apk add --no-cache bash jq + +COPY . /app +WORKDIR /app + +RUN npm i --omit=dev --omit=optional +RUN date '+%d %B %Y' > BUILD_DATE + +ENTRYPOINT ["/app/docker-entrypoint.sh"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..e62ec04c --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ +GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 00000000..cd136b9f --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# Space Ninja Server + +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-vanilla.json](config-vanilla.json), 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.eidolonOverride` can be set to `day` or `night` to lock the time to day/fass and night/vome on Plains of Eidolon/Cambion Drift. +- `worldState.vallisOverride` can be set to `warm` or `cold` to lock the temperature on Orb Vallis. +- `worldState.duviriOverride` can be set to `joy`, `anger`, `envy`, `sorrow`, or `fear` to lock the Duviri spiral. +- `worldState.nightwaveOverride` will lock the nightwave season, assuming the client is new enough for it. Valid values: + - `RadioLegionIntermission13Syndicate` for Nora's Mix Vol. 9 + - `RadioLegionIntermission12Syndicate` for Nora's Mix Vol. 8 + - `RadioLegionIntermission11Syndicate` for Nora's Mix Vol. 7 + - `RadioLegionIntermission10Syndicate` for Nora's Mix Vol. 6 + - `RadioLegionIntermission9Syndicate` for Nora's Mix Vol. 5 + - `RadioLegionIntermission8Syndicate` for Nora's Mix Vol. 4 + - `RadioLegionIntermission7Syndicate` for Nora's Mix Vol. 3 + - `RadioLegionIntermission6Syndicate` for Nora's Mix Vol. 2 + - `RadioLegionIntermission5Syndicate` for Nora's Mix Vol. 1 + - `RadioLegionIntermission4Syndicate` for Nora's Choice + - `RadioLegionIntermission3Syndicate` for Intermission III + - `RadioLegion3Syndicate` for Glassmaker + - `RadioLegionIntermission2Syndicate` for Intermission II + - `RadioLegion2Syndicate` for The Emissary + - `RadioLegionIntermissionSyndicate` for Intermission I + - `RadioLegionSyndicate` for The Wolf of Saturn Six +- `allTheFissures` can be set to `normal` or `hard` to enable all fissures either in normal or steel path, respectively. +- `worldState.circuitGameModes` can be set to an array of game modes which will override the otherwise-random pattern in The Circuit. Valid element values are `Survival`, `VoidFlood`, `Excavation`, `Defense`, `Exterminate`, `Assassination`, and `Alchemy`. diff --git a/UPDATE AND START SERVER.bat b/UPDATE AND START SERVER.bat new file mode 100644 index 00000000..ac8b2515 --- /dev/null +++ b/UPDATE AND START SERVER.bat @@ -0,0 +1,28 @@ +@echo off + +echo Updating SpaceNinjaServer... +git fetch --prune +if %errorlevel% == 0 ( + git stash + git checkout -f origin/main + + if exist static\data\0\ ( + echo Updating stripped assets... + cd static\data\0\ + git pull + cd ..\..\..\ + ) + + echo Updating dependencies... + call npm i --omit=dev + + call npm run build + if %errorlevel% == 0 ( + call npm run start + echo SpaceNinjaServer seems to have crashed. + ) +) + +:a +pause > nul +goto a diff --git a/UPDATE AND START SERVER.sh b/UPDATE AND START SERVER.sh new file mode 100644 index 00000000..fd77f973 --- /dev/null +++ b/UPDATE AND START SERVER.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +echo "Updating SpaceNinjaServer..." +git fetch --prune +if [ $? -eq 0 ]; then + git stash + git checkout -f origin/main + + if [ -d "static/data/0/" ]; then + echo "Updating stripped assets..." + cd static/data/0/ + git pull + cd ../../../ + fi + + echo "Updating dependencies..." + npm i --omit=dev + + npm run build + if [ $? -eq 0 ]; then + npm run start + echo "SpaceNinjaServer seems to have crashed." + fi +fi diff --git a/config-vanilla.json b/config-vanilla.json new file mode 100644 index 00000000..37efdda5 --- /dev/null +++ b/config-vanilla.json @@ -0,0 +1,77 @@ +{ + "mongodbUrl": "mongodb://127.0.0.1:27017/openWF", + "logger": { + "files": true, + "level": "trace" + }, + "myAddress": "localhost", + "httpPort": 80, + "httpsPort": 443, + "administratorNames": [], + "autoCreateAccount": true, + "skipTutorial": false, + "unlockAllScans": false, + "unlockAllShipFeatures": false, + "unlockAllShipDecorations": false, + "unlockAllFlavourItems": false, + "unlockAllSkins": false, + "unlockAllCapturaScenes": false, + "fullyStockedVendors": false, + "skipClanKeyCrafting": false, + "noDojoRoomBuildStage": false, + "noDojoDecoBuildStage": false, + "fastDojoRoomDestruction": false, + "noDojoResearchCosts": false, + "noDojoResearchTime": false, + "fastClanAscension": false, + "spoofMasteryRank": -1, + "relicRewardItemCountMultiplier": 1, + "nightwaveStandingMultiplier": 1, + "unfaithfulBugFixes": { + "ignore1999LastRegionPlayed": false, + "fixXtraCheeseTimer": false, + "useAnniversaryTagForOldGoals": true + }, + "worldState": { + "creditBoost": false, + "affinityBoost": false, + "resourceBoost": false, + "tennoLiveRelay": false, + "baroTennoConRelay": false, + "baroAlwaysAvailable": false, + "baroFullyStocked": false, + "varziaFullyStocked": false, + "wolfHunt": false, + "orphixVenom": false, + "longShadow": false, + "hallowedFlame": false, + "anniversary": null, + "hallowedNightmares": false, + "hallowedNightmaresRewardsOverride": 0, + "proxyRebellion": false, + "proxyRebellionRewardsOverride": 0, + "galleonOfGhouls": 0, + "ghoulEmergenceOverride": null, + "plagueStarOverride": null, + "starDaysOverride": null, + "dogDaysOverride": null, + "dogDaysRewardsOverride": null, + "bellyOfTheBeast": false, + "bellyOfTheBeastProgressOverride": 0, + "eightClaw": false, + "eightClawProgressOverride": 0, + "thermiaFracturesOverride": null, + "thermiaFracturesProgressOverride": 0, + "eidolonOverride": "", + "vallisOverride": "", + "duviriOverride": "", + "nightwaveOverride": "", + "allTheFissures": "", + "varziaOverride": "", + "circuitGameModes": null, + "darvoStockMultiplier": 1 + }, + "dev": { + "keepVendorsExpired": false + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..d9f89348 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +services: + spaceninjaserver: + # The image to use. If you have an ARM CPU, replace 'latest' with 'latest-arm64'. + image: openwf/spaceninjaserver:latest + + volumes: + - ./docker-data/conf:/app/conf + - ./docker-data/static-data:/app/static/data + - ./docker-data/logs:/app/logs + ports: + - 80:80 + - 443:443 + + # Normally, the image is fetched from Docker Hub, but you can use the local Dockerfile by removing "image" above and adding this: + #build: . + # Works best when using `docker-compose up --force-recreate --build`. + + depends_on: + - mongodb + mongodb: + image: docker.io/library/mongo:8.0.0-noble + environment: + MONGO_INITDB_ROOT_USERNAME: openwfagent + MONGO_INITDB_ROOT_PASSWORD: spaceninjaserver + volumes: + - ./docker-data/database:/data/db + command: mongod --quiet --logpath /dev/null diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 00000000..e67508c2 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +if [ ! -f conf/config.json ]; then + jq --arg value "mongodb://openwfagent:spaceninjaserver@mongodb:27017/" '.mongodbUrl = $value' /app/config-vanilla.json > /app/conf/config.json +fi + +exec npm run raw -- --configPath conf/config.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..da5a0fcc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5776 @@ +{ + "name": "wf-emulator", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wf-emulator", + "version": "0.1.0", + "license": "GNU", + "dependencies": { + "chokidar": "^4.0.3", + "crc-32": "^1.2.2", + "express": "^5", + "json-with-bigint": "^3.4.4", + "mongoose": "^8.11.0", + "morgan": "^1.10.0", + "ncp": "^2.0.0", + "undici": "^7.10.0", + "warframe-public-export-plus": "^0.5.83", + "warframe-riven-info": "^0.1.2", + "winston": "^3.17.0", + "winston-daily-rotate-file": "^5.0.0", + "ws": "^8.18.2" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^8.28.0", + "@typescript-eslint/parser": "^8.28.0", + "eslint": "^8", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-prettier": "^5.2.5", + "prettier": "^3.5.3", + "tree-kill": "^1.2.2" + }, + "engines": { + "node": ">=20.18.1" + }, + "optionalDependencies": { + "@types/express": "^5", + "@types/morgan": "^1.9.9", + "@types/websocket": "^1.0.10", + "@types/ws": "^8.18.1", + "@typescript/native-preview": "^7.0.0-dev.20250625.1", + "typescript": "^5.7" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", + "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", + "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "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": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "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" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "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" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.0.tgz", + "integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", + "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", + "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "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==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/morgan": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT", + "optional": true + }, + "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==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/websocket": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.10.tgz", + "integrity": "sha512-svjGZvPB7EzuYS94cI7a+qhwgGU1y89wUgjT6E2wVUfmAGIvRfT7obBvRtnhXCSsoMdlG4gBFGE7MfkIXZLoww==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.41.0.tgz", + "integrity": "sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.41.0", + "@typescript-eslint/type-utils": "8.41.0", + "@typescript-eslint/utils": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.41.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.41.0.tgz", + "integrity": "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.41.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0", + "debug": "^4.3.4" + }, + "engines": { + "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 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.41.0.tgz", + "integrity": "sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.41.0", + "@typescript-eslint/types": "^8.41.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.41.0.tgz", + "integrity": "sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.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/tsconfig-utils": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.41.0.tgz", + "integrity": "sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.41.0.tgz", + "integrity": "sha512-63qt1h91vg3KsjVVonFJWjgSK7pZHSQFKH6uwqxAH9bBrsyRhO6ONoKyXxyVBzG1lJnFAJcKAcxLS54N1ee1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.0", + "@typescript-eslint/utils": "8.41.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "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 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.41.0.tgz", + "integrity": "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.41.0.tgz", + "integrity": "sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.41.0", + "@typescript-eslint/tsconfig-utils": "8.41.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.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.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.41.0.tgz", + "integrity": "sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.41.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.0" + }, + "engines": { + "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 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.41.0.tgz", + "integrity": "sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.41.0", + "eslint-visitor-keys": "^4.2.1" + }, + "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.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "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.20250826.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20250826.1.tgz", + "integrity": "sha512-+NuzOfk/lu6pLYSCio+R7uzJ9pfOasc1fshxVmLp6wgcB8yuUYYvBaT7CoHapUnNBYZXkJ9u0UOECnq3dbzgSQ==", + "license": "Apache-2.0", + "optional": true, + "bin": { + "tsgo": "bin/tsgo.js" + }, + "engines": { + "node": ">=20.6.0" + }, + "optionalDependencies": { + "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20250826.1", + "@typescript/native-preview-darwin-x64": "7.0.0-dev.20250826.1", + "@typescript/native-preview-linux-arm": "7.0.0-dev.20250826.1", + "@typescript/native-preview-linux-arm64": "7.0.0-dev.20250826.1", + "@typescript/native-preview-linux-x64": "7.0.0-dev.20250826.1", + "@typescript/native-preview-win32-arm64": "7.0.0-dev.20250826.1", + "@typescript/native-preview-win32-x64": "7.0.0-dev.20250826.1" + } + }, + "node_modules/@typescript/native-preview-darwin-arm64": { + "version": "7.0.0-dev.20250826.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20250826.1.tgz", + "integrity": "sha512-UTkmzj0+NpraW4GcgG5PECJZ25SASkiK7TP90lUx9/RZRidrClhHqyf/A3NPpI+1pqoKqGXxIt3+jV+BY15KyQ==", + "cpu": [ + "arm64" + ], + "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.20250826.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20250826.1.tgz", + "integrity": "sha512-9UyLnKkJMW2T118FmS8Jj1djWDXgXGz2d9gUA/d+AdTAOaTBc1TmWhiUVcyKgIzG3+OAFIY+pVLeBWxNpFQu4Q==", + "cpu": [ + "x64" + ], + "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.20250826.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20250826.1.tgz", + "integrity": "sha512-CRH103rrFme+V917lftxtcttiT69fDNZPcnNzHeuj1z6/G3eIY6Hmon4xQb/Q4CjgblcO/BIEz6zDVphlas3dA==", + "cpu": [ + "arm" + ], + "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.20250826.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20250826.1.tgz", + "integrity": "sha512-GFcWo//t482UhZW+K1Nqlp/eOO2NS1uAIR77UyPn1FK/NOQBaxg8w4AZk0OG5ZHzvut4iZwQa2Gp+8zbNnfAIA==", + "cpu": [ + "arm64" + ], + "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.20250826.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20250826.1.tgz", + "integrity": "sha512-ePQiUwa9YpUUc5g8lu77kCOgZyAseJn14B+5Uwz7n7CFPJn48L0mcKvjr3jVP8sER0r6rFmndEr2uyHnc+Qj2w==", + "cpu": [ + "x64" + ], + "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.20250826.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20250826.1.tgz", + "integrity": "sha512-kL3qH6CeOPdtAgZw1cuFgwpyup3VSkAM+ayZGk08SM42Zd90VMTDf736kXm/xxAcpvmCQ0YrH1FDIG0dPiRCAw==", + "cpu": [ + "arm64" + ], + "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.20250826.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20250826.1.tgz", + "integrity": "sha512-WPiCnQDz64ZtwUEZ/ezXLHjhZTOetdfZI16ZbjZGkeZ5O3DNMaHrwzvvqqIq7gPxICEOK+6pM9eQ92I6wDq0qQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.6.0" + } + }, + "node_modules/@ungap/structured-clone": { + "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" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "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" + } + }, + "node_modules/ansi-styles": { + "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" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "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==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/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/body-parser": { + "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": "^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": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "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", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "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.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "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" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/concat-map": { + "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": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "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" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.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": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-import-context": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz", + "integrity": "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-tsconfig": "^4.10.1", + "stable-hash-x": "^0.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-context" + }, + "peerDependencies": { + "unrs-resolver": "^1.0.0" + }, + "peerDependenciesMeta": { + "unrs-resolver": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.4.tgz", + "integrity": "sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==", + "dev": true, + "license": "ISC", + "dependencies": { + "debug": "^4.4.1", + "eslint-import-context": "^0.1.8", + "get-tsconfig": "^4.10.1", + "is-bun-module": "^2.0.0", + "stable-hash-x": "^0.2.0", + "tinyglobby": "^0.2.14", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^16.17.0 || >=18.6.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "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" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "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" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "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.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "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", + "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.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "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": { + "reusify": "^1.0.4" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-stream-rotator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", + "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "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/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "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" + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs.realpath": { + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "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.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "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", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "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" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "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": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "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", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "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": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "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.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.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/mongodb": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.18.0.tgz", + "integrity": "sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.9", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.18.0.tgz", + "integrity": "sha512-3TixPihQKBdyaYDeJqRjzgb86KbilEH07JmzV8SoSjgoskNTpa6oTBmDxeoF9p8YnWQoz7shnCyPkSV/48y3yw==", + "license": "MIT", + "dependencies": { + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.18.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/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/morgan/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/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==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/napi-postinstall": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", + "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "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", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "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" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "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": ">= 18" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "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", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "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.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/stable-hash-x": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz", + "integrity": "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "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" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-api-utils": { + "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": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "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", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.15.0.tgz", + "integrity": "sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "license": "MIT", + "optional": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/warframe-public-export-plus": { + "version": "0.5.84", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.84.tgz", + "integrity": "sha512-ZpI1Y5CgWDmCwM4/oQpv9u0GD6KFvsJ9f1vJVXYhm5VD9DdOJcFzXgXgg98HXJ5JHbO16ZGIj83117qdpd0RQA==" + }, + "node_modules/warframe-riven-info": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/warframe-riven-info/-/warframe-riven-info-0.1.2.tgz", + "integrity": "sha512-j09BbfWGyCEKv19jP8c0/Eb0gupJsyLNGaHgCIg3wYkTx5dlr8jSPfTpB1rkMOUiURpWKAOrfnPGrgFPbfNtWw==" + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "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.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-daily-rotate-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", + "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", + "license": "MIT", + "dependencies": { + "file-stream-rotator": "^0.6.1", + "object-hash": "^3.0.0", + "triple-beam": "^1.4.1", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "winston": "^3" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..1659e0aa --- /dev/null +++ b/package.json @@ -0,0 +1,65 @@ +{ + "name": "wf-emulator", + "version": "0.1.0", + "description": "WF Emulator", + "main": "index.ts", + "scripts": { + "start": "node --enable-source-maps build/src/index.js", + "build": "tsgo --sourceMap && ncp static/webui build/static/webui", + "build:tsc": "tsc --incremental --sourceMap && ncp static/webui build/static/webui", + "build:dev": "tsgo --sourceMap", + "build:dev:tsc": "tsc --incremental --sourceMap", + "build-and-start": "npm run build && npm run start", + "build-and-start:bun": "npm run verify && npm run bun-run", + "dev": "node scripts/dev.cjs", + "dev:bun": "bun scripts/dev.cjs", + "verify": "tsgo --noEmit", + "verify:tsc": "tsc --noEmit", + "raw": "node --experimental-transform-types src/index.ts", + "raw:bun": "bun src/index.ts", + "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.cjs", + "fix": "npm run update-translations && npm run lint:fix" + }, + "license": "GNU", + "type": "module", + "dependencies": { + "chokidar": "^4.0.3", + "crc-32": "^1.2.2", + "express": "^5", + "json-with-bigint": "^3.4.4", + "mongoose": "^8.11.0", + "morgan": "^1.10.0", + "ncp": "^2.0.0", + "undici": "^7.10.0", + "warframe-public-export-plus": "^0.5.83", + "warframe-riven-info": "^0.1.2", + "winston": "^3.17.0", + "winston-daily-rotate-file": "^5.0.0", + "ws": "^8.18.2" + }, + "optionalDependencies": { + "@types/express": "^5", + "@types/morgan": "^1.9.9", + "@types/websocket": "^1.0.10", + "@types/ws": "^8.18.1", + "@typescript/native-preview": "^7.0.0-dev.20250625.1", + "typescript": "^5.7" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^8.28.0", + "@typescript-eslint/parser": "^8.28.0", + "eslint": "^8", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-prettier": "^5.2.5", + "prettier": "^3.5.3", + "tree-kill": "^1.2.2" + }, + "engines": { + "node": ">=20.18.1" + } +} diff --git a/scripts/dev.cjs b/scripts/dev.cjs new file mode 100644 index 00000000..7931df6b --- /dev/null +++ b/scripts/dev.cjs @@ -0,0 +1,58 @@ +/* eslint-disable */ +const { spawn } = require("child_process"); +const chokidar = require("chokidar"); +const kill = require("tree-kill"); + +let secret = ""; +for (let i = 0; i != 10; ++i) { + secret += String.fromCharCode(Math.floor(Math.random() * 26) + 0x41); +} + +const args = [...process.argv].splice(2); +args.push("--dev"); +args.push("--secret"); +args.push(secret); + +let buildproc, runproc; +const spawnopts = { stdio: "inherit", shell: true }; +function run(changedFile) { + if (changedFile) { + console.log(`Change to ${changedFile} detected`); + } + + if (buildproc) { + kill(buildproc.pid); + buildproc = undefined; + } + if (runproc) { + kill(runproc.pid); + runproc = undefined; + } + + const thisbuildproc = spawn("npm", ["run", process.versions.bun ? "verify" : "build:dev"], spawnopts); + const thisbuildstart = Date.now(); + buildproc = thisbuildproc; + buildproc.on("exit", code => { + if (buildproc !== thisbuildproc) { + return; + } + buildproc = undefined; + if (code === 0) { + console.log(`${process.versions.bun ? "Verified" : "Built"} in ${Date.now() - thisbuildstart} ms`); + runproc = spawn("npm", ["run", process.versions.bun ? "raw:bun" : "start", "--", ...args], spawnopts); + runproc.on("exit", () => { + runproc = undefined; + }); + } + }); +} + +run(); +chokidar.watch("src").on("change", run); +chokidar.watch("static/fixed_responses").on("change", run); + +chokidar.watch("static/webui").on("change", async () => { + try { + await fetch("http://localhost/custom/webuiFileChangeDetected?secret=" + secret); + } catch (e) {} +}); diff --git a/scripts/update-translations.cjs b/scripts/update-translations.cjs new file mode 100644 index 00000000..6c840a5d --- /dev/null +++ b/scripts/update-translations.cjs @@ -0,0 +1,47 @@ +// Based on https://onlyg.it/OpenWF/Translations/src/branch/main/update.php +// Converted via ChatGPT-4o + +/* eslint-disable */ +const fs = require("fs"); + +function extractStrings(content) { + const regex = /([a-zA-Z0-9_]+): `([^`]*)`,/g; + let matches; + const strings = {}; + while ((matches = regex.exec(content)) !== null) { + strings[matches[1]] = matches[2]; + } + return strings; +} + +const source = fs.readFileSync("../static/webui/translations/en.js", "utf8"); +const sourceStrings = extractStrings(source); +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") { + const content = fs.readFileSync(`../static/webui/translations/${file}`, "utf8"); + const targetStrings = extractStrings(content); + const contentLines = content.split("\n"); + + const fileHandle = fs.openSync(`../static/webui/translations/${file}`, "w"); + fs.writeSync(fileHandle, contentLines[0] + "\n"); + + sourceLines.forEach(line => { + const strings = extractStrings(line); + if (Object.keys(strings).length > 0) { + Object.entries(strings).forEach(([key, value]) => { + if (targetStrings.hasOwnProperty(key) && !targetStrings[key].startsWith("[UNTRANSLATED]")) { + fs.writeSync(fileHandle, ` ${key}: \`${targetStrings[key]}\`,\n`); + } else { + fs.writeSync(fileHandle, ` ${key}: \`[UNTRANSLATED] ${value}\`,\n`); + } + }); + } else { + fs.writeSync(fileHandle, line + "\n"); + } + }); + + fs.closeSync(fileHandle); + } +}); diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 00000000..971fe210 --- /dev/null +++ b/src/app.ts @@ -0,0 +1,51 @@ +import express from "express"; + +import bodyParser from "body-parser"; +import { unknownEndpointHandler } from "./middleware/middleware.ts"; +import { requestLogger } from "./middleware/morgenMiddleware.ts"; +import { errorHandler } from "./middleware/errorHandler.ts"; + +import { apiRouter } from "./routes/api.ts"; +import { cacheRouter } from "./routes/cache.ts"; +import { customRouter } from "./routes/custom.ts"; +import { dynamicController } from "./routes/dynamic.ts"; +import { payRouter } from "./routes/pay.ts"; +import { statsRouter } from "./routes/stats.ts"; +import { webuiRouter } from "./routes/webui.ts"; + +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({ 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); +app.use("/", webuiRouter); + +app.use(unknownEndpointHandler); +app.use(errorHandler); + +export { app }; diff --git a/src/constants/timeConstants.ts b/src/constants/timeConstants.ts new file mode 100644 index 00000000..4411f556 --- /dev/null +++ b/src/constants/timeConstants.ts @@ -0,0 +1,19 @@ +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, + week: unixWeek +}; diff --git a/src/controllers/api/abandonLibraryDailyTaskController.ts b/src/controllers/api/abandonLibraryDailyTaskController.ts new file mode 100644 index 00000000..0b44a2ee --- /dev/null +++ b/src/controllers/api/abandonLibraryDailyTaskController.ts @@ -0,0 +1,11 @@ +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { 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..803c8baa --- /dev/null +++ b/src/controllers/api/abortDojoComponentController.ts @@ -0,0 +1,46 @@ +import { + getDojoClient, + getGuildForRequestEx, + hasAccessToDojo, + hasGuildPermission, + removeDojoDeco, + removeDojoRoom +} from "../../services/guildService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import type { 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..8ea79363 --- /dev/null +++ b/src/controllers/api/abortDojoComponentDestructionController.ts @@ -0,0 +1,26 @@ +import { + getDojoClient, + getGuildForRequestEx, + hasAccessToDojo, + hasGuildPermission +} from "../../services/guildService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import type { 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 new file mode 100644 index 00000000..ce452b37 --- /dev/null +++ b/src/controllers/api/activateRandomModController.ts @@ -0,0 +1,46 @@ +import { toOid } from "../../helpers/inventoryHelpers.ts"; +import { + createVeiledRivenFingerprint, + createUnveiledRivenFingerprint, + rivenRawToRealWeighted +} from "../../helpers/rivenHelper.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { addMods, getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getRandomElement } from "../../services/rngService.ts"; +import type { RequestHandler } from "express"; +import { ExportUpgrades } from "warframe-public-export-plus"; + +export const activateRandomModController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "RawUpgrades Upgrades instantFinishRivenChallenge"); + const request = getJSONfromString(String(req.body)); + addMods(inventory, [ + { + ItemType: request.ItemType, + ItemCount: -1 + } + ]); + const rivenType = getRandomElement(rivenRawToRealWeighted[request.ItemType])!; + const fingerprint = inventory.instantFinishRivenChallenge + ? createUnveiledRivenFingerprint(ExportUpgrades[rivenType]) + : createVeiledRivenFingerprint(ExportUpgrades[rivenType]); + const upgradeIndex = + inventory.Upgrades.push({ + ItemType: rivenType, + 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: { + UpgradeFingerprint: fingerprint, + ItemType: rivenType, + ItemId: toOid(inventory.Upgrades[upgradeIndex]._id) + } + }); +}; + +interface IActiveRandomModRequest { + ItemType: string; +} diff --git a/src/controllers/api/addFriendController.ts b/src/controllers/api/addFriendController.ts new file mode 100644 index 00000000..c6a24e20 --- /dev/null +++ b/src/controllers/api/addFriendController.ts @@ -0,0 +1,60 @@ +import { toOid } from "../../helpers/inventoryHelpers.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { Friendship } from "../../models/friendModel.ts"; +import { addAccountDataToFriendInfo, addInventoryDataToFriendInfo } from "../../services/friendService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IFriendInfo } from "../../types/friendTypes.ts"; +import type { 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 new file mode 100644 index 00000000..d3702c12 --- /dev/null +++ b/src/controllers/api/addFriendImageController.ts @@ -0,0 +1,25 @@ +import type { RequestHandler } from "express"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { Inventory } from "../../models/inventoryModels/inventoryModel.ts"; + +export const addFriendImageController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const json = getJSONfromString(String(req.body)); + + await Inventory.updateOne( + { + accountOwnerId: accountId + }, + { + ActiveAvatarImageType: json.AvatarImageType + } + ); + + res.json({}); +}; + +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..b268e498 --- /dev/null +++ b/src/controllers/api/addIgnoredUserController.ts @@ -0,0 +1,30 @@ +import { toOid } from "../../helpers/inventoryHelpers.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { Account, Ignore } from "../../models/loginModel.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IFriendInfo } from "../../types/friendTypes.ts"; +import type { 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..fc1e06c9 --- /dev/null +++ b/src/controllers/api/addPendingFriendController.ts @@ -0,0 +1,52 @@ +import { toMongoDate, toOid } from "../../helpers/inventoryHelpers.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { Friendship } from "../../models/friendModel.ts"; +import { Account } from "../../models/loginModel.ts"; +import { addInventoryDataToFriendInfo, areFriendsOfFriends } from "../../services/friendService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IFriendInfo } from "../../types/friendTypes.ts"; +import type { 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..6e004bf2 --- /dev/null +++ b/src/controllers/api/addToAllianceController.ts @@ -0,0 +1,117 @@ +import { getJSONfromString, regexEscape } from "../../helpers/stringHelpers.ts"; +import { Alliance, AllianceMember, Guild, GuildMember } from "../../models/guildModel.ts"; +import { createMessage } from "../../services/inboxService.ts"; +import { getEffectiveAvatarImageType, getInventory } from "../../services/inventoryService.ts"; +import { getAccountForRequest, getSuffixedName } from "../../services/loginService.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import { logger } from "../../utils/logger.ts"; +import type { 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..57fb6193 --- /dev/null +++ b/src/controllers/api/addToGuildController.ts @@ -0,0 +1,112 @@ +import { toMongoDate } from "../../helpers/inventoryHelpers.ts"; +import { Guild, GuildMember } from "../../models/guildModel.ts"; +import { Account } from "../../models/loginModel.ts"; +import { addInventoryDataToFriendInfo, areFriends } from "../../services/friendService.ts"; +import { hasGuildPermission } from "../../services/guildService.ts"; +import { createMessage } from "../../services/inboxService.ts"; +import { getEffectiveAvatarImageType, getInventory } from "../../services/inventoryService.ts"; +import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "../../services/loginService.ts"; +import type { IOid } from "../../types/commonTypes.ts"; +import type { IGuildMemberClient } from "../../types/guildTypes.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import { logger } from "../../utils/logger.ts"; +import type { 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/adoptPetController.ts b/src/controllers/api/adoptPetController.ts new file mode 100644 index 00000000..607ea5d2 --- /dev/null +++ b/src/controllers/api/adoptPetController.ts @@ -0,0 +1,27 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { RequestHandler } from "express"; + +export const adoptPetController: 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.Name = data.name; + await inventory.save(); + res.json({ + petId: data.petId, + newName: data.name + } satisfies IAdoptPetResponse); +}; + +interface IAdoptPetRequest { + petId: string; + name: string; +} + +interface IAdoptPetResponse { + petId: string; + newName: string; +} diff --git a/src/controllers/api/apartmentController.ts b/src/controllers/api/apartmentController.ts new file mode 100644 index 00000000..247d5b10 --- /dev/null +++ b/src/controllers/api/apartmentController.ts @@ -0,0 +1,22 @@ +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getPersonalRooms } from "../../services/personalRoomsService.ts"; +import type { RequestHandler } from "express"; + +export const apartmentController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const personalRooms = await getPersonalRooms(accountId, "Apartment"); + const response: IApartmentResponse = {}; + if (req.query.backdrop !== undefined) { + response.NewBackdropItem = personalRooms.Apartment.VideoWallBackdrop = req.query.backdrop as string; + } + if (req.query.soundscape !== undefined) { + response.NewSoundscapeItem = personalRooms.Apartment.Soundscape = req.query.soundscape as string; + } + await personalRooms.save(); + res.json(response); +}; + +interface IApartmentResponse { + NewBackdropItem?: string; + NewSoundscapeItem?: string; +} diff --git a/src/controllers/api/arcaneCommonController.ts b/src/controllers/api/arcaneCommonController.ts new file mode 100644 index 00000000..299b1b22 --- /dev/null +++ b/src/controllers/api/arcaneCommonController.ts @@ -0,0 +1,76 @@ +import type { RequestHandler } from "express"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getInventory, addMods } from "../../services/inventoryService.ts"; +import type { IOid } from "../../types/commonTypes.ts"; + +export const arcaneCommonController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const json = getJSONfromString(String(req.body)); + const inventory = await getInventory(accountId); + const upgrade = inventory.Upgrades.id(json.arcane.ItemId.$oid); + if (json.newRank == -1) { + // Break down request? + if (!upgrade || !upgrade.UpgradeFingerprint) { + throw new Error(`Failed to find upgrade with OID ${json.arcane.ItemId.$oid}`); + } + + // Remove Upgrade + inventory.Upgrades.pull({ _id: json.arcane.ItemId.$oid }); + + // Add RawUpgrades + const numRawUpgradesToGive = arcaneLevelCounts[(JSON.parse(upgrade.UpgradeFingerprint) as { lvl: number }).lvl]; + addMods(inventory, [ + { + ItemType: json.arcane.ItemType, + ItemCount: numRawUpgradesToGive + } + ]); + + res.json({ upgradeId: json.arcane.ItemId.$oid, numConsumed: numRawUpgradesToGive }); + } else { + // Upgrade request? + let numConsumed = arcaneLevelCounts[json.newRank]; + let upgradeId = json.arcane.ItemId.$oid; + if (upgrade) { + // Have an existing Upgrade item? + if (upgrade.UpgradeFingerprint) { + const existingLevel = (JSON.parse(upgrade.UpgradeFingerprint) as { lvl: number }).lvl; + numConsumed -= arcaneLevelCounts[existingLevel]; + } + upgrade.UpgradeFingerprint = JSON.stringify({ lvl: json.newRank }); + } else { + const newLength = inventory.Upgrades.push({ + ItemType: json.arcane.ItemType, + UpgradeFingerprint: JSON.stringify({ lvl: json.newRank }) + }); + upgradeId = inventory.Upgrades[newLength - 1]._id.toString(); + } + + // Remove RawUpgrades + addMods(inventory, [ + { + ItemType: json.arcane.ItemType, + ItemCount: numConsumed * -1 + } + ]); + + res.json({ newLevel: json.newRank, numConsumed, upgradeId }); + } + await inventory.save(); +}; + +const arcaneLevelCounts = [0, 3, 6, 10, 15, 21]; + +interface IArcaneCommonRequest { + arcane: { + ItemType: string; + ItemId: IOid; + FromSKU: boolean; + UpgradeFingerprint: string; + PendingRerollFingerprint: string; + ItemCount: number; + LastAdded: IOid; + }; + newRank: number; +} diff --git a/src/controllers/api/archonFusionController.ts b/src/controllers/api/archonFusionController.ts new file mode 100644 index 00000000..3c046d41 --- /dev/null +++ b/src/controllers/api/archonFusionController.ts @@ -0,0 +1,51 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { addMiscItems, getInventory } from "../../services/inventoryService.ts"; +import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts"; +import { colorToShard, combineColors, shardToColor } from "../../helpers/shardHelper.ts"; + +export const archonFusionController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const request = JSON.parse(String(req.body)) as IArchonFusionRequest; + const inventory = await getInventory(accountId); + request.Consumed.forEach(x => { + x.ItemCount *= -1; + }); + addMiscItems(inventory, request.Consumed); + const newArchons: IMiscItem[] = []; + switch (request.FusionType) { + case "AFT_ASCENT": + newArchons.push({ + ItemType: request.Consumed[0].ItemType + "Mythic", + ItemCount: 1 + }); + break; + + case "AFT_COALESCENT": + newArchons.push({ + ItemType: + colorToShard[ + combineColors( + shardToColor[request.Consumed[0].ItemType], + shardToColor[request.Consumed[1].ItemType] + ) + ], + ItemCount: 1 + }); + break; + + default: + throw new Error(`unknown archon fusion type: ${request.FusionType}`); + } + addMiscItems(inventory, newArchons); + await inventory.save(); + res.json({ + NewArchons: newArchons + }); +}; + +interface IArchonFusionRequest { + Consumed: IMiscItem[]; + FusionType: string; + StatResultType: "SRT_NEW_STAT"; // ??? +} diff --git a/src/controllers/api/artifactTransmutationController.ts b/src/controllers/api/artifactTransmutationController.ts new file mode 100644 index 00000000..69cc41ee --- /dev/null +++ b/src/controllers/api/artifactTransmutationController.ts @@ -0,0 +1,169 @@ +import { fromOid, toOid } from "../../helpers/inventoryHelpers.ts"; +import { createVeiledRivenFingerprint, rivenRawToRealWeighted } from "../../helpers/rivenHelper.ts"; +import { addMiscItems, addMods, getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getRandomElement, getRandomWeightedReward, getRandomWeightedRewardUc } from "../../services/rngService.ts"; +import type { IUpgradeFromClient } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { RequestHandler } from "express"; +import type { TRarity } from "warframe-public-export-plus"; +import { ExportBoosterPacks, ExportUpgrades } 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/artifactsController.ts b/src/controllers/api/artifactsController.ts new file mode 100644 index 00000000..4fb2155d --- /dev/null +++ b/src/controllers/api/artifactsController.ts @@ -0,0 +1,68 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { RequestHandler } from "express"; +import type { IInventoryClient, IUpgradeClient } from "../../types/inventoryTypes/inventoryTypes.ts"; +import { addMods, getInventory } from "../../services/inventoryService.ts"; + +export const artifactsController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const artifactsData = getJSONfromString(String(req.body)); + + const { Upgrade, LevelDiff, Cost, FusionPointCost } = artifactsData; + + const inventory = await getInventory(accountId); + const { Upgrades } = inventory; + const { ItemType, UpgradeFingerprint, ItemId } = Upgrade; + + const safeUpgradeFingerprint = UpgradeFingerprint || '{"lvl":0}'; + const parsedUpgradeFingerprint = JSON.parse(safeUpgradeFingerprint) as { lvl: number }; + parsedUpgradeFingerprint.lvl += LevelDiff; + const stringifiedUpgradeFingerprint = JSON.stringify(parsedUpgradeFingerprint); + + let itemIndex = Upgrades.findIndex(upgrade => upgrade._id.equals(ItemId.$oid)); + + if (itemIndex !== -1) { + Upgrades[itemIndex].UpgradeFingerprint = stringifiedUpgradeFingerprint; + } else { + itemIndex = + Upgrades.push({ + UpgradeFingerprint: stringifiedUpgradeFingerprint, + ItemType + }) - 1; + + addMods(inventory, [{ ItemType, ItemCount: -1 }]); + } + + if (!inventory.infiniteCredits) { + inventory.RegularCredits -= Cost; + } + if (!inventory.infiniteEndo) { + inventory.FusionPoints -= FusionPointCost; + } + + if (artifactsData.LegendaryFusion) { + addMods(inventory, [ + { + ItemType: "/Lotus/Upgrades/Mods/Fusers/LegendaryModFuser", + ItemCount: -1 + } + ]); + } + + const changedInventory = await inventory.save(); + const itemId = changedInventory.toJSON().Upgrades[itemIndex].ItemId.$oid; + + if (!itemId) { + throw new Error("Item Id not found in upgradeMod"); + } + + res.send(itemId); +}; + +interface IArtifactsRequest { + Upgrade: IUpgradeClient; + LevelDiff: number; + Cost: number; + FusionPointCost: number; + LegendaryFusion?: boolean; +} diff --git a/src/controllers/api/cancelGuildAdvertisementController.ts b/src/controllers/api/cancelGuildAdvertisementController.ts new file mode 100644 index 00000000..a94fd2d0 --- /dev/null +++ b/src/controllers/api/cancelGuildAdvertisementController.ts @@ -0,0 +1,20 @@ +import { GuildAd } from "../../models/guildModel.ts"; +import { getGuildForRequestEx, hasGuildPermission } from "../../services/guildService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import type { 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 new file mode 100644 index 00000000..f4ab62ce --- /dev/null +++ b/src/controllers/api/changeDojoRootController.ts @@ -0,0 +1,99 @@ +import type { RequestHandler } from "express"; +import { + getDojoClient, + getGuildForRequestEx, + hasAccessToDojo, + hasGuildPermission +} from "../../services/guildService.ts"; +import { logger } from "../../utils/logger.ts"; +import type { IDojoComponentDatabase } from "../../types/guildTypes.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import { Types } from "mongoose"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; + +export const changeDojoRootController: 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; + } + + // 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 => { + idToNode[x._id.toString()] = { + component: x, + parent: undefined, + children: [] + }; + }); + + let oldRoot: INode | undefined; + guild.DojoComponents.forEach(x => { + const node = idToNode[x._id.toString()]; + if (x.pi) { + idToNode[x.pi.toString()].children.push(node); + node.parent = idToNode[x.pi.toString()]; + } else { + oldRoot = node; + } + }); + logger.debug("Old tree:\n" + treeToString(oldRoot!)); + + const newRoot = idToNode[req.query.newRoot as string]; + recursivelyTurnParentsIntoChildren(newRoot); + newRoot.component.pi = undefined; + newRoot.component.op = undefined; + newRoot.component.pp = undefined; + newRoot.parent = undefined; + + // Set/update SortId in top-to-bottom order + const stack: INode[] = [newRoot]; + while (stack.length != 0) { + const top = stack.shift()!; + top.component.SortId = new Types.ObjectId(); + top.children.forEach(x => stack.push(x)); + } + + logger.debug("New tree:\n" + treeToString(newRoot)); + + await guild.save(); + + res.json(await getDojoClient(guild, 0)); +}; + +interface INode { + component: IDojoComponentDatabase; + parent: INode | undefined; + children: INode[]; +} + +const treeToString = (root: INode, depth: number = 0): string => { + let str = " ".repeat(depth * 4) + root.component.pf + " (" + root.component._id.toString() + ")\n"; + root.children.forEach(x => { + str += treeToString(x, depth + 1); + }); + return str; +}; + +const recursivelyTurnParentsIntoChildren = (node: INode): void => { + if (node.parent!.parent) { + recursivelyTurnParentsIntoChildren(node.parent!); + } + + node.parent!.component.pi = node.component._id; + node.parent!.component.op = node.component.pp; + node.parent!.component.pp = node.component.op; + + node.parent!.parent = node; + node.parent!.children.splice(node.parent!.children.indexOf(node), 1); + node.children.push(node.parent!); +}; diff --git a/src/controllers/api/changeGuildRankController.ts b/src/controllers/api/changeGuildRankController.ts new file mode 100644 index 00000000..30ebe2c3 --- /dev/null +++ b/src/controllers/api/changeGuildRankController.ts @@ -0,0 +1,38 @@ +import { GuildMember } from "../../models/guildModel.ts"; +import { getGuildForRequest, hasGuildPermissionEx } from "../../services/guildService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import type { 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/checkDailyMissionBonusController.ts b/src/controllers/api/checkDailyMissionBonusController.ts new file mode 100644 index 00000000..4bdcb628 --- /dev/null +++ b/src/controllers/api/checkDailyMissionBonusController.ts @@ -0,0 +1,12 @@ +import { getAccountForRequest } from "../../services/loginService.ts"; +import type { RequestHandler } from "express"; + +export const checkDailyMissionBonusController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + const today = Math.trunc(Date.now() / 86400000) * 86400; + if (account.DailyFirstWinDate != today) { + res.send("DailyMissionBonus:1-DailyPVPWinBonus:1\n"); + } else { + res.send("DailyMissionBonus:0-DailyPVPWinBonus:1\n"); + } +}; diff --git a/src/controllers/api/claimCompletedRecipeController.ts b/src/controllers/api/claimCompletedRecipeController.ts new file mode 100644 index 00000000..39782101 --- /dev/null +++ b/src/controllers/api/claimCompletedRecipeController.ts @@ -0,0 +1,307 @@ +//this is a controller for the claimCompletedRecipe route +//it will claim a recipe for the user + +import type { RequestHandler } from "express"; +import { logger } from "../../utils/logger.ts"; +import { getRecipe } from "../../services/itemDataService.ts"; +import type { IOidWithLegacySupport } from "../../types/commonTypes.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import type { TAccountDocument } from "../../services/loginService.ts"; +import { getAccountForRequest } from "../../services/loginService.ts"; +import { + getInventory, + updateCurrency, + addItem, + addRecipes, + occupySlot, + combineInventoryChanges, + addKubrowPetPrint, + addPowerSuit, + addEquipment +} from "../../services/inventoryService.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; +import type { IPendingRecipeDatabase } from "../../types/inventoryTypes/inventoryTypes.ts"; +import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts"; +import { fromOid, toOid2 } from "../../helpers/inventoryHelpers.ts"; +import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts"; +import type { IRecipe } from "warframe-public-export-plus"; +import type { IEquipmentClient } from "../../types/equipmentTypes.ts"; +import { EquipmentFeatures, Status } from "../../types/equipmentTypes.ts"; + +interface IClaimCompletedRecipeRequest { + RecipeIds: IOidWithLegacySupport[]; +} + +interface IClaimCompletedRecipeResponse { + InventoryChanges: IInventoryChanges; + BrandedSuits?: IOidWithLegacySupport[]; +} + +export const claimCompletedRecipeController: RequestHandler = async (req, res) => { + const claimCompletedRecipeRequest = getJSONfromString(String(req.body)); + const account = await getAccountForRequest(req); + const inventory = await getInventory(account._id.toString()); + const resp: IClaimCompletedRecipeResponse = { + InventoryChanges: {} + }; + for (const recipeId of claimCompletedRecipeRequest.RecipeIds) { + const pendingRecipe = inventory.PendingRecipes.id(fromOid(recipeId)); + if (!pendingRecipe) { + throw new Error(`no pending recipe found with id ${fromOid(recipeId)}`); + } + + //check recipe is indeed ready to be completed + // if (pendingRecipe.CompletionDate > new Date()) { + // throw new Error(`recipe ${pendingRecipe._id} is not ready to be completed`); + // } + + inventory.PendingRecipes.pull(pendingRecipe._id); + + const recipe = getRecipe(pendingRecipe.ItemType); + if (!recipe) { + throw new Error(`no completed item found for recipe ${pendingRecipe._id.toString()}`); + } + + if (req.query.cancel) { + const inventoryChanges: IInventoryChanges = {}; + await refundRecipeIngredients(inventory, inventoryChanges, recipe, pendingRecipe); + await inventory.save(); + res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root. + return; + } + + await claimCompletedRecipe(account, inventory, recipe, pendingRecipe, resp, req.query.rush); + } + await inventory.save(); + res.json(resp); +}; + +const claimCompletedRecipe = async ( + account: TAccountDocument, + inventory: TInventoryDatabaseDocument, + recipe: IRecipe, + pendingRecipe: IPendingRecipeDatabase, + resp: IClaimCompletedRecipeResponse, + rush: any +): Promise => { + logger.debug("Claiming Recipe", { recipe, pendingRecipe }); + + if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") { + inventory.PendingSpectreLoadouts ??= []; + inventory.SpectreLoadouts ??= []; + + const pendingLoadoutIndex = inventory.PendingSpectreLoadouts.findIndex(x => x.ItemType == recipe.resultType); + if (pendingLoadoutIndex != -1) { + const loadoutIndex = inventory.SpectreLoadouts.findIndex(x => x.ItemType == recipe.resultType); + if (loadoutIndex != -1) { + inventory.SpectreLoadouts.splice(loadoutIndex, 1); + } + logger.debug( + "moving spectre loadout from pending to active", + inventory.toJSON().PendingSpectreLoadouts![pendingLoadoutIndex] + ); + inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]); + inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1); + } + } else if (recipe.secretIngredientAction == "SIA_UNBRAND") { + inventory.BrandedSuits!.splice( + inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)), + 1 + ); + resp.BrandedSuits = [toOid2(pendingRecipe.SuitToUnbrand!, account.BuildLabel)]; + } + + if (recipe.consumeOnUse) { + addRecipes(inventory, [ + { + ItemType: pendingRecipe.ItemType, + ItemCount: -1 + } + ]); + } + + if (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 = + progress > 0.5 ? Math.round(recipe.skipBuildTimePrice * (1 - (progress - 0.5))) : recipe.skipBuildTimePrice; + combineInventoryChanges(resp.InventoryChanges, updateCurrency(inventory, cost, true)); + } + + if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") { + const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!; + if (pet.Details!.HatchDate!.getTime() > Date.now()) { + pet.Details!.HatchDate = new Date(); + } + let canSetActive = true; + for (const pet of inventory.KubrowPets) { + if (pet.Details!.Status == Status.StatusAvailable) { + canSetActive = false; + break; + } + } + pet.Details!.Status = canSetActive ? Status.StatusAvailable : Status.StatusStasis; + } else if (recipe.secretIngredientAction == "SIA_DISTILL_PRINT") { + const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!; + addKubrowPetPrint(inventory, pet, resp.InventoryChanges); + } else if (recipe.secretIngredientAction != "SIA_UNBRAND") { + if (recipe.resultType == "/Lotus/Powersuits/Excalibur/ExcaliburUmbra") { + // Quite the special case here... + // We don't just get Umbra, but also Skiajati and Umbra Mods. Both items are max rank, potatoed, and with the mods are pre-installed. + // Source: https://wiki.warframe.com/w/The_Sacrifice, https://wiki.warframe.com/w/Excalibur/Umbra, https://wiki.warframe.com/w/Skiajati + + const umbraModA = ( + await addItem( + inventory, + "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModA", + 1, + false, + undefined, + `{"lvl":5}` + ) + ).Upgrades![0]; + const umbraModB = ( + await addItem( + inventory, + "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModB", + 1, + false, + undefined, + `{"lvl":5}` + ) + ).Upgrades![0]; + const umbraModC = ( + await addItem( + inventory, + "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModC", + 1, + false, + undefined, + `{"lvl":5}` + ) + ).Upgrades![0]; + const sacrificeModA = ( + await addItem( + inventory, + "/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModA", + 1, + false, + undefined, + `{"lvl":5}` + ) + ).Upgrades![0]; + const sacrificeModB = ( + await addItem( + inventory, + "/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModB", + 1, + false, + undefined, + `{"lvl":5}` + ) + ).Upgrades![0]; + resp.InventoryChanges.Upgrades ??= []; + resp.InventoryChanges.Upgrades.push(umbraModA, umbraModB, umbraModC, sacrificeModA, sacrificeModB); + + await addPowerSuit( + inventory, + "/Lotus/Powersuits/Excalibur/ExcaliburUmbra", + { + Configs: [ + { + Upgrades: [ + "", + "", + "", + "", + "", + umbraModA.ItemId.$oid, + umbraModB.ItemId.$oid, + umbraModC.ItemId.$oid + ] + } + ], + XP: 900_000, + Features: EquipmentFeatures.DOUBLE_CAPACITY + }, + resp.InventoryChanges + ); + inventory.XPInfo.push({ + ItemType: "/Lotus/Powersuits/Excalibur/ExcaliburUmbra", + XP: 900_000 + }); + + addEquipment( + inventory, + "Melee", + "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana", + { + Configs: [ + { Upgrades: ["", "", "", "", "", "", sacrificeModA.ItemId.$oid, sacrificeModB.ItemId.$oid] } + ], + XP: 450_000, + Features: EquipmentFeatures.DOUBLE_CAPACITY + }, + resp.InventoryChanges + ); + inventory.XPInfo.push({ + ItemType: "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana", + XP: 450_000 + }); + } else { + combineInventoryChanges( + resp.InventoryChanges, + await addItem( + inventory, + recipe.resultType, + recipe.num, + false, + undefined, + pendingRecipe.TargetFingerprint + ) + ); + } + } + if ( + inventory.claimingBlueprintRefundsIngredients && + recipe.secretIngredientAction != "SIA_CREATE_KUBROW" // Can't refund the egg + ) { + await refundRecipeIngredients(inventory, resp.InventoryChanges, recipe, pendingRecipe); + } +}; + +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/claimJunctionChallengeRewardController.ts b/src/controllers/api/claimJunctionChallengeRewardController.ts new file mode 100644 index 00000000..4e45852e --- /dev/null +++ b/src/controllers/api/claimJunctionChallengeRewardController.ts @@ -0,0 +1,35 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { combineInventoryChanges, getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { handleStoreItemAcquisition } from "../../services/purchaseService.ts"; +import type { RequestHandler } from "express"; +import { ExportChallenges } from "warframe-public-export-plus"; + +export const claimJunctionChallengeRewardController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const data = getJSONfromString(String(req.body)); + const challengeProgress = inventory.ChallengeProgress.find(x => x.Name == data.Challenge)!; + if (challengeProgress.ReceivedJunctionReward) { + throw new Error(`attempt to double-claim junction reward`); + } + challengeProgress.ReceivedJunctionReward = true; + inventory.ClaimedJunctionChallengeRewards ??= []; + inventory.ClaimedJunctionChallengeRewards.push(data.Challenge); + const challengeMeta = Object.entries(ExportChallenges).find(arr => arr[0].endsWith("/" + data.Challenge))![1]; + const inventoryChanges = {}; + for (const reward of challengeMeta.countedRewards!) { + combineInventoryChanges( + inventoryChanges, + (await handleStoreItemAcquisition(reward.StoreItem, inventory, reward.ItemCount)).InventoryChanges + ); + } + await inventory.save(); + res.json({ + inventoryChanges: inventoryChanges // Yeah, it's "inventoryChanges" in the response here. + }); +}; + +interface IClaimJunctionChallengeRewardRequest { + Challenge: string; +} diff --git a/src/controllers/api/claimLibraryDailyTaskRewardController.ts b/src/controllers/api/claimLibraryDailyTaskRewardController.ts new file mode 100644 index 00000000..423c3ba2 --- /dev/null +++ b/src/controllers/api/claimLibraryDailyTaskRewardController.ts @@ -0,0 +1,31 @@ +import { addFusionPoints, getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { 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 new file mode 100644 index 00000000..4a657dfe --- /dev/null +++ b/src/controllers/api/clearDialogueHistoryController.ts @@ -0,0 +1,25 @@ +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { RequestHandler } from "express"; + +export const clearDialogueHistoryController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + 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) { + inventory.DialogueHistory.Dialogues.splice(index, 1); + } + } + } + await inventory.save(); + res.end(); +}; + +interface IClearDialogueRequest { + Dialogues: string[]; +} diff --git a/src/controllers/api/clearNewEpisodeRewardController.ts b/src/controllers/api/clearNewEpisodeRewardController.ts new file mode 100644 index 00000000..dcd8e46c --- /dev/null +++ b/src/controllers/api/clearNewEpisodeRewardController.ts @@ -0,0 +1,6 @@ +import type { 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..5faef69c --- /dev/null +++ b/src/controllers/api/completeCalendarEventController.ts @@ -0,0 +1,37 @@ +import { checkCalendarAutoAdvance, getCalendarProgress, getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { handleStoreItemAcquisition } from "../../services/purchaseService.ts"; +import { getWorldState } from "../../services/worldStateService.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; +import type { 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 = {}; + const dayIndex = calendarProgress.SeasonProgress.LastCompletedDayIdx + 1; + const day = currentSeason.Days[dayIndex]; + if (day.events.length != 0) { + if (day.events[0].type == "CET_CHALLENGE") { + throw new Error(`completeCalendarEvent should not be used for challenges`); + } + 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}`); + } + } + calendarProgress.SeasonProgress.LastCompletedDayIdx = dayIndex; + checkCalendarAutoAdvance(inventory, currentSeason); + 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..d3c0055a --- /dev/null +++ b/src/controllers/api/completeRandomModChallengeController.ts @@ -0,0 +1,45 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { addMiscItems, getInventory, updateCurrency } from "../../services/inventoryService.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; +import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import type { IVeiledRivenFingerprint } from "../../helpers/rivenHelper.ts"; + +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; + } + + // Complete the riven challenge + const upgrade = inventory.Upgrades.id(request.ItemId)!; + const fp = JSON.parse(upgrade.UpgradeFingerprint!) as IVeiledRivenFingerprint; + fp.challenge.Progress = fp.challenge.Required; + upgrade.UpgradeFingerprint = JSON.stringify(fp); + + 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..095fa165 --- /dev/null +++ b/src/controllers/api/confirmAllianceInvitationController.ts @@ -0,0 +1,37 @@ +import { Alliance, AllianceMember, Guild, GuildMember } from "../../models/guildModel.ts"; +import { getAllianceClient } from "../../services/guildService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { 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..bfe30fb2 --- /dev/null +++ b/src/controllers/api/confirmGuildInvitationController.ts @@ -0,0 +1,121 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { Guild, GuildMember } from "../../models/guildModel.ts"; +import { Account } from "../../models/loginModel.ts"; +import { + deleteGuild, + getGuildClient, + giveClanKey, + hasGuildPermission, + removeDojoKeyItems +} from "../../services/guildService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "../../services/loginService.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; +import type { 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 skipClanKeyCrafting" + ); + 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..3a04ee6d --- /dev/null +++ b/src/controllers/api/contributeGuildClassController.ts @@ -0,0 +1,67 @@ +import { toMongoDate } from "../../helpers/inventoryHelpers.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { Guild } from "../../models/guildModel.ts"; +import { checkClanAscensionHasRequiredContributors } from "../../services/guildService.ts"; +import { addFusionPoints, getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { 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..14c43878 --- /dev/null +++ b/src/controllers/api/contributeToDojoComponentController.ts @@ -0,0 +1,170 @@ +import type { TGuildDatabaseDocument } from "../../models/guildModel.ts"; +import { GuildMember } from "../../models/guildModel.ts"; +import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts"; +import { + addGuildMemberMiscItemContribution, + getDojoClient, + getGuildForRequestEx, + hasAccessToDojo, + processDojoBuildMaterialsGathered, + scaleRequiredCount, + setDojoRoomLogFunded +} from "../../services/guildService.ts"; +import { addMiscItems, getInventory, updateCurrency } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IDojoContributable, IGuildMemberDatabase } from "../../types/guildTypes.ts"; +import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; +import type { RequestHandler } from "express"; +import type { IDojoBuild } from "warframe-public-export-plus"; +import { ExportDojoRecipes } 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..d65930ce --- /dev/null +++ b/src/controllers/api/contributeToVaultController.ts @@ -0,0 +1,109 @@ +import type { TGuildDatabaseDocument, TGuildMemberDatabaseDocument } from "../../models/guildModel.ts"; +import { Alliance, Guild, GuildMember } from "../../models/guildModel.ts"; +import { + addGuildMemberMiscItemContribution, + addGuildMemberShipDecoContribution, + addVaultFusionTreasures, + addVaultMiscItems, + addVaultShipDecos, + getGuildForRequestEx +} from "../../services/guildService.ts"; +import { + addFusionTreasures, + addMiscItems, + addShipDecorations, + getInventory, + updateCurrency +} from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { ITypeCount } from "../../types/commonTypes.ts"; +import type { IFusionTreasure, IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { 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..48fdd5cb --- /dev/null +++ b/src/controllers/api/createAllianceController.ts @@ -0,0 +1,50 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { Alliance, AllianceMember, Guild, GuildMember } from "../../models/guildModel.ts"; +import { getAllianceClient } from "../../services/guildService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import type { 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 new file mode 100644 index 00000000..239b3318 --- /dev/null +++ b/src/controllers/api/createGuildController.ts @@ -0,0 +1,44 @@ +import type { RequestHandler } from "express"; +import { getAccountForRequest } from "../../services/loginService.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { Guild, GuildMember } from "../../models/guildModel.ts"; +import { createUniqueClanName, getGuildClient, giveClanKey } from "../../services/guildService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; + +export const createGuildController: RequestHandler = async (req, res) => { + 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: await createUniqueClanName(payload.guildName) + }); + await guild.save(); + + // Create guild member on database + await GuildMember.insertOne({ + accountId: account._id, + guildId: guild._id, + status: 0, + rank: 0 + }); + + const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes skipClanKeyCrafting"); + inventory.GuildId = guild._id; + const inventoryChanges: IInventoryChanges = {}; + giveClanKey(inventory, inventoryChanges); + await inventory.save(); + + res.json({ + ...(await getGuildClient(guild, account)), + InventoryChanges: inventoryChanges + }); +}; + +interface ICreateGuildRequest { + guildName: string; +} diff --git a/src/controllers/api/creditsController.ts b/src/controllers/api/creditsController.ts new file mode 100644 index 00000000..8b6a5474 --- /dev/null +++ b/src/controllers/api/creditsController.ts @@ -0,0 +1,32 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; + +export const creditsController: RequestHandler = async (req, res) => { + const inventory = ( + await Promise.all([ + getAccountIdForRequest(req), + getInventory( + req.query.accountId as string, + "RegularCredits TradesRemaining PremiumCreditsFree PremiumCredits infiniteCredits infinitePlatinum" + ) + ]) + )[1]; + + const response = { + RegularCredits: inventory.RegularCredits, + TradesRemaining: inventory.TradesRemaining, + PremiumCreditsFree: inventory.PremiumCreditsFree, + PremiumCredits: inventory.PremiumCredits + }; + + if (inventory.infiniteCredits) { + response.RegularCredits = 999999999; + } + if (inventory.infinitePlatinum) { + response.PremiumCreditsFree = 0; + response.PremiumCredits = 999999999; + } + + res.json(response); +}; diff --git a/src/controllers/api/crewMembersController.ts b/src/controllers/api/crewMembersController.ts new file mode 100644 index 00000000..6f95836d --- /dev/null +++ b/src/controllers/api/crewMembersController.ts @@ -0,0 +1,54 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { ICrewMemberClient } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { 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/crewShipFusionController.ts b/src/controllers/api/crewShipFusionController.ts new file mode 100644 index 00000000..a7b697c3 --- /dev/null +++ b/src/controllers/api/crewShipFusionController.ts @@ -0,0 +1,107 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { addMiscItems, freeUpSlot, getInventory, updateCurrency } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IOid } from "../../types/commonTypes.ts"; +import type { ICrewShipComponentFingerprint } from "../../types/inventoryTypes/inventoryTypes.ts"; +import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; +import type { RequestHandler } from "express"; +import { ExportCustoms, ExportDojoRecipes } from "warframe-public-export-plus"; + +export const crewShipFusionController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const payload = getJSONfromString(String(req.body)); + + const isWeapon = inventory.CrewShipWeapons.id(payload.PartA.$oid); + const itemA = isWeapon ?? inventory.CrewShipWeaponSkins.id(payload.PartA.$oid)!; + const category = isWeapon ? "CrewShipWeapons" : "CrewShipWeaponSkins"; + const salvageCategory = isWeapon ? "CrewShipSalvagedWeapons" : "CrewShipSalvagedWeaponSkins"; + const itemB = inventory[payload.SourceRecipe ? salvageCategory : category].id(payload.PartB.$oid)!; + const tierA = itemA.ItemType.charCodeAt(itemA.ItemType.length - 1) - 65; + const tierB = itemB.ItemType.charCodeAt(itemB.ItemType.length - 1) - 65; + + const inventoryChanges: IInventoryChanges = {}; + + // Charge partial repair cost if fusing with an identified but unrepaired part + if (payload.SourceRecipe) { + const recipe = ExportDojoRecipes.research[payload.SourceRecipe]; + updateCurrency(inventory, Math.round(recipe.price * 0.4), false, inventoryChanges); + const miscItemChanges = recipe.ingredients.map(x => ({ ...x, ItemCount: Math.round(x.ItemCount * -0.4) })); + addMiscItems(inventory, miscItemChanges); + inventoryChanges.MiscItems = miscItemChanges; + } + + // Remove inferior item + if (payload.SourceRecipe) { + inventory[salvageCategory].pull({ _id: payload.PartB.$oid }); + inventoryChanges.RemovedIdItems = [{ ItemId: payload.PartB }]; + } else { + const inferiorId = tierA < tierB ? payload.PartA : payload.PartB; + inventory[category].pull({ _id: inferiorId.$oid }); + inventoryChanges.RemovedIdItems = [{ ItemId: inferiorId }]; + freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS); + inventoryChanges[InventorySlot.RJ_COMPONENT_AND_ARMAMENTS] = { count: -1, platinum: 0, Slots: 1 }; + } + + // Upgrade superior item + const superiorItem = tierA < tierB ? itemB : itemA; + const inferiorItem = tierA < tierB ? itemA : itemB; + const fingerprint: ICrewShipComponentFingerprint = JSON.parse( + superiorItem.UpgradeFingerprint! + ) as ICrewShipComponentFingerprint; + const inferiorFingerprint: ICrewShipComponentFingerprint = inferiorItem.UpgradeFingerprint + ? (JSON.parse(inferiorItem.UpgradeFingerprint) as ICrewShipComponentFingerprint) + : { compat: "", buffs: [] }; + if (isWeapon) { + for (let i = 0; i != fingerprint.buffs.length; ++i) { + const buffA = fingerprint.buffs[i]; + const buffB = i < inferiorFingerprint.buffs.length ? inferiorFingerprint.buffs[i] : undefined; + const fvalA = buffA.Value / 0x3fffffff; + const fvalB = (buffB?.Value ?? 0) / 0x3fffffff; + const percA = 0.3 + fvalA * (0.6 - 0.3); + const percB = 0.3 + fvalB * (0.6 - 0.3); + const newPerc = Math.min(0.6, Math.max(percA, percB) * FUSE_MULTIPLIERS[Math.abs(tierA - tierB)]); + const newFval = (newPerc - 0.3) / (0.6 - 0.3); + buffA.Value = Math.trunc(newFval * 0x3fffffff); + } + } else { + const superiorMeta = ExportCustoms[superiorItem.ItemType].randomisedUpgrades ?? []; + const inferiorMeta = ExportCustoms[inferiorItem.ItemType].randomisedUpgrades ?? []; + for (let i = 0; i != inferiorFingerprint.buffs.length; ++i) { + const buffA = fingerprint.buffs[i]; + const buffB = inferiorFingerprint.buffs[i]; + const fvalA = buffA.Value / 0x3fffffff; + const fvalB = buffB.Value / 0x3fffffff; + const rangeA = superiorMeta[i].range; + const rangeB = inferiorMeta[i].range; + const percA = rangeA[0] + fvalA * (rangeA[1] - rangeA[0]); + const percB = rangeB[0] + fvalB * (rangeB[1] - rangeB[0]); + const newPerc = Math.min(rangeA[1], Math.max(percA, percB) * FUSE_MULTIPLIERS[Math.abs(tierA - tierB)]); + const newFval = (newPerc - rangeA[0]) / (rangeA[1] - rangeA[0]); + buffA.Value = Math.trunc(newFval * 0x3fffffff); + } + if (inferiorFingerprint.SubroutineIndex !== undefined) { + const useSuperiorSubroutine = tierA < tierB ? !payload.UseSubroutineA : payload.UseSubroutineA; + if (!useSuperiorSubroutine) { + fingerprint.SubroutineIndex = inferiorFingerprint.SubroutineIndex; + } + } + } + superiorItem.UpgradeFingerprint = JSON.stringify(fingerprint); + inventoryChanges[category] = [superiorItem.toJSON() as any]; + + await inventory.save(); + res.json({ + InventoryChanges: inventoryChanges + }); +}; + +interface ICrewShipFusionRequest { + PartA: IOid; + PartB: IOid; + SourceRecipe: string; + UseSubroutineA: boolean; +} + +const FUSE_MULTIPLIERS = [1.1, 1.05, 1.02]; diff --git a/src/controllers/api/crewShipIdentifySalvageController.ts b/src/controllers/api/crewShipIdentifySalvageController.ts new file mode 100644 index 00000000..317da572 --- /dev/null +++ b/src/controllers/api/crewShipIdentifySalvageController.ts @@ -0,0 +1,87 @@ +import { + addCrewShipSalvagedWeaponSkin, + addCrewShipRawSalvage, + getInventory, + addEquipment +} from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { RequestHandler } from "express"; +import type { + ICrewShipComponentFingerprint, + IInnateDamageFingerprint +} from "../../types/inventoryTypes/inventoryTypes.ts"; +import { ExportCustoms, ExportRailjackWeapons, ExportUpgrades } from "warframe-public-export-plus"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; +import { getRandomInt } from "../../services/rngService.ts"; +import type { IFingerprintStat } from "../../helpers/rivenHelper.ts"; +import type { IEquipmentDatabase } from "../../types/equipmentTypes.ts"; + +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..616407c3 --- /dev/null +++ b/src/controllers/api/customObstacleCourseLeaderboardController.ts @@ -0,0 +1,64 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { Guild } from "../../models/guildModel.ts"; +import { hasAccessToDojo, hasGuildPermission } from "../../services/guildService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountForRequest, getAccountIdForRequest } from "../../services/loginService.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import { logger } from "../../utils/logger.ts"; +import type { RequestHandler } from "express"; + +export const customObstacleCourseLeaderboardController: RequestHandler = async (req, res) => { + const data = getJSONfromString(String(req.body)); + const guild = (await Guild.findById(data.g, "DojoComponents Ranks"))!; + 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 if (req.query.act == "c") { + // TOVERIFY: What clan permission is actually needed for this? + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId LevelKeys"); + if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Decorator))) { + res.status(400).end(); + return; + } + + component.Leaderboard = undefined; + 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..afc7225c --- /dev/null +++ b/src/controllers/api/customizeGuildRanksController.ts @@ -0,0 +1,22 @@ +import { getGuildForRequest, hasGuildPermission } from "../../services/guildService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IGuildRank } from "../../types/guildTypes.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import type { 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..4c8953ed --- /dev/null +++ b/src/controllers/api/declineAllianceInviteController.ts @@ -0,0 +1,17 @@ +import { AllianceMember, GuildMember } from "../../models/guildModel.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { 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..4fca79bd --- /dev/null +++ b/src/controllers/api/declineGuildInviteController.ts @@ -0,0 +1,14 @@ +import { GuildMember } from "../../models/guildModel.ts"; +import { getAccountForRequest } from "../../services/loginService.ts"; +import type { 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/deleteSessionController.ts b/src/controllers/api/deleteSessionController.ts new file mode 100644 index 00000000..52967522 --- /dev/null +++ b/src/controllers/api/deleteSessionController.ts @@ -0,0 +1,9 @@ +import type { RequestHandler } from "express"; +import { deleteSession } from "../../managers/sessionManager.ts"; + +const deleteSessionController: RequestHandler = (_req, res) => { + deleteSession(_req.query.sessionId as string); + res.sendStatus(200); +}; + +export { deleteSessionController }; diff --git a/src/controllers/api/destroyDojoDecoController.ts b/src/controllers/api/destroyDojoDecoController.ts new file mode 100644 index 00000000..c47dfde3 --- /dev/null +++ b/src/controllers/api/destroyDojoDecoController.ts @@ -0,0 +1,51 @@ +import { + getDojoClient, + getGuildForRequestEx, + hasAccessToDojo, + hasGuildPermission, + refundDojoDeco, + removeDojoDeco +} from "../../services/guildService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import { logger } from "../../utils/logger.ts"; +import type { 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 | IClearObstacleCourseRequest; + if ("DecoType" in request) { + removeDojoDeco(guild, request.ComponentId, request.DecoId); + } else if (request.Act == "cObst") { + const component = guild.DojoComponents.id(request.ComponentId)!; + if (component.Decos) { + for (const deco of component.Decos) { + refundDojoDeco(guild, component, deco); + } + component.Decos.splice(0, component.Decos.length); + } + } else { + logger.error(`unhandled destroyDojoDeco request`, request); + } + + await guild.save(); + res.json(await getDojoClient(guild, 0, request.ComponentId)); +}; + +interface IDestroyDojoDecoRequest { + DecoType: string; + ComponentId: string; + DecoId: string; +} + +interface IClearObstacleCourseRequest { + ComponentId: string; + Act: "cObst" | "maybesomethingelsewedontknowabout"; +} diff --git a/src/controllers/api/divvyAllianceVaultController.ts b/src/controllers/api/divvyAllianceVaultController.ts new file mode 100644 index 00000000..0ebdcf67 --- /dev/null +++ b/src/controllers/api/divvyAllianceVaultController.ts @@ -0,0 +1,67 @@ +import { Alliance, AllianceMember, Guild, GuildMember } from "../../models/guildModel.ts"; +import { getAccountForRequest } from "../../services/loginService.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import { parallelForeach } from "../../utils/async-utils.ts"; +import { logger } from "../../utils/logger.ts"; +import type { 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..4bee8424 --- /dev/null +++ b/src/controllers/api/dojoComponentRushController.ts @@ -0,0 +1,83 @@ +import type { TGuildDatabaseDocument } from "../../models/guildModel.ts"; +import { GuildMember } from "../../models/guildModel.ts"; +import { + getDojoClient, + getGuildForRequestEx, + hasAccessToDojo, + scaleRequiredCount +} from "../../services/guildService.ts"; +import { getInventory, updateCurrency } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IDojoContributable } from "../../types/guildTypes.ts"; +import type { RequestHandler } from "express"; +import type { IDojoBuild } from "warframe-public-export-plus"; +import { ExportDojoRecipes } 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 new file mode 100644 index 00000000..913a2176 --- /dev/null +++ b/src/controllers/api/dojoController.ts @@ -0,0 +1,11 @@ +import type { 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 new file mode 100644 index 00000000..07cbaedd --- /dev/null +++ b/src/controllers/api/dronesController.ts @@ -0,0 +1,145 @@ +import { toMongoDate, toOid } from "../../helpers/inventoryHelpers.ts"; +import { addMiscItems, getInventory } from "../../services/inventoryService.ts"; +import { fromStoreItem } from "../../services/itemDataService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getRandomInt, getRandomWeightedRewardUc } from "../../services/rngService.ts"; +import type { IMongoDate, IOid } from "../../types/commonTypes.ts"; +import type { IDroneClient } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; +import type { RequestHandler } from "express"; +import { ExportDrones, ExportResources, ExportSystems } from "warframe-public-export-plus"; + +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 instantResourceExtractorDrones noResourceExtractorDronesDamage" + ); + const drone = inventory.Drones.id(req.query.droneId as string)!; + const droneMeta = ExportDrones[drone.ItemType]; + drone.DeployTime = inventory.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 = inventory.instantResourceExtractorDrones + ? new Date() + : new Date(Date.now() + getRandomInt(3 * 3600 * 1000, 4 * 3600 * 1000)); + drone.PendingDamage = + !inventory.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 = droneMeta.binCapacity * droneMeta.capacityMultipliers[resource.Rarity]; + } + 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`); + } +}; + +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 new file mode 100644 index 00000000..1bcdc06d --- /dev/null +++ b/src/controllers/api/endlessXpController.ts @@ -0,0 +1,534 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { combineInventoryChanges, getInventory } from "../../services/inventoryService.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import type { + IEndlessXpReward, + IInventoryClient, + TEndlessXpCategory +} from "../../types/inventoryTypes/inventoryTypes.ts"; +import { logger } from "../../utils/logger.ts"; +import type { ICountedStoreItem } from "warframe-public-export-plus"; +import { ExportRewards } from "warframe-public-export-plus"; +import { getRandomElement } from "../../services/rngService.ts"; +import { handleStoreItemAcquisition } from "../../services/purchaseService.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; + +export const endlessXpController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const payload = getJSONfromString(String(req.body)); + 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}`); + } +}; + +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..93f0dbb7 --- /dev/null +++ b/src/controllers/api/entratiLabConquestModeController.ts @@ -0,0 +1,52 @@ +import { toMongoDate } from "../../helpers/inventoryHelpers.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { getInventory, updateEntratiVault } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { 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)); + updateEntratiVault(inventory); + 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 new file mode 100644 index 00000000..9ed211c9 --- /dev/null +++ b/src/controllers/api/evolveWeaponController.ts @@ -0,0 +1,59 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { addMiscItems, getInventory } from "../../services/inventoryService.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import type { WeaponTypeInternal } from "../../services/itemDataService.ts"; +import { getRecipe } from "../../services/itemDataService.ts"; +import { EquipmentFeatures } from "../../types/equipmentTypes.ts"; + +export const evolveWeaponController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const payload = getJSONfromString(String(req.body)); + + const recipe = getRecipe(payload.Recipe)!; + if (payload.Action == "EWA_INSTALL") { + addMiscItems( + inventory, + recipe.ingredients.map(x => ({ ItemType: x.ItemType, ItemCount: x.ItemCount * -1 })) + ); + + const item = inventory[payload.Category].id(req.query.ItemId as string)!; + item.Features ??= 0; + item.Features |= EquipmentFeatures.INCARNON_GENESIS; + + item.SkillTree = "0"; + + inventory.EvolutionProgress ??= []; + if (!inventory.EvolutionProgress.find(entry => entry.ItemType == payload.EvoType)) { + inventory.EvolutionProgress.push({ + Progress: 0, + Rank: 1, + ItemType: payload.EvoType + }); + } + } else if (payload.Action == "EWA_UNINSTALL") { + addMiscItems(inventory, [ + { + ItemType: recipe.resultType, + ItemCount: 1 + } + ]); + + 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}`); + } + + await inventory.save(); + res.end(); +}; + +interface IEvolveWeaponRequest { + Action: string; + Category: WeaponTypeInternal; + Recipe: string; // e.g. "/Lotus/Types/Items/MiscItems/IncarnonAdapters/UnlockerBlueprints/DespairIncarnonBlueprint" + UninstallRecipe: ""; + EvoType: string; // e.g. "/Lotus/Weapons/Tenno/ThrowingWeapons/StalkerKunai" +} diff --git a/src/controllers/api/findSessionsController.ts b/src/controllers/api/findSessionsController.ts new file mode 100644 index 00000000..5dd819f7 --- /dev/null +++ b/src/controllers/api/findSessionsController.ts @@ -0,0 +1,28 @@ +import type { RequestHandler } from "express"; +import { getSession } from "../../managers/sessionManager.ts"; +import { logger } from "../../utils/logger.ts"; +import type { IFindSessionRequest } from "../../types/session.ts"; + +export const findSessionsController: RequestHandler = (_req, res) => { + const req = JSON.parse(String(_req.body)) as IFindSessionRequest; + logger.debug("FindSession Request ", req); + if (req.id != undefined) { + logger.debug("Found ID"); + const session = getSession(req.id); + + if (session.length) res.json({ queryId: req.queryId, Sessions: session }); + else res.json({}); + } else if (req.originalSessionId != undefined) { + logger.debug("Found OriginalSessionID"); + + const session = getSession(req.originalSessionId); + if (session.length) res.json({ queryId: req.queryId, Sessions: session }); + else res.json({}); + } else { + logger.debug("Found SessionRequest"); + + const session = getSession(req); + if (session.length) res.json({ queryId: req.queryId, Sessions: session }); + else res.json({}); + } +}; diff --git a/src/controllers/api/fishmongerController.ts b/src/controllers/api/fishmongerController.ts new file mode 100644 index 00000000..85502d0b --- /dev/null +++ b/src/controllers/api/fishmongerController.ts @@ -0,0 +1,46 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { addMiscItems, addStanding, getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { RequestHandler } from "express"; +import { ExportResources } from "warframe-public-export-plus"; + +export const fishmongerController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const body = getJSONfromString(String(req.body)); + const miscItemChanges: IMiscItem[] = []; + let syndicateTag: string | undefined; + let gainedStanding = 0; + for (const fish of body.Fish) { + const fishData = ExportResources[fish.ItemType]; + if (req.query.dissect == "1") { + for (const part of fishData.dissectionParts!) { + const partItem = miscItemChanges.find(x => x.ItemType == part.ItemType); + if (partItem) { + partItem.ItemCount += part.ItemCount * fish.ItemCount; + } else { + miscItemChanges.push({ ItemType: part.ItemType, ItemCount: part.ItemCount * fish.ItemCount }); + } + } + } else { + syndicateTag = fishData.syndicateTag!; + gainedStanding += fishData.standingBonus! * fish.ItemCount; + } + miscItemChanges.push({ ItemType: fish.ItemType, ItemCount: fish.ItemCount * -1 }); + } + addMiscItems(inventory, miscItemChanges); + if (gainedStanding && syndicateTag) addStanding(inventory, syndicateTag, gainedStanding); + await inventory.save(); + res.json({ + InventoryChanges: { + MiscItems: miscItemChanges + }, + SyndicateTag: syndicateTag, + StandingChange: gainedStanding + }); +}; + +interface IFishmongerRequest { + Fish: IMiscItem[]; +} diff --git a/src/controllers/api/focusController.ts b/src/controllers/api/focusController.ts new file mode 100644 index 00000000..415f84e0 --- /dev/null +++ b/src/controllers/api/focusController.ts @@ -0,0 +1,242 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getInventory, addMiscItems, addEquipment, occupySlot } from "../../services/inventoryService.ts"; +import type { IMiscItem, TFocusPolarity, TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts"; +import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts"; +import { logger } from "../../utils/logger.ts"; +import { ExportFocusUpgrades } from "warframe-public-export-plus"; +import { Inventory } from "../../models/inventoryModels/inventoryModel.ts"; + +export const focusController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + switch (req.query.op) { + default: + logger.error("Unhandled focus op type: " + String(req.query.op)); + logger.debug(String(req.body)); + res.end(); + break; + case FocusOperation.InstallLens: { + const request = JSON.parse(String(req.body)) as ILensInstallRequest; + const inventory = await getInventory(accountId); + 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({ + weaponId: request.WeaponId, + lensType: request.LensType + }); + break; + } + case FocusOperation.UnlockWay: { + const focusType = (JSON.parse(String(req.body)) as IWayRequest).FocusType; + const focusPolarity = focusTypeToPolarity(focusType); + const inventory = await getInventory(accountId, "FocusAbility FocusUpgrades FocusXP"); + const cost = inventory.FocusAbility ? 50_000 : 0; + inventory.FocusAbility ??= focusType; + inventory.FocusUpgrades.push({ ItemType: focusType }); + if (cost) { + inventory.FocusXP![focusPolarity]! -= cost; + } + await inventory.save(); + res.json({ + FocusUpgrade: { ItemType: focusType }, + FocusPointCosts: { [focusPolarity]: cost } + }); + break; + } + case FocusOperation.ActivateWay: { + const focusType = (JSON.parse(String(req.body)) as IWayRequest).FocusType; + + await Inventory.updateOne( + { + accountOwnerId: accountId + }, + { + FocusAbility: focusType + } + ); + + res.json({ + FocusUpgrade: { ItemType: focusType } + }); + break; + } + case FocusOperation.UnlockUpgrade: { + const request = JSON.parse(String(req.body)) as IUnlockUpgradeRequest; + const focusPolarity = focusTypeToPolarity(request.FocusTypes[0]); + const inventory = await getInventory(accountId); + let cost = 0; + for (const focusType of request.FocusTypes) { + cost += ExportFocusUpgrades[focusType].baseFocusPointCost; + inventory.FocusUpgrades.push({ ItemType: focusType, Level: 0 }); + } + inventory.FocusXP![focusPolarity]! -= cost; + await inventory.save(); + res.json({ + FocusTypes: request.FocusTypes, + FocusPointCosts: { [focusPolarity]: cost } + }); + break; + } + case FocusOperation.LevelUpUpgrade: { + const request = JSON.parse(String(req.body)) as ILevelUpUpgradeRequest; + const focusPolarity = focusTypeToPolarity(request.FocusInfos[0].ItemType); + const inventory = await getInventory(accountId); + let cost = 0; + for (const focusUpgrade of request.FocusInfos) { + cost += focusUpgrade.FocusXpCost; + const focusUpgradeDb = inventory.FocusUpgrades.find(entry => entry.ItemType == focusUpgrade.ItemType)!; + focusUpgradeDb.Level = focusUpgrade.Level; + } + inventory.FocusXP![focusPolarity]! -= cost; + await inventory.save(); + res.json({ + FocusInfos: request.FocusInfos, + FocusPointCosts: { [focusPolarity]: cost } + }); + break; + } + case FocusOperation.SentTrainingAmplifier: { + const request = JSON.parse(String(req.body)) as ISentTrainingAmplifierRequest; + const inventory = await getInventory(accountId); + 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![0]); + break; + } + case FocusOperation.UnbindUpgrade: { + const request = JSON.parse(String(req.body)) as IUnbindUpgradeRequest; + const focusPolarity = focusTypeToPolarity(request.FocusTypes[0]); + const inventory = await getInventory(accountId); + inventory.FocusXP![focusPolarity]! -= 750_000 * request.FocusTypes.length; + addMiscItems(inventory, [ + { + ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantItem", + ItemCount: request.FocusTypes.length * -1 + } + ]); + request.FocusTypes.forEach(type => { + const focusUpgradeDb = inventory.FocusUpgrades.find(entry => entry.ItemType == type)!; + focusUpgradeDb.IsUniversal = true; + }); + await inventory.save(); + res.json({ + FocusTypes: request.FocusTypes, + FocusPointCosts: { + [focusPolarity]: 750_000 * request.FocusTypes.length + }, + MiscItemCosts: [ + { + ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantItem", + ItemCount: request.FocusTypes.length + } + ] + }); + break; + } + case FocusOperation.ConvertShard: { + const request = JSON.parse(String(req.body)) as IConvertShardRequest; + // Tally XP + let xp = 0; + for (const shard of request.Shards) { + xp += shardValues[shard.ItemType as keyof typeof shardValues] * shard.ItemCount; + } + // Send response + res.json({ + FocusPointGains: { + [request.Polarity]: xp + }, + MiscItemCosts: request.Shards + }); + // Commit added XP and removed shards to DB + for (const shard of request.Shards) { + shard.ItemCount *= -1; + } + const inventory = await getInventory(accountId); + const polarity = request.Polarity; + inventory.FocusXP ??= {}; + inventory.FocusXP[polarity] ??= 0; + inventory.FocusXP[polarity] += xp; + addMiscItems(inventory, request.Shards); + await inventory.save(); + break; + } + } +}; + +enum FocusOperation { + InstallLens = "1", + UnlockWay = "2", + UnlockUpgrade = "3", + LevelUpUpgrade = "4", + ActivateWay = "5", + SentTrainingAmplifier = "7", + UnbindUpgrade = "8", + ConvertShard = "9" +} + +// For UnlockWay & ActivateWay +interface IWayRequest { + FocusType: string; +} + +interface IUnlockUpgradeRequest { + FocusTypes: string[]; +} + +interface ILevelUpUpgradeRequest { + FocusInfos: { + ItemType: string; + FocusXpCost: number; + IsUniversal: boolean; + Level: number; + IsActiveAbility: boolean; + }[]; +} + +interface IUnbindUpgradeRequest { + ShardTypes: string[]; + FocusTypes: string[]; +} + +interface IConvertShardRequest { + Shards: IMiscItem[]; + Polarity: TFocusPolarity; +} + +interface ISentTrainingAmplifierRequest { + StartingWeaponType: string; +} + +interface ILensInstallRequest { + LensType: string; + Category: TEquipmentKey; + WeaponId: string; +} + +// Works for ways & upgrades +const focusTypeToPolarity = (type: string): TFocusPolarity => { + return ("AP_" + type.substr(1).split("/")[3].toUpperCase()) as TFocusPolarity; +}; + +const shardValues = { + "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardCommonItem": 2_500, + "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardSynthesizedItem": 5_000, + "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantItem": 25_000, + "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantTierTwoItem": 40_000 +}; diff --git a/src/controllers/api/fusionTreasuresController.ts b/src/controllers/api/fusionTreasuresController.ts new file mode 100644 index 00000000..86e33034 --- /dev/null +++ b/src/controllers/api/fusionTreasuresController.ts @@ -0,0 +1,44 @@ +import type { RequestHandler } from "express"; +import { ExportResources } from "warframe-public-export-plus"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { addFusionTreasures, addMiscItems, getInventory } from "../../services/inventoryService.ts"; +import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts"; +import { parseFusionTreasure } from "../../helpers/inventoryHelpers.ts"; + +interface IFusionTreasureRequest { + oldTreasureName: string; + newTreasureName: string; +} + +export const fusionTreasuresController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + 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); + const fusionTreasureChanges = [oldTreasure, newTreasure]; + addFusionTreasures(inventory, fusionTreasureChanges); + + // Remove consumed stars + const miscItemChanges: IMiscItem[] = []; + const filledSockets = newTreasure.Sockets & ~oldTreasure.Sockets; + for (let i = 0; filledSockets >> i; ++i) { + if ((filledSockets >> i) & 1) { + //console.log("Socket", i, "has been filled with", ExportResources[oldTreasure.ItemType].sockets![i]); + miscItemChanges.push({ + ItemType: ExportResources[oldTreasure.ItemType].sockets![i], + ItemCount: -1 + }); + } + } + addMiscItems(inventory, miscItemChanges); + + await inventory.save(); + // 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..e98c8a6a --- /dev/null +++ b/src/controllers/api/gardeningController.ts @@ -0,0 +1,83 @@ +import { toMongoDate } from "../../helpers/inventoryHelpers.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { addMiscItem, getInventory } from "../../services/inventoryService.ts"; +import { toStoreItem } from "../../services/itemDataService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { createGarden, getPersonalRooms } from "../../services/personalRoomsService.ts"; +import type { IMongoDate } from "../../types/commonTypes.ts"; +import type { IMissionReward } from "../../types/missionTypes.ts"; +import type { IGardeningClient, IPersonalRoomsClient } from "../../types/personalRoomsTypes.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; +import type { 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 new file mode 100644 index 00000000..3a602661 --- /dev/null +++ b/src/controllers/api/genericUpdateController.ts @@ -0,0 +1,16 @@ +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { updateGeneric } from "../../services/inventoryService.ts"; +import type { RequestHandler } from "express"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import type { IGenericUpdate } from "../../types/genericUpdate.ts"; + +// This endpoint used to be /api/genericUpdate.php, but sometime around the Jade Shadows update, it was changed to /api/updateNodeIntros.php. +// SpaceNinjaServer supports both endpoints right now. + +const genericUpdateController: RequestHandler = async (request, response) => { + const accountId = await getAccountIdForRequest(request); + const update = getJSONfromString(String(request.body)); + response.json(await updateGeneric(update, accountId)); +}; + +export { genericUpdateController }; diff --git a/src/controllers/api/getAllianceController.ts b/src/controllers/api/getAllianceController.ts new file mode 100644 index 00000000..928a06e4 --- /dev/null +++ b/src/controllers/api/getAllianceController.ts @@ -0,0 +1,26 @@ +import { Alliance, Guild } from "../../models/guildModel.ts"; +import { getAllianceClient } from "../../services/guildService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { RequestHandler } from "express"; + +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(); +}; + +// POST request since U27 +/*interface IGetAllianceRequest { + memberCount: number; + clanLeaderName: string; + clanLeaderId: string; +}*/ diff --git a/src/controllers/api/getDailyDealStockLevelsController.ts b/src/controllers/api/getDailyDealStockLevelsController.ts new file mode 100644 index 00000000..b53d8fbc --- /dev/null +++ b/src/controllers/api/getDailyDealStockLevelsController.ts @@ -0,0 +1,10 @@ +import { DailyDeal } from "../../models/worldStateModel.ts"; +import type { RequestHandler } from "express"; + +export const getDailyDealStockLevelsController: RequestHandler = async (req, res) => { + const dailyDeal = (await DailyDeal.findOne({ StoreItem: req.query.productName }, "AmountSold"))!; + res.json({ + StoreItem: req.query.productName, + AmountSold: dailyDeal.AmountSold + }); +}; diff --git a/src/controllers/api/getFriendsController.ts b/src/controllers/api/getFriendsController.ts new file mode 100644 index 00000000..797b44b9 --- /dev/null +++ b/src/controllers/api/getFriendsController.ts @@ -0,0 +1,54 @@ +import { toOid } from "../../helpers/inventoryHelpers.ts"; +import { Friendship } from "../../models/friendModel.ts"; +import { addAccountDataToFriendInfo, addInventoryDataToFriendInfo } from "../../services/friendService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IFriendInfo } from "../../types/friendTypes.ts"; +import type { Request, RequestHandler, Response } from "express"; + +// 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); +}; + +// 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..201a3767 --- /dev/null +++ b/src/controllers/api/getGuildContributionsController.ts @@ -0,0 +1,19 @@ +import { GuildMember } from "../../models/guildModel.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IGuildMemberClient } from "../../types/guildTypes.ts"; +import type { 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 new file mode 100644 index 00000000..a8003a33 --- /dev/null +++ b/src/controllers/api/getGuildController.ts @@ -0,0 +1,32 @@ +import type { RequestHandler } from "express"; +import { Guild } from "../../models/guildModel.ts"; +import { getAccountForRequest } from "../../services/loginService.ts"; +import { logger } from "../../utils/logger.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { createUniqueClanName, getGuildClient } from "../../services/guildService.ts"; + +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.findById(inventory.GuildId); + if (guild) { + // 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.end(); +}; diff --git a/src/controllers/api/getGuildDojoController.ts b/src/controllers/api/getGuildDojoController.ts new file mode 100644 index 00000000..05d4685c --- /dev/null +++ b/src/controllers/api/getGuildDojoController.ts @@ -0,0 +1,35 @@ +import type { RequestHandler } from "express"; +import { Types } from "mongoose"; +import { Guild } from "../../models/guildModel.ts"; +import { getDojoClient } from "../../services/guildService.ts"; +import { Account } from "../../models/loginModel.ts"; + +export const getGuildDojoController: RequestHandler = async (req, res) => { + const guildId = req.query.guildId as string; + + const guild = await Guild.findById(guildId); + if (!guild) { + res.status(404).end(); + return; + } + + // Populate dojo info if not present + 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(); + } + + 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/getGuildEventScoreController.ts b/src/controllers/api/getGuildEventScoreController.ts new file mode 100644 index 00000000..a0b229b5 --- /dev/null +++ b/src/controllers/api/getGuildEventScoreController.ts @@ -0,0 +1,26 @@ +import type { RequestHandler } from "express"; +import { getAccountForRequest } from "../../services/loginService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { Guild } from "../../models/guildModel.ts"; + +export const getGuildEventScoreController: 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 goalId = req.query.goalId as string; + if (guild && guild.GoalProgress && goalId) { + const goal = guild.GoalProgress.find(x => x.goalId.toString() == goalId); + if (goal) { + res.json({ + Tier: guild.Tier, + GoalProgress: { + Count: goal.Count, + Tag: goal.Tag, + _id: { $oid: goal.goalId } + } + }); + return; + } + } + res.json({}); +}; diff --git a/src/controllers/api/getGuildLogController.ts b/src/controllers/api/getGuildLogController.ts new file mode 100644 index 00000000..0e5daf1f --- /dev/null +++ b/src/controllers/api/getGuildLogController.ts @@ -0,0 +1,60 @@ +import { toMongoDate } from "../../helpers/inventoryHelpers.ts"; +import { Guild } from "../../models/guildModel.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IMongoDate } from "../../types/commonTypes.ts"; +import type { RequestHandler } from "express"; + +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 new file mode 100644 index 00000000..de46cfec --- /dev/null +++ b/src/controllers/api/getIgnoredUsersController.ts @@ -0,0 +1,20 @@ +import { toOid } from "../../helpers/inventoryHelpers.ts"; +import { Account, Ignore } from "../../models/loginModel.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IFriendInfo } from "../../types/friendTypes.ts"; +import { parallelForeach } from "../../utils/async-utils.ts"; +import type { RequestHandler } from "express"; + +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.json({ IgnoredUsers: ignoredUsers }); +}; diff --git a/src/controllers/api/getNewRewardSeedController.ts b/src/controllers/api/getNewRewardSeedController.ts new file mode 100644 index 00000000..b069a07e --- /dev/null +++ b/src/controllers/api/getNewRewardSeedController.ts @@ -0,0 +1,19 @@ +import { Inventory } from "../../models/inventoryModels/inventoryModel.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { generateRewardSeed } from "../../services/rngService.ts"; +import type { RequestHandler } from "express"; + +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 }); +}; diff --git a/src/controllers/api/getPastWeeklyChallengesController.ts b/src/controllers/api/getPastWeeklyChallengesController.ts new file mode 100644 index 00000000..806198c8 --- /dev/null +++ b/src/controllers/api/getPastWeeklyChallengesController.ts @@ -0,0 +1,62 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { EPOCH, getSeasonChallengePools, getWorldState, pushWeeklyActs } from "../../services/worldStateService.ts"; +import { unixTimesInMs } from "../../constants/timeConstants.ts"; +import type { ISeasonChallenge } from "../../types/worldStateTypes.ts"; +import { ExportChallenges } from "warframe-public-export-plus"; + +export const getPastWeeklyChallengesController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "SeasonChallengeHistory ChallengeProgress"); + const worldState = getWorldState(undefined); + + if (worldState.SeasonInfo) { + const pools = getSeasonChallengePools(worldState.SeasonInfo.AffiliationTag); + const nightwaveStartTimestamp = Number(worldState.SeasonInfo.Activation.$date.$numberLong); + const nightwaveSeason = worldState.SeasonInfo.Season; + const timeMs = worldState.Time * 1000; + const completedChallengesIds = new Set(); + + inventory.SeasonChallengeHistory.forEach(challengeHistory => { + const entryNightwaveSeason = parseInt(challengeHistory.id.slice(0, 4), 10) - 1; + if (nightwaveSeason == entryNightwaveSeason) { + const meta = Object.entries(ExportChallenges).find( + ([key]) => key.split("/").pop() === challengeHistory.challenge + ); + if (meta) { + const [, challengeMeta] = meta; + const challengeProgress = inventory.ChallengeProgress.find( + c => c.Name === challengeHistory.challenge + ); + + if (challengeProgress && challengeProgress.Progress >= (challengeMeta.requiredCount ?? 1)) { + completedChallengesIds.add(challengeHistory.id); + } + } + } + }); + + const PastWeeklyChallenges: ISeasonChallenge[] = []; + + let week = Math.trunc((timeMs - EPOCH) / unixTimesInMs.week) - 1; + + while (EPOCH + week * unixTimesInMs.week >= nightwaveStartTimestamp && PastWeeklyChallenges.length < 3) { + const tempActs: ISeasonChallenge[] = []; + pushWeeklyActs(tempActs, pools, week, nightwaveStartTimestamp, nightwaveSeason); + + for (const act of tempActs) { + if (!completedChallengesIds.has(act._id.$oid) && PastWeeklyChallenges.length < 3) { + if (act.Challenge.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")) { + act.Permanent = true; + } + PastWeeklyChallenges.push(act); + } + } + + week--; + } + + res.json({ PastWeeklyChallenges: PastWeeklyChallenges }); + } +}; diff --git a/src/controllers/api/getShipController.ts b/src/controllers/api/getShipController.ts new file mode 100644 index 00000000..3166655c --- /dev/null +++ b/src/controllers/api/getShipController.ts @@ -0,0 +1,39 @@ +import type { RequestHandler } from "express"; +import { config } from "../../services/configService.ts"; +import allShipFeatures from "../../../static/fixed_responses/allShipFeatures.json" with { type: "json" }; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { createGarden, getPersonalRooms } from "../../services/personalRoomsService.ts"; +import type { IGetShipResponse, IPersonalRoomsClient } from "../../types/personalRoomsTypes.ts"; +import { getLoadout } from "../../services/loadoutService.ts"; +import { toOid } from "../../helpers/inventoryHelpers.ts"; + +export const getShipController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const personalRoomsDb = await getPersonalRooms(accountId); + + // 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 getShipResponse: IGetShipResponse = { + ShipOwnerId: accountId, + LoadOutInventory: { LoadOutPresets: loadout.toJSON() }, + Ship: { + ...personalRooms.Ship, + ShipId: toOid(personalRoomsDb.activeShipId) + }, + Apartment: personalRooms.Apartment, + TailorShop: personalRooms.TailorShop + }; + + if (config.unlockAllShipFeatures) { + getShipResponse.Ship.Features = allShipFeatures; + } + + res.json(getShipResponse); +}; diff --git a/src/controllers/api/getVendorInfoController.ts b/src/controllers/api/getVendorInfoController.ts new file mode 100644 index 00000000..52a3f5cb --- /dev/null +++ b/src/controllers/api/getVendorInfoController.ts @@ -0,0 +1,29 @@ +import type { RequestHandler } from "express"; +import { applyStandingToVendorManifest, getVendorManifestByTypeName } from "../../services/serversideVendorsService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { config } from "../../services/configService.ts"; + +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); + if (config.dev?.keepVendorsExpired) { + manifest = { + VendorInfo: { + ...manifest.VendorInfo, + Expiry: { $date: { $numberLong: "0" } } + } + }; + } + } + + res.json(manifest); +}; diff --git a/src/controllers/api/getVoidProjectionRewardsController.ts b/src/controllers/api/getVoidProjectionRewardsController.ts new file mode 100644 index 00000000..46e960e0 --- /dev/null +++ b/src/controllers/api/getVoidProjectionRewardsController.ts @@ -0,0 +1,38 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { crackRelic } from "../../helpers/relicHelper.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IVoidTearParticipantInfo } from "../../types/requestTypes.ts"; +import type { RequestHandler } from "express"; + +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 + }; + res.json(response); +}; + +interface IVoidProjectionRewardRequest { + CurrentWave: number; + ParticipantInfo: IVoidTearParticipantInfo; + VoidTier: string; + DifficultyTier: number; + VoidProjectionRemovalHash: string; +} + +interface IVoidProjectionRewardResponse { + CurrentWave: number; + ParticipantInfo: IVoidTearParticipantInfo; + DifficultyTier: number; +} diff --git a/src/controllers/api/giftingController.ts b/src/controllers/api/giftingController.ts new file mode 100644 index 00000000..9de09482 --- /dev/null +++ b/src/controllers/api/giftingController.ts @@ -0,0 +1,126 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { Account } from "../../models/loginModel.ts"; +import { areFriends } from "../../services/friendService.ts"; +import { createMessage } from "../../services/inboxService.ts"; +import { + combineInventoryChanges, + getEffectiveAvatarImageType, + getInventory, + updateCurrency +} from "../../services/inventoryService.ts"; +import { getAccountForRequest, getSuffixedName } from "../../services/loginService.ts"; +import { handleDailyDealPurchase, handleStoreItemAcquisition } from "../../services/purchaseService.ts"; +import type { IOid } from "../../types/commonTypes.ts"; +import type { IPurchaseParams, IPurchaseResponse } from "../../types/purchaseTypes.ts"; +import { PurchaseSource } from "../../types/purchaseTypes.ts"; +import type { RequestHandler } from "express"; +import { ExportBundles, ExportFlavour } from "warframe-public-export-plus"; + +const checkPurchaseParams = (params: IPurchaseParams): boolean => { + switch (params.Source) { + case PurchaseSource.Market: + return params.UsePremium; + + case PurchaseSource.DailyDeal: + return true; + } + return false; +}; + +export const giftingController: RequestHandler = async (req, res) => { + const data = getJSONfromString(String(req.body)); + if (!checkPurchaseParams(data.PurchaseParams)) { + 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 response: IPurchaseResponse = { + InventoryChanges: {} + }; + if (data.PurchaseParams.Source == PurchaseSource.DailyDeal) { + await handleDailyDealPurchase(senderInventory, data.PurchaseParams, response); + } else { + updateCurrency(senderInventory, data.PurchaseParams.ExpectedPrice, true, response.InventoryChanges); + } + if (data.PurchaseParams.StoreItem in ExportBundles) { + const bundle = ExportBundles[data.PurchaseParams.StoreItem]; + if (bundle.giftingBonus) { + combineInventoryChanges( + response.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(response); +}; + +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 new file mode 100644 index 00000000..0bd921b3 --- /dev/null +++ b/src/controllers/api/gildWeaponController.ts @@ -0,0 +1,79 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { sendWsBroadcastTo } from "../../services/wsService.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { addMiscItems, getInventory } from "../../services/inventoryService.ts"; +import type { TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { ArtifactPolarity } from "../../types/inventoryTypes/commonInventoryTypes.ts"; +import { ExportRecipes } from "warframe-public-export-plus"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; +import type { IEquipmentClient } from "../../types/equipmentTypes.ts"; +import { EquipmentFeatures } from "../../types/equipmentTypes.ts"; + +interface IGildWeaponRequest { + ItemName: string; + Recipe: string; // e.g. /Lotus/Weapons/SolarisUnited/LotusGildKitgunBlueprint + PolarizeSlot?: number; + PolarizeValue?: ArtifactPolarity; + ItemId: string; + Category: TEquipmentKey; +} + +export const gildWeaponController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const data = getJSONfromString(String(req.body)); + data.ItemId = String(req.query.ItemId); + data.Category = req.query.Category as TEquipmentKey; + + const inventory = await getInventory(accountId); + const weaponIndex = inventory[data.Category].findIndex(x => String(x._id) === data.ItemId); + if (weaponIndex === -1) { + throw new Error(`Weapon with ${data.ItemId} not found in category ${String(req.query.Category)}`); + } + + const weapon = inventory[data.Category][weaponIndex]; + 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 = [ + { + Slot: data.PolarizeSlot, + Value: data.PolarizeValue + } + ]; + } + inventory[data.Category][weaponIndex] = weapon; + const inventoryChanges: IInventoryChanges = {}; + inventoryChanges[data.Category] = [weapon.toJSON()]; + + 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 + }); + sendWsBroadcastTo(accountId, { update_inventory: true }); +}; diff --git a/src/controllers/api/giveKeyChainTriggeredItemsController.ts b/src/controllers/api/giveKeyChainTriggeredItemsController.ts new file mode 100644 index 00000000..22d2fccc --- /dev/null +++ b/src/controllers/api/giveKeyChainTriggeredItemsController.ts @@ -0,0 +1,17 @@ +import type { RequestHandler } from "express"; +import { parseString } from "../../helpers/general.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { giveKeyChainItem } from "../../services/questService.ts"; +import type { IKeyChainRequest } from "../../types/requestTypes.ts"; + +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 giveKeyChainItem(inventory, keyChainInfo); + await inventory.save(); + + res.send(inventoryChanges); +}; diff --git a/src/controllers/api/giveKeyChainTriggeredMessageController.ts b/src/controllers/api/giveKeyChainTriggeredMessageController.ts new file mode 100644 index 00000000..e4d243be --- /dev/null +++ b/src/controllers/api/giveKeyChainTriggeredMessageController.ts @@ -0,0 +1,16 @@ +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { giveKeyChainMessage } from "../../services/questService.ts"; +import type { IKeyChainRequest } from "../../types/requestTypes.ts"; +import type { 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 inventory = await getInventory(accountId, "QuestKeys"); + await giveKeyChainMessage(inventory, accountId, keyChainInfo); + await inventory.save(); + + res.send(1); +}; diff --git a/src/controllers/api/giveQuestKeyRewardController.ts b/src/controllers/api/giveQuestKeyRewardController.ts new file mode 100644 index 00000000..9cbdfd40 --- /dev/null +++ b/src/controllers/api/giveQuestKeyRewardController.ts @@ -0,0 +1,45 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { addItem, getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IOid } from "../../types/commonTypes.ts"; +import type { RequestHandler } from "express"; + +export const giveQuestKeyRewardController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const rewardRequest = getJSONfromString((req.body as Buffer).toString()); + + if (Array.isArray(rewardRequest.reward)) { + throw new Error("Multiple rewards not expected"); + } + + const reward = rewardRequest.reward; + const inventory = await getInventory(accountId); + const inventoryChanges = await addItem(inventory, reward.ItemType, reward.Amount); + await inventory.save(); + res.json(inventoryChanges); + //TODO: consider whishlist changes +}; + +interface IQuestKeyRewardRequest { + reward: IQuestKeyReward; +} + +interface IQuestKeyReward { + RewardType: string; + CouponType: string; + Icon: string; + ItemType: string; + StoreItemType: string; + ProductCategory: string; + Amount: number; + ScalingMultiplier: number; + Durability: string; + DisplayName: string; + Duration: number; + CouponSku: number; + Syndicate: string; + //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..a61d27cf --- /dev/null +++ b/src/controllers/api/giveShipDecoAndLoreFragmentController.ts @@ -0,0 +1,21 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { addLoreFragmentScans, addShipDecorations, getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { ITypeCount } from "../../types/commonTypes.ts"; +import type { ILoreFragmentScan } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { 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..cd964ef1 --- /dev/null +++ b/src/controllers/api/giveStartingGearController.ts @@ -0,0 +1,16 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { addStartingGear, getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { TPartialStartingGear } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { 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 new file mode 100644 index 00000000..84bce3b2 --- /dev/null +++ b/src/controllers/api/guildTechController.ts @@ -0,0 +1,465 @@ +import type { RequestHandler } from "express"; +import { + addGuildMemberMiscItemContribution, + getGuildForRequestEx, + getGuildVault, + hasAccessToDojo, + hasGuildPermission, + processCompletedGuildTechProject, + processFundedGuildTechProject, + processGuildTechProjectContributionsUpdate, + removePigmentsFromGuildMembers, + scaleRequiredCount, + setGuildTechLogState +} from "../../services/guildService.ts"; +import { ExportDojoRecipes, ExportRailjackWeapons } from "warframe-public-export-plus"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { + addCrewShipWeaponSkin, + addEquipment, + addItem, + addMiscItems, + addRecipes, + combineInventoryChanges, + getInventory, + occupySlot, + updateCurrency +} from "../../services/inventoryService.ts"; +import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts"; +import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; +import { config } from "../../services/configService.ts"; +import type { ITechProjectClient } from "../../types/guildTypes.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import { GuildMember } from "../../models/guildModel.ts"; +import { toMongoDate, toOid } from "../../helpers/inventoryHelpers.ts"; +import { logger } from "../../utils/logger.ts"; +import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts"; + +export const guildTechController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const data = JSON.parse(String(req.body)) as TGuildTechRequest; + 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() && + setGuildTechLogState(guild, project.ItemType, 4, project.CompletionDate) + ) { + processCompletedGuildTechProject(guild, project.ItemType); + needSave = true; + } + } + 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 (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) { + const contributedItems = ingredient.ItemCount - reqItem.ItemCount; + inventoryChanges.MiscItems.push({ + ItemType: ingredient.ItemType, + ItemCount: contributedItems + }); + } + } + addMiscItems(inventory, inventoryChanges.MiscItems); + + await inventory.save(); + res.json({ + action: "Cancel", + isPersonal: true, + inventoryChanges: inventoryChanges, + personalTech: { + ItemId: toOid(personalTechProject._id) + } + }); + } 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: "/Lotus/Types/Items/MiscItems/InstantSalvageRepairItem", + ItemCount: -1 + } + ]; + addMiscItems(inventory, inventoryChanges.MiscItems); + await inventory.save(); + res.json({ + inventoryChanges: inventoryChanges + }); + } else { + logger.debug(`data provided to ${req.path}: ${String(req.body)}`); + throw new Error(`unhandled guildTech request`); + } +}; + +type TGuildTechRequest = + | { Action: "Sync" | "SomethingElseThatWeMightNotKnowAbout" } + | IGuildTechBasicRequest + | IGuildTechContributeRequest; + +interface IGuildTechBasicRequest { + Action: "Start" | "Fabricate" | "Pause" | "Unpause" | "Cancel" | "Rush" | "InstantFinish"; + Mode: "Guild" | "Personal"; + RecipeType: string; + TechProductCategory?: string; + CategoryItemId?: string; +} + +interface IGuildTechBuyRequest extends Omit { + Action: string; +} + +interface IGuildTechContributeRequest { + Action: "Contribute"; + ResearchId: string; + RecipeType: string; + RegularCredits: number; + MiscItems: IMiscItem[]; + VaultCredits: number; + VaultMiscItems: IMiscItem[]; +} + +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, { + UpgradeType: ExportRailjackWeapons[salvageItem.ItemType].defaultUpgrades?.[0].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 new file mode 100644 index 00000000..c31fa447 --- /dev/null +++ b/src/controllers/api/hostSessionController.ts @@ -0,0 +1,24 @@ +import type { RequestHandler } from "express"; +import { getAccountForRequest } from "../../services/loginService.ts"; +import { createNewSession } from "../../managers/sessionManager.ts"; +import { logger } from "../../utils/logger.ts"; +import type { ISession } from "../../types/session.ts"; +import { JSONParse } from "json-with-bigint"; +import { toOid2, version_compare } from "../../helpers/inventoryHelpers.ts"; + +const hostSessionController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + const hostSessionRequest = JSONParse(String(req.body)) as ISession; + logger.debug("HostSession Request", { hostSessionRequest }); + const session = createNewSession(hostSessionRequest, account._id); + logger.debug(`New Session Created`, { session }); + + 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..467186b5 --- /dev/null +++ b/src/controllers/api/hubBlessingController.ts @@ -0,0 +1,45 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { addBooster, getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getRandomInt } from "../../services/rngService.ts"; +import type { 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); // unfaithful, but current HUB server does not handle broadcasting, so this way users can bless themselves. + 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/hubController.ts b/src/controllers/api/hubController.ts new file mode 100644 index 00000000..bc0129e5 --- /dev/null +++ b/src/controllers/api/hubController.ts @@ -0,0 +1,7 @@ +import type { RequestHandler } from "express"; +import { getReflexiveAddress } from "../../services/configService.ts"; + +export const hubController: RequestHandler = (req, res) => { + const { myAddress } = getReflexiveAddress(req); + res.json(`hub ${myAddress}:6952`); +}; diff --git a/src/controllers/api/hubInstancesController.ts b/src/controllers/api/hubInstancesController.ts new file mode 100644 index 00000000..89a5564c --- /dev/null +++ b/src/controllers/api/hubInstancesController.ts @@ -0,0 +1,7 @@ +import type { RequestHandler } from "express"; + +const hubInstancesController: RequestHandler = (_req, res) => { + res.json("list 50 1 0 0 scenarios 0 0 0 0 0 0"); +}; + +export { hubInstancesController }; diff --git a/src/controllers/api/inboxController.ts b/src/controllers/api/inboxController.ts new file mode 100644 index 00000000..61911684 --- /dev/null +++ b/src/controllers/api/inboxController.ts @@ -0,0 +1,143 @@ +import type { RequestHandler } from "express"; +import { Inbox } from "../../models/inboxModel.ts"; +import { + createMessage, + createNewEventMessages, + deleteAllMessagesRead, + deleteMessageRead, + getAllMessagesSorted, + getMessage +} from "../../services/inboxService.ts"; +import { getAccountForRequest, getAccountFromSuffixedName, getSuffixedName } from "../../services/loginService.ts"; +import { + addItems, + combineInventoryChanges, + getEffectiveAvatarImageType, + getInventory, + updateCurrency +} from "../../services/inventoryService.ts"; +import { logger } from "../../utils/logger.ts"; +import { ExportFlavour } from "warframe-public-export-plus"; +import { handleStoreItemAcquisition } from "../../services/purchaseService.ts"; +import { fromStoreItem, isStoreItem } from "../../services/itemDataService.ts"; +import type { IOid } from "../../types/commonTypes.ts"; + +export const inboxController: RequestHandler = async (req, res) => { + const { deleteId, lastMessage: latestClientMessageId, messageId } = req.query; + + const account = await getAccountForRequest(req); + const accountId = account._id.toString(); + + if (deleteId) { + if (deleteId === "DeleteAllRead") { + await deleteAllMessagesRead(accountId); + res.status(200).end(); + return; + } + + await deleteMessageRead(parseOid(deleteId as string)); + res.status(200).end(); + } else if (messageId) { + const message = await getMessage(parseOid(messageId as string)); + message.r = true; + 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; + } + + const inventory = await getInventory(accountId); + const inventoryChanges = {}; + if (attachmentItems) { + await addItems( + inventory, + attachmentItems.map(attItem => ({ + ItemType: isStoreItem(attItem) ? fromStoreItem(attItem) : attItem, + ItemCount: 1 + })), + inventoryChanges + ); + } + 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 + } + ]); + } + } + } + if (message.RegularCredits) { + updateCurrency(inventory, -message.RegularCredits, false, inventoryChanges); + } + await inventory.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() === parseOid(latestClientMessageId as string)); + + if (!latestClientMessage) { + logger.debug(`this should only happen after DeleteAllRead `); + res.json({ Inbox: messages }); + return; + } + const newMessages = messages.filter(m => m.date > latestClientMessage.date); + + if (newMessages.length === 0) { + res.send("no-new"); + return; + } + + res.json({ Inbox: newMessages }); + } else { + //newly created event messages must be newer than account.LatestEventMessageDate + await createNewEventMessages(req); + const messages = await getAllMessagesSorted(accountId); + const inbox = messages.map(m => m.toJSON()); + 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 new file mode 100644 index 00000000..de0b714e --- /dev/null +++ b/src/controllers/api/infestedFoundryController.ts @@ -0,0 +1,463 @@ +import type { RequestHandler } from "express"; +import { getAccountForRequest } from "../../services/loginService.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { getInventory, addMiscItems, updateCurrency, addRecipes, freeUpSlot } from "../../services/inventoryService.ts"; +import type { IOid } from "../../types/commonTypes.ts"; +import type { + IConsumedSuit, + IHelminthFoodRecord, + IInventoryClient, + IMiscItem +} from "../../types/inventoryTypes/inventoryTypes.ts"; +import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts"; +import { ExportMisc } from "warframe-public-export-plus"; +import { getRecipe } from "../../services/itemDataService.ts"; +import { toMongoDate, version_compare } from "../../helpers/inventoryHelpers.ts"; +import { logger } from "../../utils/logger.ts"; +import { colorToShard } from "../../helpers/shardHelper.ts"; +import { + addInfestedFoundryXP, + applyCheatsToInfestedFoundry, + handleSubsumeCompletion +} from "../../services/infestedFoundryService.ts"; + +export const infestedFoundryController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + switch (req.query.mode) { + case "s": { + // shard installation + const request = getJSONfromString(String(req.body)); + const inventory = await getInventory(account._id.toString()); + const suit = inventory.Suits.id(request.SuitId.$oid)!; + suit.ArchonCrystalUpgrades ??= []; + while (suit.ArchonCrystalUpgrades.length < request.Slot) { + suit.ArchonCrystalUpgrades.push({}); + } + suit.ArchonCrystalUpgrades[request.Slot] = { + UpgradeType: request.UpgradeType, + Color: request.Color + }; + const miscItemChanges = [ + { + ItemType: colorToShard[request.Color], + ItemCount: -1 + } + ]; + addMiscItems(inventory, miscItemChanges); + await inventory.save(); + res.json({ + InventoryChanges: { + MiscItems: miscItemChanges + } + }); + break; + } + + case "x": { + // shard removal + const request = getJSONfromString(String(req.body)); + const inventory = await getInventory(account._id.toString()); + const suit = inventory.Suits.id(request.SuitId.$oid)!; + + 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 (!inventory.infiniteHelminthMaterials) { + let type: string; + let count: number; + if (account.BuildLabel && version_compare(account.BuildLabel, "2025.05.20.10.18") < 0) { + // < 38.6.0 + type = "/Lotus/Types/Items/InfestedFoundry/HelminthBile"; + count = 300; + } else { + // >= 38.6.0 + type = + archonCrystalRemovalResource[ + suit.ArchonCrystalUpgrades![request.Slot].Color!.replace("_MYTHIC", "") + ]; + count = suit.ArchonCrystalUpgrades![request.Slot].Color!.indexOf("_MYTHIC") != -1 ? 300 : 150; + } + inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == type)!.Count -= count; + } + } + + // remove from suit + suit.ArchonCrystalUpgrades![request.Slot].UpgradeType = undefined; + suit.ArchonCrystalUpgrades![request.Slot].Color = undefined; + + await inventory.save(); + + const infestedFoundry = inventory.toJSON().InfestedFoundry!; + applyCheatsToInfestedFoundry(inventory, infestedFoundry); + res.json({ + InventoryChanges: { + MiscItems: miscItemChanges, + InfestedFoundry: infestedFoundry + } + }); + break; + } + + case "n": { + // name the beast + const request = getJSONfromString(String(req.body)); + const inventory = await getInventory(account._id.toString()); + inventory.InfestedFoundry ??= {}; + inventory.InfestedFoundry.Name = request.newName; + await inventory.save(); + res.json({ + InventoryChanges: { + InfestedFoundry: { + Name: inventory.InfestedFoundry.Name + } + } + }); + break; + } + + case "c": { + // consume items + + const inventory = await getInventory(account._id.toString()); + + if (inventory.infiniteHelminthMaterials) { + res.status(400).end(); + return; + } + + const request = getJSONfromString(String(req.body)); + inventory.InfestedFoundry ??= {}; + inventory.InfestedFoundry.Resources ??= []; + + const miscItemChanges: IMiscItem[] = []; + let totalPercentagePointsGained = 0; + + const currentUnixSeconds = Math.trunc(Date.now() / 1000); + + for (const contribution of request.ResourceContributions) { + const snack = ExportMisc.helminthSnacks[contribution.ItemType]; + + // tally items for removal + const change = miscItemChanges.find(x => x.ItemType == contribution.ItemType); + if (change) { + change.ItemCount -= snack.count; + } else { + miscItemChanges.push({ ItemType: contribution.ItemType, ItemCount: snack.count * -1 }); + } + + if (snack.type == "/Lotus/Types/Items/InfestedFoundry/HelminthAppetiteCooldownReducer") { + // sentinent apetite + let mostDislikedSnackRecord: IHelminthFoodRecord = { ItemType: "", Date: 0 }; + for (const resource of inventory.InfestedFoundry.Resources) { + if (resource.RecentlyConvertedResources) { + for (const record of resource.RecentlyConvertedResources) { + if (record.Date > mostDislikedSnackRecord.Date) { + mostDislikedSnackRecord = record; + } + } + } + } + logger.debug("helminth eats sentient resource; most disliked snack:", { + type: mostDislikedSnackRecord.ItemType, + date: mostDislikedSnackRecord.Date + }); + mostDislikedSnackRecord.Date = currentUnixSeconds + 24 * 60 * 60; // Possibly unfaithful + continue; + } + + let resource = inventory.InfestedFoundry.Resources.find(x => x.ItemType == snack.type); + if (!resource) { + resource = + inventory.InfestedFoundry.Resources[ + inventory.InfestedFoundry.Resources.push({ ItemType: snack.type, Count: 0 }) - 1 + ]; + } + + resource.RecentlyConvertedResources ??= []; + let record = resource.RecentlyConvertedResources.find(x => x.ItemType == contribution.ItemType); + if (!record) { + record = + resource.RecentlyConvertedResources[ + resource.RecentlyConvertedResources.push({ ItemType: contribution.ItemType, Date: 0 }) - 1 + ]; + } + + const hoursRemaining = (record.Date - currentUnixSeconds) / 3600; + const apetiteFactor = apetiteModel(hoursRemaining) / 30; + logger.debug(`helminth eating ${contribution.ItemType} (+${(snack.gain * 100).toFixed(0)}%)`, { + hoursRemaining, + apetiteFactor + }); + if (hoursRemaining >= 18) { + record.Date = currentUnixSeconds + 72 * 60 * 60; // Possibly unfaithful + } else { + record.Date = currentUnixSeconds + 24 * 60 * 60; + } + + totalPercentagePointsGained += snack.gain * 100 * apetiteFactor; // 30% would be gain=0.3, so percentage points is equal to gain * 100. + resource.Count += Math.trunc(snack.gain * 1000 * apetiteFactor); // 30% would be gain=0.3 or Count=300, so Count=gain*1000. + if (resource.Count > 1000) resource.Count = 1000; + } + + const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry, 666 * totalPercentagePointsGained); + addRecipes(inventory, recipeChanges); + addMiscItems(inventory, miscItemChanges); + await inventory.save(); + + res.json({ + InventoryChanges: { + Recipes: recipeChanges, + InfestedFoundry: { + XP: inventory.InfestedFoundry.XP, + Resources: inventory.InfestedFoundry.Resources, + Slots: inventory.InfestedFoundry.Slots + }, + MiscItems: miscItemChanges + } + }); + break; + } + + case "o": { + // offerings update + const request = getJSONfromString(String(req.body)); + const inventory = await getInventory(account._id.toString()); + inventory.InfestedFoundry ??= {}; + inventory.InfestedFoundry.InvigorationIndex = request.OfferingsIndex; + inventory.InfestedFoundry.InvigorationSuitOfferings = request.SuitTypes; + if (request.Extra) { + inventory.InfestedFoundry.InvigorationsApplied = 0; + } + await inventory.save(); + const infestedFoundry = inventory.toJSON().InfestedFoundry!; + applyCheatsToInfestedFoundry(inventory, infestedFoundry); + res.json({ + InventoryChanges: { + InfestedFoundry: infestedFoundry + } + }); + break; + } + + case "a": { + // subsume warframe + const request = getJSONfromString(String(req.body)); + const inventory = await getInventory(account._id.toString()); + const recipe = getRecipe(request.Recipe)!; + if (!inventory.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[0] && suit.Configs[0].pricol) { + consumedSuit.c = suit.Configs[0].pricol; + } + if ((inventory.InfestedFoundry!.XP ?? 0) < 73125_00) { + inventory.InfestedFoundry!.Slots!--; + } + inventory.InfestedFoundry!.ConsumedSuits ??= []; + inventory.InfestedFoundry!.ConsumedSuits.push(consumedSuit); + inventory.InfestedFoundry!.LastConsumedSuit = suit; + 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(inventory, infestedFoundry); + res.json({ + InventoryChanges: { + Recipes: recipeChanges, + RemovedIdItems: [ + { + ItemId: request.SuitId + } + ], + SuitBin: { + count: -1, + platinum: 0, + Slots: 1 + }, + InfestedFoundry: infestedFoundry + } + }); + break; + } + + case "r": { + // rush subsume + 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(inventory, infestedFoundry); + res.json({ + InventoryChanges: { + ...currencyChanges, + Recipes: recipeChanges, + InfestedFoundry: infestedFoundry + } + }); + break; + } + + case "u": { + const request = getJSONfromString(String(req.body)); + const inventory = await getInventory(account._id.toString()); + const suit = inventory.Suits.id(request.SuitId.$oid)!; + 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); + if (!inventory.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(inventory, infestedFoundry); + res.json({ + SuitId: request.SuitId, + OffensiveUpgrade: request.OffensiveUpgradeType, + DefensiveUpgrade: request.DefensiveUpgradeType, + UpgradesExpiry: toMongoDate(upgradesExpiry), + InventoryChanges: { + Recipes: recipeChanges, + InfestedFoundry: infestedFoundry + } + }); + break; + } + + case "custom_unlockall": { + const inventory = await getInventory(account._id.toString()); + inventory.InfestedFoundry ??= {}; + inventory.InfestedFoundry.XP ??= 0; + if (151875_00 > inventory.InfestedFoundry.XP) { + const recipeChanges = addInfestedFoundryXP( + inventory.InfestedFoundry, + 151875_00 - inventory.InfestedFoundry.XP + ); + addRecipes(inventory, recipeChanges); + await inventory.save(); + } + res.end(); + break; + } + + default: + logger.debug(`data provided to ${req.path}: ${String(req.body)}`); + throw new Error(`unhandled infestedFoundry mode: ${String(req.query.mode)}`); + } +}; + +interface IShardInstallRequest { + SuitId: IOid; + Slot: number; + UpgradeType: string; + Color: string; +} + +interface IShardUninstallRequest { + SuitId: IOid; + Slot: number; +} + +interface IHelminthNameRequest { + newName: string; +} + +interface IHelminthFeedRequest { + ResourceContributions: { + ItemType: string; + Date: number; // unix timestamp + }[]; +} + +interface IHelminthSubsumeRequest { + SuitId: IOid; + Recipe: string; +} + +interface IHelminthOfferingsUpdate { + OfferingsIndex: number; + SuitTypes: string[]; + Extra: boolean; +} + +interface IHelminthInvigorationRequest { + SuitId: IOid; + OffensiveUpgradeType: string; + DefensiveUpgradeType: string; + ResourceTypes: string[]; + ResourceCosts: number[]; +} + +// A fitted model for observed apetite values. Likely slightly inaccurate. +// +// Hours remaining, percentage points gained (out of 30 total) +// 0, 30 +// 5, 25.8 +// 10, 21.6 +// 12, 20 +// 16, 16.6 +// 17, 15.8 +// 18, 15 +// 20, 15 +// 24, 15 +// 36, 15 +// 40, 13.6 +// 47, 11.3 +// 48, 11 +// 50, 10.3 +// 60, 7 +// 70, 3.6 +// 71, 3.3 +// 72, 3 +const apetiteModel = (x: number): number => { + if (x <= 0) { + return 30; + } + if (x < 18) { + return -0.84 * x + 30; + } + if (x <= 36) { + return 15; + } + if (x < 71.9) { + return -0.3327892 * x + 26.94135; + } + 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 new file mode 100644 index 00000000..13bbd21a --- /dev/null +++ b/src/controllers/api/inventoryController.ts @@ -0,0 +1,515 @@ +import type { RequestHandler } from "express"; +import { getAccountForRequest } from "../../services/loginService.ts"; +import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts"; +import { Inventory } from "../../models/inventoryModels/inventoryModel.ts"; +import { config } from "../../services/configService.ts"; +import allDialogue from "../../../static/fixed_responses/allDialogue.json" with { type: "json" }; +import type { ILoadoutDatabase } from "../../types/saveLoadoutTypes.ts"; +import type { IInventoryClient, IShipInventory } from "../../types/inventoryTypes/inventoryTypes.ts"; +import { equipmentKeys } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { IPolarity } from "../../types/inventoryTypes/commonInventoryTypes.ts"; +import { ArtifactPolarity } from "../../types/inventoryTypes/commonInventoryTypes.ts"; +import type { ICountedItem } from "warframe-public-export-plus"; +import { eFaction, ExportCustoms, ExportFlavour, ExportResources, ExportVirtuals } from "warframe-public-export-plus"; +import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "../../services/infestedFoundryService.ts"; +import { + addEmailItem, + addItem, + addMiscItems, + allDailyAffiliationKeys, + checkCalendarAutoAdvance, + cleanupInventory, + createLibraryDailyTask, + getCalendarProgress +} from "../../services/inventoryService.ts"; +import { logger } from "../../utils/logger.ts"; +import { addString, catBreadHash } from "../../helpers/stringHelpers.ts"; +import { Types } from "mongoose"; +import { getNemesisManifest } from "../../helpers/nemesisHelpers.ts"; +import { getPersonalRooms } from "../../services/personalRoomsService.ts"; +import type { IPersonalRoomsClient } from "../../types/personalRoomsTypes.ts"; +import { Ship } from "../../models/shipModel.ts"; +import { toLegacyOid, toOid, version_compare } from "../../helpers/inventoryHelpers.ts"; +import { Inbox } from "../../models/inboxModel.ts"; +import { unixTimesInMs } from "../../constants/timeConstants.ts"; +import { DailyDeal } from "../../models/worldStateModel.ts"; +import { EquipmentFeatures } from "../../types/equipmentTypes.ts"; +import { generateRewardSeed } from "../../services/rngService.ts"; +import { getInvasionByOid, getWorldState } from "../../services/worldStateService.ts"; +import { createMessage } from "../../services/inboxService.ts"; + +export const inventoryController: RequestHandler = async (request, response) => { + const account = await getAccountForRequest(request); + + const inventory = await Inventory.findOne({ accountOwnerId: account._id }); + + if (!inventory) { + response.status(400).json({ error: "inventory was undefined" }); + return; + } + + // Handle daily reset + if (!inventory.NextRefill || Date.now() >= inventory.NextRefill.getTime()) { + const today = Math.trunc(Date.now() / 86400000); + + for (const key of allDailyAffiliationKeys) { + inventory[key] = 16000 + inventory.PlayerLevel * 500; + } + inventory.DailyFocus = 250000 + inventory.PlayerLevel * 5000; + inventory.GiftsRemaining = Math.max(8, inventory.PlayerLevel); + inventory.TradesRemaining = inventory.PlayerLevel; + + inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask(); + + if (inventory.NextRefill) { + const lastLoginDay = Math.trunc(inventory.NextRefill.getTime() / 86400000) - 1; + const daysPassed = today - lastLoginDay; + + if (inventory.noArgonCrystalDecay) { + inventory.FoundToday = undefined; + } else { + 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; + } + } + + if (inventory.UsedDailyDeals.length != 0) { + if (daysPassed == 1) { + const todayAt0Utc = today * 86400000; + const darvoIndex = Math.trunc((todayAt0Utc - 25200000) / (26 * unixTimesInMs.hour)); + const darvoStart = darvoIndex * (26 * unixTimesInMs.hour) + 25200000; + const darvoOid = + ((darvoStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "adc51a72f7324d95"; + const deal = await DailyDeal.findById(darvoOid); + if (deal) { + inventory.UsedDailyDeals = inventory.UsedDailyDeals.filter(x => x == deal.StoreItem); // keep only the deal that came into this new day with us + } else { + inventory.UsedDailyDeals = []; + } + } else { + inventory.UsedDailyDeals = []; + } + } + } + + // TODO: Setup CalendarProgress as part of 1999 mission completion? + + const previousYearIteration = inventory.CalendarProgress?.Iteration; + + // We need to do the following to ensure the in-game calendar does not break: + getCalendarProgress(inventory); // Keep the CalendarProgress up-to-date (at least for the current year iteration) (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2364) + checkCalendarAutoAdvance(inventory, getWorldState().KnownCalendarSeasons[0]); // Skip birthday events for characters if we do not have them unlocked yet (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2424) + + // also handle sending of kiss cinematic at year rollover + if ( + inventory.CalendarProgress!.Iteration != previousYearIteration && + inventory.DialogueHistory && + inventory.DialogueHistory.Dialogues + ) { + let kalymos = false; + for (const { dialogueName, kissEmail } of [ + { + dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue", + kissEmail: "/Lotus/Types/Items/EmailItems/ArthurKissEmailItem" + }, + { + dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue", + kissEmail: "/Lotus/Types/Items/EmailItems/EleanorKissEmailItem" + }, + { + dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue", + kissEmail: "/Lotus/Types/Items/EmailItems/LettieKissEmailItem" + }, + { + dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue", + kissEmail: "/Lotus/Types/Items/EmailItems/AmirKissEmailItem" + }, + { + dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue", + kissEmail: "/Lotus/Types/Items/EmailItems/AoiKissEmailItem" + }, + { + dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue", + kissEmail: "/Lotus/Types/Items/EmailItems/QuincyKissEmailItem" + } + ]) { + const dialogue = inventory.DialogueHistory.Dialogues.find(x => x.DialogueName == dialogueName); + if (dialogue) { + if (dialogue.Rank == 7) { + await addEmailItem(inventory, kissEmail); + kalymos = false; + break; + } + if (dialogue.Rank == 6) { + kalymos = true; + } + } + } + if (kalymos) { + await addEmailItem(inventory, "/Lotus/Types/Items/EmailItems/KalymosKissEmailItem"); + } + } + + cleanupInventory(inventory); + + inventory.NextRefill = new Date((today + 1) * 86400000); // tomorrow at 0 UTC + //await inventory.save(); + } + + if ( + inventory.InfestedFoundry && + inventory.InfestedFoundry.AbilityOverrideUnlockCooldown && + new Date() >= inventory.InfestedFoundry.AbilityOverrideUnlockCooldown + ) { + handleSubsumeCompletion(inventory); + //await inventory.save(); + } + + for (let i = 0; i != inventory.QualifyingInvasions.length; ) { + const qi = inventory.QualifyingInvasions[i]; + const invasion = getInvasionByOid(qi.invasionId.toString()); + if (!invasion) { + logger.debug(`removing QualifyingInvasions entry for unknown invasion: ${qi.invasionId.toString()}`); + inventory.QualifyingInvasions.splice(i, 1); + continue; + } + if (invasion.Completed) { + let factionSidedWith: string | undefined; + let battlePay: ICountedItem[] | undefined; + if (qi.AttackerScore >= 3) { + factionSidedWith = invasion.Faction; + battlePay = invasion.AttackerReward.countedItems; + logger.debug(`invasion pay from ${factionSidedWith}`, { battlePay }); + } else if (qi.DefenderScore >= 3) { + factionSidedWith = invasion.DefenderFaction; + battlePay = invasion.DefenderReward.countedItems; + logger.debug(`invasion pay from ${factionSidedWith}`, { battlePay }); + } + if (factionSidedWith) { + if (battlePay) { + // Decoupling rewards from the inbox message because it may delete itself without being read + for (const item of battlePay) { + await addItem(inventory, item.ItemType, item.ItemCount); + } + await createMessage(account._id, [ + { + sndr: eFaction.find(x => x.tag == factionSidedWith)?.name ?? factionSidedWith, // TOVERIFY + msg: `/Lotus/Language/G1Quests/${factionSidedWith}_InvasionThankyouMessageBody`, + sub: `/Lotus/Language/G1Quests/${factionSidedWith}_InvasionThankyouMessageSubject`, + countedAtt: battlePay, + attVisualOnly: true, + icon: + factionSidedWith == "FC_GRINEER" + ? "/Lotus/Interface/Icons/Npcs/EliteRifleLancerAvatar.png" // Source: https://www.reddit.com/r/Warframe/comments/1aj4usx/battle_pay_worth_10_plat/, https://www.youtube.com/watch?v=XhNZ6ai6BOY + : "/Lotus/Interface/Icons/Npcs/CrewmanNormal.png", // My best source for this is https://www.youtube.com/watch?v=rxrCCFm73XE around 1:37 + // TOVERIFY: highPriority? + endDate: 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 not clear if this is correct. + } + ]); + } + if (invasion.Faction != "FC_INFESTATION") { + // Sided with grineer -> opposed corpus -> send zanuka (harvester) + // Sided with corpus -> opposed grineer -> send g3 (death squad) + inventory[factionSidedWith != "FC_GRINEER" ? "DeathSquadable" : "Harvestable"] = true; + // TOVERIFY: Should this happen earlier? + // TOVERIFY: Should this send an (ephemeral) email? + } + } + logger.debug(`removing QualifyingInvasions entry for completed invasion: ${qi.invasionId.toString()}`); + inventory.QualifyingInvasions.splice(i, 1); + continue; + } + ++i; + } + + 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, + buildLabel: string | undefined +): Promise => { + const [inventoryWithLoadOutPresets, ships, latestMessage] = await Promise.all([ + inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>("LoadOutPresets"), + Ship.find({ ShipOwnerId: inventory.accountOwnerId }), + Inbox.findOne({ ownerId: inventory.accountOwnerId }, "_id").sort({ date: -1 }) + ]); + const inventoryResponse = inventoryWithLoadOutPresets.toJSON(); + inventoryResponse.Ships = ships.map(x => x.toJSON()); + + // In case mission inventory update added an inbox message, we need to send the Mailbox part so the client knows to refresh it. + if (latestMessage) { + inventoryResponse.Mailbox = { + LastInboxId: toOid(latestMessage._id) + }; + } + + if (inventory.infiniteCredits) { + inventoryResponse.RegularCredits = 999999999; + } + if (inventory.infinitePlatinum) { + inventoryResponse.PremiumCreditsFree = 0; + inventoryResponse.PremiumCredits = 999999999; + } + if (inventory.infiniteEndo) { + inventoryResponse.FusionPoints = 999999999; + } + if (inventory.infiniteRegalAya) { + inventoryResponse.PrimeTokens = 999999999; + } + + if (inventory.skipAllDialogue) { + inventoryResponse.TauntHistory = [ + { + node: "TreasureTutorial", + state: "TS_COMPLETED" + } + ]; + for (const str of allDialogue) { + addString(inventoryResponse.NodeIntrosCompleted, str); + } + } + + if (config.unlockAllShipDecorations) { + inventoryResponse.ShipDecorations = []; + for (const [uniqueName, item] of Object.entries(ExportResources)) { + if (item.productCategory == "ShipDecorations") { + inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 999_999 }); + } + } + } + + if (config.unlockAllFlavourItems) { + inventoryResponse.FlavourItems = []; + for (const uniqueName in ExportFlavour) { + inventoryResponse.FlavourItems.push({ ItemType: uniqueName }); + } + } else if (config.worldState?.baroTennoConRelay) { + [ + "/Lotus/Types/Items/Events/TennoConRelay2022EarlyAccess", + "/Lotus/Types/Items/Events/TennoConRelay2023EarlyAccess", + "/Lotus/Types/Items/Events/TennoConRelay2024EarlyAccess", + "/Lotus/Types/Items/Events/TennoConRelay2025EarlyAccess" + ].forEach(uniqueName => { + if (!inventoryResponse.FlavourItems.some(x => x.ItemType == uniqueName)) { + inventoryResponse.FlavourItems.push({ ItemType: uniqueName }); + } + }); + } + + if (config.unlockAllSkins) { + const missingWeaponSkins = new Set(Object.keys(ExportCustoms)); + inventoryResponse.WeaponSkins.forEach(x => missingWeaponSkins.delete(x.ItemType)); + for (const uniqueName of missingWeaponSkins) { + inventoryResponse.WeaponSkins.push({ + ItemId: { + $oid: "ca70ca70ca70ca70" + catBreadHash(uniqueName).toString(16).padStart(8, "0") + }, + ItemType: uniqueName + }); + } + } + + if (config.unlockAllCapturaScenes) { + for (const uniqueName of Object.keys(ExportResources)) { + if (resourceInheritsFrom(uniqueName, "/Lotus/Types/Items/MiscItems/PhotoboothTile")) { + inventoryResponse.MiscItems.push({ + ItemType: uniqueName, + ItemCount: 1 + }); + } + } + } + + if (typeof config.spoofMasteryRank === "number" && config.spoofMasteryRank >= 0) { + inventoryResponse.PlayerLevel = config.spoofMasteryRank; + if (!xpBasedLevelCapDisabled) { + // This client has not been patched to accept any mastery rank, need to fake the XP. + inventoryResponse.XPInfo = []; + let numFrames = getExpRequiredForMr(Math.min(config.spoofMasteryRank, 5030)) / 6000; + while (numFrames-- > 0) { + inventoryResponse.XPInfo.push({ + ItemType: "/Lotus/Powersuits/Mag/Mag", + XP: 1_600_000 + }); + } + } + } + + if (inventory.universalPolarityEverywhere) { + const Polarity: IPolarity[] = []; + // 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 + }); + } + for (const key of equipmentKeys) { + if (key in inventoryResponse) { + for (const equipment of inventoryResponse[key]) { + equipment.Polarity = Polarity; + } + } + } + } + + if (inventory.unlockDoubleCapacityPotatoesEverywhere) { + for (const key of equipmentKeys) { + if (key in inventoryResponse) { + for (const equipment of inventoryResponse[key]) { + equipment.Features ??= 0; + equipment.Features |= EquipmentFeatures.DOUBLE_CAPACITY; + } + } + } + } + + if (inventory.unlockExilusEverywhere) { + for (const key of equipmentKeys) { + if (key in inventoryResponse) { + for (const equipment of inventoryResponse[key]) { + equipment.Features ??= 0; + equipment.Features |= EquipmentFeatures.UTILITY_SLOT; + } + } + } + } + + if (inventory.unlockArcanesEverywhere) { + for (const key of equipmentKeys) { + if (key in inventoryResponse) { + for (const equipment of inventoryResponse[key]) { + equipment.Features ??= 0; + equipment.Features |= EquipmentFeatures.ARCANE_SLOT; + } + } + } + } + + if (inventory.noDailyStandingLimits) { + const spoofedDailyAffiliation = Math.max(999_999, 16000 + inventoryResponse.PlayerLevel * 500); + for (const key of allDailyAffiliationKeys) { + inventoryResponse[key] = spoofedDailyAffiliation; + } + } + + if (inventory.noDailyFocusLimit) { + inventoryResponse.DailyFocus = Math.max(999_999, 250000 + inventoryResponse.PlayerLevel * 5000); + } + + if (inventoryResponse.InfestedFoundry) { + applyCheatsToInfestedFoundry(inventory, inventoryResponse.InfestedFoundry); + } + + // Set 2FA enabled so trading post can be used + inventoryResponse.HWIDProtectEnabled = true; + + if (buildLabel) { + // Fix nemesis for older versions + if ( + inventoryResponse.Nemesis && + version_compare(buildLabel, getNemesisManifest(inventoryResponse.Nemesis.manifest).minBuild) < 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; +}; + +const getExpRequiredForMr = (rank: number): number => { + if (rank <= 30) { + return 2500 * rank * rank; + } + return 2_250_000 + 147_500 * (rank - 30); +}; + +const resourceInheritsFrom = (resourceName: string, targetName: string): boolean => { + let parentName = resourceGetParent(resourceName); + for (; parentName != undefined; parentName = resourceGetParent(parentName)) { + if (parentName == targetName) { + return true; + } + } + return false; +}; + +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; +}; diff --git a/src/controllers/api/inventorySlotsController.ts b/src/controllers/api/inventorySlotsController.ts new file mode 100644 index 00000000..56bad04c --- /dev/null +++ b/src/controllers/api/inventorySlotsController.ts @@ -0,0 +1,70 @@ +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getInventory, updateCurrency, updateSlots } from "../../services/inventoryService.ts"; +import type { RequestHandler } from "express"; +import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts"; +import { exhaustive } from "../../utils/ts-utils.ts"; + +/* + loadout slots are additionally purchased slots only + 1 slot per mastery rank is automatically given above mr10, without database needing to save the mastery slots + extra = everything above the base + 2 slots (e.g. for warframes) + new slot = extra + 1 and slots +1 + using slot = slots -1, except for when purchased with platinum, then slots are included in price + + e.g. number of frames: + 19 slots, 71 extra + = 71 - 19 + 2 = 54 + 19 actually available slots in ingame inventory = 17 extra + 2 Base (base amount depends on slot) (+ 1 for every mastery rank above 10) + number of frames = extra - slots + 2 +*/ + +export const inventorySlotsController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const body = JSON.parse(req.body as string) as IInventorySlotsRequest; + + let price; + let amount; + switch (body.Bin) { + case InventorySlot.SUITS: + case InventorySlot.MECHSUITS: + case InventorySlot.PVE_LOADOUTS: + case InventorySlot.CREWMEMBERS: + price = 20; + amount = 1; + break; + + case InventorySlot.SPACESUITS: + price = 12; + amount = 1; + break; + + case InventorySlot.WEAPONS: + case InventorySlot.SPACEWEAPONS: + case InventorySlot.SENTINELS: + case InventorySlot.RJ_COMPONENT_AND_ARMAMENTS: + case InventorySlot.AMPS: + price = 12; + amount = 2; + break; + + case InventorySlot.RIVENS: + price = 60; + amount = 3; + break; + + default: + exhaustive(body.Bin); + throw new Error(`unexpected slot purchase of type ${body.Bin as string}`); + } + + const inventory = await getInventory(accountId); + const currencyChanges = updateCurrency(inventory, price, true); + updateSlots(inventory, body.Bin, amount, amount); + 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 new file mode 100644 index 00000000..95cb874d --- /dev/null +++ b/src/controllers/api/joinSessionController.ts @@ -0,0 +1,14 @@ +import type { RequestHandler } from "express"; +import { getSessionByID } from "../../managers/sessionManager.ts"; +import { logger } from "../../utils/logger.ts"; + +export const joinSessionController: RequestHandler = (req, res) => { + const reqBody = JSON.parse(String(req.body)) as IJoinSessionRequest; + logger.debug(`JoinSession Request`, { reqBody }); + const session = getSessionByID(reqBody.sessionIds[0]); + res.json({ rewardSeed: session?.rewardSeed, sessionId: { $oid: session?.sessionId } }); +}; + +interface IJoinSessionRequest { + sessionIds: string[]; +} diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts new file mode 100644 index 00000000..bff89eb7 --- /dev/null +++ b/src/controllers/api/loginController.ts @@ -0,0 +1,136 @@ +import type { RequestHandler } from "express"; + +import { config, getReflexiveAddress } from "../../services/configService.ts"; +import { buildConfig } from "../../services/buildConfigService.ts"; + +import { Account } from "../../models/loginModel.ts"; +import { createAccount, createNonce, getUsernameFromEmail, isCorrectPassword } from "../../services/loginService.ts"; +import type { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "../../types/loginTypes.ts"; +import { logger } from "../../utils/logger.ts"; +import { version_compare } from "../../helpers/inventoryHelpers.ts"; +import { sendWsBroadcastTo } from "../../services/wsService.ts"; + +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 + + const account = await Account.findOne({ email: loginRequest.email }); + + const buildLabel: string = + typeof request.query.buildLabel == "string" + ? request.query.buildLabel.split(" ").join("+") + : buildConfig.buildLabel; + + const { myAddress, myUrlBase } = getReflexiveAddress(request); + + if ( + !account && + ((config.autoCreateAccount && loginRequest.ClientType != "webui") || + loginRequest.ClientType == "webui-register") + ) { + try { + const name = await getUsernameFromEmail(loginRequest.email); + const newAccount = await createAccount({ + email: loginRequest.email, + password: loginRequest.password, + DisplayName: name, + CountryCode: loginRequest.lang?.toUpperCase() ?? "EN", + ClientType: loginRequest.ClientType, + Nonce: createNonce(), + BuildLabel: buildLabel, + LastLogin: new Date() + }); + logger.debug("created new account"); + response.json(createLoginResponse(myAddress, myUrlBase, newAccount, buildLabel)); + return; + } catch (error: unknown) { + if (error instanceof Error) { + throw new Error(`error creating account ${error.message}`); + } + } + } + + if (!account) { + response.status(400).json({ error: "unknown user" }); + return; + } + + if (!isCorrectPassword(loginRequest.password, account.password)) { + response.status(400).json({ error: "incorrect login data" }); + return; + } + + if (account.Nonce && 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 = createNonce(); + account.CountryCode = loginRequest.lang?.toUpperCase() ?? "EN"; + account.BuildLabel = buildLabel; + account.LastLogin = new Date(); + await account.save(); + + sendWsBroadcastTo(account._id.toString(), { nonce_updated: true }); + + response.json(createLoginResponse(myAddress, myUrlBase, account.toJSON(), buildLabel)); +}; + +const createLoginResponse = ( + myAddress: string, + myUrlBase: string, + account: IDatabaseAccountJson, + buildLabel: string +): ILoginResponse => { + const resp: ILoginResponse = { + id: account.id, + DisplayName: account.DisplayName, + CountryCode: account.CountryCode, + AmazonAuthToken: account.AmazonAuthToken, + AmazonRefreshToken: account.AmazonRefreshToken, + Nonce: account.Nonce, + BuildLabel: buildLabel + }; + if (version_compare(buildLabel, "2015.02.13.10.41") >= 0) { + resp.NRS = [myAddress]; + } + 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 = 0; // bit 0 enables DTLS. if enabled, additional bits can be set, e.g. bit 2 to enable logging. on live, the value is 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) { + if (version_compare(buildLabel, "2025.08.26.09.49") >= 0) { + resp.platformCDNs = [`${myUrlBase}/dynamic/`]; + } else { + resp.platformCDNs = [`${myUrlBase}/`]; + } + } + return resp; +}; diff --git a/src/controllers/api/loginRewardsController.ts b/src/controllers/api/loginRewardsController.ts new file mode 100644 index 00000000..d412a5ae --- /dev/null +++ b/src/controllers/api/loginRewardsController.ts @@ -0,0 +1,57 @@ +import type { RequestHandler } from "express"; +import { getAccountForRequest } from "../../services/loginService.ts"; +import type { ILoginRewardsReponse } from "../../services/loginRewardService.ts"; +import { + claimLoginReward, + getRandomLoginRewards, + isLoginRewardAChoice, + setAccountGotLoginRewardToday +} from "../../services/loginRewardService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { sendWsBroadcastTo } from "../../services/wsService.ts"; + +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) { + const inventory = await getInventory(account._id.toString()); + if (!inventory.disableDailyTribute) { + 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]); + setAccountGotLoginRewardToday(account); + await Promise.all([inventory.save(), account.save()]); + + sendWsBroadcastTo(account._id.toString(), { update_inventory: true }); + } + res.json(response); + return; + } + } + res.json({ + DailyTributeInfo: { + IsMilestoneDay: isMilestoneDay, + IsChooseRewardSet: isLoginRewardAChoice(account), + LoginDays: account.LoginDays, + NextMilestoneReward: "", + NextMilestoneDay: nextMilestoneDay + } + } satisfies ILoginRewardsReponse); +}; diff --git a/src/controllers/api/loginRewardsSelectionController.ts b/src/controllers/api/loginRewardsSelectionController.ts new file mode 100644 index 00000000..13e7ec4d --- /dev/null +++ b/src/controllers/api/loginRewardsSelectionController.ts @@ -0,0 +1,65 @@ +import { getInventory } from "../../services/inventoryService.ts"; +import { + claimLoginReward, + getRandomLoginRewards, + setAccountGotLoginRewardToday +} from "../../services/loginRewardService.ts"; +import { getAccountForRequest } from "../../services/loginService.ts"; +import { handleStoreItemAcquisition } from "../../services/purchaseService.ts"; +import { sendWsBroadcastTo } from "../../services/wsService.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; +import { logger } from "../../utils/logger.ts"; +import type { 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); + } + setAccountGotLoginRewardToday(account); + await Promise.all([inventory.save(), account.save()]); + + sendWsBroadcastTo(account._id.toString(), { update_inventory: true }); + 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 new file mode 100644 index 00000000..d1c13c2d --- /dev/null +++ b/src/controllers/api/logoutController.ts @@ -0,0 +1,32 @@ +import type { RequestHandler } from "express"; +import { Account } from "../../models/loginModel.ts"; +import { sendWsBroadcastTo } from "../../services/wsService.ts"; + +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"); + } + + const stat = await Account.updateOne( + { + _id: req.query.accountId, + Nonce: nonce + }, + { + Nonce: 0 + } + ); + if (stat.modifiedCount) { + sendWsBroadcastTo(req.query.accountId as string, { nonce_updated: true }); + } + + res.writeHead(200, { + "Content-Type": "text/html", + "Content-Length": 1 + }); + res.end("1"); +}; diff --git a/src/controllers/api/marketRecommendationsController.ts b/src/controllers/api/marketRecommendationsController.ts new file mode 100644 index 00000000..fd105836 --- /dev/null +++ b/src/controllers/api/marketRecommendationsController.ts @@ -0,0 +1,16 @@ +import type { RequestHandler } from "express"; + +const marketRecommendationsController: RequestHandler = (_req, res) => { + const data = Buffer.from([ + 0x7b, 0x22, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3a, 0x7b, 0x22, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x64, 0x22, + 0x3a, 0x5b, 0x5d, 0x2c, 0x22, 0x50, 0x6f, 0x70, 0x75, 0x6c, 0x61, 0x72, 0x22, 0x3a, 0x5b, 0x5d, 0x7d, 0x7d + ]); + res.writeHead(200, { + "Content-Type": "text/html", + "Content-Length": data.length + }); + res.end(data); +}; + +export { marketRecommendationsController }; diff --git a/src/controllers/api/marketSearchRecommendationsController.ts b/src/controllers/api/marketSearchRecommendationsController.ts new file mode 100644 index 00000000..1360a7ab --- /dev/null +++ b/src/controllers/api/marketSearchRecommendationsController.ts @@ -0,0 +1,7 @@ +import type { RequestHandler } from "express"; + +const marketSearchRecommendationsController: RequestHandler = (_req, res) => { + res.sendStatus(200); +}; + +export { marketSearchRecommendationsController }; diff --git a/src/controllers/api/maturePetController.ts b/src/controllers/api/maturePetController.ts new file mode 100644 index 00000000..9ddc2786 --- /dev/null +++ b/src/controllers/api/maturePetController.ts @@ -0,0 +1,27 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { 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 new file mode 100644 index 00000000..9902f021 --- /dev/null +++ b/src/controllers/api/missionInventoryUpdateController.ts @@ -0,0 +1,144 @@ +import type { RequestHandler } from "express"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { getAccountForRequest } from "../../services/loginService.ts"; +import type { IMissionInventoryUpdateRequest } from "../../types/requestTypes.ts"; +import { addMissionInventoryUpdates, addMissionRewards } from "../../services/missionInventoryUpdateService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getInventoryResponse } from "./inventoryController.ts"; +import { logger } from "../../utils/logger.ts"; +import type { + IMissionInventoryUpdateResponse, + IMissionInventoryUpdateResponseBackToDryDock, + IMissionInventoryUpdateResponseRailjackInterstitial +} from "../../types/missionTypes.ts"; +import { sendWsBroadcastTo } from "../../services/wsService.ts"; +import { generateRewardSeed } from "../../services/rngService.ts"; + +/* +**** INPUT **** +- [ ] crossPlaySetting +- [ ] rewardsMultiplier +- [ ] ActiveBoosters +- [x] LongGuns +- [x] Pistols +- [x] Suits +- [x] Melee +- [x] RawUpgrades +- [x] MiscItems +- [x] RegularCredits +- [ ] RandomUpgradesIdentified +- [ ] MissionFailed +- [ ] MissionStatus +- [ ] CurrentLoadOutIds +- [ ] AliveTime +- [ ] MissionTime +- [x] Missions +- [ ] CompletedAlerts +- [ ] LastRegionPlayed +- [ ] GameModeId +- [ ] hosts +- [x] ChallengeProgress +- [ ] SeasonChallengeHistory +- [ ] PS (anticheat data) +- [ ] ActiveDojoColorResearch +- [x] RewardInfo +- [ ] ReceivedCeremonyMsg +- [ ] LastCeremonyResetDate +- [ ] MissionPTS (Used to validate the mission/alive time above.) +- [ ] RepHash +- [ ] EndOfMatchUpload +- [ ] ObjectiveReached +- [ ] FpsAvg +- [ ] FpsMin +- [ ] FpsMax +- [ ] FpsSamples +*/ +//move credit calc in here, return MissionRewards: [] if no reward info +export const missionInventoryUpdateController: RequestHandler = async (req, res): Promise => { + const account = await getAccountForRequest(req); + const missionReport = getJSONfromString((req.body as string).toString()); + logger.debug("mission report:", 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" && + !( + missionReport.RewardInfo?.jobId || + missionReport.RewardInfo?.challengeMissionId || + missionReport.RewardInfo?.T + ) + ) { + if (missionReport.EndOfMatchUpload) { + inventory.RewardSeed = generateRewardSeed(); + } + await inventory.save(); + const inventoryResponse = await getInventoryResponse(inventory, true, account.BuildLabel); + res.json({ + InventoryJson: JSON.stringify(inventoryResponse), + MissionRewards: [] + }); + sendWsBroadcastTo(account._id.toString(), { update_inventory: true }); + return; + } + + const { + MissionRewards, + inventoryChanges, + credits, + AffiliationMods, + SyndicateXPItemReward, + ConquestCompletedMissionsCount + } = await addMissionRewards(account, inventory, missionReport, firstCompletion); + + if (missionReport.EndOfMatchUpload) { + inventory.RewardSeed = generateRewardSeed(); + } + await inventory.save(); + + //TODO: figure out when to send inventory. it is needed for many cases. + const deltas: IMissionInventoryUpdateResponseRailjackInterstitial = { + InventoryChanges: inventoryChanges, + MissionRewards, + ...credits, + ...inventoryUpdates, + //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 + }; + if (missionReport.RJ) { + logger.debug(`railjack interstitial request, sending only deltas`, deltas); + res.json(deltas); + } else if (missionReport.RewardInfo) { + logger.debug(`classic mission completion, sending everything`); + const inventoryResponse = await getInventoryResponse(inventory, true, account.BuildLabel); + res.json({ + InventoryJson: JSON.stringify(inventoryResponse), + ...deltas + } satisfies IMissionInventoryUpdateResponse); + } else { + logger.debug(`no reward info, assuming this wasn't a mission completion and we should just sync inventory`); + const inventoryResponse = await getInventoryResponse(inventory, true, account.BuildLabel); + res.json({ + InventoryJson: JSON.stringify(inventoryResponse) + } satisfies IMissionInventoryUpdateResponseBackToDryDock); + } + + sendWsBroadcastTo(account._id.toString(), { update_inventory: true }); +}; + +/* +**** OUTPUT **** +- [x] InventoryJson +- [x] MissionRewards +- [x] TotalCredits +- [x] CreditsBonus +- [x] MissionCredits +- [x] InventoryChanges +- [x] FusionPoints +*/ diff --git a/src/controllers/api/modularWeaponCraftingController.ts b/src/controllers/api/modularWeaponCraftingController.ts new file mode 100644 index 00000000..7e235241 --- /dev/null +++ b/src/controllers/api/modularWeaponCraftingController.ts @@ -0,0 +1,201 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { sendWsBroadcastTo } from "../../services/wsService.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { + getInventory, + updateCurrency, + addEquipment, + addMiscItems, + applyDefaultUpgrades, + occupySlot, + productCategoryToInventoryBin, + combineInventoryChanges, + addSpecialItem +} from "../../services/inventoryService.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; +import { getDefaultUpgrades } from "../../services/itemDataService.ts"; +import { modularWeaponTypes } from "../../helpers/modularWeaponHelper.ts"; +import { getRandomInt } from "../../services/rngService.ts"; +import type { IDefaultUpgrade } from "warframe-public-export-plus"; +import { ExportSentinels, ExportWeapons } from "warframe-public-export-plus"; +import type { IEquipmentDatabase } from "../../types/equipmentTypes.ts"; +import { Status } from "../../types/equipmentTypes.ts"; + +interface IModularCraftRequest { + WeaponType: string; + Parts: string[]; + isWebUi?: boolean; +} + +export const modularWeaponCraftingController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const data = getJSONfromString(String(req.body)); + if (!(data.WeaponType in modularWeaponTypes)) { + throw new Error(`unknown modular weapon type: ${data.WeaponType}`); + } + const category = modularWeaponTypes[data.WeaponType]; + const inventory = await getInventory(accountId); + + 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 = []; + 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); + } + + await inventory.save(); + // Tell client what we did + res.json({ + InventoryChanges: { + ...inventoryChanges, + ...currencyChanges, + MiscItems: miscItemChanges + } + }); + sendWsBroadcastTo(accountId, { update_inventory: true }); +}; diff --git a/src/controllers/api/modularWeaponSaleController.ts b/src/controllers/api/modularWeaponSaleController.ts new file mode 100644 index 00000000..71b18f3f --- /dev/null +++ b/src/controllers/api/modularWeaponSaleController.ts @@ -0,0 +1,186 @@ +import type { RequestHandler } from "express"; +import { ExportWeapons } from "warframe-public-export-plus"; +import type { IMongoDate } from "../../types/commonTypes.ts"; +import { toMongoDate } from "../../helpers/inventoryHelpers.ts"; +import { SRng } from "../../services/rngService.ts"; +import type { ArtifactPolarity } from "../../types/inventoryTypes/commonInventoryTypes.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { + addEquipment, + applyDefaultUpgrades, + getInventory, + occupySlot, + productCategoryToInventoryBin, + updateCurrency +} from "../../services/inventoryService.ts"; +import { getDefaultUpgrades } from "../../services/itemDataService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { sendWsBroadcastTo } from "../../services/wsService.ts"; +import { modularWeaponTypes } from "../../helpers/modularWeaponHelper.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; +import { EquipmentFeatures } from "../../types/equipmentTypes.ts"; + +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 + }); + sendWsBroadcastTo(accountId, { update_inventory: true }); + } else { + throw new Error(`unknown modularWeaponSale op: ${String(req.query.op)}`); + } +}; + +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 new file mode 100644 index 00000000..ece87fef --- /dev/null +++ b/src/controllers/api/nameWeaponController.ts @@ -0,0 +1,32 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getInventory, updateCurrency } from "../../services/inventoryService.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import type { TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts"; +import { sendWsBroadcastTo } from "../../services/wsService.ts"; + +interface INameWeaponRequest { + ItemName: string; +} + +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].id(req.query.ItemId as string)!; + if (body.ItemName != "") { + item.ItemName = body.ItemName; + } else { + item.ItemName = undefined; + } + const currencyChanges = updateCurrency( + inventory, + req.query.Category == "Horses" || "webui" in req.query ? 0 : 15, + true + ); + await inventory.save(); + res.json({ + InventoryChanges: currencyChanges + }); + sendWsBroadcastTo(accountId, { update_inventory: true }); +}; diff --git a/src/controllers/api/nemesisController.ts b/src/controllers/api/nemesisController.ts new file mode 100644 index 00000000..71329285 --- /dev/null +++ b/src/controllers/api/nemesisController.ts @@ -0,0 +1,446 @@ +import { fromDbOid, version_compare } from "../../helpers/inventoryHelpers.ts"; +import type { IKnifeResponse } from "../../helpers/nemesisHelpers.ts"; +import { + antivirusMods, + decodeNemesisGuess, + encodeNemesisGuess, + getInfNodes, + getKnifeUpgrade, + getNemesisManifest, + getNemesisPasscode, + GUESS_CORRECT, + GUESS_INCORRECT, + GUESS_NEUTRAL, + GUESS_NONE, + GUESS_WILDCARD, + parseUpgrade +} from "../../helpers/nemesisHelpers.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts"; +import { Loadout } from "../../models/inventoryModels/loadoutModel.ts"; +import { addMods, freeUpSlot, getInventory } from "../../services/inventoryService.ts"; +import { getAccountForRequest } from "../../services/loginService.ts"; +import { SRng } from "../../services/rngService.ts"; +import type { IMongoDate, IOid } from "../../types/commonTypes.ts"; +import type { IEquipmentClient } from "../../types/equipmentTypes.ts"; +import type { + IInnateDamageFingerprint, + IInventoryClient, + INemesisClient, + IUpgradeClient, + IWeaponSkinClient, + TEquipmentKey, + TNemesisFaction +} from "../../types/inventoryTypes/inventoryTypes.ts"; +import { InventorySlot, LoadoutIndex } from "../../types/inventoryTypes/inventoryTypes.ts"; +import { logger } from "../../utils/logger.ts"; +import type { RequestHandler } from "express"; +import { Types } from "mongoose"; + +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] || body.guess[i] == GUESS_WILDCARD) { + ++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]; + const result1 = passcode == guess[0] ? GUESS_CORRECT : GUESS_INCORRECT; + const result2 = passcode == guess[1] ? GUESS_CORRECT : GUESS_INCORRECT; + const result3 = passcode == guess[2] ? GUESS_CORRECT : GUESS_INCORRECT; + inventory.Nemesis!.GuessHistory.push( + encodeNemesisGuess([ + { + symbol: guess[0], + result: result1 + }, + { + symbol: guess[1], + result: result2 + }, + { + symbol: guess[2], + result: result3 + } + ]) + ); + + // Increase antivirus if correct antivirus mod is installed + const response: IKnifeResponse = {}; + if (result1 == GUESS_CORRECT || result2 == GUESS_CORRECT || result3 == GUESS_CORRECT) { + let antivirusGain = 5; + const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!; + const dataknifeLoadout = loadout.DATAKNIFE.id( + fromDbOid(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE]) + ); + 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": + case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod": + case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod": + antivirusGain += 10; + consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); + break; + case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod": // Instant Secure + case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod": // Immuno Shield + antivirusGain += 15; + consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); + break; + } + } + inventory.Nemesis!.HenchmenKilled += antivirusGain; + if (inventory.Nemesis!.HenchmenKilled >= 100) { + inventory.Nemesis!.HenchmenKilled = 100; + + // Weaken nemesis now. + inventory.Nemesis!.InfNodes = [ + { + Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode, + Influence: 1 + } + ]; + inventory.Nemesis!.Weakened = true; + const upgrade = getKnifeUpgrade(inventory, dataknifeUpgrades, antivirusMods[passcode]); + consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); + } + } + + if (inventory.Nemesis!.HenchmenKilled < 100) { + inventory.Nemesis!.InfNodes = getInfNodes(getNemesisManifest(inventory.Nemesis!.manifest), 0); + } + + await inventory.save(); + res.json(response); + } else { + // For first guess, create a new entry. + if (body.position == 0) { + inventory.Nemesis!.GuessHistory.push( + encodeNemesisGuess([ + { + symbol: GUESS_NONE, + result: GUESS_NEUTRAL + }, + { + symbol: GUESS_NONE, + result: GUESS_NEUTRAL + }, + { + symbol: GUESS_NONE, + result: GUESS_NEUTRAL + } + ]) + ); + } + + // Evaluate guess + const correct = + body.guess == GUESS_WILDCARD || getNemesisPasscode(inventory.Nemesis!)[body.position] == body.guess; + + // Update entry + const guess = decodeNemesisGuess( + inventory.Nemesis!.GuessHistory[inventory.Nemesis!.GuessHistory.length - 1] + ); + guess[body.position].symbol = body.guess; + guess[body.position].result = correct ? GUESS_CORRECT : GUESS_INCORRECT; + inventory.Nemesis!.GuessHistory[inventory.Nemesis!.GuessHistory.length - 1] = encodeNemesisGuess(guess); + + const response: INemesisRequiemResponse = {}; + if (correct) { + if (body.position == 2) { + // That was all 3 guesses correct, nemesis is now weakened. + inventory.Nemesis!.InfNodes = [ + { + Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode, + Influence: 1 + } + ]; + inventory.Nemesis!.Weakened = true; + + // Subtract a charge from all requiem mods installed on parazon + const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!; + const dataknifeLoadout = loadout.DATAKNIFE.id( + fromDbOid(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE]) + ); + const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0; + const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!; + for (let i = 3; i != 6; ++i) { + //logger.debug(`subtracting a charge from ${dataknifeUpgrades[i]}`); + const upgrade = parseUpgrade(inventory, dataknifeUpgrades[i]); + consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); + } + } + } else { + // Guess was incorrect, increase rank + response.RankIncrease = 1; + const manifest = getNemesisManifest(inventory.Nemesis!.manifest); + inventory.Nemesis!.Rank = Math.min(inventory.Nemesis!.Rank + 1, manifest.systemIndexes.length - 1); + inventory.Nemesis!.InfNodes = getInfNodes(manifest, inventory.Nemesis!.Rank); + } + await inventory.save(); + res.json(response); + } + } 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"); + if (inventory.Nemesis) { + logger.warn(`overwriting an existing nemesis as a new one is being requested`); + } + const body = getJSONfromString(String(req.body)); + body.target.fp = BigInt(body.target.fp); + + const manifest = getNemesisManifest(body.target.manifest); + if (account.BuildLabel && version_compare(account.BuildLabel, manifest.minBuild) < 0) { + logger.warn( + `client on version ${account.BuildLabel} provided nemesis manifest ${body.target.manifest} which was expected to require ${manifest.minBuild} or above. please file a bug report.` + ); + } + + 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; + if (body.target.DisallowedWeapons) { + do { + const weapon = weapons[weaponIdx]; + if (body.target.DisallowedWeapons.indexOf(weapon) == -1) { + break; + } + weaponIdx = (weaponIdx + 1) % weapons.length; + } while (weaponIdx != initialWeaponIdx); + } + } + + inventory.Nemesis = { + 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: false, + PrevOwners: 0, + HenchmenKilled: 0, + SecondInCommand: false, + 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"); + //const body = getJSONfromString(String(req.body)); + + // As of 38.6.0, this request is no longer sent, instead mode=r already weakens the nemesis if appropriate. + // We always weaken the nemesis in mode=r so simply giving the client back the nemesis. + + const response: INemesisWeakenResponse = { + target: inventory.toJSON().Nemesis! + }; + 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 INemesisRequiemResponse extends IKnifeResponse { + RankIncrease?: number; +} + +// interface INemesisWeakenRequest { +// target: INemesisClient; +// knife: IKnife; +// } + +interface INemesisWeakenResponse extends IKnifeResponse { + target: INemesisClient; +} + +interface IKnife { + Item: IEquipmentClient; + Skins: IWeaponSkinClient[]; + ModSlot: number; + CustSlot: number; + AttachedUpgrades: IUpgradeClient[]; + HiddenWhenHolstered: boolean; +} + +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); + } +}; diff --git a/src/controllers/api/placeDecoInComponentController.ts b/src/controllers/api/placeDecoInComponentController.ts new file mode 100644 index 00000000..a56aae67 --- /dev/null +++ b/src/controllers/api/placeDecoInComponentController.ts @@ -0,0 +1,138 @@ +import { + getDojoClient, + getGuildForRequestEx, + getVaultMiscItemCount, + hasAccessToDojo, + hasGuildPermission, + processDojoBuildMaterialsGathered, + scaleRequiredCount +} from "../../services/guildService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import type { RequestHandler } from "express"; +import { Types } from "mongoose"; +import { ExportDojoRecipes, ExportResources } from "warframe-public-export-plus"; +import { config } from "../../services/configService.ts"; + +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 entry = Object.entries(ExportResources).find(arr => arr[1].deco == deco.Type); + if (!entry) { + throw new Error(`unknown deco type: ${deco.Type}`); + } + const [itemType, meta] = entry; + if (meta.dojoCapacityCost === undefined) { + throw new Error(`unknown deco type: ${deco.Type}`); + } + component.DecoCapacity -= meta.dojoCapacityCost; + 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 ( + deco.Type.startsWith("/Lotus/Objects/Tenno/Dojo/NpcPlaceables/") || + (guild.AutoContributeFromVault && guild.VaultRegularCredits && guild.VaultMiscItems) + ) { + if (!guild.VaultRegularCredits || !guild.VaultMiscItems) { + throw new Error(`dojo visitor placed without anything in vault?!`); + } + 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..6197c08f --- /dev/null +++ b/src/controllers/api/playedParkourTutorialController.ts @@ -0,0 +1,9 @@ +import { Inventory } from "../../models/inventoryModels/inventoryModel.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { 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 new file mode 100644 index 00000000..0a75ff1b --- /dev/null +++ b/src/controllers/api/playerSkillsController.ts @@ -0,0 +1,58 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { addConsumables, getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IPlayerSkills } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; +import type { RequestHandler } from "express"; + +export const playerSkillsController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "PlayerSkills Consumables"); + const request = getJSONfromString(String(req.body)); + + const oldRank: number = inventory.PlayerSkills[request.Skill as keyof IPlayerSkills]; + const cost = (request.Pool == "LPP_DRIFTER" ? drifterCosts[oldRank] : 1 << oldRank) * 1000; + inventory.PlayerSkills[request.Pool as keyof IPlayerSkills] -= cost; + inventory.PlayerSkills[request.Skill as keyof IPlayerSkills]++; + + const inventoryChanges: IInventoryChanges = {}; + if (request.Skill == "LPS_COMMAND") { + if (inventory.PlayerSkills.LPS_COMMAND == 9) { + const consumablesChanges = [ + { + ItemType: "/Lotus/Types/Restoratives/Consumable/CrewmateBall", + ItemCount: 1 + } + ]; + addConsumables(inventory, consumablesChanges); + inventoryChanges.Consumables = consumablesChanges; + } + } else if (request.Skill == "LPS_DRIFT_RIDING") { + if (inventory.PlayerSkills.LPS_DRIFT_RIDING == 9) { + const consumablesChanges = [ + { + ItemType: "/Lotus/Types/Restoratives/ErsatzSummon", + ItemCount: 1 + } + ]; + addConsumables(inventory, consumablesChanges); + inventoryChanges.Consumables = consumablesChanges; + } + } + + await inventory.save(); + res.json({ + Pool: request.Pool, + PoolInc: -cost, + Skill: request.Skill, + Rank: oldRank + 1, + InventoryChanges: inventoryChanges + }); +}; + +interface IPlayerSkillsRequest { + Pool: string; + Skill: string; +} + +const drifterCosts = [20, 25, 30, 45, 65, 90, 125, 160, 205, 255]; diff --git a/src/controllers/api/postGuildAdvertisementController.ts b/src/controllers/api/postGuildAdvertisementController.ts new file mode 100644 index 00000000..308b1f96 --- /dev/null +++ b/src/controllers/api/postGuildAdvertisementController.ts @@ -0,0 +1,75 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { GuildAd, GuildMember } from "../../models/guildModel.ts"; +import { + addGuildMemberMiscItemContribution, + addVaultMiscItems, + getGuildForRequestEx, + getVaultMiscItemCount, + hasGuildPermissionEx +} from "../../services/guildService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getVendorManifestByTypeName } from "../../services/serversideVendorsService.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import type { IPurchaseParams } from "../../types/purchaseTypes.ts"; +import type { 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 new file mode 100644 index 00000000..366f07ef --- /dev/null +++ b/src/controllers/api/projectionManagerController.ts @@ -0,0 +1,68 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { addMiscItems, getInventory } from "../../services/inventoryService.ts"; +import type { IRelic } from "warframe-public-export-plus"; +import { ExportRelics } from "warframe-public-export-plus"; + +export const projectionManagerController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "MiscItems dontSubtractVoidTraces"); + const request = JSON.parse(String(req.body)) as IProjectionUpgradeRequest; + const [era, category, currentQuality] = parseProjection(request.projectionType); + const upgradeCost = inventory.dontSubtractVoidTraces + ? 0 + : qualityNumberToCost[request.qualityTag] - qualityNumberToCost[qualityKeywordToNumber[currentQuality]]; + const newProjectionType = findProjection(era, category, qualityNumberToKeyword[request.qualityTag]); + addMiscItems(inventory, [ + { + ItemType: request.projectionType, + ItemCount: -1 + }, + { + ItemType: newProjectionType, + ItemCount: 1 + }, + { + ItemType: "/Lotus/Types/Items/MiscItems/VoidTearDrop", + ItemCount: -upgradeCost + } + ]); + await inventory.save(); + res.json({ + prevProjection: request.projectionType, + upgradedProjection: newProjectionType, + upgradeCost: upgradeCost + }); +}; + +interface IProjectionUpgradeRequest { + projectionType: string; + qualityTag: number; +} + +type VoidProjectionQuality = "VPQ_BRONZE" | "VPQ_SILVER" | "VPQ_GOLD" | "VPQ_PLATINUM"; + +const qualityNumberToKeyword: VoidProjectionQuality[] = ["VPQ_BRONZE", "VPQ_SILVER", "VPQ_GOLD", "VPQ_PLATINUM"]; +const qualityKeywordToNumber: Record = { + VPQ_BRONZE: 0, + VPQ_SILVER: 1, + VPQ_GOLD: 2, + VPQ_PLATINUM: 3 +}; +const qualityNumberToCost = [0, 25, 50, 100]; + +// 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}`); + } + return [relic.era, relic.category, relic.quality]; +}; + +const findProjection = (era: string, category: string, quality: VoidProjectionQuality): string => { + return Object.entries(ExportRelics).find( + ([_, relic]) => relic.era == era && relic.category == category && relic.quality == quality + )![0]; +}; diff --git a/src/controllers/api/purchaseController.ts b/src/controllers/api/purchaseController.ts new file mode 100644 index 00000000..4658da51 --- /dev/null +++ b/src/controllers/api/purchaseController.ts @@ -0,0 +1,71 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IPurchaseRequest } from "../../types/purchaseTypes.ts"; +import { handlePurchase } from "../../services/purchaseService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { sendWsBroadcastTo } from "../../services/wsService.ts"; +import { logger } from "../../utils/logger.ts"; + +export const purchaseController: RequestHandler = async (req, res) => { + const purchaseRequest = JSON.parse(String(req.body)) as IPurchaseRequest; + const accountId = await getAccountIdForRequest(req); + + try { + const inventory = await getInventory(accountId); + + // 记录购买前的货币状态 + const beforePurchase = { + RegularCredits: inventory.RegularCredits, + PremiumCredits: inventory.PremiumCredits, + PremiumCreditsFree: inventory.PremiumCreditsFree + }; + + logger.debug(`Purchase attempt - Account: ${accountId}, Item: ${purchaseRequest.PurchaseParams.StoreItem}, Price: ${purchaseRequest.PurchaseParams.ExpectedPrice}, UsePremium: ${purchaseRequest.PurchaseParams.UsePremium}`); + logger.debug(`Currency before purchase:`, beforePurchase); + + const response = await handlePurchase(purchaseRequest, inventory); + await inventory.save(); + + // 记录购买后的货币状态 + const afterPurchase = { + RegularCredits: inventory.RegularCredits, + PremiumCredits: inventory.PremiumCredits, + PremiumCreditsFree: inventory.PremiumCreditsFree + }; + + logger.debug(`Currency after purchase:`, afterPurchase); + + res.json(response); + + // 立即发送货币更新广播,确保客户端实时同步 + sendWsBroadcastTo(accountId, { + update_inventory: true, + currency_update: { + RegularCredits: inventory.RegularCredits, + PremiumCredits: inventory.PremiumCredits, + PremiumCreditsFree: inventory.PremiumCreditsFree + } + }); + + } catch (error) { + logger.error(`Purchase failed for account ${accountId}:`, error); + + // 返回详细的错误信息给客户端 + if (error instanceof Error && error.message.includes('Insufficient')) { + res.status(400).json({ + error: 'INSUFFICIENT_CURRENCY', + message: error.message, + details: { + item: purchaseRequest.PurchaseParams.StoreItem, + price: purchaseRequest.PurchaseParams.ExpectedPrice, + usePremium: purchaseRequest.PurchaseParams.UsePremium + } + }); + } else { + res.status(500).json({ + error: 'PURCHASE_FAILED', + message: error instanceof Error ? error.message : 'Unknown error occurred' + }); + } + } +}; diff --git a/src/controllers/api/questControlController.ts b/src/controllers/api/questControlController.ts new file mode 100644 index 00000000..6750fdf6 --- /dev/null +++ b/src/controllers/api/questControlController.ts @@ -0,0 +1,25 @@ +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { 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 new file mode 100644 index 00000000..98cddd70 --- /dev/null +++ b/src/controllers/api/queueDojoComponentDestructionController.ts @@ -0,0 +1,29 @@ +import { config } from "../../services/configService.ts"; +import { + getDojoClient, + getGuildForRequestEx, + hasAccessToDojo, + hasGuildPermission +} from "../../services/guildService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import type { RequestHandler } from "express"; + +export const queueDojoComponentDestructionController: 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 = new Date( + (Math.trunc(Date.now() / 1000) + (config.fastDojoRoomDestruction ? 5 : 2 * 3600)) * 1000 + ); + + await guild.save(); + 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..b81177b8 --- /dev/null +++ b/src/controllers/api/redeemPromoCodeController.ts @@ -0,0 +1,34 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import type { RequestHandler } from "express"; +import glyphCodes from "../../../static/fixed_responses/glyphsCodes.json" with { type: "json" }; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { addItem, getInventory } from "../../services/inventoryService.ts"; + +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..fb707b7a --- /dev/null +++ b/src/controllers/api/releasePetController.ts @@ -0,0 +1,29 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { getInventory, updateCurrency } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { sendWsBroadcastTo } from "../../services/wsService.ts"; +import type { 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. + sendWsBroadcastTo(accountId, { update_inventory: true }); +}; + +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..0631921a --- /dev/null +++ b/src/controllers/api/removeFriendController.ts @@ -0,0 +1,99 @@ +import { toOid } from "../../helpers/inventoryHelpers.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { Friendship } from "../../models/friendModel.ts"; +import { Account } from "../../models/loginModel.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IOid } from "../../types/commonTypes.ts"; +import { parallelForeach } from "../../utils/async-utils.ts"; +import type { RequestHandler } from "express"; +import type { 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..c425a24d --- /dev/null +++ b/src/controllers/api/removeFromAllianceController.ts @@ -0,0 +1,38 @@ +import { AllianceMember, Guild, GuildMember } from "../../models/guildModel.ts"; +import { deleteAlliance } from "../../services/guildService.ts"; +import { getAccountForRequest } from "../../services/loginService.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import type { 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..9535d4d7 --- /dev/null +++ b/src/controllers/api/removeFromGuildController.ts @@ -0,0 +1,93 @@ +import { GuildMember } from "../../models/guildModel.ts"; +import { Inbox } from "../../models/inboxModel.ts"; +import { Account } from "../../models/loginModel.ts"; +import { + deleteGuild, + getGuildForRequest, + hasGuildPermission, + removeDojoKeyItems +} from "../../services/guildService.ts"; +import { createMessage } from "../../services/inboxService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountForRequest, getSuffixedName } from "../../services/loginService.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import type { 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..21e0722b --- /dev/null +++ b/src/controllers/api/removeIgnoredUserController.ts @@ -0,0 +1,21 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { Account, Ignore } from "../../models/loginModel.ts"; +import { getAccountForRequest } from "../../services/loginService.ts"; +import type { 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/renamePetController.ts b/src/controllers/api/renamePetController.ts new file mode 100644 index 00000000..d1f0cc3b --- /dev/null +++ b/src/controllers/api/renamePetController.ts @@ -0,0 +1,32 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { getInventory, updateCurrency } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { sendWsBroadcastTo } from "../../services/wsService.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; +import type { RequestHandler } from "express"; + +export const renamePetController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "KubrowPets PremiumCredits PremiumCreditsFree"); + const data = getJSONfromString(String(req.body)); + const details = inventory.KubrowPets.id(data.petId)!.Details!; + + details.Name = data.name; + + const inventoryChanges: IInventoryChanges = {}; + if (!("webui" in req.query)) { + updateCurrency(inventory, 15, true, inventoryChanges); + } + + await inventory.save(); + res.json({ + ...data, + inventoryChanges: inventoryChanges + }); + sendWsBroadcastTo(accountId, { update_inventory: true }); +}; + +interface IRenamePetRequest { + petId: string; + name: string; +} diff --git a/src/controllers/api/rerollRandomModController.ts b/src/controllers/api/rerollRandomModController.ts new file mode 100644 index 00000000..cbed0276 --- /dev/null +++ b/src/controllers/api/rerollRandomModController.ts @@ -0,0 +1,84 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { addMiscItems, getInventory } from "../../services/inventoryService.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import type { RivenFingerprint } from "../../helpers/rivenHelper.ts"; +import { createUnveiledRivenFingerprint, randomiseRivenStats } from "../../helpers/rivenHelper.ts"; +import { ExportUpgrades } from "warframe-public-export-plus"; +import type { IOid } from "../../types/commonTypes.ts"; + +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 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++; + upgrade.UpgradeFingerprint = JSON.stringify(fingerprint); + + randomiseRivenStats(ExportUpgrades[upgrade.ItemType], fingerprint); + upgrade.PendingRerollFingerprint = JSON.stringify(fingerprint); + } + + changes.push({ + ItemId: { $oid: request.ItemIds[0] }, + UpgradeFingerprint: upgrade.UpgradeFingerprint, + PendingRerollFingerprint: upgrade.PendingRerollFingerprint + }); + }); + + await inventory.save(); + + res.json({ + changes: changes, + cost: totalKuvaCost + }); + } else { + const inventory = await getInventory(accountId, "Upgrades"); + const upgrade = inventory.Upgrades.id(request.ItemId)!; + if (request.CommitReroll && upgrade.PendingRerollFingerprint) { + upgrade.UpgradeFingerprint = upgrade.PendingRerollFingerprint; + } + upgrade.PendingRerollFingerprint = undefined; + await inventory.save(); + res.send(upgrade.UpgradeFingerprint); + } +}; + +type RerollRandomModRequest = LetsGoGamblingRequest | AwDangitRequest; + +interface LetsGoGamblingRequest { + ItemIds: string[]; +} + +interface AwDangitRequest { + ItemId: string; + CommitReroll: boolean; +} + +interface IChange { + ItemId: IOid; + UpgradeFingerprint?: string; + PendingRerollFingerprint?: string; +} + +const rerollCosts = [900, 1000, 1200, 1400, 1700, 2000, 2350, 2750, 3150]; diff --git a/src/controllers/api/resetQuestProgressController.ts b/src/controllers/api/resetQuestProgressController.ts new file mode 100644 index 00000000..ecb893fe --- /dev/null +++ b/src/controllers/api/resetQuestProgressController.ts @@ -0,0 +1,5 @@ +import type { RequestHandler } from "express"; + +export const resetQuestProgressController: RequestHandler = (_req, res) => { + res.send("1").end(); +}; diff --git a/src/controllers/api/retrievePetFromStasisController.ts b/src/controllers/api/retrievePetFromStasisController.ts new file mode 100644 index 00000000..be4194fc --- /dev/null +++ b/src/controllers/api/retrievePetFromStasisController.ts @@ -0,0 +1,33 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { Status } from "../../types/equipmentTypes.ts"; +import type { 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 new file mode 100644 index 00000000..0df66a65 --- /dev/null +++ b/src/controllers/api/saveDialogueController.ts @@ -0,0 +1,105 @@ +import { addEmailItem, getDialogue, getInventory, updateCurrency } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { ICompletedDialogue } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; +import type { 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, "DialogueHistory noKimCooldowns"); + inventory.DialogueHistory ??= {}; + inventory.DialogueHistory.YearIteration = request.YearIteration; + await inventory.save(); + res.end(); + } else { + const inventory = await getInventory(accountId); + const inventoryChanges: IInventoryChanges = {}; + const tomorrowAt0Utc = inventory.noKimCooldowns + ? Date.now() + : (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000; + const dialogue = getDialogue(inventory, request.DialogueName); + dialogue.Rank = request.Rank; + dialogue.Chemistry += request.Chemistry; + 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); + if (index != -1) { + dialogue.Booleans.splice(index, 1); + } + } + 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(); + } + } +}; + +type SaveDialogueRequest = SaveYearIterationRequest | SaveCompletedDialogueRequest; + +interface SaveYearIterationRequest { + YearIteration: number; +} + +interface SaveCompletedDialogueRequest { + DialogueName: string; + Rank: number; + Chemistry: number; + CompletionType: number; + QueuedDialogues: string[]; + Gift?: { + Item: string; + GainedChemistry: number; + Cost: number; + GiftedQuantity: number; + }; + Booleans: string[]; + ResetBooleans: string[]; + Data?: ICompletedDialogue; + OtherDialogueInfos: IOtherDialogueInfo[]; +} + +interface IOtherDialogueInfo { + Dialogue: string; + Tag: string; + Value: number; +} diff --git a/src/controllers/api/saveLoadoutController.ts b/src/controllers/api/saveLoadoutController.ts new file mode 100644 index 00000000..60431610 --- /dev/null +++ b/src/controllers/api/saveLoadoutController.ts @@ -0,0 +1,22 @@ +import type { RequestHandler } from "express"; +import type { ISaveLoadoutRequest } from "../../types/saveLoadoutTypes.ts"; +import { handleInventoryItemConfigChange } from "../../services/saveLoadoutService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; + +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 new file mode 100644 index 00000000..cc99c743 --- /dev/null +++ b/src/controllers/api/saveSettingsController.ts @@ -0,0 +1,22 @@ +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import type { RequestHandler } from "express"; +import type { ISettings } from "../../types/inventoryTypes/inventoryTypes.ts"; + +interface ISaveSettingsRequest { + Settings: ISettings; +} + +const saveSettingsController: RequestHandler = async (req, res): Promise => { + const accountId = await getAccountIdForRequest(req); + + const settingResults = getJSONfromString(String(req.body)); + + const inventory = await getInventory(accountId, "Settings"); + inventory.Settings = Object.assign(inventory.Settings ?? {}, settingResults.Settings); + await inventory.save(); + 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..7e91f5e1 --- /dev/null +++ b/src/controllers/api/saveVaultAutoContributeController.ts @@ -0,0 +1,25 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { Guild } from "../../models/guildModel.ts"; +import { hasGuildPermission } from "../../services/guildService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import type { 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 new file mode 100644 index 00000000..dd160c5a --- /dev/null +++ b/src/controllers/api/sellController.ts @@ -0,0 +1,384 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { + getInventory, + addMods, + addRecipes, + addMiscItems, + addConsumables, + freeUpSlot, + combineInventoryChanges, + addCrewShipRawSalvage, + addFusionPoints, + addCrewShipFusionPoints, + addFusionTreasures +} from "../../services/inventoryService.ts"; +import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts"; +import { ExportDojoRecipes } from "warframe-public-export-plus"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; +import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts"; +import { sendWsBroadcastEx } from "../../services/wsService.ts"; +import { parseFusionTreasure } from "../../helpers/inventoryHelpers.ts"; + +export const sellController: RequestHandler = async (req, res) => { + const payload = JSON.parse(String(req.body)) as ISellRequest; + //console.log(JSON.stringify(payload, null, 2)); + const accountId = await getAccountIdForRequest(req); + const requiredFields = new Set(); + if (payload.SellCurrency == "SC_RegularCredits") { + requiredFields.add("RegularCredits"); + } else if (payload.SellCurrency == "SC_FusionPoints") { + requiredFields.add("FusionPoints"); + } else if (payload.SellCurrency == "SC_CrewShipFusionPoints") { + requiredFields.add("CrewShipFusionPoints"); + } 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.CrewMembers) { + requiredFields.add(InventorySlot.CREWMEMBERS); + } + 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") { + addFusionPoints(inventory, payload.SellPrice); + } else if (payload.SellCurrency == "SC_CrewShipFusionPoints") { + addCrewShipFusionPoints(inventory, payload.SellPrice); + } else if (payload.SellCurrency == "SC_PrimeBucks") { + addMiscItems(inventory, [ + { + ItemType: "/Lotus/Types/Items/MiscItems/PrimeBucks", + ItemCount: payload.SellPrice + } + ]); + } else if (payload.SellCurrency == "SC_DistillPoints") { + addMiscItems(inventory, [ + { + ItemType: "/Lotus/Types/Items/MiscItems/DistillPoints", + 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.KubrowPetPrints) { + payload.Items.KubrowPetPrints.forEach(sellItem => { + inventory.KubrowPetPrints.pull({ _id: sellItem.String }); + }); + } + if (payload.Items.CrewMembers) { + payload.Items.CrewMembers.forEach(sellItem => { + inventory.CrewMembers.pull({ _id: sellItem.String }); + freeUpSlot(inventory, InventorySlot.CREWMEMBERS); + }); + } + 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) { + const consumablesChanges = []; + for (const sellItem of payload.Items.Consumables) { + consumablesChanges.push({ + ItemType: sellItem.String, + ItemCount: sellItem.Count * -1 + }); + } + addConsumables(inventory, consumablesChanges); + } + if (payload.Items.Recipes) { + const recipeChanges = []; + for (const sellItem of payload.Items.Recipes) { + recipeChanges.push({ + ItemType: sellItem.String, + ItemCount: sellItem.Count * -1 + }); + } + addRecipes(inventory, recipeChanges); + } + if (payload.Items.Upgrades) { + payload.Items.Upgrades.forEach(sellItem => { + if (sellItem.Count == 0) { + inventory.Upgrades.pull({ _id: sellItem.String }); + } else { + addMods(inventory, [ + { + ItemType: sellItem.String, + ItemCount: sellItem.Count * -1 + } + ]); + } + }); + } + if (payload.Items.MiscItems) { + payload.Items.MiscItems.forEach(sellItem => { + addMiscItems(inventory, [ + { + ItemType: sellItem.String, + ItemCount: sellItem.Count * -1 + } + ]); + }); + } + if (payload.Items.FusionTreasures) { + payload.Items.FusionTreasures.forEach(sellItem => { + addFusionTreasures(inventory, [parseFusionTreasure(sellItem.String, sellItem.Count * -1)]); + }); + } + + await inventory.save(); + res.json({ + inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges" + }); + sendWsBroadcastEx({ update_inventory: true }, accountId, parseInt(String(req.query.wsid))); +}; + +interface ISellRequest { + Items: { + Suits?: ISellItem[]; + LongGuns?: ISellItem[]; + Pistols?: ISellItem[]; + Melee?: ISellItem[]; + Consumables?: ISellItem[]; + Recipes?: ISellItem[]; + Upgrades?: ISellItem[]; + MiscItems?: ISellItem[]; + SpaceSuits?: ISellItem[]; + SpaceGuns?: ISellItem[]; + SpaceMelee?: ISellItem[]; + MechSuits?: ISellItem[]; + Sentinels?: ISellItem[]; + SentinelWeapons?: ISellItem[]; + MoaPets?: ISellItem[]; + OperatorAmps?: ISellItem[]; + Hoverboards?: ISellItem[]; + Drones?: ISellItem[]; + KubrowPetPrints?: ISellItem[]; + CrewMembers?: ISellItem[]; + CrewShipWeapons?: ISellItem[]; + CrewShipWeaponSkins?: ISellItem[]; + FusionTreasures?: ISellItem[]; + }; + SellPrice: number; + SellCurrency: + | "SC_RegularCredits" + | "SC_PrimeBucks" + | "SC_FusionPoints" + | "SC_DistillPoints" + | "SC_CrewShipFusionPoints" + | "SC_Resources" + | "somethingelsewemightnotknowabout"; + buildLabel: string; +} + +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..57731d56 --- /dev/null +++ b/src/controllers/api/sendMsgToInBoxController.ts @@ -0,0 +1,31 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { createMessage } from "../../services/inboxService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { 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/setActiveQuestController.ts b/src/controllers/api/setActiveQuestController.ts new file mode 100644 index 00000000..969398b3 --- /dev/null +++ b/src/controllers/api/setActiveQuestController.ts @@ -0,0 +1,18 @@ +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { RequestHandler } from "express"; + +export const setActiveQuestController: RequestHandler< + Record, + undefined, + undefined, + { quest: string | undefined } +> = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const quest = req.query.quest; + + const inventory = await getInventory(accountId, "ActiveQuest"); + inventory.ActiveQuest = quest ?? ""; + await inventory.save(); + res.status(200).end(); +}; diff --git a/src/controllers/api/setActiveShipController.ts b/src/controllers/api/setActiveShipController.ts new file mode 100644 index 00000000..b21baaba --- /dev/null +++ b/src/controllers/api/setActiveShipController.ts @@ -0,0 +1,15 @@ +import { getPersonalRooms } from "../../services/personalRoomsService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { parseString } from "../../helpers/general.ts"; +import type { RequestHandler } from "express"; +import { Types } from "mongoose"; + +export const setActiveShipController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const shipId = parseString(req.query.shipId); + + const personalRooms = await getPersonalRooms(accountId); + personalRooms.activeShipId = new Types.ObjectId(shipId); + await personalRooms.save(); + res.status(200).end(); +}; diff --git a/src/controllers/api/setAllianceGuildPermissionsController.ts b/src/controllers/api/setAllianceGuildPermissionsController.ts new file mode 100644 index 00000000..17f1555d --- /dev/null +++ b/src/controllers/api/setAllianceGuildPermissionsController.ts @@ -0,0 +1,38 @@ +import { AllianceMember, GuildMember } from "../../models/guildModel.ts"; +import { getAccountForRequest } from "../../services/loginService.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import type { 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/setBootLocationController.ts b/src/controllers/api/setBootLocationController.ts new file mode 100644 index 00000000..ecb13729 --- /dev/null +++ b/src/controllers/api/setBootLocationController.ts @@ -0,0 +1,24 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getPersonalRooms } from "../../services/personalRoomsService.ts"; +import type { TBootLocation } from "../../types/personalRoomsTypes.ts"; +import { getInventory } from "../../services/inventoryService.ts"; + +export const setBootLocationController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const personalRooms = await getPersonalRooms(accountId); + personalRooms.Ship.BootLocation = req.query.bootLocation as string as TBootLocation; + await personalRooms.save(); + + if (personalRooms.Ship.BootLocation == "SHOP") { + // Temp fix so the motorcycle in the backroom doesn't appear broken. + // This code may be removed when quests are fully implemented. + const inventory = await getInventory(accountId); + if (inventory.Motorcycles.length == 0) { + inventory.Motorcycles.push({ ItemType: "/Lotus/Types/Vehicles/Motorcycle/MotorcyclePowerSuit" }); + await inventory.save(); + } + } + + res.end(); +}; diff --git a/src/controllers/api/setDojoComponentColorsController.ts b/src/controllers/api/setDojoComponentColorsController.ts new file mode 100644 index 00000000..96203a9b --- /dev/null +++ b/src/controllers/api/setDojoComponentColorsController.ts @@ -0,0 +1,39 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { + getDojoClient, + getGuildForRequestEx, + hasAccessToDojo, + hasGuildPermission +} from "../../services/guildService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import type { 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 new file mode 100644 index 00000000..04788616 --- /dev/null +++ b/src/controllers/api/setDojoComponentMessageController.ts @@ -0,0 +1,18 @@ +import type { RequestHandler } from "express"; +import { getDojoClient, getGuildForRequest } from "../../services/guildService.ts"; + +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.id(req.query.componentId as string)!; + const payload = JSON.parse(String(req.body)) as SetDojoComponentMessageRequest; + if ("Name" in payload) { + component.Name = payload.Name; + } else { + component.Message = payload.Message; + } + await guild.save(); + 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..5c8d4fa3 --- /dev/null +++ b/src/controllers/api/setDojoComponentSettingsController.ts @@ -0,0 +1,30 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { + getDojoClient, + getGuildForRequestEx, + hasAccessToDojo, + hasGuildPermission +} from "../../services/guildService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import type { 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 new file mode 100644 index 00000000..3d4876e1 --- /dev/null +++ b/src/controllers/api/setEquippedInstrumentController.ts @@ -0,0 +1,24 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { Inventory } from "../../models/inventoryModels/inventoryModel.ts"; + +export const setEquippedInstrumentController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const body = getJSONfromString(String(req.body)); + + await Inventory.updateOne( + { + accountOwnerId: accountId + }, + { + EquippedInstrument: body.Instrument + } + ); + + res.end(); +}; + +interface ISetEquippedInstrumentRequest { + Instrument: string; +} diff --git a/src/controllers/api/setFriendNoteController.ts b/src/controllers/api/setFriendNoteController.ts new file mode 100644 index 00000000..835963c3 --- /dev/null +++ b/src/controllers/api/setFriendNoteController.ts @@ -0,0 +1,30 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { Friendship } from "../../models/friendModel.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { 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..8007bab0 --- /dev/null +++ b/src/controllers/api/setGuildMotdController.ts @@ -0,0 +1,65 @@ +import { version_compare } from "../../helpers/inventoryHelpers.ts"; +import { Alliance, Guild, GuildMember } from "../../models/guildModel.ts"; +import { hasGuildPermissionEx } from "../../services/guildService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountForRequest, getSuffixedName } from "../../services/loginService.ts"; +import type { ILongMOTD } from "../../types/guildTypes.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import type { 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..1f0326a4 --- /dev/null +++ b/src/controllers/api/setHubNpcCustomizationsController.ts @@ -0,0 +1,21 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IHubNpcCustomization } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { 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 new file mode 100644 index 00000000..0281a0c2 --- /dev/null +++ b/src/controllers/api/setPlacedDecoInfoController.ts @@ -0,0 +1,19 @@ +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { ISetPlacedDecoInfoRequest } from "../../types/personalRoomsTypes.ts"; +import type { RequestHandler } from "express"; +import { handleSetPlacedDecoInfo } from "../../services/shipCustomizationsService.ts"; + +export const setPlacedDecoInfoController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const payload = JSON.parse(req.body as string) as ISetPlacedDecoInfoRequest; + //console.log(JSON.stringify(payload, null, 2)); + await handleSetPlacedDecoInfo(accountId, payload); + res.json({ + ...payload, + IsPicture: !!payload.PictureFrameInfo + } satisfies ISetPlacedDecoInfoResponse); +}; + +interface ISetPlacedDecoInfoResponse extends ISetPlacedDecoInfoRequest { + IsPicture: boolean; +} diff --git a/src/controllers/api/setShipCustomizationsController.ts b/src/controllers/api/setShipCustomizationsController.ts new file mode 100644 index 00000000..de8cc40b --- /dev/null +++ b/src/controllers/api/setShipCustomizationsController.ts @@ -0,0 +1,20 @@ +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { setShipCustomizations } from "../../services/shipCustomizationsService.ts"; +import type { ISetShipCustomizationsRequest } from "../../types/personalRoomsTypes.ts"; +import { logger } from "../../utils/logger.ts"; +import type { RequestHandler } from "express"; + +export const setShipCustomizationsController: RequestHandler = async (req, res) => { + try { + const accountId = await getAccountIdForRequest(req); + const setShipCustomizationsRequest = JSON.parse(req.body as string) as ISetShipCustomizationsRequest; + + const setShipCustomizationsResponse = await setShipCustomizations(accountId, setShipCustomizationsRequest); + res.json(setShipCustomizationsResponse); + } catch (error: unknown) { + if (error instanceof Error) { + logger.error(`error in setShipCustomizationsController: ${error.message}`); + res.status(400).json({ error: error.message }); + } + } +}; diff --git a/src/controllers/api/setShipFavouriteLoadoutController.ts b/src/controllers/api/setShipFavouriteLoadoutController.ts new file mode 100644 index 00000000..1fe696bc --- /dev/null +++ b/src/controllers/api/setShipFavouriteLoadoutController.ts @@ -0,0 +1,42 @@ +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { RequestHandler } from "express"; +import { getPersonalRooms } from "../../services/personalRoomsService.ts"; +import type { IOid } from "../../types/commonTypes.ts"; +import { Types } from "mongoose"; +import type { IFavouriteLoadoutDatabase, TBootLocation } from "../../types/personalRoomsTypes.ts"; + +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 == "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}`); + } + await personalRooms.save(); + res.json(body); +}; + +interface ISetShipFavouriteLoadoutRequest { + BootLocation: TBootLocation; + FavouriteLoadoutId: IOid; + 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..22e2f2b1 --- /dev/null +++ b/src/controllers/api/setShipVignetteController.ts @@ -0,0 +1,48 @@ +import { addMiscItems, combineInventoryChanges, getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getPersonalRooms } from "../../services/personalRoomsService.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; +import { logger } from "../../utils/logger.ts"; +import type { 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/setSuitInfectionController.ts b/src/controllers/api/setSuitInfectionController.ts new file mode 100644 index 00000000..6b9952e1 --- /dev/null +++ b/src/controllers/api/setSuitInfectionController.ts @@ -0,0 +1,22 @@ +import { fromMongoDate, fromOid } from "../../helpers/inventoryHelpers.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IEquipmentClient } from "../../types/equipmentTypes.ts"; +import type { RequestHandler } from "express"; + +export const setSuitInfectionController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "Suits"); + const payload = getJSONfromString(String(req.body)); + for (const clientSuit of payload.Suits) { + const dbSuit = inventory.Suits.id(fromOid(clientSuit.ItemId))!; + dbSuit.InfestationDate = fromMongoDate(clientSuit.InfestationDate!); + } + await inventory.save(); + res.end(); +}; + +interface ISetSuitInfectionRequest { + Suits: IEquipmentClient[]; +} diff --git a/src/controllers/api/setSupportedSyndicateController.ts b/src/controllers/api/setSupportedSyndicateController.ts new file mode 100644 index 00000000..53db10b9 --- /dev/null +++ b/src/controllers/api/setSupportedSyndicateController.ts @@ -0,0 +1,18 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { Inventory } from "../../models/inventoryModels/inventoryModel.ts"; + +export const setSupportedSyndicateController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + + 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 new file mode 100644 index 00000000..2b7ae409 --- /dev/null +++ b/src/controllers/api/setWeaponSkillTreeController.ts @@ -0,0 +1,29 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { Inventory } from "../../models/inventoryModels/inventoryModel.ts"; +import type { TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts"; +import { equipmentKeys } from "../../types/inventoryTypes/inventoryTypes.ts"; + +export const setWeaponSkillTreeController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const payload = getJSONfromString(String(req.body)); + + 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 + } + ); + } + + res.end(); +}; + +interface ISetWeaponSkillTreeRequest { + SkillTree: string; +} diff --git a/src/controllers/api/shipDecorationsController.ts b/src/controllers/api/shipDecorationsController.ts new file mode 100644 index 00000000..ec318f82 --- /dev/null +++ b/src/controllers/api/shipDecorationsController.ts @@ -0,0 +1,17 @@ +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IShipDecorationsRequest, IResetShipDecorationsRequest } from "../../types/personalRoomsTypes.ts"; +import type { RequestHandler } from "express"; +import { handleResetShipDecorations, handleSetShipDecorations } from "../../services/shipCustomizationsService.ts"; + +export const shipDecorationsController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + if (req.query.reset == "1") { + const request = JSON.parse(req.body as string) as IResetShipDecorationsRequest; + const response = await handleResetShipDecorations(accountId, request); + res.send(response); + } else { + const shipDecorationsRequest = JSON.parse(req.body as string) as IShipDecorationsRequest; + const placedDecoration = await handleSetShipDecorations(accountId, shipDecorationsRequest); + res.send(placedDecoration); + } +}; diff --git a/src/controllers/api/startCollectibleEntryController.ts b/src/controllers/api/startCollectibleEntryController.ts new file mode 100644 index 00000000..639d239a --- /dev/null +++ b/src/controllers/api/startCollectibleEntryController.ts @@ -0,0 +1,27 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IIncentiveState } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { 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 new file mode 100644 index 00000000..69b9e58e --- /dev/null +++ b/src/controllers/api/startDojoRecipeController.ts @@ -0,0 +1,69 @@ +import type { RequestHandler } from "express"; +import type { IDojoComponentClient } from "../../types/guildTypes.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import { + getDojoClient, + getGuildForRequestEx, + hasAccessToDojo, + hasGuildPermission, + processDojoBuildMaterialsGathered, + setDojoRoomLogFunded +} from "../../services/guildService.ts"; +import { Types } from "mongoose"; +import { ExportDojoRecipes } from "warframe-public-export-plus"; +import { config } from "../../services/configService.ts"; +import { getAccountForRequest } from "../../services/loginService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; + +interface IStartDojoRecipeRequest { + PlacedComponent: IDojoComponentClient; + Revision: number; +} + +export const startDojoRecipeController: RequestHandler = async (req, res) => { + 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); + if (room) { + guild.DojoCapacity += room.capacity; + guild.DojoEnergy += room.energy; + } + + 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(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..ff8b56b8 --- /dev/null +++ b/src/controllers/api/startLibraryDailyTaskController.ts @@ -0,0 +1,11 @@ +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { 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 new file mode 100644 index 00000000..5eb74d78 --- /dev/null +++ b/src/controllers/api/startLibraryPersonalTargetController.ts @@ -0,0 +1,14 @@ +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { RequestHandler } from "express"; + +export const startLibraryPersonalTargetController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + inventory.LibraryPersonalTarget = req.query.target as string; + await inventory.save(); + res.json({ + 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 new file mode 100644 index 00000000..90c0bdda --- /dev/null +++ b/src/controllers/api/startRecipeController.ts @@ -0,0 +1,138 @@ +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { logger } from "../../utils/logger.ts"; +import type { RequestHandler } from "express"; +import { getRecipe } from "../../services/itemDataService.ts"; +import { addItem, addKubrowPet, freeUpSlot, getInventory, updateCurrency } from "../../services/inventoryService.ts"; +import { unixTimesInMs } from "../../constants/timeConstants.ts"; +import { Types } from "mongoose"; +import type { ISpectreLoadout } from "../../types/inventoryTypes/inventoryTypes.ts"; +import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts"; +import { fromOid, toOid } from "../../helpers/inventoryHelpers.ts"; +import { ExportWeapons } from "warframe-public-export-plus"; +import { getRandomElement } from "../../services/rngService.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; + +interface IStartRecipeRequest { + RecipeName: string; + Ids: string[]; +} + +export const startRecipeController: RequestHandler = async (req, res) => { + const startRecipeRequest = getJSONfromString(String(req.body)); + logger.debug("StartRecipe Request", { startRecipeRequest }); + + const accountId = await getAccountIdForRequest(req); + + const recipeName = startRecipeRequest.RecipeName; + const recipe = getRecipe(recipeName); + + if (!recipe) { + throw new Error(`unknown recipe ${recipeName}`); + } + + const inventory = await getInventory(accountId); + updateCurrency(inventory, recipe.buildPrice, false); + + const pr = + inventory.PendingRecipes[ + inventory.PendingRecipes.push({ + ItemType: recipeName, + CompletionDate: new Date(Date.now() + recipe.buildTime * unixTimesInMs.second), + _id: new Types.ObjectId() + }) - 1 + ]; + + for (let i = 0; i != recipe.ingredients.length; ++i) { + if (startRecipeRequest.Ids[i] && startRecipeRequest.Ids[i][0] != "/") { + if (recipe.ingredients[i].ItemType == "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem") { + const index = inventory.KubrowPetEggs.findIndex(x => x._id.equals(startRecipeRequest.Ids[i])); + if (index != -1) { + inventory.KubrowPetEggs.splice(index, 1); + } + } else { + const category = ExportWeapons[recipe.ingredients[i].ItemType].productCategory; + if (category != "LongGuns" && category != "Pistols" && category != "Melee") { + throw new Error(`unexpected equipment ingredient type: ${category}`); + } + const equipmentIndex = inventory[category].findIndex(x => x._id.equals(startRecipeRequest.Ids[i])); + if (equipmentIndex == -1) { + throw new Error(`could not find equipment item to use for recipe`); + } + pr[category] ??= []; + pr[category].push(inventory[category][equipmentIndex]); + inventory[category].splice(equipmentIndex, 1); + freeUpSlot(inventory, InventorySlot.WEAPONS); + } + } else { + await addItem(inventory, recipe.ingredients[i].ItemType, recipe.ingredients[i].ItemCount * -1); + } + } + + let inventoryChanges: IInventoryChanges | undefined; + if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") { + inventoryChanges = addKubrowPet(inventory, getRandomElement(recipe.secretIngredients!)!.ItemType); + pr.KubrowPet = new Types.ObjectId(fromOid(inventoryChanges.KubrowPets![0].ItemId)); + } else if (recipe.secretIngredientAction == "SIA_DISTILL_PRINT") { + pr.KubrowPet = new Types.ObjectId(startRecipeRequest.Ids[recipe.ingredients.length]); + const pet = inventory.KubrowPets.id(pr.KubrowPet)!; + pet.Details!.PrintsRemaining -= 1; + } else if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") { + const spectreLoadout: ISpectreLoadout = { + ItemType: recipe.resultType, + Suits: "", + LongGuns: "", + Pistols: "", + Melee: "" + }; + for ( + let secretIngredientsIndex = 0; + secretIngredientsIndex != recipe.secretIngredients!.length; + ++secretIngredientsIndex + ) { + const type = recipe.secretIngredients![secretIngredientsIndex].ItemType; + const oid = startRecipeRequest.Ids[recipe.ingredients.length + secretIngredientsIndex]; + if (oid == "ffffffffffffffffffffffff") { + // user chose to preserve the active loadout + break; + } + if (type == "/Lotus/Types/Game/PowerSuits/PlayerPowerSuit") { + const item = inventory.Suits.id(oid)!; + spectreLoadout.Suits = item.ItemType; + } else if (type == "/Lotus/Weapons/Tenno/Pistol/LotusPistol") { + const item = inventory.Pistols.id(oid)!; + spectreLoadout.Pistols = item.ItemType; + spectreLoadout.PistolsModularParts = item.ModularParts; + } else if (type == "/Lotus/Weapons/Tenno/LotusLongGun") { + const item = inventory.LongGuns.id(oid)!; + spectreLoadout.LongGuns = item.ItemType; + spectreLoadout.LongGunsModularParts = item.ModularParts; + } else { + console.assert(type == "/Lotus/Types/Game/LotusMeleeWeapon"); + const item = inventory.Melee.id(oid)!; + spectreLoadout.Melee = item.ItemType; + spectreLoadout.MeleeModularParts = item.ModularParts; + } + } + if ( + spectreLoadout.Suits != "" && + spectreLoadout.LongGuns != "" && + spectreLoadout.Pistols != "" && + spectreLoadout.Melee != "" + ) { + inventory.PendingSpectreLoadouts ??= []; + const existingIndex = inventory.PendingSpectreLoadouts.findIndex(x => x.ItemType == recipe.resultType); + if (existingIndex != -1) { + inventory.PendingSpectreLoadouts.splice(existingIndex, 1); + } + 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]); + } + + await inventory.save(); + + res.json({ RecipeId: toOid(pr._id), InventoryChanges: inventoryChanges }); +}; diff --git a/src/controllers/api/stepSequencersController.ts b/src/controllers/api/stepSequencersController.ts new file mode 100644 index 00000000..5ebc9a94 --- /dev/null +++ b/src/controllers/api/stepSequencersController.ts @@ -0,0 +1,14 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import type { IStepSequencer } from "../../types/inventoryTypes/inventoryTypes.ts"; + +export const stepSequencersController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const stepSequencer = JSON.parse(String(req.body)) as IStepSequencer; + delete stepSequencer.ItemId; + const stepSequencerIndex = inventory.StepSequencers.push(stepSequencer); + const changedInventory = await inventory.save(); + res.json(changedInventory.StepSequencers[stepSequencerIndex - 1]); // unsure about the expected response format, but it seems anything works. +}; diff --git a/src/controllers/api/surveysController.ts b/src/controllers/api/surveysController.ts new file mode 100644 index 00000000..18e92fe2 --- /dev/null +++ b/src/controllers/api/surveysController.ts @@ -0,0 +1,14 @@ +import type { RequestHandler } from "express"; + +const surveysController: RequestHandler = (_req, res) => { + const data = Buffer.from([ + 0x7b, 0x22, 0x53, 0x75, 0x72, 0x76, 0x65, 0x79, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x22, 0x3a, 0x5b, 0x5d, 0x7d + ]); + res.writeHead(200, { + "Content-Type": "text/html", + "Content-Length": data.length + }); + res.end(data); +}; + +export { surveysController }; diff --git a/src/controllers/api/syndicateSacrificeController.ts b/src/controllers/api/syndicateSacrificeController.ts new file mode 100644 index 00000000..5aac5a61 --- /dev/null +++ b/src/controllers/api/syndicateSacrificeController.ts @@ -0,0 +1,116 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { ISyndicateSacrifice } from "warframe-public-export-plus"; +import { ExportSyndicates } from "warframe-public-export-plus"; +import { handleStoreItemAcquisition } from "../../services/purchaseService.ts"; +import { addMiscItem, combineInventoryChanges, getInventory, updateCurrency } from "../../services/inventoryService.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; +import { toStoreItem } from "../../services/itemDataService.ts"; +import { logger } from "../../utils/logger.ts"; + +export const syndicateSacrificeController: RequestHandler = async (request, response) => { + const accountId = await getAccountIdForRequest(request); + const inventory = await getInventory(accountId); + const data = getJSONfromString(String(request.body)); + + let syndicate = inventory.Affiliations.find(x => x.Tag == data.AffiliationTag); + if (!syndicate) { + syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: data.AffiliationTag, Standing: 0 }) - 1]; + } + + const oldLevel = syndicate.Title ?? 0; + const levelIncrease = data.SacrificeLevel - oldLevel; + if (levelIncrease < 0) { + throw new Error(`syndicate sacrifice can not decrease level`); + } + if (levelIncrease > 1 && !data.AllowMultiple) { + throw new Error(`desired syndicate level is an increase of ${levelIncrease}, max. allowed increase is 1`); + } + + const res: ISyndicateSacrificeResponse = { + AffiliationTag: data.AffiliationTag, + InventoryChanges: {}, + Level: data.SacrificeLevel, + LevelIncrease: data.SacrificeLevel < 0 ? 1 : levelIncrease, + NewEpisodeReward: false + }; + + // Process sacrifices and rewards for every level we're reaching + const manifest = ExportSyndicates[data.AffiliationTag]; + for (let level = oldLevel + Math.min(levelIncrease, 1); level <= data.SacrificeLevel; ++level) { + let sacrifice: ISyndicateSacrifice | undefined; + if (level == 0) { + sacrifice = manifest.initiationSacrifice; + if (manifest.initiationReward) { + combineInventoryChanges( + res.InventoryChanges, + (await handleStoreItemAcquisition(manifest.initiationReward, inventory)).InventoryChanges + ); + } + syndicate.Initiated = true; + } else { + sacrifice = manifest.titles?.find(x => x.level == level)?.sacrifice; + } + + if (sacrifice) { + updateCurrency(inventory, sacrifice.credits, false, res.InventoryChanges); + + for (const item of sacrifice.items) { + addMiscItem(inventory, item.ItemType, item.ItemCount * -1, res.InventoryChanges); + } + } + + // Quacks like a nightwave syndicate? + if (manifest.dailyChallenges) { + const title = manifest.titles!.find(x => x.level == level); + if (title) { + res.NewEpisodeReward = true; + let rewardType: string; + let rewardCount: number; + if (title.storeItemReward) { + rewardType = title.storeItemReward; + rewardCount = 1; + } else { + rewardType = toStoreItem(title.reward!.ItemType); + rewardCount = title.reward!.ItemCount; + } + const rewardInventoryChanges = (await handleStoreItemAcquisition(rewardType, inventory, rewardCount)) + .InventoryChanges; + if (Object.keys(rewardInventoryChanges).length == 0) { + logger.debug(`nightwave rank up reward did not seem to get added, giving 50 creds instead`); + const nightwaveCredsItemType = manifest.titles![0].reward!.ItemType; + addMiscItem(inventory, nightwaveCredsItemType, 50, rewardInventoryChanges); + } + combineInventoryChanges(res.InventoryChanges, rewardInventoryChanges); + } + } else { + if (level > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == level)) { + syndicate.FreeFavorsEarned ??= []; + if (!syndicate.FreeFavorsEarned.includes(level)) { + syndicate.FreeFavorsEarned.push(level); + } + } + } + } + + // Commit + syndicate.Title = data.SacrificeLevel < 0 ? data.SacrificeLevel + 1 : data.SacrificeLevel; + await inventory.save(); + + response.json(res); +}; + +interface ISyndicateSacrificeRequest { + AffiliationTag: string; + SacrificeLevel: number; + AllowMultiple: boolean; +} + +interface ISyndicateSacrificeResponse { + AffiliationTag: string; + Level: number; + LevelIncrease: number; + InventoryChanges: IInventoryChanges; + NewEpisodeReward: boolean; +} diff --git a/src/controllers/api/syndicateStandingBonusController.ts b/src/controllers/api/syndicateStandingBonusController.ts new file mode 100644 index 00000000..ea04e0bc --- /dev/null +++ b/src/controllers/api/syndicateStandingBonusController.ts @@ -0,0 +1,76 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { addMiscItems, addStanding, freeUpSlot, getInventory } from "../../services/inventoryService.ts"; +import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts"; +import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { IOid } from "../../types/commonTypes.ts"; +import { ExportSyndicates, ExportWeapons } from "warframe-public-export-plus"; +import { logger } from "../../utils/logger.ts"; +import type { IAffiliationMods, IInventoryChanges } from "../../types/purchaseTypes.ts"; +import { EquipmentFeatures } from "../../types/equipmentTypes.ts"; + +export const syndicateStandingBonusController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const request = JSON.parse(String(req.body)) as ISyndicateStandingBonusRequest; + + 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); + if (medallion) { + gainedStanding += medallion.standing * item.ItemCount; + } + + item.ItemCount *= -1; + }); + const inventory = await getInventory(accountId); + addMiscItems(inventory, request.Operation.Items); + const inventoryChanges: IInventoryChanges = {}; + inventoryChanges.MiscItems = request.Operation.Items; + + // Process modular weapon + if (request.Operation.ModularWeaponId.$oid != "000000000000000000000000") { + const category = req.query.Category as "LongGuns" | "Pistols" | "Melee" | "OperatorAmps"; + const weapon = inventory[category].id(request.Operation.ModularWeaponId.$oid)!; + if (gainedStanding !== 0) { + throw new Error(`modular weapon standing bonus should be mutually exclusive`); + } + weapon.ModularParts!.forEach(part => { + const partStandingBonus = ExportWeapons[part].donationStandingBonus; + if (partStandingBonus === undefined) { + throw new Error(`no standing bonus for ${part}`); + } + logger.debug(`modular weapon part ${part} gives ${partStandingBonus} standing`); + gainedStanding += partStandingBonus; + }); + if (weapon.Features && (weapon.Features & EquipmentFeatures.GILDED) != 0) { + gainedStanding *= 2; + } + inventoryChanges.RemovedIdItems = [{ ItemId: request.Operation.ModularWeaponId }]; + inventory[category].pull({ _id: request.Operation.ModularWeaponId.$oid }); + const slotBin = category == "OperatorAmps" ? InventorySlot.AMPS : InventorySlot.WEAPONS; + freeUpSlot(inventory, slotBin); + inventoryChanges[slotBin] = { count: -1, platinum: 0, Slots: 1 }; + } + + const affiliationMods: IAffiliationMods[] = []; + addStanding(inventory, request.Operation.AffiliationTag, gainedStanding, affiliationMods, true); + + await inventory.save(); + + res.json({ + InventoryChanges: inventoryChanges, + AffiliationMods: affiliationMods + }); +}; + +interface ISyndicateStandingBonusRequest { + Operation: { + AffiliationTag: string; + AlternateBonusReward: ""; // ??? + Items: IMiscItem[]; + ModularWeaponId: IOid; + }; +} diff --git a/src/controllers/api/tauntHistoryController.ts b/src/controllers/api/tauntHistoryController.ts new file mode 100644 index 00000000..51726bfe --- /dev/null +++ b/src/controllers/api/tauntHistoryController.ts @@ -0,0 +1,21 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import type { ITaunt } from "../../types/inventoryTypes/inventoryTypes.ts"; +import { logger } from "../../utils/logger.ts"; + +export const tauntHistoryController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const clientTaunt = JSON.parse(String(req.body)) as ITaunt; + logger.debug(`updating taunt ${clientTaunt.node} to state ${clientTaunt.state}`); + inventory.TauntHistory ??= []; + const taunt = inventory.TauntHistory.find(x => x.node == clientTaunt.node); + if (taunt) { + taunt.state = clientTaunt.state; + } else { + inventory.TauntHistory.push(clientTaunt); + } + await inventory.save(); + res.end(); +}; diff --git a/src/controllers/api/tradingController.ts b/src/controllers/api/tradingController.ts new file mode 100644 index 00000000..f159a521 --- /dev/null +++ b/src/controllers/api/tradingController.ts @@ -0,0 +1,23 @@ +import { getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "../../services/guildService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { GuildPermission } from "../../types/guildTypes.ts"; +import type { 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 new file mode 100644 index 00000000..8c1d55bd --- /dev/null +++ b/src/controllers/api/trainingResultController.ts @@ -0,0 +1,79 @@ +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import type { IMongoDate } from "../../types/commonTypes.ts"; +import type { RequestHandler } from "express"; +import { unixTimesInMs } from "../../constants/timeConstants.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; +import { createMessage } from "../../services/inboxService.ts"; + +interface ITrainingResultsRequest { + numLevelsGained: number; +} + +interface ITrainingResultsResponse { + NewTrainingDate: IMongoDate; + NewLevel: number; + InventoryChanges: IInventoryChanges; +} + +const trainingResultController: RequestHandler = async (req, res): Promise => { + const accountId = await getAccountIdForRequest(req); + + const trainingResults = getJSONfromString(String(req.body)); + + const inventory = await getInventory(accountId, "TrainingDate PlayerLevel TradesRemaining noMasteryRankUpCooldown"); + + if (trainingResults.numLevelsGained == 1) { + let time = Date.now(); + if (!inventory.noMasteryRankUpCooldown) { + time += unixTimesInMs.hour * 23; + } + inventory.TrainingDate = new Date(time); + + inventory.PlayerLevel += 1; + inventory.TradesRemaining += 1; + + if (inventory.PlayerLevel == 2) { + await createMessage(accountId, [ + { + sndr: "/Lotus/Language/Game/Maroo", + msg: "/Lotus/Language/Clan/MarooClanSearchDesc", + sub: "/Lotus/Language/Clan/MarooClanSearchTitle", + icon: "/Lotus/Interface/Icons/Npcs/Maroo.png" + } + ]); + } + + await createMessage(accountId, [ + { + sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender", + 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(); + + res.json({ + NewTrainingDate: { + $date: { $numberLong: changedinventory.TrainingDate.getTime().toString() } + }, + NewLevel: trainingResults.numLevelsGained == 1 ? changedinventory.PlayerLevel : inventory.PlayerLevel, + InventoryChanges: {} + } satisfies ITrainingResultsResponse); +}; + +export { trainingResultController }; diff --git a/src/controllers/api/umbraController.ts b/src/controllers/api/umbraController.ts new file mode 100644 index 00000000..fc7a3ada --- /dev/null +++ b/src/controllers/api/umbraController.ts @@ -0,0 +1,27 @@ +import { fromMongoDate, fromOid } from "../../helpers/inventoryHelpers.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { addMiscItem, getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IEquipmentClient } from "../../types/equipmentTypes.ts"; +import type { RequestHandler } from "express"; + +export const umbraController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "Suits MiscItems"); + const payload = getJSONfromString(String(req.body)); + for (const clientSuit of payload.Suits) { + const dbSuit = inventory.Suits.id(fromOid(clientSuit.ItemId))!; + if (clientSuit.UmbraDate) { + addMiscItem(inventory, "/Lotus/Types/Items/MiscItems/UmbraEchoes", -1); + dbSuit.UmbraDate = fromMongoDate(clientSuit.UmbraDate); + } else { + dbSuit.UmbraDate = undefined; + } + } + await inventory.save(); + res.end(); +}; + +interface IUmbraRequest { + Suits: IEquipmentClient[]; +} diff --git a/src/controllers/api/unlockShipFeatureController.ts b/src/controllers/api/unlockShipFeatureController.ts new file mode 100644 index 00000000..fb047d8d --- /dev/null +++ b/src/controllers/api/unlockShipFeatureController.ts @@ -0,0 +1,11 @@ +import type { RequestHandler } from "express"; +import { updateShipFeature } from "../../services/personalRoomsService.ts"; +import type { IUnlockShipFeatureRequest } from "../../types/requestTypes.ts"; +import { parseString } from "../../helpers/general.ts"; + +export const unlockShipFeatureController: RequestHandler = async (req, res) => { + const accountId = parseString(req.query.accountId); + const shipFeatureRequest = JSON.parse((req.body as string).toString()) as IUnlockShipFeatureRequest; + await updateShipFeature(accountId, shipFeatureRequest.Feature); + res.send([]); +}; diff --git a/src/controllers/api/updateAlignmentController.ts b/src/controllers/api/updateAlignmentController.ts new file mode 100644 index 00000000..79cb5d65 --- /dev/null +++ b/src/controllers/api/updateAlignmentController.ts @@ -0,0 +1,25 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IAlignment } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { 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 new file mode 100644 index 00000000..7d43e2f6 --- /dev/null +++ b/src/controllers/api/updateChallengeProgressController.ts @@ -0,0 +1,77 @@ +import type { RequestHandler } from "express"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { getAccountForRequest } from "../../services/loginService.ts"; +import { addCalendarProgress, addChallenges, getInventory } from "../../services/inventoryService.ts"; +import type { IChallengeProgress, ISeasonChallenge } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { IAffiliationMods } from "../../types/purchaseTypes.ts"; +import { getEntriesUnsafe } from "../../utils/ts-utils.ts"; +import { logger } from "../../utils/logger.ts"; + +export const updateChallengeProgressController: RequestHandler = async (req, res) => { + const challenges = getJSONfromString(String(req.body)); + const account = await getAccountForRequest(req); + logger.debug(`challenge report:`, challenges); + + const inventory = await getInventory( + account._id.toString(), + "ChallengesFixVersion ChallengeProgress SeasonChallengeHistory Affiliations CalendarProgress" + ); + let affiliationMods: IAffiliationMods[] = []; + if (challenges.ChallengeProgress) { + affiliationMods = await addChallenges( + account, + inventory, + challenges.ChallengeProgress, + challenges.SeasonChallengeCompletions + ); + } + for (const [key, value] of getEntriesUnsafe(challenges)) { + if (value === undefined) { + logger.error(`Challenge progress update key ${key} has no value`); + continue; + } + switch (key) { + case "ChallengesFixVersion": + inventory.ChallengesFixVersion = value; + break; + + case "SeasonChallengeHistory": + value.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 }); + } + }); + break; + + case "CalendarProgress": + addCalendarProgress(inventory, value); + break; + + case "ChallengeProgress": + case "SeasonChallengeCompletions": + case "ChallengePTS": + case "crossPlaySetting": + break; + default: + logger.warn(`unknown challenge progress entry`, { key, value }); + } + } + await inventory.save(); + + res.json({ + AffiliationMods: affiliationMods + }); +}; + +interface IUpdateChallengeProgressRequest { + ChallengePTS?: number; + ChallengesFixVersion?: number; + ChallengeProgress?: IChallengeProgress[]; + SeasonChallengeHistory?: ISeasonChallenge[]; + SeasonChallengeCompletions?: ISeasonChallenge[]; + CalendarProgress?: { challenge: string }[]; + crossPlaySetting?: string; +} diff --git a/src/controllers/api/updateQuestController.ts b/src/controllers/api/updateQuestController.ts new file mode 100644 index 00000000..0ca4d857 --- /dev/null +++ b/src/controllers/api/updateQuestController.ts @@ -0,0 +1,32 @@ +import type { RequestHandler } from "express"; +import { parseString } from "../../helpers/general.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import type { IUpdateQuestRequest } from "../../services/questService.ts"; +import { updateQuestKey } from "../../services/questService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; + +export const updateQuestController: RequestHandler = async (req, res) => { + const accountId = parseString(req.query.accountId); + const updateQuestRequest = getJSONfromString((req.body as string).toString()); + + // updates should be made only to one quest key per request + if (updateQuestRequest.QuestKeys.length > 1) { + throw new Error(`quest keys array should only have 1 item, but has ${updateQuestRequest.QuestKeys.length}`); + } + + const inventory = await getInventory(accountId); + + const updateQuestResponse: { CustomData?: string; InventoryChanges?: IInventoryChanges; MissionRewards: [] } = { + MissionRewards: [] + }; + 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) { + updateQuestResponse.CustomData = updateQuestRequest.QuestKeys[0].CustomData; + } + + await inventory.save(); + res.send(updateQuestResponse); +}; diff --git a/src/controllers/api/updateSessionController.ts b/src/controllers/api/updateSessionController.ts new file mode 100644 index 00000000..787f2d8c --- /dev/null +++ b/src/controllers/api/updateSessionController.ts @@ -0,0 +1,13 @@ +import type { RequestHandler } from "express"; +import { updateSession } from "../../managers/sessionManager.ts"; + +const updateSessionGetController: RequestHandler = (_req, res) => { + res.json({}); +}; +const updateSessionPostController: RequestHandler = (_req, res) => { + //console.log("UpdateSessions POST Request:", JSON.parse(String(_req.body))); + //console.log("ReqID:", _req.query.sessionId as string); + updateSession(_req.query.sessionId as string, String(_req.body)); + res.json({}); +}; +export { updateSessionGetController, updateSessionPostController }; diff --git a/src/controllers/api/updateSongChallengeController.ts b/src/controllers/api/updateSongChallengeController.ts new file mode 100644 index 00000000..0fc485fd --- /dev/null +++ b/src/controllers/api/updateSongChallengeController.ts @@ -0,0 +1,50 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { addShipDecorations, getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; +import type { 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 new file mode 100644 index 00000000..0998964b --- /dev/null +++ b/src/controllers/api/updateThemeController.ts @@ -0,0 +1,23 @@ +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import type { RequestHandler } from "express"; +import { getInventory } from "../../services/inventoryService.ts"; + +export const updateThemeController: RequestHandler = async (request, response) => { + const accountId = await getAccountIdForRequest(request); + const data = getJSONfromString(String(request.body)); + + 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({}); +}; + +interface IThemeUpdateRequest { + Style?: string; + Background?: string; + Sounds?: string; +} diff --git a/src/controllers/api/upgradeOperatorController.ts b/src/controllers/api/upgradeOperatorController.ts new file mode 100644 index 00000000..4aedef6f --- /dev/null +++ b/src/controllers/api/upgradeOperatorController.ts @@ -0,0 +1,16 @@ +import { getInventory, updateCurrency } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { RequestHandler } from "express"; + +export const upgradeOperatorController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory( + accountId, + "OperatorCustomizationSlotPurchases PremiumCredits PremiumCreditsFree" + ); + inventory.OperatorCustomizationSlotPurchases ??= 0; + inventory.OperatorCustomizationSlotPurchases += 1; + const inventoryChanges = updateCurrency(inventory, 10, true); + await inventory.save(); + res.json({ InventoryChanges: inventoryChanges }); +}; diff --git a/src/controllers/api/upgradesController.ts b/src/controllers/api/upgradesController.ts new file mode 100644 index 00000000..dbb9f1db --- /dev/null +++ b/src/controllers/api/upgradesController.ts @@ -0,0 +1,161 @@ +import type { RequestHandler } from "express"; +import type { IUpgradesRequest } from "../../types/requestTypes.ts"; +import type { ArtifactPolarity, IAbilityOverride } from "../../types/inventoryTypes/commonInventoryTypes.ts"; +import type { IInventoryClient, IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { addMiscItems, addRecipes, getInventory, updateCurrency } from "../../services/inventoryService.ts"; +import { getRecipeByResult } from "../../services/itemDataService.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; +import { addInfestedFoundryXP, applyCheatsToInfestedFoundry } from "../../services/infestedFoundryService.ts"; +import { sendWsBroadcastTo } from "../../services/wsService.ts"; +import type { IEquipmentDatabase } from "../../types/equipmentTypes.ts"; +import { EquipmentFeatures } from "../../types/equipmentTypes.ts"; + +export const upgradesController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const payload = JSON.parse(String(req.body)) as IUpgradesRequest; + const inventory = await getInventory(accountId); + const inventoryChanges: IInventoryChanges = {}; + for (const operation of payload.Operations) { + if ( + operation.UpgradeRequirement == "/Lotus/Types/Items/MiscItems/ModSlotUnlocker" || + operation.UpgradeRequirement == "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker" + ) { + updateCurrency(inventory, 10, true); + } 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, + ItemCount: -1 + } satisfies IMiscItem + ]); + } + + if (operation.OperationType == "UOT_ABILITY_OVERRIDE") { + console.assert(payload.ItemCategory == "Suits"); + const suit = inventory.Suits.id(payload.ItemId.$oid)!; + + let newAbilityOverride: IAbilityOverride | undefined; + let totalPercentagePointsConsumed = 0; + if (operation.UpgradeRequirement != "") { + newAbilityOverride = { + Ability: operation.UpgradeRequirement, + Index: operation.PolarizeSlot + }; + + const recipe = getRecipeByResult(operation.UpgradeRequirement)!; + for (const ingredient of recipe.ingredients) { + totalPercentagePointsConsumed += ingredient.ItemCount / 10; + if (!inventory.infiniteHelminthMaterials) { + inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == ingredient.ItemType)!.Count -= + ingredient.ItemCount; + } + } + } + + for (const entry of operation.PolarityRemap) { + suit.Configs[entry.Slot] ??= {}; + suit.Configs[entry.Slot].AbilityOverride = newAbilityOverride; + } + + const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry!, totalPercentagePointsConsumed * 8); + addRecipes(inventory, recipeChanges); + + inventoryChanges.Recipes = recipeChanges; + inventoryChanges.InfestedFoundry = inventory.toJSON().InfestedFoundry; + applyCheatsToInfestedFoundry(inventory, inventoryChanges.InfestedFoundry!); + } else + switch (operation.UpgradeRequirement) { + case "/Lotus/Types/Items/MiscItems/OrokinReactor": + 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": { + const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!; + item.Features ??= 0; + item.Features |= EquipmentFeatures.UTILITY_SLOT; + break; + } + case "/Lotus/Types/Items/MiscItems/HeavyWeaponCatalyst": { + console.assert(payload.ItemCategory == "SpaceGuns"); + 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": { + 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": { + const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!; + item.XP = 0; + setSlotPolarity(item, operation.PolarizeSlot, operation.PolarizeValue); + item.Polarized ??= 0; + item.Polarized += 1; + sendWsBroadcastTo(accountId, { update_inventory: true }); // webui may need to to re-add "max rank" button + 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": { + const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!; + item.CustomizationSlotPurchases ??= 0; + item.CustomizationSlotPurchases += 1; + break; + } + case "": { + console.assert(operation.OperationType == "UOT_SWAP_POLARITY"); + 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); + } + } + await inventory.save(); + res.json({ InventoryChanges: inventoryChanges }); +}; + +const setSlotPolarity = (item: IEquipmentDatabase, slot: number, polarity: ArtifactPolarity): void => { + item.Polarity ??= []; + const entry = item.Polarity.find(entry => entry.Slot == slot); + if (entry) { + entry.Value = polarity; + } else { + item.Polarity.push({ Slot: slot, Value: polarity }); + } +}; diff --git a/src/controllers/api/valenceSwapController.ts b/src/controllers/api/valenceSwapController.ts new file mode 100644 index 00000000..198415a0 --- /dev/null +++ b/src/controllers/api/valenceSwapController.ts @@ -0,0 +1,29 @@ +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IOid } from "../../types/commonTypes.ts"; +import type { IInnateDamageFingerprint, TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { 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..2ff6ee52 --- /dev/null +++ b/src/controllers/api/wishlistController.ts @@ -0,0 +1,24 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { 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/abilityOverrideController.ts b/src/controllers/custom/abilityOverrideController.ts new file mode 100644 index 00000000..47b7e13a --- /dev/null +++ b/src/controllers/custom/abilityOverrideController.ts @@ -0,0 +1,33 @@ +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { RequestHandler } from "express"; + +export const abilityOverrideController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const request = req.body as IAbilityOverrideRequest; + if (request.category === "Suits") { + const inventory = await getInventory(accountId, request.category); + const item = inventory[request.category].id(request.oid); + if (item) { + if (request.action == "set") { + item.Configs[request.configIndex].AbilityOverride = request.AbilityOverride; + } else { + item.Configs[request.configIndex].AbilityOverride = undefined; + } + await inventory.save(); + } + } + res.end(); +}; + +interface IAbilityOverrideRequest { + category: TEquipmentKey; + oid: string; + action: "set" | "remove"; + configIndex: number; + AbilityOverride: { + Ability: string; + Index: number; + }; +} diff --git a/src/controllers/custom/addCurrencyController.ts b/src/controllers/custom/addCurrencyController.ts new file mode 100644 index 00000000..dd7f472b --- /dev/null +++ b/src/controllers/custom/addCurrencyController.ts @@ -0,0 +1,21 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { addFusionPoints, getInventory } from "../../services/inventoryService.ts"; + +export const addCurrencyController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const request = req.body as IAddCurrencyRequest; + 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(); +}; + +interface IAddCurrencyRequest { + currency: "RegularCredits" | "PremiumCredits" | "FusionPoints" | "PrimeTokens"; + delta: number; +} diff --git a/src/controllers/custom/addItemsController.ts b/src/controllers/custom/addItemsController.ts new file mode 100644 index 00000000..1dc76718 --- /dev/null +++ b/src/controllers/custom/addItemsController.ts @@ -0,0 +1,20 @@ +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getInventory, addItem } from "../../services/inventoryService.ts"; +import type { RequestHandler } from "express"; + +export const addItemsController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const requests = req.body as IAddItemRequest[]; + const inventory = await getInventory(accountId); + for (const request of requests) { + await addItem(inventory, request.ItemType, request.ItemCount, true, undefined, request.Fingerprint, true); + } + await inventory.save(); + res.end(); +}; + +interface IAddItemRequest { + ItemType: string; + ItemCount: number; + Fingerprint?: string; +} diff --git a/src/controllers/custom/addMissingHelminthBlueprintsController.ts b/src/controllers/custom/addMissingHelminthBlueprintsController.ts new file mode 100644 index 00000000..27f6dec8 --- /dev/null +++ b/src/controllers/custom/addMissingHelminthBlueprintsController.ts @@ -0,0 +1,24 @@ +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getInventory, addRecipes } from "../../services/inventoryService.ts"; +import type { RequestHandler } from "express"; +import { ExportRecipes } from "warframe-public-export-plus"; + +export const addMissingHelminthBlueprintsController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "Recipes"); + const allHelminthRecipes = Object.keys(ExportRecipes).filter( + key => ExportRecipes[key].secretIngredientAction === "SIA_WARFRAME_ABILITY" + ); + const inventoryHelminthRecipes = inventory.Recipes.filter(recipe => + recipe.ItemType.startsWith("/Lotus/Types/Recipes/AbilityOverrides/") + ).map(recipe => recipe.ItemType); + + const missingHelminthRecipes = allHelminthRecipes + .filter(key => !inventoryHelminthRecipes.includes(key)) + .map(ItemType => ({ ItemType, ItemCount: 1 })); + + addRecipes(inventory, missingHelminthRecipes); + + 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..46a299a3 --- /dev/null +++ b/src/controllers/custom/addMissingMaxRankModsController.ts @@ -0,0 +1,44 @@ +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { 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 new file mode 100644 index 00000000..0b469168 --- /dev/null +++ b/src/controllers/custom/addXpController.ts @@ -0,0 +1,32 @@ +import { applyClientEquipmentUpdates, getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IOid } from "../../types/commonTypes.ts"; +import type { IEquipmentClient } from "../../types/equipmentTypes.ts"; +import type { TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { RequestHandler } from "express"; +import { ExportMisc } from "warframe-public-export-plus"; + +export const addXpController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + 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 as IOid).$oid); + if (dbItem) { + if (dbItem.ItemType in ExportMisc.uniqueLevelCaps) { + if ((dbItem.Polarized ?? 0) < 5) { + dbItem.Polarized = 5; + } + } + } + } + applyClientEquipmentUpdates(inventory, gear, category as TEquipmentKey); + } + await inventory.save(); + res.end(); +}; + +type IAddXpRequest = { + [_ in TEquipmentKey]: IEquipmentClient[]; +}; diff --git a/src/controllers/custom/changeModularPartsController.ts b/src/controllers/custom/changeModularPartsController.ts new file mode 100644 index 00000000..df63da13 --- /dev/null +++ b/src/controllers/custom/changeModularPartsController.ts @@ -0,0 +1,65 @@ +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { RequestHandler } from "express"; + +export const changeModularPartsController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const request = req.body as IUpdateFingerPrintRequest; + const inventory = await getInventory(accountId, request.category); + const item = inventory[request.category].id(request.oid); + if (item) { + item.ModularParts = request.modularParts; + + request.modularParts.forEach(part => { + const categoryMap = mapping[part]; + if (categoryMap && categoryMap[request.category]) { + item.ItemType = categoryMap[request.category]!; + } + }); + await inventory.save(); + } + res.end(); +}; + +interface IUpdateFingerPrintRequest { + category: TEquipmentKey; + oid: string; + modularParts: string[]; +} + +const mapping: Partial>>> = { + "/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelAPart": { + LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun", + Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun" + }, + "/Lotus/Weapons/Infested/Pistols/InfKitGun/Barrels/InfBarrelEgg/InfModularBarrelEggPart": { + LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun", + Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun" + }, + "/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelBPart": { + LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", + Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary" + }, + "/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelCPart": { + LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary", + Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary" + }, + "/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelDPart": { + LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam", + Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam" + }, + "/Lotus/Weapons/Infested/Pistols/InfKitGun/Barrels/InfBarrelBeam/InfModularBarrelBeamPart": { + LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam", + Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam" + }, + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA": { + MoaPets: "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit" + }, + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB": { + MoaPets: "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit" + }, + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC": { + MoaPets: "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit" + } +}; diff --git a/src/controllers/custom/completeAllMissionsController.ts b/src/controllers/custom/completeAllMissionsController.ts new file mode 100644 index 00000000..e56a6780 --- /dev/null +++ b/src/controllers/custom/completeAllMissionsController.ts @@ -0,0 +1,41 @@ +import { addString } from "../../helpers/stringHelpers.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { addFixedLevelRewards } from "../../services/missionInventoryUpdateService.ts"; +import { handleStoreItemAcquisition } from "../../services/purchaseService.ts"; +import type { IMissionReward } from "../../types/missionTypes.ts"; +import type { RequestHandler } from "express"; +import { ExportRegions } from "warframe-public-export-plus"; + +export const completeAllMissionsController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const MissionRewards: IMissionReward[] = []; + for (const [tag, node] of Object.entries(ExportRegions)) { + let mission = inventory.Missions.find(x => x.Tag == tag); + if (!mission) { + mission = + inventory.Missions[ + inventory.Missions.push({ + Completes: 0, + Tier: 0, + Tag: tag + }) - 1 + ]; + } + if (mission.Completes == 0) { + mission.Completes++; + if (node.missionReward) { + addFixedLevelRewards(node.missionReward, MissionRewards); + } + } + mission.Tier = 1; + } + for (const reward of MissionRewards) { + await handleStoreItemAcquisition(reward.StoreItem, inventory, reward.ItemCount, undefined, true); + } + addString(inventory.NodeIntrosCompleted, "TeshinHardModeUnlocked"); + addString(inventory.NodeIntrosCompleted, "CetusSyndicate_IntroJob"); + await inventory.save(); + res.end(); +}; diff --git a/src/controllers/custom/configController.ts b/src/controllers/custom/configController.ts new file mode 100644 index 00000000..70b61174 --- /dev/null +++ b/src/controllers/custom/configController.ts @@ -0,0 +1,47 @@ +import type { RequestHandler } from "express"; +import { config, syncConfigWithDatabase } from "../../services/configService.ts"; +import { getAccountForRequest, isAdministrator } from "../../services/loginService.ts"; +import { saveConfig } from "../../services/configWriterService.ts"; +import { sendWsBroadcastEx } from "../../services/wsService.ts"; + +export const getConfigController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + if (isAdministrator(account)) { + const responseData: Record = {}; + for (const id of req.body as string[]) { + const [obj, idx] = configIdToIndexable(id); + responseData[id] = obj[idx] ?? null; + } + res.json(responseData); + } else { + res.status(401).end(); + } +}; + +export const setConfigController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + if (isAdministrator(account)) { + for (const [id, value] of Object.entries(req.body as Record)) { + const [obj, idx] = configIdToIndexable(id); + obj[idx] = value; + } + sendWsBroadcastEx({ config_reloaded: true }, undefined, parseInt(String(req.query.wsid))); + syncConfigWithDatabase(); + await saveConfig(); + res.end(); + } else { + res.status(401).end(); + } +}; + +const configIdToIndexable = (id: string): [Record, string] => { + let obj = config as unknown as Record; + const arr = id.split("."); + while (arr.length > 1) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + obj[arr[0]] ??= {} as never; + obj = obj[arr[0]]; + arr.splice(0, 1); + } + return [obj, arr[0]]; +}; diff --git a/src/controllers/custom/createAccountController.ts b/src/controllers/custom/createAccountController.ts new file mode 100644 index 00000000..3cae26d6 --- /dev/null +++ b/src/controllers/custom/createAccountController.ts @@ -0,0 +1,16 @@ +import { toCreateAccount, toDatabaseAccount } from "../../helpers/customHelpers/customHelpers.ts"; +import { createAccount, isNameTaken } from "../../services/loginService.ts"; +import type { RequestHandler } from "express"; + +const createAccountController: RequestHandler = async (req, res) => { + const createAccountData = toCreateAccount(req.body); + if (await isNameTaken(createAccountData.DisplayName)) { + res.status(409).json("Name already in use"); + } else { + const databaseAccount = toDatabaseAccount(createAccountData); + const account = await createAccount(databaseAccount); + res.json(account); + } +}; + +export { createAccountController }; diff --git a/src/controllers/custom/createMessageController.ts b/src/controllers/custom/createMessageController.ts new file mode 100644 index 00000000..b8d744a9 --- /dev/null +++ b/src/controllers/custom/createMessageController.ts @@ -0,0 +1,15 @@ +import type { IMessageCreationTemplate } from "../../services/inboxService.ts"; +import { createMessage } from "../../services/inboxService.ts"; +import type { RequestHandler } from "express"; + +export const createMessageController: RequestHandler = async (req, res) => { + const message = req.body as (IMessageCreationTemplate & { ownerId: string })[] | undefined; + + if (!message) { + res.status(400).send("No message provided"); + return; + } + const savedMessages = await createMessage(message[0].ownerId, message); + + res.json(savedMessages); +}; diff --git a/src/controllers/custom/deleteAccountController.ts b/src/controllers/custom/deleteAccountController.ts new file mode 100644 index 00000000..5b52f46f --- /dev/null +++ b/src/controllers/custom/deleteAccountController.ts @@ -0,0 +1,44 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { Account, Ignore } from "../../models/loginModel.ts"; +import { Inbox } from "../../models/inboxModel.ts"; +import { Inventory } from "../../models/inventoryModels/inventoryModel.ts"; +import { Loadout } from "../../models/inventoryModels/loadoutModel.ts"; +import { PersonalRooms } from "../../models/personalRoomsModel.ts"; +import { Ship } from "../../models/shipModel.ts"; +import { Stats } from "../../models/statsModel.ts"; +import { GuildMember } from "../../models/guildModel.ts"; +import { Leaderboard } from "../../models/leaderboardModel.ts"; +import { deleteGuild } from "../../services/guildService.ts"; +import { Friendship } from "../../models/friendModel.ts"; +import { sendWsBroadcastTo } from "../../services/wsService.ts"; + +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.deleteMany({ ShipOwnerId: accountId }), + Stats.deleteOne({ accountOwnerId: accountId }) + ]); + + sendWsBroadcastTo(accountId, { logged_out: true }); + + res.end(); +}; diff --git a/src/controllers/custom/editSuitInvigorationUpgradeController.ts b/src/controllers/custom/editSuitInvigorationUpgradeController.ts new file mode 100644 index 00000000..200b403a --- /dev/null +++ b/src/controllers/custom/editSuitInvigorationUpgradeController.ts @@ -0,0 +1,34 @@ +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import type { RequestHandler } from "express"; + +const DEFAULT_UPGRADE_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000; // 7 days + +export const editSuitInvigorationUpgradeController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const { oid, data } = req.body as { + oid: string; + data?: { + DefensiveUpgrade: string; + OffensiveUpgrade: string; + UpgradesExpiry?: number; + }; + }; + const inventory = await getInventory(accountId); + const suit = inventory.Suits.id(oid)!; + if (data) { + suit.DefensiveUpgrade = data.DefensiveUpgrade; + suit.OffensiveUpgrade = data.OffensiveUpgrade; + if (data.UpgradesExpiry) { + suit.UpgradesExpiry = new Date(data.UpgradesExpiry); + } else { + suit.UpgradesExpiry = new Date(Date.now() + DEFAULT_UPGRADE_EXPIRY_MS); + } + } else { + suit.DefensiveUpgrade = undefined; + suit.OffensiveUpgrade = undefined; + suit.UpgradesExpiry = undefined; + } + await inventory.save(); + res.end(); +}; diff --git a/src/controllers/custom/getAccountInfoController.ts b/src/controllers/custom/getAccountInfoController.ts new file mode 100644 index 00000000..12848317 --- /dev/null +++ b/src/controllers/custom/getAccountInfoController.ts @@ -0,0 +1,44 @@ +import { AllianceMember, Guild, GuildMember } from "../../models/guildModel.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountForRequest, isAdministrator } from "../../services/loginService.ts"; +import type { 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 new file mode 100644 index 00000000..dc2d6c4f --- /dev/null +++ b/src/controllers/custom/getItemListsController.ts @@ -0,0 +1,373 @@ +import type { RequestHandler } from "express"; +import { getDict, getItemName, getString } from "../../services/itemDataService.ts"; +import type { TRelicQuality } from "warframe-public-export-plus"; +import { + ExportAbilities, + ExportArcanes, + ExportAvionics, + ExportBoosters, + ExportCustoms, + ExportDrones, + ExportGear, + ExportKeys, + ExportMisc, + ExportRailjackWeapons, + ExportRecipes, + ExportRelics, + ExportResources, + ExportSentinels, + ExportSyndicates, + ExportUpgrades, + ExportWarframes, + ExportWeapons +} from "warframe-public-export-plus"; +import allIncarnons from "../../../static/fixed_responses/allIncarnonList.json" with { type: "json" }; +import varzia from "../../../static/fixed_responses/worldState/varzia.json" with { type: "json" }; + +interface ListedItem { + uniqueName: string; + name: string; + subtype?: string; + fusionLimit?: number; + exalted?: string[]; + badReason?: "starter" | "frivolous" | "notraw"; + partType?: string; + chainLength?: number; + parazon?: boolean; +} + +interface ItemLists { + 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[]; + Boosters: ListedItem[]; + VarziaOffers: ListedItem[]; + Abilities: ListedItem[]; + //circuitGameModes: ListedItem[]; +} + +const relicQualitySuffixes: Record = { + VPQ_BRONZE: "", + VPQ_SILVER: " [Exceptional]", + VPQ_GOLD: " [Flawless]", + VPQ_PLATINUM: " [Radiant]" +}; + +/*const toTitleCase = (str: string): string => { + return str.replace(/[^\s-]+/g, word => word.charAt(0).toUpperCase() + word.substr(1).toLowerCase()); +};*/ + +const getItemListsController: RequestHandler = (req, response) => { + const lang = getDict(typeof req.query.lang == "string" ? req.query.lang : "en"); + const res: ItemLists = { + uniqueLevelCaps: ExportMisc.uniqueLevelCaps, + Suits: [], + LongGuns: [], + Melee: [], + ModularParts: [], + Pistols: [], + Sentinels: [], + SentinelWeapons: [], + SpaceGuns: [], + SpaceMelee: [], + SpaceSuits: [], + MechSuits: [], + miscitems: [], + Syndicates: [], + OperatorAmps: [], + QuestKeys: [], + KubrowPets: [], + EvolutionProgress: [], + mods: [], + Boosters: [], + VarziaOffers: [], + Abilities: [] + /*circuitGameModes: [ + { + uniqueName: "Survival", + name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Survival", lang)) + }, + { + uniqueName: "VoidFlood", + name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Corruption", lang)) + }, + { + uniqueName: "Excavation", + name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Excavation", lang)) + }, + { + uniqueName: "Defense", + name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Defense", lang)) + }, + { + uniqueName: "Exterminate", + name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Exterminate", lang)) + }, + { + uniqueName: "Assassination", + name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Assassination", lang)) + }, + { + uniqueName: "Alchemy", + name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Alchemy", lang)) + } + ]*/ + }; + for (const [uniqueName, item] of Object.entries(ExportWarframes)) { + res[item.productCategory].push({ + uniqueName, + name: getString(item.name, lang), + exalted: item.exalted + }); + item.abilities.forEach(ability => { + res.Abilities.push({ + uniqueName: ability.uniqueName, + name: getString(ability.name || uniqueName, lang) + }); + }); + } + 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), + exalted: item.exalted + }); + } + } + for (const [uniqueName, item] of Object.entries(ExportWeapons)) { + if (item.partType) { + if (!uniqueName.split("/")[7]?.startsWith("PvPVariant")) { + // not a pvp variant + 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: uniqueName, + name: getString(item.name, lang) + }); + } + } + } else if (item.totalDamage !== 0) { + if ( + item.productCategory == "LongGuns" || + item.productCategory == "Pistols" || + item.productCategory == "Melee" || + item.productCategory == "SpaceGuns" || + item.productCategory == "SpaceMelee" || + item.productCategory == "SentinelWeapons" || + item.productCategory == "OperatorAmps" + ) { + res[item.productCategory].push({ + uniqueName, + name: getString(item.name, lang) + }); + } + } else if (!item.excludeFromCodex) { + res.miscitems.push({ + uniqueName: uniqueName, + name: getString(item.name, lang) + }); + } + } + for (const [uniqueName, item] of Object.entries(ExportResources)) { + let name = getString(item.name, lang); + if ("dissectionParts" in item) { + name = getString("/Lotus/Language/Fish/FishDisplayName", lang).split("|FISH_NAME|").join(name); + 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 { + 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, + subtype: "Resource" + }); + } + } + for (const [uniqueName, item] of Object.entries(ExportRelics)) { + res.miscitems.push({ + 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: uniqueName, + name: getString(item.name, lang), + subtype: "Gear" + }); + } + const recipeNameTemplate = getString("/Lotus/Language/Items/BlueprintAndItem", lang); + for (const [uniqueName, item] of Object.entries(ExportRecipes)) { + 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: 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) + }); + } + + for (const [uniqueName, upgrade] of Object.entries(ExportUpgrades)) { + const mod: ListedItem = { + uniqueName, + name: getString(upgrade.name, lang), + fusionLimit: upgrade.fusionLimit + }; + 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({ + uniqueName, + name: getString(upgrade.name, lang), + fusionLimit: upgrade.fusionLimit + }); + } + for (const [uniqueName, arcane] of Object.entries(ExportArcanes)) { + 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)) { + res.Syndicates.push({ + uniqueName, + 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) + }); + } + + for (const item of Object.values(ExportBoosters)) { + res.Boosters.push({ + uniqueName: item.typeName, + name: getString(item.name, lang) + }); + } + + for (const item of Object.values(varzia.primeDualPacks)) { + res.VarziaOffers.push({ + uniqueName: item.ItemType, + name: getString(getItemName(item.ItemType) || "", lang) + }); + } + + for (const [uniqueName, ability] of Object.entries(ExportAbilities)) { + res.Abilities.push({ + uniqueName, + name: getString(ability.name || uniqueName, lang) + }); + } + + 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..2480e637 --- /dev/null +++ b/src/controllers/custom/getNameController.ts @@ -0,0 +1,7 @@ +import type { RequestHandler } from "express"; +import { getAccountForRequest } from "../../services/loginService.ts"; + +export const getNameController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + res.json(account.DisplayName); +}; diff --git a/src/controllers/custom/importController.ts b/src/controllers/custom/importController.ts new file mode 100644 index 00000000..02528e64 --- /dev/null +++ b/src/controllers/custom/importController.ts @@ -0,0 +1,39 @@ +import { importInventory, importLoadOutPresets, importPersonalRooms } from "../../services/importService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getLoadout } from "../../services/loadoutService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getPersonalRooms } from "../../services/personalRoomsService.ts"; +import type { IInventoryClient } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { IGetShipResponse } from "../../types/personalRoomsTypes.ts"; +import type { RequestHandler } from "express"; + +export const importController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const request = req.body as IImportRequest; + + const inventory = await getInventory(accountId); + importInventory(inventory, request.inventory); + await inventory.save(); + + if ("LoadOutPresets" in request.inventory && request.inventory.LoadOutPresets) { + const loadout = await getLoadout(accountId); + importLoadOutPresets(loadout, request.inventory.LoadOutPresets); + await loadout.save(); + } + + if ( + request.inventory.Ship?.Rooms || // very old accounts may have Ship with { Features: [ ... ] } + "Apartment" in request.inventory || + "TailorShop" in request.inventory + ) { + const personalRooms = await getPersonalRooms(accountId); + importPersonalRooms(personalRooms, request.inventory); + await personalRooms.save(); + } + + res.end(); +}; + +interface IImportRequest { + inventory: Partial | Partial; +} diff --git a/src/controllers/custom/ircDroppedController.ts b/src/controllers/custom/ircDroppedController.ts new file mode 100644 index 00000000..86be32d3 --- /dev/null +++ b/src/controllers/custom/ircDroppedController.ts @@ -0,0 +1,24 @@ +import { Account } from "../../models/loginModel.ts"; +import type { 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 new file mode 100644 index 00000000..4c7d614d --- /dev/null +++ b/src/controllers/custom/manageQuestsController.ts @@ -0,0 +1,160 @@ +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { + addQuestKey, + completeQuest, + giveKeyChainMissionReward, + giveKeyChainStageTriggered +} from "../../services/questService.ts"; +import { logger } from "../../utils/logger.ts"; +import type { RequestHandler } from "express"; +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 + | "completeAll" + | "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)) { + if ("chainStages" in v) { + allQuestKeys.push(k); + } + } + const inventory = await getInventory(accountId); + + switch (operation) { + case "completeAll": { + for (const questKey of inventory.QuestKeys) { + await completeQuest(inventory, questKey.ItemType); + } + break; + } + 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 "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: 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(); + res.status(200).end(); +}; diff --git a/src/controllers/custom/popArchonCrystalUpgradeController.ts b/src/controllers/custom/popArchonCrystalUpgradeController.ts new file mode 100644 index 00000000..2d5e40b9 --- /dev/null +++ b/src/controllers/custom/popArchonCrystalUpgradeController.ts @@ -0,0 +1,18 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; + +export const popArchonCrystalUpgradeController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + 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) + ); + await inventory.save(); + res.end(); + return; + } + res.status(400).end(); +}; diff --git a/src/controllers/custom/pushArchonCrystalUpgradeController.ts b/src/controllers/custom/pushArchonCrystalUpgradeController.ts new file mode 100644 index 00000000..6eb7276e --- /dev/null +++ b/src/controllers/custom/pushArchonCrystalUpgradeController.ts @@ -0,0 +1,22 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; + +export const pushArchonCrystalUpgradeController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + const suit = inventory.Suits.id(req.query.oid as string); + if (suit) { + suit.ArchonCrystalUpgrades ??= []; + const count = (req.query.count as number | undefined) ?? 1; + if (count >= 1 && count <= 10000) { + for (let i = 0; i != count; ++i) { + suit.ArchonCrystalUpgrades.push({ UpgradeType: req.query.type as string }); + } + await inventory.save(); + res.end(); + return; + } + } + res.status(400).end(); +}; diff --git a/src/controllers/custom/renameAccountController.ts b/src/controllers/custom/renameAccountController.ts new file mode 100644 index 00000000..a0665fd1 --- /dev/null +++ b/src/controllers/custom/renameAccountController.ts @@ -0,0 +1,29 @@ +import type { RequestHandler } from "express"; +import { getAccountForRequest, isAdministrator, isNameTaken } from "../../services/loginService.ts"; +import { config } from "../../services/configService.ts"; +import { saveConfig } from "../../services/configWriterService.ts"; + +export const renameAccountController: RequestHandler = async (req, res) => { + const account = await getAccountForRequest(req); + if (typeof req.query.newname == "string") { + 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 { + res.status(400).end(); + } +}; diff --git a/src/controllers/custom/setAccountCheatController.ts b/src/controllers/custom/setAccountCheatController.ts new file mode 100644 index 00000000..5a188777 --- /dev/null +++ b/src/controllers/custom/setAccountCheatController.ts @@ -0,0 +1,18 @@ +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { IAccountCheats } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { RequestHandler } from "express"; + +export const setAccountCheatController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const payload = req.body as ISetAccountCheatRequest; + const inventory = await getInventory(accountId, payload.key); + inventory[payload.key] = payload.value; + await inventory.save(); + res.end(); +}; + +interface ISetAccountCheatRequest { + key: keyof IAccountCheats; + value: boolean; +} diff --git a/src/controllers/custom/setBoosterController.ts b/src/controllers/custom/setBoosterController.ts new file mode 100644 index 00000000..756aad98 --- /dev/null +++ b/src/controllers/custom/setBoosterController.ts @@ -0,0 +1,45 @@ +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import type { RequestHandler } from "express"; +import { ExportBoosters } from "warframe-public-export-plus"; + +const I32_MAX = 0x7fffffff; + +export const setBoosterController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const requests = req.body as { ItemType: string; ExpiryDate: number }[]; + const inventory = await getInventory(accountId, "Boosters"); + const boosters = inventory.Boosters; + if ( + requests.some(request => { + if (typeof request.ItemType !== "string") return true; + if (Object.entries(ExportBoosters).find(([_, item]) => item.typeName === request.ItemType) === undefined) + return true; + if (typeof request.ExpiryDate !== "number") return true; + if (request.ExpiryDate < 0 || request.ExpiryDate > I32_MAX) return true; + return false; + }) + ) { + res.status(400).send("Invalid ItemType provided."); + return; + } + const now = Math.trunc(Date.now() / 1000); + for (const { ItemType, ExpiryDate } of requests) { + if (ExpiryDate <= now) { + // remove expired boosters + const index = boosters.findIndex(item => item.ItemType === ItemType); + if (index !== -1) { + boosters.splice(index, 1); + } + } else { + const boosterItem = boosters.find(item => item.ItemType === ItemType); + if (boosterItem) { + boosterItem.ExpiryDate = ExpiryDate; + } else { + boosters.push({ ItemType, ExpiryDate }); + } + } + } + await inventory.save(); + res.end(); +}; diff --git a/src/controllers/custom/setEvolutionProgressController.ts b/src/controllers/custom/setEvolutionProgressController.ts new file mode 100644 index 00000000..c68ccaff --- /dev/null +++ b/src/controllers/custom/setEvolutionProgressController.ts @@ -0,0 +1,33 @@ +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { 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/tunablesController.ts b/src/controllers/custom/tunablesController.ts new file mode 100644 index 00000000..87419fcc --- /dev/null +++ b/src/controllers/custom/tunablesController.ts @@ -0,0 +1,23 @@ +import type { RequestHandler } from "express"; + +// This endpoint is specific to the OpenWF Bootstrapper: https://openwf.io/bootstrapper-manual + +interface ITunables { + prohibit_skip_mission_start_timer?: boolean; + prohibit_fov_override?: boolean; + prohibit_freecam?: boolean; + prohibit_teleport?: boolean; + prohibit_scripts?: boolean; +} + +const tunablesController: RequestHandler = (_req, res) => { + const tunables: ITunables = {}; + //tunables.prohibit_skip_mission_start_timer = true; + //tunables.prohibit_fov_override = true; + //tunables.prohibit_freecam = true; + //tunables.prohibit_teleport = true; + //tunables.prohibit_scripts = true; + res.json(tunables); +}; + +export { tunablesController }; diff --git a/src/controllers/custom/unlockAllIntrinsicsController.ts b/src/controllers/custom/unlockAllIntrinsicsController.ts new file mode 100644 index 00000000..8dd484ce --- /dev/null +++ b/src/controllers/custom/unlockAllIntrinsicsController.ts @@ -0,0 +1,19 @@ +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { 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/unlockAllProfitTakerStagesController.ts b/src/controllers/custom/unlockAllProfitTakerStagesController.ts new file mode 100644 index 00000000..79fe3b87 --- /dev/null +++ b/src/controllers/custom/unlockAllProfitTakerStagesController.ts @@ -0,0 +1,24 @@ +import { getInventory } from "../../services/inventoryService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { RequestHandler } from "express"; + +const allEudicoHeistJobs = [ + "/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyOne", + "/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyTwo", + "/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyThree", + "/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyFour" +]; + +export const unlockAllProfitTakerStagesController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "CompletedJobChains"); + inventory.CompletedJobChains ??= []; + const chain = inventory.CompletedJobChains.find(x => x.LocationTag == "EudicoHeists"); + if (chain) { + chain.Jobs = allEudicoHeistJobs; + } else { + inventory.CompletedJobChains.push({ LocationTag: "EudicoHeists", Jobs: allEudicoHeistJobs }); + } + await inventory.save(); + res.end(); +}; diff --git a/src/controllers/custom/unlockAllSimarisResearchEntriesController.ts b/src/controllers/custom/unlockAllSimarisResearchEntriesController.ts new file mode 100644 index 00000000..fae3c55d --- /dev/null +++ b/src/controllers/custom/unlockAllSimarisResearchEntriesController.ts @@ -0,0 +1,20 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; + +export const unlockAllSimarisResearchEntriesController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "LibraryPersonalTarget LibraryPersonalProgress"); + inventory.LibraryPersonalTarget = undefined; + inventory.LibraryPersonalProgress = [ + "/Lotus/Types/Game/Library/Targets/Research1Target", + "/Lotus/Types/Game/Library/Targets/Research2Target", + "/Lotus/Types/Game/Library/Targets/Research3Target", + "/Lotus/Types/Game/Library/Targets/Research4Target", + "/Lotus/Types/Game/Library/Targets/Research5Target", + "/Lotus/Types/Game/Library/Targets/Research6Target", + "/Lotus/Types/Game/Library/Targets/Research7Target" + ].map(type => ({ TargetType: type, Scans: 10, Completed: true })); + await inventory.save(); + res.end(); +}; diff --git a/src/controllers/custom/updateFingerprintController.ts b/src/controllers/custom/updateFingerprintController.ts new file mode 100644 index 00000000..f600b578 --- /dev/null +++ b/src/controllers/custom/updateFingerprintController.ts @@ -0,0 +1,39 @@ +import { getInventory } from "../../services/inventoryService.ts"; +import type { WeaponTypeInternal } from "../../services/itemDataService.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { RequestHandler } from "express"; + +export const updateFingerprintController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const request = req.body as IUpdateFingerPrintRequest; + const inventory = await getInventory(accountId, request.category); + const item = inventory[request.category].id(request.oid); + if (item) { + if (request.action == "set" && request.upgradeFingerprint.buffs[0].Tag) { + const newUpgradeFingerprint = request.upgradeFingerprint; + if (!newUpgradeFingerprint.compact) newUpgradeFingerprint.compact = item.ItemType; + + item.UpgradeType = request.upgradeType; + item.UpgradeFingerprint = JSON.stringify(newUpgradeFingerprint); + } else if (request.action == "remove") { + item.UpgradeFingerprint = undefined; + item.UpgradeType = undefined; + } + await inventory.save(); + } + res.end(); +}; + +interface IUpdateFingerPrintRequest { + category: WeaponTypeInternal; + oid: string; + action: "set" | "remove"; + upgradeType: string; + upgradeFingerprint: { + compact?: string; + buffs: { + Tag: string; + Value: number; + }[]; + }; +} diff --git a/src/controllers/custom/webuiFileChangeDetectedController.ts b/src/controllers/custom/webuiFileChangeDetectedController.ts new file mode 100644 index 00000000..d8a3e508 --- /dev/null +++ b/src/controllers/custom/webuiFileChangeDetectedController.ts @@ -0,0 +1,10 @@ +import { args } from "../../helpers/commandLineArguments.ts"; +import { sendWsBroadcast } from "../../services/wsService.ts"; +import type { RequestHandler } from "express"; + +export const webuiFileChangeDetectedController: RequestHandler = (req, res) => { + if (args.dev && args.secret && req.query.secret == args.secret) { + sendWsBroadcast({ reload: true }); + } + res.end(); +}; diff --git a/src/controllers/dynamic/aggregateSessionsController.ts b/src/controllers/dynamic/aggregateSessionsController.ts new file mode 100644 index 00000000..2162c731 --- /dev/null +++ b/src/controllers/dynamic/aggregateSessionsController.ts @@ -0,0 +1,8 @@ +import type { RequestHandler } from "express"; +import aggregateSessions from "../../../static/fixed_responses/aggregateSessions.json" with { type: "json" }; + +const aggregateSessionsController: RequestHandler = (_req, res) => { + res.json(aggregateSessions); +}; + +export { aggregateSessionsController }; diff --git a/src/controllers/dynamic/getGuildAdsController.ts b/src/controllers/dynamic/getGuildAdsController.ts new file mode 100644 index 00000000..1f4cf6e2 --- /dev/null +++ b/src/controllers/dynamic/getGuildAdsController.ts @@ -0,0 +1,26 @@ +import { toMongoDate, toOid } from "../../helpers/inventoryHelpers.ts"; +import { GuildAd } from "../../models/guildModel.ts"; +import type { IGuildAdInfoClient } from "../../types/guildTypes.ts"; +import type { 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..808c3242 --- /dev/null +++ b/src/controllers/dynamic/getProfileViewingDataController.ts @@ -0,0 +1,347 @@ +import { fromDbOid, toMongoDate, toOid } from "../../helpers/inventoryHelpers.ts"; +import type { TGuildDatabaseDocument } from "../../models/guildModel.ts"; +import { Guild, GuildMember } from "../../models/guildModel.ts"; +import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts"; +import { Inventory } from "../../models/inventoryModels/inventoryModel.ts"; +import { Loadout } from "../../models/inventoryModels/loadoutModel.ts"; +import { Account } from "../../models/loginModel.ts"; +import type { TStatsDatabaseDocument } from "../../models/statsModel.ts"; +import { Stats } from "../../models/statsModel.ts"; +import { allDailyAffiliationKeys } from "../../services/inventoryService.ts"; +import type { IMongoDate, IOid } from "../../types/commonTypes.ts"; +import type { + IAffiliation, + IAlignment, + IChallengeProgress, + IDailyAffiliations, + IMission, + IPlayerSkills, + ITypeXPItem +} from "../../types/inventoryTypes/inventoryTypes.ts"; +import { LoadoutIndex } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { RequestHandler } from "express"; +import { catBreadHash, getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { ExportCustoms, ExportDojoRecipes } from "warframe-public-export-plus"; +import type { IStatsClient } from "../../types/statTypes.ts"; +import { toStoreItem } from "../../services/itemDataService.ts"; +import type { FlattenMaps } from "mongoose"; +import type { IEquipmentClient } from "../../types/equipmentTypes.ts"; +import type { ILoadoutConfigClient } from "../../types/saveLoadoutTypes.ts"; + +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 + 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( + fromDbOid(inventory.CurrentLoadOutIds[LoadoutIndex.NORMAL]) + )!.toJSON(); + result.LoadOutPreset.ItemId = undefined; + const skins = new Set(); + if (result.LoadOutPreset.s?.ItemId) { + result.LoadOutInventory.Suits = [ + inventory.Suits.id(fromDbOid(result.LoadOutPreset.s.ItemId))!.toJSON() + ]; + resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.Suits[0]); + } + if (result.LoadOutPreset.p?.ItemId) { + result.LoadOutInventory.Pistols = [ + inventory.Pistols.id(fromDbOid(result.LoadOutPreset.p.ItemId))!.toJSON() + ]; + resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.Pistols[0]); + } + if (result.LoadOutPreset.l?.ItemId) { + result.LoadOutInventory.LongGuns = [ + inventory.LongGuns.id(fromDbOid(result.LoadOutPreset.l.ItemId))!.toJSON() + ]; + resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.LongGuns[0]); + } + if (result.LoadOutPreset.m?.ItemId) { + result.LoadOutInventory.Melee = [ + inventory.Melee.id(fromDbOid(result.LoadOutPreset.m.ItemId))!.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 new file mode 100644 index 00000000..c027a52a --- /dev/null +++ b/src/controllers/dynamic/worldStateController.ts @@ -0,0 +1,19 @@ +import type { RequestHandler } from "express"; +import { getWorldState, populateDailyDeal, populateFissures } from "../../services/worldStateService.ts"; +import { version_compare } from "../../helpers/inventoryHelpers.ts"; + +export const worldStateController: RequestHandler = async (req, res) => { + const buildLabel = req.query.buildLabel as string | undefined; + const worldState = getWorldState(buildLabel); + + const populatePromises = [populateDailyDeal(worldState)]; + + // Omitting void fissures for versions prior to Dante Unbound to avoid script errors. + if (!buildLabel || version_compare(buildLabel, "2024.03.24.20.00") >= 0) { + populatePromises.push(populateFissures(worldState)); + } + + await Promise.all(populatePromises); + + res.json(worldState); +}; diff --git a/src/controllers/pay/getSkuCatalogController.ts b/src/controllers/pay/getSkuCatalogController.ts new file mode 100644 index 00000000..8495c316 --- /dev/null +++ b/src/controllers/pay/getSkuCatalogController.ts @@ -0,0 +1,5 @@ +import type { RequestHandler } from "express"; + +export const getSkuCatalogController: RequestHandler = (_req, res) => { + res.sendFile("static/fixed_responses/getSkuCatalog.json", { root: "./" }); +}; diff --git a/src/controllers/pay/steamPacksController.ts b/src/controllers/pay/steamPacksController.ts new file mode 100644 index 00000000..ea4c5b89 --- /dev/null +++ b/src/controllers/pay/steamPacksController.ts @@ -0,0 +1,6 @@ +import type { RequestHandler } from "express"; + +const steamPacksController: RequestHandler = (_req, res) => { + res.sendStatus(200); +}; +export { steamPacksController }; diff --git a/src/controllers/stats/leaderboardController.ts b/src/controllers/stats/leaderboardController.ts new file mode 100644 index 00000000..bd269a27 --- /dev/null +++ b/src/controllers/stats/leaderboardController.ts @@ -0,0 +1,25 @@ +import { getLeaderboard } from "../../services/leaderboardService.ts"; +import type { 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 new file mode 100644 index 00000000..dd7e3d77 --- /dev/null +++ b/src/controllers/stats/uploadController.ts @@ -0,0 +1,15 @@ +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { updateStats } from "../../services/statsService.ts"; +import type { IStatsUpdate } from "../../types/statTypes.ts"; +import type { RequestHandler } from "express"; + +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); + await updateStats(accountId, payload); + res.status(200).end(); +}; + +export { uploadController }; diff --git a/src/controllers/stats/viewController.ts b/src/controllers/stats/viewController.ts new file mode 100644 index 00000000..52d5e97f --- /dev/null +++ b/src/controllers/stats/viewController.ts @@ -0,0 +1,46 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { config } from "../../services/configService.ts"; +import allScans from "../../../static/fixed_responses/allScans.json" with { type: "json" }; +import { ExportEnemies } from "warframe-public-export-plus"; +import { getInventory } from "../../services/inventoryService.ts"; +import { getStats } from "../../services/statsService.ts"; +import type { IStatsClient } from "../../types/statTypes.ts"; + +const viewController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "XPInfo"); + const playerStats = await getStats(accountId); + + const responseJson = playerStats.toJSON() as IStatsClient; + responseJson.Weapons ??= []; + 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; + } else { + responseJson.Weapons.push({ type: item.ItemType, xp: item.XP }); + } + } + if (config.unlockAllScans) { + const scans = new Set(allScans); + for (const type of Object.keys(ExportEnemies.avatars)) { + if (!scans.has(type)) scans.add(type); + } + + // 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 }); + } + } + res.json(responseJson); +}; + +export { viewController }; diff --git a/src/helpers/commandLineArguments.ts b/src/helpers/commandLineArguments.ts new file mode 100644 index 00000000..09af105d --- /dev/null +++ b/src/helpers/commandLineArguments.ts @@ -0,0 +1,23 @@ +interface IArguments { + configPath?: string; + dev?: boolean; + secret?: string; +} + +export const args: IArguments = {}; + +for (let i = 2; i < process.argv.length; ) { + switch (process.argv[i++]) { + case "--configPath": + args.configPath = process.argv[i++]; + break; + + case "--dev": + args.dev = true; + break; + + case "--secret": + args.secret = process.argv[i++]; + break; + } +} diff --git a/src/helpers/customHelpers/customHelpers.ts b/src/helpers/customHelpers/customHelpers.ts new file mode 100644 index 00000000..e0d5a483 --- /dev/null +++ b/src/helpers/customHelpers/customHelpers.ts @@ -0,0 +1,56 @@ +import type { IAccountCreation } from "../../types/customTypes.ts"; +import type { IDatabaseAccountRequiredFields } from "../../types/loginTypes.ts"; +import crypto from "crypto"; +import { isString, parseEmail, parseString } from "../general.ts"; + +const getWhirlpoolHash = (rawPassword: string): string => { + const whirlpool = crypto.createHash("whirlpool"); + const data = whirlpool.update(rawPassword, "utf8"); + const hash = data.digest("hex"); + return hash; +}; + +const parsePassword = (passwordCandidate: unknown): string => { + // a different function could be called that checks whether the password has a certain shape + if (!isString(passwordCandidate)) { + throw new Error("incorrect password format"); + } + return passwordCandidate; +}; + +const toAccountCreation = (accountCreation: unknown): IAccountCreation => { + if (!accountCreation || typeof accountCreation !== "object") { + throw new Error("incorrect or missing account creation data"); + } + + if ( + "email" in accountCreation && + "password" in accountCreation && + "DisplayName" in accountCreation && + "CountryCode" in accountCreation + ) { + const rawPassword = parsePassword(accountCreation.password); + return { + email: parseEmail(accountCreation.email), + password: getWhirlpoolHash(rawPassword), + CountryCode: parseString(accountCreation.CountryCode), + DisplayName: parseString(accountCreation.DisplayName) + }; + } + throw new Error("incorrect account creation data: incorrect properties"); +}; + +const toDatabaseAccount = (createAccount: IAccountCreation): IDatabaseAccountRequiredFields => { + return { + ...createAccount, + ClientType: "", + ConsentNeeded: false, + CrossPlatformAllowed: true, + ForceLogoutVersion: 0, + TrackedSettings: [], + Nonce: 0, + LastLogin: new Date() + } satisfies IDatabaseAccountRequiredFields; +}; + +export { toDatabaseAccount, toAccountCreation as toCreateAccount }; diff --git a/src/helpers/general.ts b/src/helpers/general.ts new file mode 100644 index 00000000..ed448d84 --- /dev/null +++ b/src/helpers/general.ts @@ -0,0 +1,72 @@ +export const isEmptyObject = (obj: unknown): boolean => { + return Boolean(obj && Object.keys(obj).length === 0 && obj.constructor === Object); +}; + +/* +alternative to isEmptyObject +export const isEmptyObject = (obj: object): boolean => { + return Object.keys(obj).length === 0; +}; +*/ + +export const isString = (text: unknown): text is string => { + return typeof text === "string" || text instanceof String; +}; + +export const parseString = (data: unknown): string => { + if (!isString(data)) { + throw new Error("data is not a string"); + } + + return data; +}; + +export const isNumber = (number: unknown): number is number => { + return typeof number === "number" && !isNaN(number); +}; + +export const parseNumber = (data: unknown): number => { + if (!isNumber(data)) { + throw new Error("data is not a number"); + } + + return Number(data); +}; + +export const isDate = (date: string): boolean => { + return Date.parse(date) != 0; +}; + +export const parseDateNumber = (date: unknown): string => { + if (!isString(date) || !isDate(date)) { + throw new Error("date could not be parsed"); + } + + return date; +}; + +export const parseEmail = (email: unknown): string => { + if (!isString(email)) { + throw new Error("incorrect email"); + } + return email; +}; + +export const isBoolean = (booleanCandidate: unknown): booleanCandidate is boolean => { + return typeof booleanCandidate === "boolean"; +}; + +export const parseBoolean = (booleanCandidate: unknown): boolean => { + if (!isBoolean(booleanCandidate)) { + throw new Error("argument was not a boolean"); + } + return booleanCandidate; +}; + +export const isObject = (objectCandidate: unknown): objectCandidate is Record => { + return ( + (typeof objectCandidate === "object" || objectCandidate instanceof Object) && + objectCandidate !== null && + !Array.isArray(objectCandidate) + ); +}; diff --git a/src/helpers/inventoryHelpers.ts b/src/helpers/inventoryHelpers.ts new file mode 100644 index 00000000..a5edcaf8 --- /dev/null +++ b/src/helpers/inventoryHelpers.ts @@ -0,0 +1,213 @@ +import type { IMongoDate, IOid, IOidWithLegacySupport } from "../types/commonTypes.ts"; +import { Types } from "mongoose"; +import type { TRarity } from "warframe-public-export-plus"; +import type { IFusionTreasure } from "../types/inventoryTypes/inventoryTypes.ts"; + +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() }; +}; + +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)!; +}; + +// For oids that may have been stored incorrectly +export const fromDbOid = (x: Types.ObjectId | IOid): Types.ObjectId => { + return "$oid" in x ? new Types.ObjectId(x.$oid) : x; +}; + +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 parseFusionTreasure = (name: string, count: number): IFusionTreasure => { + const arr = name.split("_"); + return { + ItemType: arr[0], + Sockets: parseInt(arr[1], 16), + ItemCount: count + }; +}; + +export type TTraitsPool = Record< + "Colors" | "EyeColors" | "FurPatterns" | "BodyTypes" | "Heads" | "Tails", + { type: string; rarity: TRarity }[] +>; + +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: TTraitsPool = { + Colors: [ + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseA", rarity: "COMMON" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseB", rarity: "COMMON" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseC", rarity: "COMMON" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseD", rarity: "COMMON" }, + + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryA", rarity: "UNCOMMON" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryB", rarity: "UNCOMMON" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryC", rarity: "UNCOMMON" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryD", rarity: "UNCOMMON" }, + + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryA", rarity: "RARE" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryB", rarity: "RARE" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryC", rarity: "RARE" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryD", rarity: "RARE" }, + + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsA", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsB", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsC", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsD", rarity: "LEGENDARY" } + ], + + EyeColors: [ + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesA", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesB", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesC", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesD", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesE", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesF", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesG", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesH", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesI", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesJ", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesK", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesL", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesM", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesN", rarity: "LEGENDARY" } + ], + + FurPatterns: [{ type: "/Lotus/Types/Game/CatbrowPet/Patterns/CatbrowPetPatternA", rarity: "COMMON" }], + + BodyTypes: [ + { type: "/Lotus/Types/Game/CatbrowPet/BodyTypes/CatbrowPetRegularBodyType", rarity: "UNCOMMON" }, + { type: "/Lotus/Types/Game/CatbrowPet/BodyTypes/CatbrowPetRegularBodyType", rarity: "LEGENDARY" } + ], + + Heads: [ + { type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadA", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadB", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadC", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadD", rarity: "LEGENDARY" } + ], + + Tails: [ + { type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailA", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailB", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailC", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailD", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailE", rarity: "LEGENDARY" } + ] +}; + +export const kubrowDetails: TTraitsPool = { + Colors: [ + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneA", rarity: "UNCOMMON" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneB", rarity: "UNCOMMON" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneC", rarity: "UNCOMMON" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneD", rarity: "UNCOMMON" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneE", rarity: "UNCOMMON" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneF", rarity: "UNCOMMON" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneG", rarity: "UNCOMMON" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneH", rarity: "UNCOMMON" }, + + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidA", rarity: "RARE" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidB", rarity: "RARE" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidC", rarity: "RARE" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidD", rarity: "RARE" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidE", rarity: "RARE" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidF", rarity: "RARE" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidG", rarity: "RARE" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidH", rarity: "RARE" }, + + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantA", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantB", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantC", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantD", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantE", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantF", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantG", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantH", rarity: "LEGENDARY" } + ], + + EyeColors: [ + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesA", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesB", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesC", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesD", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesE", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesF", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesG", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesH", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesI", rarity: "LEGENDARY" } + ], + + FurPatterns: [ + { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternB", rarity: "UNCOMMON" }, + { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternA", rarity: "UNCOMMON" }, + + { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternC", rarity: "RARE" }, + { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternD", rarity: "RARE" }, + + { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternE", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternF", rarity: "LEGENDARY" } + ], + + BodyTypes: [ + { type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetRegularBodyType", rarity: "UNCOMMON" }, + { type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetHeavyBodyType", rarity: "LEGENDARY" }, + { type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetThinBodyType", rarity: "LEGENDARY" } + ], + + Heads: [], + + Tails: [] +}; diff --git a/src/helpers/modularWeaponHelper.ts b/src/helpers/modularWeaponHelper.ts new file mode 100644 index 00000000..111f85c9 --- /dev/null +++ b/src/helpers/modularWeaponHelper.ts @@ -0,0 +1,26 @@ +import type { TEquipmentKey } from "../types/inventoryTypes/inventoryTypes.ts"; + +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..14cb6ba8 --- /dev/null +++ b/src/helpers/nemesisHelpers.ts @@ -0,0 +1,492 @@ +import { ExportRegions, ExportWarframes } from "warframe-public-export-plus"; +import type { IInfNode, TNemesisFaction } from "../types/inventoryTypes/inventoryTypes.ts"; +import { generateRewardSeed, getRewardAtPercentage, SRng } from "../services/rngService.ts"; +import type { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel.ts"; +import type { IOid } from "../types/commonTypes.ts"; +import { isArchwingMission } from "../services/worldStateService.ts"; + +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; + killMessageSubject: string; + killMessageBody: string; + convertMessageSubject: string; + convertMessageBody: string; + convertMessageIcon: 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"; + killMessageSubject = "/Lotus/Language/Inbox/VanquishKuvaMsgTitle"; + killMessageBody = "/Lotus/Language/Inbox/VanquishLichMsgBody"; + convertMessageSubject = "/Lotus/Language/Kingpins/InboxKuvaConvertedSubject"; + convertMessageBody = "/Lotus/Language/Kingpins/InboxKuvaConvertedBody"; + convertMessageIcon = "/Lotus/Interface/Graphics/WorldStatePanel/Grineer.png"; + 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"; + killMessageSubject = "/Lotus/Language/Inbox/VanquishLawyerMsgTitle"; + killMessageBody = "/Lotus/Language/Inbox/VanquishLichMsgBody"; + convertMessageSubject = "/Lotus/Language/Kingpins/InboxSisterConvertedSubject"; + convertMessageBody = "/Lotus/Language/Kingpins/InboxSisterConvertedBody"; + convertMessageIcon = "/Lotus/Interface/Graphics/WorldStatePanel/Corpus.png"; + 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"; + killMessageSubject = "/Lotus/Language/Inbox/VanquishBandMsgTitle"; + killMessageBody = "/Lotus/Language/Inbox/VanquishBandMsgBody"; + convertMessageSubject = "/Lotus/Language/Kingpins/InboxBandConvertedSubject"; + convertMessageBody = "/Lotus/Language/Kingpins/InboxBandConvertedBody"; + convertMessageIcon = "/Lotus/Interface/Graphics/WorldStatePanel/Infested.png"; + 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 requiemMods: 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" +];*/ + +export 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 => requiemMods[i]); +};*/ + +// Symbols; 0-7 are the normal requiem mods. +export const GUESS_NONE = 8; +export const GUESS_WILDCARD = 9; + +// Results; there are 3, 4, 5 as well which are more muted versions but unused afaik. +export const GUESS_NEUTRAL = 0; +export const GUESS_INCORRECT = 1; +export const GUESS_CORRECT = 2; + +interface NemesisPositionGuess { + symbol: number; + result: number; +} + +export type NemesisGuess = [NemesisPositionGuess, NemesisPositionGuess, NemesisPositionGuess]; + +export const encodeNemesisGuess = (guess: NemesisGuess): number => { + return ( + (guess[0].symbol & 0xf) | + ((guess[0].result & 3) << 12) | + ((guess[1].symbol << 4) & 0xff) | + ((guess[1].result << 14) & 0xffff) | + ((guess[2].symbol & 0xf) << 8) | + ((guess[2].result & 3) << 16) + ); +}; + +export const decodeNemesisGuess = (val: number): NemesisGuess => { + return [ + { + symbol: val & 0xf, + result: (val >> 12) & 3 + }, + { + symbol: (val & 0xff) >> 4, + result: (val & 0xffff) >> 14 + }, + { + symbol: (val >> 8) & 0xf, + result: (val >> 16) & 3 + } + ]; +}; + +export interface IKnifeResponse { + 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 parseUpgrade = ( + inventory: TInventoryDatabaseDocument, + str: string +): { ItemId: IOid; ItemType: string } => { + if (str.length == 24) { + const upgrade = inventory.Upgrades.id(str); + if (upgrade) { + return { + ItemId: { $oid: str }, + ItemType: upgrade.ItemType + }; + } + throw new Error(`Could not resolve oid ${str}`); + } else { + return { + ItemId: { $oid: "000000000000000000000000" }, + ItemType: str + }; + } +}; + +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..69b0c373 --- /dev/null +++ b/src/helpers/pathHelper.ts @@ -0,0 +1,4 @@ +import path from "path"; + +export const rootDir = path.join(import.meta.dirname, "../.."); +export const repoDir = path.basename(rootDir) != "build" ? rootDir : path.join(rootDir, ".."); diff --git a/src/helpers/purchaseHelpers.ts b/src/helpers/purchaseHelpers.ts new file mode 100644 index 00000000..3e5c0783 --- /dev/null +++ b/src/helpers/purchaseHelpers.ts @@ -0,0 +1,23 @@ +import type { SlotPurchase, SlotPurchaseName } from "../types/purchaseTypes.ts"; + +export const slotPurchaseNameToSlotName: SlotPurchase = { + 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 } +}; + +export const isSlotPurchaseName = (slotPurchaseName: string): slotPurchaseName is SlotPurchaseName => { + return slotPurchaseName in slotPurchaseNameToSlotName; +}; + +export const parseSlotPurchaseName = (slotPurchaseName: string): SlotPurchaseName => { + if (!isSlotPurchaseName(slotPurchaseName)) throw new Error(`invalid slot name ${slotPurchaseName}`); + return slotPurchaseName; +}; diff --git a/src/helpers/relicHelper.ts b/src/helpers/relicHelper.ts new file mode 100644 index 00000000..964e43e2 --- /dev/null +++ b/src/helpers/relicHelper.ts @@ -0,0 +1,85 @@ +import type { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel.ts"; +import type { IVoidTearParticipantInfo } from "../types/requestTypes.ts"; +import type { TRarity } from "warframe-public-export-plus"; +import { ExportRelics, ExportRewards } from "warframe-public-export-plus"; +import type { IRngResult } from "../services/rngService.ts"; +import { getRandomWeightedReward } from "../services/rngService.ts"; +import { logger } from "../utils/logger.ts"; +import { addMiscItems, combineInventoryChanges } from "../services/inventoryService.ts"; +import { handleStoreItemAcquisition } from "../services/purchaseService.ts"; +import type { IInventoryChanges } from "../types/purchaseTypes.ts"; +import { config } from "../services/configService.ts"; + +export const crackRelic = async ( + inventory: TInventoryDatabaseDocument, + participant: IVoidTearParticipantInfo, + inventoryChanges: IInventoryChanges = {} +): Promise => { + const relic = ExportRelics[participant.VoidProjection]; + let weights = refinementToWeights[relic.quality]; + if (relic.quality == "VPQ_SILVER" && inventory.exceptionalRelicsAlwaysGiveBronzeReward) { + weights = { COMMON: 1, UNCOMMON: 0, RARE: 0, LEGENDARY: 0 }; + } else if (relic.quality == "VPQ_GOLD" && inventory.flawlessRelicsAlwaysGiveSilverReward) { + weights = { COMMON: 0, UNCOMMON: 1, RARE: 0, LEGENDARY: 0 }; + } else if (relic.quality == "VPQ_PLATINUM" && inventory.radiantRelicsAlwaysGiveGoldReward) { + weights = { COMMON: 0, UNCOMMON: 0, RARE: 1, LEGENDARY: 0 }; + } + logger.debug(`opening a relic of quality ${relic.quality}; rarity weights are`, weights); + let 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 + )!; + if (config.relicRewardItemCountMultiplier !== undefined && (config.relicRewardItemCountMultiplier ?? 1) != 1) { + reward = { + ...reward, + itemCount: reward.itemCount * config.relicRewardItemCountMultiplier + }; + } + 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..61f10ccd --- /dev/null +++ b/src/helpers/rivenHelper.ts @@ -0,0 +1,121 @@ +import type { IUpgrade } from "warframe-public-export-plus"; +import { getRandomElement, getRandomInt, getRandomReward } from "../services/rngService.ts"; + +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/shardHelper.ts b/src/helpers/shardHelper.ts new file mode 100644 index 00000000..0971f7b7 --- /dev/null +++ b/src/helpers/shardHelper.ts @@ -0,0 +1,66 @@ +export const colorToShard: Record = { + ACC_RED: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalAmar", + ACC_RED_MYTHIC: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalAmarMythic", + ACC_YELLOW: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalNira", + ACC_YELLOW_MYTHIC: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalNiraMythic", + ACC_BLUE: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalBoreal", + ACC_BLUE_MYTHIC: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalBorealMythic", + ACC_GREEN: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalGreen", + ACC_GREEN_MYTHIC: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalGreenMythic", + ACC_ORANGE: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalOrange", + ACC_ORANGE_MYTHIC: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalOrangeMythic", + ACC_PURPLE: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalViolet", + ACC_PURPLE_MYTHIC: "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalVioletMythic" +}; + +export const shardToColor: Record = { + "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalAmar": "ACC_RED", + "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalAmarMythic": "ACC_RED_MYTHIC", + "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalNira": "ACC_YELLOW", + "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalNiraMythic": "ACC_YELLOW_MYTHIC", + "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalBoreal": "ACC_BLUE", + "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalBorealMythic": "ACC_BLUE_MYTHIC", + "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalGreen": "ACC_GREEN", + "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalGreenMythic": "ACC_GREEN_MYTHIC", + "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalOrange": "ACC_ORANGE", + "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalOrangeMythic": "ACC_ORANGE_MYTHIC", + "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalViolet": "ACC_PURPLE", + "/Lotus/Types/Gameplay/NarmerSorties/ArchonCrystalVioletMythic": "ACC_PURPLE_MYTHIC" +}; + +export const combineColors = (a: string, b: string): string => { + return ( + combinePlainColors(a.replace("_MYTHIC", ""), b.replace("_MYTHIC", "")) + + (a.indexOf("_MYTHIC") != -1 ? "_MYTHIC" : "") + ); +}; + +const combinePlainColors = (a: string, b: string): string => { + switch (a) { + case "ACC_RED": + switch (b) { + case "ACC_YELLOW": + return "ACC_ORANGE"; + case "ACC_BLUE": + return "ACC_PURPLE"; + } + break; + case "ACC_YELLOW": + switch (b) { + case "ACC_RED": + return "ACC_ORANGE"; + case "ACC_BLUE": + return "ACC_GREEN"; + } + break; + case "ACC_BLUE": + switch (b) { + case "ACC_RED": + return "ACC_PURPLE"; + case "ACC_YELLOW": + return "ACC_GREEN"; + } + break; + } + throw new Error(`invalid color combination request: ${a} and ${b}`); +}; diff --git a/src/helpers/stringHelpers.ts b/src/helpers/stringHelpers.ts new file mode 100644 index 00000000..3512c1ea --- /dev/null +++ b/src/helpers/stringHelpers.ts @@ -0,0 +1,62 @@ +import { JSONParse } from "json-with-bigint"; + +export const getJSONfromString = (str: string): T => { + const jsonSubstring = str.substring(0, str.lastIndexOf("}") + 1); + return JSONParse(jsonSubstring) as T; +}; + +export const getSubstringFromKeyword = (str: string, keyword: string): string => { + const index = str.indexOf(keyword); + if (index == -1) { + throw new Error(`keyword ${keyword} not found in string ${str}`); + } + return str.substring(index); +}; + +export const getSubstringFromKeywordToKeyword = (str: string, keywordBegin: string, keywordEnd: string): string => { + const beginIndex = str.lastIndexOf(keywordBegin) + 1; + const endIndex = str.indexOf(keywordEnd); + return str.substring(beginIndex, endIndex + 1); +}; + +export const getIndexAfter = (str: string, searchWord: string): number => { + const index = str.indexOf(searchWord); + if (index === -1) { + return -1; + } + 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; +}; + +export const addString = (arr: string[], str: string): void => { + if (arr.indexOf(str) == -1) { + arr.push(str); + } +}; diff --git a/src/helpers/syndicateStandingHelper.ts b/src/helpers/syndicateStandingHelper.ts new file mode 100644 index 00000000..ec9c939e --- /dev/null +++ b/src/helpers/syndicateStandingHelper.ts @@ -0,0 +1,23 @@ +import type { ISyndicate } from "warframe-public-export-plus"; + +export const getMaxStanding = (syndicate: ISyndicate, title: number): number => { + if (!syndicate.titles) { + // LibrarySyndicate + return 125000; + } + if (title == 0) { + return syndicate.titles.find(x => x.level == 1)!.minStanding; + } + return syndicate.titles.find(x => x.level == title)!.maxStanding; +}; + +export const getMinStanding = (syndicate: ISyndicate, title: number): number => { + if (!syndicate.titles) { + // LibrarySyndicate + return 0; + } + if (title == 0) { + return syndicate.titles.find(x => x.level == -1)!.maxStanding; + } + return syndicate.titles.find(x => x.level == title)!.minStanding; +}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..3021b14b --- /dev/null +++ b/src/index.ts @@ -0,0 +1,56 @@ +// First, init config. +import { config, configPath, loadConfig, syncConfigWithDatabase } from "./services/configService.ts"; +import fs from "fs"; +try { + loadConfig(); +} catch (e) { + if (fs.existsSync("config.json")) { + console.log("Failed to load " + configPath + ": " + (e as Error).message); + } else { + console.log("Failed to load " + configPath + ". You can copy config-vanilla.json to create your config file."); + } + process.exit(1); +} + +// Now we can init the logger with the settings provided in the config. +import { logger } from "./utils/logger.ts"; +logger.info("Starting up..."); + +// Proceed with normal startup: bring up config watcher service, validate config, connect to MongoDB, and finally start listening for HTTP. +import mongoose from "mongoose"; +import path from "path"; +import { JSONStringify } from "json-with-bigint"; +import { startWebServer } from "./services/webService.ts"; +import { validateConfig } from "./services/configWatcherService.ts"; +import { updateWorldStateCollections } from "./services/worldStateService.ts"; +import { repoDir } from "./helpers/pathHelper.ts"; + +JSON.stringify = JSONStringify; // Patch JSON.stringify to work flawlessly with Bigints. + +validateConfig(); + +fs.readFile(path.join(repoDir, "BUILD_DATE"), "utf-8", (err, data) => { + if (!err) { + logger.info(`Docker image was built on ${data.trim()}`); + } +}); + +mongoose + .connect(config.mongodbUrl) + .then(() => { + logger.info("Connected to MongoDB"); + syncConfigWithDatabase(); + + startWebServer(); + + void updateWorldStateCollections(); + setInterval(() => { + void updateWorldStateCollections(); + }, 60_000); + }) + .catch(error => { + if (error instanceof Error) { + logger.error(`Error connecting to MongoDB server: ${error.message}`); + } + process.exit(1); + }); diff --git a/src/managers/sessionManager.ts b/src/managers/sessionManager.ts new file mode 100644 index 00000000..a6bbee08 --- /dev/null +++ b/src/managers/sessionManager.ts @@ -0,0 +1,119 @@ +import type { ISession, IFindSessionRequest } from "../types/session.ts"; +import { logger } from "../utils/logger.ts"; +import { JSONParse } from "json-with-bigint"; +import { Types } from "mongoose"; + +const sessions: ISession[] = []; + +function createNewSession(sessionData: ISession, Creator: Types.ObjectId): ISession { + const sessionId = new Types.ObjectId(); + const newSession: ISession = { + sessionId, + creatorId: Creator, + maxPlayers: sessionData.maxPlayers || 4, + minPlayers: sessionData.minPlayers || 1, + privateSlots: sessionData.privateSlots || 0, + scoreLimit: sessionData.scoreLimit || 15, + timeLimit: sessionData.timeLimit || 900, + gameModeId: sessionData.gameModeId || 0, + eloRating: sessionData.eloRating || 3, + regionId: sessionData.regionId || 3, + difficulty: sessionData.difficulty || 0, + hasStarted: sessionData.hasStarted || false, + enableVoice: sessionData.enableVoice || true, + matchType: sessionData.matchType || "NORMAL", + maps: sessionData.maps || [], + originalSessionId: sessionData.originalSessionId || "", + customSettings: sessionData.customSettings || "", + rewardSeed: sessionData.rewardSeed || -1, + guildId: sessionData.guildId || "", + buildId: sessionData.buildId || 4920386201513015989n, + platform: sessionData.platform || 0, + xplatform: sessionData.xplatform || true, + freePublic: sessionData.freePublic || 3, + freePrivate: sessionData.freePrivate || 0, + fullReset: 0 + }; + sessions.push(newSession); + return newSession; +} + +function getAllSessions(): ISession[] { + return sessions; +} + +function getSessionByID(sessionId: string | Types.ObjectId): ISession | undefined { + return sessions.find(session => session.sessionId.equals(sessionId)); +} + +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 [ + { + createdBy: session.creatorId, + id: session.sessionId + } + ]; + } + return []; + } + + const request = sessionIdOrRequest; + const matchingSessions = sessions.filter(session => { + for (const key in request) { + if ( + key !== "eloRating" && + key !== "queryId" && + request[key as keyof IFindSessionRequest] !== session[key as keyof ISession] + ) { + return false; + } + } + logger.debug("Found Matching Sessions:", { matchingSessions }); + return true; + }); + return matchingSessions.map(session => ({ + createdBy: session.creatorId, + id: session.sessionId + })); +} + +function getSessionByCreatorID(creatorId: string | Types.ObjectId): ISession | undefined { + return sessions.find(session => session.creatorId.equals(creatorId)); +} + +function updateSession(sessionId: string | Types.ObjectId, sessionData: string): boolean { + const session = sessions.find(session => session.sessionId.equals(sessionId)); + if (!session) return false; + try { + Object.assign(session, JSONParse(sessionData)); + return true; + } catch (error) { + console.error("Invalid JSON string for session update."); + return false; + } +} + +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; + } + return false; +} + +export { + createNewSession, + getAllSessions, + getSessionByID, + getSessionByCreatorID, + updateSession, + deleteSession, + getSession +}; diff --git a/src/middleware/errorHandler.ts b/src/middleware/errorHandler.ts new file mode 100644 index 00000000..8556e903 --- /dev/null +++ b/src/middleware/errorHandler.ts @@ -0,0 +1,11 @@ +import type { NextFunction, Request, Response } from "express"; +import { logError } from "../utils/logger.ts"; + +export const errorHandler = (err: Error, req: Request, res: Response, _next: NextFunction): void => { + if (err.message == "Invalid accountId-nonce pair") { + res.status(400).send("Log-in expired"); + } else { + logError(err, `processing ${req.path} request`); + res.status(500).end(); + } +}; diff --git a/src/middleware/middleware.ts b/src/middleware/middleware.ts new file mode 100644 index 00000000..2b67c3bb --- /dev/null +++ b/src/middleware/middleware.ts @@ -0,0 +1,20 @@ +import { logger } from "../utils/logger.ts"; +import type { /*NextFunction,*/ Request, Response } from "express"; + +const unknownEndpointHandler = (request: Request, response: Response): void => { + logger.error(`unknown endpoint ${request.method} ${request.path}`); + if (request.body) { + logger.debug(`data provided to ${request.path}: ${String(request.body)}`); + } + response.status(404).json({ error: "endpoint was not found" }); +}; + +// const requestLogger = (request: Request, _response: Response, next: NextFunction) => { +// console.log("Method:", request.method); +// console.log("Path: ", request.path); +// console.log("Body: ", request.body); +// console.log("---"); +// next(); +// }; + +export { unknownEndpointHandler }; diff --git a/src/middleware/morgenMiddleware.ts b/src/middleware/morgenMiddleware.ts new file mode 100644 index 00000000..2888d77c --- /dev/null +++ b/src/middleware/morgenMiddleware.ts @@ -0,0 +1,6 @@ +import morgan from "morgan"; +import { logger } from "../utils/logger.ts"; + +export const requestLogger = morgan("dev", { + stream: { write: message => logger.http(message.trim()) } +}); diff --git a/src/models/commonModel.ts b/src/models/commonModel.ts new file mode 100644 index 00000000..94dd8016 --- /dev/null +++ b/src/models/commonModel.ts @@ -0,0 +1,25 @@ +import { Schema } from "mongoose"; +import type { IColor, IShipCustomization } from "../types/inventoryTypes/commonInventoryTypes.ts"; + +export const colorSchema = new Schema( + { + t0: Number, + t1: Number, + t2: Number, + t3: Number, + en: Number, + e1: Number, + m0: Number, + m1: Number + }, + { _id: false } +); + +export const shipCustomizationSchema = new Schema( + { + SkinFlavourItem: String, + Colors: colorSchema, + ShipAttachments: { HOOD_ORNAMENT: String } + }, + { _id: false } +); diff --git a/src/models/friendModel.ts b/src/models/friendModel.ts new file mode 100644 index 00000000..a40affd1 --- /dev/null +++ b/src/models/friendModel.ts @@ -0,0 +1,15 @@ +import type { IFriendship } from "../types/friendTypes.ts"; +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 new file mode 100644 index 00000000..e7d53297 --- /dev/null +++ b/src/models/guildModel.ts @@ -0,0 +1,323 @@ +import type { + IGuildDatabase, + IDojoComponentDatabase, + ITechProjectDatabase, + IDojoDecoDatabase, + ILongMOTD, + IGuildMemberDatabase, + IGuildLogEntryNumber, + IGuildRank, + IGuildLogRoomChange, + IGuildLogEntryRoster, + IGuildLogEntryContributable, + IDojoLeaderboardEntry, + IGuildAdDatabase, + IAllianceDatabase, + IAllianceMemberDatabase +} from "../types/guildTypes.ts"; +import { GuildPermission } from "../types/guildTypes.ts"; +import type { Document, Model } from "mongoose"; +import { model, Schema, Types } from "mongoose"; +import { fusionTreasuresSchema, typeCountSchema } from "./inventoryModels/inventoryModel.ts"; +import { pictureFrameInfoSchema } from "./personalRoomsModel.ts"; +import type { IGoalProgressClient, IGoalProgressDatabase } from "../types/inventoryTypes/inventoryTypes.ts"; +import { toOid } from "../helpers/inventoryHelpers.ts"; + +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, + op: String, + pp: String, + Name: String, + Message: String, + 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( + { + ItemType: String, + ReqCredits: Number, + ReqItems: [typeCountSchema], + State: Number, + CompletionDate: Date + }, + { _id: false } +); + +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 goalProgressSchema = new Schema( + { + Count: Number, + Tag: String, + goalId: Types.ObjectId + }, + { _id: false } +); + +goalProgressSchema.set("toJSON", { + virtuals: true, + transform(_doc, obj: Record) { + const db = obj as IGoalProgressDatabase; + const client = obj as IGoalProgressClient; + + client._id = toOid(db.goalId); + + delete obj.goalId; + delete obj.__v; + } +}); + +const guildSchema = new Schema( + { + 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 }, + 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 }, + GoalProgress: { type: [goalProgressSchema], default: undefined } + }, + { id: false } +); + +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 new file mode 100644 index 00000000..9adfb8ef --- /dev/null +++ b/src/models/inboxModel.ts @@ -0,0 +1,185 @@ +import type { Types } from "mongoose"; +import { model, Schema } from "mongoose"; +import { toMongoDate, toOid } from "../helpers/inventoryHelpers.ts"; +import { typeCountSchema } from "./inventoryModels/inventoryModel.ts"; +import type { IMongoDate, IOid, ITypeCount } from "../types/commonTypes.ts"; + +export interface IMessageClient + extends Omit< + IMessageDatabase, + "_id" | "globaUpgradeId" | "date" | "startDate" | "endDate" | "ownerId" | "attVisualOnly" | "expiry" + > { + _id?: IOid; + globaUpgradeId?: IOid; // [sic] + date: IMongoDate; + startDate?: IMongoDate; + endDate?: IMongoDate; + messageId: IOid; +} + +export interface IMessageDatabase extends IMessage { + ownerId: Types.ObjectId; + globaUpgradeId?: Types.ObjectId; // [sic] + date: Date; //created at + attVisualOnly?: boolean; + _id: Types.ObjectId; +} + +export interface IMessage { + sndr: string; + msg: string; + cinematic?: string; + sub: string; + customData?: string; + icon?: string; + highPriority?: boolean; + lowPrioNewPlayers?: boolean; + transmission?: string; + att?: string[]; + countedAtt?: ITypeCount[]; + startDate?: Date; + endDate?: Date; + goalTag?: string; + CrossPlatform?: boolean; + arg?: Arg[]; + gifts?: IGift[]; + r?: boolean; + contextInfo?: string; + acceptAction?: string; + declineAction?: string; + hasAccountAction?: boolean; + RegularCredits?: number; +} + +export interface Arg { + Key: string; + Tag: string | number; +} + +export interface IGift { + GiftType: string; +} + +//types are wrong +// export interface IMessageDatabase { +// _id: Types.ObjectId; +// messageId: string; +// sub: string; +// sndr: string; +// msg: string; +// startDate: Date; +// endDate: Date; +// date: Date; +// contextInfo: string; +// icon: string; +// att: string[]; +// modPacks: string[]; +// countedAtt: string[]; +// attSpecial: string[]; +// transmission: string; +// ordisReactionTransmission: string; +// arg: string[]; +// r: string; +// acceptAction: string; +// declineAction: string; +// highPriority: boolean; +// lowPrioNewPlayers: boolean +// gifts: string[]; +// teleportLoc: string; +// RegularCredits: string; +// PremiumCredits: string; +// PrimeTokens: string; +// Coupons: string[]; +// syndicateAttachment: string[]; +// tutorialTag: string; +// url: string; +// urlButtonText: string; +// cinematic: string; +// requiredLevel: string; +// } + +const giftSchema = new Schema( + { + GiftType: String + }, + { _id: false } +); + +const messageSchema = new Schema( + { + ownerId: Schema.Types.ObjectId, + globaUpgradeId: Schema.Types.ObjectId, + sndr: String, + msg: String, + cinematic: String, + sub: String, + customData: String, + icon: String, + highPriority: Boolean, + lowPrioNewPlayers: Boolean, + startDate: Date, + endDate: Date, + goalTag: String, + date: { type: Date, required: true }, + r: Boolean, + CrossPlatform: 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: Schema.Types.Mixed, + _id: false + } + ], + default: undefined + }, + contextInfo: String, + acceptAction: String, + declineAction: String, + hasAccountAction: Boolean, + RegularCredits: Number + }, + { id: false } +); + +messageSchema.virtual("messageId").get(function (this: IMessageDatabase) { + return toOid(this._id); +}); + +messageSchema.set("toJSON", { + virtuals: true, + transform(_document, returnedObject: Record) { + 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; + + if (messageDatabase.globaUpgradeId) { + messageClient.globaUpgradeId = toOid(messageDatabase.globaUpgradeId); + } + + messageClient.date = toMongoDate(messageDatabase.date); + + if (messageDatabase.startDate && messageDatabase.endDate) { + messageClient.startDate = toMongoDate(messageDatabase.startDate); + messageClient.endDate = toMongoDate(messageDatabase.endDate); + } else { + delete messageClient.startDate; + delete messageClient.endDate; + } + } +}); + +messageSchema.index({ ownerId: 1 }); +messageSchema.index({ endDate: 1 }, { expireAfterSeconds: 0 }); + +export const Inbox = model("Inbox", messageSchema, "inbox"); diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts new file mode 100644 index 00000000..ff017f5d --- /dev/null +++ b/src/models/inventoryModels/inventoryModel.ts @@ -0,0 +1,1919 @@ +import type { Document, Model } from "mongoose"; +import { Schema, Types, model } from "mongoose"; +import type { + IRawUpgrade, + IMiscItem, + IInventoryDatabase, + IBooster, + IInventoryClient, + ISlots, + IDuviriInfo, + IPendingRecipeDatabase, + IPendingRecipeClient, + IFocusXP, + IFocusUpgrade, + ITypeXPItem, + IChallengeProgress, + IStepSequencer, + IAffiliation, + INotePacks, + ICompletedJobChain, + ISeasonChallenge, + IPlayerSkills, + ISettings, + IInfestedFoundryDatabase, + IHelminthResource, + IMissionDatabase, + IConsumedSuit, + IQuestStage, + IQuestKeyDatabase, + IQuestKeyClient, + IFusionTreasure, + ISpectreLoadout, + IWeaponSkinDatabase, + ITaunt, + IPeriodicMissionCompletionDatabase, + IPeriodicMissionCompletionResponse, + ILoreFragmentScan, + IEvolutionProgress, + IEndlessXpProgressDatabase, + IEndlessXpProgressClient, + IHelminthFoodRecord, + IDialogueHistoryDatabase, + IDialogueDatabase, + IDialogueGift, + ICompletedDialogue, + IDialogueClient, + IUpgradeDatabase, + TEquipmentKey, + IKubrowPetEggDatabase, + IKubrowPetEggClient, + ICustomMarkers, + IMarkerInfo, + 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, + IRewardAttenuation, + IInvasionProgressDatabase, + IInvasionProgressClient, + IAccolades, + IHubNpcCustomization, + IEndlessXpReward, + IGoalProgressDatabase, + IGoalProgressClient, + IKubrowPetPrintClient, + IKubrowPetPrintDatabase +} from "../../types/inventoryTypes/inventoryTypes.ts"; +import { equipmentKeys } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { IOid, ITypeCount } from "../../types/commonTypes.ts"; +import type { + IAbilityOverride, + ICrewShipCustomization, + IFlavourItem, + IItemConfig, + ILotusCustomization, + IOperatorConfigDatabase, + IPolarity +} from "../../types/inventoryTypes/commonInventoryTypes.ts"; +import { fromDbOid, toMongoDate, toOid } from "../../helpers/inventoryHelpers.ts"; +import { EquipmentSelectionSchema } from "./loadoutModel.ts"; +import type { ICountedStoreItem } from "warframe-public-export-plus"; +import { colorSchema, shipCustomizationSchema } from "../commonModel.ts"; +import type { + IArchonCrystalUpgrade, + ICrewShipMemberClient, + ICrewShipMemberDatabase, + ICrewShipMembersDatabase, + ICrewShipWeaponDatabase, + ICrewShipWeaponEmplacementsDatabase, + IEquipmentClient, + IEquipmentDatabase, + IKubrowPetDetailsClient, + IKubrowPetDetailsDatabase, + ITraits +} from "../../types/equipmentTypes.ts"; + +export const typeCountSchema = new Schema({ ItemType: String, ItemCount: Number }, { _id: false }); + +typeCountSchema.set("toJSON", { + transform(_doc, obj: Record) { + if (obj.ItemCount > 2147483647) { + obj.ItemCount = 2147483647; + } else if (obj.ItemCount < -2147483648) { + obj.ItemCount = -2147483648; + } + } +}); + +const focusXPSchema = new Schema( + { + AP_POWER: Number, + AP_TACTIC: Number, + AP_DEFENSE: Number, + AP_ATTACK: Number, + AP_WARD: Number + }, + { _id: false } +); + +const focusUpgradeSchema = new Schema( + { + ItemType: String, + Level: Number, + IsUniversal: Boolean + }, + { _id: false } +); + +const polaritySchema = new Schema( + { + Slot: Number, + Value: String + }, + { _id: false } +); + +const abilityOverrideSchema = new Schema( + { + Ability: String, + Index: Number + }, + { _id: false } +); + +const operatorConfigSchema = new Schema( + { + Skins: [String], + pricol: colorSchema, + attcol: colorSchema, + sigcol: colorSchema, + eyecol: colorSchema, + facial: colorSchema, + syancol: colorSchema, + cloth: colorSchema, + Upgrades: [String], + Name: String, // not sure if possible in operator + ugly: Boolean // not sure if possible in operator + }, + { id: false } +); + +operatorConfigSchema.virtual("ItemId").get(function () { + return { $oid: this._id.toString() } satisfies IOid; +}); + +operatorConfigSchema.set("toJSON", { + virtuals: true, + transform(_document, returnedObject: Record) { + delete returnedObject._id; + delete returnedObject.__v; + } +}); + +///TODO: clearly seperate the different config schemas. (suit and weapon and so on) +const ItemConfigSchema = new Schema( + { + Skins: [String], + pricol: colorSchema, + attcol: colorSchema, + sigcol: colorSchema, + eyecol: colorSchema, + facial: colorSchema, + syancol: colorSchema, + Upgrades: [String], + Songs: { + type: [ + { + m: String, + b: String, + p: String, + s: String + } + ], + default: undefined + }, + Name: String, + AbilityOverride: abilityOverrideSchema, + PvpUpgrades: [String], + ugly: Boolean + }, + { _id: false } +); + +ItemConfigSchema.set("toJSON", { + transform(_document, returnedObject: Record) { + delete returnedObject.__v; + } +}); + +const ArchonCrystalUpgradeSchema = new Schema( + { + UpgradeType: String, + Color: String + }, + { _id: false } +); + +const boosterSchema = new Schema( + { + ExpiryDate: Number, + ItemType: String + }, + { _id: false } +); + +const RawUpgrades = new Schema( + { + ItemType: String, + ItemCount: Number + }, + { id: false } +); + +RawUpgrades.virtual("LastAdded").get(function () { + return { $oid: this._id.toString() } satisfies IOid; +}); + +RawUpgrades.set("toJSON", { + virtuals: true, + transform(_document, returnedObject: Record) { + delete returnedObject._id; + delete returnedObject.__v; + } +}); + +const upgradeSchema = new Schema( + { + UpgradeFingerprint: String, + PendingRerollFingerprint: { type: String, required: false }, + ItemType: String + }, + { id: false } +); + +upgradeSchema.virtual("ItemId").get(function () { + return toOid(this._id); +}); + +upgradeSchema.set("toJSON", { + virtuals: true, + transform(_document, returnedObject: Record) { + delete returnedObject._id; + delete returnedObject.__v; + } +}); + +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: Record) { + 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, + Extra: Number + }, + { _id: false } +); + +const FlavourItemSchema = new Schema( + { + ItemType: String + }, + { _id: false } +); + +FlavourItemSchema.set("toJSON", { + transform(_document, returnedObject: Record) { + delete returnedObject._id; + delete returnedObject.__v; + } +}); + +/*const MailboxSchema = new Schema( + { + LastInboxId: Schema.Types.ObjectId + }, + { id: false, _id: false } +); + +MailboxSchema.set("toJSON", { + transform(_document, returnedObject: Record) { + const mailboxDatabase = returnedObject as HydratedDocument; + delete mailboxDatabase.__v; + (returnedObject as IMailboxClient).LastInboxId = toOid(mailboxDatabase.LastInboxId); + } +});*/ + +const DuviriInfoSchema = new Schema( + { + Seed: { type: BigInt, required: true }, + NumCompletions: { type: Number, required: true } + }, + { + _id: false, + id: false + } +); + +DuviriInfoSchema.set("toJSON", { + transform(_document, returnedObject: Record) { + delete returnedObject.__v; + } +}); + +const TypeXPItemSchema = new Schema( + { + ItemType: String, + XP: Number + }, + { _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: Record) { + 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 personalGoalProgressSchema = new Schema( + { + Best: Number, + Count: Number, + Tag: String, + goalId: Types.ObjectId + }, + { _id: false } +); + +personalGoalProgressSchema.set("toJSON", { + virtuals: true, + transform(_doc, obj: Record) { + const db = obj as IGoalProgressDatabase; + const client = obj as IGoalProgressClient; + + client._id = toOid(db.goalId); + + delete obj.goalId; + delete obj.__v; + } +}); + +const challengeProgressSchema = new Schema( + { + Progress: Number, + Completed: { type: [String], default: undefined }, + ReceivedJunctionReward: Boolean, + Name: { type: String, required: true } + }, + { _id: false } +); + +const notePacksSchema = new Schema( + { + MELODY: String, + BASS: String, + PERCUSSION: String + }, + { _id: false } +); + +const StepSequencersSchema = new Schema( + { + NotePacks: notePacksSchema, + FingerPrint: String, + Name: String + }, + { id: false } +); + +StepSequencersSchema.virtual("ItemId").get(function () { + return { $oid: this._id.toString() } satisfies IOid; +}); + +StepSequencersSchema.set("toJSON", { + virtuals: true, + transform(_document, returnedObject: Record) { + delete returnedObject._id; + delete returnedObject.__v; + } +}); + +const kubrowPetEggSchema = new Schema( + { + ItemType: String + }, + { id: false } +); +kubrowPetEggSchema.set("toJSON", { + virtuals: true, + transform(_document, obj: Record) { + const client = obj as IKubrowPetEggClient; + const db = obj as IKubrowPetEggDatabase; + + client.ExpirationDate = { $date: { $numberLong: "2000000000000" } }; + client.ItemId = toOid(db._id); + + delete obj._id; + delete obj.__v; + } +}); + +const weeklyMissionSchema = new Schema( + { + MissionIndex: Number, + CompletedMission: Boolean, + JobManifest: String, + Challenges: [String], + ChallengesReset: Boolean, + WeekCount: Number + }, + { _id: false } +); + +const affiliationsSchema = new Schema( + { + Initiated: Boolean, + Standing: Number, + Title: Number, + FreeFavorsEarned: { type: [Number], default: undefined }, + FreeFavorsUsed: { type: [Number], default: undefined }, + WeeklyMissions: { type: [weeklyMissionSchema], default: undefined }, + Tag: String + }, + { _id: false } +); + +const completedJobChainsSchema = new Schema( + { + LocationTag: String, + Jobs: [String] + }, + { _id: false } +); + +const seasonChallengeHistorySchema = new Schema( + { + challenge: String, + id: String + }, + { _id: false } +); + +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: Record) { + 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 }, + LPS_PILOTING: { type: Number, default: 0 }, + LPS_GUNNERY: { type: Number, default: 0 }, + LPS_TACTICAL: { type: Number, default: 0 }, + LPS_ENGINEERING: { type: Number, default: 0 }, + LPS_COMMAND: { type: Number, default: 0 }, + LPP_DRIFTER: { type: Number, default: 0 }, + LPS_DRIFT_COMBAT: { type: Number, default: 0 }, + LPS_DRIFT_RIDING: { type: Number, default: 0 }, + LPS_DRIFT_OPPORTUNITY: { type: Number, default: 0 }, + LPS_DRIFT_ENDURANCE: { type: Number, default: 0 } + }, + { _id: false } +); + +const settingsSchema = new Schema({ + FriendInvRestriction: String, + GiftMode: String, + GuildInvRestriction: String, + ShowFriendInvNotifications: Boolean, + TradingRulesConfirmed: Boolean, + SubscribedToSurveys: Boolean +}); + +const consumedSchuitsSchema = new Schema( + { + s: String, + c: colorSchema + }, + { _id: false } +); + +const helminthFoodRecordSchema = new Schema( + { + ItemType: String, + Date: Number + }, + { _id: false } +); + +const helminthResourceSchema = new Schema( + { + ItemType: String, + Count: Number, + RecentlyConvertedResources: { type: [helminthFoodRecordSchema], default: undefined } + }, + { _id: false } +); + +const missionSchema = new Schema( + { + Tag: String, + Completes: { type: Number, default: 0 }, + Tier: { type: Number, required: false } + }, + { _id: false } +); + +const questProgressSchema = new Schema( + { + c: Number, + i: Boolean, + m: Boolean, + b: [] + }, + { _id: false } +); + +const questKeysSchema = new Schema( + { + Progress: { type: [questProgressSchema], default: [] }, + unlock: Boolean, + Completed: Boolean, + CustomData: String, + CompletionDate: Date, + ItemType: String + }, + { + _id: false + } +); + +questKeysSchema.set("toJSON", { + transform(_doc, ret: Record) { + const questKeysDatabase = ret as IQuestKeyDatabase; + + if (questKeysDatabase.CompletionDate) { + (questKeysDatabase as IQuestKeyClient).CompletionDate = toMongoDate(questKeysDatabase.CompletionDate); + } + } +}); + +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: Record) { + 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( + { + ItemType: String, + Suits: String, + LongGuns: String, + LongGunsModularParts: { type: [String], default: undefined }, + Pistols: String, + PistolsModularParts: { type: [String], default: undefined }, + Melee: String, + MeleeModularParts: { type: [String], default: undefined } + }, + { _id: false } +); + +const weaponSkinsSchema = new Schema( + { + ItemType: String, + Favorite: Boolean, + IsNew: Boolean + }, + { id: false } +); + +weaponSkinsSchema.virtual("ItemId").get(function () { + return { $oid: this._id.toString() }; +}); + +weaponSkinsSchema.set("toJSON", { + virtuals: true, + transform(_doc, ret: Record) { + delete ret._id; + delete ret.__v; + } +}); + +const tauntSchema = new Schema( + { + node: String, + state: String + }, + { _id: false } +); + +const periodicMissionCompletionsSchema = new Schema( + { + date: Date, + tag: String, + count: Number + }, + { _id: false } +); + +periodicMissionCompletionsSchema.set("toJSON", { + transform(_doc, ret: Record) { + const periodicMissionCompletionDatabase = ret as IPeriodicMissionCompletionDatabase; + + (periodicMissionCompletionDatabase as unknown as IPeriodicMissionCompletionResponse).date = toMongoDate( + periodicMissionCompletionDatabase.date + ); + } +}); + +const loreFragmentScansSchema = new Schema( + { + Progress: Number, + Region: String, + ItemType: String + }, + { _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, + Rank: Number, + ItemType: String + }, + { _id: false } +); + +const countedStoreItemSchema = new Schema( + { + StoreItem: String, + ItemCount: Number + }, + { _id: false } +); + +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: Record) { + 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, + PRIMARY_B: EquipmentSelectionSchema, + SECONDARY_A: EquipmentSelectionSchema, + SECONDARY_B: EquipmentSelectionSchema + }, + { _id: false } +); + +const crewShipWeaponSchema = new Schema( + { + PILOT: crewShipWeaponEmplacementsSchema, + PORT_GUNS: crewShipWeaponEmplacementsSchema, + STARBOARD_GUNS: crewShipWeaponEmplacementsSchema, + ARTILLERY: crewShipWeaponEmplacementsSchema, + SCANNER: crewShipWeaponEmplacementsSchema + }, + { _id: false } +); + +const crewShipCustomizationSchema = new Schema( + { + CrewshipInterior: shipCustomizationSchema + }, + { _id: false } +); + +const crewShipMemberSchema = new Schema( + { + ItemId: { type: Schema.Types.ObjectId, required: false }, + NemesisFingerprint: { type: BigInt, required: false } + }, + { _id: false } +); +crewShipMemberSchema.set("toJSON", { + virtuals: true, + transform(_doc, obj: Record) { + const db = obj as ICrewShipMemberDatabase; + const client = obj as ICrewShipMemberClient; + if (db.ItemId) { + client.ItemId = toOid(db.ItemId); + } + } +}); + +const crewShipMembersSchema = new Schema( + { + SLOT_A: { type: crewShipMemberSchema, required: false }, + SLOT_B: { type: crewShipMemberSchema, required: false }, + SLOT_C: { type: crewShipMemberSchema, required: false } + }, + { _id: false } +); + +const dialogueGiftSchema = new Schema( + { + Item: String, + GiftedQuantity: Number + }, + { _id: false } +); + +const completedDialogueSchema = new Schema( + { + Id: { type: String, required: true }, + Booleans: { type: [String], required: true }, + Choices: { type: [Number], required: true } + }, + { _id: false } +); + +const dialogueSchema = new Schema( + { + Rank: Number, + Chemistry: Number, + AvailableDate: Date, + AvailableGiftDate: Date, + RankUpExpiry: Date, + BountyChemExpiry: Date, + QueuedDialogues: { type: [String], default: [] }, + Gifts: { type: [dialogueGiftSchema], default: [] }, + Booleans: { type: [String], default: [] }, + Completed: { type: [completedDialogueSchema], default: [] }, + DialogueName: String + }, + { _id: false } +); +dialogueSchema.set("toJSON", { + virtuals: true, + transform(_doc, ret: Record) { + const db = ret as IDialogueDatabase; + const client = ret as IDialogueClient; + + client.AvailableDate = toMongoDate(db.AvailableDate); + client.AvailableGiftDate = toMongoDate(db.AvailableGiftDate); + client.RankUpExpiry = toMongoDate(db.RankUpExpiry); + client.BountyChemExpiry = toMongoDate(db.BountyChemExpiry); + } +}); + +const dialogueHistorySchema = new Schema( + { + YearIteration: Number, + Resets: Number, + Dialogues: { type: [dialogueSchema], required: false } + }, + { _id: false } +); + +const traitsSchema = new Schema( + { + BaseColor: String, + SecondaryColor: String, + TertiaryColor: String, + AccentColor: String, + EyeColor: String, + FurPattern: String, + Personality: String, + BodyType: String, + Head: { type: String, required: false }, + Tail: { type: String, required: false } + }, + { _id: false } +); + +const kubrowPetPrintSchema = new Schema({ + ItemType: String, + Name: String, + IsMale: Boolean, + Size: Number, + DominantTraits: traitsSchema, + RecessiveTraits: traitsSchema +}); +kubrowPetPrintSchema.set("toJSON", { + virtuals: true, + transform(_doc, obj: Record) { + const db = obj as IKubrowPetPrintDatabase; + const client = obj as IKubrowPetPrintClient; + + client.ItemId = toOid(db._id); + + delete obj._id; + delete obj.__v; + } +}); + +const detailsSchema = new Schema( + { + Name: String, + IsPuppy: Boolean, + HasCollar: Boolean, + PrintsRemaining: Number, + Status: String, + HatchDate: Date, + DominantTraits: traitsSchema, + RecessiveTraits: traitsSchema, + IsMale: Boolean, + Size: Number + }, + { _id: false } +); + +detailsSchema.set("toJSON", { + transform(_doc, returnedObject: Record) { + delete returnedObject.__v; + + const db = returnedObject as IKubrowPetDetailsDatabase; + const client = returnedObject as IKubrowPetDetailsClient; + + if (db.HatchDate) { + client.HatchDate = toMongoDate(db.HatchDate); + } + } +}); + +const EquipmentSchema = new Schema( + { + ItemType: String, + Configs: { type: [ItemConfigSchema], default: [] }, + UpgradeVer: { type: Number, default: 101 }, + XP: { type: Number, default: 0 }, + Features: Number, + Polarized: Number, + Polarity: [polaritySchema], + FocusLens: String, + ModSlotPurchases: Number, + CustomizationSlotPurchases: Number, + UpgradeType: String, + UpgradeFingerprint: String, + ItemName: String, + InfestationDate: Date, + InfestationDays: Number, + InfestationType: String, + ModularParts: { type: [String], default: undefined }, + UnlockLevel: Number, + Expiry: Date, + SkillTree: String, + OffensiveUpgrade: String, + DefensiveUpgrade: String, + UpgradesExpiry: Date, + UmbraDate: Date, + ArchonCrystalUpgrades: { type: [ArchonCrystalUpgradeSchema], default: undefined }, + Weapon: crewShipWeaponSchema, + Customization: crewShipCustomizationSchema, + RailjackImage: FlavourItemSchema, + CrewMembers: crewShipMembersSchema, + Details: detailsSchema, + Favorite: Boolean, + IsNew: Boolean + }, + { id: false } +); + +EquipmentSchema.virtual("ItemId").get(function () { + return { $oid: this._id.toString() } satisfies IOid; +}); + +EquipmentSchema.set("toJSON", { + virtuals: true, + transform(_document, returnedObject: Record) { + delete returnedObject._id; + delete returnedObject.__v; + + const db = returnedObject as IEquipmentDatabase; + const client = returnedObject as IEquipmentClient; + + if (db.InfestationDate) { + client.InfestationDate = toMongoDate(db.InfestationDate); + } + if (db.UpgradesExpiry) { + client.UpgradesExpiry = toMongoDate(db.UpgradesExpiry); + } + if (db.UmbraDate) { + client.UmbraDate = toMongoDate(db.UmbraDate); + } + + if (client.ArchonCrystalUpgrades) { + // For some reason, mongoose turns empty objects here into nulls, so we have to fix it. + client.ArchonCrystalUpgrades = client.ArchonCrystalUpgrades.map(x => (x as unknown) ?? {}); + } + } +}); + +const equipmentFields: Record = {}; + +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 }, + KubrowPet: { 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: Record) { + delete returnedObject._id; + delete returnedObject.__v; + delete returnedObject.LongGuns; + delete returnedObject.Pistols; + delete returnedObject.Melees; + delete returnedObject.SuitToUnbrand; + delete returnedObject.KubrowPet; + (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, + Resources: { type: [helminthResourceSchema], default: undefined }, + Slots: Number, + XP: Number, + ConsumedSuits: { type: [consumedSchuitsSchema], default: undefined }, + InvigorationIndex: Number, + InvigorationSuitOfferings: { type: [String], default: undefined }, + InvigorationsApplied: Number, + LastConsumedSuit: { type: EquipmentSchema, default: undefined }, + AbilityOverrideUnlockCooldown: Date + }, + { _id: false } +); + +infestedFoundrySchema.set("toJSON", { + transform(_doc, ret: Record) { + if (ret.AbilityOverrideUnlockCooldown) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + ret.AbilityOverrideUnlockCooldown = toMongoDate(ret.AbilityOverrideUnlockCooldown); + } + } +}); + +const markerSchema = new Schema( + { + anchorName: String, + color: Number, + label: String, + x: Number, + y: Number, + z: Number, + showInHud: Boolean + }, + { _id: false } +); + +const markerInfoSchema = new Schema( + { + icon: String, + markers: [markerSchema] + }, + { _id: false } +); + +const CustomMarkersSchema = new Schema( + { + tag: String, + markerInfos: [markerInfoSchema] + }, + { _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: -1 }, + LastCompletedChallengeDayIdx: { type: Number, default: -1 }, + 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: Record) { + 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: Record) { + (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: Record) { + 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: Record) { + const db = obj as ILastSortieRewardDatabase; + const client = obj as ILastSortieRewardClient; + + client.SortieId = toOid(db.SortieId); + + delete obj._id; + delete obj.__v; + } +}); + +const rewardAttenutationSchema = new Schema( + { + Tag: { type: String, required: true }, + Atten: { type: Number, required: true } + }, + { _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, + + // SNS account cheats + skipAllDialogue: Boolean, + dontSubtractPurchaseCreditCost: Boolean, + dontSubtractPurchasePlatinumCost: Boolean, + dontSubtractPurchaseItemCost: Boolean, + dontSubtractPurchaseStandingCost: Boolean, + dontSubtractVoidTraces: Boolean, + dontSubtractConsumables: Boolean, + finishInvasionsInOneMission: Boolean, + infiniteCredits: Boolean, + infinitePlatinum: Boolean, + infiniteEndo: Boolean, + infiniteRegalAya: Boolean, + infiniteHelminthMaterials: Boolean, + universalPolarityEverywhere: Boolean, + unlockDoubleCapacityPotatoesEverywhere: Boolean, + unlockExilusEverywhere: Boolean, + unlockArcanesEverywhere: Boolean, + syndicateMissionsRepeatable: Boolean, + instantFinishRivenChallenge: Boolean, + noDailyStandingLimits: Boolean, + noDailyFocusLimit: Boolean, + noArgonCrystalDecay: Boolean, + noMasteryRankUpCooldown: Boolean, + noVendorPurchaseLimits: Boolean, + noDeathMarks: Boolean, + noKimCooldowns: Boolean, + claimingBlueprintRefundsIngredients: Boolean, + instantResourceExtractorDrones: Boolean, + noResourceExtractorDronesDamage: Boolean, + missionsCanGiveAllRelics: Boolean, + exceptionalRelicsAlwaysGiveBronzeReward: Boolean, + flawlessRelicsAlwaysGiveSilverReward: Boolean, + radiantRelicsAlwaysGiveGoldReward: Boolean, + disableDailyTribute: Boolean, + + SubscribedToEmails: { type: Number, default: 0 }, + SubscribedToEmailsPersonalized: { type: Number, default: 0 }, + RewardSeed: BigInt, + + //Credit + RegularCredits: { type: Number, default: 0 }, + //Platinum + PremiumCredits: { type: Number, default: 0 }, + //Gift Platinum(Non trade) + PremiumCreditsFree: { type: Number, default: 0 }, + //Endo + FusionPoints: { type: Number, default: 0 }, + //Dirac + CrewShipFusionPoints: { type: Number, default: 0 }, + //Regal Aya + PrimeTokens: { type: Number, default: 0 }, + + //Slots + SuitBin: { type: slotsBinSchema, default: { Slots: 3 } }, + WeaponBin: { type: slotsBinSchema, default: { Slots: 11 } }, + SentinelBin: { type: slotsBinSchema, default: { Slots: 10 } }, + SpaceSuitBin: { type: slotsBinSchema, default: { Slots: 4 } }, + SpaceWeaponBin: { type: slotsBinSchema, default: { Slots: 4 } }, + PvpBonusLoadoutBin: { type: slotsBinSchema, default: { Slots: 0 } }, + PveBonusLoadoutBin: { type: slotsBinSchema, default: { Slots: 0 } }, + RandomModBin: { type: slotsBinSchema, default: { Slots: 15 } }, + OperatorAmpBin: { type: slotsBinSchema, default: { Slots: 8 } }, + CrewShipSalvageBin: { type: slotsBinSchema, default: { Slots: 8 } }, + MechBin: { type: slotsBinSchema, default: { Slots: 4 } }, + CrewMemberBin: { type: slotsBinSchema, default: { Slots: 3 } }, + + ...equipmentFields, + + //How many trades do you have left + TradesRemaining: { type: Number, default: 0 }, + //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], + + //Syndicate currently being pledged to. + SupportedSyndicate: String, + //Curent Syndicates rank\exp + Affiliations: [affiliationsSchema], + //Syndicates Missions complate(Navigation->Syndicate) + CompletedSyndicates: [String], + //Daily Syndicates Exp + DailyAffiliation: { type: Number, default: 16000 }, + DailyAffiliationPvp: { type: Number, default: 16000 }, + DailyAffiliationLibrary: { type: Number, default: 16000 }, + DailyAffiliationCetus: { type: Number, default: 16000 }, + DailyAffiliationQuills: { type: Number, default: 16000 }, + DailyAffiliationSolaris: { type: Number, default: 16000 }, + DailyAffiliationVentkids: { type: Number, default: 16000 }, + DailyAffiliationVox: { type: Number, default: 16000 }, + DailyAffiliationEntrati: { type: Number, default: 16000 }, + DailyAffiliationNecraloid: { type: Number, default: 16000 }, + DailyAffiliationZariman: { type: Number, default: 16000 }, + DailyAffiliationKahl: { type: Number, default: 16000 }, + DailyAffiliationCavia: { type: Number, default: 16000 }, + DailyAffiliationHex: { type: Number, default: 16000 }, + + //Daily Focus limit + DailyFocus: { type: Number, default: 250000 }, + //Focus XP per School + FocusXP: focusXPSchema, + //Curent active like Active school focuses is = "Zenurik" + FocusAbility: String, + //The treeways of the Focus school.(Active and passive Ability) + FocusUpgrades: [focusUpgradeSchema], + + //Achievement + ChallengeProgress: [challengeProgressSchema], + + //Account Item like Ferrite,Form,Kuva etc + MiscItems: { type: [typeCountSchema], default: [] }, + FoundToday: { type: [typeCountSchema], default: undefined }, + + //Non Upgrade Mods Example:I have 999 item WeaponElectricityDamageMod (only "ItemCount"+"ItemType") + RawUpgrades: [RawUpgrades], + //Upgrade Mods\Riven\Arcane Example:"UpgradeFingerprint"+"ItemType"+"" + Upgrades: [upgradeSchema], + + //The Mandachord(Octavia) is a step sequencer + StepSequencers: [StepSequencersSchema], + + KubrowPetEggs: [kubrowPetEggSchema], + //Prints Cat(3 Prints)\Kubrow(2 Prints) Pets + KubrowPetPrints: [kubrowPetPrintSchema], + + //Item for EquippedGear example:Scaner,LoadoutTechSummon etc + Consumables: [typeCountSchema], + //Weel Emotes+Gear + EquippedEmotes: [String], + EquippedGear: [String], + //Equipped Shawzin + EquippedInstrument: String, + ReceivedStartingGear: Boolean, + + ArchwingEnabled: Boolean, + HasOwnedVoidProjectionsPreviously: Boolean, + + //Use Operator\Drifter + UseAdultOperatorLoadout: Boolean, + //Operator + OperatorLoadOuts: [operatorConfigSchema], + //Drifter + AdultOperatorLoadOuts: [operatorConfigSchema], + OperatorCustomizationSlotPurchases: Number, + // Kahl + KahlLoadOuts: [operatorConfigSchema], + + //LandingCraft like Liset + Ships: { type: [Schema.Types.ObjectId], ref: "Ships" }, + // /Lotus/Types/Items/ShipDecos/ + ShipDecorations: [typeCountSchema], + + //Railjack/Components(https://warframe.fandom.com/wiki/Railjack/Components) + CrewShipRawSalvage: [typeCountSchema], + + //Default RailJack + CrewShipAmmo: [typeCountSchema], + CrewShipWeaponSkins: [upgradeSchema], + CrewShipSalvagedWeaponSkins: [upgradeSchema], + + //RailJack Crew + CrewMembers: [crewMemberSchema], + + //Complete Mission\Quests + Missions: [missionSchema], + QuestKeys: [questKeysSchema], + ActiveQuest: { type: String, default: "" }, + //item like DojoKey or Boss missions key + LevelKeys: [typeCountSchema], + //Active quests + //Quests: [Schema.Types.Mixed], + + //Cosmetics like profile glyphs\Kavasa Prime Kubrow Collar\Game Theme etc + FlavourItems: [FlavourItemSchema], + + //Mastery Rank*(Need item XPInfo to rank up) + PlayerLevel: { type: Number, default: 0 }, + //Item Mastery Rank exp + XPInfo: [TypeXPItemSchema], + //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, + + //Blueprints for Foundry + Recipes: [typeCountSchema], + //Crafting Blueprint(Item Name + CompletionDate) + PendingRecipes: [pendingRecipeSchema], + + //Skins for Suits, Weapons etc. + WeaponSkins: [weaponSkinsSchema], + + //Ayatan Item + FusionTreasures: [fusionTreasuresSchema], + //only used for Maroo apparently - { "node": "TreasureTutorial", "state": "TS_COMPLETED" } + TauntHistory: { type: [tauntSchema], default: undefined }, + + //noShow2FA,VisitPrimeVault etc + //WebFlags: Schema.Types.Mixed, + //Id CompletedAlerts + CompletedAlerts: [String], + + //Warframe\Duviri + StoryModeChoice: { type: String, default: "WARFRAME" }, + + //Alert->Kuva Siphon + PeriodicMissionCompletions: [periodicMissionCompletionsSchema], + + //Codex->LoreFragment + LoreFragmentScans: [loreFragmentScansSchema], + + //Resource,Credit,Affinity etc or Bless any boosters + Boosters: [boosterSchema], + BlessingCooldown: Date, // Date convert to IMongoDate + + //the color your clan requests like Items/Research/DojoColors/DojoColorPlainsB + ActiveDojoColorResearch: String, + + //SentientSpawnChanceBoosters: Schema.Types.Mixed, + + QualifyingInvasions: [invasionProgressSchema], + FactionScores: [Number], + + // https://warframe.fandom.com/wiki/Specter_(Tenno) + PendingSpectreLoadouts: { type: [spectreLoadoutsSchema], default: undefined }, + SpectreLoadouts: { type: [spectreLoadoutsSchema], default: undefined }, + + //Darvo Deal + UsedDailyDeals: [String], + + //New Quest Email + EmailItems: [typeCountSchema], + + //Profile->Wishlist + Wishlist: [String], + + //https://warframe.fandom.com/wiki/Alignment + //like "Alignment": { "Wisdom": 9, "Alignment": 1 }, + Alignment: alignmentSchema, + AlignmentReplay: alignmentSchema, + + //https://warframe.fandom.com/wiki/Sortie + CompletedSorties: [String], + LastSortieReward: { type: [lastSortieRewardSchema], default: undefined }, + LastLiteSortieReward: { type: [lastSortieRewardSchema], default: undefined }, + SortieRewardAttenuation: { type: [rewardAttenutationSchema], default: undefined }, + + // Resource Extractor Drones + Drones: [droneSchema], + + //Active profile ico + ActiveAvatarImageType: String, + + // open location store like EidolonPlainsDiscoverable or OrbVallisCaveDiscoverable + DiscoveredMarkers: [discoveredMarkerSchema], + //Open location mission like "JobId" + "StageCompletions" + //CompletedJobs: [Schema.Types.Mixed], + + //Game mission\ivent score example "Tag": "WaterFight", "Best": 170, "Count": 1258, + PersonalGoalProgress: { type: [personalGoalProgressSchema], default: undefined }, + + //Setting interface Style + ThemeStyle: String, + ThemeBackground: String, + ThemeSounds: String, + + //Daily LoginRewards + LoginMilestoneRewards: { type: [String], default: [] }, + + //You first Dialog with NPC or use new Item + NodeIntrosCompleted: [String], + + //Current guild id, if applicable. + GuildId: { type: Schema.Types.ObjectId, ref: "Guild" }, + + //https://warframe.fandom.com/wiki/Heist + //ProfitTaker(1-4) Example:"LocationTag": "EudicoHeists", "Jobs":Mission name + CompletedJobChains: { type: [completedJobChainsSchema], default: undefined }, + //Night Wave Challenge + SeasonChallengeHistory: [seasonChallengeHistorySchema], + + LibraryPersonalTarget: String, + //Cephalon Simaris Entries Example:"TargetType"+"Scans"(1-10)+"Completed": true|false + LibraryPersonalProgress: { type: [libraryPersonalProgressSchema], default: [] }, + //Cephalon Simaris Daily Task + LibraryAvailableDailyTaskInfo: libraryDailyTaskInfoSchema, + LibraryActiveDailyTaskInfo: libraryDailyTaskInfoSchema, + + //https://warframe.fandom.com/wiki/Invasion + //InvasionChainProgress: [Schema.Types.Mixed], + + //CorpusLich or GrineerLich + 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: { 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, + + //https://warframe.fandom.com/wiki/Helminth + InfestedFoundry: infestedFoundrySchema, + + 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: { type: lotusCustomizationSchema, default: undefined }, + + //Progress+Rank+ItemType(ZarimanPumpShotgun) + //https://warframe.fandom.com/wiki/Incarnon + EvolutionProgress: { type: [evolutionProgressSchema], default: undefined }, + + //https://warframe.fandom.com/wiki/Loc-Pin + CustomMarkers: { type: [CustomMarkersSchema], default: undefined }, + + //Unknown and system + DuviriInfo: DuviriInfoSchema, + LastInventorySync: Schema.Types.ObjectId, + //Mailbox: MailboxSchema, + HandlerPoints: Number, + ChallengesFixVersion: Number, + PlayedParkourTutorial: Boolean, + //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], // should be Types.ObjectId[] but might be IOid[] because of old commits + RandomUpgradesIdentified: Number, + BountyScore: Number, + //ChallengeInstanceStates: [Schema.Types.Mixed], + RecentVendorPurchases: { type: [recentVendorPurchaseSchema], default: undefined }, + //Robotics: [Schema.Types.Mixed], + CollectibleSeries: { type: [collectibleEntrySchema], default: undefined }, + HasResetAccount: { type: Boolean, default: false }, + + //Discount Coupon + PendingCoupon: pendingCouponSchema, + //Like BossAladV,BossCaptainVor come for you on missions % chance + DeathMarks: { type: [String], default: [] }, + //Zanuka + Harvestable: Boolean, + //Grustag three + DeathSquadable: Boolean, + + EndlessXP: { type: [endlessXpProgressSchema], default: undefined }, + + 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 }, + + ClaimedJunctionChallengeRewards: { type: [String], default: undefined }, + + SpecialItemRewardAttenuation: { type: [rewardAttenutationSchema], default: undefined } + }, + { timestamps: { createdAt: "Created", updatedAt: false } } +); + +inventorySchema.set("toJSON", { + transform(_document, returnedObject: Record) { + delete returnedObject._id; + delete returnedObject.__v; + delete returnedObject.accountOwnerId; + + const inventoryDatabase = returnedObject as Partial; + const inventoryResponse = returnedObject as IInventoryClient; + + if (inventoryDatabase.Created) { + inventoryResponse.Created = toMongoDate(inventoryDatabase.Created); + } + if (inventoryDatabase.CurrentLoadOutIds) { + inventoryResponse.CurrentLoadOutIds = inventoryDatabase.CurrentLoadOutIds.map(x => toOid(fromDbOid(x))); + } + if (inventoryDatabase.TrainingDate) { + inventoryResponse.TrainingDate = toMongoDate(inventoryDatabase.TrainingDate); + } + if (inventoryDatabase.GuildId) { + inventoryResponse.GuildId = toOid(inventoryDatabase.GuildId); + } + if (inventoryDatabase.BlessingCooldown) { + inventoryResponse.BlessingCooldown = toMongoDate(inventoryDatabase.BlessingCooldown); + } + if (inventoryDatabase.NextRefill) { + inventoryResponse.NextRefill = toMongoDate(inventoryDatabase.NextRefill); + } + if (inventoryDatabase.EntratiVaultCountResetDate) { + inventoryResponse.EntratiVaultCountResetDate = toMongoDate(inventoryDatabase.EntratiVaultCountResetDate); + } + if (inventoryDatabase.BrandedSuits) { + inventoryResponse.BrandedSuits = inventoryDatabase.BrandedSuits.map(toOid); + } + if (inventoryDatabase.LockedWeaponGroup) { + inventoryResponse.LockedWeaponGroup = { + s: toOid(inventoryDatabase.LockedWeaponGroup.s), + l: inventoryDatabase.LockedWeaponGroup.l ? toOid(inventoryDatabase.LockedWeaponGroup.l) : undefined, + p: inventoryDatabase.LockedWeaponGroup.p ? toOid(inventoryDatabase.LockedWeaponGroup.p) : undefined, + m: inventoryDatabase.LockedWeaponGroup.m ? toOid(inventoryDatabase.LockedWeaponGroup.m) : undefined, + sn: inventoryDatabase.LockedWeaponGroup.sn ? toOid(inventoryDatabase.LockedWeaponGroup.sn) : undefined + }; + } + 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; + RawUpgrades: Types.DocumentArray; + Upgrades: Types.DocumentArray; + MiscItems: Types.DocumentArray; + Boosters: Types.DocumentArray; + OperatorLoadOuts: Types.DocumentArray; + AdultOperatorLoadOuts: Types.DocumentArray; + 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; + KubrowPetPrints: Types.DocumentArray; +} & { [K in TEquipmentKey]: Types.DocumentArray }; + +// 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/no-empty-object-type +export type TInventoryDatabaseDocument = Document & + Omit< + IInventoryDatabase & { + _id: Types.ObjectId; + } & { + __v: number; + }, + keyof InventoryDocumentProps + > & + InventoryDocumentProps; diff --git a/src/models/inventoryModels/loadoutModel.ts b/src/models/inventoryModels/loadoutModel.ts new file mode 100644 index 00000000..676f7389 --- /dev/null +++ b/src/models/inventoryModels/loadoutModel.ts @@ -0,0 +1,119 @@ +import { fromDbOid, toOid } from "../../helpers/inventoryHelpers.ts"; +import type { IOid } from "../../types/commonTypes.ts"; +import type { IEquipmentSelectionClient, IEquipmentSelectionDatabase } from "../../types/equipmentTypes.ts"; +import type { ILoadoutConfigDatabase, ILoadoutDatabase } from "../../types/saveLoadoutTypes.ts"; +import type { Document, Model, Types } from "mongoose"; +import { Schema, model } from "mongoose"; + +//create a mongoose schema based on interface M +export const EquipmentSelectionSchema = new Schema( + { + ItemId: Schema.Types.Mixed, // should be Types.ObjectId but might be IOid because of old commits + mod: Number, + cus: Number, + hide: Boolean + }, + { + _id: false + } +); +EquipmentSelectionSchema.set("toJSON", { + virtuals: true, + transform(_doc, ret: Record) { + const db = ret as IEquipmentSelectionDatabase; + const client = ret as IEquipmentSelectionClient; + + if (db.ItemId) { + client.ItemId = toOid(fromDbOid(db.ItemId)); + } + } +}); + +export const loadoutConfigSchema = new Schema( + { + FocusSchool: String, + PresetIcon: String, + Favorite: Boolean, + n: String, // Loadout name + s: EquipmentSelectionSchema, // Suit + l: EquipmentSelectionSchema, // Primary weapon + p: EquipmentSelectionSchema, // Secondary weapon + m: EquipmentSelectionSchema, // Melee weapon + h: EquipmentSelectionSchema, // Gravimag weapon + a: EquipmentSelectionSchema // Necromech exalted weapon + }, + { + id: false + } +); + +loadoutConfigSchema.virtual("ItemId").get(function () { + return { $oid: this._id.toString() } satisfies IOid; +}); + +loadoutConfigSchema.set("toJSON", { + virtuals: true, + transform(_doc, ret: Record) { + delete ret._id; + delete ret.__v; + } +}); + +export const loadoutSchema = new Schema({ + NORMAL: [loadoutConfigSchema], + SENTINEL: [loadoutConfigSchema], + ARCHWING: [loadoutConfigSchema], + NORMAL_PVP: [loadoutConfigSchema], + LUNARO: [loadoutConfigSchema], + OPERATOR: [loadoutConfigSchema], + GEAR: [loadoutConfigSchema], + KDRIVE: [loadoutConfigSchema], + DATAKNIFE: [loadoutConfigSchema], + MECH: [loadoutConfigSchema], + OPERATOR_ADULT: [loadoutConfigSchema], + DRIFTER: [loadoutConfigSchema], + loadoutOwnerId: Schema.Types.ObjectId +}); + +loadoutSchema.set("toJSON", { + transform(_doc, ret: Record) { + delete ret._id; + delete ret.__v; + delete ret.loadoutOwnerId; + } +}); + +loadoutSchema.index({ loadoutOwnerId: 1 }, { unique: true }); + +//create database typefor ILoadoutConfig +type loadoutDocumentProps = { + NORMAL: Types.DocumentArray; + SENTINEL: Types.DocumentArray; + ARCHWING: Types.DocumentArray; + NORMAL_PVP: Types.DocumentArray; + LUNARO: Types.DocumentArray; + OPERATOR: Types.DocumentArray; + GEAR: Types.DocumentArray; + KDRIVE: Types.DocumentArray; + DATAKNIFE: Types.DocumentArray; + MECH: Types.DocumentArray; + OPERATOR_ADULT: Types.DocumentArray; + DRIFTER: Types.DocumentArray; +}; + +// 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..35602b13 --- /dev/null +++ b/src/models/leaderboardModel.ts @@ -0,0 +1,28 @@ +import type { Document, Types } from "mongoose"; +import { model, Schema } from "mongoose"; +import type { ILeaderboardEntryDatabase } from "../types/leaderboardTypes.ts"; + +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: Date, + 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 new file mode 100644 index 00000000..4403011d --- /dev/null +++ b/src/models/loginModel.ts @@ -0,0 +1,53 @@ +import type { IDatabaseAccountJson, IIgnore } from "../types/loginTypes.ts"; +import type { SchemaOptions } from "mongoose"; +import { model, Schema } from "mongoose"; + +const opts = { + toJSON: { virtuals: true }, + toObject: { virtuals: true } +} satisfies SchemaOptions; + +const databaseAccountSchema = new Schema( + { + email: { type: String, required: true, unique: true }, + password: { type: String, required: true }, + DisplayName: { type: String, required: true, unique: true }, + CountryCode: { type: String, default: "" }, + ClientType: { type: String }, + CrossPlatformAllowed: { type: Boolean, default: true }, + ForceLogoutVersion: { type: Number, default: 0 }, + AmazonAuthToken: { type: String }, + AmazonRefreshToken: { type: String }, + ConsentNeeded: { type: Boolean, default: false }, + TrackedSettings: { type: [String], default: [] }, + Nonce: { type: Number, 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 }, + DailyFirstWinDate: { type: Number, default: 0 } + }, + opts +); + +databaseAccountSchema.set("toJSON", { + transform(_document, returnedObject: Record) { + delete returnedObject._id; + delete returnedObject.__v; + }, + virtuals: true +}); + +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 new file mode 100644 index 00000000..28b9a103 --- /dev/null +++ b/src/models/personalRoomsModel.ts @@ -0,0 +1,245 @@ +import { toMongoDate, toOid } from "../helpers/inventoryHelpers.ts"; +import type { + IApartmentDatabase, + ICustomizationInfoDatabase, + IFavouriteLoadoutDatabase, + IGardeningDatabase, + IOrbiterClient, + IOrbiterDatabase, + IPersonalRoomsDatabase, + IPictureFrameInfo, + IPlacedDecosDatabase, + IPlantClient, + IPlantDatabase, + IPlanterDatabase, + IRoomDatabase, + ITailorShopDatabase, + PersonalRoomsModelType +} from "../types/personalRoomsTypes.ts"; +import type { Types } from "mongoose"; +import { Schema, model } from "mongoose"; +import { colorSchema, shipCustomizationSchema } from "./commonModel.ts"; +import { loadoutConfigSchema } from "./inventoryModels/loadoutModel.ts"; + +export const pictureFrameInfoSchema = new Schema( + { + Image: String, + Filter: String, + XOffset: Number, + YOffset: Number, + Scale: Number, + InvertX: Boolean, + InvertY: Boolean, + ColorCorrection: Number, + Text: String, + TextScale: Number, + TextColorA: Number, + TextColorB: Number, + TextOrientation: Number + }, + { _id: false } +); + +export const customizationInfoSchema = new Schema( + { + Anim: String, + AnimPose: Number, + LoadOutPreset: loadoutConfigSchema, + VehiclePreset: loadoutConfigSchema, + EquippedWeapon: String, + AvatarType: String, + LoadOutType: String + }, + { _id: false } +); + +const placedDecosSchema = new Schema( + { + Type: String, + Pos: [Number], + Rot: [Number], + Scale: Number, + Sockets: Number, + PictureFrameInfo: { type: pictureFrameInfoSchema, default: undefined }, + CustomizationInfo: { type: customizationInfoSchema, default: undefined }, + AnimPoseItem: String + }, + { id: false } +); + +placedDecosSchema.virtual("id").get(function (this: IPlacedDecosDatabase) { + return toOid(this._id); +}); + +placedDecosSchema.set("toJSON", { + virtuals: true, + transform(_document, returnedObject: Record) { + delete returnedObject._id; + } +}); + +const roomSchema = new Schema( + { + Name: String, + MaxCapacity: Number, + PlacedDecos: { type: [placedDecosSchema], default: [] } + }, + { _id: false } +); + +const favouriteLoadoutSchema = new Schema( + { + Tag: String, + LoadoutId: Schema.Types.ObjectId + }, + { _id: false } +); +favouriteLoadoutSchema.set("toJSON", { + virtuals: true, + transform(_document, returnedObject: Record) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + returnedObject.LoadoutId = toOid(returnedObject.LoadoutId); + } +}); + +const plantSchema = new Schema( + { + PlantType: String, + EndTime: Date, + PlotIndex: Number + }, + { _id: false } +); + +plantSchema.set("toJSON", { + virtuals: true, + transform(_doc, obj: Record) { + 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, + VideoWallBackdrop: String, + Soundscape: String + }, + { _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], + ShipInterior: shipCustomizationSchema, + VignetteFish: { type: [String], default: undefined }, + FavouriteLoadoutId: Schema.Types.ObjectId, + Wallpaper: String, + Vignette: String, + ContentUrlSignature: { type: String, required: false }, + BootLocation: String + }, + { _id: false } +); +orbiterSchema.set("toJSON", { + virtuals: true, + transform(_doc, obj: Record) { + const db = obj as IOrbiterDatabase; + const client = obj as IOrbiterClient; + + if (db.FavouriteLoadoutId) { + client.FavouriteLoadoutId = toOid(db.FavouriteLoadoutId); + } + } +}); +const orbiterDefault: IOrbiterDatabase = { + 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], + Colors: { type: colorSchema, required: false }, + CustomJson: String, + LevelDecosVisible: Boolean, + Rooms: [roomSchema] + }, + { _id: false } +); +const tailorShopDefault: ITailorShopDatabase = { + FavouriteLoadouts: [], + CustomJson: "{}", + LevelDecosVisible: true, + Rooms: [ + { Name: "LabRoom", MaxCapacity: 4000 }, + { Name: "LivingQuartersRoom", MaxCapacity: 3000 }, + { Name: "HelminthRoom", MaxCapacity: 2000 } + ] +}; + +export const personalRoomsSchema = new Schema({ + personalRoomsOwnerId: Schema.Types.ObjectId, + activeShipId: Schema.Types.ObjectId, + Ship: { type: orbiterSchema, default: orbiterDefault }, + Apartment: { type: apartmentSchema, default: apartmentDefault }, + 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 new file mode 100644 index 00000000..b0c74fea --- /dev/null +++ b/src/models/shipModel.ts @@ -0,0 +1,56 @@ +import type { Document, Types } from "mongoose"; +import { Schema, model } from "mongoose"; +import type { IShipDatabase } from "../types/shipTypes.ts"; +import { toOid } from "../helpers/inventoryHelpers.ts"; +import { colorSchema } from "./commonModel.ts"; +import type { IShipInventory } from "../types/inventoryTypes/inventoryTypes.ts"; + +const shipSchema = new Schema( + { + ItemType: String, + ShipOwnerId: Schema.Types.ObjectId, + ShipExteriorColors: colorSchema, + AirSupportPower: String, + ShipAttachments: { HOOD_ORNAMENT: String }, + SkinFlavourItem: String + }, + { id: false } +); + +shipSchema.virtual("ItemId").get(function () { + return toOid(this._id); +}); + +shipSchema.set("toJSON", { + virtuals: true, + transform(_document, returnedObject: Record) { + const shipResponse = returnedObject as IShipInventory; + const shipDatabase = returnedObject as IShipDatabase; + delete returnedObject._id; + delete returnedObject.__v; + delete returnedObject.ShipOwnerId; + + shipResponse.ShipExterior = { + Colors: shipDatabase.ShipExteriorColors, + ShipAttachments: shipDatabase.ShipAttachments, + SkinFlavourItem: shipDatabase.SkinFlavourItem + }; + delete shipDatabase.ShipExteriorColors; + delete shipDatabase.ShipAttachments; + delete shipDatabase.SkinFlavourItem; + } +}); + +shipSchema.set("toObject", { + virtuals: true +}); + +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 new file mode 100644 index 00000000..fbc572b6 --- /dev/null +++ b/src/models/statsModel.ts @@ -0,0 +1,141 @@ +import type { Document, Types } from "mongoose"; +import { Schema, model } from "mongoose"; +import type { + IEnemy, + IMission, + IScan, + ITutorial, + IAbility, + IWeapon, + IStatsDatabase, + IRace +} from "../types/statTypes.ts"; + +const abilitySchema = new Schema( + { + type: { type: String, required: true }, + used: { type: Number, required: true } + }, + { _id: false } +); + +const enemySchema = new Schema( + { + type: { type: String, required: true }, + executions: Number, + headshots: Number, + kills: Number, + assists: Number, + deaths: Number, + captures: Number + }, + { _id: false } +); + +const missionSchema = new Schema( + { + type: { type: String, required: true }, + highScore: Number + }, + { _id: false } +); + +const scanSchema = new Schema( + { + type: { type: String, required: true }, + scans: { type: Number, required: true } + }, + { _id: false } +); + +const tutorialSchema = new Schema( + { + stage: Number + }, + { _id: false } +); + +const weaponSchema = new Schema( + { + type: { type: String, required: true }, + equipTime: Number, + hits: Number, + kills: Number, + xp: Number, + assists: Number, + headshots: Number, + fired: Number + }, + { _id: false } +); + +const raceSchema = new Schema( + { + highScore: Number + }, + { _id: false } +); + +const statsSchema = new Schema({ + accountOwnerId: { type: Schema.Types.ObjectId, required: true }, + CiphersSolved: Number, + CiphersFailed: Number, + CipherTime: Number, + Weapons: { type: [weaponSchema], default: [] }, + Enemies: { type: [enemySchema], default: [] }, + MeleeKills: Number, + MissionsCompleted: Number, + MissionsQuit: Number, + MissionsFailed: Number, + MissionsInterrupted: Number, + MissionsDumped: Number, + TimePlayedSec: Number, + PickupCount: Number, + Tutorial: { type: Map, of: tutorialSchema, default: {} }, + Abilities: { type: [abilitySchema], default: [] }, + Rating: Number, + Income: Number, + Rank: Number, + PlayerLevel: Number, + Scans: { type: [scanSchema], default: [] }, + Missions: { type: [missionSchema], default: [] }, + Deaths: Number, + HealCount: Number, + ReviveCount: Number, + Races: { type: Map, of: raceSchema, default: {} }, + ZephyrScore: Number, + SentinelGameScore: Number, + CaliberChicksScore: Number, + OlliesCrashCourseScore: Number, + DojoObstacleScore: Number, + + Halloween16: Number, + AmalgamEventScoreMax: Number, + Halloween19ScoreMax: Number, + FlotillaEventScore: Number, + FlotillaSpaceBadgesTier1: Number, + FlotillaSpaceBadgesTier2: Number, + FlotillaSpaceBadgesTier3: Number, + FlotillaGroundBadgesTier1: Number, + FlotillaGroundBadgesTier2: Number, + FlotillaGroundBadgesTier3: Number, + MechSurvivalScoreMax: Number +}); + +statsSchema.set("toJSON", { + transform(_document, returnedObject: Record) { + delete returnedObject._id; + delete returnedObject.__v; + delete returnedObject.accountOwnerId; + } +}); + +statsSchema.index({ accountOwnerId: 1 }, { unique: true }); + +export const Stats = model("Stats", statsSchema); + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export type TStatsDatabaseDocument = Document & { + _id: Types.ObjectId; + __v: number; +} & IStatsDatabase; diff --git a/src/models/worldStateModel.ts b/src/models/worldStateModel.ts new file mode 100644 index 00000000..59e243c0 --- /dev/null +++ b/src/models/worldStateModel.ts @@ -0,0 +1,30 @@ +import type { IDailyDealDatabase, IFissureDatabase } from "../types/worldStateTypes.ts"; +import { model, Schema } from "mongoose"; + +const fissureSchema = new Schema({ + Activation: Date, + Expiry: Date, + Node: String, // must be unique + Modifier: String, + Hard: Boolean +}); + +fissureSchema.index({ Expiry: 1 }, { expireAfterSeconds: 0 }); // With this, MongoDB will automatically delete expired entries. + +export const Fissure = model("Fissure", fissureSchema); + +const dailyDealSchema = new Schema({ + StoreItem: { type: String, required: true }, + Activation: { type: Date, required: true }, + Expiry: { type: Date, required: true }, + Discount: { type: Number, required: true }, + OriginalPrice: { type: Number, required: true }, + SalePrice: { type: Number, required: true }, + AmountTotal: { type: Number, required: true }, + AmountSold: { type: Number, required: true } +}); + +dailyDealSchema.index({ StoreItem: 1 }, { unique: true }); +dailyDealSchema.index({ Expiry: 1 }, { expireAfterSeconds: 86400 }); + +export const DailyDeal = model("DailyDeal", dailyDealSchema); diff --git a/src/routes/api.ts b/src/routes/api.ts new file mode 100644 index 00000000..a9c84b84 --- /dev/null +++ b/src/routes/api.ts @@ -0,0 +1,362 @@ +import express from "express"; +import { abandonLibraryDailyTaskController } from "../controllers/api/abandonLibraryDailyTaskController.ts"; +import { abortDojoComponentController } from "../controllers/api/abortDojoComponentController.ts"; +import { abortDojoComponentDestructionController } from "../controllers/api/abortDojoComponentDestructionController.ts"; +import { activateRandomModController } from "../controllers/api/activateRandomModController.ts"; +import { addFriendController } from "../controllers/api/addFriendController.ts"; +import { addFriendImageController } from "../controllers/api/addFriendImageController.ts"; +import { addIgnoredUserController } from "../controllers/api/addIgnoredUserController.ts"; +import { addPendingFriendController } from "../controllers/api/addPendingFriendController.ts"; +import { addToAllianceController } from "../controllers/api/addToAllianceController.ts"; +import { addToGuildController } from "../controllers/api/addToGuildController.ts"; +import { adoptPetController } from "../controllers/api/adoptPetController.ts"; +import { apartmentController } from "../controllers/api/apartmentController.ts"; +import { arcaneCommonController } from "../controllers/api/arcaneCommonController.ts"; +import { archonFusionController } from "../controllers/api/archonFusionController.ts"; +import { artifactsController } from "../controllers/api/artifactsController.ts"; +import { artifactTransmutationController } from "../controllers/api/artifactTransmutationController.ts"; +import { cancelGuildAdvertisementController } from "../controllers/api/cancelGuildAdvertisementController.ts"; +import { changeDojoRootController } from "../controllers/api/changeDojoRootController.ts"; +import { changeGuildRankController } from "../controllers/api/changeGuildRankController.ts"; +import { checkDailyMissionBonusController } from "../controllers/api/checkDailyMissionBonusController.ts"; +import { claimCompletedRecipeController } from "../controllers/api/claimCompletedRecipeController.ts"; +import { claimJunctionChallengeRewardController } from "../controllers/api/claimJunctionChallengeRewardController.ts"; +import { claimLibraryDailyTaskRewardController } from "../controllers/api/claimLibraryDailyTaskRewardController.ts"; +import { clearDialogueHistoryController } from "../controllers/api/clearDialogueHistoryController.ts"; +import { clearNewEpisodeRewardController } from "../controllers/api/clearNewEpisodeRewardController.ts"; +import { completeCalendarEventController } from "../controllers/api/completeCalendarEventController.ts"; +import { completeRandomModChallengeController } from "../controllers/api/completeRandomModChallengeController.ts"; +import { confirmAllianceInvitationController } from "../controllers/api/confirmAllianceInvitationController.ts"; +import { confirmGuildInvitationGetController, confirmGuildInvitationPostController } from "../controllers/api/confirmGuildInvitationController.ts"; +import { contributeGuildClassController } from "../controllers/api/contributeGuildClassController.ts"; +import { contributeToDojoComponentController } from "../controllers/api/contributeToDojoComponentController.ts"; +import { contributeToVaultController } from "../controllers/api/contributeToVaultController.ts"; +import { createAllianceController } from "../controllers/api/createAllianceController.ts"; +import { createGuildController } from "../controllers/api/createGuildController.ts"; +import { creditsController } from "../controllers/api/creditsController.ts"; +import { crewMembersController } from "../controllers/api/crewMembersController.ts"; +import { crewShipFusionController } from "../controllers/api/crewShipFusionController.ts"; +import { crewShipIdentifySalvageController } from "../controllers/api/crewShipIdentifySalvageController.ts"; +import { customizeGuildRanksController } from "../controllers/api/customizeGuildRanksController.ts"; +import { customObstacleCourseLeaderboardController } from "../controllers/api/customObstacleCourseLeaderboardController.ts"; +import { declineAllianceInviteController } from "../controllers/api/declineAllianceInviteController.ts"; +import { declineGuildInviteController } from "../controllers/api/declineGuildInviteController.ts"; +import { deleteSessionController } from "../controllers/api/deleteSessionController.ts"; +import { destroyDojoDecoController } from "../controllers/api/destroyDojoDecoController.ts"; +import { divvyAllianceVaultController } from "../controllers/api/divvyAllianceVaultController.ts"; +import { dojoComponentRushController } from "../controllers/api/dojoComponentRushController.ts"; +import { dojoController, setDojoURLController } from "../controllers/api/dojoController.ts"; +import { dronesController } from "../controllers/api/dronesController.ts"; +import { endlessXpController } from "../controllers/api/endlessXpController.ts"; +import { entratiLabConquestModeController } from "../controllers/api/entratiLabConquestModeController.ts"; +import { evolveWeaponController } from "../controllers/api/evolveWeaponController.ts"; +import { findSessionsController } from "../controllers/api/findSessionsController.ts"; +import { fishmongerController } from "../controllers/api/fishmongerController.ts"; +import { focusController } from "../controllers/api/focusController.ts"; +import { fusionTreasuresController } from "../controllers/api/fusionTreasuresController.ts"; +import { gardeningController } from "../controllers/api/gardeningController.ts"; +import { genericUpdateController } from "../controllers/api/genericUpdateController.ts"; +import { getAllianceController } from "../controllers/api/getAllianceController.ts"; +import { getDailyDealStockLevelsController } from "../controllers/api/getDailyDealStockLevelsController.ts"; +import { getFriendsController } from "../controllers/api/getFriendsController.ts"; +import { getGuildContributionsController } from "../controllers/api/getGuildContributionsController.ts"; +import { getGuildController } from "../controllers/api/getGuildController.ts"; +import { getGuildDojoController } from "../controllers/api/getGuildDojoController.ts"; +import { getGuildEventScoreController } from "../controllers/api/getGuildEventScoreController.ts"; +import { getGuildLogController } from "../controllers/api/getGuildLogController.ts"; +import { getIgnoredUsersController } from "../controllers/api/getIgnoredUsersController.ts"; +import { getNewRewardSeedController } from "../controllers/api/getNewRewardSeedController.ts"; +import { getProfileViewingDataPostController } from "../controllers/dynamic/getProfileViewingDataController.ts"; +import { getPastWeeklyChallengesController } from "../controllers/api/getPastWeeklyChallengesController.ts"; +import { getShipController } from "../controllers/api/getShipController.ts"; +import { getVendorInfoController } from "../controllers/api/getVendorInfoController.ts"; +import { getVoidProjectionRewardsController } from "../controllers/api/getVoidProjectionRewardsController.ts"; +import { giftingController } from "../controllers/api/giftingController.ts"; +import { gildWeaponController } from "../controllers/api/gildWeaponController.ts"; +import { giveKeyChainTriggeredItemsController } from "../controllers/api/giveKeyChainTriggeredItemsController.ts"; +import { giveKeyChainTriggeredMessageController } from "../controllers/api/giveKeyChainTriggeredMessageController.ts"; +import { giveQuestKeyRewardController } from "../controllers/api/giveQuestKeyRewardController.ts"; +import { giveShipDecoAndLoreFragmentController } from "../controllers/api/giveShipDecoAndLoreFragmentController.ts"; +import { giveStartingGearController } from "../controllers/api/giveStartingGearController.ts"; +import { guildTechController } from "../controllers/api/guildTechController.ts"; +import { hostSessionController } from "../controllers/api/hostSessionController.ts"; +import { hubBlessingController } from "../controllers/api/hubBlessingController.ts"; +import { hubController } from "../controllers/api/hubController.ts"; +import { hubInstancesController } from "../controllers/api/hubInstancesController.ts"; +import { inboxController } from "../controllers/api/inboxController.ts"; +import { infestedFoundryController } from "../controllers/api/infestedFoundryController.ts"; +import { inventoryController } from "../controllers/api/inventoryController.ts"; +import { inventorySlotsController } from "../controllers/api/inventorySlotsController.ts"; +import { joinSessionController } from "../controllers/api/joinSessionController.ts"; +import { loginController } from "../controllers/api/loginController.ts"; +import { loginRewardsController } from "../controllers/api/loginRewardsController.ts"; +import { loginRewardsSelectionController } from "../controllers/api/loginRewardsSelectionController.ts"; +import { logoutController } from "../controllers/api/logoutController.ts"; +import { marketRecommendationsController } from "../controllers/api/marketRecommendationsController.ts"; +import { maturePetController } from "../controllers/api/maturePetController.ts"; +import { missionInventoryUpdateController } from "../controllers/api/missionInventoryUpdateController.ts"; +import { modularWeaponCraftingController } from "../controllers/api/modularWeaponCraftingController.ts"; +import { modularWeaponSaleController } from "../controllers/api/modularWeaponSaleController.ts"; +import { nameWeaponController } from "../controllers/api/nameWeaponController.ts"; +import { nemesisController } from "../controllers/api/nemesisController.ts"; +import { placeDecoInComponentController } from "../controllers/api/placeDecoInComponentController.ts"; +import { playedParkourTutorialController } from "../controllers/api/playedParkourTutorialController.ts"; +import { playerSkillsController } from "../controllers/api/playerSkillsController.ts"; +import { postGuildAdvertisementController } from "../controllers/api/postGuildAdvertisementController.ts"; +import { projectionManagerController } from "../controllers/api/projectionManagerController.ts"; +import { purchaseController } from "../controllers/api/purchaseController.ts"; +import { questControlController } from "../controllers/api/questControlController.ts"; +import { queueDojoComponentDestructionController } from "../controllers/api/queueDojoComponentDestructionController.ts"; +import { redeemPromoCodeController } from "../controllers/api/redeemPromoCodeController.ts"; +import { releasePetController } from "../controllers/api/releasePetController.ts"; +import { removeFriendGetController, removeFriendPostController } from "../controllers/api/removeFriendController.ts"; +import { removeFromAllianceController } from "../controllers/api/removeFromAllianceController.ts"; +import { removeFromGuildController } from "../controllers/api/removeFromGuildController.ts"; +import { removeIgnoredUserController } from "../controllers/api/removeIgnoredUserController.ts"; +import { renamePetController } from "../controllers/api/renamePetController.ts"; +import { rerollRandomModController } from "../controllers/api/rerollRandomModController.ts"; +import { resetQuestProgressController } from "../controllers/api/resetQuestProgressController.ts"; +import { retrievePetFromStasisController } from "../controllers/api/retrievePetFromStasisController.ts"; +import { saveDialogueController } from "../controllers/api/saveDialogueController.ts"; +import { saveLoadoutController } from "../controllers/api/saveLoadoutController.ts"; +import { saveSettingsController } from "../controllers/api/saveSettingsController.ts"; +import { saveVaultAutoContributeController } from "../controllers/api/saveVaultAutoContributeController.ts"; +import { sellController } from "../controllers/api/sellController.ts"; +import { sendMsgToInBoxController } from "../controllers/api/sendMsgToInBoxController.ts"; +import { setActiveQuestController } from "../controllers/api/setActiveQuestController.ts"; +import { setActiveShipController } from "../controllers/api/setActiveShipController.ts"; +import { setAllianceGuildPermissionsController } from "../controllers/api/setAllianceGuildPermissionsController.ts"; +import { setBootLocationController } from "../controllers/api/setBootLocationController.ts"; +import { setDojoComponentColorsController } from "../controllers/api/setDojoComponentColorsController.ts"; +import { setDojoComponentMessageController } from "../controllers/api/setDojoComponentMessageController.ts"; +import { setDojoComponentSettingsController } from "../controllers/api/setDojoComponentSettingsController.ts"; +import { setEquippedInstrumentController } from "../controllers/api/setEquippedInstrumentController.ts"; +import { setFriendNoteController } from "../controllers/api/setFriendNoteController.ts"; +import { setGuildMotdController } from "../controllers/api/setGuildMotdController.ts"; +import { setHubNpcCustomizationsController } from "../controllers/api/setHubNpcCustomizationsController.ts"; +import { setPlacedDecoInfoController } from "../controllers/api/setPlacedDecoInfoController.ts"; +import { setShipCustomizationsController } from "../controllers/api/setShipCustomizationsController.ts"; +import { setShipFavouriteLoadoutController } from "../controllers/api/setShipFavouriteLoadoutController.ts"; +import { setShipVignetteController } from "../controllers/api/setShipVignetteController.ts"; +import { setSuitInfectionController } from "../controllers/api/setSuitInfectionController.ts"; +import { setSupportedSyndicateController } from "../controllers/api/setSupportedSyndicateController.ts"; +import { setWeaponSkillTreeController } from "../controllers/api/setWeaponSkillTreeController.ts"; +import { shipDecorationsController } from "../controllers/api/shipDecorationsController.ts"; +import { startCollectibleEntryController } from "../controllers/api/startCollectibleEntryController.ts"; +import { startDojoRecipeController } from "../controllers/api/startDojoRecipeController.ts"; +import { startLibraryDailyTaskController } from "../controllers/api/startLibraryDailyTaskController.ts"; +import { startLibraryPersonalTargetController } from "../controllers/api/startLibraryPersonalTargetController.ts"; +import { startRecipeController } from "../controllers/api/startRecipeController.ts"; +import { stepSequencersController } from "../controllers/api/stepSequencersController.ts"; +import { surveysController } from "../controllers/api/surveysController.ts"; +import { syndicateSacrificeController } from "../controllers/api/syndicateSacrificeController.ts"; +import { syndicateStandingBonusController } from "../controllers/api/syndicateStandingBonusController.ts"; +import { tauntHistoryController } from "../controllers/api/tauntHistoryController.ts"; +import { tradingController } from "../controllers/api/tradingController.ts"; +import { trainingResultController } from "../controllers/api/trainingResultController.ts"; +import { umbraController } from "../controllers/api/umbraController.ts"; +import { unlockShipFeatureController } from "../controllers/api/unlockShipFeatureController.ts"; +import { updateAlignmentController } from "../controllers/api/updateAlignmentController.ts"; +import { updateChallengeProgressController } from "../controllers/api/updateChallengeProgressController.ts"; +import { updateQuestController } from "../controllers/api/updateQuestController.ts"; +import { updateSessionGetController, updateSessionPostController } from "../controllers/api/updateSessionController.ts"; +import { updateSongChallengeController } from "../controllers/api/updateSongChallengeController.ts"; +import { updateThemeController } from "../controllers/api/updateThemeController.ts"; +import { upgradeOperatorController } from "../controllers/api/upgradeOperatorController.ts"; +import { upgradesController } from "../controllers/api/upgradesController.ts"; +import { valenceSwapController } from "../controllers/api/valenceSwapController.ts"; +import { wishlistController } from "../controllers/api/wishlistController.ts"; + +const apiRouter = express.Router(); + +// get +apiRouter.get("/abandonLibraryDailyTask.php", abandonLibraryDailyTaskController); +apiRouter.get("/abortDojoComponentDestruction.php", abortDojoComponentDestructionController); +apiRouter.get("/apartment.php", apartmentController); +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("/getGuildEventScore.php", getGuildEventScoreController); +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("/getPastWeeklyChallenges.php", getPastWeeklyChallengesController) +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); +apiRouter.get("/inbox.php", inboxController); +apiRouter.get("/inventory.php", inventoryController); +apiRouter.get("/loginRewards.php", loginRewardsController); +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("/resetQuestProgress.php", resetQuestProgressController); +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); +apiRouter.get("/upgradeOperator.php", upgradeOperatorController); + +// 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("/adoptPet.php", adoptPetController); +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("/claimJunctionChallengeReward.php", claimJunctionChallengeRewardController); +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("/crewShipFusion.php", crewShipFusionController); +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("/logout.php", logoutController); // from ~U16, don't know when they changed it to GET +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("/renamePet.php", renamePetController); +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("/setSuitInfection.php", setSuitInfectionController); +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); +apiRouter.post("/syndicateSacrifice.php", syndicateSacrificeController); +apiRouter.post("/syndicateStandingBonus.php", syndicateStandingBonusController); +apiRouter.post("/tauntHistory.php", tauntHistoryController); +apiRouter.post("/trainingResult.php", trainingResultController); +apiRouter.post("/umbra.php", umbraController); +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("/valenceSwap.php", valenceSwapController); +apiRouter.post("/wishlist.php", wishlistController); + +export { apiRouter }; diff --git a/src/routes/cache.ts b/src/routes/cache.ts new file mode 100644 index 00000000..7c450ac6 --- /dev/null +++ b/src/routes/cache.ts @@ -0,0 +1,31 @@ +import express from "express"; +import { buildConfig } from "../services/buildConfigService.ts"; +import fs from "fs/promises"; + +const cacheRouter = express.Router(); + +cacheRouter.get(/^\/origin\/[a-zA-Z0-9]+\/[0-9]+\/H\.Cache\.bin.*$/, (req, res) => { + if (typeof req.query.version == "string" && req.query.version.match(/^\d\d\d\d\.\d\d\.\d\d\.\d\d\.\d\d$/)) { + res.sendFile(`static/data/H.Cache_${req.query.version}.bin`, { root: "./" }); + } else { + res.sendFile(`static/data/H.Cache_${buildConfig.version}.bin`, { root: "./" }); + } +}); + +cacheRouter.get(/^\/0\/.+!.+$/, async (req, res) => { + try { + const dir = req.path.substr(0, req.path.lastIndexOf("/")); + const file = req.path.substr(dir.length + 1); + const filePath = `static/data${dir}/${file}`; + + // Return file if we have it + await fs.access(filePath); + const data = await fs.readFile(filePath, null); + res.send(data); + } catch (err) { + // 404 if we don't + res.status(404).end(); + } +}); + +export { cacheRouter }; diff --git a/src/routes/custom.ts b/src/routes/custom.ts new file mode 100644 index 00000000..8f272e33 --- /dev/null +++ b/src/routes/custom.ts @@ -0,0 +1,74 @@ +import express from "express"; + +import { tunablesController } from "../controllers/custom/tunablesController.ts"; +import { getItemListsController } from "../controllers/custom/getItemListsController.ts"; +import { pushArchonCrystalUpgradeController } from "../controllers/custom/pushArchonCrystalUpgradeController.ts"; +import { popArchonCrystalUpgradeController } from "../controllers/custom/popArchonCrystalUpgradeController.ts"; +import { deleteAccountController } from "../controllers/custom/deleteAccountController.ts"; +import { getNameController } from "../controllers/custom/getNameController.ts"; +import { getAccountInfoController } from "../controllers/custom/getAccountInfoController.ts"; +import { renameAccountController } from "../controllers/custom/renameAccountController.ts"; +import { ircDroppedController } from "../controllers/custom/ircDroppedController.ts"; +import { unlockAllIntrinsicsController } from "../controllers/custom/unlockAllIntrinsicsController.ts"; +import { addMissingMaxRankModsController } from "../controllers/custom/addMissingMaxRankModsController.ts"; +import { webuiFileChangeDetectedController } from "../controllers/custom/webuiFileChangeDetectedController.ts"; +import { completeAllMissionsController } from "../controllers/custom/completeAllMissionsController.ts"; +import { addMissingHelminthBlueprintsController } from "../controllers/custom/addMissingHelminthBlueprintsController.ts"; +import { unlockAllProfitTakerStagesController } from "../controllers/custom/unlockAllProfitTakerStagesController.ts"; +import { unlockAllSimarisResearchEntriesController } from "../controllers/custom/unlockAllSimarisResearchEntriesController.ts"; + +import { abilityOverrideController } from "../controllers/custom/abilityOverrideController.ts"; +import { createAccountController } from "../controllers/custom/createAccountController.ts"; +import { createMessageController } from "../controllers/custom/createMessageController.ts"; +import { addCurrencyController } from "../controllers/custom/addCurrencyController.ts"; +import { addItemsController } from "../controllers/custom/addItemsController.ts"; +import { addXpController } from "../controllers/custom/addXpController.ts"; +import { importController } from "../controllers/custom/importController.ts"; +import { manageQuestsController } from "../controllers/custom/manageQuestsController.ts"; +import { setEvolutionProgressController } from "../controllers/custom/setEvolutionProgressController.ts"; +import { setBoosterController } from "../controllers/custom/setBoosterController.ts"; +import { updateFingerprintController } from "../controllers/custom/updateFingerprintController.ts"; +import { changeModularPartsController } from "../controllers/custom/changeModularPartsController.ts"; +import { editSuitInvigorationUpgradeController } from "../controllers/custom/editSuitInvigorationUpgradeController.ts"; +import { setAccountCheatController } from "../controllers/custom/setAccountCheatController.ts"; + +import { getConfigController, setConfigController } from "../controllers/custom/configController.ts"; + +const customRouter = express.Router(); + +customRouter.get("/tunables.json", tunablesController); +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.get("/webuiFileChangeDetected", webuiFileChangeDetectedController); +customRouter.get("/completeAllMissions", completeAllMissionsController); +customRouter.get("/addMissingHelminthBlueprints", addMissingHelminthBlueprintsController); +customRouter.get("/unlockAllProfitTakerStages", unlockAllProfitTakerStagesController); +customRouter.get("/unlockAllSimarisResearchEntries", unlockAllSimarisResearchEntriesController); + +customRouter.post("/abilityOverride", abilityOverrideController); +customRouter.post("/createAccount", createAccountController); +customRouter.post("/createMessage", createMessageController); +customRouter.post("/addCurrency", addCurrencyController); +customRouter.post("/addItems", addItemsController); +customRouter.post("/addXp", addXpController); +customRouter.post("/import", importController); +customRouter.post("/manageQuests", manageQuestsController); +customRouter.post("/setEvolutionProgress", setEvolutionProgressController); +customRouter.post("/setBooster", setBoosterController); +customRouter.post("/updateFingerprint", updateFingerprintController); +customRouter.post("/changeModularParts", changeModularPartsController); +customRouter.post("/editSuitInvigorationUpgrade", editSuitInvigorationUpgradeController); +customRouter.post("/setAccountCheat", setAccountCheatController); + +customRouter.post("/getConfig", getConfigController); +customRouter.post("/setConfig", setConfigController); + +export { customRouter }; diff --git a/src/routes/dynamic.ts b/src/routes/dynamic.ts new file mode 100644 index 00000000..d9120f46 --- /dev/null +++ b/src/routes/dynamic.ts @@ -0,0 +1,14 @@ +import express from "express"; +import { aggregateSessionsController } from "../controllers/dynamic/aggregateSessionsController.ts"; +import { getGuildAdsController } from "../controllers/dynamic/getGuildAdsController.ts"; +import { getProfileViewingDataGetController } from "../controllers/dynamic/getProfileViewingDataController.ts"; +import { worldStateController } from "../controllers/dynamic/worldStateController.ts"; + +const dynamicController = express.Router(); + +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/pay.ts b/src/routes/pay.ts new file mode 100644 index 00000000..6d62fdf8 --- /dev/null +++ b/src/routes/pay.ts @@ -0,0 +1,11 @@ +import express from "express"; + +import { getSkuCatalogController } from "../controllers/pay/getSkuCatalogController.ts"; +import { steamPacksController } from "../controllers/pay/steamPacksController.ts"; + +const payRouter = express.Router(); + +payRouter.get("/getSkuCatalog.php", getSkuCatalogController); +payRouter.post("/steamPacks.php", steamPacksController); + +export { payRouter }; diff --git a/src/routes/stats.ts b/src/routes/stats.ts new file mode 100644 index 00000000..22d54d28 --- /dev/null +++ b/src/routes/stats.ts @@ -0,0 +1,13 @@ +import express from "express"; +import { viewController } from "../controllers/stats/viewController.ts"; +import { uploadController } from "../controllers/stats/uploadController.ts"; +import { leaderboardController } from "../controllers/stats/leaderboardController.ts"; + +const statsRouter = express.Router(); + +statsRouter.get("/view.php", viewController); +statsRouter.post("/upload.php", uploadController); +statsRouter.post("/leaderboardWeekly.php", leaderboardController); +statsRouter.post("/leaderboardArchived.php", leaderboardController); + +export { statsRouter }; diff --git a/src/routes/webui.ts b/src/routes/webui.ts new file mode 100644 index 00000000..b31b2746 --- /dev/null +++ b/src/routes/webui.ts @@ -0,0 +1,67 @@ +import express from "express"; +import path from "path"; +import { repoDir, rootDir } from "../helpers/pathHelper.ts"; +import { args } from "../helpers/commandLineArguments.ts"; + +const baseDir = args.dev ? repoDir : rootDir; + +const webuiRouter = express.Router(); + +// Redirect / to /webui/ +webuiRouter.get("/", (_req, res) => { + res.redirect("/webui/"); +}); + +// Redirect /webui to /webui/ +webuiRouter.use("/webui", (req, res, next) => { + if (req.originalUrl === "/") { + return res.redirect("/webui/"); + } + next(); +}); + +// Serve virtual routes +webuiRouter.get("/webui/inventory", (_req, res) => { + res.sendFile(path.join(baseDir, "static/webui/index.html")); +}); +webuiRouter.get("/webui/detailedView", (_req, res) => { + res.sendFile(path.join(baseDir, "static/webui/index.html")); +}); +webuiRouter.get("/webui/mods", (_req, res) => { + res.sendFile(path.join(baseDir, "static/webui/index.html")); +}); +webuiRouter.get("/webui/settings", (_req, res) => { + res.sendFile(path.join(baseDir, "static/webui/index.html")); +}); +webuiRouter.get("/webui/quests", (_req, res) => { + res.sendFile(path.join(baseDir, "static/webui/index.html")); +}); +webuiRouter.get("/webui/cheats", (_req, res) => { + res.sendFile(path.join(baseDir, "static/webui/index.html")); +}); +webuiRouter.get("/webui/import", (_req, res) => { + res.sendFile(path.join(baseDir, "static/webui/index.html")); +}); + +// Serve static files +webuiRouter.use("/webui", express.static(path.join(baseDir, "static/webui"))); + +// Serve favicon +webuiRouter.get("/favicon.ico", (_req, res) => { + res.sendFile(path.join(repoDir, "static/fixed_responses/favicon.ico")); +}); + +// Serve warframe-riven-info +webuiRouter.get("/webui/riven-tool/", (_req, res) => { + res.sendFile(path.join(repoDir, "node_modules/warframe-riven-info/index.html")); +}); +webuiRouter.get("/webui/riven-tool/RivenParser.js", (_req, res) => { + res.sendFile(path.join(repoDir, "node_modules/warframe-riven-info/RivenParser.js")); +}); + +// Serve translations +webuiRouter.get("/translations/:file", (req, res) => { + res.sendFile(path.join(baseDir, `static/webui/translations/${req.params.file}`)); +}); + +export { webuiRouter }; diff --git a/src/services/buildConfigService.ts b/src/services/buildConfigService.ts new file mode 100644 index 00000000..81c76cc4 --- /dev/null +++ b/src/services/buildConfigService.ts @@ -0,0 +1,20 @@ +import path from "path"; +import fs from "fs"; +import { repoDir } from "../helpers/pathHelper.ts"; + +interface IBuildConfig { + version: string; + buildLabel: string; + matchmakingBuildId: string; +} + +export const buildConfig: IBuildConfig = { + version: "", + buildLabel: "", + matchmakingBuildId: "" +}; + +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 new file mode 100644 index 00000000..c79bc808 --- /dev/null +++ b/src/services/configService.ts @@ -0,0 +1,187 @@ +import fs from "fs"; +import path from "path"; +import { repoDir } from "../helpers/pathHelper.ts"; +import { args } from "../helpers/commandLineArguments.ts"; +import { Inbox } from "../models/inboxModel.ts"; +import type { Request } from "express"; + +export interface IConfig { + mongodbUrl: string; + logger: { + files: boolean; + level: string; // "fatal" | "error" | "warn" | "info" | "http" | "debug" | "trace"; + }; + myAddress: string; + httpPort?: number; + httpsPort?: number; + myIrcAddresses?: string[]; + administratorNames?: string[]; + autoCreateAccount?: boolean; + skipTutorial?: boolean; + unlockAllScans?: boolean; + unlockAllShipFeatures?: boolean; + unlockAllShipDecorations?: boolean; + unlockAllFlavourItems?: boolean; + unlockAllSkins?: boolean; + unlockAllCapturaScenes?: boolean; + unlockAllDecoRecipes?: boolean; + fullyStockedVendors?: boolean; + skipClanKeyCrafting?: boolean; + noDojoRoomBuildStage?: boolean; + noDojoDecoBuildStage?: boolean; + fastDojoRoomDestruction?: boolean; + noDojoResearchCosts?: boolean; + noDojoResearchTime?: boolean; + fastClanAscension?: boolean; + spoofMasteryRank?: number; + relicRewardItemCountMultiplier?: number; + nightwaveStandingMultiplier?: number; + unfaithfulBugFixes?: { + ignore1999LastRegionPlayed?: boolean; + fixXtraCheeseTimer?: boolean; + useAnniversaryTagForOldGoals?: boolean; + }; + worldState?: { + creditBoost?: boolean; + affinityBoost?: boolean; + resourceBoost?: boolean; + tennoLiveRelay?: boolean; + baroTennoConRelay?: boolean; + baroAlwaysAvailable?: boolean; + baroFullyStocked?: boolean; + varziaFullyStocked?: boolean; + wolfHunt?: boolean; + orphixVenom?: boolean; + longShadow?: boolean; + hallowedFlame?: boolean; + anniversary?: number; + hallowedNightmares?: boolean; + hallowedNightmaresRewardsOverride?: number; + proxyRebellion?: boolean; + proxyRebellionRewardsOverride?: number; + galleonOfGhouls?: number; + ghoulEmergenceOverride?: boolean; + plagueStarOverride?: boolean; + starDaysOverride?: boolean; + dogDaysOverride?: boolean; + dogDaysRewardsOverride?: number; + bellyOfTheBeast?: boolean; + bellyOfTheBeastProgressOverride?: number; + eightClaw?: boolean; + eightClawProgressOverride?: number; + thermiaFracturesOverride?: boolean; + thermiaFracturesProgressOverride?: number; + eidolonOverride?: string; + vallisOverride?: string; + duviriOverride?: string; + nightwaveOverride?: string; + allTheFissures?: string; + varziaOverride?: string; + circuitGameModes?: string[]; + darvoStockMultiplier?: number; + }; + dev?: { + keepVendorsExpired?: boolean; + }; +} + +export const configRemovedOptionsKeys = [ + "NRS", + "skipAllDialogue", + "infiniteCredits", + "infinitePlatinum", + "infiniteEndo", + "infiniteRegalAya", + "infiniteHelminthMaterials", + "claimingBlueprintRefundsIngredients", + "dontSubtractPurchaseCreditCost", + "dontSubtractPurchasePlatinumCost", + "dontSubtractPurchaseItemCost", + "dontSubtractPurchaseStandingCost", + "dontSubtractVoidTraces", + "dontSubtractConsumables", + "universalPolarityEverywhere", + "unlockDoubleCapacityPotatoesEverywhere", + "unlockExilusEverywhere", + "unlockArcanesEverywhere", + "unlockAllProfitTakerStages", + "unlockAllSimarisResearchEntries", + "noDailyStandingLimits", + "noDailyFocusLimit", + "noArgonCrystalDecay", + "noMasteryRankUpCooldown", + "noVendorPurchaseLimits", + "noDecoBuildStage", + "noDeathMarks", + "noKimCooldowns", + "syndicateMissionsRepeatable", + "instantFinishRivenChallenge", + "instantResourceExtractorDrones", + "noResourceExtractorDronesDamage", + "baroAlwaysAvailable", + "baroFullyStocked", + "missionsCanGiveAllRelics", + "exceptionalRelicsAlwaysGiveBronzeReward", + "flawlessRelicsAlwaysGiveSilverReward", + "radiantRelicsAlwaysGiveGoldReward", + "disableDailyTribute" +]; + +export const configPath = path.join(repoDir, args.configPath ?? "config.json"); + +export const config: IConfig = { + mongodbUrl: "mongodb://127.0.0.1:27017/openWF", + logger: { + files: true, + level: "trace" + }, + myAddress: "localhost" +}; + +export const loadConfig = (): void => { + const newConfig = JSON.parse(fs.readFileSync(configPath, "utf-8")) as IConfig; + + // 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-unsafe-member-access + (config as any)[key] = undefined; + } + + Object.assign(config, newConfig); +}; + +export const syncConfigWithDatabase = (): void => { + // Event messages are deleted after endDate. Since we don't use beginDate/endDate and instead have config toggles, we need to delete the messages once those bools are false. + // Also, for some reason, I can't just do `Inbox.deleteMany(...)`; - it needs this whole circus. + if (!config.worldState?.creditBoost) { + void Inbox.deleteMany({ globaUpgradeId: "5b23106f283a555109666672" }).then(() => {}); + } + if (!config.worldState?.affinityBoost) { + void Inbox.deleteMany({ globaUpgradeId: "5b23106f283a555109666673" }).then(() => {}); + } + if (!config.worldState?.resourceBoost) { + void Inbox.deleteMany({ globaUpgradeId: "5b23106f283a555109666674" }).then(() => {}); + } + if (!config.worldState?.galleonOfGhouls) { + void Inbox.deleteMany({ goalTag: "GalleonRobbery" }).then(() => {}); + } +}; + +export const getReflexiveAddress = (request: Request): { myAddress: string; myUrlBase: string } => { + 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; + } + } + return { myAddress, myUrlBase }; +}; diff --git a/src/services/configWatcherService.ts b/src/services/configWatcherService.ts new file mode 100644 index 00000000..d0f72981 --- /dev/null +++ b/src/services/configWatcherService.ts @@ -0,0 +1,85 @@ +import chokidar from "chokidar"; +import { logger } from "../utils/logger.ts"; +import { + config, + configPath, + configRemovedOptionsKeys, + loadConfig, + syncConfigWithDatabase, + type IConfig +} from "./configService.ts"; +import { saveConfig, shouldReloadConfig } from "./configWriterService.ts"; +import { getWebPorts, startWebServer, stopWebServer } from "./webService.ts"; +import { sendWsBroadcast } from "./wsService.ts"; +import varzia from "../../static/fixed_responses/worldState/varzia.json" with { type: "json" }; + +chokidar.watch(configPath).on("change", () => { + if (shouldReloadConfig()) { + logger.info("Detected a change to config file, reloading its contents."); + try { + loadConfig(); + } catch (e) { + logger.error("Config changes were not applied: " + (e as Error).message); + return; + } + validateConfig(); + syncConfigWithDatabase(); + + const webPorts = getWebPorts(); + if (config.httpPort != webPorts.http || config.httpsPort != webPorts.https) { + logger.info(`Restarting web server to apply port changes.`); + + // Tell webui clients to reload with new port + sendWsBroadcast({ ports: { http: config.httpPort, https: config.httpsPort } }); + + void stopWebServer().then(startWebServer); + } else { + sendWsBroadcast({ config_reloaded: true }); + } + } +}); + +export const validateConfig = (): void => { + let modified = false; + for (const key of configRemovedOptionsKeys) { + if (config[key as keyof IConfig] !== undefined) { + logger.debug( + `Spotted removed option ${key} with value ${String(config[key as keyof IConfig])} in config.json.` + ); + delete config[key as keyof IConfig]; + modified = true; + } + } + if (config.administratorNames) { + if (!Array.isArray(config.administratorNames)) { + config.administratorNames = [config.administratorNames]; + modified = true; + } + for (let i = 0; i != config.administratorNames.length; ++i) { + if (typeof config.administratorNames[i] != "string") { + config.administratorNames[i] = String(config.administratorNames[i]); + modified = true; + } + } + } + if ( + config.worldState?.galleonOfGhouls && + config.worldState.galleonOfGhouls != 1 && + config.worldState.galleonOfGhouls != 2 && + config.worldState.galleonOfGhouls != 3 + ) { + config.worldState.galleonOfGhouls = 0; + modified = true; + } + if ( + config.worldState?.varziaOverride && + !varzia.primeDualPacks.some(p => p.ItemType === config.worldState?.varziaOverride) + ) { + config.worldState.varziaOverride = ""; + modified = true; + } + if (modified) { + logger.info(`Updating config file to fix some issues with it.`); + void saveConfig(); + } +}; diff --git a/src/services/configWriterService.ts b/src/services/configWriterService.ts new file mode 100644 index 00000000..60ffdf5e --- /dev/null +++ b/src/services/configWriterService.ts @@ -0,0 +1,17 @@ +import fsPromises from "fs/promises"; +import { config, configPath } from "./configService.ts"; + +let amnesia = false; + +export const saveConfig = async (): Promise => { + amnesia = true; + await fsPromises.writeFile(configPath, JSON.stringify(config, null, 2)); +}; + +export const shouldReloadConfig = (): boolean => { + if (amnesia) { + amnesia = false; + return false; + } + return true; +}; diff --git a/src/services/friendService.ts b/src/services/friendService.ts new file mode 100644 index 00000000..445e4321 --- /dev/null +++ b/src/services/friendService.ts @@ -0,0 +1,47 @@ +import type { IFriendInfo } from "../types/friendTypes.ts"; +import { getInventory } from "./inventoryService.ts"; +import { config } from "./configService.ts"; +import { Account } from "../models/loginModel.ts"; +import type { Types } from "mongoose"; +import { Friendship } from "../models/friendModel.ts"; +import { fromOid, toMongoDate } from "../helpers/inventoryHelpers.ts"; + +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 new file mode 100644 index 00000000..630f675a --- /dev/null +++ b/src/services/guildService.ts @@ -0,0 +1,918 @@ +import type { Request } from "express"; +import type { TAccountDocument } from "./loginService.ts"; +import { getAccountIdForRequest } from "./loginService.ts"; +import { addLevelKeys, addRecipes, combineInventoryChanges, getInventory } from "./inventoryService.ts"; +import type { TGuildDatabaseDocument } from "../models/guildModel.ts"; +import { Alliance, AllianceMember, Guild, GuildAd, GuildMember } from "../models/guildModel.ts"; +import type { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel.ts"; +import type { + IAllianceClient, + IAllianceDatabase, + IAllianceMemberClient, + IDojoClient, + IDojoComponentClient, + IDojoComponentDatabase, + IDojoContributable, + IDojoDecoClient, + IDojoDecoDatabase, + IGuildClient, + IGuildMemberClient, + IGuildMemberDatabase, + IGuildVault, + ITechProjectDatabase +} from "../types/guildTypes.ts"; +import { GuildPermission } from "../types/guildTypes.ts"; +import { toMongoDate, toOid, toOid2 } from "../helpers/inventoryHelpers.ts"; +import type { Types } from "mongoose"; +import type { IDojoBuild, IDojoResearch } from "warframe-public-export-plus"; +import { ExportDojoRecipes, ExportResources } from "warframe-public-export-plus"; +import { logger } from "../utils/logger.ts"; +import { config } from "./configService.ts"; +import { getRandomInt } from "./rngService.ts"; +import { Inbox } from "../models/inboxModel.ts"; +import type { IFusionTreasure } from "../types/inventoryTypes/inventoryTypes.ts"; +import type { IInventoryChanges } from "../types/purchaseTypes.ts"; +import { parallelForeach } from "../utils/async-utils.ts"; +import allDecoRecipes from "../../static/fixed_responses/allDecoRecipes.json" with { type: "json" }; +import { createMessage } from "./inboxService.ts"; +import { addAccountDataToFriendInfo, addInventoryDataToFriendInfo } from "./friendService.ts"; +import type { ITypeCount } from "../types/commonTypes.ts"; + +export const getGuildForRequest = async (req: Request): Promise => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + return await getGuildForRequestEx(req, inventory); +}; + +export const getGuildForRequestEx = async ( + req: Request, + inventory: TInventoryDatabaseDocument +): Promise => { + const guildId = req.query.guildId as string; + 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.findById(guildId); + if (!guild) { + throw new Error("Account thinks it is in a guild that doesn't exist"); + } + return guild; +}; + +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, + 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, + GoalProgress: guild.GoalProgress + ? guild.GoalProgress.map(gp => ({ + Count: gp.Count, + Tag: gp.Tag, + _id: { $oid: gp.goalId.toString() } + })) + : 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: [] + }; + 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 (roomsToRemove.length) { + logger.debug(`removing now-destroyed rooms`, roomsToRemove); + for (const id of roomsToRemove) { + await removeDojoRoom(guild, id); + } + 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; +}; + +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 => refundDojoDeco(guild, component, 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]; + refundDojoDeco(guild, component, deco); +}; + +export const refundDojoDeco = ( + guild: TGuildDatabaseDocument, + component: IDojoComponentDatabase, + deco: IDojoDecoDatabase +): void => { + const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type); + if (meta) { + if (meta.capacityCost) { + component.DecoCapacity! += meta.capacityCost; + } + } else { + const [itemType, meta] = Object.entries(ExportResources).find(arr => arr[1].deco == deco.Type)!; + component.DecoCapacity! += meta.dojoCapacityCost!; + if (deco.Sockets !== undefined) { + addVaultFusionTreasures(guild, [ + { + ItemType: itemType, + ItemCount: 1, + Sockets: deco.Sockets + } + ]); + } else { + addVaultShipDecos(guild, [ + { + ItemType: itemType, + ItemCount: 1 + } + ]); + } + } + moveResourcesToVault(guild, deco); // Refund resources spent on construction +}; + +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); + if (config.noDojoResearchTime) { + processCompletedGuildTechProject(guild, techProject.ItemType); + } +}; + +export const processCompletedGuildTechProject = (guild: TGuildDatabaseDocument, type: string): void => { + if (type.startsWith("/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/")) { + guild.VaultDecoRecipes ??= []; + guild.VaultDecoRecipes.push({ + ItemType: type, + ItemCount: 1 + }); + } +}; + +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 + } + }; +}; + +export const handleGuildGoalProgress = async ( + guild: TGuildDatabaseDocument, + upload: { Count: number; Tag: string; goalId: Types.ObjectId } +): Promise => { + guild.GoalProgress ??= []; + const goalProgress = guild.GoalProgress.find(x => x.goalId.equals(upload.goalId)); + if (!goalProgress) { + guild.GoalProgress.push({ + Count: upload.Count, + Tag: upload.Tag, + goalId: upload.goalId + }); + } + const totalCount = (goalProgress?.Count ?? 0) + upload.Count; + const guildRewards = goalGuildRewardByTag[upload.Tag].rewards; + const tierGoals = goalGuildRewardByTag[upload.Tag].guildGoals[guild.Tier - 1]; + const rewards = []; + if (tierGoals.length && guildRewards.length) { + for (let i = 0; i < tierGoals.length; i++) { + if ( + tierGoals[i] && + tierGoals[i] <= totalCount && + (!goalProgress || goalProgress.Count < tierGoals[i]) && + guildRewards[i] + ) { + rewards.push(guildRewards[i]); + } + } + + if (rewards.length) { + logger.debug(`guild goal rewards`, rewards); + guild.VaultDecoRecipes ??= []; + rewards.forEach(type => { + guild.VaultDecoRecipes!.push({ + ItemType: type, + ItemCount: 1 + }); + }); + } + } + + if (goalProgress) { + goalProgress.Count += upload.Count; + } + await guild.save(); +}; + +export const goalGuildRewardByTag: Record = { + JadeShadowsEvent: { + guildGoals: [ + // I don't know what ClanGoal means + [15, 30, 45, 60], + [45, 90, 135, 180], + [150, 300, 450, 600], + [450, 900, 1350, 1800], + [1500, 3000, 4500, 6000] + ], + rewards: [ + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventPewterTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventGoldTrophyRecipe" + ] + }, + DuviriMurmurEvent: { + guildGoals: [ + // I don't know what ClanGoal means + [260, 519, 779, 1038], + [779, 1557, 2336, 3114], + [2595, 5190, 7785, 10380], + [7785, 15570, 23355, 31140], + [29950, 51900, 77850, 103800] + ], + rewards: [ + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DuviriMurmurEventClayTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DuviriMurmurEventBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DuviriMurmurEventSilverTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DuviriMurmurEventGoldTrophyRecipe" + ] + }, + MechSurvival: { + guildGoals: [ + [1390, 5860, 13920, 18850], + [3510, 22275, 69120, 137250], + [11700, 75250, 230400, 457500], + [35100, 222750, 691200, 1372500], + [117000, 742500, 2304000, 4575000] + ], + rewards: [ + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MechEventTrophyTerracottaRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MechEventTrophyBronzeRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MechEventTrophySilverRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/MechEventTrophyGoldRecipe" + ] + } +}; diff --git a/src/services/importService.ts b/src/services/importService.ts new file mode 100644 index 00000000..3783607f --- /dev/null +++ b/src/services/importService.ts @@ -0,0 +1,576 @@ +import { Types } from "mongoose"; +import type { + IItemConfig, + IOperatorConfigClient, + IOperatorConfigDatabase +} from "../types/inventoryTypes/commonInventoryTypes.ts"; +import type { IMongoDate } from "../types/commonTypes.ts"; +import type { + IDialogueClient, + IDialogueDatabase, + IDialogueHistoryClient, + IDialogueHistoryDatabase, + IInfestedFoundryClient, + IInfestedFoundryDatabase, + IInventoryClient, + INemesisClient, + INemesisDatabase, + IPendingRecipeClient, + IPendingRecipeDatabase, + IQuestKeyClient, + IQuestKeyDatabase, + ISlots, + IUpgradeClient, + IUpgradeDatabase, + IWeaponSkinClient, + IWeaponSkinDatabase +} from "../types/inventoryTypes/inventoryTypes.ts"; +import { equipmentKeys } from "../types/inventoryTypes/inventoryTypes.ts"; +import type { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel.ts"; +import type { + ILoadoutConfigClient, + ILoadoutConfigDatabase, + ILoadoutDatabase, + ILoadOutPresets +} from "../types/saveLoadoutTypes.ts"; +import { slotNames } from "../types/purchaseTypes.ts"; +import type { + ICrewShipMemberClient, + ICrewShipMemberDatabase, + ICrewShipMembersClient, + ICrewShipMembersDatabase, + ICrewShipWeaponClient, + ICrewShipWeaponDatabase, + ICrewShipWeaponEmplacementsClient, + ICrewShipWeaponEmplacementsDatabase, + IEquipmentClient, + IEquipmentDatabase, + IEquipmentSelectionClient, + IEquipmentSelectionDatabase, + IKubrowPetDetailsClient, + IKubrowPetDetailsDatabase +} from "../types/equipmentTypes.ts"; +import type { + IApartmentClient, + IApartmentDatabase, + ICustomizationInfoClient, + ICustomizationInfoDatabase, + IFavouriteLoadout, + IFavouriteLoadoutDatabase, + IGetShipResponse, + IOrbiterClient, + IOrbiterDatabase, + IPersonalRoomsDatabase, + IPlacedDecosClient, + IPlacedDecosDatabase, + IPlantClient, + IPlantDatabase, + IPlanterClient, + IPlanterDatabase, + IRoomClient, + IRoomDatabase, + ITailorShop, + ITailorShopDatabase +} from "../types/personalRoomsTypes.ts"; + +const convertDate = (value: IMongoDate): Date => { + return new Date(parseInt(value.$date.$numberLong)); +}; + +const convertOptionalDate = (value: IMongoDate | undefined): Date | undefined => { + return value ? convertDate(value) : undefined; +}; + +const convertEquipment = (client: IEquipmentClient): IEquipmentDatabase => { + const { ItemId, ...rest } = client; + return { + ...rest, + _id: new Types.ObjectId(ItemId.$oid), + InfestationDate: convertOptionalDate(client.InfestationDate), + Expiry: convertOptionalDate(client.Expiry), + UpgradesExpiry: convertOptionalDate(client.UpgradesExpiry), + UmbraDate: convertOptionalDate(client.UmbraDate), + Weapon: client.Weapon ? importCrewShipWeapon(client.Weapon) : undefined, + CrewMembers: client.CrewMembers ? importCrewShipMembers(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( + Object.entries(obj).filter(([_, value]) => !Array.isArray(value) || value.length > 0) + ) + ) + : [] + }; +}; + +const convertWeaponSkin = (client: IWeaponSkinClient): IWeaponSkinDatabase => { + const { ItemId, ...rest } = client; + return { + ...rest, + _id: new Types.ObjectId(ItemId.$oid) + }; +}; + +const convertUpgrade = (client: IUpgradeClient): IUpgradeDatabase => { + const { ItemId, ...rest } = client; + return { + ...rest, + _id: new Types.ObjectId(ItemId.$oid) + }; +}; + +const convertOperatorConfig = (client: IOperatorConfigClient): IOperatorConfigDatabase => { + const { ItemId, ...rest } = client; + return { + ...rest, + _id: new Types.ObjectId(ItemId.$oid) + }; +}; + +const replaceArray = (arr: T[], replacement: T[]): void => { + arr.splice(0, arr.length); + replacement.forEach(x => { + arr.push(x); + }); +}; + +const replaceSlots = (db: ISlots, client: ISlots): void => { + db.Extra = client.Extra; + db.Slots = client.Slots; +}; + +const convertEquipmentSelection = (es: IEquipmentSelectionClient): IEquipmentSelectionDatabase => { + const { ItemId, ...rest } = es; + return { + ...rest, + ItemId: ItemId ? new Types.ObjectId(ItemId.$oid) : undefined + }; +}; + +const convertCrewShipWeaponEmplacements = ( + obj: ICrewShipWeaponEmplacementsClient +): ICrewShipWeaponEmplacementsDatabase => { + return { + PRIMARY_A: obj.PRIMARY_A ? convertEquipmentSelection(obj.PRIMARY_A) : undefined, + PRIMARY_B: obj.PRIMARY_B ? convertEquipmentSelection(obj.PRIMARY_B) : undefined, + SECONDARY_A: obj.SECONDARY_A ? convertEquipmentSelection(obj.SECONDARY_A) : undefined, + SECONDARY_B: obj.SECONDARY_B ? convertEquipmentSelection(obj.SECONDARY_B) : undefined + }; +}; + +export const importCrewShipWeapon = (weapon: ICrewShipWeaponClient): ICrewShipWeaponDatabase => { + return { + PILOT: weapon.PILOT ? convertCrewShipWeaponEmplacements(weapon.PILOT) : undefined, + PORT_GUNS: weapon.PORT_GUNS ? convertCrewShipWeaponEmplacements(weapon.PORT_GUNS) : undefined, + STARBOARD_GUNS: weapon.STARBOARD_GUNS ? convertCrewShipWeaponEmplacements(weapon.STARBOARD_GUNS) : undefined, + ARTILLERY: weapon.ARTILLERY ? convertCrewShipWeaponEmplacements(weapon.ARTILLERY) : undefined, + SCANNER: weapon.SCANNER ? convertCrewShipWeaponEmplacements(weapon.SCANNER) : undefined + }; +}; + +const importCrewMemberId = (crewMemberId: ICrewShipMemberClient): ICrewShipMemberDatabase => { + if (crewMemberId.ItemId) { + return { ItemId: new Types.ObjectId(crewMemberId.ItemId.$oid) }; + } + return { NemesisFingerprint: BigInt(crewMemberId.NemesisFingerprint ?? 0) }; +}; + +export const importCrewShipMembers = (client: ICrewShipMembersClient): ICrewShipMembersDatabase => { + return { + 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 + }; +}; + +const convertInfestedFoundry = (client: IInfestedFoundryClient): IInfestedFoundryDatabase => { + return { + ...client, + LastConsumedSuit: client.LastConsumedSuit ? convertEquipment(client.LastConsumedSuit) : undefined, + AbilityOverrideUnlockCooldown: convertOptionalDate(client.AbilityOverrideUnlockCooldown) + }; +}; + +const convertDialogue = (client: IDialogueClient): IDialogueDatabase => { + return { + ...client, + AvailableDate: convertDate(client.AvailableDate), + AvailableGiftDate: convertDate(client.AvailableGiftDate), + RankUpExpiry: convertDate(client.RankUpExpiry), + BountyChemExpiry: convertDate(client.BountyChemExpiry) + }; +}; + +const convertDialogueHistory = (client: IDialogueHistoryClient): IDialogueHistoryDatabase => { + return { + YearIteration: client.YearIteration, + Dialogues: client.Dialogues ? client.Dialogues.map(convertDialogue) : undefined + }; +}; + +const convertKubrowDetails = (client: IKubrowPetDetailsClient): IKubrowPetDetailsDatabase => { + return { + ...client, + HatchDate: convertDate(client.HatchDate) + }; +}; + +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) { + replaceArray(db[key], client[key].map(convertEquipment)); + } + } + if (client.WeaponSkins !== undefined) { + replaceArray(db.WeaponSkins, client.WeaponSkins.map(convertWeaponSkin)); + } + 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", + "Recipes", + "LevelKeys", + "EmailItems", + "ShipDecorations", + "CrewShipAmmo", + "CrewShipRawSalvage" + ] as const) { + if (client[key] !== undefined) { + db[key].splice(0, db[key].length); + client[key].forEach(x => { + db[key].push({ + ItemType: x.ItemType, + ItemCount: x.ItemCount + }); + }); + } + } + for (const key of ["AdultOperatorLoadOuts", "OperatorLoadOuts", "KahlLoadOuts"] as const) { + if (client[key] !== undefined) { + replaceArray(db[key], client[key].map(convertOperatorConfig)); + } + } + for (const key of slotNames) { + if (client[key] !== undefined) { + replaceSlots(db[key], client[key]); + } + } + // 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", + "TradesRemaining", + "GiftsRemaining", + "ChallengesFixVersion", + "Founder", + "Guide" + ] as const) { + if (client[key] !== undefined) { + db[key] = client[key]; + } + } + // string + for (const key of [ + "ThemeStyle", + "ThemeBackground", + "ThemeSounds", + "EquippedInstrument", + "FocusAbility", + "ActiveQuest", + "SupportedSyndicate", + "ActiveAvatarImageType" + ] as const) { + if (client[key] !== undefined) { + db[key] = client[key]; + } + } + // string[] + for (const key of [ + "EquippedGear", + "EquippedEmotes", + "NodeIntrosCompleted", + "DeathMarks", + "Wishlist", + "NemesisAbandonedRewards" + ] as const) { + if (client[key] !== undefined) { + db[key] = client[key]; + } + } + // IRewardAtten[] + for (const key of ["SortieRewardAttenuation", "SpecialItemRewardAttenuation"] as const) { + if (client[key] !== undefined) { + db[key] = client[key]; + } + } + if (client.XPInfo !== undefined) { + db.XPInfo = client.XPInfo; + } + if (client.CurrentLoadOutIds !== undefined) { + db.CurrentLoadOutIds = client.CurrentLoadOutIds; + } + if (client.Affiliations !== undefined) { + db.Affiliations = client.Affiliations; + } + if (client.FusionTreasures !== undefined) { + db.FusionTreasures = client.FusionTreasures; + } + if (client.FocusUpgrades !== undefined) { + db.FocusUpgrades = client.FocusUpgrades; + } + if (client.EvolutionProgress !== undefined) { + db.EvolutionProgress = client.EvolutionProgress; + } + if (client.InfestedFoundry !== undefined) { + db.InfestedFoundry = convertInfestedFoundry(client.InfestedFoundry); + } + 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; + } +}; + +export const importLoadOutConfig = (client: ILoadoutConfigClient): ILoadoutConfigDatabase => { + const { ItemId, ...rest } = client; + return { + ...rest, + _id: new Types.ObjectId(ItemId.$oid), + s: client.s ? convertEquipmentSelection(client.s) : undefined, + p: client.p ? convertEquipmentSelection(client.p) : undefined, + l: client.l ? convertEquipmentSelection(client.l) : undefined, + m: client.m ? convertEquipmentSelection(client.m) : undefined, + h: client.h ? convertEquipmentSelection(client.h) : undefined, + a: client.a ? convertEquipmentSelection(client.a) : undefined + }; +}; + +export const importLoadOutPresets = (db: ILoadoutDatabase, client: ILoadOutPresets): void => { + db.NORMAL = client.NORMAL.map(importLoadOutConfig); + db.SENTINEL = client.SENTINEL.map(importLoadOutConfig); + db.ARCHWING = client.ARCHWING.map(importLoadOutConfig); + db.NORMAL_PVP = client.NORMAL_PVP.map(importLoadOutConfig); + db.LUNARO = client.LUNARO.map(importLoadOutConfig); + db.OPERATOR = client.OPERATOR.map(importLoadOutConfig); + db.GEAR = client.GEAR?.map(importLoadOutConfig); + db.KDRIVE = client.KDRIVE.map(importLoadOutConfig); + db.DATAKNIFE = client.DATAKNIFE.map(importLoadOutConfig); + db.MECH = client.MECH.map(importLoadOutConfig); + db.OPERATOR_ADULT = client.OPERATOR_ADULT.map(importLoadOutConfig); + db.DRIFTER = client.DRIFTER.map(importLoadOutConfig); +}; + +export const convertCustomizationInfo = (client: ICustomizationInfoClient): ICustomizationInfoDatabase => { + return { + ...client, + LoadOutPreset: client.LoadOutPreset ? importLoadOutConfig(client.LoadOutPreset) : undefined, + VehiclePreset: client.VehiclePreset ? importLoadOutConfig(client.VehiclePreset) : undefined + }; +}; + +const convertDeco = (client: IPlacedDecosClient): IPlacedDecosDatabase => { + const { id, ...rest } = client; + return { + ...rest, + CustomizationInfo: client.CustomizationInfo ? convertCustomizationInfo(client.CustomizationInfo) : undefined, + _id: new Types.ObjectId(id.$oid) + }; +}; + +const convertRoom = (client: IRoomClient): IRoomDatabase => { + return { + ...client, + PlacedDecos: client.PlacedDecos ? client.PlacedDecos.map(convertDeco) : [] + }; +}; + +const convertShip = (client: IOrbiterClient): IOrbiterDatabase => { + return { + ...client, + ShipInterior: { + ...client.ShipInterior, + Colors: typeof client.ShipInterior == "object" ? client.ShipInterior.Colors : undefined + }, + Rooms: client.Rooms.map(convertRoom), + FavouriteLoadoutId: client.FavouriteLoadoutId ? new Types.ObjectId(client.FavouriteLoadoutId.$oid) : undefined + }; +}; + +const convertPlant = (client: IPlantClient): IPlantDatabase => { + return { + ...client, + EndTime: convertDate(client.EndTime) + }; +}; + +const convertPlanter = (client: IPlanterClient): IPlanterDatabase => { + return { + ...client, + Plants: client.Plants.map(convertPlant) + }; +}; + +const convertFavouriteLoadout = (client: IFavouriteLoadout): IFavouriteLoadoutDatabase => { + return { + ...client, + LoadoutId: new Types.ObjectId(client.LoadoutId.$oid) + }; +}; + +const convertApartment = (client: IApartmentClient): IApartmentDatabase => { + return { + ...client, + Rooms: client.Rooms.map(convertRoom), + Gardening: { Planters: client.Gardening.Planters.map(convertPlanter) }, + FavouriteLoadouts: client.FavouriteLoadouts ? client.FavouriteLoadouts.map(convertFavouriteLoadout) : [] + }; +}; + +const convertTailorShop = (client: ITailorShop): ITailorShopDatabase => { + return { + ...client, + Rooms: client.Rooms.map(convertRoom), + Colors: Array.isArray(client.Colors) ? {} : client.Colors, + FavouriteLoadouts: client.FavouriteLoadouts ? client.FavouriteLoadouts.map(convertFavouriteLoadout) : [] + }; +}; + +export const importPersonalRooms = (db: IPersonalRoomsDatabase, client: Partial): void => { + if (client.Ship?.Rooms) db.Ship = convertShip(client.Ship); + if (client.Apartment !== undefined) db.Apartment = convertApartment(client.Apartment); + if (client.TailorShop !== undefined) db.TailorShop = convertTailorShop(client.TailorShop); +}; diff --git a/src/services/inboxService.ts b/src/services/inboxService.ts new file mode 100644 index 00000000..38053c0d --- /dev/null +++ b/src/services/inboxService.ts @@ -0,0 +1,159 @@ +import type { IMessageDatabase } from "../models/inboxModel.ts"; +import { Inbox } from "../models/inboxModel.ts"; +import { getAccountForRequest } from "./loginService.ts"; +import type { HydratedDocument } from "mongoose"; +import { Types } from "mongoose"; +import type { Request } from "express"; +import { unixTimesInMs } from "../constants/timeConstants.ts"; +import { config } from "./configService.ts"; + +export const getAllMessagesSorted = async (accountId: string): Promise[]> => { + const inbox = await Inbox.find({ ownerId: accountId }).sort({ date: -1 }); + return inbox; +}; + +export const getMessage = async (messageId: string): Promise> => { + const message = await Inbox.findById(messageId); + + if (!message) { + throw new Error(`Message not found ${messageId}`); + } + return message; +}; + +export const deleteMessageRead = async (messageId: string): Promise => { + await Inbox.findOneAndDelete({ _id: messageId, r: true }); +}; + +export const deleteAllMessagesRead = async (accountId: string): Promise => { + await Inbox.deleteMany({ ownerId: accountId, r: true }); +}; + +export const createNewEventMessages = async (req: Request): Promise => { + const account = await getAccountForRequest(req); + const newEventMessages: IMessageCreationTemplate[] = []; + + // Baro + const baroIndex = Math.trunc((Date.now() - 910800000) / (unixTimesInMs.day * 14)); + const baroStart = baroIndex * (unixTimesInMs.day * 14) + 910800000; + const baroActualStart = baroStart + unixTimesInMs.day * (config.worldState?.baroAlwaysAvailable ? 0 : 12); + if (Date.now() >= baroActualStart && account.LatestEventMessageDate.getTime() < baroActualStart) { + newEventMessages.push({ + sndr: "/Lotus/Language/G1Quests/VoidTraderName", + sub: "/Lotus/Language/CommunityMessages/VoidTraderAppearanceTitle", + msg: "/Lotus/Language/CommunityMessages/VoidTraderAppearanceMessage", + icon: "/Lotus/Interface/Icons/Npcs/BaroKiTeerPortrait.png", + startDate: new Date(baroActualStart), + endDate: new Date(baroStart + unixTimesInMs.day * 14), + CrossPlatform: true, + arg: [ + { + Key: "NODE_NAME", + Tag: ["EarthHUB", "MercuryHUB", "SaturnHUB", "PlutoHUB"][baroIndex % 4] + } + ], + date: new Date(baroActualStart) + }); + } + + // BUG: Deleting the inbox message manually means it'll just be automatically re-created. This is because we don't use startDate/endDate for these config-toggled events. + const promises = []; + if (config.worldState?.creditBoost) { + promises.push( + (async (): Promise => { + if (!(await Inbox.exists({ ownerId: account._id, globaUpgradeId: "5b23106f283a555109666672" }))) { + newEventMessages.push({ + globaUpgradeId: new Types.ObjectId("5b23106f283a555109666672"), + sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender", + sub: "/Lotus/Language/Items/EventDoubleCreditsName", + msg: "/Lotus/Language/Items/EventDoubleCreditsDesc", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + startDate: new Date(), + CrossPlatform: true + }); + } + })() + ); + } + if (config.worldState?.affinityBoost) { + promises.push( + (async (): Promise => { + if (!(await Inbox.exists({ ownerId: account._id, globaUpgradeId: "5b23106f283a555109666673" }))) { + newEventMessages.push({ + globaUpgradeId: new Types.ObjectId("5b23106f283a555109666673"), + sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender", + sub: "/Lotus/Language/Items/EventDoubleAffinityName", + msg: "/Lotus/Language/Items/EventDoubleAffinityDesc", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + startDate: new Date(), + CrossPlatform: true + }); + } + })() + ); + } + if (config.worldState?.resourceBoost) { + promises.push( + (async (): Promise => { + if (!(await Inbox.exists({ ownerId: account._id, globaUpgradeId: "5b23106f283a555109666674" }))) { + newEventMessages.push({ + globaUpgradeId: new Types.ObjectId("5b23106f283a555109666674"), + sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender", + sub: "/Lotus/Language/Items/EventDoubleResourceName", + msg: "/Lotus/Language/Items/EventDoubleResourceDesc", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + startDate: new Date(), + CrossPlatform: true + }); + } + })() + ); + } + if (config.worldState?.galleonOfGhouls) { + promises.push( + (async (): Promise => { + if (!(await Inbox.exists({ ownerId: account._id, goalTag: "GalleonRobbery" }))) { + newEventMessages.push({ + sndr: "/Lotus/Language/Bosses/BossCouncilorVayHek", + sub: "/Lotus/Language/Events/GalleonRobberyIntroMsgTitle", + msg: "/Lotus/Language/Events/GalleonRobberyIntroMsgDesc", + icon: "/Lotus/Interface/Icons/Npcs/VayHekPortrait.png", + transmission: "/Lotus/Sounds/Dialog/GalleonOfGhouls/DGhoulsWeekOneInbox0010VayHek", + att: ["/Lotus/Upgrades/Skins/Events/OgrisOldSchool"], + startDate: new Date(), + goalTag: "GalleonRobbery" + }); + } + })() + ); + } + await Promise.all(promises); + + if (newEventMessages.length === 0) { + return; + } + + await createMessage(account._id, newEventMessages); + + const latestEventMessage = newEventMessages.reduce((prev, current) => + prev.startDate! > current.startDate! ? prev : current + ); + account.LatestEventMessageDate = new Date(latestEventMessage.startDate!); + await account.save(); +}; + +export const createMessage = async ( + accountId: string | Types.ObjectId, + messages: IMessageCreationTemplate[] +): Promise => { + const ownerIdMessages = messages.map(m => ({ + ...m, + date: m.date ?? new Date(), + ownerId: accountId + })); + await Inbox.insertMany(ownerIdMessages); +}; + +export interface IMessageCreationTemplate extends Omit { + date?: Date; +} diff --git a/src/services/infestedFoundryService.ts b/src/services/infestedFoundryService.ts new file mode 100644 index 00000000..08afc3b8 --- /dev/null +++ b/src/services/infestedFoundryService.ts @@ -0,0 +1,114 @@ +import { ExportRecipes } from "warframe-public-export-plus"; +import type { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel.ts"; +import type { + IAccountCheats, + IInfestedFoundryClient, + IInfestedFoundryDatabase +} from "../types/inventoryTypes/inventoryTypes.ts"; +import { addRecipes } from "./inventoryService.ts"; +import type { ITypeCount } from "../types/commonTypes.ts"; + +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 = (cheats: IAccountCheats, infestedFoundry: IInfestedFoundryClient): void => { + if (cheats.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 new file mode 100644 index 00000000..143d7010 --- /dev/null +++ b/src/services/inventoryService.ts @@ -0,0 +1,2515 @@ +import type { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel.ts"; +import { Inventory } from "../models/inventoryModels/inventoryModel.ts"; +import { config } from "./configService.ts"; +import { Types } from "mongoose"; +import type { SlotNames, IInventoryChanges, IBinChanges, IAffiliationMods } from "../types/purchaseTypes.ts"; +import { slotNames } from "../types/purchaseTypes.ts"; +import type { + IChallengeProgress, + IMiscItem, + IMission, + IRawUpgrade, + ISeasonChallenge, + IWeaponSkinClient, + TEquipmentKey, + IFusionTreasure, + IDailyAffiliations, + IKubrowPetEggDatabase, + IKubrowPetEggClient, + ILibraryDailyTaskInfo, + IDroneClient, + IUpgradeClient, + TPartialStartingGear, + ILoreFragmentScan, + ICrewMemberClient, + ICalendarProgress, + INemesisWeaponTargetFingerprint, + INemesisPetTargetFingerprint, + IDialogueDatabase, + IKubrowPetPrintClient +} from "../types/inventoryTypes/inventoryTypes.ts"; +import { InventorySlot, equipmentKeys } from "../types/inventoryTypes/inventoryTypes.ts"; +import type { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate.ts"; +import type { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes.ts"; +import { logger } from "../utils/logger.ts"; +import { convertInboxMessage, fromStoreItem, getKeyChainItems } from "./itemDataService.ts"; +import type { IFlavourItem, IItemConfig } from "../types/inventoryTypes/commonInventoryTypes.ts"; +import type { IDefaultUpgrade, IPowersuit, ISentinel, TStandingLimitBin } from "warframe-public-export-plus"; +import { + ExportArcanes, + ExportBoosters, + ExportBundles, + ExportChallenges, + ExportCustoms, + ExportDrones, + ExportEmailItems, + ExportEnemies, + ExportFlavour, + ExportFusionBundles, + ExportGear, + ExportKeys, + ExportMisc, + ExportRailjackWeapons, + ExportRecipes, + ExportResources, + ExportSentinels, + ExportSyndicates, + ExportUpgrades, + ExportWarframes, + ExportWeapons +} from "warframe-public-export-plus"; +import { createShip } from "./shipService.ts"; +import type { TTraitsPool } from "../helpers/inventoryHelpers.ts"; +import { + catbrowDetails, + fromDbOid, + fromMongoDate, + fromOid, + kubrowDetails, + kubrowFurPatternsWeights, + kubrowWeights, + toOid +} from "../helpers/inventoryHelpers.ts"; +import { addQuestKey, completeQuest } from "./questService.ts"; +import { handleBundleAcqusition } from "./purchaseService.ts"; +import libraryDailyTasks from "../../static/fixed_responses/libraryDailyTasks.json" with { type: "json" }; +import { generateRewardSeed, getRandomElement, getRandomInt, getRandomWeightedReward, SRng } from "./rngService.ts"; +import type { IMessageCreationTemplate } from "./inboxService.ts"; +import { createMessage } from "./inboxService.ts"; +import { getMaxStanding, getMinStanding } from "../helpers/syndicateStandingHelper.ts"; +import { getNightwaveSyndicateTag, getWorldState } from "./worldStateService.ts"; +import type { ICalendarSeason } from "../types/worldStateTypes.ts"; +import type { INemesisProfile } from "../helpers/nemesisHelpers.ts"; +import { generateNemesisProfile } from "../helpers/nemesisHelpers.ts"; +import type { TAccountDocument } from "./loginService.ts"; +import { unixTimesInMs } from "../constants/timeConstants.ts"; +import { addString } from "../helpers/stringHelpers.ts"; +import type { + IEquipmentClient, + IEquipmentDatabase, + IKubrowPetDetailsDatabase, + ITraits +} from "../types/equipmentTypes.ts"; +import { EquipmentFeatures, Status } from "../types/equipmentTypes.ts"; +import type { ITypeCount } from "../types/commonTypes.ts"; + +export const createInventory = async ( + accountOwnerId: Types.ObjectId, + defaultItemReferences: { loadOutPresetId: Types.ObjectId; ship: Types.ObjectId } +): Promise => { + try { + const inventory = new Inventory({ + accountOwnerId: accountOwnerId, + LoadOutPresets: defaultItemReferences.loadOutPresetId, + 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) { + inventory.PlayedParkourTutorial = true; + await addStartingGear(inventory); + await completeQuest(inventory, "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain"); + + const completedMissions = ["SolNode27", "SolNode89", "SolNode63", "SolNode85", "SolNode15", "SolNode79"]; + + inventory.Missions.push( + ...completedMissions.map(tag => ({ + Completes: 1, + Tag: tag + })) + ); + } + + await inventory.save(); + } catch (error) { + throw new Error(`Error creating inventory: ${error instanceof Error ? error.message : "Unknown error type"}`); + } +}; + +//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[] = delta[key]; + for (const item of right) { + left.push(item); + } + } 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; + left.Extra += right.Extra; + } + } else if (typeof delta[key] === "number") { + (InventoryChanges[key] as number) += delta[key]; + } else { + throw new Error(`inventory change not merged: unhandled type for inventory key ${key}`); + } + } +}; + +export const getInventory = async ( + accountOwnerId: string, + projection?: string +): Promise => { + const inventory = await Inventory.findOne({ accountOwnerId: accountOwnerId }, projection); + + if (!inventory) { + throw new Error(`Didn't find an inventory for ${accountOwnerId}`); + } + + 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, + 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 = [ + { + ItemType: typeName, + ItemCount: quantity + } satisfies ITypeCount + ]; + addRecipes(inventory, recipeChanges); + return { + Recipes: recipeChanges + }; + } + if (typeName in ExportResources) { + if (ExportResources[typeName].productCategory == "MiscItems") { + const miscItemChanges = [ + { + ItemType: typeName, + ItemCount: quantity + } satisfies IMiscItem + ]; + addMiscItems(inventory, miscItemChanges); + return { + 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 { + Ships: [ + { + ItemId: { $oid: oid.toString() }, + ItemType: typeName + } + ] + }; + } else if (ExportResources[typeName].productCategory == "CrewShips") { + 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") + : {}) + }; + } else if (ExportResources[typeName].productCategory == "ShipDecorations") { + const changes = [ + { + ItemType: typeName, + ItemCount: quantity + } satisfies IMiscItem + ]; + addShipDecorations(inventory, changes); + return { + 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", + _id: new Types.ObjectId() + }; + inventory.KubrowPetEggs.push(egg); + changes.push({ + ItemType: egg.ItemType, + ExpirationDate: { $date: { $numberLong: "2000000000000" } }, + ItemId: toOid(egg._id) // TODO: Pass on buildLabel from purchaseService + }); + } + return { + KubrowPetEggs: changes + }; + } else { + throw new Error(`unknown product category: ${ExportResources[typeName].productCategory}`); + } + } + if (typeName in ExportCustoms) { + 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) { + return addCustomization(inventory, typeName); + } + if (typeName in ExportUpgrades || typeName in ExportArcanes) { + if (targetFingerprint) { + if (quantity != 1) { + logger.warn(`adding 1 of ${typeName} ${targetFingerprint} even tho quantity ${quantity} was requested`); + } + const upgrade = + inventory.Upgrades[ + inventory.Upgrades.push({ + ItemType: typeName, + UpgradeFingerprint: targetFingerprint + }) - 1 + ]; + return { Upgrades: [upgrade.toJSON()] }; + } + const changes = [ + { + ItemType: typeName, + ItemCount: quantity + } + ]; + addMods(inventory, changes); + return { + 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; + logger.debug(`non-exact acquisition of ${typeName}; factored quantity is ${quantity}`); + } + const consumablesChanges = [ + { + ItemType: typeName, + ItemCount: quantity + } satisfies ITypeCount + ]; + addConsumables(inventory, consumablesChanges); + return { + Consumables: consumablesChanges + }; + } + if (typeName in ExportWeapons) { + const weapon = ExportWeapons[typeName]; + if (weapon.totalDamage != 0) { + 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 + const miscItemChanges = [ + { + ItemType: typeName, + ItemCount: quantity + } satisfies IMiscItem + ]; + addMiscItems(inventory, miscItemChanges); + return { + MiscItems: miscItemChanges + }; + } + } + 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 { + RegularCredits: creditsTotal + }; + } + if (typeName in ExportFusionBundles) { + const fusionPointsTotal = ExportFusionBundles[typeName].fusionPoints * quantity; + addFusionPoints(inventory, fusionPointsTotal); + return { + FusionPoints: fusionPointsTotal + }; + } + if (typeName in ExportKeys) { + // 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); + } + + // Boosters are an odd case. They're only added like this via Baro's Void Surplus afaik. + { + const boosterEntry = Object.entries(ExportBoosters).find(arr => arr[1].typeName == typeName); + if (boosterEntry) { + addBooster(typeName, quantity, inventory); + return { + Boosters: [{ ItemType: typeName, ExpiryDate: quantity }] + }; + } + } + + // Path-based duck typing + switch (typeName.substr(1).split("/")[1]) { + case "Powersuits": + if (typeName.endsWith("AugmentCard")) break; + switch (typeName.substr(1).split("/")[2]) { + default: { + return { + ...(await addPowerSuit(inventory, typeName, { + Features: premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined + })), + ...occupySlot(inventory, InventorySlot.SUITS, premiumPurchase) + }; + } + case "Archwing": { + inventory.ArchwingEnabled = true; + return { + ...addSpaceSuit( + inventory, + typeName, + {}, + premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined + ), + ...occupySlot(inventory, InventorySlot.SPACESUITS, premiumPurchase) + }; + } + case "EntratiMech": { + return { + ...(await addMechSuit( + inventory, + typeName, + {}, + premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined + )), + ...occupySlot(inventory, InventorySlot.MECHSUITS, premiumPurchase) + }; + } + } + break; + case "Upgrades": { + switch (typeName.substr(1).split("/")[2]) { + case "Mods": // Legendary Core + case "CosmeticEnhancers": // Traumatic Peculiar + { + const changes = [ + { + ItemType: typeName, + ItemCount: quantity + } + ]; + addMods(inventory, changes); + return { + RawUpgrades: changes + }; + } + break; + + case "Boons": + // Can purchase /Lotus/Upgrades/Boons/DuviriVendorBoonItem from Acrithis, doesn't need to be added to inventory. + return {}; + + case "Stickers": + { + const entry = inventory.RawUpgrades.find(x => x.ItemType == typeName); + 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; + + case "Skins": { + return addSkin(inventory, typeName); + } + } + break; + } + case "Types": + switch (typeName.substr(1).split("/")[2]) { + case "Sentinels": { + return addSentinel(inventory, typeName, premiumPurchase); + } + case "Game": { + if (typeName.substr(1).split("/")[3] == "Projections") { + // Void Relics, e.g. /Lotus/Types/Game/Projections/T2VoidProjectionGaussPrimeDBronze + const miscItemChanges = [ + { + ItemType: typeName, + ItemCount: quantity + } satisfies IMiscItem + ]; + addMiscItems(inventory, miscItemChanges); + inventory.HasOwnedVoidProjectionsPreviously = true; + return { + MiscItems: miscItemChanges + }; + } else if ( + typeName.substr(1).split("/")[3] == "CatbrowPet" || + typeName.substr(1).split("/")[3] == "KubrowPet" + ) { + if ( + typeName != "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem" && + typeName != "/Lotus/Types/Game/KubrowPet/BlankTraitPrint" && + typeName != "/Lotus/Types/Game/KubrowPet/ImprintedTraitPrint" + ) { + 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 |= BigInt(Math.trunc(inventory.Created.getTime() / 1000) & 0xffffff) << 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 "Items": { + if (typeName.substr(1).split("/")[3] == "Emotes") { + return addCustomization(inventory, typeName); + } + break; + } + case "NeutralCreatures": { + if (inventory.Horses.length != 0) { + logger.warn("refusing to add Horse because account already has one"); + return {}; + } + const horseIndex = inventory.Horses.push({ ItemType: typeName }); + return { + Horses: [inventory.Horses[horseIndex - 1].toJSON()] + }; + } + case "Vehicles": + if (typeName == "/Lotus/Types/Vehicles/Motorcycle/MotorcyclePowerSuit") { + return addMotorcycle(inventory, typeName); + } + break; + case "Lore": + if (typeName == "/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentRewards") { + const fragmentType = getRandomElement([ + "/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentA", + "/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentB", + "/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentC", + "/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentD", + "/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentE", + "/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentF", + "/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentG", + "/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentH", + "/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentI", + "/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentJ", + "/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentK", + "/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentL", + "/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentM" + ])!; + addLoreFragmentScans(inventory, [ + { + Progress: 1, + Region: "", + ItemType: fragmentType + } + ]); + } + break; + } + break; + case "Weapons": { + if (typeName.substr(1).split("/")[4] == "MeleeTrees") break; + const productCategory = typeName.substr(1).split("/")[3]; + switch (productCategory) { + case "Pistols": + case "LongGuns": + case "Melee": { + const inventoryChanges = addEquipment(inventory, productCategory, typeName); + return { + ...inventoryChanges, + ...occupySlot( + inventory, + productCategoryToInventoryBin(productCategory) ?? InventorySlot.WEAPONS, + premiumPurchase + ) + }; + } + } + break; + } + } + throw new Error(`unable to add item: ${typeName}`); +}; + +export const addItems = async ( + inventory: TInventoryDatabaseDocument, + items: ITypeCount[] | string[], + inventoryChanges: IInventoryChanges = {} +): Promise => { + let inventoryDelta; + for (const item of items) { + if (typeof item === "string") { + inventoryDelta = await addItem(inventory, item, 1, true); + } else { + inventoryDelta = await addItem(inventory, item.ItemType, item.ItemCount, true); + } + combineInventoryChanges(inventoryChanges, inventoryDelta); + } + return inventoryChanges; +}; + +export const applyDefaultUpgrades = ( + inventory: TInventoryDatabaseDocument, + defaultUpgrades: IDefaultUpgrade[] | undefined +): IItemConfig[] => { + const modsToGive: IRawUpgrade[] = []; + const configs: IItemConfig[] = []; + if (defaultUpgrades) { + const upgrades = []; + 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; + } + } + if (upgrades.length != 0) { + configs.push({ Upgrades: upgrades }); + } + } + addMods(inventory, modsToGive); + 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.push(inventory.Sentinels[sentinelIndex].toJSON()); + + return inventoryChanges; +}; + +const addSentinelWeapon = ( + inventory: TInventoryDatabaseDocument, + typeName: string, + premiumPurchase: boolean, + inventoryChanges: IInventoryChanges +): void => { + // Sentinel weapons also occupy a slot in the sentinels bin + combineInventoryChanges(inventoryChanges, occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase)); + + const index = inventory.SentinelWeapons.push({ ItemType: typeName, XP: 0 }) - 1; + inventoryChanges.SentinelWeapons ??= []; + inventoryChanges.SentinelWeapons.push(inventory.SentinelWeapons[index].toJSON()); +}; + +export const addPowerSuit = async ( + inventory: TInventoryDatabaseDocument, + powersuitName: string, + defaultOverwrites?: Partial, + inventoryChanges: IInventoryChanges = {} +): 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 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.push(inventory.Suits[suitIndex].toJSON()); + return inventoryChanges; +}; + +export const addMechSuit = async ( + inventory: TInventoryDatabaseDocument, + mechsuitName: string, + 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, + Features: features, + IsNew: inventory.MechSuits.find(x => x.ItemType == mechsuitName) ? undefined : true + }) - 1; + inventoryChanges.MechSuits ??= []; + inventoryChanges.MechSuits.push(inventory.MechSuits[suitIndex].toJSON()); + return inventoryChanges; +}; + +export const addSpecialItem = ( + inventory: TInventoryDatabaseDocument, + itemName: string, + inventoryChanges: IInventoryChanges +): void => { + if (inventory.SpecialItems.find(x => x.ItemType == itemName)) { + return; + } + const specialItemIndex = + inventory.SpecialItems.push({ + ItemType: itemName, + Configs: [], + Features: 1, + UpgradeVer: 101, + XP: 0 + }) - 1; + inventoryChanges.SpecialItems ??= []; + 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; +}; + +const createRandomTraits = (kubrowPetName: string, traitsPool: TTraitsPool): ITraits => { + return { + 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: traitsPool.Heads.length ? getRandomWeightedReward(traitsPool.Heads, kubrowWeights)!.type : undefined, + Tail: traitsPool.Tails.length ? getRandomWeightedReward(traitsPool.Tails, kubrowWeights)!.type : undefined + }; +}; + +export const addKubrowPet = ( + inventory: TInventoryDatabaseDocument, + kubrowPetName: string, + details?: IKubrowPetDetailsDatabase, + premiumPurchase: boolean = false, + inventoryChanges: IInventoryChanges = {} +): IInventoryChanges => { + combineInventoryChanges(inventoryChanges, occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase)); + + // TODO: When incubating, this should only be given when claiming the recipe. + const kubrowPet = ExportSentinels[kubrowPetName] as ISentinel | undefined; + const exalted = kubrowPet?.exalted ?? []; + for (const specialItem of exalted) { + addSpecialItem(inventory, specialItem, inventoryChanges); + } + + const configs: IItemConfig[] = applyDefaultUpgrades(inventory, kubrowPet?.defaultUpgrades); + + if (!details) { + const isCatbrow = [ + "/Lotus/Types/Game/CatbrowPet/CheshireCatbrowPetPowerSuit", + "/Lotus/Types/Game/CatbrowPet/MirrorCatbrowPetPowerSuit", + "/Lotus/Types/Game/CatbrowPet/VampireCatbrowPetPowerSuit" + ].includes(kubrowPetName); + + const traitsPool = isCatbrow ? catbrowDetails : kubrowDetails; + let dominantTraits: ITraits; + if (kubrowPetName == "/Lotus/Types/Game/CatbrowPet/VampireCatbrowPetPowerSuit") { + dominantTraits = { + 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 { + dominantTraits = createRandomTraits(kubrowPetName, traitsPool); + if (kubrowPetName == "/Lotus/Types/Game/KubrowPet/ChargerKubrowPetPowerSuit") { + dominantTraits.BodyType = "/Lotus/Types/Game/KubrowPet/BodyTypes/ChargerKubrowPetBodyType"; + dominantTraits.FurPattern = "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternInfested"; + } + } + + const recessiveTraits: ITraits = createRandomTraits( + getRandomElement( + isCatbrow + ? [ + "/Lotus/Types/Game/CatbrowPet/MirrorCatbrowPetPowerSuit", + "/Lotus/Types/Game/CatbrowPet/CheshireCatbrowPetPowerSuit" + ] + : [ + "/Lotus/Types/Game/KubrowPet/AdventurerKubrowPetPowerSuit", + "/Lotus/Types/Game/KubrowPet/FurtiveKubrowPetPowerSuit", + "/Lotus/Types/Game/KubrowPet/GuardKubrowPetPowerSuit", + "/Lotus/Types/Game/KubrowPet/HunterKubrowPetPowerSuit", + "/Lotus/Types/Game/KubrowPet/RetrieverKubrowPetPowerSuit" + ] + )!, + traitsPool + ); + for (const key of Object.keys(recessiveTraits) as (keyof ITraits)[]) { + // My heurstic approximation is a 20% chance for a dominant trait to be copied into the recessive traits. TODO: A more scientific statistical analysis maybe? + if (Math.random() < 0.2) { + recessiveTraits[key] = dominantTraits[key]!; + } + } + + details = { + Name: "", + IsPuppy: !premiumPurchase, + HasCollar: true, + PrintsRemaining: isCatbrow ? 3 : 2, + Status: premiumPurchase ? Status.StatusStasis : Status.StatusIncubating, + HatchDate: premiumPurchase ? new Date() : new Date(Date.now() + 10 * unixTimesInMs.hour), // On live, this seems to be somewhat randomised so that the pet hatches 9~11 hours after start. + IsMale: !!getRandomInt(0, 1), + Size: getRandomInt(70, 100) / 100, + DominantTraits: dominantTraits, + RecessiveTraits: recessiveTraits + }; + } + + 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; +}; + +export const addKubrowPetPrint = ( + inventory: TInventoryDatabaseDocument, + pet: IEquipmentDatabase, + inventoryChanges: IInventoryChanges +): void => { + inventoryChanges.KubrowPetPrints ??= []; + inventoryChanges.KubrowPetPrints.push( + inventory.KubrowPetPrints[ + inventory.KubrowPetPrints.push({ + ItemType: "/Lotus/Types/Game/KubrowPet/ImprintedTraitPrint", + Name: pet.Details!.Name, + IsMale: pet.Details!.IsMale, + Size: pet.Details!.Size, + DominantTraits: pet.Details!.DominantTraits, + RecessiveTraits: pet.Details!.RecessiveTraits + }) - 1 + ].toJSON() + ); +}; + +export const updateSlots = ( + inventory: TInventoryDatabaseDocument, + slotName: SlotNames, + slotAmount: number, + extraAmount: number +): void => { + inventory[slotName].Slots += slotAmount; + if (extraAmount != 0) { + inventory[slotName].Extra ??= 0; + inventory[slotName].Extra += extraAmount; + } +}; + +const isCurrencyTracked = (inventory: TInventoryDatabaseDocument, usePremium: boolean): boolean => { + return usePremium ? !inventory.infinitePlatinum : !inventory.infiniteCredits; +}; + +export const updateCurrency = ( + inventory: TInventoryDatabaseDocument, + price: number, + usePremium: boolean, + inventoryChanges: IInventoryChanges = {}, + validateBalance: boolean = true +): IInventoryChanges => { + if (price != 0 && isCurrencyTracked(inventory, usePremium)) { + if (usePremium) { + // 验证白金余额是否足够 + if (validateBalance) { + const totalPlatinum = inventory.PremiumCredits + inventory.PremiumCreditsFree; + if (totalPlatinum < price) { + throw new Error(`Insufficient platinum balance. Required: ${price}, Available: ${totalPlatinum}`); + } + } + + if (inventory.PremiumCreditsFree > 0) { + const premiumCreditsFreeDelta = Math.min(price, inventory.PremiumCreditsFree) * -1; + inventoryChanges.PremiumCreditsFree ??= 0; + inventoryChanges.PremiumCreditsFree += premiumCreditsFreeDelta; + inventory.PremiumCreditsFree += premiumCreditsFreeDelta; + } + inventoryChanges.PremiumCredits ??= 0; + inventoryChanges.PremiumCredits -= price; + inventory.PremiumCredits -= price; + logger.debug(`currency changes `, { PremiumCredits: -price }); + } else { + // 验证现金余额是否足够 + if (validateBalance && inventory.RegularCredits < price) { + throw new Error(`Insufficient credits balance. Required: ${price}, Available: ${inventory.RegularCredits}`); + } + + inventoryChanges.RegularCredits ??= 0; + inventoryChanges.RegularCredits -= price; + inventory.RegularCredits -= price; + logger.debug(`currency changes `, { RegularCredits: -price }); + } + } + return inventoryChanges; +}; + +export const addFusionPoints = (inventory: TInventoryDatabaseDocument, add: number): number => { + if (inventory.FusionPoints + add > 2147483647) { + logger.warn(`capping FusionPoints balance at 2147483647`); + add = 2147483647 - inventory.FusionPoints; + } + inventory.FusionPoints += add; + return add; +}; + +export const addCrewShipFusionPoints = (inventory: TInventoryDatabaseDocument, add: number): number => { + if (inventory.CrewShipFusionPoints + add > 2147483647) { + logger.warn(`capping CrewShipFusionPoints balance at 2147483647`); + add = 2147483647 - inventory.CrewShipFusionPoints; + } + inventory.CrewShipFusionPoints += add; + return add; +}; + +const standingLimitBinToInventoryKey: Record< + Exclude, + keyof IDailyAffiliations +> = { + STANDING_LIMIT_BIN_NORMAL: "DailyAffiliation", + STANDING_LIMIT_BIN_PVP: "DailyAffiliationPvp", + STANDING_LIMIT_BIN_LIBRARY: "DailyAffiliationLibrary", + STANDING_LIMIT_BIN_CETUS: "DailyAffiliationCetus", + STANDING_LIMIT_BIN_QUILLS: "DailyAffiliationQuills", + STANDING_LIMIT_BIN_SOLARIS: "DailyAffiliationSolaris", + STANDING_LIMIT_BIN_VENTKIDS: "DailyAffiliationVentkids", + STANDING_LIMIT_BIN_VOX: "DailyAffiliationVox", + STANDING_LIMIT_BIN_ENTRATI: "DailyAffiliationEntrati", + STANDING_LIMIT_BIN_NECRALOID: "DailyAffiliationNecraloid", + STANDING_LIMIT_BIN_ZARIMAN: "DailyAffiliationZariman", + STANDING_LIMIT_BIN_KAHL: "DailyAffiliationKahl", + STANDING_LIMIT_BIN_CAVIA: "DailyAffiliationCavia", + STANDING_LIMIT_BIN_HEX: "DailyAffiliationHex" +}; + +export const allDailyAffiliationKeys: (keyof IDailyAffiliations)[] = Object.values(standingLimitBinToInventoryKey); + +const getStandingLimit = (inventory: TInventoryDatabaseDocument, bin: TStandingLimitBin): number => { + if (bin == "STANDING_LIMIT_BIN_NONE" || inventory.noDailyStandingLimits) { + return Number.MAX_SAFE_INTEGER; + } + return inventory[standingLimitBinToInventoryKey[bin]]; +}; + +const updateStandingLimit = ( + inventory: TInventoryDatabaseDocument, + bin: TStandingLimitBin, + subtrahend: number +): void => { + if (bin != "STANDING_LIMIT_BIN_NONE" && !inventory.noDailyStandingLimits) { + inventory[standingLimitBinToInventoryKey[bin]] -= subtrahend; + } +}; + +export const addStanding = ( + inventory: TInventoryDatabaseDocument, + syndicateTag: string, + gainedStanding: number, + affiliationMods: IAffiliationMods[] = [], + isMedallion: boolean = false, + propagateAlignments: boolean = true +): void => { + 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 (syndicate.Standing + gainedStanding < -71000) { + gainedStanding = -71000 - 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; + const affiliationMod: IAffiliationMods = { + Tag: syndicateTag, + Standing: gainedStanding + }; + affiliationMods.push(affiliationMod); + + if (syndicateMeta.alignments) { + if (propagateAlignments) { + for (const [tag, factor] of Object.entries(syndicateMeta.alignments)) { + addStanding(inventory, tag, gainedStanding * factor, affiliationMods, isMedallion, false); + } + } else { + while (syndicate.Standing < getMinStanding(syndicateMeta, syndicate.Title ?? 0)) { + syndicate.Title ??= 0; + syndicate.Title -= 1; + affiliationMod.Title ??= 0; + affiliationMod.Title -= 1; + logger.debug(`${syndicateTag} is decreasing to title ${syndicate.Title} after applying alignment`); + } + } + } +}; + +// TODO: AffiliationMods support (Nightwave). +export const updateGeneric = async (data: IGenericUpdate, accountId: string): Promise => { + const inventory = await getInventory(accountId, "NodeIntrosCompleted MiscItems ShipDecorations"); + + // 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 == "TC2025") { + inventoryChanges.ShipDecorations = [ + { + ItemType: "/Lotus/Types/Items/ShipDecos/TauGrineerLancerBobbleHead", + ItemCount: 1 + } + ]; + addShipDecorations(inventory, inventoryChanges.ShipDecorations); + } else 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); + + // Remove duplicate entries. + const nodes = [...new Set(data.NodeIntrosCompleted)]; + + inventory.NodeIntrosCompleted = nodes; + await inventory.save(); + + return { + MissionRewards: [], + InventoryChanges: inventoryChanges + }; +}; + +export const addEquipment = ( + inventory: TInventoryDatabaseDocument, + category: TEquipmentKey, + type: string, + defaultOverwrites?: Partial, + inventoryChanges: IInventoryChanges = {} +): IInventoryChanges => { + const equipment: Omit = Object.assign( + { + ItemType: type, + Configs: [], + XP: 0, + 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].push(inventory[category][index].toJSON()); + return inventoryChanges; +}; + +export const addCustomization = ( + inventory: TInventoryDatabaseDocument, + customizationName: string, + inventoryChanges: IInventoryChanges = {} +): IInventoryChanges => { + if (!inventory.FlavourItems.some(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; +}; + +export const addSkin = ( + inventory: TInventoryDatabaseDocument, + typeName: string, + inventoryChanges: IInventoryChanges = {} +): IInventoryChanges => { + if (inventory.WeaponSkins.some(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: typeName.startsWith("/Lotus/Upgrades/Skins/RailJack/") ? undefined : true // railjack skins are incompatible with this flag + }) - 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; +}; + +const addCrewShip = ( + inventory: TInventoryDatabaseDocument, + typeName: string, + inventoryChanges: IInventoryChanges = {} +): IInventoryChanges => { + 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; +}; + +const addCrewShipHarness = ( + inventory: TInventoryDatabaseDocument, + typeName: string, + inventoryChanges: IInventoryChanges = {} +): IInventoryChanges => { + 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; +}; + +const addMotorcycle = ( + inventory: TInventoryDatabaseDocument, + 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) { + const msg: IMessageCreationTemplate = convertInboxMessage(meta.message); + if (msg.cinematic == "/Lotus/Levels/1999/PlayerHomeBalconyCinematics.level") { + msg.customData = JSON.stringify({ + Tag: msg.customData + "KissCin", + CinLoadout: { + Skins: inventory.AdultOperatorLoadOuts[0].Skins, + Upgrades: inventory.AdultOperatorLoadOuts[0].Upgrades, + attcol: inventory.AdultOperatorLoadOuts[0].attcol, + cloth: inventory.AdultOperatorLoadOuts[0].cloth, + eyecol: inventory.AdultOperatorLoadOuts[0].eyecol, + pricol: inventory.AdultOperatorLoadOuts[0].pricol, + syancol: inventory.AdultOperatorLoadOuts[0].syancol + } + }); + } + await createMessage(inventory.accountOwnerId, [msg]); + + if (emailItem) { + emailItem.ItemCount += 1; + } else { + inventory.EmailItems.push({ ItemType: typeName, ItemCount: 1 }); + } + + inventoryChanges.EmailItems ??= []; + inventoryChanges.EmailItems.push({ ItemType: typeName, ItemCount: 1 }); + } + return inventoryChanges; +}; + +const xpEarningParts: readonly string[] = [ + "LWPT_BLADE", + "LWPT_GUN_BARREL", + "LWPT_AMP_OCULUS", + "LWPT_MOA_HEAD", + "LWPT_ZANUKA_HEAD", + "LWPT_HB_DECK" +]; + +export const applyClientEquipmentUpdates = ( + inventory: TInventoryDatabaseDocument, + gearArray: IEquipmentClient[], + categoryName: TEquipmentKey +): void => { + const category = inventory[categoryName]; + + gearArray.forEach(({ ItemId, XP, InfestationDate }) => { + const item = category.id(fromOid(ItemId)); + if (!item) { + logger.warn(`Skipping unknown ${categoryName} item: id ${fromOid(ItemId)} not found`); + return; + } + + if (XP) { + item.XP ??= 0; + item.XP += XP; + + if ( + categoryName != "SpecialItems" || + item.ItemType == "/Lotus/Powersuits/Khora/Kavat/KhoraKavatPowerSuit" || + item.ItemType == "/Lotus/Powersuits/Khora/Kavat/KhoraPrimeKavatPowerSuit" + ) { + let xpItemType = item.ItemType; + if (item.ModularParts) { + for (const part of item.ModularParts) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const partType = ExportWeapons[part]?.partType; + if (partType !== undefined && xpEarningParts.indexOf(partType) != -1) { + xpItemType = part; + break; + } + } + logger.debug(`adding xp to ${xpItemType} for modular item ${fromOid(ItemId)} (${item.ItemType})`); + } + + const xpinfoIndex = inventory.XPInfo.findIndex(x => x.ItemType == xpItemType); + if (xpinfoIndex !== -1) { + const xpinfo = inventory.XPInfo[xpinfoIndex]; + xpinfo.XP += XP; + } else { + inventory.XPInfo.push({ + ItemType: xpItemType, + XP: XP + }); + } + } + } + + if (InfestationDate) { + // 2147483647000 means cured, otherwise became infected + item.InfestationDate = + InfestationDate.$date.$numberLong == "2147483647000" ? new Date(0) : fromMongoDate(InfestationDate); + } + }); +}; + +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 }) => { + if (ItemCount == 0) { + return; + } + + let itemIndex = MiscItems.findIndex(x => x.ItemType === ItemType); + if (itemIndex == -1) { + itemIndex = MiscItems.push({ ItemType, ItemCount: 0 }) - 1; + } + + 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(`inventory.MiscItems has a negative count for ${ItemType}`); + } + }); +}; + +const applyArrayChanges = ( + inventory: TInventoryDatabaseDocument, + key: "ShipDecorations" | "Consumables" | "CrewShipRawSalvage" | "CrewShipAmmo" | "Recipes" | "LevelKeys", + changes: ITypeCount[] +): void => { + 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; + } + + 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 addShipDecorations = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { + applyArrayChanges(inventory, "ShipDecorations", itemsArray); +}; + +export const addConsumables = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { + applyArrayChanges(inventory, "Consumables", itemsArray); +}; + +export const addCrewShipRawSalvage = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { + applyArrayChanges(inventory, "CrewShipRawSalvage", itemsArray); +}; + +export const addCrewShipAmmo = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { + applyArrayChanges(inventory, "CrewShipAmmo", itemsArray); +}; + +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 }) => { + if (ItemCount == 0) { + return; + } + + let itemIndex = RawUpgrades.findIndex(x => x.ItemType === ItemType); + if (itemIndex == -1) { + itemIndex = RawUpgrades.push({ ItemType, ItemCount: 0 }) - 1; + } + + RawUpgrades[itemIndex].ItemCount += ItemCount; + if (RawUpgrades[itemIndex].ItemCount == 0) { + RawUpgrades.splice(itemIndex, 1); + } else if (RawUpgrades[itemIndex].ItemCount <= 0) { + logger.warn(`inventory.RawUpgrades has a negative count for ${ItemType}`); + } + }); +}; + +export const addFusionTreasures = (inventory: TInventoryDatabaseDocument, itemsArray: IFusionTreasure[]): void => { + const { FusionTreasures } = inventory; + 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[]): void => { + enum FocusType { + AP_UNIVERSAL, + AP_ATTACK, + AP_DEFENSE, + AP_TACTIC, + AP_POWER, + AP_PRECEPT, + AP_FUSION, + AP_WARD, + AP_UMBRA, + AP_ANY + } + + inventory.FocusXP ??= {}; + if (focusXpPlus[FocusType.AP_ATTACK]) { + inventory.FocusXP.AP_ATTACK ??= 0; + inventory.FocusXP.AP_ATTACK += focusXpPlus[FocusType.AP_ATTACK]; + } + if (focusXpPlus[FocusType.AP_DEFENSE]) { + inventory.FocusXP.AP_DEFENSE ??= 0; + inventory.FocusXP.AP_DEFENSE += focusXpPlus[FocusType.AP_DEFENSE]; + } + if (focusXpPlus[FocusType.AP_TACTIC]) { + inventory.FocusXP.AP_TACTIC ??= 0; + inventory.FocusXP.AP_TACTIC += focusXpPlus[FocusType.AP_TACTIC]; + } + if (focusXpPlus[FocusType.AP_POWER]) { + inventory.FocusXP.AP_POWER ??= 0; + inventory.FocusXP.AP_POWER += focusXpPlus[FocusType.AP_POWER]; + } + if (focusXpPlus[FocusType.AP_WARD]) { + inventory.FocusXP.AP_WARD ??= 0; + inventory.FocusXP.AP_WARD += focusXpPlus[FocusType.AP_WARD]; + } + + if (!inventory.noDailyFocusLimit) { + inventory.DailyFocus -= focusXpPlus.reduce((a, b) => a + b, 0); + } +}; + +export const addLoreFragmentScans = (inventory: TInventoryDatabaseDocument, arr: ILoreFragmentScan[]): void => { + arr.forEach(clientFragment => { + const fragment = inventory.LoreFragmentScans.find(x => x.ItemType == clientFragment.ItemType); + if (fragment) { + fragment.Progress += clientFragment.Progress; + } else { + inventory.LoreFragmentScans.push(clientFragment); + } + }); +}; + +const challengeRewardsInboxMessages: Record = { + SentEvoEphemeraRankOne: { + sub: "/Lotus/Language/Inbox/EvolvingEphemeraUnlockAName", + sndr: "/Lotus/Language/Bosses/Ordis", + msg: "/Lotus/Language/Inbox/EvolvingEphemeraUnlockADesc", + icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", + att: ["/Lotus/Upgrades/Skins/Effects/NarmerEvolvingEphemeraB"] + }, + SentEvoEphemeraRankTwo: { + sub: "/Lotus/Language/Inbox/EvolvingEphemeraUnlockBName", + sndr: "/Lotus/Language/Bosses/Ordis", + msg: "/Lotus/Language/Inbox/EvolvingEphemeraUnlockBDesc", + icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", + att: ["/Lotus/Upgrades/Skins/Effects/NarmerEvolvingEphemeraC"] + }, + SentEvoSyandanaRankOne: { + sub: "/Lotus/Language/Inbox/EvolvingSyandanaUnlockAName", + sndr: "/Lotus/Language/Bosses/Ordis", + msg: "/Lotus/Language/Inbox/EvolvingSyandanaUnlockADesc", + icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", + att: ["/Lotus/Upgrades/Skins/Scarves/NarmerEvolvingSyandanaBCape"] + }, + SentEvoSyandanaRankTwo: { + sub: "/Lotus/Language/Inbox/EvolvingSyandanaUnlockBName", + sndr: "/Lotus/Language/Bosses/Ordis", + msg: "/Lotus/Language/Inbox/EvolvingSyandanaUnlockBDesc", + icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", + att: ["/Lotus/Upgrades/Skins/Scarves/NarmerEvolvingSyandanaCCape"] + }, + SentEvoSekharaRankOne: { + sub: "/Lotus/Language/Inbox/EvolvingSekharaUnlockAName", + sndr: "/Lotus/Language/Bosses/Ordis", + msg: "/Lotus/Language/Inbox/EvolvingSekharaUnlockADesc", + icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", + att: ["/Lotus/Upgrades/Skins/Clan/ZarimanEvolvingSekharaBadgeItemB"] + }, + SentEvoSekharaRankTwo: { + sub: "/Lotus/Language/Inbox/EvolvingSekharaUnlockBName", + sndr: "/Lotus/Language/Bosses/Ordis", + msg: "/Lotus/Language/Inbox/EvolvingSekharaUnlockBDesc", + icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", + att: ["/Lotus/Upgrades/Skins/Clan/ZarimanEvolvingSekharaBadgeItemC"] + }, + // In theory, the following should only give what is owned, but based on the limited information I can find, DE may have simply taken the easy way: https://www.reddit.com/r/Warframe/comments/rzlnku/receiving_all_protovyre_armor_evolution_but_only/ + SentEvoArmorRankOne: { + sub: "/Lotus/Language/Inbox/EvolvingArmorUnlockAName", + sndr: "/Lotus/Language/Bosses/Ordis", + msg: "/Lotus/Language/Inbox/EvolvingArmorUnlockADesc", + icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", + att: [ + "/Lotus/Upgrades/Skins/Armor/SentEvoArmor/SentEvoArmor2A", + "/Lotus/Upgrades/Skins/Armor/SentEvoArmor/SentEvoArmor2C", + "/Lotus/Upgrades/Skins/Armor/SentEvoArmor/SentEvoArmor2L" + ] + }, + SentEvoArmorRankTwo: { + sub: "/Lotus/Language/Inbox/EvolvingArmorUnlockBName", + sndr: "/Lotus/Language/Bosses/Ordis", + msg: "/Lotus/Language/Inbox/EvolvingArmorUnlockBDesc", + icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", + att: [ + "/Lotus/Upgrades/Skins/Armor/SentEvoArmor/SentEvoArmor3A", + "/Lotus/Upgrades/Skins/Armor/SentEvoArmor/SentEvoArmor3C", + "/Lotus/Upgrades/Skins/Armor/SentEvoArmor/SentEvoArmor3L" + ] + } +}; + +/*const evolvingWeaponSkins: Record = { + "/Lotus/Upgrades/Skins/Armor/SentEvoArmor/SentEvoArmor1A": { + challenge: "SentEvoArmorRankOne", + reward: "/Lotus/Upgrades/Skins/Armor/SentEvoArmor/SentEvoArmor2A" + }, + "/Lotus/Upgrades/Skins/Armor/SentEvoArmor/SentEvoArmor1C": { + challenge: "SentEvoArmorRankOne", + reward: "/Lotus/Upgrades/Skins/Armor/SentEvoArmor/SentEvoArmor2C" + }, + "/Lotus/Upgrades/Skins/Armor/SentEvoArmor/SentEvoArmor1L": { + challenge: "SentEvoArmorRankOne", + reward: "/Lotus/Upgrades/Skins/Armor/SentEvoArmor/SentEvoArmor2L" + }, + "/Lotus/Upgrades/Skins/Armor/SentEvoArmor/SentEvoArmor2A": { + challenge: "SentEvoArmorRankTwo", + reward: "/Lotus/Upgrades/Skins/Armor/SentEvoArmor/SentEvoArmor3A" + }, + "/Lotus/Upgrades/Skins/Armor/SentEvoArmor/SentEvoArmor2C": { + challenge: "SentEvoArmorRankTwo", + reward: "/Lotus/Upgrades/Skins/Armor/SentEvoArmor/SentEvoArmor3C" + }, + "/Lotus/Upgrades/Skins/Armor/SentEvoArmor/SentEvoArmor2L": { + challenge: "SentEvoArmorRankTwo", + reward: "/Lotus/Upgrades/Skins/Armor/SentEvoArmor/SentEvoArmor3L" + } +};*/ + +export const addChallenges = async ( + account: TAccountDocument, + inventory: TInventoryDatabaseDocument, + ChallengeProgress: IChallengeProgress[], + SeasonChallengeCompletions: ISeasonChallenge[] | undefined +): Promise => { + for (const { Name, Progress, Completed } of ChallengeProgress) { + let dbChallenge = inventory.ChallengeProgress.find(x => x.Name == Name); + if (dbChallenge) { + dbChallenge.Progress = Progress; + } else { + dbChallenge = { Name, Progress }; + inventory.ChallengeProgress.push(dbChallenge); + } + + if (Name.startsWith("Calendar")) { + addString(getCalendarProgress(inventory).SeasonProgress.ActivatedChallenges, Name); + } + + if ((Completed?.length ?? 0) > (dbChallenge.Completed?.length ?? 0)) { + dbChallenge.Completed ??= []; + for (const completion of Completed!) { + if (dbChallenge.Completed.indexOf(completion) == -1) { + dbChallenge.Completed.push(completion); + if (completion == "challengeRewards") { + if (Name in challengeRewardsInboxMessages) { + await createMessage(account._id, [challengeRewardsInboxMessages[Name]]); + // Would love to somehow let the client know about inbox or inventory changes, but there doesn't seem to anything for updateChallengeProgress. + continue; + } + logger.warn(`ignoring unknown challenge completion`, { challenge: Name, completion }); + dbChallenge.Progress = 0; + dbChallenge.Completed = []; + } + } + } + } else { + dbChallenge.Completed = Completed; + } + } + + 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 + ]; + } + + const standingToAdd = meta.standing! * (config.nightwaveStandingMultiplier ?? 1); + affiliation.Standing += standingToAdd; + if (affiliationMods.length == 0) { + affiliationMods.push({ Tag: nightwaveSyndicateTag }); + } + affiliationMods[0].Standing ??= 0; + affiliationMods[0].Standing += standingToAdd; + } + } + } + return affiliationMods; +}; + +export const addCalendarProgress = (inventory: TInventoryDatabaseDocument, value: { challenge: string }[]): void => { + const calendarProgress = getCalendarProgress(inventory); + const currentSeason = getWorldState().KnownCalendarSeasons[0]; + calendarProgress.SeasonProgress.LastCompletedChallengeDayIdx = currentSeason.Days.findIndex( + day => day.events.length != 0 && day.events[0].challenge == value[value.length - 1].challenge + ); + checkCalendarAutoAdvance(inventory, currentSeason); +}; + +export const addMissionComplete = (inventory: TInventoryDatabaseDocument, { Tag, Completes, Tier }: IMission): void => { + const { Missions } = inventory; + const itemIndex = Missions.findIndex(item => item.Tag === Tag); + + if (itemIndex !== -1) { + Missions[itemIndex].Completes += Completes; + if (Tier) { + Missions[itemIndex].Tier = Tier; + } + } else { + Missions.push({ Tag, Completes }); + } +}; + +export const addBooster = (ItemType: string, time: number, inventory: TInventoryDatabaseDocument): void => { + const currentTime = Math.floor(Date.now() / 1000); + + const { Boosters } = inventory; + + const itemIndex = Boosters.findIndex(booster => booster.ItemType === ItemType); + + if (itemIndex !== -1) { + const existingBooster = Boosters[itemIndex]; + existingBooster.ExpiryDate = Math.max(existingBooster.ExpiryDate, currentTime) + time; + } else { + Boosters.push({ ItemType, ExpiryDate: currentTime + time }); + } +}; + +export const updateSyndicate = ( + inventory: TInventoryDatabaseDocument, + syndicateUpdate: IMissionInventoryUpdateRequest["AffiliationChanges"] +): void => { + syndicateUpdate?.forEach(affiliation => { + const syndicate = inventory.Affiliations.find(x => x.Tag == affiliation.Tag); + if (syndicate !== undefined) { + syndicate.Standing += affiliation.Standing; + syndicate.Title = syndicate.Title === undefined ? affiliation.Title : syndicate.Title + affiliation.Title; + } else { + inventory.Affiliations.push({ + Standing: affiliation.Standing, + Title: affiliation.Title, + Tag: affiliation.Tag, + FreeFavorsEarned: [], + FreeFavorsUsed: [] + }); + } + updateStandingLimit(inventory, ExportSyndicates[affiliation.Tag].dailyLimitBin, affiliation.Standing); + }); +}; + +/** + * @returns object with inventory keys of changes or empty object when no items were added + */ +export const addKeyChainItems = async ( + inventory: TInventoryDatabaseDocument, + keyChainData: IKeyChainRequest +): Promise => { + const keyChainItems = getKeyChainItems(keyChainData); + + logger.debug( + `adding key chain items ${keyChainItems.join()} for ${keyChainData.KeyChain} at stage ${keyChainData.ChainStage}` + ); + + const nonStoreItems = keyChainItems.map(item => fromStoreItem(item)); + + const inventoryChanges: IInventoryChanges = {}; + + for (const item of nonStoreItems) { + const inventoryChangesDelta = await addItem(inventory, item); + combineInventoryChanges(inventoryChanges, inventoryChangesDelta); + } + + 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 => { + inventory.CurrentLoadOutIds = inventory.CurrentLoadOutIds.map(fromDbOid); + + 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 = {}; + } + } + + { + let numFixed = 0; + for (const equipmentKey of equipmentKeys) { + for (const item of inventory[equipmentKey]) { + if (item.ModularParts?.length === 0) { + item.ModularParts = undefined; + ++numFixed; + } + } + } + if (numFixed != 0) { + logger.debug(`removed ModularParts from ${numFixed} non-modular items`); + } + } +}; + +export const getDialogue = (inventory: TInventoryDatabaseDocument, dialogueName: string): IDialogueDatabase => { + inventory.DialogueHistory ??= {}; + inventory.DialogueHistory.Dialogues ??= []; + 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: -1, + LastCompletedChallengeDayIdx: -1, + 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 checkCalendarAutoAdvance = ( + inventory: TInventoryDatabaseDocument, + currentSeason: ICalendarSeason +): void => { + const calendarProgress = inventory.CalendarProgress!; + for ( + let dayIndex = calendarProgress.SeasonProgress.LastCompletedDayIdx + 1; + dayIndex != currentSeason.Days.length; + ++dayIndex + ) { + const day = currentSeason.Days[dayIndex]; + if (day.events.length == 0) { + // birthday + if (day.day == 1) { + // kaya + if ((inventory.Affiliations.find(x => x.Tag == "HexSyndicate")?.Title || 0) >= 4) { + break; + } + logger.debug(`cannot talk to kaya, skipping birthday`); + calendarProgress.SeasonProgress.LastCompletedDayIdx++; + } else if (day.day == 74 || day.day == 355) { + // minerva, velimir + if ((inventory.Affiliations.find(x => x.Tag == "HexSyndicate")?.Title || 0) >= 5) { + break; + } + logger.debug(`cannot talk to minerva/velimir, skipping birthday`); + calendarProgress.SeasonProgress.LastCompletedDayIdx++; + } else { + break; + } + } else if (day.events[0].type == "CET_CHALLENGE") { + if (calendarProgress.SeasonProgress.LastCompletedChallengeDayIdx < dayIndex) { + break; + } + //logger.debug(`already completed the challenge, skipping ahead`); + calendarProgress.SeasonProgress.LastCompletedDayIdx++; + } else { + break; + } + } +}; + +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"; +}; + +export const updateEntratiVault = (inventory: TInventoryDatabaseDocument): void => { + 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 = []; + } + } +}; diff --git a/src/services/itemDataService.ts b/src/services/itemDataService.ts new file mode 100644 index 00000000..bb11b611 --- /dev/null +++ b/src/services/itemDataService.ts @@ -0,0 +1,311 @@ +import type { IKeyChainRequest } from "../types/requestTypes.ts"; +import type { + IDefaultUpgrade, + IInboxMessage, + IKey, + IMissionReward, + IRecipe, + TReward +} from "warframe-public-export-plus"; +import { + dict_de, + dict_en, + dict_es, + dict_fr, + dict_it, + dict_ja, + dict_ko, + dict_pl, + dict_pt, + dict_ru, + dict_tc, + dict_th, + dict_tr, + dict_uk, + dict_zh, + ExportArcanes, + ExportBoosters, + ExportBundles, + ExportCustoms, + ExportDrones, + ExportGear, + ExportKeys, + ExportRecipes, + ExportResources, + ExportSentinels, + ExportWarframes, + ExportWeapons +} from "warframe-public-export-plus"; +import type { IMessage } from "../models/inboxModel.ts"; +import { logger } from "../utils/logger.ts"; + +export type WeaponTypeInternal = + | "LongGuns" + | "Pistols" + | "Melee" + | "SpaceMelee" + | "SpaceGuns" + | "SentinelWeapons" + | "OperatorAmps" + | "SpecialItems"; + +export const getRecipe = (uniqueName: string): IRecipe | undefined => { + // Handle crafting of archwing summon for versions prior to 39.0.0 as this blueprint was removed then. + if (uniqueName == "/Lotus/Types/Recipes/EidolonRecipes/OpenArchwingSummonBlueprint") { + return { + resultType: "/Lotus/Types/Restoratives/OpenArchwingSummon", + buildPrice: 7500, + buildTime: 1800, + skipBuildTimePrice: 10, + consumeOnUse: false, + num: 1, + codexSecret: false, + alwaysAvailable: true, + ingredients: [ + { + ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/IraditeItem", + ItemCount: 50 + }, + { + ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/GrokdrulItem", + ItemCount: 50 + }, + { + ItemType: "/Lotus/Types/Items/Fish/Eidolon/FishParts/EidolonFishOilItem", + ItemCount: 30 + }, + { + ItemType: "/Lotus/Types/Items/MiscItems/Circuits", + ItemCount: 600 + } + ], + excludeFromMarket: true + }; + } + + return ExportRecipes[uniqueName]; +}; + +export const getRecipeByResult = (resultType: string): IRecipe | undefined => { + return Object.values(ExportRecipes).find(x => x.resultType == resultType); +}; + +export const getItemCategoryByUniqueName = (uniqueName: string): string | undefined => { + if (uniqueName in ExportCustoms) { + return ExportCustoms[uniqueName].productCategory; + } + if (uniqueName in ExportDrones) { + return "Drones"; + } + if (uniqueName in ExportKeys) { + return "LevelKeys"; + } + if (uniqueName in ExportGear) { + return "Consumables"; + } + if (uniqueName in ExportResources) { + return ExportResources[uniqueName].productCategory; + } + if (uniqueName in ExportSentinels) { + return ExportSentinels[uniqueName].productCategory; + } + if (uniqueName in ExportWarframes) { + return ExportWarframes[uniqueName].productCategory; + } + if (uniqueName in ExportWeapons) { + return ExportWeapons[uniqueName].productCategory; + } + return undefined; +}; + +export const getItemName = (uniqueName: string): string | undefined => { + if (uniqueName in ExportArcanes) { + return ExportArcanes[uniqueName].name; + } + if (uniqueName in ExportBundles) { + return ExportBundles[uniqueName].name; + } + if (uniqueName in ExportCustoms) { + return ExportCustoms[uniqueName].name; + } + if (uniqueName in ExportDrones) { + return ExportDrones[uniqueName].name; + } + if (uniqueName in ExportKeys) { + return ExportKeys[uniqueName].name; + } + if (uniqueName in ExportGear) { + return ExportGear[uniqueName].name; + } + if (uniqueName in ExportResources) { + return ExportResources[uniqueName].name; + } + if (uniqueName in ExportSentinels) { + return ExportSentinels[uniqueName].name; + } + if (uniqueName in ExportWarframes) { + return ExportWarframes[uniqueName].name; + } + if (uniqueName in ExportWeapons) { + return ExportWeapons[uniqueName].name; + } + return undefined; +}; + +export const getDict = (lang: string): Record => { + switch (lang) { + case "de": + return dict_de; + case "es": + return dict_es; + case "fr": + return dict_fr; + case "it": + return dict_it; + case "ja": + return dict_ja; + case "ko": + return dict_ko; + case "pl": + return dict_pl; + case "pt": + return dict_pt; + case "ru": + return dict_ru; + case "tc": + return dict_tc; + case "th": + return dict_th; + case "tr": + return dict_tr; + case "uk": + return dict_uk; + case "zh": + return dict_zh; + } + return dict_en; +}; + +export const getString = (key: string, dict: Record): string => { + return dict[key] ?? key; +}; + +export const getKeyChainItems = ({ KeyChain, ChainStage }: IKeyChainRequest): string[] => { + const chainStages = ExportKeys[KeyChain].chainStages; + if (!chainStages) { + throw new Error(`KeyChain ${KeyChain} does not contain chain stages`); + } + + const keyChainStage = chainStages[ChainStage]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!keyChainStage) { + throw new Error(`KeyChainStage ${ChainStage} not found`); + } + + if (keyChainStage.itemsToGiveWhenTriggered.length === 0) { + throw new Error( + `client requested key chain items in KeyChain ${KeyChain} at stage ${ChainStage}, but they did not exist` + ); + } + + return keyChainStage.itemsToGiveWhenTriggered; +}; + +export const getLevelKeyRewards = ( + levelKey: string +): { levelKeyRewards?: IMissionReward; levelKeyRewards2?: TReward[] } => { + const key = ExportKeys[levelKey] as IKey | undefined; + + const levelKeyRewards = key?.missionReward; + const levelKeyRewards2 = key?.rewards; + + if (!levelKeyRewards && !levelKeyRewards2) { + logger.warn( + `Could not find any reward information for ${levelKey}, gonna have to potentially short-change you` + ); + } + + return { + levelKeyRewards, + levelKeyRewards2 + }; +}; + +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`); + } + + let i = ChainStage; + let chainStageMessage = chainStages[i].messageToSendWhenTriggered; + while (!chainStageMessage) { + if (++i >= chainStages.length) { + break; + } + chainStageMessage = chainStages[i].messageToSendWhenTriggered; + } + + if (!chainStageMessage) { + throw new Error( + `client requested key chain message in keychain ${KeyChain} at stage ${ChainStage} but they did not exist` + ); + } + return convertInboxMessage(chainStageMessage); +}; + +export const convertInboxMessage = (message: IInboxMessage): IMessage => { + return { + sndr: message.sender, + msg: message.body, + cinematic: message.cinematic, + sub: message.title, + customData: message.customData, + 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/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..2ee36b92 --- /dev/null +++ b/src/services/leaderboardService.ts @@ -0,0 +1,126 @@ +import { Guild } from "../models/guildModel.ts"; +import type { TLeaderboardEntryDocument } from "../models/leaderboardModel.ts"; +import { Leaderboard } from "../models/leaderboardModel.ts"; +import type { ILeaderboardEntryClient } from "../types/leaderboardTypes.ts"; +import { handleGuildGoalProgress } from "./guildService.ts"; +import { getWorldState } from "./worldStateService.ts"; +import { Types } from "mongoose"; + +export const submitLeaderboardScore = async ( + schedule: "weekly" | "daily" | "events", + leaderboard: string, + ownerId: string, + displayName: string, + score: number, + guildId: string | undefined +): Promise => { + let expiry: Date | undefined; + if (schedule == "daily") { + expiry = new Date(Math.trunc(Date.now() / 86400000) * 86400000 + 86400000); + } else if (schedule == "weekly") { + 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); + } + + if (guildId) { + const guild = (await Guild.findById(guildId, "Name Tier GoalProgress VaultDecoRecipes"))!; + if (schedule == "events") { + const prevAccount = await Leaderboard.findOne( + { leaderboard: `${schedule}.accounts.${leaderboard}`, ownerId }, + "score" + ); + const delta = score - (prevAccount?.score ?? 0); + if (delta > 0) { + await Leaderboard.findOneAndUpdate( + { leaderboard: `${schedule}.guilds.${leaderboard}`, ownerId: guildId }, + { $inc: { score: delta }, $set: { displayName: guild.Name, guildTier: guild.Tier } }, + { upsert: true } + ); + const goal = getWorldState().Goals.find(x => x.ScoreMaxTag == leaderboard); + if (goal) { + await handleGuildGoalProgress(guild, { + Count: delta, + Tag: goal.Tag, + goalId: new Types.ObjectId(goal._id.$oid) + }); + } + } + } else { + await Leaderboard.findOneAndUpdate( + { leaderboard: `${schedule}.guilds.${leaderboard}`, ownerId: guildId }, + { $max: { score }, $set: { displayName: guild.Name, guildTier: guild.Tier, expiry } }, + { upsert: true, new: true } + ); + } + } + + await Leaderboard.findOneAndUpdate( + { leaderboard: `${schedule}.accounts.${leaderboard}`, ownerId }, + { $max: { score }, $set: { displayName, guildId, expiry } }, + { upsert: true } + ); +}; + +export const getLeaderboard = async ( + leaderboard: string, + before: number, + after: number, + pivotId: string | undefined, + guildId: string | undefined, + guildTier: number | undefined +): Promise => { + leaderboard = leaderboard.replace("archived", guildTier || guildId ? "events.guilds" : "events.accounts"); + 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 new file mode 100644 index 00000000..a84836c3 --- /dev/null +++ b/src/services/loadoutService.ts @@ -0,0 +1,12 @@ +import type { TLoadoutDatabaseDocument } from "../models/inventoryModels/loadoutModel.ts"; +import { Loadout } from "../models/inventoryModels/loadoutModel.ts"; + +export const getLoadout = async (accountId: string): Promise => { + const loadout = await Loadout.findOne({ loadoutOwnerId: accountId }); + + if (!loadout) { + throw new Error(`loadout not found for account ${accountId}`); + } + + return loadout; +}; diff --git a/src/services/loginRewardService.ts b/src/services/loginRewardService.ts new file mode 100644 index 00000000..3ea615fc --- /dev/null +++ b/src/services/loginRewardService.ts @@ -0,0 +1,168 @@ +import randomRewards from "../../static/fixed_responses/loginRewards/randomRewards.json" with { type: "json" }; +import type { IInventoryChanges } from "../types/purchaseTypes.ts"; +import type { TAccountDocument } from "./loginService.ts"; +import { mixSeeds, SRng } from "./rngService.ts"; +import type { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel.ts"; +import { addBooster, updateCurrency } from "./inventoryService.ts"; +import { handleStoreItemAcquisition } from "./purchaseService.ts"; +import { + ExportBoosterPacks, + ExportBoosters, + ExportRecipes, + ExportWarframes, + ExportWeapons +} from "warframe-public-export-plus"; +import { toStoreItem } from "./itemDataService.ts"; + +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, undefined, true)) + .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 new file mode 100644 index 00000000..b9e200cf --- /dev/null +++ b/src/services/loginService.ts @@ -0,0 +1,123 @@ +import { Account } from "../models/loginModel.ts"; +import { createInventory } from "./inventoryService.ts"; +import type { IDatabaseAccountJson, IDatabaseAccountRequiredFields } from "../types/loginTypes.ts"; +import { createShip } from "./shipService.ts"; +import type { Document, Types } from "mongoose"; +import { Loadout } from "../models/inventoryModels/loadoutModel.ts"; +import { PersonalRooms } from "../models/personalRoomsModel.ts"; +import type { Request } from "express"; +import { config } from "./configService.ts"; +import { createStats } from "./statsService.ts"; +import crc32 from "crc-32"; + +export const isCorrectPassword = (requestPassword: string, databasePassword: string): boolean => { + return requestPassword === databasePassword; +}; + +export const isNameTaken = async (name: string): Promise => { + return !!(await Account.findOne({ DisplayName: name })); +}; + +export const createNonce = (): number => { + return Math.round(Math.random() * Number.MAX_SAFE_INTEGER); +}; + +export const getUsernameFromEmail = async (email: string): Promise => { + const nameFromEmail = email.substring(0, email.indexOf("@")); + let name = nameFromEmail || email.substring(1) || "SpaceNinja"; + if (await isNameTaken(name)) { + let suffix = 0; + do { + ++suffix; + name = nameFromEmail + suffix; + } while (await isNameTaken(name)); + } + return name; +}; + +export const createAccount = async (accountData: IDatabaseAccountRequiredFields): Promise => { + const account = new Account(accountData); + try { + await account.save(); + const loadoutId = await createLoadout(account._id); + const shipId = await createShip(account._id); + await createInventory(account._id, { loadOutPresetId: loadoutId, ship: shipId }); + await createPersonalRooms(account._id, shipId); + await createStats(account._id.toString()); + return account.toJSON(); + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } + throw new Error("error creating account that is not of instance Error"); + } +}; + +export const createLoadout = async (accountId: Types.ObjectId): Promise => { + const loadout = new Loadout({ loadoutOwnerId: accountId }); + const savedLoadout = await loadout.save(); + return savedLoadout._id; +}; + +export const createPersonalRooms = async (accountId: Types.ObjectId, shipId: Types.ObjectId): Promise => { + const personalRooms = new PersonalRooms({ + personalRoomsOwnerId: accountId, + activeShipId: shipId + }); + if (config.skipTutorial) { + // 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/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"); + } + const nonce: number = parseInt(req.query.nonce as string); + if (!nonce) { + throw new Error("Request is missing nonce parameter"); + } + + 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 => { + return (await getAccountForRequest(req))._id.toString(); +}; + +export const isAdministrator = (account: TAccountDocument): boolean => { + 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 new file mode 100644 index 00000000..a6f17c73 --- /dev/null +++ b/src/services/missionInventoryUpdateService.ts @@ -0,0 +1,2715 @@ +import type { IMissionReward as IMissionRewardExternal, IRegion, IReward } from "warframe-public-export-plus"; +import { + ExportEnemies, + ExportFusionBundles, + ExportRegions, + ExportRelics, + ExportRewards +} from "warframe-public-export-plus"; +import type { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes.ts"; +import { logger } from "../utils/logger.ts"; +import type { IRngResult } from "./rngService.ts"; +import { SRng, generateRewardSeed, getRandomElement, getRandomReward } from "./rngService.ts"; +import type { IMission, TEquipmentKey } from "../types/inventoryTypes/inventoryTypes.ts"; +import { equipmentKeys } from "../types/inventoryTypes/inventoryTypes.ts"; +import { + addBooster, + addCalendarProgress, + addChallenges, + addConsumables, + addCrewShipAmmo, + addCrewShipRawSalvage, + addEmailItem, + addFocusXpIncreases, + addFusionPoints, + addFusionTreasures, + addItem, + addLevelKeys, + addLoreFragmentScans, + addMiscItems, + addMissionComplete, + addMods, + addRecipes, + addShipDecorations, + addSkin, + addStanding, + applyClientEquipmentUpdates, + combineInventoryChanges, + getDialogue, + giveNemesisPetRecipe, + giveNemesisWeaponRecipe, + updateCurrency, + updateEntratiVault, + updateSyndicate +} from "./inventoryService.ts"; +import { updateQuestKey } from "./questService.ts"; +import { Types } from "mongoose"; +import type { IAffiliationMods, IInventoryChanges } from "../types/purchaseTypes.ts"; +import { fromStoreItem, getLevelKeyRewards, isStoreItem, toStoreItem } from "./itemDataService.ts"; +import type { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel.ts"; +import { getEntriesUnsafe } from "../utils/ts-utils.ts"; +import { handleStoreItemAcquisition } from "./purchaseService.ts"; +import type { IMissionCredits, IMissionReward } from "../types/missionTypes.ts"; +import { crackRelic } from "../helpers/relicHelper.ts"; +import type { IMessageCreationTemplate } from "./inboxService.ts"; +import { createMessage } from "./inboxService.ts"; +import kuriaMessage50 from "../../static/fixed_responses/kuriaMessages/fiftyPercent.json" with { type: "json" }; +import kuriaMessage75 from "../../static/fixed_responses/kuriaMessages/seventyFivePercent.json" with { type: "json" }; +import kuriaMessage100 from "../../static/fixed_responses/kuriaMessages/oneHundredPercent.json" with { type: "json" }; +import conservationAnimals from "../../static/fixed_responses/conservationAnimals.json" with { type: "json" }; +import { + generateNemesisProfile, + getInfestedLichItemRewards, + getInfNodes, + getKillTokenRewardCount, + getNemesisManifest, + getNemesisPasscode +} from "../helpers/nemesisHelpers.ts"; +import { Loadout } from "../models/inventoryModels/loadoutModel.ts"; +import { + getLiteSortie, + getSortie, + getWorldState, + idToBountyCycle, + idToDay, + idToWeek, + pushClassicBounties +} from "./worldStateService.ts"; +import { config } from "./configService.ts"; +import libraryDailyTasks from "../../static/fixed_responses/libraryDailyTasks.json" with { type: "json" }; +import type { IGoal, ISyndicateMissionInfo } from "../types/worldStateTypes.ts"; +import { fromOid } from "../helpers/inventoryHelpers.ts"; +import type { TAccountDocument } from "./loginService.ts"; +import type { ITypeCount } from "../types/commonTypes.ts"; +import type { IEquipmentClient } from "../types/equipmentTypes.ts"; +import { Guild } from "../models/guildModel.ts"; +import { handleGuildGoalProgress } from "./guildService.ts"; +import { importLoadOutConfig } from "./importService.ts"; + +const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] => { + // Disruption missions just tell us (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2599) + if (rewardInfo.rewardTierOverrides) { + return rewardInfo.rewardTierOverrides; + } + + // 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(Math.min(i, 2)); + } + return rotations; + } + + const region = ExportRegions[rewardInfo.node] as IRegion | undefined; + const missionIndex: number | undefined = region?.missionIndex; + + // For Rescue missions + if (missionIndex == 3 && rewardInfo.rewardTier) { + return [rewardInfo.rewardTier]; + } + + // 'rewardQualifications' is unreliable for non-endless railjack missions (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2586, https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2612) + switch (region?.missionName) { + case "/Lotus/Language/Missions/MissionName_Railjack": + case "/Lotus/Language/Missions/MissionName_RailjackVolatile": + case "/Lotus/Language/Missions/MissionName_RailjackExterminate": + case "/Lotus/Language/Missions/MissionName_RailjackAssassinate": + return [0]; + } + + 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++) { + rotatedValues.push(rotationPattern[i % rotationPattern.length]); + } + + return rotatedValues; +}; + +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[]); +}; + +//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 = async ( + account: TAccountDocument, + inventory: TInventoryDatabaseDocument, + inventoryUpdates: IMissionInventoryUpdateRequest +): 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) { + 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 (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)) { + if (value === undefined) { + logger.error(`Inventory update key ${key} has no value`); + continue; + } + switch (key) { + case "RegularCredits": + inventory.RegularCredits += value; + break; + case "QuestKeys": + await updateQuestKey(inventory, value); + break; + case "AffiliationChanges": + updateSyndicate(inventory, value); + break; + // Incarnon Challenges + case "EvolutionProgress": { + for (const evoProgress of value) { + const entry = inventory.EvolutionProgress + ? inventory.EvolutionProgress.find(entry => entry.ItemType == evoProgress.ItemType) + : undefined; + if (entry) { + entry.Progress = evoProgress.Progress; + entry.Rank = evoProgress.Rank; + } else { + inventory.EvolutionProgress ??= []; + inventory.EvolutionProgress.push(evoProgress); + } + } + break; + } + case "Missions": + addMissionComplete(inventory, value); + break; + case "LastRegionPlayed": + if (!(config.unfaithfulBugFixes?.ignore1999LastRegionPlayed && value === "1999MapName")) { + inventory.LastRegionPlayed = value; + } + break; + case "RawUpgrades": + addMods(inventory, value); + break; + case "MiscItems": + case "BonusMiscItems": + addMiscItems(inventory, value); + break; + case "Consumables": + if (inventory.dontSubtractConsumables) { + addConsumables( + inventory, + value.filter(x => x.ItemCount > 0) + ); + } else { + addConsumables(inventory, value); + } + break; + case "Recipes": + addRecipes(inventory, value); + break; + case "ChallengeProgress": + await addChallenges(account, inventory, value, inventoryUpdates.SeasonChallengeCompletions); + break; + case "FusionTreasures": + addFusionTreasures(inventory, value); + break; + case "CrewShipRawSalvage": + addCrewShipRawSalvage(inventory, value); + break; + 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 fusionPointsDelta = 0; + for (const fusionBundle of value) { + 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); + } + break; + } + case "FocusXpIncreases": { + addFocusXpIncreases(inventory, value); + break; + } + case "PlayerSkillGains": { + inventory.PlayerSkills.LPP_SPACE += value.LPP_SPACE ?? 0; + inventory.PlayerSkills.LPP_DRIFTER += value.LPP_DRIFTER ?? 0; + break; + } + case "CustomMarkers": { + value.forEach(markers => { + const map = inventory.CustomMarkers + ? inventory.CustomMarkers.find(entry => entry.tag == markers.tag) + : undefined; + if (map) { + map.markerInfos = markers.markerInfos; + } else { + inventory.CustomMarkers ??= []; + inventory.CustomMarkers.push(markers); + } + }); + 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": { + if (!inventory.syndicateMissionsRepeatable) { + inventory.CompletedSyndicates.push(value); + } + break; + } + case "SortieId": { + if (inventory.CompletedSorties.indexOf(value) == -1) { + inventory.CompletedSorties.push(value); + } + break; + } + case "SeasonChallengeCompletions": { + const processedCompletions = value.map(({ challenge, id }) => ({ + challenge: challenge.substring(challenge.lastIndexOf("/") + 1), + id + })); + inventory.SeasonChallengeHistory.push(...processedCompletions); + break; + } + case "DeathMarks": { + if (!inventory.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, + endDate: 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 not 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.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. + } + ]); + } + inventory.DeathSquadable = false; + 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": { + // Unsure what to do with this + 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 loadoutConfigDatabase = importLoadOutConfig(loadoutConfig); + 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 "GoalProgress": { + for (const uploadProgress of value) { + const goal = getWorldState().Goals.find(x => x._id.$oid == uploadProgress._id.$oid); + if (goal && goal.Personal) { + inventory.PersonalGoalProgress ??= []; + const goalProgress = inventory.PersonalGoalProgress.find(x => x.goalId.equals(goal._id.$oid)); + if (!goalProgress) { + inventory.PersonalGoalProgress.push({ + Best: uploadProgress.Best, + Count: uploadProgress.Count, + Tag: goal.Tag, + goalId: new Types.ObjectId(goal._id.$oid) + }); + } + + const currentNode = inventoryUpdates.RewardInfo!.node; + let currentMissionKey: string | undefined; + if (currentNode == goal.Node) { + currentMissionKey = goal.MissionKeyName; + } else if (goal.ConcurrentNodes && goal.ConcurrentMissionKeyNames) { + for (let i = 0; i < goal.ConcurrentNodes.length; i++) { + if (currentNode == goal.ConcurrentNodes[i]) { + currentMissionKey = goal.ConcurrentMissionKeyNames[i]; + break; + } + } + } + const rewards = []; + let countBeforeUpload = goalProgress?.Count ?? 0; + let totalCount = countBeforeUpload + uploadProgress.Count; + if (goal.Best) { + countBeforeUpload = goalProgress?.Best ?? 0; + totalCount = uploadProgress.Best; + } + + { + if (goal.InterimGoals && goal.InterimRewards) { + for (let i = 0; i < goal.InterimGoals.length; i++) { + if ( + goal.InterimGoals[i] && + goal.InterimGoals[i] <= totalCount && + (!goalProgress || countBeforeUpload < goal.InterimGoals[i]) && + goal.InterimRewards[i] + ) { + rewards.push(goal.InterimRewards[i]); + break; + } + } + } + if ( + goal.Goal && + goal.Goal <= totalCount && + (!goalProgress || countBeforeUpload < goal.Goal) && + goal.Reward + ) { + rewards.push(goal.Reward); + } + if ( + goal.BonusGoal && + goal.BonusGoal <= totalCount && + (!goalProgress || countBeforeUpload < goal.BonusGoal) && + goal.BonusReward + ) { + rewards.push(goal.BonusReward); + } + } + + const messages: IMessageCreationTemplate[] = []; + const infos: { + sndr: string; + msg: string; + sub: string; + icon: string; + arg?: string[]; + }[] = []; + + { + if (currentMissionKey && currentMissionKey in goalMessagesByKey) { + infos.push(goalMessagesByKey[currentMissionKey]); + } else if (goal.Tag in goalMessagesByTag) { + const combinedGoals = [...(goal.InterimGoals || []), goal.Goal, goal.BonusGoal]; + combinedGoals.forEach((n, i) => { + if (n !== undefined && n > countBeforeUpload && n <= totalCount) { + infos.push(goalMessagesByTag[goal.Tag][i]); + } + }); + } + } + + for (let i = 0; i < rewards.length; i++) { + if (infos[i]) { + const info = infos[i]; + const reward = rewards[i]; + const message: IMessageCreationTemplate = { + sndr: info.sndr, + msg: info.msg, + sub: info.sub, + icon: info.icon, + highPriority: true + }; + if (reward.items) { + message.att = reward.items.map(x => (isStoreItem(x) ? fromStoreItem(x) : x)); + } + if (reward.countedItems) { + message.countedAtt = reward.countedItems; + } + if (reward.credits) { + message.RegularCredits = reward.credits; + } + if (info.arg) { + const args: Record = { + PLAYER_NAME: account.DisplayName, + CREDIT_REWARD: reward.credits ?? 0 + }; + + for (let j = 0; j < info.arg.length; j++) { + const key = info.arg[j]; + const value = args[key]; + if (value) { + message.arg ??= []; + message.arg.push({ + Key: key, + Tag: value + }); + } + } + } + messages.push(message); + } + } + + if (messages.length > 0) await createMessage(inventory.accountOwnerId, messages); + + if (goalProgress) { + goalProgress.Best = Math.max(goalProgress.Best!, uploadProgress.Best); + goalProgress.Count += uploadProgress.Count; + } + } + if (goal && goal.ClanGoal && inventory.GuildId) { + const guild = await Guild.findById(inventory.GuildId, "GoalProgress Tier VaultDecoRecipes"); + if (guild) { + await handleGuildGoalProgress(guild, { + Count: uploadProgress.Count, + Tag: goal.Tag, + goalId: new Types.ObjectId(goal._id.$oid) + }); + } + } + } + break; + } + case "InvasionProgress": { + for (const clientProgress of value) { + if (inventory.finishInvasionsInOneMission) { + clientProgress.Delta *= 3; + clientProgress.AttackerScore *= 3; + clientProgress.DefenderScore *= 3; + } + 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": { + addCalendarProgress(inventory, value); + 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(); + inventory.DuviriInfo!.NumCompletions += 1; + } + 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); + } + } + + await createMessage(inventory.accountOwnerId, [ + { + sndr: value.killed ? "/Lotus/Language/Bosses/Ordis" : value.nemesisName, + msg: value.killed ? manifest.killMessageBody : manifest.convertMessageBody, + arg: [ + { + Key: "LICH_NAME", + Tag: value.nemesisName + } + ], + att: att, + countedAtt: countedAtt, + attVisualOnly: true, + sub: value.killed ? manifest.killMessageSubject : manifest.convertMessageSubject, + icon: value.killed ? "/Lotus/Interface/Icons/Npcs/Ordis.png" : manifest.convertMessageIcon, + highPriority: true + } + ]); + + inventory.Nemesis = undefined; + } + break; + default: + if (equipmentKeys.includes(key as TEquipmentKey)) { + applyClientEquipmentUpdates(inventory, value as IEquipmentClient[], key as TEquipmentKey); + } + break; + // if ( + // (ignoredInventoryUpdateKeys as readonly string[]).includes(key) || + // knownUnhandledKeys.includes(key) + // ) { + // continue; + // } + // logger.error(`Unhandled inventory update key: ${key}`); + } + } + + 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" +}; + +const isEligibleForCreditReward = (rewardInfo: IRewardInfo, missions: IMission, node: IRegion): boolean => { + // (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) + if (getRotations(rewardInfo).length == 0) { + return missions.Tag == "SolNode720"; // Netracells don't use rewardQualifications but probably should give credits anyway + } + // The rest here might not be needed anymore, but just to be sure we don't give undue credits... + return ( + 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 + ); +}; + +//TODO: return type of partial missioninventoryupdate response +export const addMissionRewards = async ( + account: TAccountDocument, + inventory: TInventoryDatabaseDocument, + { + wagerTier: wagerTier, + Nemesis: nemesis, + RewardInfo: rewardInfo, + LevelKeyName: levelKeyName, + Missions: missions, + RegularCredits: creditDrops, + VoidTearParticipantsCurrWave: voidTearWave, + StrippedItems: strippedItems, + AffiliationChanges: AffiliationMods + }: 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 `); + return { MissionRewards: [] }; + } + + //TODO: check double reward merging + const MissionRewards: IMissionReward[] = getRandomMissionDrops( + inventory, + rewardInfo, + missions, + wagerTier, + firstCompletion + ); + logger.debug("random mission drops:", MissionRewards); + const inventoryChanges: IInventoryChanges = {}; + 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 (rewardInfo.goalId) { + const goal = getWorldState().Goals.find(x => x._id.$oid == rewardInfo.goalId); + if (goal) { + if (rewardInfo.node == goal.Node && goal.MissionKeyName) levelKeyName = goal.MissionKeyName; + if (goal.ConcurrentNodes && goal.ConcurrentMissionKeyNames) { + for (let i = 0; i < goal.ConcurrentNodes.length && i < goal.ConcurrentMissionKeyNames.length; i++) { + if (rewardInfo.node == goal.ConcurrentNodes[i]) { + levelKeyName = goal.ConcurrentMissionKeyNames[i]; + break; + } + } + } + if (rewardInfo.GoalProgressAmount && goal.Tag.startsWith("MechSurvival")) { + MissionRewards.push({ + StoreItem: "/Lotus/StoreItems/Types/Items/MiscItems/MechSurvivalEventCreds", + ItemCount: Math.trunc(rewardInfo.GoalProgressAmount / 10) + }); + } + } + } + + if (levelKeyName) { + const fixedLevelRewards = getLevelKeyRewards(levelKeyName); + //logger.debug(`fixedLevelRewards ${fixedLevelRewards}`); + if (fixedLevelRewards.levelKeyRewards) { + missionCompletionCredits += addFixedLevelRewards( + fixedLevelRewards.levelKeyRewards, + MissionRewards, + rewardInfo + ); + } + if (fixedLevelRewards.levelKeyRewards2) { + for (const reward of fixedLevelRewards.levelKeyRewards2) { + //quest stage completion credit rewards + if (reward.rewardType == "RT_CREDITS") { + missionCompletionCredits += reward.amount; + continue; + } + 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 (isEligibleForCreditReward(rewardInfo, missions, node)) { + const levelCreditReward = getLevelCreditRewards(node); + missionCompletionCredits += levelCreditReward; + logger.debug(`levelCreditReward ${levelCreditReward}`); + } + + if (node.missionReward) { + missionCompletionCredits += addFixedLevelRewards(node.missionReward, 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) { + 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); + } + + inventory.RegularCredits += missionCompletionCredits; + + const credits = await addCredits(account, inventory, { + missionCompletionCredits, + missionDropCredits: creditDrops ?? 0, + rngRewardCredits: inventoryChanges.RegularCredits ?? 0 + }); + + 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`); + } + } + // e.g. H-09 Apex Turret Sumdali + if (si.DROP_MISC_ITEM) { + const resourceDroptable = droptables.find(x => x.type == "resource"); + if (resourceDroptable) { + for (let i = 0; i != si.DROP_MISC_ITEM.length; ++i) { + const reward = getRandomReward(resourceDroptable.items)!; + logger.debug(`stripped droptable (resources pool) rolled`, reward); + if (Object.keys(await addItem(inventory, reward.type)).length == 0) { + logger.debug(`item already owned, skipping`); + } else { + MissionRewards.push({ + StoreItem: toStoreItem(reward.type), + ItemCount: 1, + FromEnemyCache: true // to show "identified" + }); + } + } + } else { + logger.error(`unknown droptable ${si.DropTable} for DROP_MISC_ITEM`); + } + } + + if (si.DropTable == "/Lotus/Types/DropTables/ContainerDropTables/VoidVaultMissionRewardsDropTable") { + // Consume netracells search pulse; only when the container reward was picked up. Discussed in https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2673 + updateEntratiVault(inventory); + inventory.EntratiVaultCountLastPeriod! += 1; + } + } + } + + 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) { + const manifest = getNemesisManifest(inventory.Nemesis.manifest); + if (inventory.Nemesis.Faction != "FC_INFESTATION") { + inventory.Nemesis.Rank = Math.min(inventory.Nemesis.Rank + 1, manifest.systemIndexes.length - 1); + inventoryChanges.Nemesis.Rank = inventory.Nemesis.Rank; + } + inventory.Nemesis.InfNodes = getInfNodes(manifest, inventory.Nemesis.Rank); + } + + if (inventory.Nemesis.Faction == "FC_INFESTATION") { + inventory.Nemesis.MissionCount += 1; + inventory.Nemesis.HenchmenKilled = Math.min(inventory.Nemesis.HenchmenKilled + 5, 95); // 5 progress per mission until 95 + + inventoryChanges.Nemesis.MissionCount ??= 0; + inventoryChanges.Nemesis.MissionCount += 1; + inventoryChanges.Nemesis.HenchmenKilled ??= 0; + inventoryChanges.Nemesis.HenchmenKilled = inventory.Nemesis.HenchmenKilled; + } + + inventoryChanges.Nemesis.InfNodes = inventory.Nemesis.InfNodes; + } + } + + AffiliationMods ??= []; + + if (rewardInfo.JobStage != undefined && rewardInfo.jobId) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [jobType, unkIndex, hubNode, syndicateMissionId] = rewardInfo.jobId.split("_"); + const syndicateMissions: ISyndicateMissionInfo[] = []; + if (syndicateMissionId) { + pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId)); + } + let syndicateEntry: ISyndicateMissionInfo | IGoal | undefined = syndicateMissions.find( + m => m._id.$oid === syndicateMissionId + ); + if ( + [ + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBounty", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBounty" + ].some(prefix => jobType.startsWith(prefix)) + ) { + const { Goals } = getWorldState(undefined); + syndicateEntry = Goals.find(m => m._id.$oid === syndicateMissionId); + if (syndicateEntry) syndicateEntry.Tag = syndicateEntry.JobAffiliationTag!; + } + if (syndicateEntry && syndicateEntry.Jobs) { + let currentJob = syndicateEntry.Jobs[rewardInfo.JobTier!]; + if ( + [ + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBounty", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBounty" + ].some(prefix => jobType.startsWith(prefix)) + ) { + currentJob = syndicateEntry.Jobs.find(j => j.jobType === jobType)!; + } + if (syndicateEntry.Tag === "EntratiSyndicate") { + if ( + [ + "DeimosRuinsExterminateBounty", + "DeimosRuinsEscortBounty", + "DeimosRuinsMistBounty", + "DeimosRuinsPurifyBounty", + "DeimosRuinsSacBounty", + "VaultBounty" + ].some(ending => jobType.endsWith(ending)) + ) { + const vault = syndicateEntry.Jobs.find(j => j.locationTag == rewardInfo.jobId!.split("_").at(-1)); + if (vault) { + currentJob = vault; + if (jobType.endsWith("VaultBounty")) { + currentJob.xpAmounts[rewardInfo.JobTier!] = currentJob.xpAmounts.reduce((s, a) => s + a, 0); + } + } + } + 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)); + } + } + if (typeof medallionAmount === "number" && !isNaN(medallionAmount)) { + await addItem(inventory, "/Lotus/Types/Items/Deimos/EntratiFragmentUncommonB", medallionAmount); + MissionRewards.push({ + StoreItem: "/Lotus/StoreItems/Types/Items/Deimos/EntratiFragmentUncommonB", + ItemCount: medallionAmount + }); + SyndicateXPItemReward = medallionAmount; + logger.debug( + `Giving ${medallionAmount} medallions for the ${rewardInfo.JobStage} stage of the ${rewardInfo.JobTier} tier bounty` + ); + } else { + logger.warning( + `${jobType} tried to give ${medallionAmount} medallions for the ${rewardInfo.JobStage} stage of the ${rewardInfo.JobTier} tier bounty` + ); + logger.warning(`currentJob`, { currentJob: currentJob }); + } + } else { + const specialCase = [ + { endings: ["Heists/HeistProfitTakerBountyOne"], stage: 2, amount: 1000 }, + { endings: ["Hunts/AllTeralystsHunt"], stage: 2, amount: 5000 }, + { + endings: [ + "Hunts/TeralystHunt", + "Heists/HeistProfitTakerBountyTwo", + "Heists/HeistProfitTakerBountyThree", + "Heists/HeistProfitTakerBountyFour", + "Heists/HeistExploiterBountyOne" + ], + amount: 1000 + } + ]; + const specialCaseReward = specialCase.find( + rule => + rule.endings.some(e => jobType.endsWith(e)) && + (rule.stage === undefined || rewardInfo.JobStage === rule.stage) + ); + + if (specialCaseReward) { + addStanding(inventory, syndicateEntry.Tag, specialCaseReward.amount, AffiliationMods); + } else { + addStanding( + inventory, + syndicateEntry.Tag, + Math.floor(currentJob.xpAmounts[rewardInfo.JobStage] / (rewardInfo.Q ? 0.8 : 1)), + AffiliationMods + ); + } + } + } + if (jobType == "/Lotus/Types/Gameplay/Eidolon/Jobs/NewbieJob") { + addStanding(inventory, "CetusSyndicate", Math.floor(200 / (rewardInfo.Q ? 0.8 : 1)), AffiliationMods); + } + } + + if (rewardInfo.challengeMissionId) { + const [syndicateTag, tierStr, chemistryBuddyStr] = rewardInfo.challengeMissionId.split("_"); + const tier = Number(tierStr); + const chemistryBuddy = Number(chemistryBuddyStr); + 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; + logger.debug(`Giving ${medallionAmount} medallions for the ${tier} tier bounty`); + } else { + let standingAmount = (tier + 1) * 1000; + if (tier > 5) standingAmount = 7500; // InfestedLichBounty + if (isSteelPath) standingAmount *= 1.5; + addStanding(inventory, syndicateTag, standingAmount, AffiliationMods); + } + if (syndicateTag == "HexSyndicate" && tier < 6) { + const buddy = chemistryBuddies[chemistryBuddy]; + const dialogue = getDialogue(inventory, buddy); + if (Date.now() >= dialogue.BountyChemExpiry.getTime()) { + logger.debug(`Giving 20 chemistry for ${buddy}`); + const tomorrowAt0Utc = inventory.noKimCooldowns + ? Date.now() + : (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000; + dialogue.Chemistry += 20; + dialogue.BountyChemExpiry = new Date(tomorrowAt0Utc); + } else { + logger.debug(`Already got today's chemistry for ${buddy}`); + } + } + 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 + }; +}; + +export const addCredits = async ( + account: TAccountDocument, + inventory: TInventoryDatabaseDocument, + { + missionDropCredits, + missionCompletionCredits, + rngRewardCredits + }: { missionDropCredits: number; missionCompletionCredits: number; rngRewardCredits: number } +): Promise => { + const finalCredits: IMissionCredits = { + MissionCredits: [missionDropCredits, missionDropCredits], + CreditsBonus: [missionCompletionCredits, missionCompletionCredits], + TotalCredits: [0, 0] + }; + + const today = Math.trunc(Date.now() / 86400000) * 86400; + if (account.DailyFirstWinDate != today) { + account.DailyFirstWinDate = today; + await account.save(); + + logger.debug(`daily first win, doubling missionCompletionCredits (${missionCompletionCredits})`); + + finalCredits.DailyMissionBonus = true; + inventory.RegularCredits += missionCompletionCredits; + finalCredits.CreditsBonus[1] *= 2; + } + + const totalCredits = finalCredits.MissionCredits[1] + finalCredits.CreditsBonus[1] + rngRewardCredits; + finalCredits.TotalCredits = [totalCredits, totalCredits]; + + if (config.worldState?.creditBoost) { + inventory.RegularCredits += finalCredits.TotalCredits[1]; + finalCredits.TotalCredits[1] += finalCredits.TotalCredits[1]; + } + const now = Math.trunc(Date.now() / 1000); // TOVERIFY: Should we maybe subtract mission time as to apply credit boosters that expired during mission? + if ((inventory.Boosters.find(x => x.ItemType == "/Lotus/Types/Boosters/CreditBooster")?.ExpiryDate ?? 0) > now) { + inventory.RegularCredits += finalCredits.TotalCredits[1]; + finalCredits.TotalCredits[1] += finalCredits.TotalCredits[1]; + } + if ((inventory.Boosters.find(x => x.ItemType == "/Lotus/Types/Boosters/CreditBlessing")?.ExpiryDate ?? 0) > now) { + inventory.RegularCredits += finalCredits.TotalCredits[1]; + finalCredits.TotalCredits[1] += finalCredits.TotalCredits[1]; + } + + return finalCredits; +}; + +export const addFixedLevelRewards = ( + rewards: IMissionRewardExternal, + MissionRewards: IMissionReward[], + rewardInfo?: IRewardInfo +): number => { + let missionBonusCredits = 0; + if (rewards.credits) { + missionBonusCredits += 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: toStoreItem(item.ItemType), + 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( + 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]; + 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 == 17) { + if (mission?.Tier == 1) { + logger.warn(`non-steel path duviri murmur tier used on steel path?!`); + } + if (config.worldState?.eightClaw) { + drops.push({ + StoreItem: "/Lotus/StoreItems/Types/Gameplay/DuviriMITW/Resources/DuviriMurmurItemEvent", + ItemCount: 10 + }); + } + drops.push({ + StoreItem: "/Lotus/StoreItems/Types/Gameplay/Duviri/Resource/DuviriDragonDropItem", + ItemCount: 10 + }); + rewardManifests = ["/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriMurmurFinalChestRewards"]; + } else if (RewardInfo.T == 19) { + if (config.worldState?.eightClaw) { + drops.push({ + StoreItem: "/Lotus/StoreItems/Types/Gameplay/DuviriMITW/Resources/DuviriMurmurItemEvent", + ItemCount: 15 + }); + } + drops.push({ + StoreItem: "/Lotus/StoreItems/Types/Gameplay/Duviri/Resource/DuviriDragonDropItem", + ItemCount: 15 + }); + rewardManifests = [ + "/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriMurmurFinalSteelChestRewards" + ]; + } else if ( + RewardInfo.T == 70 || + RewardInfo.T == 6 // https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2526 + ) { + // 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.jobId) { + if (RewardInfo.JobStage! >= 0) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [jobType, unkIndex, hubNode, syndicateMissionId] = RewardInfo.jobId.split("_"); + let isEndlessJob = false; + if (syndicateMissionId) { + const syndicateMissions: ISyndicateMissionInfo[] = []; + if (syndicateMissionId) { + pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId)); + } + let syndicateEntry: ISyndicateMissionInfo | IGoal | undefined = syndicateMissions.find( + m => m._id.$oid === syndicateMissionId + ); + if ( + [ + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBounty", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBounty" + ].some(prefix => jobType.startsWith(prefix)) + ) { + const { Goals } = getWorldState(undefined); + syndicateEntry = Goals.find(m => m._id.$oid === syndicateMissionId); + if (syndicateEntry) syndicateEntry.Tag = syndicateEntry.JobAffiliationTag!; + } + if (syndicateEntry && syndicateEntry.Jobs) { + let job = syndicateEntry.Jobs[RewardInfo.JobTier!]; + + if (syndicateEntry.Tag === "EntratiSyndicate") { + if ( + [ + "DeimosRuinsExterminateBounty", + "DeimosRuinsEscortBounty", + "DeimosRuinsMistBounty", + "DeimosRuinsPurifyBounty", + "DeimosRuinsSacBounty", + "VaultBounty" + ].some(ending => jobType.endsWith(ending)) + ) { + const vault = syndicateEntry.Jobs.find( + j => j.locationTag === RewardInfo.jobId!.split("_").at(-1) + ); + if (vault) { + job = vault; + if (jobType.endsWith("VaultBounty")) { + job.rewards = job.rewards.replace( + "/Lotus/Types/Game/MissionDecks/", + "/Supplementals/" + ); + job.xpAmounts = [job.xpAmounts.reduce((partialSum, a) => partialSum + a, 0)]; + } + } + } + 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; + } + } + } + } + if ( + [ + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBounty", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBounty" + ].some(prefix => jobType.startsWith(prefix)) + ) { + job = syndicateEntry.Jobs.find(j => j.jobType === jobType)!; + } + rewardManifests = [job.rewards]; + if (job.xpAmounts.length > 1) { + const curentStage = RewardInfo.JobStage! + 1; + const totalStage = job.xpAmounts.length; + let tableIndex = 1; // Stage 2, Stage 3 of 4, and Stage 3 of 5 + + if (curentStage == 1) { + tableIndex = 0; + } else if (curentStage == totalStage) { + tableIndex = 3; + } else if (totalStage == 5 && curentStage == 4) { + tableIndex = 2; + } + + rotations = [tableIndex]; + } else { + rotations = [0]; + } + if ( + RewardInfo.Q && + (RewardInfo.JobStage === job.xpAmounts.length - 1 || jobType.endsWith("VaultBounty")) && + !isEndlessJob + ) { + rotations.push(ExportRewards[job.rewards].length - 1); + } + } + } + if (jobType == "/Lotus/Types/Gameplay/Eidolon/Jobs/NewbieJob") { + rewardManifests = ["/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierATableARewards"]; + rotations = [3]; + if (RewardInfo.Q) rotations.push(3); + } + } + } 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 }); + } + } + }); + + // Railjack Abandoned Cache Rewards, Rotation A (Mandatory Objectives) + if (RewardInfo.POICompletions) { + if (region.cacheRewardManifest) { + const deck = ExportRewards[region.cacheRewardManifest]; + for (let cache = 0; cache != RewardInfo.POICompletions; ++cache) { + const drop = getRandomRewardByChance(deck[0]); + if (drop) { + drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount, FromEnemyCache: true }); + } + } + } else { + logger.error(`POI completed, but there was no cache reward manifest at ${RewardInfo.node}`); + } + } + + // Railjack Abandoned Cache Rewards, Rotation B (Optional Objectives) + if (RewardInfo.LootDungeonCompletions) { + if (region.cacheRewardManifest) { + const deck = ExportRewards[region.cacheRewardManifest]; + for (let cache = 0; cache != RewardInfo.LootDungeonCompletions; ++cache) { + const drop = getRandomRewardByChance(deck[1]); + if (drop) { + drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount, FromEnemyCache: true }); + } + } + } else { + logger.error(`Loot dungeon completed, but there was no cache reward manifest at ${RewardInfo.node}`); + } + } + + 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({ StoreItem: drop.type, ItemCount: drop.itemCount, FromEnemyCache: true }); + } + } + } + + if (RewardInfo.nightmareMode) { + const deck = ExportRewards["/Lotus/Types/Game/MissionDecks/NightmareModeRewards"]; + let rotation = 0; + + if (region.missionIndex === 3 && RewardInfo.rewardTier) { + rotation = RewardInfo.rewardTier; + } else if ([6, 7, 8, 10, 11].includes(region.systemIndex)) { + rotation = 2; + } else if ([4, 9, 12, 14, 15, 16, 17, 18].includes(region.systemIndex)) { + rotation = 1; + } + + const drop = getRandomRewardByChance(deck[rotation]); + if (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 }); + } + } + + if (inventory.missionsCanGiveAllRelics) { + for (const drop of drops) { + const itemType = fromStoreItem(drop.StoreItem); + if (itemType in ExportRelics) { + const relic = ExportRelics[itemType]; + const replacement = getRandomElement( + Object.entries(ExportRelics).filter( + arr => arr[1].era == relic.era && arr[1].quality == relic.quality + ) + )!; + logger.debug(`replacing ${relic.era} ${relic.category} with ${replacement[1].category}`); + drop.StoreItem = toStoreItem(replacement[0]); + } + } + } + + 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 chemistryBuddies: readonly 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 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 }; +};*/ + +const goalMessagesByKey: Record = { + "/Lotus/Types/Keys/GalleonRobberyAlert": { + sndr: "/Lotus/Language/Bosses/BossCouncilorVayHek", + msg: "/Lotus/Language/Messages/GalleonRobbery2025RewardMsgA", + sub: "/Lotus/Language/Messages/GalleonRobbery2025MissionTitleA", + icon: "/Lotus/Interface/Icons/Npcs/VayHekPortrait.png" + }, + "/Lotus/Types/Keys/GalleonRobberyAlertB": { + sndr: "/Lotus/Language/Bosses/BossCouncilorVayHek", + msg: "/Lotus/Language/Messages/GalleonRobbery2025RewardMsgB", + sub: "/Lotus/Language/Messages/GalleonRobbery2025MissionTitleB", + icon: "/Lotus/Interface/Icons/Npcs/VayHekPortrait.png" + }, + "/Lotus/Types/Keys/GalleonRobberyAlertC": { + sndr: "/Lotus/Language/Bosses/BossCouncilorVayHek", + msg: "/Lotus/Language/Messages/GalleonRobbery2025RewardMsgC", + sub: "/Lotus/Language/Messages/GalleonRobbery2025MissionTitleC", + icon: "/Lotus/Interface/Icons/Npcs/VayHekPortrait.png" + }, + "/Lotus/Types/Keys/TacAlertKeyWaterFightA": { + sndr: "/Lotus/Language/Bosses/BossKelaDeThaym", + msg: "/Lotus/Language/Inbox/WaterFightRewardMsgA", + sub: "/Lotus/Language/Inbox/WaterFightRewardSubjectA", + icon: "/Lotus/Interface/Icons/Npcs/Grineer/KelaDeThaym.png" + }, + "/Lotus/Types/Keys/TacAlertKeyWaterFightB": { + sndr: "/Lotus/Language/Bosses/BossKelaDeThaym", + msg: "/Lotus/Language/Inbox/WaterFightRewardMsgB", + sub: "/Lotus/Language/Inbox/WaterFightRewardSubjectB", + icon: "/Lotus/Interface/Icons/Npcs/Grineer/KelaDeThaym.png" + }, + "/Lotus/Types/Keys/TacAlertKeyWaterFightC": { + sndr: "/Lotus/Language/Bosses/BossKelaDeThaym", + msg: "/Lotus/Language/Inbox/WaterFightRewardMsgC", + sub: "/Lotus/Language/Inbox/WaterFightRewardSubjectC", + icon: "/Lotus/Interface/Icons/Npcs/Grineer/KelaDeThaym.png" + }, + "/Lotus/Types/Keys/TacAlertKeyWaterFightD": { + sndr: "/Lotus/Language/Bosses/BossKelaDeThaym", + msg: "/Lotus/Language/Inbox/WaterFightRewardMsgD", + sub: "/Lotus/Language/Inbox/WaterFightRewardSubjectD", + icon: "/Lotus/Interface/Icons/Npcs/Grineer/KelaDeThaym.png" + }, + "/Lotus/Types/Keys/WolfTacAlertReduxA": { + sndr: "/Lotus/Language/Bosses/NoraNight", + msg: "/Lotus/Language/Inbox/WolfTacAlertBody", + sub: "/Lotus/Language/Inbox/WolfTacAlertTitle", + icon: "/Lotus/Interface/Icons/Npcs/Seasonal/NoraNight.png" + }, + "/Lotus/Types/Keys/WolfTacAlertReduxB": { + sndr: "/Lotus/Language/Bosses/NoraNight", + msg: "/Lotus/Language/Inbox/WolfTacAlertBody", + sub: "/Lotus/Language/Inbox/WolfTacAlertTitle", + icon: "/Lotus/Interface/Icons/Npcs/Seasonal/NoraNight.png" + }, + "/Lotus/Types/Keys/WolfTacAlertReduxD": { + sndr: "/Lotus/Language/Bosses/NoraNight", + msg: "/Lotus/Language/Inbox/WolfTacAlertBody", + sub: "/Lotus/Language/Inbox/WolfTacAlertTitle", + icon: "/Lotus/Interface/Icons/Npcs/Seasonal/NoraNight.png" + }, + "/Lotus/Types/Keys/WolfTacAlertReduxC": { + sndr: "/Lotus/Language/Bosses/NoraNight", + msg: "/Lotus/Language/Inbox/WolfTacAlertBody", + sub: "/Lotus/Language/Inbox/WolfTacAlertTitle", + icon: "/Lotus/Interface/Icons/Npcs/Seasonal/NoraNight.png" + }, + "/Lotus/Types/Keys/LanternEndlessEventKeyA": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/G1Quests/GenericEventRewardMsgDesc", + sub: "/Lotus/Language/G1Quests/GenericTacAlertRewardMsgTitle", + icon: "/Lotus/Interface/Icons/Npcs/LotusVamp_d.png" + }, + "/Lotus/Types/Keys/LanternEndlessEventKeyB": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/G1Quests/GenericEventRewardMsgDesc", + sub: "/Lotus/Language/G1Quests/GenericTacAlertRewardMsgTitle", + icon: "/Lotus/Interface/Icons/Npcs/LotusVamp_d.png" + }, + "/Lotus/Types/Keys/LanternEndlessEventKeyD": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/G1Quests/GenericEventRewardMsgDesc", + sub: "/Lotus/Language/G1Quests/GenericTacAlertRewardMsgTitle", + icon: "/Lotus/Interface/Icons/Npcs/LotusVamp_d.png" + }, + "/Lotus/Types/Keys/LanternEndlessEventKeyC": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/G1Quests/GenericEventRewardMsgDesc", + sub: "/Lotus/Language/G1Quests/GenericTacAlertRewardMsgTitle", + icon: "/Lotus/Interface/Icons/Npcs/LotusVamp_d.png" + }, + "/Lotus/Types/Keys/TacAlertKeyHalloween": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/G1Quests/TacAlertHalloweenRewardsBonusBody", + sub: "/Lotus/Language/G1Quests/TacAlertHalloweenRewardsBonusTitle", + icon: "/Lotus/Interface/Icons/Npcs/LotusVamp_d.png" + }, + "/Lotus/Types/Keys/TacAlertKeyHalloweenBonus": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/G1Quests/TacAlertHalloweenRewardsBody", + sub: "/Lotus/Language/G1Quests/TacAlertHalloweenRewardsTitle", + icon: "/Lotus/Interface/Icons/Npcs/LotusVamp_d.png" + }, + "/Lotus/Types/Keys/TacAlertKeyHalloweenTimeAttack": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/G1Quests/TacAlertHalloweenRewardsBody", + sub: "/Lotus/Language/G1Quests/TacAlertHalloweenRewardsTitle", + icon: "/Lotus/Interface/Icons/Npcs/LotusVamp_d.png" + }, + "/Lotus/Types/Keys/TacAlertKeyProxyRebellionOne": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/G1Quests/RazorbackArmadaRewardBody", + sub: "/Lotus/Language/G1Quests/GenericTacAlertSmallRewardMsgTitle", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["CREDIT_REWARD"] + }, + "/Lotus/Types/Keys/TacAlertKeyProxyRebellionTwo": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/G1Quests/RazorbackArmadaRewardBody", + sub: "/Lotus/Language/G1Quests/GenericTacAlertSmallRewardMsgTitle", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["CREDIT_REWARD"] + }, + "/Lotus/Types/Keys/TacAlertKeyProxyRebellionThree": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/G1Quests/RazorbackArmadaRewardBody", + sub: "/Lotus/Language/G1Quests/GenericTacAlertSmallRewardMsgTitle", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["CREDIT_REWARD"] + }, + "/Lotus/Types/Keys/TacAlertKeyProxyRebellionFour": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/G1Quests/GenericTacAlertBadgeRewardMsgDesc", + sub: "/Lotus/Language/G1Quests/GenericTacAlertBadgeRewardMsgTitle", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png" + }, + "/Lotus/Types/Keys/TacAlertKeyProjectNightwatchEasy": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/G1Quests/ProjectNightwatchRewardMsgA", + sub: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertMissionOneTitle", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["CREDIT_REWARD"] + }, + "/Lotus/Types/Keys/TacAlertKeyProjectNightwatch": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertMissionRewardBody", + sub: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertMissionTwoTitle", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png" + }, + "/Lotus/Types/Keys/TacAlertKeyProjectNightwatchHard": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertMissionRewardBody", + sub: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertMissionThreeTitle", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png" + }, + "/Lotus/Types/Keys/TacAlertKeyProjectNightwatchBonus": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertMissionRewardBody", + sub: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertMissionFourTitle", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png" + }, + "/Lotus/Types/Keys/MechSurvivalCorpusShip": { + sndr: "/Lotus/Language/Bosses/DeimosFather", + msg: "/Lotus/Language/Inbox/MechEvent2020Tier1CompleteDesc", + sub: "/Lotus/Language/Inbox/MechEvent2020Tier1CompleteTitle", + icon: "/Lotus/Interface/Icons/Npcs/Entrati/Father.png" + }, + "/Lotus/Types/Keys/MechSurvivalGrineerGalleon": { + sndr: "/Lotus/Language/Bosses/DeimosFather", + msg: "/Lotus/Language/Inbox/MechEvent2020Tier2CompleteDesc", + sub: "/Lotus/Language/Inbox/MechEvent2020Tier2CompleteTitle", + icon: "/Lotus/Interface/Icons/Npcs/Entrati/Father.png" + }, + "/Lotus/Types/Keys/MechSurvivalGasCity": { + sndr: "/Lotus/Language/Bosses/DeimosFather", + msg: "/Lotus/Language/Inbox/MechEvent2020Tier3CompleteDesc", + sub: "/Lotus/Language/Inbox/MechEvent2020Tier3CompleteTitle", + icon: "/Lotus/Interface/Icons/Npcs/Entrati/Father.png" + }, + "/Lotus/Types/Keys/MechSurvivalCorpusShipEndurance": { + sndr: "/Lotus/Language/Bosses/DeimosFather", + msg: "/Lotus/Language/Inbox/MechEvent2020Tier3CompleteDesc", + sub: "/Lotus/Language/Inbox/MechEvent2020Tier3CompleteTitle", + icon: "/Lotus/Interface/Icons/Npcs/Entrati/Father.png" + }, + "/Lotus/Types/Keys/MechSurvivalGrineerGalleonEndurance": { + sndr: "/Lotus/Language/Bosses/DeimosFather", + msg: "/Lotus/Language/Inbox/MechEvent2020Tier3CompleteDesc", + sub: "/Lotus/Language/Inbox/MechEvent2020Tier3CompleteTitle", + icon: "/Lotus/Interface/Icons/Npcs/Entrati/Father.png" + }, + "/Lotus/Types/Keys/TacAlertKeyAnniversary2019E": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/Messages/Anniversary2024RewardMsgB", + sub: "/Lotus/Language/Messages/Anniversary2024MissionTitleB", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["PLAYER_NAME"] + }, + "/Lotus/Types/Keys/TacAlertKeyAnniversary2020F": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/Messages/Anniversary2024RewardMsgC", + sub: "/Lotus/Language/Messages/Anniversary2024MissionTitleB", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["PLAYER_NAME"] + }, + "/Lotus/Types/Keys/TacAlertKeyAnniversary2024ChallengeModeA": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/Messages/Anniversary2024RewardMsgD", + sub: "/Lotus/Language/Messages/Anniversary2024MissionTitleD", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["PLAYER_NAME"] + }, + "/Lotus/Types/Keys/TacAlertKeyAnniversary2017C": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/Messages/Anniversary2019RewardMsgC", + sub: "/Lotus/Language/Messages/Anniversary2019MissionTitleC", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["PLAYER_NAME"] + }, + "/Lotus/Types/Keys/TacAlertKeyAnniversary2020H": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/Messages/Anniversary2020RewardMsgH", + sub: "/Lotus/Language/Messages/Anniversary2020MissionTitleH", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["PLAYER_NAME"] + }, + "/Lotus/Types/Keys/TacAlertKeyAnniversary2022J": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/Messages/Anniversary2022RewardMsgJ", + sub: "/Lotus/Language/Messages/Anniversary2022MissionTitleJ", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["PLAYER_NAME"] + }, + "/Lotus/Types/Keys/TacAlertKeyAnniversary2025D": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/Messages/Anniversary2025RewardMsgB", + sub: "/Lotus/Language/Messages/Anniversary2025MissionTitleB", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["PLAYER_NAME"] + }, + "/Lotus/Types/Keys/TacAlertKeyAnniversary2025ChallengeModeA": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/Messages/Anniversary2025RewardMsgC", + sub: "/Lotus/Language/Messages/Anniversary2025MissionTitleC", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["PLAYER_NAME"] + }, + "/Lotus/Types/Keys/TacAlertKeyAnniversary2020G": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/Messages/Anniversary2020RewardMsgG", + sub: "/Lotus/Language/Messages/Anniversary2020MissionTitleG", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["PLAYER_NAME"] + }, + "/Lotus/Types/Keys/TacAlertKeyAnniversary2017B": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/Messages/Anniversary2019RewardMsgB", + sub: "/Lotus/Language/Messages/Anniversary2019MissionTitleB", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["PLAYER_NAME"] + }, + "/Lotus/Types/Keys/TacAlertKeyAnniversary2017A": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/Messages/Anniversary2019RewardMsgA", + sub: "/Lotus/Language/Messages/Anniversary2019MissionTitleA", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["PLAYER_NAME"] + }, + "/Lotus/Types/Keys/TacAlertKeyAnniversary2023K": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/Messages/Anniversary2025RewardMsgG", + sub: "/Lotus/Language/Messages/Anniversary2025MissionTitleG", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["PLAYER_NAME"] + }, + "/Lotus/Types/Keys/TacAlertKeyAnniversary2025ChallengeModeB": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/Messages/Anniversary2025RewardMsgD", + sub: "/Lotus/Language/Messages/Anniversary2025MissionTitleD", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["PLAYER_NAME"] + }, + "/Lotus/Types/Keys/TacAlertKeyAnniversary2025A": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/Messages/Anniversary2025RewardMsgA", + sub: "/Lotus/Language/Messages/Anniversary2025MissionTitleA", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["PLAYER_NAME"] + }, + "/Lotus/Types/Keys/TacAlertKeyAnniversary2018D": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/Messages/Anniversary2024RewardMsgG", + sub: "/Lotus/Language/Messages/Anniversary2024MissionTitleG", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["PLAYER_NAME"] + }, + "/Lotus/Types/Keys/TacAlertKeyAnniversary2025C": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/Messages/Anniversary2024RewardMsgF", + sub: "/Lotus/Language/Messages/Anniversary2024MissionTitleF", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["PLAYER_NAME"] + }, + "/Lotus/Types/Keys/TacAlertKeyAnniversary2024L": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/Messages/Anniversary2024RewardMsgA", + sub: "/Lotus/Language/Messages/Anniversary2024MissionTitleA", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["PLAYER_NAME"] + }, + "/Lotus/Types/Keys/TacAlertKeyAnniversary2024ChallengeModeB": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/Messages/Anniversary2024RewardMsgE", + sub: "/Lotus/Language/Messages/Anniversary2024MissionTitleE", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["PLAYER_NAME"] + }, + "/Lotus/Types/Keys/TacAlertKeyAnniversary2021I": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/Messages/Anniversary2024RewardMsgH", + sub: "/Lotus/Language/Messages/Anniversary2024MissionTitleH", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["PLAYER_NAME"] + }, + "/Lotus/Types/Keys/TacAlertKeyAnniversary2025B": { + sndr: "/Lotus/Language/Bosses/Lotus", + msg: "/Lotus/Language/Messages/Anniversary2025RewardMsgE", + sub: "/Lotus/Language/Messages/Anniversary2025MissionTitleE", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + arg: ["PLAYER_NAME"] + } +}; + +const goalMessagesByTag: Record = { + HeatFissure: [ + { + sndr: "/Lotus/Language/Npcs/Eudico", + msg: "/Lotus/Language/Messages/OrbHeistEventRewardAInboxMessageBody", + sub: "/Lotus/Language/Messages/OrbHeistEventRewardAInboxMessageTitle", + icon: "/Lotus/Interface/Icons/Npcs/Eudico.png" + }, + { + sndr: "/Lotus/Language/Npcs/Eudico", + msg: "/Lotus/Language/Messages/OrbHeistEventRewardBInboxMessageBody", + sub: "/Lotus/Language/Messages/OrbHeistEventRewardBInboxMessageTitle", + icon: "/Lotus/Interface/Icons/Npcs/Eudico.png" + }, + { + sndr: "/Lotus/Language/Npcs/Eudico", + msg: "/Lotus/Language/Messages/OrbHeistEventRewardCInboxMessageBody", + sub: "/Lotus/Language/Messages/OrbHeistEventRewardCInboxMessageTitle", + icon: "/Lotus/Interface/Icons/Npcs/Eudico.png" + }, + { + sndr: "/Lotus/Language/Npcs/Eudico", + msg: "/Lotus/Language/Messages/OrbHeistEventRewardDInboxMessageBody", + sub: "/Lotus/Language/Messages/OrbHeistEventRewardDInboxMessageTitle", + icon: "/Lotus/Interface/Icons/Npcs/Eudico.png" + }, + { + sndr: "/Lotus/Language/Npcs/Eudico", + msg: "/Lotus/Language/Messages/OrbHeistEventRewardEInboxMessageBody", + sub: "/Lotus/Language/Messages/OrbHeistEventRewardEInboxMessageTitle", + icon: "/Lotus/Interface/Icons/Npcs/Eudico.png" + } + ] +}; diff --git a/src/services/personalRoomsService.ts b/src/services/personalRoomsService.ts new file mode 100644 index 00000000..56c32af9 --- /dev/null +++ b/src/services/personalRoomsService.ts @@ -0,0 +1,92 @@ +import { PersonalRooms } from "../models/personalRoomsModel.ts"; +import { addItem, getInventory } from "./inventoryService.ts"; +import type { IGardeningDatabase, TPersonalRoomsDatabaseDocument } from "../types/personalRoomsTypes.ts"; +import { getRandomElement } from "./rngService.ts"; + +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}`); + } + return personalRooms; +}; + +export const updateShipFeature = async (accountId: string, shipFeature: string): Promise => { + const personalRooms = await getPersonalRooms(accountId); + + if (personalRooms.Ship.Features.includes(shipFeature)) { + throw new Error(`ship feature ${shipFeature} already unlocked`); + } + + personalRooms.Ship.Features.push(shipFeature); + await personalRooms.save(); + + const inventory = await getInventory(accountId); + 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 new file mode 100644 index 00000000..4d7a0cf8 --- /dev/null +++ b/src/services/purchaseService.ts @@ -0,0 +1,679 @@ +import { parseSlotPurchaseName, slotPurchaseNameToSlotName } from "../helpers/purchaseHelpers.ts"; +import { getSubstringFromKeyword } from "../helpers/stringHelpers.ts"; +import { + addBooster, + addItem, + addMiscItems, + combineInventoryChanges, + updateCurrency, + updateSlots +} from "./inventoryService.ts"; +import { getRandomReward, getRandomWeightedRewardUc } from "./rngService.ts"; +import { applyStandingToVendorManifest, getVendorManifestByOid } from "./serversideVendorsService.ts"; +import type { IMiscItem } from "../types/inventoryTypes/inventoryTypes.ts"; +import type { + IPurchaseRequest, + IPurchaseResponse, + IInventoryChanges, + IPurchaseParams +} from "../types/purchaseTypes.ts"; +import { PurchaseSource } from "../types/purchaseTypes.ts"; +import { logger } from "../utils/logger.ts"; +import { getWorldState } from "./worldStateService.ts"; +import type { TRarity } from "warframe-public-export-plus"; +import { + ExportBoosterPacks, + ExportBoosters, + ExportBundles, + ExportGear, + ExportMisc, + ExportResources, + ExportSyndicates, + ExportVendors +} from "warframe-public-export-plus"; +import type { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel.ts"; +import { fromStoreItem, toStoreItem } from "./itemDataService.ts"; +import { DailyDeal } from "../models/worldStateModel.ts"; +import { fromMongoDate, toMongoDate } from "../helpers/inventoryHelpers.ts"; +import { Guild } from "../models/guildModel.ts"; +import { handleGuildGoalProgress } from "./guildService.ts"; +import { Types } from "mongoose"; + +export const getStoreItemCategory = (storeItem: string): string => { + const storeItemString = getSubstringFromKeyword(storeItem, "StoreItems/"); + const storeItemElements = storeItemString.split("/"); + return storeItemElements[1]; +}; + +export const getStoreItemTypesCategory = (typesItem: string): string => { + const typesString = getSubstringFromKeyword(typesItem, "Types"); + const typeElements = typesString.split("/"); + if (typesItem.includes("StoreItems")) { + return typeElements[2]; + } + return typeElements[1]; +}; + +const tallyVendorPurchase = ( + inventory: TInventoryDatabaseDocument, + inventoryChanges: IInventoryChanges, + VendorType: string, + ItemId: string, + numPurchased: number, + Expiry: Date +): void => { + if (!inventory.noVendorPurchaseLimits) { + inventory.RecentVendorPurchases ??= []; + let vendorPurchases = inventory.RecentVendorPurchases.find(x => x.VendorType == VendorType); + if (!vendorPurchases) { + vendorPurchases = + inventory.RecentVendorPurchases[ + inventory.RecentVendorPurchases.push({ + VendorType: VendorType, + PurchaseHistory: [] + }) - 1 + ]; + } + let historyEntry = vendorPurchases.PurchaseHistory.find(x => x.ItemId == ItemId); + if (historyEntry) { + if (Date.now() >= historyEntry.Expiry.getTime()) { + historyEntry.NumPurchased = numPurchased; + historyEntry.Expiry = Expiry; + } else { + historyEntry.NumPurchased += numPurchased; + } + } else { + historyEntry = + vendorPurchases.PurchaseHistory[ + vendorPurchases.PurchaseHistory.push({ + ItemId: ItemId, + NumPurchased: numPurchased, + Expiry: Expiry + }) - 1 + ]; + } + inventoryChanges.NewVendorPurchase = { + VendorType: VendorType, + PurchaseHistory: [ + { + ItemId: ItemId, + NumPurchased: historyEntry.NumPurchased, + Expiry: toMongoDate(Expiry) + } + ] + }; + inventoryChanges.RecentVendorPurchases = inventoryChanges.NewVendorPurchase; + } +}; + +export const handlePurchase = async ( + purchaseRequest: IPurchaseRequest, + inventory: TInventoryDatabaseDocument +): Promise => { + logger.debug("purchase request", purchaseRequest); + + const prePurchaseInventoryChanges: IInventoryChanges = {}; + let seed: bigint | undefined; + if (purchaseRequest.PurchaseParams.Source == PurchaseSource.Vendor) { + let manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!); + if (manifest) { + 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 ? ItemId : purchaseRequest.PurchaseParams.StoreItem}`); + } + if (!inventory.dontSubtractPurchaseCreditCost) { + if (offer.RegularPrice) { + updateCurrency(inventory, offer.RegularPrice[0], false, prePurchaseInventoryChanges); + } + } + if (!inventory.dontSubtractPurchasePlatinumCost) { + if (offer.PremiumPrice) { + updateCurrency(inventory, offer.PremiumPrice[0], true, prePurchaseInventoryChanges); + } + } + if ( + inventory.GuildId && + offer.ItemPrices && + manifest.VendorInfo.TypeName == + "/Lotus/Types/Game/VendorManifests/Events/DuviriMurmurInvasionVendorManifest" + ) { + const guild = await Guild.findById(inventory.GuildId, "GoalProgress Tier VaultDecoRecipes"); + const goal = getWorldState().Goals.find(x => x.Tag == "DuviriMurmurEvent"); + if (guild && goal) { + await handleGuildGoalProgress(guild, { + Count: offer.ItemPrices[0].ItemCount * purchaseRequest.PurchaseParams.Quantity, + Tag: goal.Tag, + goalId: new Types.ObjectId(goal._id.$oid) + }); + } + } + if (!inventory.dontSubtractPurchaseItemCost) { + if (offer.ItemPrices) { + handleItemPrices( + inventory, + offer.ItemPrices, + purchaseRequest.PurchaseParams.Quantity, + prePurchaseInventoryChanges + ); + } + } + if (offer.LocTagRandSeed !== undefined) { + seed = BigInt(offer.LocTagRandSeed); + } + if (ItemId) { + 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; + } + tallyVendorPurchase( + inventory, + prePurchaseInventoryChanges, + manifest.VendorInfo.TypeName, + ItemId, + purchaseRequest.PurchaseParams.Quantity, + new Date(expiry) + ); + } + purchaseRequest.PurchaseParams.Quantity *= offer.QuantityMultiplier; + } 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, + undefined, + false, + purchaseRequest.PurchaseParams.UsePremium, + seed + ); + combineInventoryChanges(purchaseResponse.InventoryChanges, prePurchaseInventoryChanges); + + updateCurrency( + inventory, + purchaseRequest.PurchaseParams.ExpectedPrice, + purchaseRequest.PurchaseParams.UsePremium, + prePurchaseInventoryChanges + ); + + switch (purchaseRequest.PurchaseParams.Source) { + case PurchaseSource.VoidTrader: { + const worldState = getWorldState(); + 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) { + if (!inventory.dontSubtractPurchaseCreditCost) { + updateCurrency(inventory, offer.RegularPrice, false, purchaseResponse.InventoryChanges); + } + if (purchaseRequest.PurchaseParams.ExpectedPrice) { + throw new Error(`vendor purchase should not have an expected price`); + } + + if (offer.PrimePrice && !inventory.dontSubtractPurchaseItemCost) { + 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); + } + + if (offer.Limit) { + tallyVendorPurchase( + inventory, + purchaseResponse.InventoryChanges, + "VoidTrader", + offer.ItemType, + purchaseRequest.PurchaseParams.Quantity, + fromMongoDate(worldState.VoidTraders[0].Expiry) + ); + } + } + break; + } + case PurchaseSource.SyndicateFavor: + { + const syndicateTag = purchaseRequest.PurchaseParams.SyndicateTag!; + if (purchaseRequest.PurchaseParams.UseFreeFavor!) { + const affiliation = inventory.Affiliations.find(x => x.Tag == syndicateTag)!; + affiliation.FreeFavorsUsed ??= []; + const lastTitle = affiliation.FreeFavorsEarned![affiliation.FreeFavorsUsed.length]; + affiliation.FreeFavorsUsed.push(lastTitle); + purchaseResponse.FreeFavorsUsed = [ + { + Tag: syndicateTag, + Title: lastTitle + } + ]; + } else if (!inventory.dontSubtractPurchaseStandingCost) { + 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 + ); + if (favour) { + const affiliation = inventory.Affiliations.find(x => x.Tag == syndicateTag); + if (affiliation) { + purchaseResponse.Standing = [ + { + Tag: syndicateTag, + Standing: favour.standingCost * purchaseRequest.PurchaseParams.Quantity + } + ]; + affiliation.Standing -= favour.standingCost * purchaseRequest.PurchaseParams.Quantity; + } + } + } + } + } + break; + case PurchaseSource.DailyDeal: + if (purchaseRequest.PurchaseParams.ExpectedPrice) { + throw new Error(`daily deal purchase should not have an expected price`); + } + await handleDailyDealPurchase(inventory, purchaseRequest.PurchaseParams, purchaseResponse); + break; + case PurchaseSource.Vendor: + 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) { + if (typeof offer.credits == "number" && !inventory.dontSubtractPurchaseCreditCost) { + updateCurrency(inventory, offer.credits, false, purchaseResponse.InventoryChanges); + } + if (typeof offer.platinum == "number" && !inventory.dontSubtractPurchasePlatinumCost) { + updateCurrency(inventory, offer.platinum, true, purchaseResponse.InventoryChanges); + } + if (offer.itemPrices && !inventory.dontSubtractPurchaseItemCost) { + 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 PurchaseSource.PrimeVaultTrader: { + const worldState = getWorldState(); + if (purchaseRequest.PurchaseParams.SourceId! != worldState.PrimeVaultTraders[0]._id.$oid) { + throw new Error("invalid request source"); + } + const offer = + worldState.PrimeVaultTraders[0].Manifest.find( + x => x.ItemType == purchaseRequest.PurchaseParams.StoreItem + ) ?? + worldState.PrimeVaultTraders[0].EvergreenManifest.find( + x => x.ItemType == purchaseRequest.PurchaseParams.StoreItem + ); + if (offer) { + if (offer.RegularPrice) { + if (!inventory.dontSubtractPurchaseItemCost) { + const invItem: IMiscItem = { + ItemType: "/Lotus/Types/Items/MiscItems/SchismKey", + ItemCount: offer.RegularPrice * purchaseRequest.PurchaseParams.Quantity * -1 + }; + + addMiscItems(inventory, [invItem]); + + purchaseResponse.InventoryChanges.MiscItems ??= []; + purchaseResponse.InventoryChanges.MiscItems.push(invItem); + } + } else if (!inventory.infiniteRegalAya) { + inventory.PrimeTokens -= offer.PrimePrice! * purchaseRequest.PurchaseParams.Quantity; + + purchaseResponse.InventoryChanges.PrimeTokens ??= 0; + purchaseResponse.InventoryChanges.PrimeTokens -= + offer.PrimePrice! * purchaseRequest.PurchaseParams.Quantity; + } + } + break; + } + } + + return purchaseResponse; +}; + +const handleItemPrices = ( + inventory: TInventoryDatabaseDocument, + itemPrices: IMiscItem[], + purchaseQuantity: number, + inventoryChanges: IInventoryChanges +): void => { + for (const item of itemPrices) { + const invItem: IMiscItem = { + ItemType: item.ItemType, + ItemCount: item.ItemCount * purchaseQuantity * -1 + }; + + addMiscItems(inventory, [invItem]); + + inventoryChanges.MiscItems ??= []; + const change = inventoryChanges.MiscItems.find(x => x.ItemType == item.ItemType); + if (change) { + change.ItemCount += invItem.ItemCount; + } else { + inventoryChanges.MiscItems.push(invItem); + } + } +}; + +export const handleDailyDealPurchase = async ( + inventory: TInventoryDatabaseDocument, + purchaseParams: IPurchaseParams, + purchaseResponse: IPurchaseResponse +): Promise => { + const dailyDeal = (await DailyDeal.findOne({ StoreItem: purchaseParams.StoreItem }))!; + dailyDeal.AmountSold += 1; + await dailyDeal.save(); + + if (!inventory.dontSubtractPurchasePlatinumCost) { + updateCurrency(inventory, dailyDeal.SalePrice, true, purchaseResponse.InventoryChanges); + } + + if (!inventory.noVendorPurchaseLimits) { + inventory.UsedDailyDeals.push(purchaseParams.StoreItem); + purchaseResponse.DailyDealUsed = purchaseParams.StoreItem; + } +}; + +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, + premiumPurchase: boolean = true, + seed?: bigint +): Promise => { + let purchaseResponse = { + InventoryChanges: {} + }; + logger.debug(`handling acquision of ${storeItemName}`); + if (storeItemName in ExportBundles) { + await handleBundleAcqusition(storeItemName, inventory, quantity, purchaseResponse.InventoryChanges); + } else { + const storeCategory = getStoreItemCategory(storeItemName); + const internalName = fromStoreItem(storeItemName); + if (!ignorePurchaseQuantity) { + if (internalName in ExportGear) { + quantity *= ExportGear[internalName].purchaseQuantity || 1; + logger.debug(`factored quantity is ${quantity}`); + } else if (internalName in ExportResources) { + quantity *= ExportResources[internalName].purchaseQuantity || 1; + logger.debug(`factored quantity is ${quantity}`); + } + } + logger.debug(`store category ${storeCategory}`); + switch (storeCategory) { + default: { + purchaseResponse = { + InventoryChanges: await addItem( + inventory, + internalName, + quantity, + premiumPurchase, + seed, + undefined, + true + ) + }; + break; + } + case "Types": + purchaseResponse = await handleTypesPurchase( + internalName, + inventory, + quantity, + ignorePurchaseQuantity, + premiumPurchase, + seed + ); + break; + case "Boosters": + purchaseResponse = handleBoostersPurchase(storeItemName, inventory, durability); + break; + } + } + return purchaseResponse; +}; + +// // extra = everything above the base +2 slots (depending on slot type) +// // new slot above base = extra + 1 and slots +1 +// // new frame = slots -1 +// // number of frames = extra - slots + 2 +const handleSlotPurchase = ( + slotPurchaseNameFull: string, + inventory: TInventoryDatabaseDocument, + quantity: number, + ignorePurchaseQuantity: boolean +): IPurchaseResponse => { + logger.debug(`slot name ${slotPurchaseNameFull}`); + const slotPurchaseName = parseSlotPurchaseName( + slotPurchaseNameFull.substring(slotPurchaseNameFull.lastIndexOf("/") + 1) + ); + logger.debug(`slot purchase name ${slotPurchaseName}`); + + const slotName = slotPurchaseNameToSlotName[slotPurchaseName].name; + let slotsPurchased = quantity; + if (!ignorePurchaseQuantity) { + slotsPurchased *= slotPurchaseNameToSlotName[slotPurchaseName].purchaseQuantity; + } + + updateSlots(inventory, slotName, slotsPurchased, slotsPurchased); + + logger.debug(`added ${slotsPurchased} slot ${slotName}`); + + const inventoryChanges: IInventoryChanges = {}; + inventoryChanges[slotName] = { + count: 0, + platinum: 1, + Slots: slotsPurchased, + Extra: slotsPurchased + }; + return { InventoryChanges: inventoryChanges }; +}; + +const handleBoosterPackPurchase = async ( + typeName: string, + inventory: TInventoryDatabaseDocument, + 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}`); + } + const purchaseResponse: IPurchaseResponse = { + 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." + ); + } + const specialItemReward = pack.components.find(x => x.PityIncreaseRate); + for (let i = 0; i != quantity; ++i) { + if (specialItemReward) { + { + const normalComponents = []; + for (const comp of pack.components) { + if (!comp.PityIncreaseRate) { + const { Probability, ...rest } = comp; + normalComponents.push({ + ...rest, + probability: Probability! + }); + } + } + const result = getRandomReward(normalComponents)!; + logger.debug(`booster pack rolled`, result); + purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + ',{"lvl":0};'; + combineInventoryChanges( + purchaseResponse.InventoryChanges, + await addItem(inventory, result.Item, result.Amount) + ); + } + + if (!inventory.WeaponSkins.some(x => x.ItemType == specialItemReward.Item)) { + inventory.SpecialItemRewardAttenuation ??= []; + let atten = inventory.SpecialItemRewardAttenuation.find(x => x.Tag == specialItemReward.Item); + if (!atten) { + atten = + inventory.SpecialItemRewardAttenuation[ + inventory.SpecialItemRewardAttenuation.push({ + Tag: specialItemReward.Item, + Atten: specialItemReward.Probability! + }) - 1 + ]; + } + if (Math.random() < atten.Atten) { + purchaseResponse.BoosterPackItems += toStoreItem(specialItemReward.Item) + ',{"lvl":0};'; + combineInventoryChanges( + purchaseResponse.InventoryChanges, + await addItem(inventory, specialItemReward.Item) + ); + atten.Atten = 0; + } else { + atten.Atten += specialItemReward.PityIncreaseRate!; + } + } + } else { + const disallowedItems = new Set(); + for (let roll = 0; roll != pack.rarityWeightsPerRoll.length; ) { + const weights = pack.rarityWeightsPerRoll[roll]; + const result = getRandomWeightedRewardUc(pack.components, weights)!; + logger.debug(`booster pack rolled`, result); + 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, result.Amount) + ); + ++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, + ignorePurchaseQuantity: boolean, + premiumPurchase: boolean = true, + seed?: bigint +): Promise => { + const typeCategory = getStoreItemTypesCategory(typesName); + logger.debug(`type category ${typeCategory}`); + switch (typeCategory) { + default: + return { + InventoryChanges: await addItem(inventory, typesName, quantity, premiumPurchase, seed, undefined, true) + }; + case "BoosterPacks": + return handleBoosterPackPurchase(typesName, inventory, quantity); + case "SlotItems": + return handleSlotPurchase(typesName, inventory, quantity, ignorePurchaseQuantity); + case "CreditBundles": + return handleCreditBundlePurchase(typesName, inventory); + } +}; + +const handleBoostersPurchase = ( + boosterStoreName: string, + inventory: TInventoryDatabaseDocument, + durability: TRarity +): { InventoryChanges: IInventoryChanges } => { + if (!(boosterStoreName in ExportBoosters)) { + logger.error(`unknown booster type: ${boosterStoreName}`); + return { InventoryChanges: {} }; + } + + const ItemType = ExportBoosters[boosterStoreName].typeName; + const ExpiryDate = ExportMisc.boosterDurations[durability]; + + addBooster(ItemType, ExpiryDate, inventory); + + return { + InventoryChanges: { + Boosters: [{ ItemType, ExpiryDate }] + } + }; +}; diff --git a/src/services/questService.ts b/src/services/questService.ts new file mode 100644 index 00000000..f79eb131 --- /dev/null +++ b/src/services/questService.ts @@ -0,0 +1,377 @@ +import type { IKeyChainRequest } from "../types/requestTypes.ts"; +import { isEmptyObject } from "../helpers/general.ts"; +import type { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel.ts"; +import { createMessage } from "./inboxService.ts"; +import { addItem, addItems, addKeyChainItems, setupKahlSyndicate } from "./inventoryService.ts"; +import { fromStoreItem, getKeyChainMessage, getLevelKeyRewards } from "./itemDataService.ts"; +import type { IQuestKeyClient, IQuestKeyDatabase, IQuestStage } from "../types/inventoryTypes/inventoryTypes.ts"; +import { logger } from "../utils/logger.ts"; +import type { Types } from "mongoose"; +import { ExportKeys } from "warframe-public-export-plus"; +import { addFixedLevelRewards } from "./missionInventoryUpdateService.ts"; +import type { IInventoryChanges } from "../types/purchaseTypes.ts"; +import questCompletionItems from "../../static/fixed_responses/questCompletionRewards.json" with { type: "json" }; +import type { ITypeCount } from "../types/commonTypes.ts"; + +export interface IUpdateQuestRequest { + QuestKeys: Omit[]; + PS: string; + questCompletion: boolean; + PlayerShipEvents: unknown[]; + crossPlaySetting: string; + DoQuestReward: boolean; +} + +export const updateQuestKey = async ( + inventory: TInventoryDatabaseDocument, + questKeyUpdate: IUpdateQuestRequest["QuestKeys"] +): Promise => { + if (questKeyUpdate.length > 1) { + logger.error(`more than 1 quest key not supported`); + throw new Error("more than 1 quest key not supported"); + } + + const questKeyIndex = inventory.QuestKeys.findIndex(questKey => questKey.ItemType === questKeyUpdate[0].ItemType); + + if (questKeyIndex === -1) { + throw new Error(`quest key ${questKeyUpdate[0].ItemType} not found`); + } + + 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 = ( + inventory: TInventoryDatabaseDocument, + { KeyChain, ChainStage }: IKeyChainRequest, + questStageUpdate: IQuestStage +): void => { + const quest = inventory.QuestKeys.find(quest => quest.ItemType === KeyChain); + + if (!quest) { + throw new Error(`Quest ${KeyChain} not found in QuestKeys`); + } + + if (!quest.Progress) { + throw new Error(`Progress should always exist when giving keychain triggered items or messages`); + } + + 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) { + throw new Error(`Quest stage index mismatch: ${questStageIndex} !== ${ChainStage}`); + } + return; + } + + Object.assign(questStage, questStageUpdate); +}; + +export const addQuestKey = ( + inventory: TInventoryDatabaseDocument, + questKey: IQuestKeyDatabase +): IQuestKeyClient | undefined => { + if (inventory.QuestKeys.some(q => q.ItemType === questKey.ItemType)) { + logger.warn(`Quest key ${questKey.ItemType} already exists. It will not be added`); + return; + } + + if (questKey.ItemType == "/Lotus/Types/Keys/InfestedMicroplanetQuest/InfestedMicroplanetQuestKeyChain") { + void createMessage(inventory.accountOwnerId, [ + { + sndr: "/Lotus/Language/Bosses/Loid", + icon: "/Lotus/Interface/Icons/Npcs/Entrati/Loid.png", + sub: "/Lotus/Language/InfestedMicroplanet/DeimosIntroQuestInboxTitle", + msg: "/Lotus/Language/InfestedMicroplanet/DeimosIntroQuestInboxMessage" + } + ]); + } + + const index = inventory.QuestKeys.push(questKey); + + return inventory.QuestKeys[index - 1].toJSON(); +}; + +export const completeQuest = async (inventory: TInventoryDatabaseDocument, questKey: string): 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 War + 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 }[] = []; + inventory.RegularCredits += addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, 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 new file mode 100644 index 00000000..c5791a0d --- /dev/null +++ b/src/services/rngService.ts @@ -0,0 +1,207 @@ +import type { TRarity } from "warframe-public-export-plus"; + +export interface IRngResult { + type: string; + itemCount: number; + probability: number; +} + +export const getRandomElement = (arr: readonly T[]): T | undefined => { + return arr[Math.floor(Math.random() * arr.length)]; +}; + +// Returns a random integer between min (inclusive) and max (inclusive). +// https://stackoverflow.com/a/1527820 +export const getRandomInt = (min: number, max: number): number => { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + 1)) + min; +}; + +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; +}; + +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 = percentage * totalChance; + + let cumulativeChance = 0; + for (const item of pool) { + cumulativeChance += item.probability; + if (randomValue <= cumulativeChance) { + return item; + } + } + return pool[pool.length - 1]; +}; + +export const getRandomReward = (pool: T[]): T | undefined => { + return getRewardAtPercentage(pool, Math.random()); +}; + +export const getRandomWeightedReward = ( + 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); +}; + +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)]; + } + + randomElementPop(arr: T[]): T | undefined { + if (arr.length != 0) { + const index = this.randomInt(0, arr.length - 1); + const elm = arr[index]; + arr.splice(index, 1); + return elm; + } + return undefined; + } + + 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; + } + } + + shuffledArray(inarr: readonly T[]): T[] { + const arr = [...inarr]; + this.shuffleArray(arr); + return arr; + } +} + +export const sequentiallyUniqueRandomElement = ( + deck: readonly T[], + idx: number, + lookbehind: number, + seed: number = 0 +): T | undefined => { + // This algorithm may modify a shuffle up to index `lookbehind + 1`. It assumes that the last `lookbehind` cards are not adjusted. + if (lookbehind + 1 >= deck.length - lookbehind) { + throw new Error( + `this algorithm cannot guarantee ${lookbehind} unique cards in a row with a deck of size ${deck.length}` + ); + } + + const iteration = Math.trunc(idx / deck.length); + const card = idx % deck.length; + const currentShuffle = new SRng(mixSeeds(new SRng(iteration).randomInt(0, 100_000), seed)).shuffledArray(deck); + if (card < currentShuffle.length - lookbehind) { + // We are indexing before the end of the deck, so adjustments may be needed to achieve uniqueness. + const window: T[] = []; + { + const previousShuffle = new SRng( + mixSeeds(new SRng(iteration - 1).randomInt(0, 100_000), seed) + ).shuffledArray(deck); + for (let i = previousShuffle.length - lookbehind; i != previousShuffle.length; ++i) { + window.push(previousShuffle[i]); + } + } + // From this point on, `window.length == lookbehind` should hold. + for (let i = 0; i != lookbehind; ++i) { + if (window.indexOf(currentShuffle[i]) != -1) { + for (let j = i; ; ++j) { + // `j < currentShuffle.length - lookbehind` should hold. + if (window.indexOf(currentShuffle[j]) == -1) { + const tmp = currentShuffle[j]; + currentShuffle[j] = currentShuffle[i]; + currentShuffle[i] = tmp; + break; + } + } + } + window.splice(0, 1); + window.push(currentShuffle[i]); + } + } + return currentShuffle[card]; +}; diff --git a/src/services/saveLoadoutService.ts b/src/services/saveLoadoutService.ts new file mode 100644 index 00000000..506a5444 --- /dev/null +++ b/src/services/saveLoadoutService.ts @@ -0,0 +1,235 @@ +import type { + IItemEntry, + ILoadoutClient, + ILoadoutEntry, + IOperatorConfigEntry, + ISaveLoadoutRequestNoUpgradeVer +} from "../types/saveLoadoutTypes.ts"; +import { Loadout } from "../models/inventoryModels/loadoutModel.ts"; +import { getInventory } from "./inventoryService.ts"; +import type { IOid } from "../types/commonTypes.ts"; +import { Types } from "mongoose"; +import { isEmptyObject } from "../helpers/general.ts"; +import { logger } from "../utils/logger.ts"; +import type { TEquipmentKey } from "../types/inventoryTypes/inventoryTypes.ts"; +import { equipmentKeys } from "../types/inventoryTypes/inventoryTypes.ts"; +import type { IItemConfig } from "../types/inventoryTypes/commonInventoryTypes.ts"; +import { importCrewShipMembers, importCrewShipWeapon, importLoadOutConfig } from "./importService.ts"; + +//TODO: setup default items on account creation or like originally in giveStartingItems.php + +//TODO: change update functions to only add and not save perhaps, functions that add and return inventory perhaps + +/* loadouts has loadoutconfigs +operatorloadouts has itemconfig, but no multiple config ids +itemconfig has multiple config ids +*/ +export const handleInventoryItemConfigChange = async ( + equipmentChanges: ISaveLoadoutRequestNoUpgradeVer, + accountId: string +): Promise => { + const inventory = await getInventory(accountId); + + for (const [_equipmentName, equipment] of Object.entries(equipmentChanges)) { + const equipmentName = _equipmentName as keyof ISaveLoadoutRequestNoUpgradeVer; + + if (isEmptyObject(equipment)) { + continue; + } + // non-empty is a change in loadout(or suit...) + switch (equipmentName) { + case "AdultOperatorLoadOuts": + case "OperatorLoadOuts": + case "KahlLoadOuts": { + const operatorConfig = equipment as IOperatorConfigEntry; + const operatorLoadout = inventory[equipmentName]; + logger.debug(`operator loadout received ${equipmentName} `, operatorConfig); + // all non-empty entries are one loadout slot + for (const [loadoutId, loadoutConfig] of Object.entries(operatorConfig)) { + logger.debug(`loadoutId ${loadoutId} loadoutConfig`, { config: loadoutConfig }); + const loadout = operatorLoadout.id(loadoutId); + + // if no config with this id exists, create a new one + if (!loadout) { + const { ItemId, ...loadoutConfigItemIdRemoved } = loadoutConfig; + operatorLoadout.push({ + _id: ItemId.$oid, + ...loadoutConfigItemIdRemoved + }); + continue; + } + loadout.set(loadoutConfig); + } + break; + } + case "LoadOuts": { + logger.debug("loadout received"); + const loadout = await Loadout.findOne({ loadoutOwnerId: accountId }); + if (!loadout) { + throw new Error("loadout not found"); + } + + let newLoadoutId: Types.ObjectId | undefined; + for (const [_loadoutSlot, _loadout] of Object.entries(equipment)) { + const loadoutSlot = _loadoutSlot as keyof ILoadoutClient; + const newLoadout = _loadout as ILoadoutEntry; + + // empty loadout slot like: "NORMAL": {} + if (isEmptyObject(newLoadout)) { + continue; + } + + // all non-empty entries are one loadout slot + for (const [loadoutId, loadoutConfig] of Object.entries(newLoadout)) { + if (loadoutConfig.Remove) { + loadout[loadoutSlot].pull({ _id: loadoutId }); + continue; + } + + const oldLoadoutConfig = loadout[loadoutSlot].id(loadoutId); + + const loadoutConfigDatabase = importLoadOutConfig(loadoutConfig); + + // if no config with this id exists, create a new one + if (!oldLoadoutConfig) { + //save the new object id and assign it for every ffff return at the end + if (loadoutConfigDatabase._id.toString() === "ffffffffffffffffffffffff") { + if (!newLoadoutId) { + newLoadoutId = new Types.ObjectId(); + } + loadoutConfigDatabase._id = newLoadoutId; + loadout[loadoutSlot].push(loadoutConfigDatabase); + continue; + } + + loadout[loadoutSlot].push(loadoutConfigDatabase); + continue; + } + + const loadoutIndex = loadout[loadoutSlot].indexOf(oldLoadoutConfig); + if (loadoutIndex === -1) { + throw new Error("loadout index not found"); + } + + loadout[loadoutSlot][loadoutIndex].overwrite(loadoutConfigDatabase); + } + } + await loadout.save(); + + //only return an id if a new loadout was added + if (newLoadoutId) { + return newLoadoutId.toString(); + } + break; + } + case "CurrentLoadOutIds": { + const loadoutIds = equipment as IOid[]; // TODO: Check for more than just an array of oids, I think i remember one instance + inventory.CurrentLoadOutIds = loadoutIds; + break; + } + case "EquippedGear": + case "EquippedEmotes": { + inventory[equipmentName] = equipment as string[]; + break; + } + case "UseAdultOperatorLoadout": { + 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) { + logger.warn(`inventory item WeaponSkins not found with id ${itemId}`); + continue; + } + 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; + } + case "ValidNewLoadoutId": { + logger.debug(`ignoring ValidNewLoadoutId (${equipmentChanges.ValidNewLoadoutId})`); + // seems always equal to the id of loadout config NORMAL[0], likely has no purpose and we're free to ignore it + break; + } + case "ActiveCrewShip": { + if (inventory.CrewShips.length != 1) { + logger.warn(`saving railjack changes with broken inventory?`); + } else if (!inventory.CrewShips[0]._id.equals(equipmentChanges.ActiveCrewShip.$oid)) { + logger.warn( + `client provided CrewShip id ${equipmentChanges.ActiveCrewShip.$oid} but id in inventory is ${inventory.CrewShips[0]._id.toString()}` + ); + } + break; + } + default: { + if (equipmentKeys.includes(equipmentName as TEquipmentKey)) { + logger.debug(`general Item config saved of type ${equipmentName}`, { + config: equipment + }); + + const itemEntries = equipment as IItemEntry; + for (const [itemId, itemConfigEntries] of Object.entries(itemEntries)) { + const inventoryItem = inventory[equipmentName].id(itemId); + + if (!inventoryItem) { + logger.warn(`inventory item ${equipmentName} not found with id ${itemId}`); + continue; + } + + for (const [configId, config] of Object.entries(itemConfigEntries)) { + 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 (itemConfigEntries.Weapon) { + inventoryItem.Weapon = importCrewShipWeapon(itemConfigEntries.Weapon); + } + if (itemConfigEntries.CrewMembers) { + inventoryItem.CrewMembers = importCrewShipMembers(itemConfigEntries.CrewMembers); + } + } + break; + } else { + logger.warn(`unknown saveLoadout field: ${equipmentName}`, { + config: equipment + }); + } + } + } + } + await inventory.save(); +}; diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts new file mode 100644 index 00000000..761adb21 --- /dev/null +++ b/src/services/serversideVendorsService.ts @@ -0,0 +1,511 @@ +import { unixTimesInMs } from "../constants/timeConstants.ts"; +import { args } from "../helpers/commandLineArguments.ts"; +import { catBreadHash } from "../helpers/stringHelpers.ts"; +import type { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel.ts"; +import { mixSeeds, SRng } from "./rngService.ts"; +import type { IItemManifest, IVendorInfo, IVendorManifest } from "../types/vendorTypes.ts"; +import { logger } from "../utils/logger.ts"; +import type { IRange, IVendor, IVendorOffer } from "warframe-public-export-plus"; +import { ExportVendors } from "warframe-public-export-plus"; +import { config } from "./configService.ts"; + +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 + } +]; + +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 (item.alwaysOffered) { + continue; + } + const durationHours = item.rotatedWeekly ? 168 : item.durationHours; + if (typeof durationHours != "number") { + dur = 1; + break; + } + if (dur != durationHours) { + dur = gcd(dur, durationHours); + } + } + return dur * unixTimesInMs.hour; +}; + +export const getVendorManifestByTypeName = (typeName: string, fullStock?: boolean): IVendorManifest | undefined => { + for (const vendorInfo of generatableVendors) { + if (vendorInfo.TypeName == typeName) { + return generateVendorManifest(vendorInfo, fullStock ?? config.fullyStockedVendors); + } + } + if (typeName in ExportVendors) { + const manifest = ExportVendors[typeName]; + return generateVendorManifest( + { + _id: { $oid: getVendorOid(typeName) }, + TypeName: typeName, + RandomSeedType: manifest.randomSeedType, + cycleDuration: getCycleDuration(manifest) + }, + fullStock ?? config.fullyStockedVendors + ); + } + return undefined; +}; + +export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined => { + for (const vendorInfo of generatableVendors) { + if (vendorInfo._id.$oid == oid) { + return generateVendorManifest(vendorInfo, config.fullyStockedVendors); + } + } + 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) + }, + config.fullyStockedVendors + ); + } + } + 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 toRange = (value: IRange | number): IRange => { + if (typeof value == "number") { + return { minValue: value, maxValue: value }; + } + return value; +}; + +const getCycleDurationRange = (manifest: IVendor): IRange | undefined => { + const res: IRange = { minValue: Number.MAX_SAFE_INTEGER, maxValue: 0 }; + for (const offer of manifest.items) { + if (offer.durationHours) { + const range = toRange(offer.durationHours); + if (res.minValue > range.minValue) { + res.minValue = range.minValue; + } + if (res.maxValue < range.maxValue) { + res.maxValue = range.maxValue; + } + } + } + return res.maxValue != 0 ? res : undefined; +}; + +type TOfferId = string; + +const getOfferId = (offer: IVendorOffer | IItemManifest): TOfferId => { + if ("storeItem" in offer) { + // IVendorOffer + return offer.storeItem + "x" + offer.quantity; + } else { + // IItemManifest + return offer.StoreItem + "x" + offer.QuantityMultiplier; + } +}; + +let vendorManifestsUsingFullStock = false; +const vendorManifestCache: Record = {}; + +const clearVendorCache = (): void => { + for (const k of Object.keys(vendorManifestCache)) { + delete vendorManifestCache[k]; + } +}; + +const generateVendorManifest = ( + vendorInfo: IGeneratableVendorInfo, + fullStock: boolean | undefined +): IVendorManifest => { + fullStock ??= config.fullyStockedVendors; + fullStock ??= false; + if (vendorManifestsUsingFullStock != fullStock) { + vendorManifestsUsingFullStock = fullStock; + clearVendorCache(); + } + + 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; + const manifest = ExportVendors[vendorInfo.TypeName]; + const cycleDurationRange = getCycleDurationRange(manifest); + let now = Date.now(); + if (cycleDurationRange && cycleDurationRange.minValue != cycleDurationRange.maxValue) { + now -= (cycleDurationRange.maxValue - 1) * unixTimesInMs.hour; + } + while (Date.now() >= parseInt(info.Expiry.$date.$numberLong)) { + // Remove expired offers + for (let i = 0; i != info.ItemManifest.length; ) { + if (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((now - cycleOffset) / cycleDuration); + const rng = new SRng(mixSeeds(vendorSeed, cycleIndex)); + const offersToAdd: IVendorOffer[] = []; + if (manifest.isOneBinPerCycle) { + if (fullStock) { + for (const rawItem of manifest.items) { + offersToAdd.push(rawItem); + } + } else { + const binThisCycle = cycleIndex % 2; // Note: May want to check the actual number of bins, but this is only used for coda weapons right now. + for (const rawItem of manifest.items) { + if (rawItem.bin == binThisCycle) { + offersToAdd.push(rawItem); + } + } + } + } else { + // Compute vendor requirements, subtracting existing offers + const remainingItemCapacity: Record = {}; + const missingItemsPerBin: Record = {}; + let numOffersThatNeedToMatchABin = 0; + if (manifest.numItemsPerBin) { + for (let bin = 0; bin != manifest.numItemsPerBin.length; ++bin) { + missingItemsPerBin[bin] = manifest.numItemsPerBin[bin]; + numOffersThatNeedToMatchABin += manifest.numItemsPerBin[bin]; + } + } + for (const item of manifest.items) { + remainingItemCapacity[getOfferId(item)] = 1 + item.duplicates; + } + for (const offer of info.ItemManifest) { + remainingItemCapacity[getOfferId(offer)] -= 1; + const bin = parseInt(offer.Bin.substring(4)); + if (missingItemsPerBin[bin]) { + missingItemsPerBin[bin] -= 1; + numOffersThatNeedToMatchABin -= 1; + } + } + + // Add permanent offers + let numUncountedOffers = 0; + let numCountedOffers = 0; + let offset = 0; + for (const item of manifest.items) { + if (item.alwaysOffered || item.rotatedWeekly) { + ++numUncountedOffers; + const id = getOfferId(item); + if (remainingItemCapacity[id] != 0) { + remainingItemCapacity[id] -= 1; + offersToAdd.push(item); + ++offset; + } + if (missingItemsPerBin[item.bin]) { + missingItemsPerBin[item.bin] -= 1; + numOffersThatNeedToMatchABin -= 1; + } + } else { + numCountedOffers += 1 + item.duplicates; + } + } + + // Add counted offers + const useRng = + manifest.numItems && + (manifest.numItems.minValue != manifest.numItems.maxValue || + manifest.numItems.minValue != numCountedOffers); + const numItemsTarget = fullStock + ? numUncountedOffers + numCountedOffers + : manifest.numItems + ? numUncountedOffers + + Math.min( + Object.values(remainingItemCapacity).reduce((a, b) => a + b, 0), + useRng + ? rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue) + : manifest.numItems.minValue + ) + : manifest.items.length; + let i = 0; + const rollableOffers = manifest.items.filter(x => x.probability !== undefined) as (Omit< + IVendorOffer, + "probability" + > & { probability: number })[]; + while (info.ItemManifest.length + offersToAdd.length < numItemsTarget) { + const item = useRng ? rng.randomReward(rollableOffers)! : rollableOffers[i++]; + if ( + remainingItemCapacity[getOfferId(item)] != 0 && + (numOffersThatNeedToMatchABin == 0 || missingItemsPerBin[item.bin]) + ) { + remainingItemCapacity[getOfferId(item)] -= 1; + if (missingItemsPerBin[item.bin]) { + missingItemsPerBin[item.bin] -= 1; + numOffersThatNeedToMatchABin -= 1; + } + offersToAdd.splice(offset, 0, item); + } + if (i == rollableOffers.length) { + i = 0; + } + } + } + const cycleStart = cycleOffset + cycleIndex * cycleDuration; + for (const rawItem of offersToAdd) { + const durationHoursRange = toRange(rawItem.durationHours ?? cycleDuration); + const expiry = rawItem.alwaysOffered + ? 2051240400_000 + : cycleStart + + (rawItem.rotatedWeekly + ? unixTimesInMs.week + : 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: rawItem.quantity, + Expiry: { $date: { $numberLong: expiry.toString() } }, + PurchaseQuantityLimit: rawItem.purchaseLimit, + RotatedWeekly: rawItem.rotatedWeekly, + AllowMultipurchase: rawItem.purchaseLimit !== 1, + 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); + } + + if (manifest.numItemsPerBin) { + info.ItemManifest.sort((a, b) => { + const aBin = parseInt(a.Bin.substring(4)); + const bBin = parseInt(b.Bin.substring(4)); + return aBin == bBin ? 0 : aBin < bBin ? +1 : -1; + }); + } + + // 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(); + + now += unixTimesInMs.hour; + } + return cacheEntry; +}; + +if (args.dev) { + if ( + getCycleDuration(ExportVendors["/Lotus/Types/Game/VendorManifests/Hubs/TeshinHardModeVendorManifest"]) != + unixTimesInMs.week + ) { + logger.warn(`getCycleDuration self test failed`); + } + + for (let i = 0; i != 2; ++i) { + const fullStock = !!i; + + const ads = getVendorManifestByTypeName( + "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest", + fullStock + )!.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`); + } + + const pall = getVendorManifestByTypeName( + "/Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest", + fullStock + )!.VendorInfo.ItemManifest; + if ( + pall.length != 5 || + pall[0].StoreItem != "/Lotus/StoreItems/Types/Items/ShipDecos/HarrowQuestKeyOrnament" || + pall[1].StoreItem != "/Lotus/StoreItems/Types/BoosterPacks/RivenModPack" || + pall[2].StoreItem != "/Lotus/StoreItems/Types/StoreItems/CreditBundles/150000Credits" || + pall[3].StoreItem != "/Lotus/StoreItems/Types/Items/MiscItems/Kuva" || + pall[4].StoreItem != "/Lotus/StoreItems/Types/BoosterPacks/RivenModPack" + ) { + logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest`); + } + } + + const cms = getVendorManifestByTypeName( + "/Lotus/Types/Game/VendorManifests/Hubs/RailjackCrewMemberVendorManifest", + false + )!.VendorInfo.ItemManifest; + if ( + cms.length != 9 || + cms[0].Bin != "BIN_2" || + cms[8].Bin != "BIN_0" || + cms.reduce((a, x) => a + (x.Bin == "BIN_2" ? 1 : 0), 0) < 2 || + cms.reduce((a, x) => a + (x.Bin == "BIN_1" ? 1 : 0), 0) < 2 || + cms.reduce((a, x) => a + (x.Bin == "BIN_0" ? 1 : 0), 0) < 4 + ) { + logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/RailjackCrewMemberVendorManifest`); + } + + const temple = getVendorManifestByTypeName( + "/Lotus/Types/Game/VendorManifests/TheHex/Temple1999VendorManifest", + false + )!.VendorInfo.ItemManifest; + if (!temple.find(x => x.StoreItem == "/Lotus/StoreItems/Types/Items/MiscItems/Kuva")) { + logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/TheHex/Temple1999VendorManifest`); + } + + const nakak = getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Ostron/MaskSalesmanManifest", false)! + .VendorInfo.ItemManifest; + if ( + nakak.length != 10 || + nakak[0].StoreItem != "/Lotus/StoreItems/Upgrades/Skins/Ostron/RevenantMask" || + nakak[1].StoreItem != "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyThumper" || + nakak[1].ItemPrices?.length != 4 || + nakak[2].StoreItem != "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyThumperMedium" || + nakak[2].ItemPrices?.length != 4 || + nakak[3].StoreItem != "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyThumperLarge" || + nakak[3].ItemPrices?.length != 4 + // The remaining offers should be computed by weighted RNG. + ) { + logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Ostron/MaskSalesmanManifest`); + } + + // strange case where numItems is 5 even tho only 3 offers can possibly be generated + const loid = getVendorManifestByTypeName( + "/Lotus/Types/Game/VendorManifests/EntratiLabs/EntratiLabsCommisionsManifest", + false + )!.VendorInfo.ItemManifest; + if (loid.length != 3) { + logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/EntratiLabs/EntratiLabsCommisionsManifest`); + } +} diff --git a/src/services/shipCustomizationsService.ts b/src/services/shipCustomizationsService.ts new file mode 100644 index 00000000..2358fe41 --- /dev/null +++ b/src/services/shipCustomizationsService.ts @@ -0,0 +1,277 @@ +import { getPersonalRooms } from "./personalRoomsService.ts"; +import { getShip } from "./shipService.ts"; +import type { + IResetShipDecorationsRequest, + IResetShipDecorationsResponse, + ISetPlacedDecoInfoRequest, + ISetShipCustomizationsRequest, + IShipDecorationsRequest, + IShipDecorationsResponse, + RoomsType, + TBootLocation, + TPersonalRoomsDatabaseDocument +} from "../types/personalRoomsTypes.ts"; +import { logger } from "../utils/logger.ts"; +import { Types } from "mongoose"; +import { addFusionTreasures, addShipDecorations, getInventory } from "./inventoryService.ts"; +import { config } from "./configService.ts"; +import { Guild } from "../models/guildModel.ts"; +import { hasGuildPermission } from "./guildService.ts"; +import { GuildPermission } from "../types/guildTypes.ts"; +import { ExportResources } from "warframe-public-export-plus"; +import { convertCustomizationInfo } from "./importService.ts"; + +export const setShipCustomizations = async ( + accountId: string, + shipCustomization: ISetShipCustomizationsRequest +): Promise => { + if (shipCustomization.IsExterior) { + const ship = await getShip(new Types.ObjectId(shipCustomization.ShipId)); + if (ship.ShipOwnerId.toString() == accountId) { + ship.set({ + ShipExteriorColors: shipCustomization.Customization.Colors, + SkinFlavourItem: shipCustomization.Customization.SkinFlavourItem, + ShipAttachments: shipCustomization.Customization.ShipAttachments, + AirSupportPower: shipCustomization.AirSupportPower! + }); + await ship.save(); + } + } else { + const personalRooms = await getPersonalRooms(accountId); + if (shipCustomization.IsShop) { + personalRooms.TailorShop.Colors = shipCustomization.Customization.Colors; + personalRooms.TailorShop.LevelDecosVisible = shipCustomization.Customization.LevelDecosVisible; + personalRooms.TailorShop.CustomJson = shipCustomization.Customization.CustomJson; + } else { + personalRooms.Ship.ShipInterior = shipCustomization.Customization; + } + await personalRooms.save(); + } +}; + +export const handleSetShipDecorations = async ( + accountId: string, + placedDecoration: IShipDecorationsRequest +): Promise => { + const personalRooms = await getPersonalRooms(accountId); + + const rooms = getRoomsForBootLocation(personalRooms, placedDecoration); + + const roomToPlaceIn = rooms.find(room => room.Name === placedDecoration.Room); + + if (!roomToPlaceIn) { + throw new Error(`unknown room: ${placedDecoration.Room}`); + } + + const entry = Object.entries(ExportResources).find(arr => arr[1].deco == placedDecoration.Type); + if (!entry) { + throw new Error(`unknown deco type: ${placedDecoration.Type}`); + } + const [itemType, meta] = entry; + if (meta.capacityCost === undefined) { + throw new Error(`unknown deco type: ${placedDecoration.Type}`); + } + + if (placedDecoration.MoveId) { + //moved within the same room + if (placedDecoration.OldRoom === placedDecoration.Room) { + const existingDecoration = roomToPlaceIn.PlacedDecos.id(placedDecoration.MoveId); + + if (!existingDecoration) { + throw new Error("decoration to be moved not found"); + } + + existingDecoration.Pos = placedDecoration.Pos; + existingDecoration.Rot = placedDecoration.Rot; + + if (placedDecoration.Scale) { + existingDecoration.Scale = placedDecoration.Scale; + } + + await personalRooms.save(); + return { + OldRoom: placedDecoration.OldRoom, + NewRoom: placedDecoration.Room, + IsApartment: placedDecoration.IsApartment, + MaxCapacityIncrease: 0 + }; + } + + //moved to a different room + const oldRoom = rooms.find(room => room.Name === placedDecoration.OldRoom); + + if (!oldRoom) { + logger.error("old room not found"); + throw new Error("old room not found"); + } + + oldRoom.PlacedDecos.pull({ _id: placedDecoration.MoveId }); + oldRoom.MaxCapacity += meta.capacityCost; + + const newDecoration = { + Type: placedDecoration.Type, + Pos: placedDecoration.Pos, + Rot: placedDecoration.Rot, + Scale: placedDecoration.Scale, + Sockets: placedDecoration.Sockets, + _id: placedDecoration.MoveId + }; + + //the new room is still roomToPlaceIn + roomToPlaceIn.PlacedDecos.push(newDecoration); + roomToPlaceIn.MaxCapacity -= meta.capacityCost; + + await personalRooms.save(); + return { + OldRoom: placedDecoration.OldRoom, + NewRoom: placedDecoration.Room, + IsApartment: placedDecoration.IsApartment, + MaxCapacityIncrease: -meta.capacityCost + }; + } + + if (placedDecoration.RemoveId) { + const decoIndex = roomToPlaceIn.PlacedDecos.findIndex(x => x._id.equals(placedDecoration.RemoveId)); + const deco = roomToPlaceIn.PlacedDecos[decoIndex]; + roomToPlaceIn.PlacedDecos.splice(decoIndex, 1); + roomToPlaceIn.MaxCapacity += meta.capacityCost; + await personalRooms.save(); + + if (!config.unlockAllShipDecorations) { + const inventory = await getInventory(accountId); + if (deco.Sockets !== undefined) { + addFusionTreasures(inventory, [{ ItemType: itemType, Sockets: deco.Sockets, ItemCount: 1 }]); + } else { + addShipDecorations(inventory, [{ ItemType: itemType, ItemCount: 1 }]); + } + await inventory.save(); + } + + return { + DecoId: placedDecoration.RemoveId, + Room: placedDecoration.Room, + IsApartment: placedDecoration.IsApartment, + MaxCapacityIncrease: 0 // Client already implies the capacity being refunded. + }; + } + + if (!config.unlockAllShipDecorations) { + const inventory = await getInventory(accountId); + if (placedDecoration.Sockets !== undefined) { + addFusionTreasures(inventory, [{ ItemType: itemType, Sockets: placedDecoration.Sockets, ItemCount: -1 }]); + } else { + addShipDecorations(inventory, [{ ItemType: itemType, ItemCount: -1 }]); + } + await inventory.save(); + } + + //place decoration + const decoId = new Types.ObjectId(); + roomToPlaceIn.PlacedDecos.push({ + Type: placedDecoration.Type, + Pos: placedDecoration.Pos, + Rot: placedDecoration.Rot, + Scale: placedDecoration.Scale, + Sockets: placedDecoration.Sockets, + _id: decoId + }); + roomToPlaceIn.MaxCapacity -= meta.capacityCost; + + await personalRooms.save(); + + return { + DecoId: decoId.toString(), + Room: placedDecoration.Room, + IsApartment: placedDecoration.IsApartment, + MaxCapacityIncrease: -meta.capacityCost + }; +}; + +const getRoomsForBootLocation = ( + personalRooms: TPersonalRoomsDatabaseDocument, + request: { BootLocation?: TBootLocation; IsApartment?: boolean } +): RoomsType[] => { + if (request.BootLocation == "SHOP") { + return personalRooms.TailorShop.Rooms; + } + if (request.BootLocation == "APARTMENT" || request.IsApartment) { + return personalRooms.Apartment.Rooms; + } + return personalRooms.Ship.Rooms; +}; + +export const handleResetShipDecorations = async ( + accountId: string, + request: IResetShipDecorationsRequest +): Promise => { + const [personalRooms, inventory] = await Promise.all([getPersonalRooms(accountId), getInventory(accountId)]); + const room = getRoomsForBootLocation(personalRooms, request).find(room => room.Name === request.Room); + if (!room) { + throw new Error(`unknown room: ${request.Room}`); + } + + for (const deco of room.PlacedDecos) { + const entry = Object.entries(ExportResources).find(arr => arr[1].deco == deco.Type); + if (!entry) { + throw new Error(`unknown deco type: ${deco.Type}`); + } + const [itemType, meta] = entry; + if (meta.capacityCost === undefined) { + throw new Error(`unknown deco type: ${deco.Type}`); + } + + // refund item + if (!config.unlockAllShipDecorations) { + if (deco.Sockets !== undefined) { + addFusionTreasures(inventory, [{ ItemType: itemType, Sockets: deco.Sockets, ItemCount: 1 }]); + } else { + addShipDecorations(inventory, [{ ItemType: itemType, ItemCount: 1 }]); + } + } + + // refund capacity + room.MaxCapacity += meta.capacityCost; + } + + // empty room + room.PlacedDecos.splice(0, room.PlacedDecos.length); + + await Promise.all([personalRooms.save(), inventory.save()]); + + return { + ResetRoom: request.Room, + ClaimedDecos: [], // Not sure what this is for; the client already implies that the decos were returned to inventory. + NewCapacity: room.MaxCapacity + }; +}; + +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 = getRoomsForBootLocation(personalRooms, req).find(room => room.Name === req.Room); + if (!room) { + throw new Error(`unknown room: ${req.Room}`); + } + + const placedDeco = room.PlacedDecos.id(req.DecoId); + if (!placedDeco) { + throw new Error(`unknown deco id: ${req.DecoId}`); + } + + placedDeco.PictureFrameInfo = req.PictureFrameInfo; + placedDeco.CustomizationInfo = req.CustomizationInfo ? convertCustomizationInfo(req.CustomizationInfo) : undefined; + placedDeco.AnimPoseItem = req.AnimPoseItem; + + await personalRooms.save(); +}; diff --git a/src/services/shipService.ts b/src/services/shipService.ts new file mode 100644 index 00000000..56bf4c31 --- /dev/null +++ b/src/services/shipService.ts @@ -0,0 +1,32 @@ +import type { TShipDatabaseDocument } from "../models/shipModel.ts"; +import { Ship } from "../models/shipModel.ts"; +import type { 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, + ShipOwnerId: accountOwnerId + }); + const newShip = await ship.save(); + return newShip._id; + } catch (error) { + if (error instanceof Error) { + throw new Error(`error creating ship" ${error.message}`); + } + throw new Error("error creating ship that is not of instance Error"); + } +}; + +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()}`); + } + + return ship; +}; diff --git a/src/services/statsService.ts b/src/services/statsService.ts new file mode 100644 index 00000000..35fbc369 --- /dev/null +++ b/src/services/statsService.ts @@ -0,0 +1,523 @@ +import type { TStatsDatabaseDocument } from "../models/statsModel.ts"; +import { Stats } from "../models/statsModel.ts"; +import type { + IStatsAdd, + IStatsMax, + IStatsSet, + IStatsTimers, + IStatsUpdate, + IUploadEntry, + IWeapon +} from "../types/statTypes.ts"; +import { logger } from "../utils/logger.ts"; +import { addEmailItem, getInventory } from "./inventoryService.ts"; +import { submitLeaderboardScore } from "./leaderboardService.ts"; + +export const createStats = async (accountId: string): Promise => { + const stats = new Stats({ accountOwnerId: accountId }); + await stats.save(); + return stats; +}; + +export const getStats = async (accountOwnerId: string): Promise => { + let stats = await Stats.findOne({ accountOwnerId: accountOwnerId }); + + if (!stats) stats = await createStats(accountOwnerId); + + return stats; +}; + +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) { + case "add": + for (const [category, data] of Object.entries(actionData as IStatsAdd)) { + switch (category) { + case "MISSION_COMPLETE": + for (const [key, value] of Object.entries(data as IUploadEntry)) { + switch (key) { + case "GS_SUCCESS": + playerStats.MissionsCompleted ??= 0; + playerStats.MissionsCompleted += value; + break; + case "GS_QUIT": + playerStats.MissionsQuit ??= 0; + playerStats.MissionsQuit += value; + break; + case "GS_FAILURE": + playerStats.MissionsFailed ??= 0; + playerStats.MissionsFailed += value; + break; + case "GS_INTERRUPTED": + playerStats.MissionsInterrupted ??= 0; + playerStats.MissionsInterrupted += value; + break; + case "GS_DUMPED": + playerStats.MissionsDumped ??= 0; + playerStats.MissionsDumped += value; + break; + default: + if (!ignoredCategories.includes(category)) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!unknownCategories[action]) { + unknownCategories[action] = []; + } + unknownCategories[action].push(category); + } + break; + } + } + break; + + case "PICKUP_ITEM": + playerStats.PickupCount ??= 0; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const [_key, value] of Object.entries(data as IUploadEntry)) { + playerStats.PickupCount += value; + } + break; + + case "SCAN": + playerStats.Scans ??= []; + for (const [type, scans] of Object.entries(data as IUploadEntry)) { + const scan = playerStats.Scans.find(element => element.type === type); + if (scan) { + scan.scans += scans; + } else { + playerStats.Scans.push({ type: type, scans }); + } + } + break; + + case "USE_ABILITY": + playerStats.Abilities ??= []; + for (const [type, used] of Object.entries(data as IUploadEntry)) { + const ability = playerStats.Abilities.find(element => element.type === type); + if (ability) { + ability.used += used; + } else { + playerStats.Abilities.push({ type: type, used }); + } + } + break; + + case "FIRE_WEAPON": + 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", + 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); + if (weapon) { + weapon[statKey] ??= 0; + weapon[statKey] += count; + } else { + const newWeapon: IWeapon = { type: type }; + newWeapon[statKey] = count; + playerStats.Weapons.push(newWeapon); + } + } + break; + } + + case "KILL_ENEMY": + case "EXECUTE_ENEMY": + case "HEADSHOT": + case "KILL_ASSIST": { + playerStats.Enemies ??= []; + 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)) { + 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; + } + } + break; + } + + case "DIE": + playerStats.Enemies ??= []; + playerStats.Deaths ??= 0; + for (const [type, deaths] of Object.entries(data as IUploadEntry)) { + playerStats.Deaths += deaths; + const enemy = playerStats.Enemies.find(element => element.type === type); + if (enemy) { + enemy.deaths ??= 0; + enemy.deaths += deaths; + } else { + playerStats.Enemies.push({ type: type, deaths }); + } + } + break; + + case "MELEE_KILL": + playerStats.MeleeKills ??= 0; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const [_key, kills] of Object.entries(data as IUploadEntry)) { + playerStats.MeleeKills += kills; + } + break; + + case "INCOME": + playerStats.Income ??= 0; + playerStats.Income += data; + break; + + case "CIPHER": + if ((data as IUploadEntry)["0"] > 0) { + playerStats.CiphersFailed ??= 0; + playerStats.CiphersFailed += (data as IUploadEntry)["0"]; + } + if ((data as IUploadEntry)["1"] > 0) { + playerStats.CiphersSolved ??= 0; + playerStats.CiphersSolved += (data as IUploadEntry)["1"]; + } + break; + + default: + if (!ignoredCategories.includes(category)) { + unknownCategories[action] ??= []; + unknownCategories[action].push(category); + } + break; + } + } + break; + + case "timers": + for (const [category, data] of Object.entries(actionData as IStatsTimers)) { + switch (category) { + case "EQUIP_WEAPON": + playerStats.Weapons ??= []; + for (const [type, equipTime] of Object.entries(data as IUploadEntry)) { + const weapon = playerStats.Weapons.find(element => element.type === type); + if (weapon) { + weapon.equipTime ??= 0; + weapon.equipTime += equipTime; + } else { + playerStats.Weapons.push({ type: type, equipTime }); + } + } + break; + + case "CURRENT_MISSION_TIME": + playerStats.TimePlayedSec ??= 0; + playerStats.TimePlayedSec += data; + break; + + case "CIPHER_TIME": + playerStats.CipherTime ??= 0; + playerStats.CipherTime += data; + break; + + default: + if (!ignoredCategories.includes(category)) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!unknownCategories[action]) { + unknownCategories[action] = []; + } + unknownCategories[action].push(category); + } + break; + } + } + break; + + case "max": + for (const [category, data] of Object.entries(actionData as IStatsMax)) { + switch (category) { + case "WEAPON_XP": + playerStats.Weapons ??= []; + for (const [type, xp] of Object.entries(data as IUploadEntry)) { + const weapon = playerStats.Weapons.find(element => element.type === type); + if (weapon) { + if (xp > (weapon.xp ?? 0)) { + weapon.xp = xp; + } + } else { + playerStats.Weapons.push({ type: type, xp }); + } + } + break; + + case "MISSION_SCORE": + playerStats.Missions ??= []; + for (const [type, highScore] of Object.entries(data as IUploadEntry)) { + const mission = playerStats.Missions.find(element => element.type === type); + if (mission) { + if (highScore > mission.highScore) { + mission.highScore = highScore; + } + } else { + playerStats.Missions.push({ type: type, highScore }); + } + await submitLeaderboardScore( + "weekly", + type, + accountOwnerId, + payload.displayName, + highScore, + payload.guildId + ); + break; + } + break; + + case "RACE_SCORE": + playerStats.Races ??= new Map(); + + for (const [race, highScore] of Object.entries(data as Record)) { + const currentRace = playerStats.Races.get(race); + + if (currentRace) { + if (highScore > currentRace.highScore) { + playerStats.Races.set(race, { highScore }); + } + } 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; + + case "Halloween16": + case "AmalgamEventScoreMax": + case "Halloween19ScoreMax": + case "FlotillaEventScore": + case "FlotillaSpaceBadgesTier1": + case "FlotillaSpaceBadgesTier2": + case "FlotillaSpaceBadgesTier3": + case "FlotillaGroundBadgesTier1": + case "FlotillaGroundBadgesTier2": + case "FlotillaGroundBadgesTier3": + case "MechSurvivalScoreMax": + playerStats[category] ??= 0; + if (data > playerStats[category]) playerStats[category] = data as number; + await submitLeaderboardScore( + "events", + category, + accountOwnerId, + payload.displayName, + data as number, + payload.guildId + ); + break; + + default: + if (!ignoredCategories.includes(category)) { + unknownCategories[action] ??= []; + unknownCategories[action].push(category); + } + break; + } + } + break; + + case "set": + for (const [category, value] of Object.entries(actionData as IStatsSet)) { + switch (category) { + case "ELO_RATING": + playerStats.Rating = value as number; + break; + + case "RANK": + playerStats.Rank = value as number; + break; + + case "PLAYER_LEVEL": + playerStats.PlayerLevel = value as number; + break; + + default: + if (!ignoredCategories.includes(category)) { + unknownCategories[action] ??= []; + unknownCategories[action].push(category); + } + break; + } + } + break; + + case "displayName": + case "guildId": + break; + + default: + logger.debug(`Unknown updateStats action: ${action}`); + break; + } + } + + for (const [action, categories] of Object.entries(unknownCategories)) { + logger.debug(`Unknown updateStats ${action} action categories: ${categories.join(", ")}`); + } + + await playerStats.save(); +}; + +const ignoredCategories = [ + //add action + "MISSION_STARTED", + "HOST_OS", + "CPU_CORES", + "CPU_MODEL", + "CPU_VENDOR", + "GPU_CLASS", + "GFX_DRIVER", + "GFX_RESOLUTION", + "GFX_ASPECT", + "GFX_WINDOW", + "GPU_VENDOR", + "GFX_HDR", + "SPEAKER_COUNT", + "MISSION_MATCHMAKING", + "PLAYER_COUNT", + "HOST_MIGRATION", + "DESTROY_DECORATION", + "MOVEMENT", + "RECEIVE_UPGRADE", + "EQUIP_COSMETIC", + "EQUIP_UPGRADE", + "MISSION_TYPE", + "MISSION_FACTION", + "MISSION_PLAYED", + "MISSION_PLAYED_TIME", + "CPU_CLOCK", + "CPU_FEATURE", + "RAM", + "ADDR_SPACE", + "GFX_SCALE", + "LOGINS", + "GPU_MODEL", + "MEDALS_TOP", + "STATS_TIMERS_RESET", + "INPUT_ACTIVITY_TIME", + "LOGINS_ITEM", + "TAKE_DAMAGE", + "SQUAD_KILL_ENEMY", + "SQUAD_HEADSHOT", + "SQUAD_MELEE_KILL", + "MELEE_KILL_ITEM", + "TAKE_DAMAGE_ITEM", + "SQUAD_KILL_ENEMY_ITEM", + "SQUAD_HEADSHOT_ITEM", + "SQUAD_MELEE_KILL_ITEM", + "PRE_DIE", + "PRE_DIE_ITEM", + "GEAR_USED", + "DIE_ITEM", + "CAPTURE_ENEMY", // handled in KILL_ENEMY + + // timers action + "IN_SHIP_TIME", + "IN_SHIP_VIEW_TIME", + "MISSION_LOAD_TIME", + "MISSION_TIME", + "REGION_TIME", + "PLATFORM_TIME", + "PRE_DIE_TIME", + "VEHICLE_TIME" +]; diff --git a/src/services/webService.ts b/src/services/webService.ts new file mode 100644 index 00000000..6dd1a5dd --- /dev/null +++ b/src/services/webService.ts @@ -0,0 +1,112 @@ +import http from "http"; +import https from "https"; +import fs from "node:fs"; +import { config } from "./configService.ts"; +import { logger } from "../utils/logger.ts"; +import { app } from "../app.ts"; +import type { AddressInfo } from "node:net"; +import { Agent, WebSocket as UnidiciWebSocket } from "undici"; +import { startWsServer, startWssServer, stopWsServers } from "./wsService.ts"; + +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, () => { + startWsServer(httpServer!); + + 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, () => { + startWssServer(httpsServer!); + + logger.info("HTTPS server started on port " + httpsPort); + + logger.info( + "Access the WebUI in your browser at http://localhost" + (httpPort == 80 ? "" : ":" + httpPort) + ); + + void runWsSelfTest("wss", httpsPort).then(ok => { + if (!ok) { + logger.warn(`WSS self-test failed. The server may not actually be reachable at port ${httpsPort}.`); + if (process.platform == "win32") { + logger.warn( + `You can check who actually has that port via powershell: Get-Process -Id (Get-NetTCPConnection -LocalPort ${httpsPort}).OwningProcess` + ); + } + } + }); + }); + }); +}; + +const runWsSelfTest = (protocol: "ws" | "wss", port: number): Promise => { + return new Promise(resolve => { + // https://github.com/oven-sh/bun/issues/20547 + if (process.versions.bun) { + const client = new WebSocket(`${protocol}://localhost:${port}/custom/selftest`, { + tls: { rejectUnauthorized: false } + } as unknown as string); + client.onmessage = (e): void => { + resolve(e.data == "SpaceNinjaServer"); + }; + client.onerror = client.onclose = (): void => { + resolve(false); + }; + } else { + const agent = new Agent({ connect: { rejectUnauthorized: false } }); + const client = new UnidiciWebSocket(`${protocol}://localhost:${port}/custom/selftest`, { + dispatcher: agent + }); + client.onmessage = (e): void => { + resolve(e.data == "SpaceNinjaServer"); + }; + client.onerror = client.onclose = (): void => { + resolve(false); + }; + } + }); +}; + +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(); + }); + }) + ); + } + stopWsServers(promises); + await Promise.all(promises); +}; diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts new file mode 100644 index 00000000..b028cc55 --- /dev/null +++ b/src/services/worldStateService.ts @@ -0,0 +1,3576 @@ +import staticWorldState from "../../static/fixed_responses/worldState/worldState.json" with { type: "json" }; +import baro from "../../static/fixed_responses/worldState/baro.json" with { type: "json" }; +import varzia from "../../static/fixed_responses/worldState/varzia.json" with { type: "json" }; +import fissureMissions from "../../static/fixed_responses/worldState/fissureMissions.json" with { type: "json" }; +import sortieTilesets from "../../static/fixed_responses/worldState/sortieTilesets.json" with { type: "json" }; +import sortieTilesetMissions from "../../static/fixed_responses/worldState/sortieTilesetMissions.json" with { type: "json" }; +import syndicateMissions from "../../static/fixed_responses/worldState/syndicateMissions.json" with { type: "json" }; +import darvoDeals from "../../static/fixed_responses/worldState/darvoDeals.json" with { type: "json" }; +import invasionNodes from "../../static/fixed_responses/worldState/invasionNodes.json" with { type: "json" }; +import invasionRewards from "../../static/fixed_responses/worldState/invasionRewards.json" with { type: "json" }; +import pvpChallenges from "../../static/fixed_responses/worldState/pvpChallenges.json" with { type: "json" }; +import { buildConfig } from "./buildConfigService.ts"; +import { unixTimesInMs } from "../constants/timeConstants.ts"; +import { config } from "./configService.ts"; +import { getRandomElement, getRandomInt, sequentiallyUniqueRandomElement, SRng } from "./rngService.ts"; +import type { IMissionReward, IRegion } from "warframe-public-export-plus"; +import { eMissionType, ExportRegions, ExportSyndicates } from "warframe-public-export-plus"; +import type { + ICalendarDay, + ICalendarEvent, + ICalendarSeason, + IGoal, + IInvasion, + ILiteSortie, + IPrimeVaultTrader, + IPrimeVaultTraderOffer, + IPVPChallengeInstance, + ISeasonChallenge, + ISortie, + ISortieMission, + ISyndicateMissionInfo, + ITmp, + IVoidStorm, + IVoidTrader, + IVoidTraderOffer, + IWorldState, + TCircuitGameMode +} from "../types/worldStateTypes.ts"; +import { toMongoDate, toOid, version_compare } from "../helpers/inventoryHelpers.ts"; +import { logger } from "../utils/logger.ts"; +import { DailyDeal, Fissure } from "../models/worldStateModel.ts"; + +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: readonly string[] = [ + "/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: readonly string[] = [ + "/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 eidolonGhoulJobs: readonly string[] = [ + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBountyAss", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBountyExt", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBountyHunt", + "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBountyRes" +]; + +const venusJobs: readonly string[] = [ + "/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: readonly string[] = [ + "/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: readonly string[] = [ + "/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: readonly string[] = [ + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessAreaDefenseBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessExcavateBounty", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessPurifyBounty" +]; + +export const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0 + +const isBeforeNextExpectedWorldStateRefresh = (nowMs: number, thenMs: number): boolean => { + return nowMs + 300_000 > thenMs; +}; + +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 && + (key != "SolNode228" || sortieBossToFaction[boss] == "FC_GRINEER") // PoE does not work for non-infested enemies + ) { + 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[]; + weeklyPermanent: string[]; +} + +export 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/") + ), + weeklyPermanent: syndicate.weeklyChallenges!.filter(x => + x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent") + ) + }; +}; + +const getSeasonDailyChallenge = (pools: IRotatingSeasonChallengePools, day: number): ISeasonChallenge => { + const dayStart = EPOCH + day * 86400000; + const dayEnd = EPOCH + (day + 3) * 86400000; + return { + _id: { $oid: "67e1b5ca9d00cb47" + day.toString().padStart(8, "0") }, + Daily: true, + Activation: { $date: { $numberLong: dayStart.toString() } }, + Expiry: { $date: { $numberLong: dayEnd.toString() } }, + Challenge: sequentiallyUniqueRandomElement(pools.daily, day, 2, 605732938)! + }; +}; + +const pushSeasonWeeklyChallenge = ( + activeChallenges: ISeasonChallenge[], + pool: string[], + nightwaveSeason: number, + week: number, + id: number +): void => { + 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)); + let challenge: string; + do { + challenge = rng.randomElement(pool)!; + } while (activeChallenges.some(x => x.Challenge == challenge)); + activeChallenges.push({ + _id: { + $oid: + (nightwaveSeason + 1).toString().padStart(4, "0") + + "bb2d9d00cb47" + + challengeId.toString().padStart(8, "0") + }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: challenge + }); +}; + +export const pushWeeklyActs = ( + activeChallenges: ISeasonChallenge[], + pools: IRotatingSeasonChallengePools, + week: number, + nightwaveStartTimestamp: number, + nightwaveSeason: number +): void => { + pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 0); + pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 1); + if (pools.weeklyPermanent.length > 0) { + const weekStart = EPOCH + week * unixTimesInMs.week; + const weekEnd = weekStart + unixTimesInMs.week; + const nightwaveWeekStart = ((): number => { + let ts = nightwaveStartTimestamp - EPOCH; + ts -= ts % unixTimesInMs.week; + return EPOCH + ts; + })(); + const nightwaveWeek = Math.trunc((weekStart - nightwaveWeekStart) / unixTimesInMs.week); + const weeklyPermanentIndex = (nightwaveWeek * 3) % pools.weeklyPermanent.length; + for (let i = 0; i < 3; i++) { + activeChallenges.push({ + _id: { + $oid: + (nightwaveSeason + 1).toString().padStart(4, "0") + + "b96e9d00cb47" + + (week * 7 + 2 + i).toString().padStart(8, "0") + }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: pools.weeklyPermanent[weeklyPermanentIndex + i] + }); + } + } else { + pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 2); + pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 3); + pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 4); + } + pushSeasonWeeklyChallenge(activeChallenges, pools.hardWeekly, nightwaveSeason, week, 5); + pushSeasonWeeklyChallenge(activeChallenges, pools.hardWeekly, nightwaveSeason, week, 6); +}; + +const generateXpAmounts = (rng: SRng, stageCount: number, minXp: number, maxXp: number): number[] => { + const step = minXp < 1000 ? 1 : 10; + const totalDeciXp = rng.randomInt(minXp / step, maxXp / step); + const xpAmounts: number[] = []; + if (stageCount < 4) { + const perStage = Math.ceil(totalDeciXp / stageCount) * step; + for (let i = 0; i != stageCount; ++i) { + xpAmounts.push(perStage); + } + } else { + const perStage = Math.ceil(Math.round(totalDeciXp * 0.667) / (stageCount - 1)) * step; + for (let i = 0; i != stageCount - 1; ++i) { + xpAmounts.push(perStage); + } + xpAmounts.push(Math.ceil(totalDeciXp * 0.332) * step); + } + return xpAmounts; +}; +// Test vectors: +//console.log(generateXpAmounts(new SRng(1337n), 5, 5000, 5000)); // [840, 840, 840, 840, 1660] +//console.log(generateXpAmounts(new SRng(1337n), 3, 40, 40)); // [14, 14, 14] +//console.log(generateXpAmounts(new SRng(1337n), 5, 150, 150)); // [25, 25, 25, 25, 50] +//console.log(generateXpAmounts(new SRng(1337n), 4, 10, 10)); // [2, 2, 2, 4] +//console.log(generateXpAmounts(new SRng(1337n), 4, 15, 15)); // [4, 4, 4, 5] +//console.log(generateXpAmounts(new SRng(1337n), 4, 20, 20)); // [5, 5, 5, 7] + +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)); + + const seed = new SRng(bountyCycle).randomInt(0, 100_000); + const bountyCycleStart = bountyCycle * 9000000; + const bountyCycleEnd = bountyCycleStart + 9000000; + + { + const rng = new SRng(seed); + const pool = [...eidolonJobs]; + 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.randomElementPop(pool), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierATable${table}Rewards`, + masteryReq: 0, + minEnemyLevel: 5, + maxEnemyLevel: 15, + xpAmounts: generateXpAmounts(rng, 3, 1000, 1500) + }, + { + jobType: rng.randomElementPop(pool), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierBTable${table}Rewards`, + masteryReq: 1, + minEnemyLevel: 10, + maxEnemyLevel: 30, + xpAmounts: generateXpAmounts(rng, 3, 1750, 2250) + }, + { + jobType: rng.randomElementPop(pool), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierCTable${table}Rewards`, + masteryReq: 2, + minEnemyLevel: 20, + maxEnemyLevel: 40, + xpAmounts: generateXpAmounts(rng, 4, 2500, 3000) + }, + { + jobType: rng.randomElementPop(pool), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierDTable${table}Rewards`, + masteryReq: 3, + minEnemyLevel: 30, + maxEnemyLevel: 50, + xpAmounts: generateXpAmounts(rng, 5, 3250, 3750) + }, + { + jobType: rng.randomElementPop(pool), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETable${table}Rewards`, + masteryReq: 5, + minEnemyLevel: 40, + maxEnemyLevel: 60, + xpAmounts: generateXpAmounts(rng, 5, 4000, 4500) + }, + { + jobType: rng.randomElementPop(pool), + 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: generateXpAmounts(rng, 5, 4500, 5000) + } + ] + }); + } + + { + const rng = new SRng(seed); + const pool = [...venusJobs]; + 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.randomElementPop(pool), + rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierATable${table}Rewards`, + masteryReq: 0, + minEnemyLevel: 5, + maxEnemyLevel: 15, + xpAmounts: generateXpAmounts(rng, 3, 1000, 1500) + }, + { + jobType: rng.randomElementPop(pool), + rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierBTable${table}Rewards`, + masteryReq: 1, + minEnemyLevel: 10, + maxEnemyLevel: 30, + xpAmounts: generateXpAmounts(rng, 3, 1750, 2250) + }, + { + jobType: rng.randomElementPop(pool), + rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierCTable${table}Rewards`, + masteryReq: 2, + minEnemyLevel: 20, + maxEnemyLevel: 40, + xpAmounts: generateXpAmounts(rng, 4, 2500, 3000) + }, + { + jobType: rng.randomElementPop(pool), + rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierDTable${table}Rewards`, + masteryReq: 3, + minEnemyLevel: 30, + maxEnemyLevel: 50, + xpAmounts: generateXpAmounts(rng, 5, 3250, 3750) + }, + { + jobType: rng.randomElementPop(pool), + rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable${table}Rewards`, + masteryReq: 5, + minEnemyLevel: 40, + maxEnemyLevel: 60, + xpAmounts: generateXpAmounts(rng, 5, 4000, 4500) + }, + { + jobType: rng.randomElementPop(pool), + 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: generateXpAmounts(rng, 5, 4500, 5000) + } + ] + }); + } + + { + const rng = new SRng(seed); + const pool = [...microplanetJobs]; + 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.randomElementPop(pool), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierATable${table}Rewards`, + masteryReq: 0, + minEnemyLevel: 5, + maxEnemyLevel: 15, + xpAmounts: generateXpAmounts(rng, 3, 12, 18) + }, + { + jobType: rng.randomElementPop(pool), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierCTable${table}Rewards`, + masteryReq: 1, + minEnemyLevel: 15, + maxEnemyLevel: 25, + xpAmounts: generateXpAmounts(rng, 3, 24, 36) + }, + { + 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.randomElementPop(pool), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierDTable${deimosDTable}Rewards`, + masteryReq: 2, + minEnemyLevel: 30, + maxEnemyLevel: 40, + xpAmounts: generateXpAmounts(rng, 4, 72, 88) + }, + { + jobType: rng.randomElementPop(pool), + rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards`, + masteryReq: 3, + minEnemyLevel: 40, + maxEnemyLevel: 60, + xpAmounts: generateXpAmounts(rng, 5, 115, 135) + }, + { + jobType: rng.randomElementPop(pool), + 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"] + }; +}; + +// Not very faithful, but to avoid the same node coming up back-to-back (which is not valid), I've split these into 2 arrays which we're alternating between. + +const voidStormMissions = { + VoidT1: [ + "CrewBattleNode519", + "CrewBattleNode518", + "CrewBattleNode515", + "CrewBattleNode503", + "CrewBattleNode509", + "CrewBattleNode522", + "CrewBattleNode511", + "CrewBattleNode512" + ], + VoidT2: ["CrewBattleNode501", "CrewBattleNode534", "CrewBattleNode530", "CrewBattleNode535", "CrewBattleNode533"], + VoidT3: ["CrewBattleNode521", "CrewBattleNode516", "CrewBattleNode524", "CrewBattleNode525"], + VoidT4: [ + "CrewBattleNode555", + "CrewBattleNode553", + "CrewBattleNode554", + "CrewBattleNode539", + "CrewBattleNode531", + "CrewBattleNode527", + "CrewBattleNode542", + "CrewBattleNode538", + "CrewBattleNode543", + "CrewBattleNode536", + "CrewBattleNode550", + "CrewBattleNode529" + ] +} as const; + +const voidStormLookbehind = { + VoidT1: 3, + VoidT2: 1, + VoidT3: 1, + VoidT4: 3 +} as const; + +const pushVoidStorms = (arr: IVoidStorm[], hour: number): void => { + const activation = hour * unixTimesInMs.hour + 40 * unixTimesInMs.minute; + const expiry = activation + 90 * unixTimesInMs.minute; + let accum = 0; + const tierIdx = { VoidT1: hour * 2, VoidT2: hour, VoidT3: hour, VoidT4: hour * 2 }; + for (const tier of ["VoidT1", "VoidT1", "VoidT2", "VoidT3", "VoidT4", "VoidT4"] as const) { + arr.push({ + _id: { + $oid: + ((activation / 1000) & 0xffffffff).toString(16).padStart(8, "0") + + "0321e89b" + + (accum++).toString().padStart(8, "0") + }, + Node: sequentiallyUniqueRandomElement( + voidStormMissions[tier], + tierIdx[tier]++, + voidStormLookbehind[tier], + 2051969264 + )!, + Activation: { $date: { $numberLong: activation.toString() } }, + Expiry: { $date: { $numberLong: expiry.toString() } }, + ActiveMissionTier: tier + }); + } +}; + +interface ITimeConstraint { + name: string; + isValidTime: (timeSecs: number) => boolean; + getIdealTimeBefore: (timeSecs: number) => number; +} + +const eidolonDayConstraint: ITimeConstraint = { + name: "eidolon day", + isValidTime: (timeSecs: number): boolean => { + const eidolonEpoch = 1391992660; + const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / 9000); + const eidolonCycleStart = eidolonEpoch + eidolonCycle * 9000; + const eidolonCycleEnd = eidolonCycleStart + 9000; + const eidolonCycleNightStart = eidolonCycleEnd - 3000; + return !isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, eidolonCycleNightStart * 1000); + }, + getIdealTimeBefore: (timeSecs: number): number => { + const eidolonEpoch = 1391992660; + const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / 9000); + const eidolonCycleStart = eidolonEpoch + eidolonCycle * 9000; + return eidolonCycleStart; + } +}; + +const eidolonNightConstraint: ITimeConstraint = { + name: "eidolon night", + isValidTime: (timeSecs: number): boolean => { + const eidolonEpoch = 1391992660; + const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / 9000); + const eidolonCycleStart = eidolonEpoch + eidolonCycle * 9000; + const eidolonCycleEnd = eidolonCycleStart + 9000; + const eidolonCycleNightStart = eidolonCycleEnd - 3000; + return ( + timeSecs >= eidolonCycleNightStart && + !isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, eidolonCycleEnd * 1000) + ); + }, + getIdealTimeBefore: (timeSecs: number): number => { + const eidolonEpoch = 1391992660; + const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / 9000); + const eidolonCycleStart = eidolonEpoch + eidolonCycle * 9000; + const eidolonCycleEnd = eidolonCycleStart + 9000; + const eidolonCycleNightStart = eidolonCycleEnd - 3000; + if (eidolonCycleNightStart > timeSecs) { + // Night hasn't started yet, but we need to return a time in the past. + return eidolonCycleNightStart - 9000; + } + return eidolonCycleNightStart; + } +}; + +const venusColdConstraint: ITimeConstraint = { + name: "venus cold", + isValidTime: (timeSecs: number): boolean => { + const vallisEpoch = 1541837628; + const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600); + const vallisCycleStart = vallisEpoch + vallisCycle * 1600; + const vallisCycleEnd = vallisCycleStart + 1600; + const vallisCycleColdStart = vallisCycleStart + 400; + return ( + timeSecs >= vallisCycleColdStart && + !isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, vallisCycleEnd * 1000) + ); + }, + getIdealTimeBefore: (timeSecs: number): number => { + const vallisEpoch = 1541837628; + const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600); + const vallisCycleStart = vallisEpoch + vallisCycle * 1600; + const vallisCycleColdStart = vallisCycleStart + 400; + if (vallisCycleColdStart > timeSecs) { + // Cold hasn't started yet, but we need to return a time in the past. + return vallisCycleColdStart - 1600; + } + return vallisCycleColdStart; + } +}; + +const venusWarmConstraint: ITimeConstraint = { + name: "venus warm", + isValidTime: (timeSecs: number): boolean => { + const vallisEpoch = 1541837628; + const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600); + const vallisCycleStart = vallisEpoch + vallisCycle * 1600; + const vallisCycleColdStart = vallisCycleStart + 400; + return !isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, vallisCycleColdStart * 1000); + }, + getIdealTimeBefore: (timeSecs: number): number => { + const vallisEpoch = 1541837628; + const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600); + const vallisCycleStart = vallisEpoch + vallisCycle * 1600; + return vallisCycleStart; + } +}; + +const getIdealTimeSatsifyingConstraints = (constraints: ITimeConstraint[]): number => { + let timeSecs = Math.trunc(Date.now() / 1000); + let allGood; + do { + allGood = true; + for (const constraint of constraints) { + if (!constraint.isValidTime(timeSecs)) { + //logger.debug(`${constraint.name} is not happy with ${timeSecs}`); + const prevTimeSecs = timeSecs; + const suggestion = constraint.getIdealTimeBefore(timeSecs); + timeSecs = suggestion; + do { + timeSecs += 60; + if (timeSecs >= prevTimeSecs || !constraint.isValidTime(timeSecs)) { + timeSecs = suggestion; // Can't find a compromise; just take the suggestion and try to compromise on another constraint. + break; + } + } while (!constraints.every(constraint => constraint.isValidTime(timeSecs))); + allGood = false; + break; + } + } + } while (!allGood); + return timeSecs; +}; + +const fullyStockBaro = (vt: IVoidTrader): void => { + for (const armorSet of baro.armorSets) { + if (Array.isArray(armorSet[0])) { + for (const set of armorSet as IVoidTraderOffer[][]) { + for (const item of set) { + vt.Manifest.push(item); + } + } + } else { + for (const item of armorSet as IVoidTraderOffer[]) { + vt.Manifest.push(item); + } + } + } + for (const item of baro.rest) { + vt.Manifest.push(item); + } +}; + +const getVarziaRotation = (week: number): string => { + const seed = new SRng(week).randomInt(0, 100_000); + const rng = new SRng(seed); + return rng.randomElement(varzia.primeDualPacks)!.ItemType; +}; + +const getVarziaManifest = (dualPack: string): IPrimeVaultTraderOffer[] => { + const rotrationManifest = varzia.primeDualPacks.find(pack => pack.ItemType === dualPack); + if (!rotrationManifest) return []; + + const mainPack = [{ ItemType: rotrationManifest.ItemType, PrimePrice: 10 }]; + const singlePacks: IPrimeVaultTraderOffer[] = []; + const items: IPrimeVaultTraderOffer[] = []; + const bobbleHeads: IPrimeVaultTraderOffer[] = []; + + for (const singlePackType of rotrationManifest.SinglePacks) { + singlePacks.push({ ItemType: singlePackType, PrimePrice: 6 }); + + const sp = varzia.primeSinglePacks.find(pack => pack.ItemType === singlePackType); + if (sp) { + items.push(...sp.Items); + sp.BobbleHeads.forEach(bobbleHead => { + bobbleHeads.push({ ItemType: bobbleHead, PrimePrice: 1 }); + }); + } + } + + const relics = rotrationManifest.Relics.map(relic => ({ ItemType: relic, RegularPrice: 1 })); + + return [singlePacks[0], ...mainPack, singlePacks[1], ...items, ...bobbleHeads, ...relics]; +}; + +const getAllVarziaManifests = (): IPrimeVaultTraderOffer[] => { + const dualPacks: IPrimeVaultTraderOffer[] = []; + const singlePacks: IPrimeVaultTraderOffer[] = []; + const items: IPrimeVaultTraderOffer[] = []; + const bobbleHeads: IPrimeVaultTraderOffer[] = []; + const relics: IPrimeVaultTraderOffer[] = []; + + const singlePackSet = new Set(); + const itemsSet = new Set(); + const bobbleHeadsSet = new Set(); + + varzia.primeDualPacks.forEach(dualPack => { + dualPacks.push({ ItemType: dualPack.ItemType, PrimePrice: 10 }); + + dualPack.SinglePacks.forEach(singlePackType => { + if (!singlePackSet.has(singlePackType)) { + singlePackSet.add(singlePackType); + singlePacks.push({ ItemType: singlePackType, PrimePrice: 6 }); + } + + const sp = varzia.primeSinglePacks.find(pack => pack.ItemType === singlePackType)!; + sp.Items.forEach(item => { + if (!itemsSet.has(item.ItemType)) { + itemsSet.add(item.ItemType); + items.push(item); + } + }); + + sp.BobbleHeads.forEach(bobbleHead => { + if (!bobbleHeadsSet.has(bobbleHead)) { + bobbleHeadsSet.add(bobbleHead); + bobbleHeads.push({ ItemType: bobbleHead, PrimePrice: 1 }); + } + }); + }); + + relics.push(...dualPack.Relics.map(relic => ({ ItemType: relic, RegularPrice: 1 }))); + }); + + return [...dualPacks, ...singlePacks, ...items, ...bobbleHeads, ...relics]; +}; + +const createInvasion = (day: number, idx: number): IInvasion => { + const id = day * 3 + idx; + const defender = (["FC_GRINEER", "FC_CORPUS", day % 2 ? "FC_GRINEER" : "FC_CORPUS"] as const)[idx]; + const rng = new SRng(new SRng(id).randomInt(0, 1_000_000)); + const isInfestationOutbreak = rng.randomInt(0, 1) == 0; + const attacker = isInfestationOutbreak ? "FC_INFESTATION" : defender == "FC_GRINEER" ? "FC_CORPUS" : "FC_GRINEER"; + const startMs = EPOCH + day * 86400_000; + const oid = + ((startMs / 1000) & 0xffffffff).toString(16).padStart(8, "0") + + "fd148cb8" + + (idx & 0xffffffff).toString(16).padStart(8, "0"); + const node = sequentiallyUniqueRandomElement(invasionNodes[defender], id, 5, 690175)!; // Can't repeat the other 2 on this day nor the last 3 + const progress = (Date.now() - startMs) / 86400_000; + const countMultiplier = isInfestationOutbreak || rng.randomInt(0, 1) ? -1 : 1; // if defender is winning, count is negative + const fiftyPercent = rng.randomInt(1000, 29000); // introduce some 'yitter' for the percentages + const rewardFloat = rng.randomFloat(); + const rewardTier = rewardFloat < 0.201 ? "RARE" : rewardFloat < 0.7788 ? "COMMON" : "UNCOMMON"; + const attackerReward: IMissionReward = {}; + const defenderReward: IMissionReward = {}; + if (isInfestationOutbreak) { + defenderReward.countedItems = [ + rng.randomElement(invasionRewards[rng.randomInt(0, 1) ? "FC_INFESTATION" : defender][rewardTier])! + ]; + } else { + attackerReward.countedItems = [rng.randomElement(invasionRewards[attacker][rewardTier])!]; + defenderReward.countedItems = [rng.randomElement(invasionRewards[defender][rewardTier])!]; + } + return { + _id: { $oid: oid }, + Faction: attacker, + DefenderFaction: defender, + Node: node, + Count: Math.round( + (progress < 0.5 ? progress * 2 * fiftyPercent : fiftyPercent + (30_000 - fiftyPercent) * (progress - 0.5)) * + countMultiplier + ), + Goal: 30000, // Value seems to range from 30000 to 98000 in intervals of 1000. Higher values are increasingly rare. I don't think this is relevant for the frontend besides dividing count by it. + LocTag: isInfestationOutbreak + ? ExportRegions[node].missionIndex == 0 + ? "/Lotus/Language/Menu/InfestedInvasionBoss" + : "/Lotus/Language/Menu/InfestedInvasionGeneric" + : attacker == "FC_CORPUS" + ? "/Lotus/Language/Menu/CorpusInvasionGeneric" + : "/Lotus/Language/Menu/GrineerInvasionGeneric", + Completed: startMs + 86400_000 < Date.now(), // Sorta unfaithful. Invasions on live are (at least in part) in fluenced by people completing them. And otherwise also probably not hardcoded to last 24 hours. + ChainID: { $oid: oid }, + AttackerReward: attackerReward, + AttackerMissionInfo: { + seed: rng.randomInt(0, 1_000_000), + faction: defender + }, + DefenderReward: defenderReward, + DefenderMissionInfo: { + seed: rng.randomInt(0, 1_000_000), + faction: attacker + }, + Activation: { + $date: { + $numberLong: startMs.toString() + } + } + }; +}; + +export const getInvasionByOid = (oid: string): IInvasion | undefined => { + const arr = oid.split("fd148cb8"); + if (arr.length == 2 && arr[0].length == 8 && arr[1].length == 8) { + return createInvasion(idToDay(oid), parseInt(arr[1], 16)); + } + return undefined; +}; + +export const getWorldState = (buildLabel?: string): IWorldState => { + const constraints: ITimeConstraint[] = []; + if (config.worldState?.eidolonOverride) { + constraints.push(config.worldState.eidolonOverride == "day" ? eidolonDayConstraint : eidolonNightConstraint); + } + if (config.worldState?.vallisOverride) { + constraints.push(config.worldState.vallisOverride == "cold" ? venusColdConstraint : venusWarmConstraint); + } + if (config.worldState?.duviriOverride) { + const duviriMoods = ["sorrow", "fear", "joy", "anger", "envy"]; + const desiredMood = duviriMoods.indexOf(config.worldState.duviriOverride); + if (desiredMood == -1) { + logger.warn(`ignoring invalid config value for worldState.duviriOverride`, { + value: config.worldState.duviriOverride, + valid_values: duviriMoods + }); + } else { + constraints.push({ + name: `duviri ${config.worldState.duviriOverride}`, + isValidTime: (timeSecs: number): boolean => { + const moodIndex = Math.trunc(timeSecs / 7200); + return moodIndex % 5 == desiredMood; + }, + getIdealTimeBefore: (timeSecs: number): number => { + let moodIndex = Math.trunc(timeSecs / 7200); + moodIndex -= ((moodIndex % 5) - desiredMood + 5) % 5; // while (moodIndex % 5 != desiredMood) --moodIndex; + const moodStart = moodIndex * 7200; + return moodStart; + } + }); + } + } + const timeSecs = getIdealTimeSatsifyingConstraints(constraints); + if (constraints.length != 0) { + const delta = Math.trunc(Date.now() / 1000) - timeSecs; + if (delta > 1) { + logger.debug( + `reported time is ${delta} seconds behind real time to satisfy selected constraints (${constraints.map(x => x.name).join(", ")})` + ); + } + } + const timeMs = timeSecs * 1000; + const day = Math.trunc((timeMs - EPOCH) / 86400000); + const week = Math.trunc(day / 7); + const weekStart = EPOCH + week * 604800000; + const weekEnd = weekStart + 604800000; + const date = new Date(timeMs); + + const worldState: IWorldState = { + BuildLabel: typeof buildLabel == "string" ? buildLabel.split(" ").join("+") : buildConfig.buildLabel, + Time: timeSecs, + Goals: [], + Alerts: [], + Sorties: [], + LiteSorties: [], + ActiveMissions: [], + FlashSales: [], + GlobalUpgrades: [], + Invasions: [], + VoidTraders: [], + PrimeVaultTraders: [], + VoidStorms: [], + DailyDeals: [], + EndlessXpChoices: [], + KnownCalendarSeasons: [], + PVPChallengeInstances: [], + ...staticWorldState, + SyndicateMissions: [...staticWorldState.SyndicateMissions], + InGameMarket: { + LandingPage: { + Categories: staticWorldState.InGameMarket.LandingPage.Categories.map(c => ({ + ...c, + Items: [...c.Items] + })) + } + } + }; + + // Old versions seem to really get hung up on not being able to load these. + if (buildLabel && version_compare(buildLabel, "2017.10.12.17.04") < 0) { + worldState.PVPChallengeInstances = []; + } + + if (config.worldState?.tennoLiveRelay) { + worldState.Goals.push({ + _id: { + $oid: "687bf9400000000000000000" + }, + Activation: { + $date: { + $numberLong: "1752955200000" + } + }, + Expiry: { + $date: { + $numberLong: "2000000000000" + } + }, + Count: 0, + Goal: 0, + Success: 0, + Personal: true, + Desc: "/Lotus/Language/Locations/RelayStationTennoConB", + ToolTip: "/Lotus/Language/Locations/RelayStationTennoConDescB", + Icon: "/Lotus/Interface/Icons/Categories/IconTennoLive.png", + Tag: "TennoConRelayB", + Node: "TennoConBHUB6" + }); + } + if (config.worldState?.baroTennoConRelay) { + worldState.Goals.push({ + _id: { $oid: "687bb2f00000000000000000" }, + Activation: { $date: { $numberLong: "1752937200000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Count: 0, + Goal: 0, + Success: 0, + Personal: true, + //"Faction": "FC_GRINEER", + Desc: "/Lotus/Language/Locations/RelayStationTennoCon", + ToolTip: "/Lotus/Language/Locations/RelayStationTennoConDesc", + Icon: "/Lotus/Interface/Icons/Categories/IconTennoConSigil.png", + Tag: "TennoConRelay", + Node: "TennoConHUB2" + }); + const vt: IVoidTrader = { + _id: { $oid: "687809030379266d790495c6" }, + Activation: { $date: { $numberLong: "1752937200000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Character: "Baro'Ki Teel", + Node: "TennoConHUB2", + Manifest: [] + }; + worldState.VoidTraders.push(vt); + fullyStockBaro(vt); + } + const isFebruary = date.getUTCMonth() == 1; + if (config.worldState?.starDaysOverride ?? isFebruary) { + worldState.Goals.push({ + _id: { $oid: "67a4dcce2a198564d62e1647" }, + Activation: { + $date: { + $numberLong: config.worldState?.starDaysOverride + ? "1738868400000" + : Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1).toString() + } + }, + Expiry: { + $date: { + $numberLong: config.worldState?.starDaysOverride + ? "2000000000000" + : Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1).toString() + } + }, + 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" + }); + } + // The client gets kinda confused when multiple goals have the same tag, so considering these mutually exclusive. + if (config.worldState?.galleonOfGhouls == 1) { + worldState.Goals.push({ + _id: { $oid: "6814ddf00000000000000000" }, + Activation: { $date: { $numberLong: "1746198000000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Count: 0, + Goal: 1, + Success: 0, + Personal: true, + Bounty: true, + ClampNodeScores: true, + Node: "EventNode19", + MissionKeyName: "/Lotus/Types/Keys/GalleonRobberyAlert", + Desc: "/Lotus/Language/Events/GalleonRobberyEventMissionTitle", + Icon: "/Lotus/Interface/Icons/Player/GalleonRobberiesEvent.png", + Tag: "GalleonRobbery", + Reward: { + items: [ + "/Lotus/StoreItems/Types/Recipes/Weapons/GrnChainSawTonfaBlueprint", + "/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem" + ] + } + }); + } else if (config.worldState?.galleonOfGhouls == 2) { + worldState.Goals.push({ + _id: { $oid: "681e18700000000000000000" }, + Activation: { $date: { $numberLong: "1746802800000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Count: 0, + Goal: 1, + Success: 0, + Personal: true, + Bounty: true, + ClampNodeScores: true, + Node: "EventNode28", // Incompatible with Wolf Hunt (2025), Orphix Venom, Warframe Anniversary + MissionKeyName: "/Lotus/Types/Keys/GalleonRobberyAlertB", + Desc: "/Lotus/Language/Events/GalleonRobberyEventMissionTitle", + Icon: "/Lotus/Interface/Icons/Player/GalleonRobberiesEvent.png", + Tag: "GalleonRobbery", + Reward: { + items: [ + "/Lotus/StoreItems/Types/Recipes/Weapons/MortiforShieldAndSwordBlueprint", + "/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem" + ] + } + }); + } else if (config.worldState?.galleonOfGhouls == 3) { + worldState.Goals.push({ + _id: { $oid: "682752f00000000000000000" }, + Activation: { $date: { $numberLong: "1747407600000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Count: 0, + Goal: 1, + Success: 0, + Personal: true, + Bounty: true, + ClampNodeScores: true, + Node: "EventNode19", + MissionKeyName: "/Lotus/Types/Keys/GalleonRobberyAlertC", + Desc: "/Lotus/Language/Events/GalleonRobberyEventMissionTitle", + Icon: "/Lotus/Interface/Icons/Player/GalleonRobberiesEvent.png", + Tag: "GalleonRobbery", + Reward: { + items: [ + "/Lotus/Types/StoreItems/Packages/EventCatalystReactorBundle", + "/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem" + ] + } + }); + } + + const firstNovemberWeekday = new Date(Date.UTC(date.getUTCFullYear(), 10, 1)).getUTCDay(); + const firstNovemberMondayOffset = (8 - firstNovemberWeekday) % 7; + + const plagueStarStart = Date.UTC(date.getUTCFullYear(), 10, firstNovemberMondayOffset + 1, 16); + const plagueStarEnd = Date.UTC(date.getUTCFullYear(), 10, firstNovemberMondayOffset + 15, 16); + + const isPlagueStarActive = timeMs >= plagueStarStart && timeMs < plagueStarEnd; + if (config.worldState?.plagueStarOverride ?? isPlagueStarActive) { + worldState.Goals.push({ + _id: { $oid: "654a5058c757487cdb11824f" }, + Activation: { + $date: { + $numberLong: config.worldState?.plagueStarOverride ? "1699372800000" : plagueStarStart.toString() + } + }, + Expiry: { + $date: { + $numberLong: config.worldState?.plagueStarOverride ? "2000000000000" : plagueStarEnd.toString() + } + }, + Tag: "InfestedPlains", + RegionIdx: 2, + Faction: "FC_INFESTATION", + Desc: "/Lotus/Language/InfestedPlainsEvent/InfestedPlainsBountyName", + ToolTip: "/Lotus/Language/InfestedPlainsEvent/InfestedPlainsBountyDesc", + Icon: "/Lotus/Materials/Emblems/PlagueStarEventBadge_e.png", + JobAffiliationTag: "EventSyndicate", + Jobs: [ + { + jobType: "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBounty", + rewards: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/PlagueStarTableRewards", + minEnemyLevel: 15, + maxEnemyLevel: 25, + xpAmounts: [50, 300, 100, 575] + }, + { + jobType: "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBountyAdvanced", + rewards: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/PlagueStarTableRewards", + minEnemyLevel: 55, + maxEnemyLevel: 65, + xpAmounts: [200, 1000, 300, 1700], + requiredItems: [ + "/Lotus/StoreItems/Types/Items/Eidolon/InfestedEventIngredient", + "/Lotus/StoreItems/Types/Items/Eidolon/InfestedEventClanIngredient" + ], + useRequiredItemsAsMiscItemFee: true + }, + { + jobType: "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBountySteelPath", + rewards: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/PlagueStarTableSteelPathRewards", + minEnemyLevel: 100, + maxEnemyLevel: 110, + xpAmounts: [200, 1100, 400, 2100], + masteryReq: 10, + requiredItems: [ + "/Lotus/StoreItems/Types/Items/Eidolon/InfestedEventIngredient", + "/Lotus/StoreItems/Types/Items/Eidolon/InfestedEventClanIngredient" + ], + useRequiredItemsAsMiscItemFee: true + } + ], + Transmission: "/Lotus/Sounds/Dialog/PlainsMeteorLeadUp/LeadUp/DLeadUp0021Lotus", + InstructionalItem: "/Lotus/Types/StoreItems/Packages/PlagueStarEventStoreItem" + }); + } + + const firstAugustWeekday = new Date(Date.UTC(date.getUTCFullYear(), 7, 1)).getUTCDay(); + const firstAugustWednesdayOffset = (3 - firstAugustWeekday + 7) % 7; + const dogDaysStart = Date.UTC(date.getUTCFullYear(), 7, 1 + firstAugustWednesdayOffset, 15); + + const firstSeptemberWeekday = new Date(Date.UTC(date.getUTCFullYear(), 8, 1)).getUTCDay(); + const firstSeptemberWednesdayOffset = (3 - firstSeptemberWeekday + 7) % 7; + const dogDaysEnd = Date.UTC(date.getUTCFullYear(), 8, 1 + firstSeptemberWednesdayOffset, 15); + + const isDogDaysActive = timeMs >= dogDaysStart && timeMs < dogDaysEnd; + if (config.worldState?.dogDaysOverride ?? isDogDaysActive) { + const activationTimeStamp = config.worldState?.dogDaysOverride ? "1699372800000" : dogDaysStart.toString(); + const expiryTimeStamp = config.worldState?.dogDaysOverride ? "2000000000000" : dogDaysEnd.toString(); + const rewards = [ + [ + { + credits: 50000, + items: ["/Lotus/StoreItems/Upgrades/Skins/Weapons/Redeemer/RedeemerRelayWaterSkin"] + }, + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/Items/MiscItems/PhotoboothTileHydroidRelay"] + }, + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/Items/ShipDecos/RelayHydroidBobbleHead"] + }, + { + items: [ + "/Lotus/StoreItems/Types/Items/MiscItems/OrokinReactor", + "/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem" + ] + } + ], + [ + { + credits: 50000, + items: ["/Lotus/StoreItems/Upgrades/Skins/Sigils/DogDays2023ASigil"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 25 + } + ] + }, + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyBeachKavat"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 50 + } + ] + }, + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyRucksackKubrow"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 75 + } + ] + }, + { + items: ["/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropCleaningDroneBeachcomber"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 100 + } + ] + } + ], + [ + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/Seasonal/AvatarImageDogDays2024Glyph"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 25 + } + ] + }, + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/Items/ShipDecos/DogDays2024Poster"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 50 + } + ] + }, + { + credits: 50000, + items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/DogDaysKubrowBadgeItem"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 75 + } + ] + }, + { + items: ["/Lotus/StoreItems/Types/Items/ShipDecos/DogDays2024LisetPropCleaningDroneBeachcomber"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 100 + } + ] + } + ], + [ + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysHydroidGlyph"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 25 + } + ] + }, + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysLokiGlyph"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 50 + } + ] + }, + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysNovaGlyph"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 75 + } + ] + }, + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageDogDaysValkyrGlyph"], + countedItems: [ + { + ItemType: "/Lotus/Types/Items/MiscItems/WaterFightBucks", + ItemCount: 100 + } + ] + } + ] + ]; + + const year = config.worldState?.dogDaysRewardsOverride ?? 3; + + worldState.Goals.push({ + _id: { + $oid: + ((dogDaysStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + + "c57487c3768936d" + + year.toString(16) + }, + Activation: { $date: { $numberLong: activationTimeStamp } }, + Expiry: { $date: { $numberLong: expiryTimeStamp } }, + Count: 0, + Goal: 100, + InterimGoals: [25, 50], + BonusGoal: 200, + Success: 0, + Personal: true, + Bounty: true, + ClampNodeScores: true, + Node: "EventNode25", // Incompatible with Hallowed Flame, Hallowed Nightmares, Warframe Anniversary + ConcurrentMissionKeyNames: [ + "/Lotus/Types/Keys/TacAlertKeyWaterFightB", + "/Lotus/Types/Keys/TacAlertKeyWaterFightC", + "/Lotus/Types/Keys/TacAlertKeyWaterFightD" + ], + ConcurrentNodeReqs: [25, 50, 100], + ConcurrentNodes: ["EventNode24", "EventNode34", "EventNode35"], // Incompatible with Hallowed Flame, Hallowed Nightmares, Warframe Anniversary + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyWaterFightA", + Faction: "FC_CORPUS", + Desc: "/Lotus/Language/Alerts/TacAlertWaterFight", + Icon: "/Lotus/Interface/Icons/StoreIcons/Emblems/SplashEventIcon.png", + Tag: "WaterFight", + InterimRewards: rewards[year].slice(0, 2), + Reward: rewards[year][2], + BonusReward: rewards[year][3], + ScoreVar: "Team1Score", + NightLevel: "/Lotus/Levels/GrineerBeach/GrineerBeachEventNight.level" + }); + + const baseStoreItem = { + ShowInMarket: true, + HideFromMarket: false, + SupporterPack: false, + Discount: 0, + BogoBuy: 0, + BogoGet: 0, + StartDate: { $date: { $numberLong: activationTimeStamp } }, + EndDate: { $date: { $numberLong: expiryTimeStamp } }, + ProductExpiryOverride: { $date: { $numberLong: expiryTimeStamp } } + }; + + const storeItems = [ + { + TypeName: "/Lotus/Types/StoreItems/Packages/WaterFightNoggleBundle", + PremiumOverride: 240, + RegularOverride: 0 + }, + { + TypeName: "/Lotus/Types/Items/ShipDecos/Events/WFBeastMasterBobbleHead", + PremiumOverride: 35, + RegularOverride: 0 + }, + { + TypeName: "/Lotus/Types/Items/ShipDecos/Events/WFChargerBobbleHead", + PremiumOverride: 35, + RegularOverride: 0 + }, + { + TypeName: "/Lotus/Types/Items/ShipDecos/Events/WFEngineerBobbleHead", + PremiumOverride: 35, + RegularOverride: 0 + }, + { + TypeName: "/Lotus/Types/Items/ShipDecos/Events/WFGruntBobbleHead", + PremiumOverride: 35, + RegularOverride: 0 + }, + { + TypeName: "/Lotus/Types/StoreItems/AvatarImages/ImagePopsicleGrineerPurple", + PremiumOverride: 0, + RegularOverride: 1 + }, + { + TypeName: "/Lotus/Types/Items/ShipDecos/Events/WFHealerBobbleHead", + PremiumOverride: 35, + RegularOverride: 0 + }, + { + TypeName: "/Lotus/Types/Items/ShipDecos/Events/WFHeavyBobbleHead", + PremiumOverride: 35, + RegularOverride: 0 + }, + { + TypeName: "/Lotus/Types/Items/ShipDecos/Events/WFHellionBobbleHead", + PremiumOverride: 35, + RegularOverride: 0 + }, + { + TypeName: "/Lotus/Types/Items/ShipDecos/Events/WFSniperBobbleHead", + PremiumOverride: 35, + RegularOverride: 0 + }, + { + TypeName: "/Lotus/Types/Items/ShipDecos/Events/WFTankBobbleHead", + PremiumOverride: 35, + RegularOverride: 0 + }, + { + TypeName: "/Lotus/Types/StoreItems/SuitCustomizations/ColourPickerRollers", + PremiumOverride: 75, + RegularOverride: 0 + } + ]; + + worldState.FlashSales.push(...storeItems.map(item => ({ ...baseStoreItem, ...item }))); + + const seasonalItems = storeItems.map(item => item.TypeName); + + const seasonalCategory = worldState.InGameMarket.LandingPage.Categories.find(c => c.CategoryName == "SEASONAL"); + + if (seasonalCategory) { + seasonalCategory.Items ??= []; + seasonalCategory.Items.push(...seasonalItems); + } else { + worldState.InGameMarket.LandingPage.Categories.push({ + CategoryName: "SEASONAL", + Name: "/Lotus/Language/Store/SeasonalCategoryTitle", + Icon: "seasonal", + AddToMenu: true, + Items: seasonalItems + }); + } + } + + if (config.worldState?.anniversary != undefined) { + // Incompatible with: Use Tag from Warframe Anniversary for old Events, Wolf Hunt (2025), Galleon Of Ghouls, Hallowed Flame, Hallowed Nightmares, Dog Days, Proxy Rebellion, Long Shadow + const goalsByWeek: Partial[][] = [ + [ + { + Node: "EventNode28", + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2019E", + Tag: "Anniversary2019TacAlert", + Reward: { + items: ["/Lotus/StoreItems/Upgrades/Skins/Excalibur/ExcaliburDexSkin"] + } + }, + { + Node: "EventNode26", + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2020F", + Tag: "Anniversary2020TacAlert", + Reward: { + items: ["/Lotus/StoreItems/Types/Items/ShipDecos/ExcaliburDexBobbleHead"] + } + }, + { + Node: "EventNode19", + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2024ChallengeModeA", + Tag: "Anniversary2024TacAlertCMA", + Reward: { + items: ["/Lotus/StoreItems/Types/Items/MiscItems/WeaponUtilityUnlocker"] + } + } + ], + [ + { + Node: "EventNode24", + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2017C", + Tag: "Anniversary2018TacAlert", + Reward: { + items: ["/Lotus/StoreItems/Weapons/Tenno/LongGuns/DexTheThird/DexTheThird"] + } + }, + { + Node: "EventNode18", + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2020H", + Tag: "Anniversary2020TacAlert", + Reward: { + items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/ImageDexAnniversary"] + } + } + ], + [ + { + Node: "EventNode18", + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2022J", + Tag: "Anniversary2022TacAlert", + Reward: { + items: ["/Lotus/StoreItems/Upgrades/Skins/Rhino/RhinoDexSkin"] + } + }, + { + Node: "EventNode38", + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2025D", + Tag: "Anniversary2020TacAlert", + Reward: { + items: ["/Lotus/StoreItems/Types/Items/ShipDecos/RhinoDexBobbleHead"] + } + }, + { + Node: "EventNode27", + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2025ChallengeModeA", + Tag: "Anniversary2024TacAlertCMA", + Reward: { + items: ["/Lotus/StoreItems/Types/Items/MiscItems/OrokinCatalyst"] + } + } + ], + [ + { + Node: "EventNode2", + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2020G", + Tag: "Anniversary2020TacAlert", + Reward: { + items: ["/Lotus/StoreItems/Upgrades/Skins/Liset/DexLisetSkin"] + } + }, + { + Node: "EventNode17", + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2017B", + Tag: "Anniversary2018TacAlert", + Reward: { + items: ["/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/DexTheSecond/DexTheSecond"] + } + } + ], + [ + { + Node: "EventNode18", + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2017A", + Tag: "Anniversary2018TacAlert", + Reward: { + items: ["/Lotus/StoreItems/Weapons/Tenno/Pistols/DexFuris/DexFuris"] + } + }, + { + Node: "EventNode26", + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2023K", + Tag: "Anniversary2025TacAlert", + Reward: { + items: ["/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageCommunityClemComic"] + } + }, + { + Node: "EventNode12", + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2025ChallengeModeB", + Tag: "Anniversary2025TacAlertCMB", + Reward: { + items: ["/Lotus/StoreItems/Types/Items/MiscItems/WeaponPrimaryArcaneUnlocker"] + } + } + ], + [ + { + Node: "EventNode17", + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2025A", + Tag: "Anniversary2025TacAlert", + Reward: { + items: [ + "/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/KatanaAndWakizashi/Dex2023Nikana/Dex2023Nikana" + ] + } + }, + { + Node: "EventNode27", + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2018D", + Tag: "Anniversary2018TacAlert", + Reward: { + items: ["/Lotus/StoreItems/Upgrades/Skins/Scarves/DexScarf"] + } + } + ], + [ + { + Node: "EventNode38", + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2025C", + Tag: "Anniversary2018TacAlert", + Reward: { + items: ["/Lotus/StoreItems/Upgrades/Skins/Wisp/DexWispSkin"] + } + }, + { + Node: "EventNode12", + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2024L", + Tag: "Anniversary2024TacAlert", + Reward: { + items: ["/Lotus/Types/StoreItems/Packages/OperatorDrifterDexBundle"] + } + }, + { + Node: "EventNode26", + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2024ChallengeModeB", + Tag: "Anniversary2024TacAlertCMB", + Reward: { + items: ["/Lotus/StoreItems/Types/Recipes/Components/UmbraFormaBlueprint"] + } + } + ], + [ + { + Node: "EventNode37", + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2021I", + Tag: "Anniversary2021TacAlert", + Reward: { + items: [ + "/Lotus/StoreItems/Upgrades/Skins/Armor/Dex2020Armor/Dex2020ArmorAArmor", + "/Lotus/StoreItems/Upgrades/Skins/Armor/Dex2020Armor/Dex2020ArmorCArmor", + "/Lotus/StoreItems/Upgrades/Skins/Armor/Dex2020Armor/Dex2020ArmorLArmor", + "/Lotus/StoreItems/Types/Game/CatbrowPet/CatbrowGeneticSignature" + ], + countedItems: [ + { + ItemType: "/Lotus/Types/Game/CatbrowPet/CatbrowGeneticSignature", + ItemCount: 10 + } + ] + } + }, + { + Node: "EventNode9", + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyAnniversary2025B", + Tag: "Anniversary2025TacAlert", + Reward: { + items: ["/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/ColourPickerAnniversaryEleven"] + } + } + ] + ]; + goalsByWeek[config.worldState.anniversary].forEach((goal, i) => { + worldState.Goals.push({ + _id: { + $oid: + "67c6d8e725b23feb" + + config.worldState?.anniversary!.toString(16).padStart(4, "0") + + i.toString(16).padStart(4, "0") + }, + Activation: { $date: { $numberLong: "1745593200000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Count: 0, + Goal: 1, + Success: 0, + Personal: true, + ClampNodeScores: true, + Node: goal.Node, + MissionKeyName: goal.MissionKeyName, + Desc: goal.Tag!.endsWith("CMB") + ? "/Lotus/Language/Events/Anniversary2024ChallengeMode" + : "/Lotus/Language/G1Quests/Anniversary2017MissionTitle", + Icon: "/Lotus/Interface/Icons/Player/GlyphLotus12Anniversary.png", + Tag: goal.Tag!, + Reward: goal.Reward + }); + }); + } + + if (config.worldState?.wolfHunt) { + worldState.Goals.push({ + _id: { + $oid: "67ed7672798d6466172e3b9d" + }, + Activation: { + $date: { + $numberLong: "1743616800000" + } + }, + Expiry: { + $date: { + $numberLong: "2000000000000" + } + }, + Count: 0, + Goal: 3, + InterimGoals: [1, 2], + BonusGoal: 4, + Success: 0, + Personal: true, + Bounty: true, + ClampNodeScores: true, + Node: "EventNode29", + ConcurrentMissionKeyNames: [ + "/Lotus/Types/Keys/WolfTacAlertReduxB", + "/Lotus/Types/Keys/WolfTacAlertReduxC", + "/Lotus/Types/Keys/WolfTacAlertReduxD" + ], + ConcurrentNodeReqs: [1, 2, 3], + ConcurrentNodes: ["EventNode28", "EventNode39", "EventNode40"], // Incompatible with Galleon Of Ghouls, Orphix Venom, Warframe Anniversary + MissionKeyName: "/Lotus/Types/Keys/WolfTacAlertReduxA", + Faction: "FC_GRINEER", + Desc: "/Lotus/Language/Alerts/WolfAlert", + Icon: "/Lotus/Interface/Icons/Npcs/Seasonal/WolfStalker.png", + Tag: "WolfHuntRedux", + InterimRewards: [ + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/Recipes/Weapons/WeaponParts/ThrowingHammerHandle"] + }, + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/Recipes/Weapons/WeaponParts/ThrowingHammerHead"] + } + ], + Reward: { + credits: 50000, + items: ["/Lotus/StoreItems/Types/Recipes/Weapons/WeaponParts/ThrowingHammerMotor"] + }, + BonusReward: { + credits: 50000, + items: [ + "/Lotus/StoreItems/Types/Recipes/Weapons/ThrowingHammerBlueprint", + "/Lotus/StoreItems/Types/Items/MiscItems/OrokinCatalyst", + "/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem" + ] + } + }); + } + + const tagsForOlderGoals: string[] = [ + "Anniversary2018TacAlert", + "Anniversary2019TacAlert", + "Anniversary2020TacAlert", + "Anniversary2021TacAlert", + "Anniversary2022TacAlert", + "Anniversary2024TacAlert", + "Anniversary2024TacAlertCMA", + "Anniversary2025TacAlert", + "Anniversary2025TacAlertCMB" + ]; + + if (config.worldState?.hallowedFlame) { + worldState.Goals.push( + { + _id: { $oid: "5db305403d34b5158873519a" }, + Activation: { $date: { $numberLong: "1699372800000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Count: 0, + Goal: 3, + InterimGoals: [1, 2], + Success: 0, + Personal: true, + Bounty: true, + ClampNodeScores: true, + Node: "EventNode24", // Incompatible with Hallowed Nightmares, Dog Days + ConcurrentMissionKeyNames: [ + "/Lotus/Types/Keys/LanternEndlessEventKeyB", + "/Lotus/Types/Keys/LanternEndlessEventKeyC" + ], + ConcurrentNodeReqs: [1, 2], + ConcurrentNodes: ["EventNode25", "EventNode34"], // Incompatible with Hallowed Nightmares, Dog Days + MissionKeyName: "/Lotus/Types/Keys/LanternEndlessEventKeyA", + Faction: "FC_INFESTATION", + Desc: "/Lotus/Language/Events/TacAlertHalloweenLantern", + Icon: "/Lotus/Interface/Icons/JackOLanternColour.png", + Tag: config.unfaithfulBugFixes?.useAnniversaryTagForOldGoals ? tagsForOlderGoals[0] : "Halloween19", + InterimRewards: [ + { items: ["/Lotus/StoreItems/Types/Items/MiscItems/OrokinCatalyst"] }, + { items: ["/Lotus/StoreItems/Types/Items/MiscItems/Forma"] } + ], + Reward: { + items: ["/Lotus/StoreItems/Types/Items/MiscItems/FormaAura"] + } + }, + { + _id: { $oid: "5db3054a3d34b5158873519c" }, + Activation: { $date: { $numberLong: "1699372800000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Count: 0, + Goal: 900, + Success: 0, + Personal: true, + Bounty: true, + Best: true, + ClampNodeScores: true, + Node: "EventNode35", + MissionKeyName: "/Lotus/Types/Keys/LanternEndlessEventKeyD", + Faction: "FC_INFESTATION", + Desc: "/Lotus/Language/Events/TacAlertHalloweenLanternEndless", + Icon: "/Lotus/Interface/Icons/JackOLanternColour.png", + Tag: "Halloween19Endless", + PrereqGoalTags: [ + config.unfaithfulBugFixes?.useAnniversaryTagForOldGoals ? tagsForOlderGoals[0] : "Halloween19" + ], + Reward: { + items: [ + "/Lotus/StoreItems/Upgrades/Skins/Effects/BatsEphemera", + "/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem" + ] + }, + ScoreVar: "EndlessMissionTimeElapsed", + ScoreMaxTag: "Halloween19ScoreMax" + } + ); + } + + if (config.worldState?.hallowedNightmares) { + const rewards = [ + // 2018 + [ + { + items: ["/Lotus/StoreItems/Upgrades/Skins/Sigils/DotD2016Sigil"] + }, + { + items: ["/Lotus/StoreItems/Upgrades/Skins/Halloween/HalloweenDread"] + }, + { + items: ["/Lotus/StoreItems/Types/Items/MiscItems/OrokinReactor"] + } + ], + // 2016 + [ + { + items: ["/Lotus/StoreItems/Upgrades/Skins/Sigils/OrokinCatalyst"] + }, + { + items: ["/Lotus/StoreItems/Upgrades/Skins/Sigils/DotD2016Sigil"] + }, + { + items: [ + "/Lotus/StoreItems/Types/Items/MiscItems/OrokinReactor", + "/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem" + ] + } + ], + // 2015 + [ + { + items: ["/Lotus/StoreItems/Upgrades/Skins/Sigils/OrokinCatalyst"] + }, + { + items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"] + } + ] + ]; + const year = config.worldState.hallowedNightmaresRewardsOverride ?? 0; + + worldState.Goals.push({ + _id: { $oid: "5bc98f00000000000000000" + year.toString(16) }, + Activation: { $date: { $numberLong: "1539972000000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Count: 0, + InterimGoals: [1], + Goal: 2, + Success: 0, + Personal: true, + Bounty: true, + Tag: config.unfaithfulBugFixes?.useAnniversaryTagForOldGoals ? tagsForOlderGoals[0] : "Halloween", + Faction: "FC_INFESTATION", + Desc: "/Lotus/Language/G1Quests/TacAlertHalloweenTitle", + ToolTip: "/Lotus/Language/G1Quests/TacAlertHalloweenToolTip", + Icon: "/Lotus/Interface/Icons/JackOLanternColour.png", + ClampNodeScores: true, + Node: "EventNode2", // Incompatible with Warframe Anniversary + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyHalloween", + ConcurrentMissionKeyNames: ["/Lotus/Types/Keys/TacAlertKeyHalloweenBonus"], + ConcurrentNodeReqs: [1], + ConcurrentNodes: ["EventNode24"], // Incompatible with Hallowed Flame, Dog Days, Warframe Anniversary + InterimRewards: [rewards[year][0]], + Reward: rewards[year][1] + }); + if (year != 2) { + worldState.Goals.push({ + _id: { $oid: "5bc98f01000000000000000" + year.toString(16) }, + Activation: { $date: { $numberLong: "1539972000000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Count: 0, + Goal: 666, + Success: 0, + Personal: true, + Bounty: true, + Best: true, + Tag: "Halloween", + PrereqGoalTags: [ + config.unfaithfulBugFixes?.useAnniversaryTagForOldGoals ? tagsForOlderGoals[0] : "Halloween" + ], + Faction: "FC_INFESTATION", + Desc: "Hallowed Nightmares - Time Attack", + ToolTip: "/Lotus/Language/G1Quests/TacAlertHalloweenToolTip", + Icon: "/Lotus/Interface/Icons/JackOLanternColour.png", + ClampNodeScores: true, + Node: "EventNode25", // Incompatible with Hallowed Flame, Dog Days + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyHalloweenTimeAttack", + ScoreVar: "TimeAttackScore", + ScoreMaxTag: "Halloween16", + Reward: rewards[year][2] + }); + } + } + + if (config.worldState?.proxyRebellion) { + const rewards = [ + // 2019 + [ + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/Items/MiscItems/UtilityUnlocker"] + }, + { + credits: 50000, + items: ["/Lotus/StoreItems/Upgrades/Mods/Randomized/RawPistolRandomMod"] + }, + { + credits: 50000, + items: ["/Lotus/Types/StoreItems/Packages/EventCatalystReactorBundle"] + }, + { + items: [ + "/Lotus/StoreItems/Upgrades/Skins/Scarves/HornSkullScarf", + "/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem" + ] + } + ], + // 2018 + [ + { + credits: 50000, + items: ["/Lotus/StoreItems/Upgrades/Mods/FusionBundles/NightwatchFusionBundle"] + }, + { + credits: 50000, + items: ["/Lotus/StoreItems/Types/Items/MiscItems/UtilityUnlocker"] + }, + { + credits: 50000, + items: ["/Lotus/Types/StoreItems/Packages/EventCatalystReactorBundle"] + }, + { + items: [ + "/Lotus/StoreItems/Upgrades/Skins/Sigils/EnergySigilA", + "/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem" + ] + } + ] + ]; + const year = config.worldState.proxyRebellionRewardsOverride ?? 0; + + worldState.Goals.push({ + _id: { $oid: "5b5b5da0000000000000000" + year.toString(16) }, + Activation: { $date: { $numberLong: "1532714400000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Count: 0, + Goal: 3, + InterimGoals: [1, 2], + BonusGoal: 4, + Success: 0, + Personal: true, + Bounty: true, + ClampNodeScores: true, + Node: "EventNode18", // Incompatible with Warframe Anniversary + ConcurrentMissionKeyNames: [ + "/Lotus/Types/Keys/TacAlertKeyProxyRebellionTwo", + "/Lotus/Types/Keys/TacAlertKeyProxyRebellionThree", + "/Lotus/Types/Keys/TacAlertKeyProxyRebellionFour" + ], + ConcurrentNodeReqs: [1, 2, 3], + ConcurrentNodes: ["EventNode7", "EventNode4", "EventNode17"], // Incompatible with Orphix venom, Warframe Anniversary + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyProxyRebellionOne", + Faction: "FC_CORPUS", + Desc: "/Lotus/Language/Alerts/TacAlertProxyRebellion", + Icon: "/Lotus/Materials/Emblems/BountyBadge_e.png", + Tag: config.unfaithfulBugFixes?.useAnniversaryTagForOldGoals ? tagsForOlderGoals[1] : "ProxyRebellion", + InterimRewards: rewards[year].slice(0, 2), + Reward: rewards[year][2], + BonusReward: rewards[year][3] + }); + } + + if (config.worldState?.longShadow) { + worldState.Goals.push({ + _id: { $oid: "5bc9e8f7272d5d184c8398c9" }, + Activation: { $date: { $numberLong: "1539972000000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Count: 0, + InterimGoals: [1, 2], + Goal: 3, + BonusGoal: 4, + Success: 0, + Personal: true, + Bounty: true, + Tag: config.unfaithfulBugFixes?.useAnniversaryTagForOldGoals ? tagsForOlderGoals[2] : "NightwatchTacAlert", + Faction: "FC_GRINEER", + Desc: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertTitle", + Icon: "/Lotus/Materials/Emblems/BountyBadge_e.png", + ClampNodeScores: true, + Node: "EventNode9", // Incompatible with Warframe Anniversary + MissionKeyName: "/Lotus/Types/Keys/TacAlertKeyProjectNightwatchEasy", + ConcurrentMissionKeyNames: [ + "/Lotus/Types/Keys/TacAlertKeyProjectNightwatch", + "/Lotus/Types/Keys/TacAlertKeyProjectNightwatchHard", + "/Lotus/Types/Keys/TacAlertKeyProjectNightwatchBonus" + ], + ConcurrentNodeReqs: [1, 2, 3], + ConcurrentNodes: ["SolNode136", "EventNode3", "EventNode0"], + InterimRewards: [ + { + credits: 50000, + countedItems: [ + { ItemType: "/Lotus/Upgrades/Mods/FusionBundles/RareFusionBundle", ItemCount: 10 } // Not sure about that + ] + }, + { + items: ["/Lotus/StoreItems/Types/Items/MiscItems/UtilityUnlocker"] + } + ], + Reward: { + items: ["/Lotus/Types/StoreItems/Packages/EventCatalystReactorBundle"] + }, + BonusReward: { items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"] } + }); + } + if (config.worldState?.bellyOfTheBeast) { + worldState.Goals.push({ + _id: { $oid: "67a5035c2a198564d62e165e" }, + Activation: { $date: { $numberLong: "1738868400000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Count: config.worldState.bellyOfTheBeastProgressOverride ?? 0, + HealthPct: (config.worldState.bellyOfTheBeastProgressOverride ?? 0) / 100, + Goal: 0, + Personal: true, + Community: true, + ClanGoal: [72, 216, 648, 1944, 5832], + Tag: "JadeShadowsEvent", + Faction: "FC_MITW", + Desc: "/Lotus/Language/JadeShadows/JadeShadowsEventName", + ToolTip: "/Lotus/Language/JadeShadows/JadeShadowsShortEventDesc", + Icon: "/Lotus/Interface/Icons/WorldStatePanel/JadeShadowsEventBadge.png", + ScoreLocTag: "/Lotus/Language/JadeShadows/JadeShadowsEventScore", + Node: "SolNode723", + MissionKeyName: "/Lotus/Types/Keys/JadeShadowsEventMission", + ItemType: "/Lotus/Types/Gameplay/JadeShadows/Resources/AscensionEventResourceItem" + }); + } + if (config.worldState?.eightClaw) { + worldState.Goals.push({ + _id: { $oid: "685c15f80000000000000000" }, + Activation: { $date: { $numberLong: "1750865400000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Count: config.worldState.eightClawProgressOverride ?? 0, + HealthPct: (config.worldState.eightClawProgressOverride ?? 0) / 100, + Goal: 0, + Personal: true, + Community: true, + ClanGoal: [72, 216, 648, 1944, 5832], + Tag: "DuviriMurmurEvent", + Faction: "FC_MITW", + Desc: "/Lotus/Language/Isleweaver/DuviriMurmurEventTitle", + ToolTip: "/Lotus/Language/Isleweaver/DuviriMurmurEventDescription", + Icon: "/Lotus/Interface/Icons/WorldStatePanel/EightClawEventBadge.png", + ScoreLocTag: "/Lotus/Language/Isleweaver/DuviriMurmurEventScore", + Node: "SolNode236", + MissionKeyName: "/Lotus/Types/Keys/DuviriMITW/DuviriMITWEventKey" + }); + } + + if (config.worldState?.orphixVenom) { + worldState.Goals.push( + { + _id: { $oid: "5fdcccb875d5ad500dc477d0" }, + Activation: { $date: { $numberLong: "1608320400000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Count: 0, + Goal: 500, + Success: 0, + Personal: true, + Best: true, + Node: "EventNode17", // Incompatible with Proxy Rebellion + MissionKeyName: "/Lotus/Types/Keys/MechSurvivalCorpusShip", + Faction: "FC_SENTIENT", + Desc: "/Lotus/Language/Events/MechEventMissionTier1", + Icon: "/Lotus/Interface/Icons/Categories/IconMech256.png", + Tag: "MechSurvivalA", + ScoreVar: "MechSurvivalScore", + Reward: { + items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/MechEventEmblemItem"] + } + }, + { + _id: { $oid: "5fdcccb875d5ad500dc477d1" }, + Activation: { $date: { $numberLong: "1608320400000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Count: 0, + Goal: 1000, + Success: 0, + Personal: true, + Best: true, + Node: "EventNode28", // Incompatible with Galleon Of Ghouls, Wolf Hunt (2025) + MissionKeyName: "/Lotus/Types/Keys/MechSurvivalGrineerGalleon", + Faction: "FC_SENTIENT", + Desc: "/Lotus/Language/Events/MechEventMissionTier2", + Icon: "/Lotus/Interface/Icons/Categories/IconMech256.png", + Tag: "MechSurvivalB", + PrereqGoalTags: ["MechSurvivalA"], + ScoreVar: "MechSurvivalScore", + Reward: { + items: ["/Lotus/StoreItems/Types/Items/FusionTreasures/OroFusexJ"] + } + }, + { + _id: { $oid: "5fdcccb875d5ad500dc477d2" }, + Activation: { $date: { $numberLong: "1608320400000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Count: 0, + Goal: 2000, + Success: 0, + Personal: true, + Best: true, + Node: "EventNode32", + MissionKeyName: "/Lotus/Types/Keys/MechSurvivalGasCity", + MissionKeyRotation: [ + "/Lotus/Types/Keys/MechSurvivalGasCity", + "/Lotus/Types/Keys/MechSurvivalCorpusShipEndurance", + "/Lotus/Types/Keys/MechSurvivalGrineerGalleonEndurance" + ], + MissionKeyRotationInterval: 3600, // 1 hour + Faction: "FC_SENTIENT", + Desc: "/Lotus/Language/Events/MechEventMissionTier3", + Icon: "/Lotus/Interface/Icons/Categories/IconMech256.png", + Tag: "MechSurvival", + PrereqGoalTags: ["MechSurvivalA", "MechSurvivalB"], + ScoreVar: "MechSurvivalScore", + ScoreMaxTag: "MechSurvivalScoreMax", + Reward: { + items: [ + "/Lotus/StoreItems/Types/Items/MiscItems/FormaAura", + "/Lotus/StoreItems/Upgrades/Skins/Necramech/MechWeapon/MechEventMausolonSkin" + ] + } + } + ); + } + + // Thermia Fractures activates for 14 days, with alternating 4 and 3-day breaks + const thermiaFracturesCycleDay = day % 35; + const isThermiaFracturesActive = + thermiaFracturesCycleDay < 14 || (thermiaFracturesCycleDay >= 18 && thermiaFracturesCycleDay < 32); + const activeThermiaFracturesCycleDay = + thermiaFracturesCycleDay - (thermiaFracturesCycleDay < 14 ? 0 : thermiaFracturesCycleDay < 18 ? 14 : 32); + + if (config.worldState?.thermiaFracturesOverride ?? isThermiaFracturesActive) { + const activeStartDay = day - activeThermiaFracturesCycleDay; + + const count = config.worldState?.thermiaFracturesProgressOverride ?? 0; + const activation = config.worldState?.thermiaFracturesOverride ? 1740416400000 : getSortieTime(activeStartDay); + const expiry = config.worldState?.thermiaFracturesOverride ? 2000000000000 : getSortieTime(activeStartDay + 14); + + // If we push it, the game may show the event even tho it's not activated yet (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2721) + if (timeMs >= activation) { + worldState.Goals.push({ + _id: { $oid: "5c7cb0d00000000000000000" }, + Activation: { $date: { $numberLong: activation.toString() } }, + Expiry: { $date: { $numberLong: expiry.toString() } }, + Node: "SolNode129", + ScoreVar: "FissuresClosed", + ScoreLocTag: "/Lotus/Language/G1Quests/HeatFissuresEventScore", + Count: count, + HealthPct: count / 100, + Regions: [1], + Desc: "/Lotus/Language/G1Quests/HeatFissuresEventName", + ToolTip: "/Lotus/Language/G1Quests/HeatFissuresEventDesc", + OptionalInMission: true, + Tag: "HeatFissure", + UpgradeIds: [{ $oid: "5c81cefa4c4566791728eaa7" }, { $oid: "5c81cefa4c4566791728eaa6" }], + Personal: true, + Community: true, + Goal: 100, + Reward: { + items: ["/Lotus/StoreItems/Weapons/Corpus/LongGuns/CrpBFG/Vandal/VandalCrpBFG"] + }, + InterimGoals: [5, 25, 50, 75], + InterimRewards: [ + { items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/OrbBadgeItem"] }, + { + items: [ + "/Lotus/StoreItems/Upgrades/Mods/DualSource/Shotgun/ShotgunMedicMod", + "/Lotus/StoreItems/Upgrades/Mods/DualSource/Rifle/SerratedRushMod" + ] + }, + { + items: [ + "/Lotus/StoreItems/Upgrades/Mods/DualSource/Pistol/MultishotDodgeMod", + "/Lotus/StoreItems/Upgrades/Mods/DualSource/Melee/CritDamageChargeSpeedMod" + ] + }, + { items: ["/Lotus/StoreItems/Upgrades/Skins/Sigils/OrbSigil"] } + ] + }); + worldState.NodeOverrides.push({ + _id: { $oid: "5c7cb0d00000000000000000" }, + Activation: { $date: { $numberLong: activation.toString() } }, + Expiry: { $date: { $numberLong: expiry.toString() } }, + Node: "SolNode129", + Faction: "FC_CORPUS", + CustomNpcEncounters: ["/Lotus/Types/Gameplay/Venus/Encounters/Heists/ExploiterHeistFissure"] + }); + if (count >= 35) { + worldState.GlobalUpgrades.push({ + _id: { $oid: "5c81cefa4c4566791728eaa6" }, + Activation: { $date: { $numberLong: activation.toString() } }, + ExpiryDate: { $date: { $numberLong: expiry.toString() } }, + UpgradeType: "GAMEPLAY_MONEY_REWARD_AMOUNT", + OperationType: "MULTIPLY", + Value: 2, + Nodes: ["SolNode129"] + }); + } + // Not sure about that + if (count == 100) { + worldState.GlobalUpgrades.push({ + _id: { $oid: "5c81cefa4c4566791728eaa7" }, + Activation: { $date: { $numberLong: activation.toString() } }, + ExpiryDate: { $date: { $numberLong: expiry.toString() } }, + UpgradeType: "GAMEPLAY_PICKUP_AMOUNT", + OperationType: "MULTIPLY", + Value: 2, + Nodes: ["SolNode129"] + }); + } + } + } + + // Nightwave Challenges + const nightwaveSyndicateTag = getNightwaveSyndicateTag(buildLabel); + if (nightwaveSyndicateTag) { + const nightwaveStartTimestamp = 1747851300000; + const nightwaveSeason = nightwaveTagToSeason[nightwaveSyndicateTag]; + worldState.SeasonInfo = { + Activation: { $date: { $numberLong: nightwaveStartTimestamp.toString() } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + AffiliationTag: nightwaveSyndicateTag, + Season: nightwaveSeason, + Phase: 0, + Params: "", + ActiveChallenges: [] + }; + const pools = getSeasonChallengePools(nightwaveSyndicateTag); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 2)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 1)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 0)); + if (isBeforeNextExpectedWorldStateRefresh(timeMs, EPOCH + (day + 1) * 86400000)) { + worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day + 1)); + } + pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week, nightwaveStartTimestamp, nightwaveSeason); + if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) { + pushWeeklyActs( + worldState.SeasonInfo.ActiveChallenges, + pools, + week + 1, + nightwaveStartTimestamp, + nightwaveSeason + ); + } + } + + // 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(timeSecs / 9000); + 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(timeMs, bountyCycleEnd) && ++bountyCycle); + + const ghoulsCycleDay = day % 21; + const isGhoulEmergenceActive = ghoulsCycleDay >= 17 && ghoulsCycleDay <= 20; // 4 days for event and 17 days for break + if (config.worldState?.ghoulEmergenceOverride ?? isGhoulEmergenceActive) { + const ghoulPool = [...eidolonGhoulJobs]; + const pastGhoulPool = [...eidolonGhoulJobs]; + + const seed = new SRng(bountyCycle).randomInt(0, 100_000); + const pastSeed = new SRng(bountyCycle - 1).randomInt(0, 100_000); + + const rng = new SRng(seed); + const pastRng = new SRng(pastSeed); + + const activeStartDay = day - ghoulsCycleDay + 17; + const activeEndDay = activeStartDay + 5; + const dayWithFraction = (timeMs - EPOCH) / 86400000; + + const progress = (dayWithFraction - activeStartDay) / (activeEndDay - activeStartDay); + const healthPct = 1 - Math.min(Math.max(progress, 0), 1); + + worldState.Goals.push({ + _id: { $oid: "687ebbe6d1d17841c9c59f38" }, + Activation: { + $date: { + $numberLong: config.worldState?.ghoulEmergenceOverride + ? "1753204900185" + : Date.UTC( + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDate() + activeStartDay + ).toString() + } + }, + Expiry: { + $date: { + $numberLong: config.worldState?.ghoulEmergenceOverride + ? "2000000000000" + : Date.UTC( + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDate() + activeEndDay + ).toString() + } + }, + HealthPct: config.worldState?.ghoulEmergenceOverride ? 1 : healthPct, + VictimNode: "SolNode228", + Regions: [2], + Success: 0, + Desc: "/Lotus/Language/GameModes/RecurringGhoulAlert", + ToolTip: "/Lotus/Language/GameModes/RecurringGhoulAlertDesc", + Icon: "/Lotus/Interface/Icons/Categories/IconGhouls256.png", + Tag: "GhoulEmergence", + JobAffiliationTag: "CetusSyndicate", + JobCurrentVersion: { + $oid: ((bountyCycle * 9000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000008" + }, + Jobs: [ + { + jobType: rng.randomElementPop(ghoulPool), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/GhoulBountyTableARewards`, + masteryReq: 1, + minEnemyLevel: 15, + maxEnemyLevel: 25, + xpAmounts: [270, 270, 270, 400] // not faithful + }, + { + jobType: rng.randomElementPop(ghoulPool), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/GhoulBountyTableBRewards`, + masteryReq: 3, + minEnemyLevel: 40, + maxEnemyLevel: 50, + xpAmounts: [480, 480, 480, 710] // not faithful + } + ], + JobPreviousVersion: { + $oid: (((bountyCycle - 1) * 9000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000008" + }, + PreviousJobs: [ + { + jobType: pastRng.randomElementPop(pastGhoulPool), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/GhoulBountyTableARewards`, + masteryReq: 1, + minEnemyLevel: 15, + maxEnemyLevel: 25, + xpAmounts: [270, 270, 270, 400] // not faithful + }, + { + jobType: pastRng.randomElementPop(pastGhoulPool), + rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/GhoulBountyTableBRewards`, + masteryReq: 3, + minEnemyLevel: 40, + maxEnemyLevel: 50, + xpAmounts: [480, 480, 480, 710] // not faithful + } + ] + }); + } + + 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: "" + }); + } + + // Rough outline of dynamic invasions. + // TODO: Invasions chains, e.g. an infestation mission would soon lead to other nodes on that planet also having an infestation invasion. + // TODO: Grineer/Corpus to fund their death stars with each invasion win. + { + worldState.Invasions.push(createInvasion(day, 0)); + worldState.Invasions.push(createInvasion(day, 1)); + worldState.Invasions.push(createInvasion(day, 2)); + + // Completed invasions stay for up to 24 hours as the winner 'occupies' that node + worldState.Invasions.push(createInvasion(day - 1, 0)); + worldState.Invasions.push(createInvasion(day - 1, 1)); + worldState.Invasions.push(createInvasion(day - 1, 2)); + } + + // Baro + { + const baroIndex = Math.trunc((Date.now() - 910800000) / (unixTimesInMs.day * 14)); + const baroStart = baroIndex * (unixTimesInMs.day * 14) + 910800000; + const baroActualStart = baroStart + unixTimesInMs.day * (config.worldState?.baroAlwaysAvailable ? 0 : 12); + const baroEnd = baroStart + unixTimesInMs.day * 14; + const baroNode = ["EarthHUB", "MercuryHUB", "SaturnHUB", "PlutoHUB"][baroIndex % 4]; + const vt: IVoidTrader = { + _id: { $oid: ((baroStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "493c96d6067610bc" }, + Activation: { $date: { $numberLong: baroActualStart.toString() } }, + Expiry: { $date: { $numberLong: baroEnd.toString() } }, + Character: "Baro'Ki Teel", + Node: baroNode, + Manifest: [] + }; + worldState.VoidTraders.push(vt); + if (isBeforeNextExpectedWorldStateRefresh(timeMs, baroActualStart)) { + if (config.worldState?.baroFullyStocked) { + fullyStockBaro(vt); + } else { + const rng = new SRng(new SRng(baroIndex).randomInt(0, 100_000)); + // TOVERIFY: Constraint for upgrades amount? + // TOVERIFY: Constraint for weapon amount? + // TOVERIFY: Constraint for relics amount? + let armorSet = rng.randomElement(baro.armorSets)!; + if (Array.isArray(armorSet[0])) { + armorSet = rng.randomElement(baro.armorSets)!; + } + while (vt.Manifest.length + armorSet.length < 31) { + const item = rng.randomElement(baro.rest)!; + if (vt.Manifest.indexOf(item) == -1) { + const set = baro.allIfAny.find(set => set.indexOf(item.ItemType) != -1); + if (set) { + for (const itemType of set) { + vt.Manifest.push(baro.rest.find(x => x.ItemType == itemType)!); + } + } else { + vt.Manifest.push(item); + } + } + } + const overflow = 31 - (vt.Manifest.length + armorSet.length); + if (overflow > 0) { + vt.Manifest.splice(0, overflow); + } + for (const armor of armorSet) { + vt.Manifest.push(armor as IVoidTraderOffer); + } + } + for (const item of baro.evergreen) { + vt.Manifest.push(item); + } + } + } + + // Varzia + { + const pt: IPrimeVaultTrader = { + _id: { $oid: ((weekStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "c36af423770eaa97" }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + InitialStartDate: { $date: { $numberLong: "1662738144266" } }, + Node: "TradeHUB1", + Manifest: [], + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + EvergreenManifest: varzia.evergreen, + ScheduleInfo: [] + }; + worldState.PrimeVaultTraders.push(pt); + const rotation = config.worldState?.varziaOverride || getVarziaRotation(week); + pt.Manifest = config.worldState?.varziaFullyStocked ? getAllVarziaManifests() : getVarziaManifest(rotation); + if (config.worldState?.varziaOverride || config.worldState?.varziaFullyStocked) { + pt.Expiry = { $date: { $numberLong: "2000000000000" } }; + } else { + pt.ScheduleInfo.push({ + Expiry: { $date: { $numberLong: (weekEnd + unixTimesInMs.week).toString() } }, + FeaturedItem: getVarziaRotation(week + 1) + }); + } + } + + // 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 (timeMs < rollover) { + worldState.Sorties.push(getSortie(day - 1)); + } + if (isBeforeNextExpectedWorldStateRefresh(timeMs, rollover)) { + worldState.Sorties.push(getSortie(day)); + } + + // The client does not seem to respect activation for classic syndicate missions, so only pushing current ones. + const sdy = timeMs >= rollover ? day : day - 1; + const rng = new SRng(sdy); + pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48049", "ArbitersSyndicate"); + pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4804a", "CephalonSudaSyndicate"); + 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"); + } + + { + const conclaveDayStart = EPOCH + day * unixTimesInMs.day + 5 * unixTimesInMs.hour + 30 * unixTimesInMs.minute; + const conclaveDayEnd = conclaveDayStart + unixTimesInMs.day; + const conclaveWeekStart = weekStart + 40 * unixTimesInMs.minute - 2 * unixTimesInMs.day; + const conclaveWeekEnd = conclaveWeekStart + unixTimesInMs.week; + + pushConclaveWeakly(worldState.PVPChallengeInstances, week); + pushConclaveDailys(worldState.PVPChallengeInstances, day); + + if (isBeforeNextExpectedWorldStateRefresh(timeMs, conclaveDayEnd)) { + pushConclaveDailys(worldState.PVPChallengeInstances, day + 1); + } + if (isBeforeNextExpectedWorldStateRefresh(timeMs, conclaveWeekEnd)) { + pushConclaveWeakly(worldState.PVPChallengeInstances, week + 1); + } + } + + // Archon Hunt cycling every week + worldState.LiteSorties.push(getLiteSortie(week)); + if (isBeforeNextExpectedWorldStateRefresh(timeMs, 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 % 11] + }); + 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(timeMs, weekEnd)) { + worldState.KnownCalendarSeasons.push(getCalendarSeason(week + 1)); + } + + // Void Storms + const hour = Math.trunc(timeMs / unixTimesInMs.hour); + const overLastHourStormExpiry = hour * unixTimesInMs.hour + 10 * unixTimesInMs.minute; + const thisHourStormActivation = hour * unixTimesInMs.hour + 40 * unixTimesInMs.minute; + if (overLastHourStormExpiry > timeMs) { + pushVoidStorms(worldState.VoidStorms, hour - 2); + } + pushVoidStorms(worldState.VoidStorms, hour - 1); + if (isBeforeNextExpectedWorldStateRefresh(timeMs, thisHourStormActivation)) { + pushVoidStorms(worldState.VoidStorms, hour); + } + + // Sentient Anomaly + Xtra Cheese cycles + const halfHour = Math.trunc(timeMs / (unixTimesInMs.hour / 2)); + const hourInSeconds = 3600; + const cheeseInterval = hourInSeconds * 8; + const cheeseDuration = hourInSeconds * 2; + const cheeseIndex = Math.trunc(timeSecs / cheeseInterval); + let cheeseStart = cheeseIndex * cheeseInterval; + let cheeseEnd = cheeseStart + cheeseDuration; + let cheeseNext = (cheeseIndex + 1) * cheeseInterval; + // Live servers only update the start time once it happens, which makes the + // client show a negative countdown during off-hours. Optionally adjust the + // times so the next activation is always in the future. + if (config.unfaithfulBugFixes?.fixXtraCheeseTimer && timeSecs >= cheeseEnd) { + cheeseStart = cheeseNext; + cheeseEnd = cheeseStart + cheeseDuration; + cheeseNext += cheeseInterval; + } + const tmp: ITmp = { + cavabegin: "1690761600", + PurchasePlatformLockEnabled: 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, + fbst: { + a: cheeseStart, + e: cheeseEnd, + n: cheeseNext + }, + sfn: [550, 553, 554, 555][halfHour % 4] + }; + if (Array.isArray(config.worldState?.circuitGameModes)) { + tmp.edg = config.worldState.circuitGameModes as TCircuitGameMode[]; + } + worldState.Tmp = JSON.stringify(tmp); + + return worldState; +}; + +export const populateFissures = async (worldState: IWorldState): Promise => { + if (config.worldState?.allTheFissures) { + let i = 0; + for (const [tier, nodes] of Object.entries(fissureMissions)) { + for (const node of nodes) { + const meta = ExportRegions[node]; + worldState.ActiveMissions.push({ + _id: { $oid: (i++).toString().padStart(8, "0") + "8e0c70ba050f1eb7" }, + Region: meta.systemIndex + 1, + Seed: 1337, + Activation: { $date: { $numberLong: "1000000000000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Node: node, + MissionType: eMissionType[meta.missionIndex].tag, + Modifier: tier, + Hard: config.worldState.allTheFissures == "hard" + }); + } + } + } else { + const fissures = await Fissure.find({}); + for (const fissure of fissures) { + const meta = ExportRegions[fissure.Node]; + worldState.ActiveMissions.push({ + _id: toOid(fissure._id), + Region: meta.systemIndex + 1, + Seed: 1337, + Activation: + fissure.Activation.getTime() < Date.now() // Activation is in the past? + ? { $date: { $numberLong: "1000000000000" } } // Let the client know 'explicitly' to avoid interference from time constraints. + : toMongoDate(fissure.Activation), + Expiry: toMongoDate(fissure.Expiry), + Node: fissure.Node, + MissionType: eMissionType[meta.missionIndex].tag, + Modifier: fissure.Modifier, + Hard: fissure.Hard + }); + } + } +}; + +export const populateDailyDeal = async (worldState: IWorldState): Promise => { + const dailyDeals = await DailyDeal.find({}); + for (const dailyDeal of dailyDeals) { + if (dailyDeal.Expiry.getTime() > Date.now()) { + worldState.DailyDeals.push({ + StoreItem: dailyDeal.StoreItem, + Activation: toMongoDate(dailyDeal.Activation), + Expiry: toMongoDate(dailyDeal.Expiry), + Discount: dailyDeal.Discount, + OriginalPrice: dailyDeal.OriginalPrice, + SalePrice: dailyDeal.SalePrice, + AmountTotal: Math.round(dailyDeal.AmountTotal * (config.worldState?.darvoStockMultiplier ?? 1)), + AmountSold: dailyDeal.AmountSold + }); + } + } +}; + +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 (config.worldState?.nightwaveOverride) { + if (config.worldState.nightwaveOverride in nightwaveTagToSeason) { + return config.worldState.nightwaveOverride; + } + logger.warn(`ignoring invalid config value for worldState.nightwaveOverride`, { + value: config.worldState.nightwaveOverride, + valid_values: Object.keys(nightwaveTagToSeason) + }); + } + if (!buildLabel || version_compare(buildLabel, "2025.05.20.10.18") >= 0) { + return "RadioLegionIntermission13Syndicate"; + } + if (version_compare(buildLabel, "2025.02.05.11.19") >= 0) { + return "RadioLegionIntermission12Syndicate"; + } + return undefined; +}; + +const nightwaveTagToSeason: Record = { + RadioLegionIntermission13Syndicate: 15, // Nora's Mix Vol. 9 + RadioLegionIntermission12Syndicate: 14, // Nora's Mix Vol. 8 + RadioLegionIntermission11Syndicate: 13, // Nora's Mix Vol. 7 + RadioLegionIntermission10Syndicate: 12, // Nora's Mix Vol. 6 + RadioLegionIntermission9Syndicate: 11, // Nora's Mix Vol. 5 + RadioLegionIntermission8Syndicate: 10, // Nora's Mix Vol. 4 + RadioLegionIntermission7Syndicate: 9, // Nora's Mix Vol. 3 + RadioLegionIntermission6Syndicate: 8, // Nora's Mix Vol. 2 + RadioLegionIntermission5Syndicate: 7, // Nora's Mix Vol. 1 + RadioLegionIntermission4Syndicate: 6, // Nora's Choice + RadioLegionIntermission3Syndicate: 5, // Intermission III + RadioLegion3Syndicate: 4, // Glassmaker + RadioLegionIntermission2Syndicate: 3, // Intermission II + RadioLegion2Syndicate: 2, // The Emissary + RadioLegionIntermissionSyndicate: 1, // Intermission I + RadioLegionSyndicate: 0 // The Wolf of Saturn Six +}; + +const updateFissures = async (): Promise => { + const fissures = await Fissure.find(); + + const activeNodes = new Set(); + const tierToFurthestExpiry: Record = { + VoidT1: 0, + VoidT2: 0, + VoidT3: 0, + VoidT4: 0, + VoidT5: 0, + VoidT6: 0, + VoidT1Hard: 0, + VoidT2Hard: 0, + VoidT3Hard: 0, + VoidT4Hard: 0, + VoidT5Hard: 0, + VoidT6Hard: 0 + }; + for (const fissure of fissures) { + activeNodes.add(fissure.Node); + + const key = fissure.Modifier + (fissure.Hard ? "Hard" : ""); + tierToFurthestExpiry[key] = Math.max(tierToFurthestExpiry[key], fissure.Expiry.getTime()); + } + + const deadline = Date.now() - 6 * unixTimesInMs.minute; + for (const [tier, expiry] of Object.entries(tierToFurthestExpiry)) { + if (expiry < deadline) { + const numFissures = getRandomInt(1, 3); + for (let i = 0; i != numFissures; ++i) { + const modifier = tier.replace("Hard", "") as + | "VoidT1" + | "VoidT2" + | "VoidT3" + | "VoidT4" + | "VoidT5" + | "VoidT6"; + let node: string; + do { + node = getRandomElement(fissureMissions[modifier])!; + } while (activeNodes.has(node)); + activeNodes.add(node); + await Fissure.insertOne({ + Activation: new Date(), + Expiry: new Date(Date.now() + getRandomInt(60, 120) * unixTimesInMs.minute), + Node: node, + Modifier: modifier, + Hard: tier.indexOf("Hard") != -1 ? true : undefined + }); + } + } + } +}; + +const updateDailyDeal = async (): Promise => { + let darvoIndex = Math.trunc((Date.now() - 25200000) / (26 * unixTimesInMs.hour)); + let darvoEnd; + do { + const darvoStart = darvoIndex * (26 * unixTimesInMs.hour) + 25200000; + darvoEnd = darvoStart + 26 * unixTimesInMs.hour; + const darvoOid = ((darvoStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "adc51a72f7324d95"; + if (!(await DailyDeal.findById(darvoOid))) { + const seed = new SRng(darvoIndex).randomInt(0, 100_000); + const rng = new SRng(seed); + let deal; + do { + deal = rng.randomReward(darvoDeals)!; // Using an actual sampling collected over roughly a year because I can't extrapolate an algorithm from it with enough certainty. + //const [storeItem, meta] = rng.randomElement(Object.entries(darvoDeals))!; + //const discount = Math.min(rng.randomInt(1, 9) * 10, (meta as { MaxDiscount?: number }).MaxDiscount ?? 1); + } while (await DailyDeal.exists({ StoreItem: deal.StoreItem })); + await DailyDeal.insertOne({ + _id: darvoOid, + StoreItem: deal.StoreItem, + Activation: new Date(darvoStart), + Expiry: new Date(darvoEnd), + Discount: deal.Discount, + OriginalPrice: deal.OriginalPrice, + SalePrice: deal.SalePrice, //Math.trunc(deal.OriginalPrice * (1 - discount)) + AmountTotal: deal.AmountTotal, + AmountSold: 0 + }); + } + } while (darvoEnd < Date.now() + 6 * unixTimesInMs.minute && ++darvoIndex); +}; + +export const updateWorldStateCollections = async (): Promise => { + await Promise.all([updateFissures(), updateDailyDeal()]); +}; + +const pushConclaveDaily = ( + activeChallenges: IPVPChallengeInstance[], + PVPMode: string, + pool: { + key: string; + ScriptParamValue: number; + PVPModeAllowed: string[]; + SyndicateXP: number; + DuringSingleMatch?: boolean; + }[], + day: number, + id: number +): void => { + const conclaveDayStart = EPOCH + day * unixTimesInMs.day + 5 * unixTimesInMs.hour + 30 * unixTimesInMs.minute; + const conclaveDayEnd = conclaveDayStart + unixTimesInMs.day; + const challengeId = day * 8 + id; + const rng = new SRng(new SRng(challengeId).randomInt(0, 100_000)); + let challenge: { + key: string; + ScriptParamValue: number; + PVPModeAllowed?: string[]; + SyndicateXP?: number; + DuringSingleMatch?: boolean; + }; + do { + challenge = rng.randomElement(pool)!; + } while ( + activeChallenges.some(x => x.challengeTypeRefID == challenge.key) && + activeChallenges.some(x => x.PVPMode == PVPMode) + ); + activeChallenges.push({ + _id: { + $oid: "689ec5d985b55902" + challengeId.toString().padStart(8, "0") + }, + challengeTypeRefID: challenge.key, + startDate: { $date: { $numberLong: conclaveDayStart.toString() } }, + endDate: { $date: { $numberLong: conclaveDayEnd.toString() } }, + params: [{ n: "ScriptParamValue", v: challenge.ScriptParamValue }], + isGenerated: true, + PVPMode, + subChallenges: [], + Category: "PVPChallengeTypeCategory_DAILY" + }); +}; + +const pushConclaveDailys = (activeChallenges: IPVPChallengeInstance[], day: number): void => { + const modes = [ + "PVPMODE_SPEEDBALL", + "PVPMODE_CAPTURETHEFLAG", + "PVPMODE_DEATHMATCH", + "PVPMODE_TEAMDEATHMATCH" + ] as const; + + const challengesMap: Record< + string, + { + key: string; + ScriptParamValue: number; + PVPModeAllowed: string[]; + SyndicateXP: number; + DuringSingleMatch?: boolean; + }[] + > = {}; + + for (const mode of modes) { + challengesMap[mode] = Object.entries(pvpChallenges) + .filter(([_, challenge]) => challenge.PVPModeAllowed.includes(mode)) + .map(([key, challenge]) => ({ key, ...challenge })); + } + + modes.forEach((mode, index) => { + pushConclaveDaily(activeChallenges, mode, challengesMap[mode], day, index * 2); + pushConclaveDaily(activeChallenges, mode, challengesMap[mode], day, index * 2 + 1); + }); +}; + +const pushConclaveWeakly = (activeChallenges: IPVPChallengeInstance[], week: number): void => { + const weekStart = EPOCH + week * unixTimesInMs.week; + const conclaveWeekStart = weekStart + 40 * unixTimesInMs.minute - 2 * unixTimesInMs.day; + const conclaveWeekEnd = conclaveWeekStart + unixTimesInMs.week; + const conclaveIdStart = ((conclaveWeekStart / 1000) & 0xffffffff).toString(16).padStart(8, "0").padEnd(23, "0"); + activeChallenges.push( + { + _id: { $oid: conclaveIdStart + "1" }, + challengeTypeRefID: "/Lotus/PVPChallengeTypes/PVPTimedChallengeGameModeWins", + startDate: { $date: { $numberLong: conclaveWeekStart.toString() } }, + endDate: { $date: { $numberLong: conclaveWeekEnd.toString() } }, + params: [{ n: "ScriptParamValue", v: 6 }], + isGenerated: true, + PVPMode: "PVPMODE_ALL", + subChallenges: [], + Category: "PVPChallengeTypeCategory_WEEKLY" + }, + { + _id: { $oid: conclaveIdStart + "2" }, + challengeTypeRefID: "/Lotus/PVPChallengeTypes/PVPTimedChallengeGameModeComplete", + startDate: { $date: { $numberLong: conclaveWeekStart.toString() } }, + endDate: { $date: { $numberLong: conclaveWeekEnd.toString() } }, + params: [{ n: "ScriptParamValue", v: 20 }], + isGenerated: true, + PVPMode: "PVPMODE_ALL", + subChallenges: [], + Category: "PVPChallengeTypeCategory_WEEKLY" + }, + { + _id: { $oid: conclaveIdStart + "3" }, + challengeTypeRefID: "/Lotus/PVPChallengeTypes/PVPTimedChallengeOtherChallengeCompleteANY", + startDate: { $date: { $numberLong: conclaveWeekStart.toString() } }, + endDate: { $date: { $numberLong: conclaveWeekEnd.toString() } }, + params: [{ n: "ScriptParamValue", v: 10 }], + isGenerated: true, + PVPMode: "PVPMODE_ALL", + subChallenges: [], + Category: "PVPChallengeTypeCategory_WEEKLY" + }, + { + _id: { $oid: conclaveIdStart + "4" }, + challengeTypeRefID: "/Lotus/PVPChallengeTypes/PVPTimedChallengeWeeklyStandardSet", + startDate: { $date: { $numberLong: conclaveWeekStart.toString() } }, + endDate: { $date: { $numberLong: conclaveWeekEnd.toString() } }, + params: [{ n: "ScriptParamValue", v: 0 }], + isGenerated: true, + PVPMode: "PVPMODE_NONE", + subChallenges: [ + { $oid: conclaveIdStart + "1" }, + { $oid: conclaveIdStart + "2" }, + { $oid: conclaveIdStart + "3" } + ], + Category: "PVPChallengeTypeCategory_WEEKLY_ROOT" + } + ); +}; diff --git a/src/services/wsService.ts b/src/services/wsService.ts new file mode 100644 index 00000000..262cd30f --- /dev/null +++ b/src/services/wsService.ts @@ -0,0 +1,218 @@ +import type http from "http"; +import type https from "https"; +import type { default as ws } from "ws"; +import { WebSocketServer } from "ws"; +import { Account } from "../models/loginModel.ts"; +import { createAccount, createNonce, getUsernameFromEmail, isCorrectPassword } from "./loginService.ts"; +import type { IDatabaseAccountJson } from "../types/loginTypes.ts"; +import type { HydratedDocument } from "mongoose"; +import { logError } from "../utils/logger.ts"; + +let wsServer: WebSocketServer | undefined; +let wssServer: WebSocketServer | undefined; + +export const startWsServer = (httpServer: http.Server): void => { + wsServer = new WebSocketServer({ server: httpServer }); + wsServer.on("connection", wsOnConnect); +}; + +export const startWssServer = (httpsServer: https.Server): void => { + wssServer = new WebSocketServer({ server: httpsServer }); + wssServer.on("connection", wsOnConnect); +}; + +export const stopWsServers = (promises: Promise[]): void => { + if (wsServer) { + promises.push( + new Promise(resolve => { + wsServer!.close(() => { + resolve(); + }); + }) + ); + } + if (wssServer) { + promises.push( + new Promise(resolve => { + wssServer!.close(() => { + resolve(); + }); + }) + ); + } +}; + +let lastWsid: number = 0; + +interface IWsCustomData extends ws { + id: number; + accountId?: string; +} + +interface IWsMsgFromClient { + auth?: { + email: string; + password: string; + isRegister: boolean; + }; + logout?: boolean; +} + +interface IWsMsgToClient { + //wsid?: number; + reload?: boolean; + ports?: { + http: number | undefined; + https: number | undefined; + }; + config_reloaded?: boolean; + auth_succ?: { + id: string; + DisplayName: string; + Nonce: number; + }; + auth_fail?: { + isRegister: boolean; + }; + nonce_updated?: boolean; + update_inventory?: boolean; + logged_out?: boolean; + currency_update?: { + RegularCredits: number; + PremiumCredits: number; + PremiumCreditsFree: number; + }; +} + +const wsOnConnect = (ws: ws, req: http.IncomingMessage): void => { + if (req.url == "/custom/selftest") { + ws.send("SpaceNinjaServer"); + ws.close(); + return; + } + + (ws as IWsCustomData).id = ++lastWsid; + ws.send(JSON.stringify({ wsid: lastWsid })); + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + ws.on("message", async msg => { + try { + const data = JSON.parse(String(msg)) as IWsMsgFromClient; + if (data.auth) { + let account: IDatabaseAccountJson | null = await Account.findOne({ email: data.auth.email }); + if (account) { + if (isCorrectPassword(data.auth.password, account.password)) { + if (!account.Nonce) { + account.ClientType = "webui"; + account.Nonce = createNonce(); + await (account as HydratedDocument).save(); + } + } else { + account = null; + } + } else if (data.auth.isRegister) { + const name = await getUsernameFromEmail(data.auth.email); + account = await createAccount({ + email: data.auth.email, + password: data.auth.password, + ClientType: "webui", + LastLogin: new Date(), + DisplayName: name, + Nonce: createNonce() + }); + } + if (account) { + (ws as IWsCustomData).accountId = account.id; + ws.send( + JSON.stringify({ + auth_succ: { + id: account.id, + DisplayName: account.DisplayName, + Nonce: account.Nonce + } + } satisfies IWsMsgToClient) + ); + } else { + ws.send( + JSON.stringify({ + auth_fail: { + isRegister: data.auth.isRegister + } + } satisfies IWsMsgToClient) + ); + } + } + if (data.logout) { + const accountId = (ws as IWsCustomData).accountId; + (ws as IWsCustomData).accountId = undefined; + await Account.updateOne( + { + _id: accountId, + ClientType: "webui" + }, + { + Nonce: 0 + } + ); + } + } catch (e) { + logError(e as Error, `processing websocket message`); + } + }); +}; + +export const sendWsBroadcast = (data: IWsMsgToClient): void => { + const msg = JSON.stringify(data); + if (wsServer) { + for (const client of wsServer.clients) { + client.send(msg); + } + } + if (wssServer) { + for (const client of wssServer.clients) { + client.send(msg); + } + } +}; + +export const sendWsBroadcastTo = (accountId: string, data: IWsMsgToClient): void => { + const msg = JSON.stringify(data); + if (wsServer) { + for (const client of wsServer.clients) { + if ((client as IWsCustomData).accountId == accountId) { + client.send(msg); + } + } + } + if (wssServer) { + for (const client of wssServer.clients) { + if ((client as IWsCustomData).accountId == accountId) { + client.send(msg); + } + } + } +}; + +export const sendWsBroadcastEx = (data: IWsMsgToClient, accountId?: string, excludeWsid?: number): void => { + const msg = JSON.stringify(data); + if (wsServer) { + for (const client of wsServer.clients) { + if ( + (!accountId || (client as IWsCustomData).accountId == accountId) && + (client as IWsCustomData).id != excludeWsid + ) { + client.send(msg); + } + } + } + if (wssServer) { + for (const client of wssServer.clients) { + if ( + (!accountId || (client as IWsCustomData).accountId == accountId) && + (client as IWsCustomData).id != excludeWsid + ) { + client.send(msg); + } + } + } +}; diff --git a/src/types/commonTypes.ts b/src/types/commonTypes.ts new file mode 100644 index 00000000..915614bd --- /dev/null +++ b/src/types/commonTypes.ts @@ -0,0 +1,26 @@ +export interface IOid { + $oid: string; +} + +export interface IOidWithLegacySupport { + $oid?: string; + $id?: string; +} + +export interface IMongoDate { + $date: { + $numberLong: string; + }; +} + +export interface ITypeCount { + ItemType: string; + ItemCount: number; +} + +export interface IReward { + items: ITypeCount[]; + credits: number; +} + +export type IJunctionRewards = Record; diff --git a/src/types/customTypes.ts b/src/types/customTypes.ts new file mode 100644 index 00000000..01c77950 --- /dev/null +++ b/src/types/customTypes.ts @@ -0,0 +1,6 @@ +export interface IAccountCreation { + email: string; + password: string; + DisplayName: string; + CountryCode: string; +} diff --git a/src/types/equipmentTypes.ts b/src/types/equipmentTypes.ts new file mode 100644 index 00000000..1515756f --- /dev/null +++ b/src/types/equipmentTypes.ts @@ -0,0 +1,175 @@ +import type { Types } from "mongoose"; +import type { IMongoDate, IOid, IOidWithLegacySupport } from "./commonTypes.ts"; +import type { + ICrewShipCustomization, + IFlavourItem, + IItemConfig, + IPolarity +} from "./inventoryTypes/commonInventoryTypes.ts"; + +export interface IEquipmentSelectionClient { + ItemId?: IOid; + mod?: number; + cus?: number; + ItemType?: string; + hide?: boolean; +} + +export interface IEquipmentSelectionDatabase extends Omit { + ItemId?: Types.ObjectId | IOid; // should be Types.ObjectId but might be IOid because of old commits +} + +export enum EquipmentFeatures { + DOUBLE_CAPACITY = 1, + UTILITY_SLOT = 2, + GRAVIMAG_INSTALLED = 4, + GILDED = 8, + ARCANE_SLOT = 32, + INCARNON_GENESIS = 512, + VALENCE_SWAP = 1024 +} + +export interface IEquipmentDatabase { + ItemType: string; + ItemName?: string; + Configs: IItemConfig[]; + UpgradeVer?: number; + XP?: number; + Features?: number; + Polarized?: number; + Polarity?: IPolarity[]; + FocusLens?: string; + ModSlotPurchases?: number; + CustomizationSlotPurchases?: number; + UpgradeType?: string; + UpgradeFingerprint?: string; + InfestationDate?: Date; + InfestationDays?: number; + InfestationType?: string; + ModularParts?: string[]; + UnlockLevel?: number; + Expiry?: Date; + SkillTree?: string; + OffensiveUpgrade?: string; + DefensiveUpgrade?: string; + UpgradesExpiry?: Date; + UmbraDate?: Date; // related to scrapped "echoes of umbra" feature + ArchonCrystalUpgrades?: IArchonCrystalUpgrade[]; + Weapon?: ICrewShipWeaponDatabase; + Customization?: ICrewShipCustomization; + RailjackImage?: IFlavourItem; + CrewMembers?: ICrewShipMembersDatabase; + Details?: IKubrowPetDetailsDatabase; + Favorite?: boolean; + IsNew?: boolean; + _id: Types.ObjectId; +} + +export interface IEquipmentClient + extends Omit< + IEquipmentDatabase, + "_id" | "InfestationDate" | "Expiry" | "UpgradesExpiry" | "UmbraDate" | "Weapon" | "CrewMembers" | "Details" + > { + ItemId: IOidWithLegacySupport; + InfestationDate?: IMongoDate; + Expiry?: IMongoDate; + UpgradesExpiry?: IMongoDate; + UmbraDate?: IMongoDate; + Weapon?: ICrewShipWeaponClient; + CrewMembers?: ICrewShipMembersClient; + Details?: IKubrowPetDetailsClient; +} + +export interface IArchonCrystalUpgrade { + UpgradeType?: string; + Color?: string; +} + +export interface ITraits { + BaseColor: string; + SecondaryColor: string; + TertiaryColor: string; + AccentColor: string; + EyeColor: string; + FurPattern: string; + Personality: string; + BodyType: string; + Head?: string; + Tail?: string; +} + +export interface IKubrowPetDetailsDatabase { + Name?: string; + IsPuppy?: boolean; + HasCollar: boolean; + PrintsRemaining: number; + Status: Status; + HatchDate?: Date; + DominantTraits: ITraits; + RecessiveTraits: ITraits; + IsMale: boolean; + Size: number; +} + +export interface IKubrowPetDetailsClient extends Omit { + HatchDate: IMongoDate; +} + +export enum Status { + StatusAvailable = "STATUS_AVAILABLE", + StatusStasis = "STATUS_STASIS", + StatusIncubating = "STATUS_INCUBATING" +} + +// inventory.CrewShips[0].Weapon +export interface ICrewShipWeaponClient { + PILOT?: ICrewShipWeaponEmplacementsClient; + PORT_GUNS?: ICrewShipWeaponEmplacementsClient; + STARBOARD_GUNS?: ICrewShipWeaponEmplacementsClient; + ARTILLERY?: ICrewShipWeaponEmplacementsClient; + SCANNER?: ICrewShipWeaponEmplacementsClient; +} + +export interface ICrewShipWeaponDatabase { + PILOT?: ICrewShipWeaponEmplacementsDatabase; + PORT_GUNS?: ICrewShipWeaponEmplacementsDatabase; + STARBOARD_GUNS?: ICrewShipWeaponEmplacementsDatabase; + ARTILLERY?: ICrewShipWeaponEmplacementsDatabase; + SCANNER?: ICrewShipWeaponEmplacementsDatabase; +} + +export interface ICrewShipWeaponEmplacementsClient { + PRIMARY_A?: IEquipmentSelectionClient; + PRIMARY_B?: IEquipmentSelectionClient; + SECONDARY_A?: IEquipmentSelectionClient; + SECONDARY_B?: IEquipmentSelectionClient; +} + +export interface ICrewShipWeaponEmplacementsDatabase { + PRIMARY_A?: IEquipmentSelectionDatabase; + PRIMARY_B?: IEquipmentSelectionDatabase; + SECONDARY_A?: IEquipmentSelectionDatabase; + SECONDARY_B?: IEquipmentSelectionDatabase; +} + +export interface ICrewShipMembersClient { + SLOT_A?: ICrewShipMemberClient; + SLOT_B?: ICrewShipMemberClient; + SLOT_C?: ICrewShipMemberClient; +} + +export interface ICrewShipMembersDatabase { + SLOT_A?: ICrewShipMemberDatabase; + SLOT_B?: ICrewShipMemberDatabase; + SLOT_C?: ICrewShipMemberDatabase; +} + +export interface ICrewShipMemberClient { + ItemId?: IOid; + NemesisFingerprint?: number | bigint; +} + +export interface ICrewShipMemberDatabase { + ItemId?: Types.ObjectId; + NemesisFingerprint?: bigint; +} diff --git a/src/types/friendTypes.ts b/src/types/friendTypes.ts new file mode 100644 index 00000000..74d81469 --- /dev/null +++ b/src/types/friendTypes.ts @@ -0,0 +1,24 @@ +import type { Types } from "mongoose"; +import type { IMongoDate, IOidWithLegacySupport } from "./commonTypes.ts"; + +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 new file mode 100644 index 00000000..5cd1819e --- /dev/null +++ b/src/types/genericUpdate.ts @@ -0,0 +1,11 @@ +import type { IInventoryChanges } from "./purchaseTypes.ts"; + +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 new file mode 100644 index 00000000..366e661c --- /dev/null +++ b/src/types/guildTypes.ts @@ -0,0 +1,335 @@ +import type { Types } from "mongoose"; +import type { IOid, IMongoDate, IOidWithLegacySupport, ITypeCount } from "./commonTypes.ts"; +import type { + IFusionTreasure, + IMiscItem, + IGoalProgressDatabase, + IGoalProgressClient +} from "./inventoryTypes/inventoryTypes.ts"; +import type { IPictureFrameInfo } from "./personalRoomsTypes.ts"; +import type { IFriendInfo } from "./friendTypes.ts"; + +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; + + GoalProgress?: IGoalProgressClient[]; +} + +export interface IGuildDatabase { + _id: Types.ObjectId; + 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[]; + + GoalProgress?: IGoalProgressDatabase[]; +} + +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: 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: IOidWithLegacySupport; + SortId?: IOidWithLegacySupport; + pf: string; // Prefab (.level) + ppf: string; + 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; // 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< + 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 { + ItemType: string; + ReqCredits: number; + ReqItems: IMiscItem[]; + State: number; // 0 = pending, 1 = complete + CompletionDate?: IMongoDate; +} + +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 new file mode 100644 index 00000000..f0d139f5 --- /dev/null +++ b/src/types/inventoryTypes/commonInventoryTypes.ts @@ -0,0 +1,93 @@ +import type { IOid } from "../commonTypes.ts"; +import type { Types } from "mongoose"; + +export interface IPolarity { + Slot: number; + Value: ArtifactPolarity; +} + +export enum ArtifactPolarity { + Any = "AP_ANY", + Attack = "AP_ATTACK", + Defense = "AP_DEFENSE", + Power = "AP_POWER", + Precept = "AP_PRECEPT", + Tactic = "AP_TACTIC", + Umbra = "AP_UMBRA", + Universal = "AP_UNIVERSAL", + Ward = "AP_WARD" +} + +export interface IColor { + t0?: number; + t1?: number; + t2?: number; + t3?: number; + en?: number; + e1?: number; + m0?: number; + m1?: number; +} + +export interface IAbilityOverride { + Ability: string; + Index: number; +} + +export interface ISlotsBin { + Slots: number; +} + +export interface IItemConfig { + Skins?: string[]; + pricol?: IColor; + attcol?: IColor; + sigcol?: IColor; + eyecol?: IColor; + facial?: IColor; + syancol?: IColor; + cloth?: IColor; + Upgrades?: string[]; + Name?: string; + OperatorAmp?: IOid; + Songs?: ISong[]; + AbilityOverride?: IAbilityOverride; + PvpUpgrades?: string[]; + ugly?: boolean; +} + +export interface ISong { + m?: string; + b?: string; + p?: string; + s: string; +} +export interface IOperatorConfigDatabase extends IItemConfig { + _id: Types.ObjectId; +} + +export interface IOperatorConfigClient extends Omit { + ItemId: IOid; +} + +export interface ILotusCustomization extends IItemConfig { + Persona: string; +} + +export interface IFlavourItem { + ItemType: string; +} + +export interface IShipAttachments { + HOOD_ORNAMENT?: string; +} + +export interface IShipCustomization { + SkinFlavourItem?: string; + Colors?: IColor; + ShipAttachments?: IShipAttachments; +} + +export interface ICrewShipCustomization { + CrewshipInterior: IShipCustomization; +} diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts new file mode 100644 index 00000000..df71f392 --- /dev/null +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -0,0 +1,1208 @@ +import type { Types } from "mongoose"; +import type { IOid, IMongoDate, IOidWithLegacySupport, ITypeCount } from "../commonTypes.ts"; +import type { + IColor, + IItemConfig, + IOperatorConfigClient, + IOperatorConfigDatabase, + IFlavourItem, + ILotusCustomization, + IShipCustomization +} from "./commonInventoryTypes.ts"; +import type { IFingerprintStat, RivenFingerprint } from "../../helpers/rivenHelper.ts"; +import type { IOrbiterClient } from "../personalRoomsTypes.ts"; +import type { ICountedStoreItem } from "warframe-public-export-plus"; +import type { IEquipmentClient, IEquipmentDatabase, ITraits } from "../equipmentTypes.ts"; +import type { ILoadOutPresets } from "../saveLoadoutTypes.ts"; + +export type InventoryDatabaseEquipment = { + [_ in TEquipmentKey]: IEquipmentDatabase[]; +}; + +// Fields specific to SNS +export interface IAccountCheats { + skipAllDialogue?: boolean; + dontSubtractPurchaseCreditCost?: boolean; + dontSubtractPurchasePlatinumCost?: boolean; + dontSubtractPurchaseItemCost?: boolean; + dontSubtractPurchaseStandingCost?: boolean; + dontSubtractVoidTraces?: boolean; + dontSubtractConsumables?: boolean; + finishInvasionsInOneMission?: boolean; + infiniteCredits?: boolean; + infinitePlatinum?: boolean; + infiniteEndo?: boolean; + infiniteRegalAya?: boolean; + infiniteHelminthMaterials?: boolean; + universalPolarityEverywhere?: boolean; + unlockDoubleCapacityPotatoesEverywhere?: boolean; + unlockExilusEverywhere?: boolean; + unlockArcanesEverywhere?: boolean; + syndicateMissionsRepeatable?: boolean; + instantFinishRivenChallenge?: boolean; + noDailyStandingLimits?: boolean; + noDailyFocusLimit?: boolean; + noArgonCrystalDecay?: boolean; + noMasteryRankUpCooldown?: boolean; + noVendorPurchaseLimits?: boolean; + noDeathMarks?: boolean; + noKimCooldowns?: boolean; + claimingBlueprintRefundsIngredients?: boolean; + instantResourceExtractorDrones?: boolean; + noResourceExtractorDronesDamage?: boolean; + missionsCanGiveAllRelics?: boolean; + exceptionalRelicsAlwaysGiveBronzeReward?: boolean; + flawlessRelicsAlwaysGiveSilverReward?: boolean; + radiantRelicsAlwaysGiveGoldReward?: boolean; + disableDailyTribute?: boolean; +} + +export interface IInventoryDatabase + extends Omit< + IInventoryClient, + | "TrainingDate" + | "LoadOutPresets" + | "Mailbox" + | "GuildId" + | "PendingRecipes" + | "Created" + | "QuestKeys" + | "BlessingCooldown" + | "Ships" + | "WeaponSkins" + | "Upgrades" + | "CrewShipWeaponSkins" + | "CrewShipSalvagedWeaponSkins" + | "AdultOperatorLoadOuts" + | "OperatorLoadOuts" + | "KahlLoadOuts" + | "InfestedFoundry" + | "DialogueHistory" + | "KubrowPetEggs" + | "KubrowPetPrints" + | "PendingCoupon" + | "Drones" + | "RecentVendorPurchases" + | "NextRefill" + | "Nemesis" + | "NemesisHistory" + | "EntratiVaultCountResetDate" + | "BrandedSuits" + | "LockedWeaponGroup" + | "PersonalTechProjects" + | "LastSortieReward" + | "LastLiteSortieReward" + | "CrewMembers" + | "QualifyingInvasions" + | "LastInventorySync" + | "EndlessXP" + | "PersonalGoalProgress" + | "CurrentLoadOutIds" + | TEquipmentKey + >, + InventoryDatabaseEquipment, + IAccountCheats { + accountOwnerId: Types.ObjectId; + Created: Date; + CurrentLoadOutIds: Types.ObjectId[] | IOid[]; // should be Types.ObjectId[] but might be IOid[] because of old commits + TrainingDate: Date; + LoadOutPresets: Types.ObjectId; // LoadOutPresets changed from ILoadOutPresets to Types.ObjectId for population + //Mailbox?: IMailboxDatabase; + GuildId?: Types.ObjectId; + PendingRecipes: IPendingRecipeDatabase[]; + QuestKeys: IQuestKeyDatabase[]; + BlessingCooldown?: Date; + Ships: Types.ObjectId[]; + WeaponSkins: IWeaponSkinDatabase[]; + Upgrades: IUpgradeDatabase[]; + CrewShipWeaponSkins: IUpgradeDatabase[]; + CrewShipSalvagedWeaponSkins: IUpgradeDatabase[]; + AdultOperatorLoadOuts: IOperatorConfigDatabase[]; + OperatorLoadOuts: IOperatorConfigDatabase[]; + KahlLoadOuts: IOperatorConfigDatabase[]; + InfestedFoundry?: IInfestedFoundryDatabase; + DialogueHistory?: IDialogueHistoryDatabase; + KubrowPetEggs: IKubrowPetEggDatabase[]; + KubrowPetPrints: IKubrowPetPrintDatabase[]; + 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[]; + PersonalGoalProgress?: IGoalProgressDatabase[]; +} + +export interface IQuestKeyDatabase { + Progress?: IQuestStage[]; + unlock?: boolean; + Completed?: boolean; + CustomData?: string; + ItemType: string; + CompletionDate?: Date; +} + +export const equipmentKeys = [ + "Suits", + "LongGuns", + "Pistols", + "Melee", + "SpecialItems", + "Sentinels", + "SentinelWeapons", + "SpaceSuits", + "SpaceGuns", + "SpaceMelee", + "Hoverboards", + "OperatorAmps", + "MoaPets", + "Scoops", + "Horses", + "DrifterGuns", + "DrifterMelee", + "Motorcycles", + "CrewShips", + "DataKnives", + "MechSuits", + "CrewShipHarnesses", + "KubrowPets", + "CrewShipWeapons", + "CrewShipSalvagedWeapons" +] as const; + +export type TEquipmentKey = (typeof equipmentKeys)[number]; + +export interface IDuviriInfo { + Seed: bigint; + NumCompletions: number; +} + +export interface IMailboxClient { + LastInboxId: IOid; +} + +/*export interface IMailboxDatabase { + LastInboxId: Types.ObjectId; +}*/ + +export type TSolarMapRegion = + | "Earth" + | "Ceres" + | "Eris" + | "Europa" + | "Jupiter" + | "Mars" + | "Mercury" + | "Neptune" + | "Phobos" + | "Pluto" + | "Saturn" + | "Sedna" + | "Uranus" + | "Venus" + | "Void" + | "SolarMapDeimosName" + | "1999MapName"; + +//TODO: perhaps split response and database into their own files + +export enum LoadoutIndex { + NORMAL = 0, + DATAKNIFE = 7 +} + +export interface IDailyAffiliations { + DailyAffiliation: number; + DailyAffiliationPvp: number; + DailyAffiliationLibrary: number; + DailyAffiliationCetus: number; + DailyAffiliationQuills: number; + DailyAffiliationSolaris: number; + DailyAffiliationVentkids: number; + DailyAffiliationVox: number; + DailyAffiliationEntrati: number; + DailyAffiliationNecraloid: number; + DailyAffiliationZariman: number; + DailyAffiliationKahl: number; + DailyAffiliationCavia: number; + DailyAffiliationHex: number; +} + +export type InventoryClientEquipment = { + [_ in TEquipmentKey]: IEquipmentClient[]; +}; + +export interface IInventoryClient extends IDailyAffiliations, InventoryClientEquipment { + AdultOperatorLoadOuts: IOperatorConfigClient[]; + OperatorLoadOuts: IOperatorConfigClient[]; + KahlLoadOuts: IOperatorConfigClient[]; + + DuviriInfo?: IDuviriInfo; + Mailbox?: IMailboxClient; + SubscribedToEmails: number; + Created: IMongoDate; + RewardSeed: bigint; + RegularCredits: number; + PremiumCredits: number; + PremiumCreditsFree: number; + FusionPoints: number; + CrewShipFusionPoints: number; //Dirac (pre-rework Railjack) + PrimeTokens: number; + SuitBin: ISlots; + WeaponBin: ISlots; + SentinelBin: ISlots; + SpaceSuitBin: ISlots; + SpaceWeaponBin: ISlots; + PvpBonusLoadoutBin: ISlots; + PveBonusLoadoutBin: ISlots; + RandomModBin: ISlots; + MechBin: ISlots; + CrewMemberBin: ISlots; + OperatorAmpBin: ISlots; + CrewShipSalvageBin: ISlots; + TradesRemaining: number; + DailyFocus: number; + GiftsRemaining: number; + HandlerPoints: number; + MiscItems: IMiscItem[]; + HasOwnedVoidProjectionsPreviously?: boolean; + ChallengesFixVersion?: number; + ChallengeProgress: IChallengeProgress[]; + RawUpgrades: IRawUpgrade[]; + ReceivedStartingGear: boolean; + Ships: IShipInventory[]; + QuestKeys: IQuestKeyClient[]; + ActiveQuest: string; + FlavourItems: IFlavourItem[]; + LoadOutPresets: ILoadOutPresets; + CurrentLoadOutIds: IOid[]; + Missions: IMission[]; + RandomUpgradesIdentified?: number; + LastRegionPlayed: TSolarMapRegion; + XPInfo: ITypeXPItem[]; + Recipes: ITypeCount[]; + WeaponSkins: IWeaponSkinClient[]; + PendingRecipes: IPendingRecipeClient[]; + TrainingDate: IMongoDate; + PlayerLevel: number; + Staff?: boolean; + Founder?: number; + Guide?: number; + Moderator?: boolean; + Partner?: boolean; + Accolades?: IAccolades; + Counselor?: boolean; + Upgrades: IUpgradeClient[]; + EquippedGear: string[]; + DeathMarks: string[]; + FusionTreasures: IFusionTreasure[]; + //WebFlags: IWebFlags; + CompletedAlerts: string[]; + Consumables: ITypeCount[]; + LevelKeys: ITypeCount[]; + TauntHistory?: ITaunt[]; + StoryModeChoice: string; + PeriodicMissionCompletions: IPeriodicMissionCompletionDatabase[]; + KubrowPetEggs?: IKubrowPetEggClient[]; + LoreFragmentScans: ILoreFragmentScan[]; + EquippedEmotes: string[]; + //PendingTrades: IPendingTrade[]; + Boosters: IBooster[]; + ActiveDojoColorResearch: string; + //SentientSpawnChanceBoosters: ISentientSpawnChanceBoosters; + SupportedSyndicate?: string; + Affiliations: IAffiliation[]; + QualifyingInvasions: IInvasionProgressClient[]; + FactionScores: number[]; + ArchwingEnabled?: boolean; + PendingSpectreLoadouts?: ISpectreLoadout[]; + SpectreLoadouts?: ISpectreLoadout[]; + UsedDailyDeals: string[]; + EmailItems: ITypeCount[]; + CompletedSyndicates: string[]; + FocusXP?: IFocusXP; + Wishlist: string[]; + Alignment?: IAlignment; + CompletedSorties: string[]; + LastSortieReward?: ILastSortieRewardClient[]; + LastLiteSortieReward?: ILastSortieRewardClient[]; + SortieRewardAttenuation?: IRewardAttenuation[]; + Drones: IDroneClient[]; + StepSequencers: IStepSequencer[]; + ActiveAvatarImageType?: string; + ShipDecorations: ITypeCount[]; + DiscoveredMarkers: IDiscoveredMarker[]; + //CompletedJobs: ICompletedJob[]; + FocusAbility?: string; + FocusUpgrades: IFocusUpgrade[]; + HasContributedToDojo?: boolean; + HWIDProtectEnabled?: boolean; + KubrowPetPrints: IKubrowPetPrintClient[]; + AlignmentReplay?: IAlignment; + PersonalGoalProgress?: IGoalProgressClient[]; + ThemeStyle: string; + ThemeBackground: string; + ThemeSounds: string; + BountyScore: number; + //ChallengeInstanceStates: IChallengeInstanceState[]; + LoginMilestoneRewards: string[]; + RecentVendorPurchases?: IRecentVendorPurchaseClient[]; + NodeIntrosCompleted: string[]; + GuildId?: IOid; + CompletedJobChains?: ICompletedJobChain[]; + SeasonChallengeHistory: ISeasonChallenge[]; + EquippedInstrument?: string; + //InvasionChainProgress: IInvasionChainProgress[]; + Nemesis?: INemesisClient; + NemesisHistory?: INemesisBaseClient[]; + //LastNemesisAllySpawnTime?: IMongoDate; + Settings?: ISettings; + PersonalTechProjects: IPersonalTechProjectClient[]; + PlayerSkills: IPlayerSkills; + CrewShipAmmo: ITypeCount[]; + CrewShipWeaponSkins: IUpgradeClient[]; + CrewShipSalvagedWeaponSkins: IUpgradeClient[]; + //TradeBannedUntil?: IMongoDate; + PlayedParkourTutorial: boolean; + SubscribedToEmailsPersonalized: number; + InfestedFoundry?: IInfestedFoundryClient; + BlessingCooldown?: IMongoDate; + CrewShipRawSalvage: ITypeCount[]; + CrewMembers: ICrewMemberClient[]; + LotusCustomization?: ILotusCustomization; + UseAdultOperatorLoadout?: boolean; + OperatorCustomizationSlotPurchases?: number; + NemesisAbandonedRewards: string[]; + LastInventorySync?: IOid; + NextRefill?: IMongoDate; + FoundToday?: IMiscItem[]; // for Argon Crystals + CustomMarkers?: ICustomMarkers[]; + //ActiveLandscapeTraps: any[]; + EvolutionProgress?: IEvolutionProgress[]; + //RepVotes: any[]; + //LeagueTickets: any[]; + //Quests: any[]; + //Robotics: any[]; + LibraryPersonalTarget?: string; + LibraryPersonalProgress: ILibraryPersonalProgress[]; + CollectibleSeries?: ICollectibleEntry[]; + LibraryAvailableDailyTaskInfo?: ILibraryDailyTaskInfo; + LibraryActiveDailyTaskInfo?: ILibraryDailyTaskInfo; + HasResetAccount: boolean; + PendingCoupon?: IPendingCouponClient; + Harvestable: boolean; + DeathSquadable: boolean; + 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?: IOrbiterClient; // U22 and below, response only + ClaimedJunctionChallengeRewards?: string[]; // U39 + SpecialItemRewardAttenuation?: IRewardAttenuation[]; // Baro's Void Surplus +} + +export interface IAffiliation { + Initiated?: boolean; + Standing: number; + 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; +} + +export interface IBooster { + ExpiryDate: number; + ItemType: string; + UsesRemaining?: number; +} + +export interface IChallengeInstanceState { + id: IOid; + Progress: number; + params: IParam[]; + IsRewardCollected: boolean; +} + +export interface IParam { + n: string; + 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; + Completed?: string[]; + ReceivedJunctionReward?: boolean; // U39 + Name: string; +} + +export interface ICollectibleEntry { + CollectibleType: string; + Count: number; + Tracking: string; + ReqScans: number; + IncentiveStates: IIncentiveState[]; +} + +export interface IIncentiveState { + threshold: number; + complete: boolean; + sent: boolean; +} + +export interface ICompletedJobChain { + LocationTag: string; + Jobs: string[]; +} + +export interface ICompletedJob { + JobId: string; + StageCompletions: number[]; +} + +export interface ICrewMemberSkill { + Assigned: number; +} + +export interface ICrewMemberSkillEfficiency { + PILOTING: ICrewMemberSkill; + GUNNERY: ICrewMemberSkill; + ENGINEERING: ICrewMemberSkill; + COMBAT: ICrewMemberSkill; + SURVIVABILITY: ICrewMemberSkill; +} + +export interface ICrewMemberClient { + ItemType: string; + NemesisFingerprint: bigint; + Seed: bigint; + AssignedRole?: number; + SkillEfficiency: ICrewMemberSkillEfficiency; + WeaponConfigIdx: number; + WeaponId: IOid; + XP: number; + PowersuitType: string; + Configs: IItemConfig[]; + SecondInCommand: boolean; // on call + ItemId: IOid; +} + +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", + AMPS = "OperatorAmpBin", + RJ_COMPONENT_AND_ARMAMENTS = "CrewShipSalvageBin", + CREWMEMBERS = "CrewMemberBin", + RIVENS = "RandomModBin" +} + +export interface ISlots { + Extra?: number; + Slots: number; +} + +export interface IUpgradeClient { + ItemType: string; + UpgradeFingerprint?: string; + PendingRerollFingerprint?: string; + ItemId: IOid; +} + +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 type IMiscItem = ITypeCount; + +export interface IDiscoveredMarker { + tag: string; + discoveryState: number[]; +} + +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; +} + +export interface IFocusUpgrade { + ItemType: string; + Level?: number; + IsUniversal?: boolean; +} + +export interface IFocusXP { + AP_POWER?: number; + AP_TACTIC?: number; + AP_DEFENSE?: number; + AP_ATTACK?: number; + AP_WARD?: number; +} + +export type TFocusPolarity = keyof IFocusXP; + +export interface IFusionTreasure { + ItemCount: number; + ItemType: string; + Sockets: number; +} + +export interface IHelminthFoodRecord { + ItemType: string; + Date: number; +} + +export interface IHelminthResource { + ItemType: string; + Count: number; + RecentlyConvertedResources?: IHelminthFoodRecord[]; +} + +export interface IInfestedFoundryClient { + Name?: string; + Resources?: IHelminthResource[]; + Slots?: number; + XP?: number; + ConsumedSuits?: IConsumedSuit[]; + InvigorationIndex?: number; + InvigorationSuitOfferings?: string[]; + InvigorationsApplied?: number; + LastConsumedSuit?: IEquipmentClient; + AbilityOverrideUnlockCooldown?: IMongoDate; +} + +export interface IInfestedFoundryDatabase + extends Omit { + LastConsumedSuit?: IEquipmentDatabase; + AbilityOverrideUnlockCooldown?: Date; +} + +export interface IConsumedSuit { + s: string; + c?: IColor; +} + +export interface IInvasionChainProgress { + id: IOid; + 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 + ItemId: IOid; +} + +export interface IKubrowPetEggDatabase { + ItemType: string; + _id: Types.ObjectId; +} + +export interface IKubrowPetPrintClient { + ItemType: "/Lotus/Types/Game/KubrowPet/ImprintedTraitPrint"; + Name: string; + IsMale: boolean; + Size: number; // seems to be 0.7 to 1.0 + DominantTraits: ITraits; + RecessiveTraits: ITraits; + ItemId: IOid; + InheritedModularParts?: any[]; +} + +export interface IKubrowPetPrintDatabase extends Omit { + _id: Types.ObjectId; +} + +export interface ILastSortieRewardClient { + SortieId: IOid; + StoreItem: string; + Manifest: string; +} + +export interface ILastSortieRewardDatabase extends Omit { + SortieId: Types.ObjectId; +} + +export interface IRewardAttenuation { + Tag: string; + Atten: number; +} + +export interface ILibraryDailyTaskInfo { + EnemyTypes: string[]; + EnemyLocTag: string; + EnemyIcon: string; + Scans?: number; + ScansRequired: number; + RewardStoreItem: string; + RewardQuantity: number; + RewardStanding: number; +} + +export interface ILibraryPersonalProgress { + TargetType: string; + Scans: number; + Completed: boolean; +} + +export enum UpgradeType { + LotusWeaponsGrineerKuvaLichUpgradesInnateDamageRandomMod = "/Lotus/Weapons/Grineer/KuvaLich/Upgrades/InnateDamageRandomMod" +} + +export interface ILoreFragmentScan { + Progress: number; + Region: string; + ItemType: string; +} + +export interface IMissionDatabase { + Tag: string; + Completes: number; + Tier?: number; +} + +export interface IMission extends IMissionDatabase { + RewardsCooldownTime?: IMongoDate; +} + +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: string; + Faction: TNemesisFaction; + Rank: number; + k: boolean; + Traded: boolean; + d: IMongoDate; + PrevOwners: number; + SecondInCommand: boolean; + Weakened: boolean; +} + +export interface INemesisBaseDatabase extends Omit { + fp: bigint; + d: Date; +} + +export interface INemesisClient extends INemesisBaseClient { + InfNodes: IInfNode[]; + HenchmenKilled: number; + HintProgress: number; + Hints: number[]; + GuessHistory: number[]; + MissionCount: number; + LastEnc: number; +} + +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 IPendingRecipeDatabase { + ItemType: string; + CompletionDate: Date; + ItemId: IOid; + TargetItemId?: string; // unsure what this is for + TargetFingerprint?: string; + LongGuns?: IEquipmentDatabase[]; + Pistols?: IEquipmentDatabase[]; + Melee?: IEquipmentDatabase[]; + SuitToUnbrand?: Types.ObjectId; + KubrowPet?: Types.ObjectId; +} + +export interface IPendingRecipeClient + extends Omit< + IPendingRecipeDatabase, + "CompletionDate" | "LongGuns" | "Pistols" | "Melee" | "SuitToUnbrand" | "KubrowPet" + > { + CompletionDate: IMongoDate; +} + +export interface IAccolades { + Heirloom?: boolean; +} + +export interface IPendingTrade { + State: number; + SelfReady: boolean; + BuddyReady: boolean; + Giving?: IGiving; + Revision: number; + Getting: IGetting; + ItemId: IOid; + ClanTax?: number; +} + +export interface IGetting { + RandomUpgrades?: IRandomUpgrade[]; + _SlotOrderInfo: GettingSlotOrderInfo[]; + PremiumCredits?: number; +} + +export interface IRandomUpgrade { + UpgradeFingerprint: RivenFingerprint; + ItemType: string; + ItemId: IOid; +} + +export interface IInnateDamageFingerprint { + compat: string; + buffs: IFingerprintStat[]; +} + +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 { + Empty = "", + LotusUpgradesModsRandomizedPlayerMeleeWeaponRandomModRare0 = "/Lotus/Upgrades/Mods/Randomized/PlayerMeleeWeaponRandomModRare:0", + P = "P" +} + +export interface IGiving { + RawUpgrades: ITypeCount[]; + _SlotOrderInfo: GivingSlotOrderInfo[]; +} + +export enum GivingSlotOrderInfo { + Empty = "", + LotusTypesSentinelsSentinelPreceptsItemVacum = "/Lotus/Types/Sentinels/SentinelPrecepts/ItemVacum", + LotusUpgradesModsPistolDualStatElectEventPistolMod = "/Lotus/Upgrades/Mods/Pistol/DualStat/ElectEventPistolMod" +} + +export interface IPeriodicMissionCompletionDatabase { + date: Date; + tag: string; + count?: number; +} + +export interface IPeriodicMissionCompletionResponse extends Omit { + date: IMongoDate; +} + +export interface IGoalProgressClient { + Best?: number; + Count: number; + Tag: string; + _id: IOid; + //ReceivedClanReward0?: boolean; + //ReceivedClanReward1?: boolean; +} + +export interface IGoalProgressDatabase extends Omit { + goalId: Types.ObjectId; +} + +export interface IPersonalTechProjectDatabase { + State: number; + ReqCredits: number; + ItemType: string; + ProductCategory?: string; + CategoryItemId?: Types.ObjectId; + ReqItems: ITypeCount[]; + HasContributions?: boolean; + CompletionDate?: Date; +} + +export interface IPersonalTechProjectClient + extends Omit { + CategoryItemId?: IOid; + CompletionDate?: IMongoDate; + ItemId: IOid; +} + +export interface IPlayerSkills { + LPP_SPACE: number; + LPS_PILOTING: number; + LPS_GUNNERY: number; + LPS_TACTICAL: number; + LPS_ENGINEERING: number; + LPS_COMMAND: number; + LPP_DRIFTER: number; + LPS_DRIFT_COMBAT: number; + LPS_DRIFT_RIDING: number; + LPS_DRIFT_OPPORTUNITY: number; + LPS_DRIFT_ENDURANCE: number; +} + +export interface IQuestKeyClient extends Omit { + CompletionDate?: IMongoDate; +} + +export interface IQuestStage { + c?: number; + i?: boolean; + m?: boolean; + b?: any[]; +} + +export interface IRawUpgrade { + ItemType: string; + ItemCount: number; + LastAdded?: IOidWithLegacySupport; +} + +export interface ISeasonChallenge { + challenge: string; + id: string; +} + +export interface ISentientSpawnChanceBoosters { + numOceanMissionsCompleted: number; +} + +export interface ISettings { + 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 { + ItemType: string; + ShipExterior: IShipCustomization; + AirSupportPower: string; + ItemId: IOid; +} + +export interface ISpectreLoadout { + ItemType: string; + Suits: string; + LongGuns: string; + LongGunsModularParts?: string[]; + Pistols: string; + PistolsModularParts?: string[]; + Melee: string; + MeleeModularParts?: string[]; +} + +export interface IStepSequencer { + NotePacks: INotePacks; + FingerPrint: string; + Name: string; + ItemId?: IOid; +} + +export interface INotePacks { + MELODY: string; + BASS: string; + PERCUSSION: string; +} + +export interface ITaunt { + node: string; + state: "TS_UNLOCKED" | "TS_COMPLETED"; +} + +export interface IWeaponSkinDatabase { + ItemType: string; + Favorite?: boolean; + IsNew?: boolean; + _id: Types.ObjectId; +} + +export interface IWeaponSkinClient extends Omit { + ItemId: IOid; +} + +export interface IWebFlags { + activeBuyPlat: number; + noShow2FA: boolean; + Tennocon2018Digital: boolean; + VisitPrimeAccess: IMongoDate; + VisitTennocon2019: IMongoDate; + enteredSC2019: IMongoDate; + VisitPrimeVault: IMongoDate; + VisitBuyPlatinum: IMongoDate; + ClickedSku_640_Page__en_buyplatinum: IMongoDate; + ClickedSku_640_Page__buyplatinum: IMongoDate; + VisitStarterPack: IMongoDate; + Tennocon2020Digital: boolean; + Anniversary2021: boolean; + HitDownloadBtn: IMongoDate; +} + +export interface IEvolutionProgress { + Progress: number; + Rank: number; + ItemType: string; +} + +export type TEndlessXpCategory = "EXC_NORMAL" | "EXC_HARD"; + +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; + Resets?: number; // added in 38.5.0 + Dialogues?: IDialogueClient[]; +} + +export interface IDialogueHistoryDatabase { + YearIteration?: number; + Resets?: number; + Dialogues?: IDialogueDatabase[]; +} + +export interface IDialogueClient { + Rank: number; + Chemistry: number; + AvailableDate: IMongoDate; + AvailableGiftDate: IMongoDate; + RankUpExpiry: IMongoDate; + BountyChemExpiry: IMongoDate; + QueuedDialogues: string[]; + Gifts: IDialogueGift[]; + Booleans: string[]; + Completed: ICompletedDialogue[]; + DialogueName: string; +} + +export interface IDialogueDatabase + extends Omit { + AvailableDate: Date; + AvailableGiftDate: Date; + RankUpExpiry: Date; + BountyChemExpiry: Date; +} + +export interface IDialogueGift { + Item: string; + GiftedQuantity: number; +} + +export interface ICompletedDialogue { + Id: string; + Booleans: string[]; + Choices: number[]; +} + +export interface ICustomMarkers { + tag: string; + markerInfos: IMarkerInfo[]; +} + +export interface IMarkerInfo { + icon: string; + markers: IMarker[]; +} + +export interface IMarker { + anchorName: string; + color: number; + label?: string; + x: number; + y: number; + 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..4d7fbea2 --- /dev/null +++ b/src/types/leaderboardTypes.ts @@ -0,0 +1,18 @@ +import type { 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 new file mode 100644 index 00000000..2d1b33a1 --- /dev/null +++ b/src/types/loginTypes.ts @@ -0,0 +1,68 @@ +import type { Types } from "mongoose"; + +export interface IAccountAndLoginResponseCommons { + DisplayName: string; + CountryCode?: string; + ClientType?: string; + CrossPlatformAllowed?: boolean; + ForceLogoutVersion?: number; + AmazonAuthToken?: string; + AmazonRefreshToken?: string; + ConsentNeeded?: boolean; + TrackedSettings?: string[]; + Nonce: number; +} + +export interface IDatabaseAccountRequiredFields extends IAccountAndLoginResponseCommons { + email: string; + password: string; + BuildLabel?: string; + LastLogin: Date; +} + +export interface IDatabaseAccount extends IDatabaseAccountRequiredFields { + Dropped?: boolean; + LatestEventMessageDate: Date; + LastLoginRewardDate: number; + LoginDays: number; + DailyFirstWinDate: number; +} + +// Includes virtual ID +export interface IDatabaseAccountJson extends IDatabaseAccount { + id: string; +} + +export interface ILoginRequest { + email: string; + password: string; + time: number; + s?: string; + lang?: string; + date: number; + ClientType?: string; + PS?: string; + kick?: boolean; +} + +export interface ILoginResponse extends IAccountAndLoginResponseCommons { + id: string; + Groups?: IGroup[]; + BuildLabel: 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 new file mode 100644 index 00000000..c1f08d65 --- /dev/null +++ b/src/types/missionTypes.ts @@ -0,0 +1,41 @@ +import type { IAffiliationMods, IInventoryChanges } from "./purchaseTypes.ts"; + +export const inventoryFields = ["RawUpgrades", "MiscItems", "Consumables", "Recipes"] as const; +export type IInventoryFieldType = (typeof inventoryFields)[number]; + +export interface IMissionReward { + StoreItem: string; + TypeName?: string; + UpgradeLevel?: number; + ItemCount: number; + DailyCooldown?: boolean; + Rarity?: number; + TweetText?: string; + ProductCategory?: string; + FromEnemyCache?: boolean; + IsStrippedItem?: boolean; +} + +export interface IMissionCredits { + MissionCredits: [number, number]; + CreditsBonus: [number, number]; // "Credit Reward"; `CreditsBonus[1]` is `CreditsBonus[0] * 2` if DailyMissionBonus + TotalCredits: [number, number]; + DailyMissionBonus?: boolean; +} + +export interface IMissionInventoryUpdateResponseRailjackInterstitial extends Partial { + ConquestCompletedMissionsCount?: number; + MissionRewards?: IMissionReward[]; + InventoryChanges?: IInventoryChanges; + FusionPoints?: number; + SyndicateXPItemReward?: number; + AffiliationMods?: IAffiliationMods[]; +} + +export interface IMissionInventoryUpdateResponse extends IMissionInventoryUpdateResponseRailjackInterstitial { + InventoryJson?: string; +} + +export interface IMissionInventoryUpdateResponseBackToDryDock { + InventoryJson: string; +} diff --git a/src/types/personalRoomsTypes.ts b/src/types/personalRoomsTypes.ts new file mode 100644 index 00000000..e0cdc0d1 --- /dev/null +++ b/src/types/personalRoomsTypes.ts @@ -0,0 +1,270 @@ +import type { IColor, IShipAttachments, IShipCustomization } from "./inventoryTypes/commonInventoryTypes.ts"; +import type { Document, Model, Types } from "mongoose"; +import type { ILoadoutClient, ILoadoutConfigClient, ILoadoutConfigDatabase } from "./saveLoadoutTypes.ts"; +import type { IMongoDate, IOid } from "./commonTypes.ts"; + +export interface IGetShipResponse { + ShipOwnerId: string; + Ship: IOrbiterClient; + Apartment: IApartmentClient; + TailorShop: ITailorShop; + LoadOutInventory: { LoadOutPresets: ILoadoutClient }; +} + +export type TBootLocation = "LISET" | "DRIFTER_CAMP" | "APARTMENT" | "SHOP"; + +export interface IOrbiterClient { + Features: string[]; + ShipId: IOid; + Rooms: IRoomClient[]; + ShipInterior?: IShipCustomization; + VignetteFish?: string[]; + FavouriteLoadoutId?: IOid; + Wallpaper?: string; + Vignette?: string; + BootLocation?: TBootLocation; + ContentUrlSignature?: string; +} + +export interface IOrbiterDatabase { + Features: string[]; + Rooms: IRoomDatabase[]; + ShipInterior?: IShipCustomization; + VignetteFish?: string[]; + FavouriteLoadoutId?: Types.ObjectId; + Wallpaper?: string; + Vignette?: string; + ContentUrlSignature?: string; + BootLocation?: TBootLocation; +} + +export interface IPersonalRoomsClient { + Ship: IOrbiterClient; + Apartment: IApartmentClient; + TailorShop: ITailorShop; +} + +export interface IPersonalRoomsDatabase { + personalRoomsOwnerId: Types.ObjectId; + activeShipId: Types.ObjectId; + + Ship: IOrbiterDatabase; + Apartment: IApartmentDatabase; + TailorShop: ITailorShopDatabase; +} + +export interface IRoomDatabase { + Name: string; + MaxCapacity: number; + PlacedDecos?: IPlacedDecosDatabase[]; +} + +export interface IRoomClient { + Name: string; + MaxCapacity: number; + PlacedDecos?: IPlacedDecosClient[]; +} + +export interface IPlantClient { + PlantType: string; + EndTime: IMongoDate; + PlotIndex: number; +} + +export interface IPlantDatabase extends Omit { + EndTime: Date; +} + +export interface IPlanterClient { + Name: string; + Plants: IPlantClient[]; +} + +export interface IPlanterDatabase { + Name: string; + Plants: IPlantDatabase[]; +} + +export interface IGardeningClient { + Planters: IPlanterClient[]; +} + +export interface IGardeningDatabase { + Planters: IPlanterDatabase[]; +} + +export interface IApartmentClient { + Gardening: IGardeningClient; + Rooms: IRoomClient[]; + FavouriteLoadouts?: IFavouriteLoadout[]; + VideoWallBackdrop?: string; + Soundscape?: string; +} + +export interface IApartmentDatabase { + Gardening: IGardeningDatabase; + Rooms: IRoomDatabase[]; + FavouriteLoadouts: IFavouriteLoadoutDatabase[]; + VideoWallBackdrop?: string; + Soundscape?: string; +} + +export interface IPlacedDecosDatabase { + Type: string; + Pos: [number, number, number]; + Rot: [number, number, number]; + Scale?: number; + Sockets?: number; + PictureFrameInfo?: IPictureFrameInfo; + CustomizationInfo?: ICustomizationInfoDatabase; + AnimPoseItem?: string; + _id: Types.ObjectId; +} + +export interface IPlacedDecosClient extends Omit { + id: IOid; + CustomizationInfo?: ICustomizationInfoClient; +} + +export interface ISetShipCustomizationsRequest { + ShipId: string; + Customization: { + SkinFlavourItem?: string; + Colors?: IColor; + ShipAttachments?: IShipAttachments; + LevelDecosVisible?: boolean; + CustomJson?: string; + }; + IsExterior: boolean; + AirSupportPower?: string; + IsShop?: boolean; +} + +export interface IShipDecorationsRequest { + Type: string; + Pos: [number, number, number]; + Rot: [number, number, number]; + Room: string; + BootLocation?: TBootLocation; + IsApartment?: boolean; + RemoveId?: string; + MoveId?: string; + OldRoom?: string; + Scale?: number; + Sockets?: number; +} + +export interface IShipDecorationsResponse { + DecoId?: string; + Room?: string; + IsApartment?: boolean; + MaxCapacityIncrease?: number; + OldRoom?: string; + NewRoom?: string; +} + +export interface IResetShipDecorationsRequest { + Room: string; + BootLocation?: TBootLocation; +} + +export interface IResetShipDecorationsResponse { + ResetRoom: string; + ClaimedDecos: []; + NewCapacity: number; +} + +export interface ISetPlacedDecoInfoRequest { + DecoType?: string; + DecoId: string; + Room: string; + PictureFrameInfo: IPictureFrameInfo; // IsPicture + CustomizationInfo?: ICustomizationInfoClient; // !IsPicture + BootLocation?: TBootLocation; + AnimPoseItem?: string; // !IsPicture + ComponentId?: string; + GuildId?: string; +} + +export interface IPictureFrameInfo { + Image: string; + Filter: string; + XOffset: number; + YOffset: number; + Scale: number; + InvertX: boolean; + InvertY: boolean; + ColorCorrection: number; + Text: string; + TextScale: number; + TextColorA: number; + TextColorB: number; + TextOrientation: number; +} + +export interface ICustomizationInfoClient { + Anim?: string; + AnimPose?: number; + LoadOutPreset?: ILoadoutConfigClient; + VehiclePreset?: ILoadoutConfigClient; + EquippedWeapon?: "SUIT_SLOT" | "LONG_GUN_SLOT" | "PISTOL_SLOT"; + AvatarType?: string; + LoadOutType?: string; // "LOT_NORMAL" +} + +export interface ICustomizationInfoDatabase extends Omit { + LoadOutPreset?: ILoadoutConfigDatabase; + VehiclePreset?: ILoadoutConfigDatabase; +} + +export interface IFavouriteLoadout { + Tag: string; + LoadoutId: IOid; +} + +export interface IFavouriteLoadoutDatabase { + Tag: string; + LoadoutId: Types.ObjectId; +} + +export interface ITailorShopDatabase { + FavouriteLoadouts: IFavouriteLoadoutDatabase[]; + Colors?: IColor; + CustomJson?: string; + LevelDecosVisible?: boolean; + Rooms: IRoomDatabase[]; +} + +export interface ITailorShop extends Omit { + Rooms: IRoomClient[]; + FavouriteLoadouts?: IFavouriteLoadout[]; +} + +export type RoomsType = { Name: string; MaxCapacity: number; PlacedDecos: Types.DocumentArray }; + +export type PersonalRoomsDocumentProps = { + Ship: Omit & { + Rooms: RoomsType[]; + }; + Apartment: Omit & { + Rooms: RoomsType[]; + }; + TailorShop: Omit & { + Rooms: RoomsType[]; + }; +}; + +// 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 new file mode 100644 index 00000000..d9b19c8f --- /dev/null +++ b/src/types/purchaseTypes.ts @@ -0,0 +1,154 @@ +import type { ITypeCount } from "./commonTypes.ts"; +import type { IEquipmentClient } from "./equipmentTypes.ts"; +import type { + IDroneClient, + IInfestedFoundryClient, + IMiscItem, + INemesisClient, + IRecentVendorPurchaseClient, + TEquipmentKey, + ICrewMemberClient, + IKubrowPetPrintClient, + IUpgradeClient +} from "./inventoryTypes/inventoryTypes.ts"; + +export enum PurchaseSource { + Market = 0, + VoidTrader = 1, + SyndicateFavor = 2, + DailyDeal = 3, + Arsenal = 4, + Profile = 5, + Hub = 6, + Vendor = 7, + AppearancePreview = 8, + Museum = 9, + Operator = 10, + PlayerShip = 11, + Crewship = 12, + MenuStyle = 13, + MenuHud = 14, + Chat = 15, + Inventory = 16, + StarChart = 17, + PrimeVaultTrader = 18, + Incubator = 19, + Prompt = 20, + Kaithe = 21, + DuviriWeapon = 22, + UpdateScreen = 23, + Motorcycle = 24 +} + +export interface IPurchaseRequest { + PurchaseParams: IPurchaseParams; + buildLabel: string; +} + +export interface IPurchaseParams { + Source: PurchaseSource; + SourceId?: string; // VoidTrader, Vendor, PrimeVaultTrader + StoreItem: string; + StorePage: string; + SearchTerm: string; + CurrentLocation: string; + Quantity: number; + UsePremium: boolean; + ExpectedPrice: number; + SyndicateTag?: string; // SyndicateFavor + UseFreeFavor?: boolean; // SyndicateFavor + ExtraPurchaseInfoJson?: string; // Vendor + IsWeekly?: boolean; // Vendor +} + +export type IInventoryChanges = { + [_ in SlotNames]?: IBinChanges; +} & { + [_ in TEquipmentKey]?: IEquipmentClient[]; +} & { + RegularCredits?: number; + PremiumCredits?: number; + PremiumCreditsFree?: number; + FusionPoints?: number; + PrimeTokens?: number; + InfestedFoundry?: IInfestedFoundryClient; + Drones?: IDroneClient[]; + MiscItems?: IMiscItem[]; + ShipDecorations?: ITypeCount[]; + EmailItems?: ITypeCount[]; + CrewShipRawSalvage?: ITypeCount[]; + Nemesis?: Partial; + NewVendorPurchase?: IRecentVendorPurchaseClient; // >= 38.5.0 + RecentVendorPurchases?: IRecentVendorPurchaseClient; // < 38.5.0 + CrewMembers?: ICrewMemberClient[]; + KubrowPetPrints?: IKubrowPetPrintClient[]; + Upgrades?: IUpgradeClient[]; // TOVERIFY +} & Record< + Exclude< + string, + | SlotNames + | TEquipmentKey + | "RegularCredits" + | "PremiumCredits" + | "PremiumCreditsFree" + | "InfestedFoundry" + | "Drones" + | "MiscItems" + | "EmailItems" + >, + number | object[] + >; + +export interface IAffiliationMods { + Tag: string; + Standing?: number; + Title?: number; +} + +export interface IPurchaseResponse { + InventoryChanges: IInventoryChanges; + Standing?: IAffiliationMods[]; + FreeFavorsUsed?: IAffiliationMods[]; + BoosterPackItems?: string; + DailyDealUsed?: string; +} + +export type IBinChanges = { + count?: number; + platinum?: number; + Slots: number; + Extra?: number; +}; + +export type SlotPurchaseName = + | "SuitSlotItem" + | "TwoSentinelSlotItem" + | "TwoWeaponSlotItem" + | "SpaceSuitSlotItem" + | "TwoSpaceWeaponSlotItem" + | "MechSlotItem" + | "TwoOperatorWeaponSlotItem" + | "RandomModSlotItem" + | "TwoCrewShipSalvageSlotItem" + | "CrewMemberSlotItem"; + +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; purchaseQuantity: number }; +}; diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts new file mode 100644 index 00000000..c8244b25 --- /dev/null +++ b/src/types/requestTypes.ts @@ -0,0 +1,252 @@ +import type { IOid, ITypeCount } from "./commonTypes.ts"; +import type { ArtifactPolarity, IPolarity } from "./inventoryTypes/commonInventoryTypes.ts"; +import type { + IBooster, + IChallengeProgress, + IEvolutionProgress, + IMission, + IRawUpgrade, + ISeasonChallenge, + TSolarMapRegion, + TEquipmentKey, + IFusionTreasure, + ICustomMarkers, + IPlayerSkills, + IQuestKeyDatabase, + ILoreFragmentScan, + IUpgradeFromClient, + ICollectibleEntry, + IDiscoveredMarker, + ILockedWeaponGroupClient, + IInvasionProgressClient, + IWeaponSkinClient, + IKubrowPetEggClient, + INemesisClient +} from "./inventoryTypes/inventoryTypes.ts"; +import type { IGroup } from "./loginTypes.ts"; +import type { ILoadOutPresets } from "./saveLoadoutTypes.ts"; +import type { IEquipmentClient } from "./equipmentTypes.ts"; + +export interface IAffiliationChange { + Tag: string; + Standing: number; + Title: number; +} + +export type IMissionInventoryUpdateRequest = { + MiscItems?: ITypeCount[]; + Recipes?: ITypeCount[]; + FusionBundles?: ITypeCount[]; + Consumables?: ITypeCount[]; + FusionBundels?: ITypeCount[]; + 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[]; + QuestKeys?: Omit[]; + RegularCredits?: number; + MissionFailed: boolean; + MissionStatus: IMissionStatus; + AliveTime: number; + MissionTime: number; + Missions?: IMission; + LastRegionPlayed?: TSolarMapRegion; + GameModeId: number; + hosts: string[]; + currentClients: unknown[]; + ChallengeProgress: IChallengeProgress[]; + 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; + RepHash: string; + EndOfMatchUpload: boolean; + ObjectiveReached: boolean; + sharedSessionId: string; + FpsAvg: number; + FpsMin: number; + FpsMax: number; + FpsSamples: number; + EvolutionProgress?: IEvolutionProgress[]; + FocusXpIncreases?: number[]; + PlayerSkillGains: Partial; + 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[]; + DROP_MISC_ITEM?: 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 + GoalProgress?: { + _id: IOid; + Count: number; + Best: number; + Tag: string; + IsMultiProgress: boolean; + MultiProgress: unknown[]; + }[]; + InvasionProgress?: IInvasionProgressClient[]; + RJ?: boolean; + 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; + goalId?: string; + goalManifest?: 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; + useVaultManifest?: boolean; + EnemyCachesFound?: number; + 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" + rewardTierOverrides?: number[]; // Disruption + PurgatoryRewardQualifications?: string; + POICompletions?: number; + LootDungeonCompletions?: 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; + GoalProgressAmount?: number; +} + +export type IMissionStatus = "GS_SUCCESS" | "GS_FAILURE" | "GS_DUMPED" | "GS_QUIT" | "GS_INTERRUPTED"; + +export interface IUpgradesRequest { + ItemCategory: TEquipmentKey; + ItemId: IOid; + ItemFeatures: number; + UpgradeVersion: number; + Operations: IUpgradeOperation[]; +} +export interface IUpgradeOperation { + OperationType: string; + UpgradeRequirement: string; // uniqueName of item being consumed + PolarizeSlot: number; + PolarizeValue: ArtifactPolarity; + PolarityRemap: IPolarity[]; +} +export interface IUnlockShipFeatureRequest { + Feature: string; + 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 new file mode 100644 index 00000000..3e0fca46 --- /dev/null +++ b/src/types/saveLoadoutTypes.ts @@ -0,0 +1,150 @@ +import type { IOid } from "./commonTypes.ts"; +import type { + ICrewShipCustomization, + IFlavourItem, + IItemConfig, + ILotusCustomization, + IOperatorConfigClient +} from "./inventoryTypes/commonInventoryTypes.ts"; +import type { Types } from "mongoose"; +import type { + ICrewShipMembersClient, + ICrewShipWeaponClient, + IEquipmentSelectionClient, + IEquipmentSelectionDatabase +} from "./equipmentTypes.ts"; + +export interface ISaveLoadoutRequest { + LoadOuts: ILoadoutClient; + LongGuns: IItemEntry; + OperatorAmps: IItemEntry; + Pistols: IItemEntry; + Suits: IItemEntry; + Melee: IItemEntry; + Sentinels: IItemEntry; + SentinelWeapons: IItemEntry; + KubrowPets: IItemEntry; + SpaceSuits: IItemEntry; + SpaceGuns: IItemEntry; + SpaceMelee: IItemEntry; + Scoops: IItemEntry; + SpecialItems: IItemEntry; + MoaPets: IItemEntry; + Hoverboards: IItemEntry; + DataKnives: IItemEntry; + Motorcycles: IItemEntry; + MechSuits: IItemEntry; + CrewShipHarnesses: IItemEntry; + Horses: IItemEntry; + DrifterMelee: IItemEntry; + UpgradeVer: number; + AdultOperatorLoadOuts: IOperatorConfigEntry; + OperatorLoadOuts: IOperatorConfigEntry; + KahlLoadOuts: IOperatorConfigEntry; + CrewShips: IItemEntry; + CurrentLoadOutIds: IOid[]; + ValidNewLoadoutId: string; + ActiveCrewShip: IOid; + EquippedGear: string[]; + EquippedEmotes: string[]; + UseAdultOperatorLoadout: boolean; + WeaponSkins: IItemEntry; + LotusCustomization: ILotusCustomization; +} + +export type ISaveLoadoutRequestNoUpgradeVer = Omit; + +export interface IOperatorConfigEntry { + [configId: string]: IOperatorConfigClient; +} + +// client +export interface IItemEntry { + [itemId: string]: IConfigEntry; +} + +// client +export type IConfigEntry = { + [configId in "0" | "1" | "2" | "3" | "4" | "5"]: IItemConfig; +} & { + Favorite?: boolean; + IsNew?: boolean; + // Railjack + ItemName?: string; + RailjackImage?: IFlavourItem; + Customization?: ICrewShipCustomization; + Weapon?: ICrewShipWeaponClient; + CrewMembers?: ICrewShipMembersClient; +}; + +export type ILoadoutClient = Omit; + +export interface ILoadoutDatabase { + NORMAL: ILoadoutConfigDatabase[]; + SENTINEL: ILoadoutConfigDatabase[]; + ARCHWING: ILoadoutConfigDatabase[]; + NORMAL_PVP: ILoadoutConfigDatabase[]; + LUNARO: ILoadoutConfigDatabase[]; + OPERATOR: ILoadoutConfigDatabase[]; + GEAR?: ILoadoutConfigDatabase[]; + KDRIVE: ILoadoutConfigDatabase[]; + DATAKNIFE: ILoadoutConfigDatabase[]; + MECH: ILoadoutConfigDatabase[]; + OPERATOR_ADULT: ILoadoutConfigDatabase[]; + DRIFTER: ILoadoutConfigDatabase[]; + _id: Types.ObjectId; + loadoutOwnerId: Types.ObjectId; +} + +export interface ILoadOutPresets { + NORMAL: ILoadoutConfigClient[]; + NORMAL_PVP: ILoadoutConfigClient[]; + LUNARO: ILoadoutConfigClient[]; + ARCHWING: ILoadoutConfigClient[]; + SENTINEL: ILoadoutConfigClient[]; + OPERATOR: ILoadoutConfigClient[]; + GEAR?: ILoadoutConfigClient[]; + KDRIVE: ILoadoutConfigClient[]; + DATAKNIFE: ILoadoutConfigClient[]; + MECH: ILoadoutConfigClient[]; + OPERATOR_ADULT: ILoadoutConfigClient[]; + DRIFTER: ILoadoutConfigClient[]; +} + +export interface ILoadoutEntry { + [key: string]: ILoadoutConfigClient; +} + +export enum FocusSchool { + Attack = "AP_ATTACK", + Defense = "AP_DEFENSE", + Power = "AP_POWER", + Tactic = "AP_TACTIC", + Ward = "AP_WARD" +} + +export interface ILoadoutConfigClient { + FocusSchool?: FocusSchool; + PresetIcon?: string; + Favorite?: boolean; + n?: string; // Loadout name + s?: IEquipmentSelectionClient; // Suit + p?: IEquipmentSelectionClient; // Secondary weapon + l?: IEquipmentSelectionClient; // Primary weapon + m?: IEquipmentSelectionClient; // Melee weapon + h?: IEquipmentSelectionClient; // Gravimag weapon + a?: IEquipmentSelectionClient; // Necromech exalted weapon + ItemId: IOid; + Remove?: boolean; // when client wants to remove a config, it only includes ItemId & Remove. +} + +export interface ILoadoutConfigDatabase + extends Omit { + _id: Types.ObjectId; + s?: IEquipmentSelectionDatabase; + p?: IEquipmentSelectionDatabase; + l?: IEquipmentSelectionDatabase; + m?: IEquipmentSelectionDatabase; + h?: IEquipmentSelectionDatabase; + a?: IEquipmentSelectionDatabase; +} diff --git a/src/types/session.ts b/src/types/session.ts new file mode 100644 index 00000000..056cc150 --- /dev/null +++ b/src/types/session.ts @@ -0,0 +1,42 @@ +import type { Types } from "mongoose"; + +export interface ISession { + 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 { + id?: string; + originalSessionId?: string; + buildId?: number; + gameModeId?: number; + regionId?: number; + maxEloDifference?: number; + eloRating?: number; + enforceElo?: boolean; + xplatform?: boolean; + queryId?: number; +} diff --git a/src/types/shipTypes.ts b/src/types/shipTypes.ts new file mode 100644 index 00000000..6cda3b93 --- /dev/null +++ b/src/types/shipTypes.ts @@ -0,0 +1,11 @@ +import type { Types } from "mongoose"; +import type { IColor, IShipAttachments } from "./inventoryTypes/commonInventoryTypes.ts"; + +export interface IShipDatabase { + ItemType: string; + ShipOwnerId: Types.ObjectId; + ShipExteriorColors?: IColor; + AirSupportPower: string; + ShipAttachments?: IShipAttachments; + SkinFlavourItem?: string; +} diff --git a/src/types/statTypes.ts b/src/types/statTypes.ts new file mode 100644 index 00000000..52bf45e0 --- /dev/null +++ b/src/types/statTypes.ts @@ -0,0 +1,191 @@ +import type { Types } from "mongoose"; + +export interface IStatsClient { + CiphersSolved?: number; + CiphersFailed?: number; + CipherTime?: number; + Weapons?: IWeapon[]; + Enemies?: IEnemy[]; + MeleeKills?: number; + MissionsCompleted?: number; + MissionsQuit?: number; + MissionsFailed?: number; + MissionsInterrupted?: number; + MissionsDumped?: number; + TimePlayedSec?: number; + PickupCount?: number; + Tutorial?: Map; + Abilities?: IAbility[]; + Rating?: number; + Income?: number; + Rank?: number; + PlayerLevel?: number; + Scans?: IScan[]; + Missions?: IMission[]; + Deaths?: number; + HealCount?: number; + ReviveCount?: number; + Races?: Map; + ZephyrScore?: number; + SentinelGameScore?: number; + CaliberChicksScore?: number; + OlliesCrashCourseScore?: number; + DojoObstacleScore?: number; + + // event scores + Halloween16?: number; + AmalgamEventScoreMax?: number; + Halloween19ScoreMax?: number; + FlotillaEventScore?: number; + FlotillaSpaceBadgesTier1?: number; + FlotillaSpaceBadgesTier2?: number; + FlotillaSpaceBadgesTier3?: number; + FlotillaGroundBadgesTier1?: number; + FlotillaGroundBadgesTier2?: number; + FlotillaGroundBadgesTier3?: number; + MechSurvivalScoreMax?: number; + + // not in schema + PVP?: { + suitDeaths?: number; + suitKills?: number; + weaponKills?: number; + type: string; + }[]; +} + +export interface IStatsDatabase extends IStatsClient { + accountOwnerId: Types.ObjectId; +} + +export interface IAbility { + type: string; + used: number; +} + +export interface IEnemy { + type: string; + executions?: number; + headshots?: number; + kills?: number; + assists?: number; + deaths?: number; + captures?: number; +} + +export interface IMission { + type: string; + highScore: number; +} + +export interface IScan { + type: string; + scans: number; +} + +export interface ITutorial { + stage: number; +} + +export interface IWeapon { + type: string; + equipTime?: number; + hits?: number; + kills?: number; + xp?: number; + assists?: number; + headshots?: number; + fired?: number; +} + +export interface IRace { + highScore: number; +} + +export interface IStatsUpdate { + displayName: string; + guildId?: string; + PS?: string; + add?: IStatsAdd; + set?: IStatsSet; + max?: IStatsMax; + timers?: IStatsTimers; +} + +export interface IStatsAdd { + GEAR_USED?: IUploadEntry; + SCAN?: IUploadEntry; + MISSION_COMPLETE?: IUploadEntry; + HEADSHOT_ITEM?: IUploadEntry; + HEADSHOT?: IUploadEntry; + PLAYER_COUNT?: IUploadEntry; + HOST_MIGRATION?: IUploadEntry; + PICKUP_ITEM?: IUploadEntry; + FIRE_WEAPON?: IUploadEntry; + HIT_ENTITY_ITEM?: IUploadEntry; + DESTROY_DECORATION?: IUploadEntry; + KILL_ENEMY?: IUploadEntry; + TAKE_DAMAGE?: IUploadEntry; + SQUAD_KILL_ENEMY?: IUploadEntry; + RECEIVE_UPGRADE?: IUploadEntry; + USE_ABILITY?: IUploadEntry; + SQUAD_VIP_KILL?: IUploadEntry; + HEAL_BUDDY?: IUploadEntry; + INCOME?: number; + CIPHER?: IUploadEntry; + EQUIP_COSMETIC?: IUploadEntry; + EQUIP_UPGRADE?: IUploadEntry; + KILL_BOSS?: IUploadEntry; + MISSION_TYPE?: IUploadEntry; + MISSION_FACTION?: IUploadEntry; + MISSION_PLAYED?: IUploadEntry; + MISSION_PLAYED_TIME?: IUploadEntry; + MEDALS_TOP?: IUploadEntry; + INPUT_ACTIVITY_TIME?: IUploadEntry; + KILL_ENEMY_ITEM?: IUploadEntry; + TAKE_DAMAGE_ITEM?: IUploadEntry; + SQUAD_KILL_ENEMY_ITEM?: IUploadEntry; + MELEE_KILL?: IUploadEntry; + SQUAD_MELEE_KILL?: IUploadEntry; + MELEE_KILL_ITEM?: IUploadEntry; + SQUAD_MELEE_KILL_ITEM?: IUploadEntry; + DIE?: IUploadEntry; + DIE_ITEM?: IUploadEntry; + EXECUTE_ENEMY?: IUploadEntry; + EXECUTE_ENEMY_ITEM?: IUploadEntry; + KILL_ASSIST?: IUploadEntry; + KILL_ASSIST_ITEM?: IUploadEntry; + CAPTURE_ENEMY?: IUploadEntry; +} + +export interface IUploadEntry { + [key: string]: number; +} + +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 { + ELO_RATING?: number; + RANK?: number; + PLAYER_LEVEL?: number; +} + +export interface IStatsTimers { + IN_SHIP_TIME?: number; + IN_SHIP_VIEW_TIME?: IUploadEntry; + EQUIP_WEAPON?: IUploadEntry; + MISSION_TIME?: IUploadEntry; + REGION_TIME?: IUploadEntry; + PLATFORM_TIME?: IUploadEntry; + CURRENT_MISSION_TIME?: number; + CIPHER_TIME?: number; +} diff --git a/src/types/vendorTypes.ts b/src/types/vendorTypes.ts new file mode 100644 index 00000000..bb31dd75 --- /dev/null +++ b/src/types/vendorTypes.ts @@ -0,0 +1,43 @@ +import type { IMongoDate, IOid } from "./commonTypes.ts"; + +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..f39c4bf7 --- /dev/null +++ b/src/types/worldStateTypes.ts @@ -0,0 +1,425 @@ +import type { IMissionReward } from "warframe-public-export-plus"; +import type { IMongoDate, IOid } from "./commonTypes.ts"; + +export interface IWorldState { + Version: number; // for goals + BuildLabel: string; + Time: number; + InGameMarket: IInGameMarket; + Goals: IGoal[]; + Alerts: []; + Sorties: ISortie[]; + LiteSorties: ILiteSortie[]; + SyndicateMissions: ISyndicateMissionInfo[]; + ActiveMissions: IFissure[]; + FlashSales: IFlashSale[]; + GlobalUpgrades: IGlobalUpgrade[]; + Invasions: IInvasion[]; + NodeOverrides: INodeOverride[]; + VoidTraders: IVoidTrader[]; + PrimeVaultTraders: IPrimeVaultTrader[]; + VoidStorms: IVoidStorm[]; + DailyDeals: IDailyDeal[]; + 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; + HealthPct?: number; + + Icon?: string; + Desc: string; + ToolTip?: string; + Faction?: string; + + Goal?: number; + InterimGoals?: number[]; + BonusGoal?: number; + ClanGoal?: number[]; + + Success?: number; + Personal?: boolean; + Community?: boolean; + Best?: boolean; // Use Best instead of Count to check for reward + Bounty?: boolean; // Tactical Alert + ClampNodeScores?: boolean; + + Transmission?: string; + InstructionalItem?: string; + ItemType?: string; + + Tag: string; + PrereqGoalTags?: string[]; + + Node?: string; + VictimNode?: string; + + ConcurrentMissionKeyNames?: string[]; + ConcurrentNodeReqs?: number[]; + ConcurrentNodes?: string[]; + RegionIdx?: number; + Regions?: number[]; + MissionKeyName?: string; + + Reward?: IMissionReward; + InterimRewards?: IMissionReward[]; + BonusReward?: IMissionReward; + + JobAffiliationTag?: string; + Jobs?: ISyndicateJob[]; + PreviousJobs?: ISyndicateJob[]; + JobCurrentVersion?: IOid; + JobPreviousVersion?: IOid; + + ScoreVar?: string; + ScoreMaxTag?: string; // Field in leaderboard + ScoreLocTag?: string; + + MissionKeyRotation?: string[]; + MissionKeyRotationInterval?: number; + + OptionalInMission?: boolean; + UpgradeIds?: IOid[]; + + NightLevel?: string; +} + +export interface ISyndicateJob { + jobType?: string; + rewards: string; + masteryReq?: number; + minEnemyLevel: number; + maxEnemyLevel: number; + xpAmounts: number[]; + endless?: boolean; + locationTag?: string; + isVault?: boolean; + requiredItems?: string[]; + useRequiredItemsAsMiscItemFee?: boolean; +} + +export interface ISyndicateMissionInfo { + _id: IOid; + Activation: IMongoDate; + Expiry: IMongoDate; + Tag: string; + Seed: number; + Nodes: string[]; + Jobs?: ISyndicateJob[]; +} + +export interface IGlobalUpgrade { + _id: IOid; + Activation: IMongoDate; + ExpiryDate: IMongoDate; + UpgradeType: string; + OperationType: string; + Value: number; + LocalizeTag?: string; + LocalizeDescTag?: string; + Nodes?: string[]; +} + +export interface IInvasion { + _id: IOid; + Faction: string; + DefenderFaction: string; + Node: string; + Count: number; + Goal: number; + LocTag: string; + Completed: boolean; + ChainID: IOid; + AttackerReward: IMissionReward; + AttackerMissionInfo: IInvasionMissionInfo; + DefenderReward: IMissionReward; + DefenderMissionInfo: IInvasionMissionInfo; + Activation: IMongoDate; +} + +export interface IInvasionMissionInfo { + seed: number; + faction: string; +} + +export interface IFissure { + _id: IOid; + Region: number; + Seed: number; + Activation: IMongoDate; + Expiry: IMongoDate; + Node: string; + MissionType: string; + Modifier: string; + Hard?: boolean; +} + +export interface IFissureDatabase { + Activation: Date; + Expiry: Date; + Node: string; + Modifier: "VoidT1" | "VoidT2" | "VoidT3" | "VoidT4" | "VoidT5" | "VoidT6"; + 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 IVoidTrader { + _id: IOid; + Activation: IMongoDate; + Expiry: IMongoDate; + Character: string; + Node: string; + Manifest: IVoidTraderOffer[]; +} + +export interface IVoidTraderOffer { + ItemType: string; + PrimePrice: number; + RegularPrice: number; + Limit?: number; +} + +export interface IVoidStorm { + _id: IOid; + Node: string; + Activation: IMongoDate; + Expiry: IMongoDate; + ActiveMissionTier: string; +} + +export interface IPrimeVaultTrader { + _id: IOid; + Activation: IMongoDate; + Expiry: IMongoDate; + InitialStartDate?: IMongoDate; + Node: string; + Manifest: IPrimeVaultTraderOffer[]; + EvergreenManifest: IPrimeVaultTraderOffer[]; + ScheduleInfo: IScheduleInfo[]; +} + +export interface IPrimeVaultTraderOffer { + ItemType: string; + PrimePrice?: number; + RegularPrice?: number; + StartDate?: IMongoDate; + EndDate?: IMongoDate; +} + +export interface IScheduleInfo { + Expiry: IMongoDate; + PreviewHiddenUntil?: IMongoDate; + FeaturedItem?: string; +} + +export interface IDailyDeal { + StoreItem: string; + Activation: IMongoDate; + Expiry: IMongoDate; + Discount: number; + OriginalPrice: number; + SalePrice: number; + AmountTotal: number; + AmountSold: number; +} + +export interface IDailyDealDatabase { + StoreItem: string; + Activation: Date; + Expiry: Date; + Discount: number; + OriginalPrice: number; + SalePrice: number; + AmountTotal: number; + AmountSold: number; +} + +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; + Permanent?: boolean; // only for getPastWeeklyChallenges response + 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; +} + +export type TCircuitGameMode = + | "Survival" + | "VoidFlood" + | "Excavation" + | "Defense" + | "Exterminate" + | "Assassination" + | "Alchemy"; + +export interface IFlashSale { + TypeName: string; + ShowInMarket: boolean; + HideFromMarket: boolean; + SupporterPack: boolean; + Discount: number; + BogoBuy: number; + BogoGet: number; + PremiumOverride: number; + RegularOverride: number; + ProductExpiryOverride?: IMongoDate; + StartDate: IMongoDate; + EndDate: IMongoDate; +} + +export interface IInGameMarket { + LandingPage: ILandingPage; +} + +export interface ILandingPage { + Categories: IGameMarketCategory[]; +} + +export interface IGameMarketCategory { + CategoryName: string; + Name: string; + Icon: string; + AddToMenu?: boolean; + Items?: string[]; +} + +export interface ITmp { + cavabegin: string; + PurchasePlatformLockEnabled: boolean; // Seems unused + pgr: IPgr; + ennnd?: boolean; // True if 1999 demo is available (no effect for >=38.6.0) + mbrt?: boolean; // Related to mobile app rating request + fbst: IFbst; + sfn: number; + edg?: TCircuitGameMode[]; // The Circuit game modes overwrite +} + +interface IPgr { + ts: string; + en: string; + fr: string; + it: string; + de: string; + es: string; + pt: string; + ru: string; + pl: string; + uk: string; + tr: string; + ja: string; + zh: string; + ko: string; + tc: string; + th: string; +} + +interface IFbst { + a: number; + e: number; + n: number; +} 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 new file mode 100644 index 00000000..64d95ce3 --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,121 @@ +import type { Logger, LeveledLogMethod } from "winston"; +import { createLogger, format, transports, addColors } from "winston"; +import "winston-daily-rotate-file"; +import { config } from "../services/configService.ts"; +import * as util from "util"; +import { isEmptyObject } from "../helpers/general.ts"; + +// const combineMessageAndSplat = () => { +// return { +// transform: (info: any, _opts: any) => { +// //combine message and args if any +// info.message = util.format(info.message, ...(info[Symbol.for("splat")] || [])); +// return info; +// } +// }; +// }; + +// const alwaysAddMetadata = () => { +// return { +// transform(info: any) { +// if (info[Symbol.for("splat")] === undefined) return info; +// info.meta = info[Symbol.for("splat")]; //[0].meta; +// return info; +// } +// }; +// }; + +//TODO: in production utils.inspect might be slowing down requests see utils.inspect +const consolelogFormat = format.printf(info => { + if (!isEmptyObject(info.metadata)) { + const metadataString = util.inspect(info.metadata, { + showHidden: false, + depth: null, + colors: true + }); + + return `${info.timestamp as string} [${info.version as string}] ${info.level}: ${info.message as string} ${metadataString}`; + } + return `${info.timestamp as string} [${info.version as string}] ${info.level}: ${info.message as string}`; +}); + +const fileFormat = format.combine( + format.uncolorize(), + //combineMessageAndSplat(), + format.timestamp(), + format.json() +); + +const errorLog = new transports.DailyRotateFile({ + filename: "logs/error.log", + format: fileFormat, + level: "error", + datePattern: "YYYY-MM-DD" +}); +const combinedLog = new transports.DailyRotateFile({ + filename: "logs/combined.log", + format: fileFormat, + datePattern: "YYYY-MM-DD" +}); + +const consoleLog = new transports.Console({ + forceConsole: false, + format: format.combine( + format.colorize(), + format.timestamp({ format: "YYYY-MM-DDTHH:mm:ss:SSS" }), // uses local timezone + //combineMessageAndSplat(), + //alwaysAddMetadata(), + format.errors({ stack: true }), + format.align(), + format.metadata({ fillExcept: ["message", "level", "timestamp", "version"] }), + consolelogFormat + ) +}); + +const transportOptions = config.logger.files ? [consoleLog, errorLog, combinedLog] : [consoleLog]; + +//possible log levels: { fatal: 0, error: 1, warn: 2, info: 3, http: 4, debug: 5, trace: 6 }, +const logLevels = { + levels: { + fatal: 0, + error: 1, + warn: 2, + info: 3, + http: 4, + debug: 5, + trace: 6 + }, + colors: { + fatal: "red", + error: "red", + warn: "yellow", + info: "green", + http: "green", + debug: "magenta", + trace: "cyan" + } +}; + +export const logger = createLogger({ + levels: logLevels.levels, + level: config.logger.level, + defaultMeta: { version: process.env.npm_package_version }, + transports: transportOptions +}) as Logger & Record; + +addColors(logLevels.colors); + +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}`)); + +export const logError = (err: Error, context: string): void => { + if (err.stack) { + const stackArr = err.stack.split("\n"); + stackArr[0] += ` while ${context}`; + logger.error(stackArr.join("\n")); + } else { + logger.error(`uncaught error while ${context}: ${err.message}`); + } +}; diff --git a/src/utils/ts-utils.ts b/src/utils/ts-utils.ts new file mode 100644 index 00000000..93b43fd4 --- /dev/null +++ b/src/utils/ts-utils.ts @@ -0,0 +1,7 @@ +type Entries = (K extends unknown ? [K, T[K]] : never)[]; + +export function getEntriesUnsafe(object: T): Entries { + return Object.entries(object) as Entries; +} + +export const exhaustive = (_: never): void => {}; diff --git a/static/certs/cert.pem b/static/certs/cert.pem new file mode 100644 index 00000000..4bce415b --- /dev/null +++ b/static/certs/cert.pem @@ -0,0 +1,71 @@ +-----BEGIN CERTIFICATE----- +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 +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 +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTgx +MTAyMDAwMDAwWhcNMzAxMjMxMjM1OTU5WjCBjzELMAkGA1UEBhMCR0IxGzAZBgNV +BAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEYMBYGA1UE +ChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQDEy5TZWN0aWdvIFJTQSBEb21haW4g +VmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA1nMz1tc8INAA0hdFuNY+B6I/x0HuMjDJsGz99J/LEpgPLT+N +TQEMgg8Xf2Iu6bhIefsWg06t1zIlk7cHv7lQP6lMw0Aq6Tn/2YHKHxYyQdqAJrkj +eocgHuP/IJo8lURvh3UGkEC0MpMWCRAIIz7S3YcPb11RFGoKacVPAXJpz9OTTG0E +oKMbgn6xmrntxZ7FN3ifmgg0+1YuWMQJDgZkW7w33PGfKGioVrCSo1yfu4iYCBsk +Haswha6vsC6eep3BwEIc4gLw6uBK0u+QDrTBQBbwb4VCSmT3pDCg/r8uoydajotY +uK3DGReEY+1vVv2Dy2A0xHS+5p3b4eTlygxfFQIDAQABo4IBbjCCAWowHwYDVR0j +BBgwFoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFI2MXsRUrYrhd+mb ++ZsF4bgBjWHhMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0G +A1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNVHSAEFDASMAYGBFUdIAAw +CAYGZ4EMAQIBMFAGA1UdHwRJMEcwRaBDoEGGP2h0dHA6Ly9jcmwudXNlcnRydXN0 +LmNvbS9VU0VSVHJ1c3RSU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDB2Bggr +BgEFBQcBAQRqMGgwPwYIKwYBBQUHMAKGM2h0dHA6Ly9jcnQudXNlcnRydXN0LmNv +bS9VU0VSVHJ1c3RSU0FBZGRUcnVzdENBLmNydDAlBggrBgEFBQcwAYYZaHR0cDov +L29jc3AudXNlcnRydXN0LmNvbTANBgkqhkiG9w0BAQwFAAOCAgEAMr9hvQ5Iw0/H +ukdN+Jx4GQHcEx2Ab/zDcLRSmjEzmldS+zGea6TvVKqJjUAXaPgREHzSyrHxVYbH +7rM2kYb2OVG/Rr8PoLq0935JxCo2F57kaDl6r5ROVm+yezu/Coa9zcV3HAO4OLGi +H19+24rcRki2aArPsrW04jTkZ6k4Zgle0rj8nSg6F0AnwnJOKf0hPHzPE/uWLMUx +RP0T7dWbqWlod3zu4f+k+TY4CFM5ooQ0nBnzvg6s1SQ36yOoeNDT5++SR2RiOSLv +xvcRviKFxmZEJCaOEDKNyJOuB56DPi/Z+fVGjmO+wea03KbNIaiGCpXZLoUmGv38 +sbZXQm2V0TP2ORQGgkE49Y9Y3IBbpNV9lXj9p5v//cWoaasm56ekBYdbqbe4oyAL +l6lFhd2zi+WJN44pDfwGF/Y4QA5C5BIG+3vzxhFoYt/jmPQT2BVPi7Fp2RBgvGQq +6jG35LWjOhSbJuMLe/0CjraZwTiXWTb2qHSihrZe68Zk6s+go/lunrotEbaGmAhY +LcmsJWTyXnW0OMGuf1pGg+pRyrbxmRE1a6Vqe8YAsOf4vmSyrcjC8azjUeqkk+B5 +yOGBQMkKW+ESPMFgKuOXwIlCypTPRpgSabuY0MLTDXJLR27lk8QyKGOHQ+SwMj4K +00u/I5sUKUErmgQfky3xxzlIPK1aEn8= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/static/certs/key.pem b/static/certs/key.pem new file mode 100644 index 00000000..6135769a --- /dev/null +++ b/static/certs/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +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/data/.gitkeep b/static/data/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/static/fixed_responses/aggregateSessions.json b/static/fixed_responses/aggregateSessions.json new file mode 100644 index 00000000..1d1e7e52 --- /dev/null +++ b/static/fixed_responses/aggregateSessions.json @@ -0,0 +1,3 @@ +{ + "Results": [] +} diff --git a/static/fixed_responses/allDecoRecipes.json b/static/fixed_responses/allDecoRecipes.json new file mode 100644 index 00000000..471f7ae3 --- /dev/null +++ b/static/fixed_responses/allDecoRecipes.json @@ -0,0 +1,104 @@ +[ + "/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/DuviriMurmurEventBronzeTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DuviriMurmurEventClayTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DuviriMurmurEventGoldTrophyRecipe", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/DuviriMurmurEventSilverTrophyRecipe", + "/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", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/OstronFemaleSitting", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/OstronFemaleStanding", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/OstronMaleStanding", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/OstronMaleStandingTwo", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/SolarisForeman", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/SolarisHazard", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/SolarisStrikerOne", + "/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/SolarisStrikerThree" +] diff --git a/static/fixed_responses/allDialogue.json b/static/fixed_responses/allDialogue.json new file mode 100644 index 00000000..4b47303e --- /dev/null +++ b/static/fixed_responses/allDialogue.json @@ -0,0 +1,140 @@ +[ + "SolarisUnitedHub1", + "/Lotus/Language/SolarisVenus/FishmongerName", + "/Lotus/Language/SolarisVenus/ProspectorName", + "/Lotus/Language/SolarisVenus/StockbrokerName", + "/Lotus/Language/SolarisVenus/WeaponsmithShopName", + "/Lotus/Language/SolarisVenus/LegsShopName", + "/Lotus/Language/Actions/KDriveVendor", + "/Lotus/Language/SolarisVenus/SolarisUnitedContactName", + "SaturnWolf1", + "SaturnWolf2", + "SaturnWolf3", + "SaturnWolf4", + "SaturnWolf5", + "ConclaveSyndicate", + "ArbitersSyndicate", + "LibrarySyndicate", + "RedVeilSyndicate", + "PerrinSyndicate", + "CephalonSudaSyndicate", + "NewLokaSyndicate", + "SteelMeridianSyndicate", + "CetusSyndicate", + "QuillsSyndicate", + "VentKidsSyndicate", + "SolarisSyndicate", + "VoxSyndicate", + "RadioLegionSyndicate", + "EventSyndicate", + "TheEmissary1", + "TheEmissary2", + "TheEmissary3", + "TheEmissary4", + "LeverianIntro", + "TheEmissary5", + "RailjackMultiToolIntro", + "EnterRailjackOnceOnly", + "ScenarioEventHub5", + "Glassmaker1", + "/Lotus/Types/Items/Glassmaker/CetusBurnedNoteCEvidence", + "/Lotus/Types/Items/Glassmaker/CetusWeaponBEvidence", + "/Lotus/Types/Items/Glassmaker/CetusDatapadOneBEvidence", + "/Lotus/Types/Items/Glassmaker/CetusEyeBEvidence", + "/Lotus/Types/Items/Glassmaker/CetusDatapadTwoAEvidence", + "/Lotus/Types/Items/Glassmaker/WeaveEvidencePartOne", + "Glassmaker2", + "/Lotus/Types/Items/Glassmaker/CorpusJournalBEvidence", + "/Lotus/Types/Items/Glassmaker/CorpusCatalogRobotsEvidence", + "/Lotus/Types/Items/Glassmaker/CorpusDatapadEvidenceC", + "/Lotus/Types/Items/Glassmaker/CorpusHelmetEngineerEvidence", + "/Lotus/Types/Items/Glassmaker/CorpusBadgeMarsHiddenEvidence", + "/Lotus/Types/Items/Glassmaker/WeaveEvidencePartTwo", + "Glassmaker3", + "/Lotus/Types/Items/Glassmaker/GrineerTechCEvidence", + "/Lotus/Types/Items/Glassmaker/GrineerWeaponBEvidence", + "/Lotus/Types/Items/Glassmaker/GrineerPlushAEvidence", + "/Lotus/Types/Items/Glassmaker/GrineerMessageBEvidence", + "/Lotus/Types/Items/Glassmaker/GrineerDatapadBEvidence", + "/Lotus/Types/Items/Glassmaker/WeaveEvidencePartThree", + "TL20Start", + "TL20End", + "EudicoHeists", + "Glassmaker4", + "/Lotus/Types/Items/Glassmaker/OrokinManifestoBEvidence", + "/Lotus/Types/Items/Glassmaker/OrokinDatapadBEvidence", + "/Lotus/Types/Items/Glassmaker/OrokinNihilPlanCEvidence", + "/Lotus/Types/Items/Glassmaker/OrokinAppraisalBEvidence", + "/Lotus/Types/Items/Glassmaker/OrokinDossierAEvidence", + "/Lotus/Types/Items/Glassmaker/WeaveEvidencePartFour", + "/Lotus/Language/SolarisVenus/LegsShopNameLoved", + "/Lotus/Language/SolarisVenus/EudicoLoved", + "/Lotus/Language/SolarisVenus/ProspectorNameLoved", + "/Lotus/Language/SolarisVenus/StockbrokerNameLoved", + "/Lotus/Language/SolarisVenus/WeaponsmithShopNameLoved", + "/Lotus/Language/SolarisVenus/FishmongerNameLoved", + "DeimosHub", + "EntratiSyndicate", + "/Lotus/Language/InfestedMicroplanet/HivemindTokenVendorName", + "/Lotus/Language/InfestedMicroplanet/HivemindProspector", + "/Lotus/Language/InfestedMicroplanet/HivemindGunsmithName", + "/Lotus/Language/InfestedMicroplanet/HivemindPetVendor", + "ModularCrafting45", + "ModularCrafting55", + "NecraloidSyndicate", + "/Lotus/Language/InfestedMicroplanet/HivemindMechsName", + "/Lotus/Language/Npcs/EntratiMother", + "/Lotus/Language/InfestedMicroplanet/HivemindFishmonger", + "/Lotus/Language/SolarisVenus/SolarisUnitedAgentLoved", + "Glassmaker5", + "/Lotus/Types/Items/Glassmaker/InfestedOroShardsAEvidence", + "/Lotus/Types/Items/Glassmaker/InfestedOroWeaponCEvidence", + "/Lotus/Types/Items/Glassmaker/InfestedOroProbeBEvidence", + "/Lotus/Types/Items/Glassmaker/InfestedOroShackleBEvidence", + "/Lotus/Types/Items/Glassmaker/InfestedOroTabletBEvidence", + "/Lotus/Types/Items/Glassmaker/WeaveEvidencePartFive", + "GlassmakerBossFight", + "/Lotus/Language/InfestedMicroplanet/HivemindGunsmithNameLoved", + "/Lotus/Language/InfestedMicroplanet/HivemindTokenVendorNameLoved", + "/Lotus/Language/Npcs/EntratiMotherLoved", + "/Lotus/Language/InfestedMicroplanet/HivemindPetVendorLoved", + "/Lotus/Language/InfestedMicroplanet/HivemindFishmongerLoved", + "/Lotus/Language/InfestedMicroplanet/HivemindProspectorLoved", + "/Lotus/Language/InfestedMicroplanet/HivemindMechsNameLoved", + "/Lotus/Language/InfestedMicroplanet/HivemindPetVendorName", + "/Lotus/Language/InfestedMicroplanet/HivemindPetVendorNameLoved", + "/Lotus/Language/InfestedMicroplanet/HivemindFishmongerName", + "/Lotus/Language/InfestedMicroplanet/HivemindFishmongerNameLoved", + "/Lotus/Language/InfestedMicroplanet/HivemindProspectorName", + "/Lotus/Language/InfestedMicroplanet/HivemindProspectorNameLoved", + "DebtTokenVendorCrewMembers_FirstVisit", + "ZarimanSyndicate", + "/Lotus/Language/Npcs/Kahl", + "RankZeroConversationOne", + "RankZeroConversationTwo", + "EntratiLabSyndicate", + "/Lotus/Language/EntratiLab/EntratiGeneral/HumanLoid", + "/Lotus/Language/EntratiLab/EntratiGeneral/Fibonacci", + "/Lotus/Language/EntratiLab/EntratiGeneral/BirdThree", + "/Lotus/Language/Zariman/Quinn", + "/Lotus/Language/EntratiLab/EntratiGeneral/TagferFirstRank1", + "VoidVaultIntro", + "PurchasePlatformLockedNotificationSeen", + "/Lotus/Language/Zariman/Yonta", + "ZarimanSyndicateFirstOpen", + "HivemindTokenVendorBarter_FirstVisit", + "OtakLastWishManifest_FirstVisit", + "/Lotus/Language/Zariman/Cavalero", + "/Lotus/Language/Duviri/Acrithis", + "/Lotus/Language/Npcs/PrimeVaultTrader", + "/Lotus/Language/Zariman/Hombask", + "EntratiLabDisruptionManifest_FirstVisit", + "/Lotus/Language/EntratiLab/EntratiGeneral/BirdThreeLoved", + "/Lotus/Language/EntratiLab/EntratiGeneral/TagferFirstRank5", + "DanteLeverian", + "/Lotus/Language/EntratiLab/EntratiGeneral/HumanLoidLoved", + "ConquestSetupIntro", + "EntratiLabConquestHardModeUnlocked", + "/Lotus/Language/Npcs/KonzuPostNewWar", + "/Lotus/Language/SolarisVenus/EudicoPostNewWar" +] diff --git a/static/fixed_responses/allIncarnonList.json b/static/fixed_responses/allIncarnonList.json new file mode 100644 index 00000000..a27798e1 --- /dev/null +++ b/static/fixed_responses/allIncarnonList.json @@ -0,0 +1,50 @@ +[ + "/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", + "/Lotus/Weapons/Tenno/Zariman/Melee/HeavyScythe/ZarimanHeavyScythe/ZarimanHeavyScytheWeapon" +] diff --git a/static/fixed_responses/allScans.json b/static/fixed_responses/allScans.json new file mode 100644 index 00000000..f4b43062 --- /dev/null +++ b/static/fixed_responses/allScans.json @@ -0,0 +1,1101 @@ +[ + "/Lotus/Types/Lore/LoreFragmentScanDeco", + "/Lotus/Types/Lore/EidolonFragmentScanDeco", + "/Lotus/Types/NeutralCreatures/Kubrow/KubrowDen", + "/Lotus/Types/Items/Plants/NightCommonPlant", + "/Lotus/Types/Items/Plants/NightUnCommonPlant", + "/Lotus/Types/PickUps/MediumLootCrateGrnA", + "/Lotus/Types/Items/Plants/DayCommonPlant", + "/Lotus/Types/Items/Plants/GftPlantRuksClawMaturePlant", + "/Lotus/Objects/Orokin/Props/CollectibleSeriesOne", + "/Lotus/Objects/GrnExplodingBarrel", + "/Lotus/Types/Lore/FighterFragmentScanDeco", + "/Lotus/Objects/Gameplay/OroFusexGDeco", + "/Lotus/Types/Lore/SongFragmentScanDeco", + "/Lotus/Characters/Sentient/Hunhow/HunhowPieces/HunhowHipsWreckageQuest", + "/Lotus/Powersuits/Dragon/DragonPeltAvatar", + "/Lotus/Types/Items/Plants/ZenPitcherPlant", + "/Lotus/Types/NeutralCreatures/Catbrow/CatbrowAvatar", + "/Lotus/Powersuits/Khora/Kavat/KhoraKavatAvatar", + "/Lotus/Objects/Grineer/Props/Computers/GrnPanelADeco", + "/Lotus/Types/Lore/QuestSpawnedFragments/GlassFragmentScanDecoA", + "/Lotus/Types/Lore/QuestSpawnedFragments/GlassFragmentScanDecoB", + "/Lotus/Types/Lore/QuestSpawnedFragments/GlassFragmentScanDecoC", + "/Lotus/Types/Lore/QuestSpawnedFragments/GlassFragmentScanDecoD", + "/Lotus/Types/Lore/QuestSpawnedFragments/GlassFragmentScanDecoE", + "/Lotus/Objects/Tenno/Props/TitaniaCodexEntryADeco", + "/Lotus/Types/Items/Plants/ZenCobraLotusPlant", + "/Lotus/Types/Items/Plants/DayUnCommonPlant", + "/Lotus/Objects/Tenno/Props/TitaniaCodexEntryBDeco", + "/Lotus/Types/Items/Plants/NightRarePlant", + "/Lotus/Types/PickUps/MediumLootCrateGrnB", + "/Lotus/Objects/Tenno/Props/TitaniaCodexEntryCDeco", + "/Lotus/Types/Items/Plants/DayRarePlant", + "/Lotus/Types/Items/Plants/MossGroundCoverAPlant", + "/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", + "/Lotus/Types/Lore/QuestSpawnedFragments/UmbraScorchScanDecoA", + "/Lotus/Types/Lore/QuestSpawnedFragments/UmbraScorchScanDecoB", + "/Lotus/Types/Lore/QuestSpawnedFragments/UmbraScorchScanDecoC", + "/Lotus/Types/Lore/QuestSpawnedFragments/UmbraSwordScanDeco", + "/Lotus/Types/Gameplay/Eidolon/Resources/FruitTree", + "/Lotus/Types/Gameplay/Eidolon/Resources/FruitTreeB", + "/Lotus/Types/Gameplay/Eidolon/Resources/IraditeContainer", + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/CommonFemaleBirdOfPreyAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/CommonMaleBirdOfPreyAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/UncommonMaleBirdOfPreyAvatar", + "/Lotus/Types/Gameplay/Grineer/BrokenLight", + "/Lotus/Types/PickUps/ResourceContainers/FerriteContainer", + "/Lotus/Types/PickUps/ResourceContainers/PolymerBundleContainer", + "/Lotus/Weapons/Grineer/Emplacements/GrnEmplcmntStndng/GrineerEmplacementStanding", + "/Lotus/Objects/OrokinExplodingBarrel", + "/Lotus/Types/PickUps/OrokinLootCrate", + "/Lotus/Types/PickUps/ResourceContainers/RubedoContainer", + "/Lotus/Weapons/Grineer/Emplacements/GrnDeployableCover/GrineerDeployableCover", + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/CommonFemaleForestRodentAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/CommonSnowRodentAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/CommonSnowArmadilloAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/CommonSnowBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/CommonSnowCritterAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/UncommonSnowCritterAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/CommonOrokinKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/CommonSnowPredatorAvatar", + "/Lotus/Objects/ExplodingBarrel", + "/Lotus/Objects/ExplodingBarrelFrozen", + "/Lotus/Types/PickUps/LootContainers/CorpusLootCrateUncommon", + "/Lotus/Types/PickUps/ResourceContainers/AlloyPlateContainer", + "/Lotus/Types/Friendly/Eidolon/EventMixerAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/RareSnowCritterAvatar", + "/Lotus/Types/Sentinels/SentinelAvatar", + "/Lotus/Types/Friendly/Agents/SquadLinkScannerAvatar", + "/Lotus/Types/PickUps/ResourceContainers/OrokinCellContainer", + "/Lotus/Objects/GrnSuperMegaExplodingBarrel", + "/Lotus/Types/PickUps/ResourceContainers/MorphicsContainer", + "/Lotus/Objects/Guild/Structural/CurvedGlassDeco", + "/Lotus/Types/Enemies/Corpus/Turrets/TurretAvatars/SecurityCameraAvatar", + "/Lotus/Types/PickUps/ResourceContainers/PlastidsContainer", + "/Lotus/Objects/Guild/Structural/CurvedGlassInteriorDeco", + "/Lotus/Types/LevelObjects/CorpusBreakableVent", + "/Lotus/Types/Friendly/Agents/DefenseComputerCorpusAvatar", + "/Lotus/Types/NeutralCreatures/CreatureAvatars/SandRayAvatar", + "/Lotus/Types/Gameplay/Grineer/DoorSensorDeco", + "/Lotus/Weapons/Grineer/Emplacements/GrnDeployableCover/AridGrnDeployableCover", + "/Lotus/Types/PickUps/ResourceContainers/SalvageContainer", + "/Lotus/Types/Friendly/Agents/DefenseComputerAvatar", + "/Lotus/Objects/Gameplay/SentientArtifactMode/SentientArtifactDecoB", + "/Lotus/Types/Friendly/Agents/SentientAmalgamArtifactAvatarA", + "/Lotus/Types/Friendly/Agents/SentientAmalgamArtifactAvatarB", + "/Lotus/Types/Friendly/Agents/EventForestDefenseAvatar", + "/Lotus/Types/PickUps/ResourceContainers/CircuitsContainer", + "/Lotus/Types/Friendly/Pets/KubrowPetAvatar", + "/Lotus/Objects/Orokin/Props/OroCoverPropSphereACapADeco", + "/Lotus/Types/PickUps/ResourceContainers/ArgonCrystalContainer", + "/Lotus/Types/PickUps/ResourceContainers/ControlModuleContainer", + "/Lotus/Types/Friendly/Agents/OrokinMobileDefenseAvatar", + "/Lotus/Types/Friendly/Agents/OrokinSabotageConsoleAvatar", + "/Lotus/Objects/CrpMegaExplodingBarrel", + "/Lotus/Types/PickUps/RareCorpusLootCrate", + "/Lotus/Objects/Guild/GasCityRemaster/Props/CrpGasCityDoorPowerSupplyTopDeco", + "/Lotus/Types/PickUps/ResourceContainers/NanoSporesContainer", + "/Lotus/Objects/Guild/GasCityRemaster/Props/CrpBarrelADynamicDeco", + "/Lotus/Objects/Guild/GasCityRemaster/Props/CrpExplodingBarrel", + "/Lotus/Objects/Guild/GasCityRemaster/Props/CrpExplodingBarrelGas", + "/Lotus/Types/Friendly/Agents/HiveMode/InfestedHiveAvatarC", + "/Lotus/Objects/Gameplay/SentientArtifactMode/SentientArtifactDecoA", + "/Lotus/Objects/Gameplay/SentientArtifactMode/SentientArtifactDecoC", + "/Lotus/Objects/Gameplay/SentientArtifactMode/SentientArtifactDecoD", + "/Lotus/Types/Friendly/Agents/SentientAmalgamArtifactAvatarC", + "/Lotus/Types/Friendly/Agents/SentientAmalgamArtifactAvatarD", + "/Lotus/Objects/Gameplay/OroFusexBDeco", + "/Lotus/Types/NeutralCreatures/Kubrow/KubrowAvatar", + "/Lotus/Types/PickUps/AmmoCrateDynamic", + "/Lotus/Types/Friendly/Agents/DefenseCorePipeAvatarGrineer", + "/Lotus/Objects/Guild/GasCityRemaster/Props/CrpLaserRailParkourCrossBar", + "/Lotus/Types/PickUps/MediumLootCrate", + "/Lotus/Types/Friendly/Agents/DefenseCorePipeAvatar", + "/Lotus/Types/Friendly/Eidolon/GrineerResourceTheftAvatar", + "/Lotus/Types/LevelObjects/GamemodeLockers/EidolonStorageLockerCache", + "/Lotus/Types/PickUps/DerelictOrokinLootCrate", + "/Lotus/Levels/GrineerGalleon/GrineerSpyVaultF/ConveyorBarrelExplode", + "/Lotus/Types/Friendly/Agents/ExcavatorAvatar", + "/Lotus/Types/PickUps/UltraRareCorpusLootCrate", + "/Lotus/Types/Friendly/Agents/OrokinDefenseAvatar", + "/Lotus/Types/Friendly/Agents/PayloadATVAvatar", + "/Lotus/Objects/Guild/GasCityRemaster/Props/CrpAnchorPointDeco", + "/Lotus/Objects/Guild/GasCityRemaster/Props/CrpGasCityDoorPowerSupplySide9mDeco", + "/Lotus/Types/Friendly/Pets/CatbrowPetAvatar", + "/Lotus/Types/Friendly/Agents/DefenseAvatar", + "/Lotus/Types/PickUps/RareGrineerLootCrate", + "/Lotus/Objects/Gameplay/OroFusexCDeco", + "/Lotus/Types/Friendly/Agents/DefenseComputerFortSabAvatar", + "/Lotus/Types/PickUps/MediumLootCrateGrnFortA", + "/Lotus/Weapons/Grineer/Emplacements/GrnEmplcmntStndng/GrineerFortressEmplacementStanding", + "/Lotus/Objects/Guild/GasCityRemaster/Props/CrpWelderBotMover", + "/Lotus/Objects/Grineer/Props/Computers/GrnPanelABlackDeco", + "/Lotus/Types/Gameplay/Grineer/GrineerShrapnelMine", + "/Lotus/Types/LevelObjects/GrineerBreakableVent", + "/Lotus/Types/PickUps/MediumLootCrateGrnFortB", + "/Lotus/Types/PickUps/ResourceContainers/NeuralSensorContainer", + "/Lotus/Types/Friendly/Agents/DefenseComputerCorpusGasImmuneAvatar", + "/Lotus/Objects/Guild/GasCityRemaster/Props/CrpCleanbotMover", + "/Lotus/Objects/Guild/Structural/Vents/DestroyableVent", + "/Lotus/Types/Lore/GasCityFragmentScanDeco", + "/Lotus/Weapons/Grineer/Emplacements/GrnDeployableCover/SeaLabGrnDeployableCover", + "/Lotus/Powersuits/MonkeyKing/HairAvatar", + "/Lotus/Types/PickUps/ResourceContainers/NeurodesContainer", + "/Lotus/Objects/Guild/GasCityRemaster/Props/CrpExplodingBarrelRadiation", + "/Lotus/Objects/Guild/GasCityRemaster/Props/CrpGasCityDoorPowerSupplySide5mDeco", + "/Lotus/Weapons/Corpus/Emplacement/CrpDeployableCover/CrpDeployableCover", + "/Lotus/Characters/Guild/QuadrapedPrototype/QuadrapedPrototypeDeco", + "/Lotus/Types/Enemies/Infested/Vip/InfestedGrenadeDeco", + "/Lotus/Types/Friendly/Agents/HiveMode/InfestedHiveAvatarB", + "/Lotus/Objects/Guild/GasCityRemaster/Props/CrpDestructableGeneratorDeco", + "/Lotus/Objects/Guild/GasCityRemaster/Props/GasRepairBotMover", + "/Lotus/Types/PickUps/ResourceContainers/GalliumContainer", + "/Lotus/Objects/SolarisVenus/Wildlife/OroRoboFishADeco", + "/Lotus/Objects/SolarisVenus/Wildlife/RobofishCorpusGDeco", + "/Lotus/Objects/SolarisVenus/Wildlife/RobofishSolarisDDeco", + "/Lotus/Types/Gameplay/Conservation/OrokinKubrow/OrokinKubrowStartGameplayObject", + "/Lotus/Types/Gameplay/Venus/Resources/SVFoliageFungusSmallContainer", + "/Lotus/Types/Gameplay/Venus/Resources/VenusTreeContainer", + "/Lotus/Objects/Gameplay/OroFusexDDeco", + "/Lotus/Powersuits/Brawler/SummonAvatar", + "/Lotus/Types/PickUps/Railjack/GrineerPointOfInterestLootCrate", + "/Lotus/Types/Enemies/Infested/Vip/InfestedMaggotSpawnPodTwoStageDisruption", + "/Lotus/Types/Gameplay/Eidolon/Resources/GrokdrulContainer", + "/Lotus/Types/Gameplay/Eidolon/Resources/Nistlebrush", + "/Lotus/Types/Enemies/Grineer/Vip/Avatars/DoubleBossAvatar", + "/Lotus/Types/Friendly/Agents/PayloadAvatar", + "/Lotus/Levels/SentientDevourer/Prefabs/SentientDevourerLootCrateCommon", + "/Lotus/Types/LevelObjects/Sentient/Attachments/BuffStationGem", + "/Lotus/Types/Game/CrewShip/GrineerDestroyer/GrineerGunnerEmplacement", + "/Lotus/Types/Gameplay/Conservation/SnowArmadillo/SnowArmadilloCallPointADeco", + "/Lotus/Types/Gameplay/Conservation/SnowArmadillo/SnowArmadilloStartGameplayObject", + "/Lotus/Types/Gameplay/Conservation/SnowBird/SnowBirdCallPointADeco", + "/Lotus/Types/Gameplay/Conservation/SnowBird/SnowBirdCallPointBDeco", + "/Lotus/Types/Gameplay/Conservation/SnowBird/SnowBirdStartGameplayObject", + "/Lotus/Types/Gameplay/Conservation/SnowCritter/SnowCritterCallPointBDeco", + "/Lotus/Types/Gameplay/Conservation/SnowCritter/SnowCritterStartGameplayObject", + "/Lotus/Types/Gameplay/Conservation/SnowPredator/SnowPredatorCallPointADeco", + "/Lotus/Types/Gameplay/Conservation/SnowPredator/SnowPredatorCallPointBDeco", + "/Lotus/Types/Gameplay/Conservation/SnowPredator/SnowPredatorStartGameplayObject", + "/Lotus/Types/Gameplay/Conservation/SnowPredator/SnowPredatorToxicScatDeco", + "/Lotus/Types/Gameplay/Venus/Resources/CoolantContainer", + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/UncommonSnowPredatorAvatar", + "/Lotus/Types/Restoratives/Deployables/LisetTurretAvatar", + "/Lotus/Types/PickUps/Railjack/GrineerPointOfInterestUncommonLootCrate", + "/Lotus/Objects/Guild/GasCityRemaster/Props/GasHazardDetonatorLarge", + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/UncommonVampireKavatAvatar", + "/Lotus/Objects/Guild/Props/CrpTrainDrone", + "/Lotus/Types/Game/Events/OrbEventGlyphEight", + "/Lotus/Types/LevelObjects/GamemodeLockers/VenusStorageLockerCache", + "/Lotus/Types/Gameplay/Conservation/ForestRodent/ForestRodentStartGameplayObject", + "/Lotus/Weapons/Grineer/RailJack/GrnCrewDeployableCover", + "/Lotus/Objects/Gameplay/OroFusexEDeco", + "/Lotus/Types/Enemies/Sentients/Mimics/MimicGalliumDeco", + "/Lotus/Types/Enemies/Sentients/Mimics/MimicRubedoDeco", + "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetAvatar", + "/Lotus/Types/Friendly/Agents/DefenseAvatarMoving", + "/Lotus/Types/Gameplay/Conservation/LegendaryKubrow/LegendaryKubrowCallPointADeco", + "/Lotus/Types/Gameplay/Conservation/LegendaryKubrow/LegendaryKubrowCallPointBDeco", + "/Lotus/Types/Gameplay/Conservation/LegendaryKubrow/LegendaryKubrowStartGameplayObject", + "/Lotus/Types/Gameplay/Venus/Resources/SVFoliageFungusLargeContainer", + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/CommonLegendaryKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/RareLegendaryKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/UncommonLegendaryKubrowAvatar", + "/Lotus/Types/Enemies/Grineer/DeathSquad/Avatars/DeathSquadSentinelAvatar", + "/Lotus/Types/Friendly/Agents/GrnOceanDefenseAvatar", + "/Lotus/Types/Game/Decorations/MiningMachineObjective", + "/Lotus/Types/Enemies/Corpus/Vehicle/WheelCarDropshipParkedAvatar", + "/Lotus/Types/Friendly/Agents/HiveMode/InfestedHiveAvatarA", + "/Lotus/Types/LevelObjects/GrineerBreakableFan", + "/Lotus/Types/LevelObjects/GamemodeLockers/VenusStorageLockerCacheIndoors", + "/Lotus/Objects/Guild/GasCityRemaster/Props/GasHazardDetonator", + "/Lotus/Objects/Gameplay/OroFusexADeco", + "/Lotus/Types/PickUps/PuzzleOrokinLootCrate", + "/Lotus/Types/Friendly/Venus/DynamicExcavatorAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/RareVampireKavatAvatar", + "/Lotus/Objects/Ostron/Wildlife/OstronFishA", + "/Lotus/Objects/Ostron/Wildlife/OstronFishF", + "/Lotus/Objects/Ostron/Wildlife/OstronFishH", + "/Lotus/Objects/Ostron/Wildlife/OstronFishI", + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/CommonBirdOfPreyAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/CommonForestRodentAvatar", + "/Lotus/Types/PickUps/RareOrokinLootCrate", + "/Lotus/Types/Enemies/Infested/AiWeek/Ancients/PussBlobDeco", + "/Lotus/Types/Friendly/Agents/DefenseComputerCorpusAvatarSmall", + "/Lotus/Types/Lore/Fragments/CorpusReliefFragments/CorpusReliefLoreFragmentADeco", + "/Lotus/Types/Lore/Fragments/CorpusReliefFragments/CorpusReliefLoreFragmentBDeco", + "/Lotus/Types/Lore/Fragments/CorpusReliefFragments/CorpusReliefLoreFragmentCDeco", + "/Lotus/Types/Lore/Fragments/CorpusReliefFragments/CorpusReliefLoreFragmentDDeco", + "/Lotus/Types/Lore/Fragments/CorpusReliefFragments/CorpusReliefLoreFragmentEDeco", + "/Lotus/Types/Lore/Fragments/CorpusReliefFragments/CorpusReliefLoreFragmentFDeco", + "/Lotus/Types/Lore/Fragments/CorpusReliefFragments/CorpusReliefLoreFragmentGDeco", + "/Lotus/Types/Lore/Fragments/CorpusReliefFragments/CorpusReliefLoreFragmentHDeco", + "/Lotus/Types/Lore/Fragments/CorpusReliefFragments/CorpusReliefLoreFragmentIDeco", + "/Lotus/Types/Enemies/Corpus/Venus/ReinforcementBeacon/VenusReinforceBeaconDeco", + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/CommonVampireKavatAvatar", + "/Lotus/Objects/SolarisVenus/Wildlife/OroRoboFishCDeco", + "/Lotus/Objects/SolarisVenus/Wildlife/RobofishCorpusADeco", + "/Lotus/Objects/SolarisVenus/Wildlife/RobofishCorpusCDeco", + "/Lotus/Objects/SolarisVenus/Wildlife/RobofishCorpusEV1Deco", + "/Lotus/Objects/SolarisVenus/Wildlife/RobofishSolarisBDeco", + "/Lotus/Objects/SolarisVenus/Wildlife/RobofishSolarisCDeco", + "/Lotus/Objects/Ostron/Wildlife/OstronFishC", + "/Lotus/Types/Lore/Fragments/CorpusReliefFragments/CorpusReliefLoreFragmentJDeco", + "/Lotus/Types/PickUps/RareGrineerForestLootCrate", + "/Lotus/Types/Lore/Fragments/CorpusReliefFragments/CorpusReliefLoreFragmentKDeco", + "/Lotus/Types/Gameplay/Eidolon/SuppliesSabotageDecoration", + "/Lotus/Types/Gameplay/Conservation/SnowRodent/SnowRodentCallPointBDeco", + "/Lotus/Types/Gameplay/Conservation/SnowRodent/SnowRodentStartGameplayObject", + "/Lotus/Types/Friendly/Agents/CoreDefenseAvatar", + "/Lotus/Weapons/Grineer/Emplacements/GrnDeployableCoverQueen/QueenGrnDeployableCover", + "/Lotus/Objects/SolarisVenus/Natural/Foliage/SvMushroomFruitSheathedDeco", + "/Lotus/Types/Gameplay/Conservation/SnowCritter/SnowCritterCallPointADeco", + "/Lotus/Types/Gameplay/Conservation/SnowRodent/SnowRodentCallPointADeco", + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/RareSnowRodentAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/UncommonSnowRodentAvatar", + "/Lotus/Objects/Ostron/Wildlife/EelFish", + "/Lotus/Objects/Ostron/Wildlife/OstronFishD", + "/Lotus/Types/Gameplay/Conservation/OstronSeaBird/OstronSeaBirdStartGameplayObject", + "/Lotus/Fx/Levels/Orokin/Moon/OMResidualVoidSphereIconDecoSphere", + "/Lotus/Types/Game/Buttons/ButtonOrokin", + "/Lotus/Levels/SentientDevourer/Prefabs/SentientDevourerLootCrateUncommon", + "/Lotus/Types/Friendly/Agents/SurvivalKuvaExtractorAvatar", + "/Lotus/Types/LevelObjects/GrineerLandmine", + "/Lotus/Types/Enemies/Infested/Vip/InfestedGrenadeStumpDeco", + "/Lotus/Types/Lore/Fragments/SolarisFragments/TheBusinessLoreFragmentDDeco", + "/Lotus/Types/Game/Events/OrbEventGlyphSeven", + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/UncommonForestRodentAvatar", + "/Lotus/Types/Game/Events/OrbEventGlyphFourteen", + "/Lotus/Types/Lore/Fragments/SolarisFragments/SmokefingerLoreFragmentEDeco", + "/Lotus/Types/Gameplay/Purify/InfestedConsole", + "/Lotus/Types/Gameplay/Purify/Purifier", + "/Lotus/Types/Friendly/Agents/DefenseComputerFortAvatar", + "/Lotus/Types/Enemies/CorpusChampions/JohnProdman/JohnProdmanAvatar", + "/Lotus/Objects/Infestation/InfestedMicroplanet/Foliage/InfGorgaricusSacDeco", + "/Lotus/Types/Gameplay/InfestedMicroplanet/EncounterObjects/ExterminateHiveDeco", + "/Lotus/Types/Gameplay/InfestedMicroplanet/EncounterObjects/ExterminateHiveDecoShellProxy", + "/Lotus/Types/Gameplay/InfestedMicroplanet/LootContainers/InfestedCommonCrateContainer", + "/Lotus/Types/Gameplay/InfestedMicroplanet/LootContainers/OrokinCommonCrateContainer", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Resources/InfMapricoTree/InfMapricoTreeDeco", + "/Lotus/Types/Gameplay/InfestedMicroplanet/Resources/OrbStone/OrbStoneContainer", + "/Lotus/Types/Gameplay/InfestedMicroplanet/EncounterObjects/DoorKeyDevice", + "/Lotus/Types/Gameplay/Conservation/Deimos/InfestedPredator/InfestedPredatorStartGameplayObject", + "/Lotus/Levels/InfestedMicroplanet/InfestedLandmine", + "/Lotus/Types/Friendly/InfestedMicroPlanet/DoorKeyDeviceAvatar", + "/Lotus/Types/Gameplay/InfestedMicroplanet/LootContainers/InfestedRareCrateContainer", + "/Lotus/Types/Gameplay/InfestedMicroplanet/EncounterObjects/PurifierAvatar", + "/Lotus/Types/Gameplay/InfestedMicroplanet/EncounterObjects/PurifyMollusk", + "/Lotus/Types/Gameplay/InfestedMicroplanet/EncounterObjects/ShrineDefenseAvatar", + "/Lotus/Types/Gameplay/InfestedMicroplanet/EncounterObjects/KeyPiecesEidolonCache", + "/Lotus/Types/Gameplay/InfestedMicroplanet/EncounterObjects/KeyPiecesMainTumorDeco", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedPredator/CommonInfestedPredatorAvatar", + "/Lotus/Objects/Infestation/InfestedMicroplanet/Gameplay/EntratiObeliskShapeshifterCrystal", + "/Lotus/Types/Friendly/InfestedMicroPlanet/DynamicExcavatorAvatar", + "/Lotus/Types/Gameplay/Conservation/Deimos/InfestedMergoo/InfestedMergooStartGameplayObject", + "/Lotus/Types/Gameplay/InfestedMicroplanet/EncounterObjects/KeyPiecesTumorDeco", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedKDrive/HighlandInfKDriveAvatar", + "/Lotus/Fx/Levels/InfestedMicroplanet/WyrmBattle/WyrmDestructibleDecoInfested", + "/Lotus/Types/Gameplay/InfestedMicroplanet/EncounterObjects/KeyPiecesVenusCache", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedCritter/CommonInfestedCritterAvatar", + "/Lotus/Types/Gameplay/Conservation/Deimos/InfestedMaggot/InfestedMaggotScatDeco", + "/Lotus/Types/Gameplay/Conservation/Deimos/InfestedMaggot/InfestedMaggotStartGameplayObject", + "/Lotus/Fx/Levels/InfestedMicroplanet/WyrmBattle/WyrmDestructibleDecoEntrati", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedCritter/UncommonInfestedCritterAvatar", + "/Lotus/Fx/Levels/InfestedMicroplanet/WyrmBattle/WyrmDestructibleDecoEntratiVault", + "/Lotus/Objects/Infestation/InfestedMicroplanet/Gameplay/EntratiObeliskTetherCrystal", + "/Lotus/Types/Gameplay/Conservation/Deimos/InfestedMaggot/InfestedMaggotCallPointADeco", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMaggot/CommonInfestedMaggotAvatar", + "/Lotus/Types/Gameplay/Conservation/Deimos/InfestedCritter/InfestedCritterCallPointADeco", + "/Lotus/Types/Gameplay/Conservation/Deimos/InfestedCritter/InfestedCritterScatDeco", + "/Lotus/Types/Gameplay/Conservation/Deimos/InfestedCritter/InfestedCritterStartGameplayObject", + "/Lotus/Types/Gameplay/Conservation/Deimos/InfestedMaggot/InfestedMaggotCallPointBDeco", + "/Lotus/Types/Gameplay/Conservation/Deimos/InfestedMergoo/InfestedMergooCallPointADeco", + "/Lotus/Types/Gameplay/Conservation/Deimos/InfestedMergoo/InfestedMergooCallPointBDeco", + "/Lotus/Types/Gameplay/Conservation/Deimos/InfestedPredator/InfestedPredatorCallPointADeco", + "/Lotus/Types/Gameplay/Conservation/Deimos/InfestedPredator/InfestedPredatorToxicScatDeco", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMaggot/UncommonInfestedMaggotAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMergoo/CommonInfestedMergooAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMergoo/RareInfestedMergooAvatar", + "/Lotus/Types/Gameplay/Conservation/Deimos/InfestedPredator/InfestedPredatorCallPointBDeco", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedPredator/UncommonInfestedPredatorAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedKDrive/SwampInfKDriveAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedPredator/RareInfestedPredatorAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedCritter/RareInfestedCritterAvatar", + "/Lotus/Fx/Levels/InfestedMicroplanet/WyrmBattle/WyrmDestructibleDecoInfestedVault", + "/Lotus/Objects/Infestation/InfestedMicroplanet/Wildlife/InfestedCommonADeco", + "/Lotus/Objects/Infestation/InfestedMicroplanet/Wildlife/InfestedCommonCDeco", + "/Lotus/Objects/Infestation/InfestedMicroplanet/Gameplay/EntratiObeliskStopmotionCrystal", + "/Lotus/Objects/Infestation/InfestedMicroplanet/Gameplay/EntratiObeliskBlindCrystal", + "/Lotus/Objects/Infestation/InfestedMicroplanet/Wildlife/HybridUncommonBDeco", + "/Lotus/Objects/Infestation/InfestedMicroplanet/Wildlife/InfestedCommonBDeco", + "/Lotus/Objects/Infestation/InfestedMicroplanet/Wildlife/InfestedCommonDDeco", + "/Lotus/Objects/Infestation/InfestedMicroplanet/Wildlife/OrokinUncommonADeco", + "/Lotus/Objects/Infestation/InfestedMicroplanet/Wildlife/InfestedUncommonADeco", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedKDrive/GrottoInfKDriveAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMaggot/RareInfestedMaggotAvatar", + "/Lotus/Types/Gameplay/Conservation/Deimos/InfestedCritter/InfestedCritterCallPointBDeco", + "/Lotus/Objects/Infestation/InfestedMicroplanet/Wildlife/OrokinRareADeco", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMergoo/UncommonInfestedMergooAvatar", + "/Lotus/Types/Friendly/Agents/TitaniaShrineDefenseAvatar", + "/Lotus/Types/Game/Events/OrbEventGlyphFive", + "/Lotus/Types/Lore/Fragments/SolarisFragments/TickerLoreFragmentDDeco", + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/UncommonBirdOfPreyAvatar", + "/Lotus/Types/PickUps/UltraRareOrokinLootCrate", + "/Lotus/Objects/Infestation/InfestedMicroplanet/Wildlife/InfestedCommonEDeco", + "/Lotus/Objects/Infestation/InfestedMicroplanet/Wildlife/HybridRareADeco", + "/Lotus/Objects/Infestation/InfestedMicroplanet/Wildlife/InfestedRareADeco", + "/Lotus/Objects/Infestation/InfestedMicroplanet/Wildlife/HybridUncommonADeco", + "/Lotus/Powersuits/Operator/UmbraAvatar", + "/Lotus/Types/Gameplay/InfestedMicroplanet/EncounterObjects/FluidSacDevice", + "/Lotus/Types/Gameplay/InfestedMicroplanet/EncounterObjects/InfestedMistDeviceAvatar", + "/Lotus/Types/Enemies/Sentients/RepeaterDeco", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedUndazoa/CommonUndazoaAvatar", + "/Lotus/Types/Game/Events/OrbEventGlyphEleven", + "/Lotus/Types/Enemies/Infested/Vip/InfestedMaggotSpawnPodTwoStage", + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/RareBirdOfPreyAvatar", + "/Lotus/Types/Gameplay/Conservation/OrokinKubrow/OrokinKubrowCallPointBDeco", + "/Lotus/Types/Gameplay/Conservation/SnowArmadillo/SnowArmadilloCallPointBDeco", + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/RareSnowPredatorAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/RareSnowArmadilloAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/UncommonSnowArmadilloAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/UncommonOrokinKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/RareSnowBirdAvatar", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAvatar", + "/Lotus/Levels/Railjack/Objects/VolatileReactorVentHazardLarge", + "/Lotus/Types/Enemies/Corpus/Lawyers/Pets/LawyerDogPetThrallAvatar", + "/Lotus/Types/Enemies/Corpus/Lawyers/Pets/LawyerDogPetAvatar", + "/Lotus/Types/Enemies/Corpus/Lawyers/Pets/LawyerDogPetCloneAvatar", + "/Lotus/Types/PickUps/Railjack/CorpusPointOfInterestLootCrate", + "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPeCloneAvatar", + "/Lotus/Types/PickUps/Railjack/CorpusPointOfInterestUncommonLootCrate", + "/Lotus/Types/PickUps/Railjack/OrokinPOILootCrateUncommon", + "/Lotus/Types/PickUps/Railjack/OrokinPOILootCrateCommon", + "/Lotus/Objects/Guild/GasCityRemaster/Props/CrpAnchorPointBDeco", + "/Lotus/Powersuits/Brawler/SummonAvatarNpc", + "/Lotus/Types/Gameplay/InfestedMicroplanet/EncounterObjects/InfestedSpawnPod", + "/Lotus/Types/Gameplay/InfestedMicroplanet/LootContainers/OrokinVaultCrateContainer", + "/Lotus/Types/Lore/Fragments/SolarisFragments/LittleDuckLoreFragmentBDeco", + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/RareForestRodentAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/CommonOstronSeaBirdAvatar", + "/Lotus/Types/Gameplay/Conservation/OrokinKubrow/OrokinKubrowCallPointADeco", + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/RareOrokinKubrowAvatar", + "/Lotus/Types/Friendly/Agents/InfestedBaitAvatar", + "/Lotus/Types/Gameplay/Conservation/BirdOfPrey/BirdOfPreyStartGameplayObject", + "/Lotus/Types/Gameplay/Eidolon/Objects/InfestedPlains/InfestedDropPodSpawnPod", + "/Lotus/Types/Gameplay/Conservation/VampireKavat/VampireKavatStartGameplayObject", + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/UncommonSnowBirdAvatar", + "/Lotus/Powersuits/Archwing/StealthJetPack/DistractionDroneWaterAvatar", + "/Lotus/Types/PickUps/UltraRareGrineerLootCrate", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedNexifera/RareInfestedNexiferaAvatar", + "/Lotus/Types/Lore/Fragments/SolarisFragments/LittleDuckLoreFragmentCDeco", + "/Lotus/Types/LevelObjects/Zariman/ZarLootCrateCommonA", + "/Lotus/Types/LevelObjects/Zariman/ZarLootCrateCommonB", + "/Lotus/Types/LevelObjects/Zariman/ZarLootCrateUncommon", + "/Lotus/Types/LevelObjects/Zariman/DoorConsole", + "/Lotus/Fx/Gameplay/Corruption/CorruptionVoidDepositDeco", + "/Lotus/Types/Enemies/Zariman/Avatars/VoidAngelAvatarEndless", + "/Lotus/Types/Gameplay/Zariman/EncounterObjects/AssassinateEndless/Barracks", + "/Lotus/Types/Gameplay/Zariman/EncounterObjects/AssassinateEndless/ScrapContainer", + "/Lotus/Types/LevelObjects/ZarimanBreakableHatchDoorA", + "/Lotus/Types/Friendly/Agents/ZarimanMobileDefenseAvatar", + "/Lotus/Types/Enemies/Grineer/Zariman/Attachments/GrnAntiWarframeMineDeco", + "/Lotus/Types/Gameplay/Conservation/BirdOfPrey/BirdOfPreyCallPointADeco", + "/Lotus/Types/Gameplay/Conservation/BirdOfPrey/BirdOfPreyCallPointBDeco", + "/Lotus/Types/Gameplay/Conservation/ForestRodent/ForestRodentCallPointADeco", + "/Lotus/Types/Gameplay/Conservation/ForestRodent/ForestRodentCallPointBDeco", + "/Lotus/Types/Gameplay/Conservation/OstronSeaBird/OstronSeaBirdCallPointADeco", + "/Lotus/Types/Gameplay/Conservation/OstronSeaBird/OstronSeaBirdCallPointBDeco", + "/Lotus/Types/Gameplay/Conservation/VampireKavat/VampireKavatCallPointBDeco", + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/RareOstronSeaBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/UncommonOstronSeaBirdAvatar", + "/Lotus/Types/Gameplay/Nightwave/CephalonMaze/GlassmakerAvatar", + "/Lotus/Objects/Grineer/Structural/FomorianMothership/FomShipForceFieldDeco", + "/Lotus/Types/Lore/SongFragmentPickupDeco", + "/Lotus/Types/Gameplay/InfestedMicroplanet/LootContainers/OrokinRareCrateContainer", + "/Lotus/Types/Gameplay/InfestedMicroplanet/LootContainers/OrokinSimpleCrateContainer", + "/Lotus/Types/PickUps/Narmer/NarmerLootCrate", + "/Lotus/Objects/Gameplay/OroFusexPickupDeco", + "/Lotus/Objects/Gameplay/OroFusexPickupBDeco", + "/Lotus/Types/Friendly/Agents/PedestalMachineDefenseAvatar", + "/Lotus/Types/Friendly/Agents/CoreDefenseBombAvatar", + "/Lotus/Types/Gameplay/Race/CrpSpaceMine", + "/Lotus/Types/PickUps/LootContainers/CorpusLootCrateCommonArchwing", + "/Lotus/Types/PickUps/LootContainers/CorpusLootCrateUncommonArchwing", + "/Lotus/Powersuits/Fairy/FlightAvatar", + "/Lotus/Objects/Infestation/InfestedMicroplanet/Gameplay/EntratiObeliskMadnessCrystal", + "/Lotus/Objects/Ostron/Wildlife/MaskedFish", + "/Lotus/Objects/Ostron/Wildlife/OstronFishJ", + "/Lotus/Objects/SolarisVenus/Wildlife/RobofishCorpusFDeco", + "/Lotus/Objects/SolarisVenus/Wildlife/RobofishCorpusStyleADeco", + "/Lotus/Objects/SolarisVenus/Wildlife/RobofishSolarisADeco", + "/Lotus/Objects/Ostron/Wildlife/OstronFishE", + "/Lotus/Objects/Ostron/Wildlife/OstronFishB", + "/Lotus/Objects/Ostron/Wildlife/OstronFishG", + "/Lotus/Types/EnvDangers/GrineerOcean/SharkMover", + "/Lotus/Objects/Infestation/InfestedMicroplanet/Wildlife/OrokinLegendaryADeco", + "/Lotus/Types/LevelObjects/GamemodeLockers/NarmerStorageLockerCache", + "/Lotus/Objects/SolarisVenus/Wildlife/OroRoboFishBDeco", + "/Lotus/Types/Gameplay/Race/OrbiterMediumAvatar", + "/Lotus/Types/Gameplay/Race/OrbiterSmallAvatar", + "/Lotus/Types/Gameplay/Race/SupplyShip", + "/Lotus/Objects/Ostron/Wildlife/FatHeadFish", + "/Lotus/Types/Game/PowerRift/PowerRiftDeco", + "/Lotus/Types/LevelObjects/Zariman/ZarLootCrateRare", + "/Lotus/Powersuits/Dragon/DragonPeltPrimeAvatar", + "/Lotus/Types/Friendly/Rescue/DefenseAvatarChipper", + "/Lotus/Weapons/Grineer/Emplacements/GrnEmplcmntStndng/GrineerEmplacementStandingKahl", + "/Lotus/Levels/KahlMissions/KahlTypes/KahlAllyNpcAvatar", + "/Lotus/Levels/KahlMissions/KahlTypes/KahlDeployableFlyingSpacemanVariableAllyNpcNoFlightAvatar", + "/Lotus/Types/Friendly/RailJack/RJCrewAvatar", + "/Lotus/Types/Game/CrewShip/CorpusDestroyer/CorpusDestroyerEmptyAvatarVariantA", + "/Lotus/Types/LevelObjects/GamemodeLockers/NarmerVenusStorageLockerCache", + "/Lotus/Types/Friendly/Quest/SimarisBombardAvatar", + "/Lotus/Types/Friendly/Quest/SimarisLancerAvatar", + "/Lotus/Types/Friendly/Quest/SimarisMoaAvatar", + "/Lotus/Types/Friendly/Quest/SimarisSpacemanAvatar", + "/Lotus/Types/Game/CrewShip/CorpusDestroyer/CorpusDestroyerAvatarVariantD", + "/Lotus/Types/Game/CrewShip/CorpusDestroyer/CorpusDestroyerEmptyAvatarVariantB", + "/Lotus/Types/Game/CrewShip/CrpSecurityNode", + "/Lotus/Types/Enemies/Corpus/Vip/Arachnoid/CamperBroodMicroDeco", + "/Lotus/Types/Game/CrewShip/CorpusDestroyer/CorpusDestroyerAvatarVariantB", + "/Lotus/Types/Game/CrewShip/CorpusDestroyer/CorpusDestroyerAvatarVariantC", + "/Lotus/Types/Lore/Fragments/SolarisFragments/SmokefingerLoreFragmentDDeco", + "/Lotus/Types/Lore/Fragments/SolarisFragments/EudicoLoreFragmentBDeco", + "/Lotus/Types/Lore/Fragments/SolarisFragments/EudicoLoreFragmentEDeco", + "/Lotus/Types/Gameplay/Conservation/Deimos/Undazoa/UndazoaCallPointADeco", + "/Lotus/Types/Gameplay/Conservation/Deimos/Undazoa/UndazoaScatDeco", + "/Lotus/Types/Gameplay/Conservation/Deimos/Undazoa/UndazoaStartGameplayObject", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedNexifera/CommonInfestedNexiferaAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedUndazoa/RareUndazoaAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedUndazoa/UncommonUndazoaAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedNexifera/UncommonInfestedNexiferaAvatar", + "/Lotus/Types/Gameplay/InfestedMicroplanet/EncounterObjects/FluidSacTumor", + "/Lotus/Objects/Infestation/InfestedMicroplanet/Gameplay/EntratiObeliskRepellentCrystal", + "/Lotus/Types/Gameplay/InfestedMicroplanet/EncounterObjects/KeyPiecesOrokinCache", + "/Lotus/Objects/Infestation/InfestedMicroplanet/Gameplay/EntratiObeliskTeleportCrystal", + "/Lotus/Types/Enemies/Infested/Vip/InfestedDeadColonistSpawnPod", + "/Lotus/Types/Enemies/Infested/AiWeek/Emissary/InfestedJetpackReformDeco", + "/Lotus/Types/Enemies/Infested/AiWeek/Emissary/InfestedJetpackReformDecoNoHead", + "/Lotus/Types/Friendly/Agents/HiveMode/InfestedHiveAvatarD", + "/Lotus/Types/Friendly/Agents/HiveMode/InfestedHiveAvatarE", + "/Lotus/Objects/Infestation/InfestedMicroplanet/Gameplay/EntratiObeliskAntibodyCrystal", + "/Lotus/Types/Gameplay/InfestedMicroplanet/EncounterObjects/CorpusSurvivorsShieldDroneAvatar", + "/Lotus/Types/Friendly/Agents/DefenseMDArchwingAvatarCorpus", + "/Lotus/Types/Friendly/Rescue/DefenseAvatarMale", + "/Lotus/Levels/KahlMissions/KahlTypes/KahlEidolonJetpackMarineAllyNpcNoFlightAvatar", + "/Lotus/Types/Game/CrewShip/CorpusDestroyer/CorpusDestroyerAvatarVariantA", + "/Lotus/Types/Friendly/LastWishDefense/DoubleDefenseAvatarBlue", + "/Lotus/Types/Friendly/LastWishDefense/DoubleDefenseAvatarRed", + "/Lotus/Levels/TheNewWar2021/Part2/CrpWeakExplodingBarrelGas", + "/Lotus/Types/Friendly/Eidolon/DynamicGhoulHuntExcavatorAvatar", + "/Lotus/Types/Friendly/Rescue/GhoulRescueAvatar", + "/Lotus/Types/Gameplay/Duviri/SideActivities/Encounters/CombatPatrol/DuviriThraxMeleeGuardAvatar", + "/Lotus/Types/Gameplay/Duviri/SideActivities/Encounters/Shepherding/CattleAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Duviri/Kavat/DuviriConservationCatbrowAvatar", + "/Lotus/Types/Enemies/CorpusChampions/JohnProdman/JohnProdmanAnniversaryAvatar", + "/Lotus/Types/Friendly/ClemAvatar", + "/Lotus/Types/Friendly/Agents/KuvaDefenseAvatar", + "/Lotus/Types/Gameplay/Duviri/SideActivities/Encounters/CombatTrappedChest/DuviriThraxMeleeGuardAvatar", + "/Lotus/Types/Friendly/Rescue/RescueAvatarDarvo", + "/Lotus/Levels/TheNewWar2021/Part2/CrpWeakExplodingBarrel", + "/Lotus/Types/Friendly/Agents/EventDefenseAvatar", + "/Lotus/Types/Enemies/GrineerChampions/ChampionPowersuits/EmpTrap", + "/Lotus/Types/Friendly/Syndicate/CacheHunt/CacheHuntRescueAvatarFemale", + "/Lotus/Objects/Entrati/Lab/Gameplay/LabVials/ORKxLabVialHazardADeco", + "/Lotus/Objects/Entrati/Lab/Gameplay/LabVials/ORKxLabVialHazardADynamicDeco", + "/Lotus/Objects/Entrati/Lab/Gameplay/LabVials/ORKxLabVialHazardARegenDeco", + "/Lotus/Objects/Entrati/Lab/Gameplay/LabVials/ORKxLabVialHazardBDeco", + "/Lotus/Objects/Entrati/Lab/Gameplay/LabVials/ORKxLabVialHazardBRegenDeco", + "/Lotus/Types/PickUps/LootContainers/EntratiLootCrateCommon", + "/Lotus/Types/PickUps/LootContainers/EntratiLootCrateUncommon", + "/Lotus/Types/PickUps/LootContainers/MITWLootCrate", + "/Lotus/Objects/Entrati/Lab/Gameplay/Destructible/LootCrates/ORKxLabVialAHealthHangingDeco", + "/Lotus/Types/Friendly/EntratiDefense/LoopDefenseAvatarEar", + "/Lotus/Types/Gameplay/EntratiLab/Quest/HumanLoidCombatAvatar", + "/Lotus/Types/PickUps/LootContainers/EntratiLootCrateRare", + "/Lotus/Types/Friendly/EntratiDefense/LoopDefenseAvatarEye", + "/Lotus/Types/PickUps/LootContainers/EntratiLootCrateUltraRare", + "/Lotus/Types/PickUps/LootContainers/EntratiLootCrateVault", + "/Lotus/Types/Enemies/Sentients/SentientBaseAvatar", + "/Lotus/Types/Friendly/Agents/DefenseAvatar", + "/Lotus/Types/Friendly/Rescue/RescueAvatar", + "/Lotus/Types/Enemies/Infested/InfestedAvatars/ZombieAvatar", + "/Lotus/Types/Enemies/TennoReplicants/SyndicateAllies/BaseSyndicateAllyAvatar", + "/Lotus/Types/Enemies/Grineer/Fortress/Avatars/GrineerAutoTurretBase", + "/Lotus/Types/Enemies/Infested/AiWeek/GreyStrain/GreyStrainAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/BaseLegendaryKubrowAvatar", + "/Lotus/Types/Enemies/CorpusChampions/CorpusArenaAllyBaseAvatar", + "/Lotus/Types/Enemies/Duviri/Avatars/BaseDuviriAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/BaseSnowRodentAvatar", + "/Lotus/Types/Game/PegasusAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/BaseForestRodentAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/BaseOrokinKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/BaseSnowArmadilloAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/BaseSnowBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/BaseSnowCritterAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/BaseSnowPredatorAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/BaseVampireKavatAvatar", + "/Lotus/Types/NeutralCreatures/Kubrow/KubrowAvatar", + "/Lotus/Types/Friendly/Pets/LisetPropCleaningDroneAvatar", + "/Lotus/Types/NeutralCreatures/ErsatzHorse/ErsatzHorseAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/BaseBirdOfPreyAvatar", + "/Lotus/Types/Enemies/Corpus/Turrets/TurretAvatars/SecurityCameraAvatar", + "/Lotus/Types/Enemies/Grineer/RailJack/Avatars/GrnBoardingBaseAvatar", + "/Lotus/Types/Game/JuggernautAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/BaseOstronSeaBirdAvatar", + "/Lotus/Levels/KahlMissions/KahlTypes/KahlJetpackMarineAvatar", + "/Lotus/Types/Enemies/Grineer/Eidolon/GrineerSkiff/UnmannedSkiff/FlyingVehicleAvatar", + "/Lotus/Types/Enemies/Grineer/GrineerAvatars/NightwatchGrineerMarineAvatar", + "/Lotus/Types/Friendly/Agents/DefenseComputerAvatar", + "/Lotus/Types/Friendly/Hub/BasicHubAvatar", + "/Lotus/Types/Friendly/Rescue/RescueAvatarMale", + "/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/RiotBipedBaseAvatar", + "/Lotus/Types/Enemies/CorpusChampions/CorpusChampionSkateBaseAvatar", + "/Lotus/Types/Enemies/TennoReplicants/SyndicateAllies/ColonyRescueAllies/ColonistRescueBaseAvatar", + "/Lotus/Types/Friendly/Pets/CatbrowPetAvatar", + "/Lotus/Types/Friendly/Agents/InfestedHiveAvatar", + "/Lotus/Types/Gameplay/Zariman/EncounterObjects/AssassinateEndless/TurretAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMaggot/BaseInfestedMaggotAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedPredator/BaseInfestedPredatorAvatar", + "/Lotus/Types/Enemies/CorpusChampions/CorpusChampionBaseAvatar", + "/Lotus/Types/Enemies/Duviri/Dragon/Flying/FlyingDragonBossAvatar", + "/Lotus/Types/Enemies/Grineer/Desert/Avatars/HeavyFemaleGrineerAvatar", + "/Lotus/Types/Enemies/Grineer/Thumper/ThumperCannonTurretAvatar", + "/Lotus/Types/Friendly/LastWishDefense/DoubleDefenseAvatar", + "/Lotus/Types/Friendly/Pets/KubrowPetAvatar", + "/Lotus/Types/Sentinels/SentinelAvatar", + "/Lotus/Types/Enemies/Corpus/Venus/Commanders/Avatars/VenusCommanderBaseAvatar", + "/Lotus/Types/Enemies/Corpus/Venus/Heavies/Hyenas/VenusHyenaBaseAvatar", + "/Lotus/Types/Enemies/Corpus/Venus/Vip/Avatars/VenusVipBaseSpacemanAvatar", + "/Lotus/Types/Enemies/Grineer/GfsSecurityCameraBaseAvatar", + "/Lotus/Types/Friendly/Agents/DarvoAvatar", + "/Lotus/Types/Friendly/Agents/SentientAmalgamArtifactAvatar", + "/Lotus/Types/Friendly/Hub/SolarisHubAvatar", + "/Lotus/Types/Friendly/Hub/SteelMeridianAvatarA", + "/Lotus/Levels/KahlMissions/KahlTypes/KahlDeployableFlyingSpacemanAvatar", + "/Lotus/Levels/MiniGames/Sentinel/Enemies/SpaceInvaders/SpaceInvaderAvatar", + "/Lotus/Powersuits/Infestation/PodMinionAvatar", + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/MinigunBombardAvatar", + "/Lotus/Types/Enemies/Grineer/Eidolon/Vip/Avatars/EidolonVipBaseAvatar", + "/Lotus/Types/Friendly/Agents/EventDefenseAvatar", + "/Lotus/Types/Friendly/Agents/PayloadAvatar", + "/Lotus/Types/Friendly/Pets/CatbrowShipAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedCritter/BaseInfestedCritterAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedKDrive/InfKDriveAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMergoo/BaseInfestedMergooAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedNexifera/BaseInfestedNexiferaAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedUndazoa/BaseUndazoaAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Duviri/Rabbit/BaseDuviriRabbitAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/CommonForestRodentAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/RareForestRodentAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/CommonLegendaryKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/RareLegendaryKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/UncommonLegendaryKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/CommonOrokinKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/RareOrokinKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/UncommonOrokinKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/CommonSnowArmadilloAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/RareSnowArmadilloAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/UncommonSnowArmadilloAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/CommonSnowBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/RareSnowBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/UncommonSnowBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/CommonSnowCritterAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/RareSnowCritterAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/UncommonSnowCritterAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/CommonSnowPredatorAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/RareSnowPredatorAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/UncommonSnowPredatorAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/CommonVampireKavatAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/RareVampireKavatAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/UncommonVampireKavatAvatar", + "/Lotus/Types/NeutralCreatures/ErsatzHorse/ErsatzHorseSummonAvatar", + "/Lotus/Levels/KahlMissions/KahlTypes/KahlDeployableFlyingSpacemanVariableAvatar", + "/Lotus/Levels/KahlMissions/KahlTypes/KahlEidolonJetpackMarineAvatar", + "/Lotus/Levels/KahlMissions/KahlTypes/KahlRescueMarineAvatar", + "/Lotus/Powersuits/Brawler/SummonAvatar", + "/Lotus/Powersuits/Dragon/DragonPeltAvatar", + "/Lotus/Types/Enemies/Corpus/Lawyers/Pets/LawyerDogPetAvatar", + "/Lotus/Types/Enemies/Corpus/QuadRobot/JackalBossTurretAvatar", + "/Lotus/Types/Enemies/CorpusChampions/CorpusChampionDroneBaseAvatar", + "/Lotus/Types/Enemies/CorpusChampions/CorpusChampionHyenaBaseAvatar", + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/GrineerHuntsmanAvatar", + "/Lotus/Types/Enemies/Grineer/Thumper/ThumperShotgunTurretAvatar", + "/Lotus/Types/Enemies/Narmer/Deacon/NarmerDeaconPatrolAvatar", + "/Lotus/Types/Enemies/Orokin/Entrati/EntratiTech/NechroTech/FriendlyNechroTechBaseAvatar", + "/Lotus/Types/Enemies/Sector/SectorTurretAvatar", + "/Lotus/Types/Enemies/Sentients/Swarmalyst/SentientSwarmalystAvatar", + "/Lotus/Types/Friendly/Agents/DefenseComputerCorpusAvatar", + "/Lotus/Types/Friendly/Agents/DefenseMDArchwingAvatar", + "/Lotus/Types/Friendly/Agents/ExcavatorAvatar", + "/Lotus/Types/Friendly/EntratiDefense/LoopDefenseAvatar", + "/Lotus/Types/Friendly/Pets/KubrowShipAvatar", + "/Lotus/Types/Gameplay/Race/OrbiterAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/CommonBirdOfPreyAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/RareBirdOfPreyAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/UncommonBirdOfPreyAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Duviri/Owl/BaseDuviriOwlAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Duviri/Wolf/DuviriWolfAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/UncommonForestRodentAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/CommonOstronSeaBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/RareOstronSeaBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/UncommonOstronSeaBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/CommonSnowRodentAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/RareSnowRodentAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/UncommonSnowRodentAvatar", + "/Lotus/Levels/KahlMissions/KahlTypes/KahlDeployableFlyingSpacemanVariableAllyNpcAvatar", + "/Lotus/Levels/KahlMissions/KahlTypes/KahlEidolonJetpackMarineAllyNpcAvatar", + "/Lotus/Levels/MiniGames/Sentinel/Enemies/BulletHell/AutoTurretAvatar", + "/Lotus/Powersuits/Archwing/StealthJetPack/DistractionDroneAvatar", + "/Lotus/Powersuits/Infestation/PodMinionAvatarPrime", + "/Lotus/Powersuits/Yareli/BoardAvatar", + "/Lotus/Types/Enemies/Corpus/Railjack/Avatars/CrpAdmiralBaseAvatar", + "/Lotus/Types/Enemies/Corpus/Vehicle/WheelCarDropshipParkedAvatar", + "/Lotus/Types/Enemies/Corpus/Venus/Avatars/VenusSuicideDroneAvatar", + "/Lotus/Types/Enemies/Corpus/Venus/Heavies/VenusGunnerSpacemanAvatar", + "/Lotus/Types/Enemies/CorpusChampions/CorpusChampionBipedRobotBaseAvatar", + "/Lotus/Types/Enemies/CorpusChampions/CorpusChampionRiotBipedRobotBaseAvatar", + "/Lotus/Types/Enemies/Corrupted/Avatars/CorruptedGrineerFlamebladeAvatar", + "/Lotus/Types/Enemies/Corrupted/Avatars/CorruptedGrineerLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Forest/Avatars/HeavyFemaleGrineerAvatar", + "/Lotus/Types/Enemies/Grineer/Fortress/Avatars/FortressAuraRifleLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Fortress/Avatars/FortressMinigunBombardAvatar", + "/Lotus/Types/Enemies/Grineer/GrineerAvatars/GrineerMarineHeavyAvatar", + "/Lotus/Types/Enemies/Infested/InfestedAvatars/InfestedHordeZombieAvatar", + "/Lotus/Types/Enemies/Infested/InfestedMicroplanet/GrenadeCrawlerMicroPlanetAvatar", + "/Lotus/Types/Enemies/Infested/InfestedMicroplanet/MeleeAncientMicroPlanetAvatar", + "/Lotus/Types/Enemies/Infested/InfestedMicroplanet/NoxiousCrawlerMicroPlanetAvatar", + "/Lotus/Types/Enemies/Narmer/Deacon/KahlNarmerDeaconPatrolAvatar", + "/Lotus/Types/Enemies/TennoReplicants/SyndicateAllies/ColonyRescueAllies/ColonistRescueVIPBaseAvatar", + "/Lotus/Types/Enemies/TennoReplicants/SyndicateAllies/RedVeilDeaconAvatarA", + "/Lotus/Types/Friendly/Agents/OrokinDefenseAvatar", + "/Lotus/Types/Friendly/Eidolon/GrineerResourceTheftAvatar", + "/Lotus/Types/Friendly/Pets/GardenerAvatar", + "/Lotus/Types/Friendly/Rescue/DefenseAvatarMale", + "/Lotus/Types/Friendly/Syndicate/CacheHunt/CacheHuntRescueAvatarFemale", + "/Lotus/Types/Friendly/Venus/DynamicExcavatorAvatar", + "/Lotus/Types/Friendly/ClemAvatar", + "/Lotus/Types/Game/VoidNegationTowerAvatar", + "/Lotus/Types/Gameplay/Duviri/Encounters/TownExecution/DaxGuardAvatar", + "/Lotus/Types/Gameplay/Duviri/SideActivities/Encounters/Sentry/SentryDuviriOwlAvatar", + "/Lotus/Types/Gameplay/EntratiLab/Quest/HumanLoidLotusNpcAvatar", + "/Lotus/Types/NeutralCreatures/Catbrow/CatbrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMaggot/CommonInfestedMaggotAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMaggot/RareInfestedMaggotAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMaggot/UncommonInfestedMaggotAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedPredator/CommonInfestedPredatorAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedPredator/RareInfestedPredatorAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedPredator/UncommonInfestedPredatorAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Duviri/Kavat/DuviriCatbrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Duviri/Rabbit/TeshinRabbitAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Duviri/DuvSheepAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/CommonFemaleForestRodentAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/RareFemaleForestRodentAvatar", + "/Lotus/Types/NeutralCreatures/ErsatzHorse/ErsatzHorseAAvatar", + "/Lotus/Levels/KahlMissions/KahlTypes/ChipperRescueAvatar", + "/Lotus/Levels/KahlMissions/KahlTypes/KahlAllyDropShipAvatar", + "/Lotus/Levels/KahlMissions/KahlTypes/KahlAllyNpcAvatar", + "/Lotus/Levels/KahlMissions/KahlTypes/KahlDeployableFlyingSpacemanVariableAllyNpcNoFlightAvatar", + "/Lotus/Levels/KahlMissions/KahlTypes/KahlEidolonJetpackMarineAllyNpcNoFlightAvatar", + "/Lotus/Levels/KahlMissions/KahlTypes/KahlNarmerHellionAvatar", + "/Lotus/Levels/KahlMissions/KahlTypes/KahlRescueMarineNoMaskAvatar", + "/Lotus/Levels/KahlMissions/KahlTypes/KahlRescueMarineRoggAvatar", + "/Lotus/Levels/KahlMissions/KahlTypes/KahlSntMeleeTrooperNoPowersAvatar", + "/Lotus/Levels/KahlMissions/KahlTypes/PNWKahlSentientBrachiolystAvatar", + "/Lotus/Levels/MiniGames/Sentinel/Enemies/BulletHell/AutoTurretAvatarII", + "/Lotus/Levels/MiniGames/Sentinel/Enemies/BulletHell/BipedRobotAvatar", + "/Lotus/Levels/MiniGames/Sentinel/Enemies/BulletHell/DroneFlyerAvatarI", + "/Lotus/Levels/MiniGames/Sentinel/Enemies/BulletHell/DroneFlyerAvatarII", + "/Lotus/Levels/MiniGames/Sentinel/Enemies/BulletHell/DroneFlyerAvatarIII", + "/Lotus/Levels/MiniGames/Sentinel/Enemies/BulletHell/EliteSpacemanAvatar", + "/Lotus/Levels/MiniGames/Sentinel/Enemies/BulletHell/HeliosBossAvatar", + "/Lotus/Levels/MiniGames/Sentinel/Enemies/BulletHell/MiniBossBipedAvatar", + "/Lotus/Levels/TheNewWar2021/Part2/RareFemaleForestRodentTNWAvatar", + "/Lotus/Powersuits/Archwing/StealthJetPack/DistractionDroneWaterAvatar", + "/Lotus/Powersuits/Brawler/SummonAvatarHostile", + "/Lotus/Powersuits/Brawler/SummonAvatarNpc", + "/Lotus/Powersuits/Dragon/DragonPeltPrimeAvatar", + "/Lotus/Powersuits/Dragon/DragonPeltPvPAvatar", + "/Lotus/Powersuits/Fairy/FlightAvatar", + "/Lotus/Powersuits/Infestation/PredasitePodMinionAvatar", + "/Lotus/Powersuits/Khora/Kavat/KhoraKavatAvatar", + "/Lotus/Powersuits/MonkeyKing/HairAvatar", + "/Lotus/Powersuits/Operator/UmbraAvatar", + "/Lotus/Powersuits/Sentient/SummonAvatar", + "/Lotus/Powersuits/Trapper/Deployables/ZapAvatar", + "/Lotus/Powersuits/Yareli/BoardArsenalAvatar", + "/Lotus/Powersuits/YinYang/YinYangSwitchAugmentOneAvatar", + "/Lotus/Types/Enemies/Corpus/BipedRobot/Vip/BossBipedAvatar", + "/Lotus/Types/Enemies/Corpus/Lawyers/Pets/LawyerDogPetCloneAvatar", + "/Lotus/Types/Enemies/Corpus/Lawyers/Pets/LawyerDogPetThrallAvatar", + "/Lotus/Types/Enemies/Corpus/Lawyers/KuvaLichTransmissionAvatar", + "/Lotus/Types/Enemies/Corpus/Narmer/Transports/NarmerWheelCarDropshipParkedAvatar", + "/Lotus/Types/Enemies/Corpus/QuadRobot/Mini/MiniRobotAvatar", + "/Lotus/Types/Enemies/Corpus/QuadRobot/TNWJackalBossTurretAvatar", + "/Lotus/Types/Enemies/Corpus/Spaceman/CivilianAvatar", + "/Lotus/Types/Enemies/Corpus/SpecialEvents/WraithM3PlutoCrpCrewMeleeAvatar", + "/Lotus/Types/Enemies/Corpus/Turrets/TurretAvatars/MoaAutoTurretAvatar", + "/Lotus/Types/Enemies/Corpus/Venus/Avatars/TurretBoundDroneAvatar", + "/Lotus/Types/Enemies/Corpus/Venus/Avatars/VenusAutoDroneTurretAvatar", + "/Lotus/Types/Enemies/Corpus/Venus/Avatars/VenusDetronSpacemanAvatar", + "/Lotus/Types/Enemies/Corpus/Venus/Avatars/VenusExperimentalRifleSpacemanAvatar", + "/Lotus/Types/Enemies/Corpus/Venus/Avatars/VenusTurretDroneAvatar", + "/Lotus/Types/Enemies/Corpus/Vip/Arachnoid/ArachnoidCamperSpawnerAvatar", + "/Lotus/Types/Enemies/CorpusChampions/JohnProdman/JohnProdmanAnniversaryAvatar", + "/Lotus/Types/Enemies/CorpusChampions/JohnProdman/JohnProdmanAvatar", + "/Lotus/Types/Enemies/CorpusChampions/CorpusChampionModularSpacemanBaseAvatar", + "/Lotus/Types/Enemies/Duviri/Avatars/DuviriCombatKubrowAvatar", + "/Lotus/Types/Enemies/Duviri/Avatars/DuviriThraxRangedAvatar", + "/Lotus/Types/Enemies/Duviri/Dragon/Flying/DragonVehicleAvatar", + "/Lotus/Types/Enemies/Duviri/Dragon/Ground/GroundDragonAvatar", + "/Lotus/Types/Enemies/Duviri/Jackal/DuviriJackalBossTurretAvatar", + "/Lotus/Types/Enemies/Duviri/Turrets/DuviriAutoTurretAvatar", + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/ElectricStickyRollingDroneAvatar", + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/RollingDroneRadialTurretAvatar", + "/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/RollingDroneSmokeTurretAvatar", + "/Lotus/Types/Enemies/Grineer/DeathSquad/Avatars/DeathSquadFullAvatar", + "/Lotus/Types/Enemies/Grineer/DeathSquad/Avatars/DeathSquadFullRaidAvatar", + "/Lotus/Types/Enemies/Grineer/DeathSquad/Avatars/DeathSquadFullRelayAvatar", + "/Lotus/Types/Enemies/Grineer/Eidolon/Avatars/EidolonCameraDroneAvatar", + "/Lotus/Types/Enemies/Grineer/Eidolon/Avatars/EidolonGhoulCarrierAvatar", + "/Lotus/Types/Enemies/Grineer/Eidolon/Avatars/EidolonGroupCoordinatorAvatar", + "/Lotus/Types/Enemies/Grineer/Fortress/Avatars/FortressCarrierRifleLancerAvatar", + "/Lotus/Types/Enemies/Grineer/Fortress/Avatars/FortressJetpackMarineCarrierAvatar", + "/Lotus/Types/Enemies/Grineer/RailJack/Avatars/GrnGalleonCommanderAvatar", + "/Lotus/Types/Enemies/Grineer/RailJack/Avatars/GrnKeycodeHolderBaseAvatar", + "/Lotus/Types/Enemies/Grineer/Thumper/ThumperLargeCannonTurretAvatar", + "/Lotus/Types/Enemies/Grineer/Thumper/ThumperLargeShotgunTurretAvatar", + "/Lotus/Types/Enemies/Grineer/Thumper/ThumperMedCannonTurretAvatar", + "/Lotus/Types/Enemies/Grineer/Thumper/ThumperMedShotgunTurretAvatar", + "/Lotus/Types/Enemies/Grineer/Vip/Avatars/DoubleBossAvatar", + "/Lotus/Types/Enemies/Grineer/Vip/Avatars/HammerTennoAvatar", + "/Lotus/Types/Enemies/Grineer/Vip/Avatars/HeavyMarineLeaderAvatar", + "/Lotus/Types/Enemies/Grineer/Vip/JetpackSisters/JetpackSistersAvatar", + "/Lotus/Types/Enemies/Grineer/Vip/JetpackSisters/JetpackSistersRaidAvatar", + "/Lotus/Types/Enemies/Infested/SpecialEvents/ArloZealotPackAvatar", + "/Lotus/Types/Enemies/Infested/Vip/Avatars/ZombieLeaderAvatar", + "/Lotus/Types/Enemies/KuvaLich/KuvaLichTransmissionAvatar", + "/Lotus/Types/Enemies/ManInTheWall/Octopede/Arm/ArmTurret/MITWOctopedeArmTurretWallAvatar", + "/Lotus/Types/Enemies/ManInTheWall/Octopede/Arm/MITWOctopedeArmLeftLeaderAvatar", + "/Lotus/Types/Enemies/ManInTheWall/Octopede/MITWBountyOctopedeAvatar", + "/Lotus/Types/Enemies/Narmer/ArchonErra/ArchonErraAvatar", + "/Lotus/Types/Enemies/Narmer/Deacon/KahlNarmerDeaconAvatar", + "/Lotus/Types/Enemies/NewWar/Archons/ArchonAmarChargeClone", + "/Lotus/Types/Enemies/NewWar/Archons/ArchonAmarCloneAvatar", + "/Lotus/Types/Enemies/NewWar/Archons/ArchonNiraChaseAvatar", + "/Lotus/Types/Enemies/NewWar/Archons/ArchonSummonAvatar", + "/Lotus/Types/Enemies/Orokin/Special/BurrowedMawAvatar", + "/Lotus/Types/Enemies/Quests/Chimera/VoidGhostSlowAvatar", + "/Lotus/Types/Enemies/Quests/RogueSentinel/RogueSentinelAvatar", + "/Lotus/Types/Enemies/Quests/WraithQuest/CorpusGhostArenaRangedAvatar", + "/Lotus/Types/Enemies/Sentients/Swarmalyst/SentientSwarmalystAttackerAvatar", + "/Lotus/Types/Enemies/Sentients/Swarmalyst/SentientSwarmalystDefenderAvatar", + "/Lotus/Types/Enemies/Sentients/DefendDroneAvatar", + "/Lotus/Types/Enemies/SpaceBattles/Corpus/Ships/SpaceFighterAlarmAvatar", + "/Lotus/Types/Enemies/TennoReplicants/SyndicateAllies/ColonyRescueAllies/ColonistRescueSteelMeridianVIPAvatarA", + "/Lotus/Types/Enemies/TennoReplicants/SyndicateAllies/RedVeilZombieAvatar", + "/Lotus/Types/Enemies/WF1999Infested/Avatars/Runner1999Avatar", + "/Lotus/Types/Enemies/WF1999Infested/Avatars/ToxicAncient1999Avatar", + "/Lotus/Types/Enemies/Zariman/Avatars/VoidAngelAvatarEndless", + "/Lotus/Types/Enemies/Zariman/ZarimanLootGhostAvatar", + "/Lotus/Types/Friendly/Agents/HiveMode/InfestedHiveAvatarA", + "/Lotus/Types/Friendly/Agents/HiveMode/InfestedHiveAvatarB", + "/Lotus/Types/Friendly/Agents/HiveMode/InfestedHiveAvatarC", + "/Lotus/Types/Friendly/Agents/HiveMode/InfestedHiveAvatarD", + "/Lotus/Types/Friendly/Agents/HiveMode/InfestedHiveAvatarE", + "/Lotus/Types/Friendly/Agents/HiveMode/InfestedHiveAvatarF", + "/Lotus/Types/Friendly/Agents/BaroDefenseAvatar", + "/Lotus/Types/Friendly/Agents/CoreDefenseAvatar", + "/Lotus/Types/Friendly/Agents/CoreDefenseBombAvatar", + "/Lotus/Types/Friendly/Agents/DarvoDefenseAvatar", + "/Lotus/Types/Friendly/Agents/DefenseAvatarMoving", + "/Lotus/Types/Friendly/Agents/DefenseChallengeAvatar", + "/Lotus/Types/Friendly/Agents/DefenseComputerCorpusAvatarSmall", + "/Lotus/Types/Friendly/Agents/DefenseComputerCorpusGasImmuneAvatar", + "/Lotus/Types/Friendly/Agents/DefenseComputerFortAvatar", + "/Lotus/Types/Friendly/Agents/DefenseComputerFortSabAvatar", + "/Lotus/Types/Friendly/Agents/DefenseCorePipeAvatar", + "/Lotus/Types/Friendly/Agents/DefenseCorePipeAvatarGrineer", + "/Lotus/Types/Friendly/Agents/DefenseMDArchwingAvatarCorpus", + "/Lotus/Types/Friendly/Agents/DefenseMDArchwingAvatarGrineer", + "/Lotus/Types/Friendly/Agents/DefenseSolarisCaptureAvatar", + "/Lotus/Types/Friendly/Agents/DefenseSolarisQuestAvatar", + "/Lotus/Types/Friendly/Agents/EventForestDefenseAvatar", + "/Lotus/Types/Friendly/Agents/FriendlyAvatar", + "/Lotus/Types/Friendly/Agents/FriendlyMaleAvatar", + "/Lotus/Types/Friendly/Agents/GrnOceanDefenseAvatar", + "/Lotus/Types/Friendly/Agents/InfestedBaitAvatar", + "/Lotus/Types/Friendly/Agents/KuvaDefenseAvatar", + "/Lotus/Types/Friendly/Agents/LureAvatar", + "/Lotus/Types/Friendly/Agents/OrokinDefenseAvatarMoving", + "/Lotus/Types/Friendly/Agents/OrokinMobileDefenseAvatar", + "/Lotus/Types/Friendly/Agents/OrokinSabotageConsoleAvatar", + "/Lotus/Types/Friendly/Agents/PayloadATVAvatar", + "/Lotus/Types/Friendly/Agents/PayloadAvatarCorpus", + "/Lotus/Types/Friendly/Agents/PedestalMachineDefenseAvatar", + "/Lotus/Types/Friendly/Agents/SectorDefenseBaseAvatar", + "/Lotus/Types/Friendly/Agents/SectorDefensePointAvatar", + "/Lotus/Types/Friendly/Agents/SectorGeneratorAvatar", + "/Lotus/Types/Friendly/Agents/SentientAmalgamArtifactAvatarA", + "/Lotus/Types/Friendly/Agents/SentientAmalgamArtifactAvatarB", + "/Lotus/Types/Friendly/Agents/SentientAmalgamArtifactAvatarC", + "/Lotus/Types/Friendly/Agents/SentientAmalgamArtifactAvatarD", + "/Lotus/Types/Friendly/Agents/SquadLinkDefenseAvatar", + "/Lotus/Types/Friendly/Agents/SquadLinkScannerAvatar", + "/Lotus/Types/Friendly/Agents/SurvivalKuvaExtractorAvatar", + "/Lotus/Types/Friendly/Agents/TitaniaShrineDefenseAvatar", + "/Lotus/Types/Friendly/Agents/TutorialShipDefenseAvatar", + "/Lotus/Types/Friendly/Agents/ZarimanMobileDefenseAvatar", + "/Lotus/Types/Friendly/Duviri/DuviriDefenseAvatar", + "/Lotus/Types/Friendly/Duviri/DuviriExcavatorAvatar", + "/Lotus/Types/Friendly/Eidolon/DynamicGhoulHuntExcavatorAvatar", + "/Lotus/Types/Friendly/Eidolon/EventMixerAvatar", + "/Lotus/Types/Friendly/EntratiDefense/LoopDefenseAvatarEar", + "/Lotus/Types/Friendly/EntratiDefense/LoopDefenseAvatarEye", + "/Lotus/Types/Friendly/Hub/PerrinColonistUnarmedAvatarA", + "/Lotus/Types/Friendly/Hub/PerrinColonistUnarmedAvatarB", + "/Lotus/Types/Friendly/Hub/PerrinMaleAvatar", + "/Lotus/Types/Friendly/Hub/SolarisHubAvatarForeman", + "/Lotus/Types/Friendly/Hub/SolarisHubAvatarLabourer", + "/Lotus/Types/Friendly/Hub/SolarisHubAvatarStrikerOne", + "/Lotus/Types/Friendly/Hub/SolarisHubAvatarStrikerThree", + "/Lotus/Types/Friendly/Hub/SteelMeridianAvatarB", + "/Lotus/Types/Friendly/Hub/SteelMeridianAvatarC", + "/Lotus/Types/Friendly/Hub/SteelMeridianAvatarD", + "/Lotus/Types/Friendly/Hub/SteelMeridianAvatarE", + "/Lotus/Types/Friendly/Hub/TNWOrdisAvatar", + "/Lotus/Types/Friendly/InfestedMicroPlanet/DoorKeyDeviceAvatar", + "/Lotus/Types/Friendly/InfestedMicroPlanet/DynamicExcavatorAvatar", + "/Lotus/Types/Friendly/LastWishDefense/DoubleDefenseAvatarBlue", + "/Lotus/Types/Friendly/LastWishDefense/DoubleDefenseAvatarRed", + "/Lotus/Types/Friendly/Pets/CreaturePets/CreaturePreceptComponents/InfestedCritterSentinelAvatar", + "/Lotus/Types/Friendly/Pets/CatbrowApartmentAvatar", + "/Lotus/Types/Friendly/Pets/CatbrowPetAvatarPalladino", + "/Lotus/Types/Friendly/Pets/CatbrowPuppyShipAvatar", + "/Lotus/Types/Friendly/Pets/DecoyCatbrowPetAvatar", + "/Lotus/Types/Friendly/Pets/GardenerHombaskAvatar", + "/Lotus/Types/Friendly/Pets/KubrowApartmentAvatar", + "/Lotus/Types/Friendly/Pets/KubrowPetKubQuestAvatar", + "/Lotus/Types/Friendly/Pets/KubrowPuppyShipAvatar", + "/Lotus/Types/Friendly/Pets/LisetPropCleaningDroneAvatarBeachcomber", + "/Lotus/Types/Friendly/Pets/LisetPropCleaningDroneAvatarColourFive", + "/Lotus/Types/Friendly/Pets/LisetPropCleaningDroneAvatarColourFour", + "/Lotus/Types/Friendly/Pets/LisetPropCleaningDroneAvatarColourOne", + "/Lotus/Types/Friendly/Pets/LisetPropCleaningDroneAvatarColourThree", + "/Lotus/Types/Friendly/Pets/LisetPropCleaningDroneAvatarColourTwo", + "/Lotus/Types/Friendly/Pets/LisetPropCleaningDroneAvatarTenno", + "/Lotus/Types/Friendly/Pets/LisetPropCleaningDroneAvatarTwitch", + "/Lotus/Types/Friendly/Pets/LisetPropCleaningDroneAvatarYareli", + "/Lotus/Types/Friendly/Pets/LisetPropCleaningDroneBaroAvatar", + "/Lotus/Types/Friendly/Pets/LisetPropCleaningDroneInfestedAvatar", + "/Lotus/Types/Friendly/Pets/LisetPropDuviriCleaningDroneAvatar", + "/Lotus/Types/Friendly/Pets/LisetPropNecraMechDroneAvatar", + "/Lotus/Types/Friendly/Pets/LisetPropOrokinMaggotAvatar", + "/Lotus/Types/Friendly/Pets/ShipPodMinionAvatar", + "/Lotus/Types/Friendly/Pets/ShipPodMinionAvatarPrime", + "/Lotus/Types/Friendly/Pets/VascaCubAvatar", + "/Lotus/Types/Friendly/PlayerControllable/KahlCrashDropshipAvatar", + "/Lotus/Types/Friendly/PlayerControllable/VesoDummyBipedAvatar", + "/Lotus/Types/Friendly/PlayerControllable/VesoMoaDeraAvatar", + "/Lotus/Types/Friendly/PlayerControllable/VesoOspreyShieldAvatar", + "/Lotus/Types/Friendly/Quest/SimarisBombardAvatar", + "/Lotus/Types/Friendly/Quest/SimarisLancerAvatar", + "/Lotus/Types/Friendly/Quest/SimarisMoaAvatar", + "/Lotus/Types/Friendly/Quest/SimarisSpacemanAvatar", + "/Lotus/Types/Friendly/RailJack/SpecterKuvaLichAAvatar", + "/Lotus/Types/Friendly/RailJack/SpecterKuvaLichFemaleAAvatar", + "/Lotus/Types/Friendly/RailJack/SpecterLawyerAAvatar", + "/Lotus/Types/Friendly/Rescue/ClemRescueAvatar", + "/Lotus/Types/Friendly/Rescue/DefenseAvatarChipper", + "/Lotus/Types/Friendly/Rescue/EventRescueAvatarFemale", + "/Lotus/Types/Friendly/Rescue/EventRescueAvatarMale", + "/Lotus/Types/Friendly/Rescue/GhoulRescueAvatar", + "/Lotus/Types/Friendly/Rescue/MarooRescueAvatar", + "/Lotus/Types/Friendly/Rescue/RescueAvatarCephalonSudaFemale", + "/Lotus/Types/Friendly/Rescue/RescueAvatarCephalonSudaMale", + "/Lotus/Types/Friendly/Rescue/RescueAvatarCorpus", + "/Lotus/Types/Friendly/Rescue/RescueAvatarDarvo", + "/Lotus/Types/Friendly/Rescue/RescueAvatarGrineer", + "/Lotus/Types/Friendly/Rescue/RescueAvatarHexisFemale", + "/Lotus/Types/Friendly/Rescue/RescueAvatarHexisMale", + "/Lotus/Types/Friendly/Rescue/RescueAvatarNewLokaFemale", + "/Lotus/Types/Friendly/Rescue/RescueAvatarNewLokaMale", + "/Lotus/Types/Friendly/Rescue/RescueAvatarPerrinFemale", + "/Lotus/Types/Friendly/Rescue/RescueAvatarPerrinMale", + "/Lotus/Types/Friendly/Rescue/RescueAvatarRedVeilFemale", + "/Lotus/Types/Friendly/Rescue/RescueAvatarRedVeilMale", + "/Lotus/Types/Friendly/Rescue/RescueAvatarSteelMeridianFemale", + "/Lotus/Types/Friendly/Rescue/RescueAvatarSteelMeridianMale", + "/Lotus/Types/Friendly/Syndicate/CacheHunt/CacheHuntRescueAvatarMale", + "/Lotus/Types/Friendly/Venus/ExploiterHeistHarvesterAvatar", + "/Lotus/Types/Friendly/Venus/LittleDuckSniperAvatar", + "/Lotus/Types/Friendly/Venus/SolarisQuestDynamicExcavatorAvatar", + "/Lotus/Types/Friendly/ArloDevotedHealerAvatar", + "/Lotus/Types/Friendly/ClemSpecterAvatar", + "/Lotus/Types/Friendly/SandRaySpecterAvatar", + "/Lotus/Types/Game/CrewShip/GrineerGalleon/GrineerGalleonTurretAvatar", + "/Lotus/Types/Gameplay/BardQuest/ScavengerHuntAvatar", + "/Lotus/Types/Gameplay/CrewShipPartsHunt/JammingDroneAvatar", + "/Lotus/Types/Gameplay/Duviri/LotusHand/LotusHandGuideAvatar", + "/Lotus/Types/Gameplay/Duviri/SideActivities/Encounters/CombatTrappedChest/DuviriThraxMeleeGuardAvatar", + "/Lotus/Types/Gameplay/Duviri/SideActivities/Encounters/DuviriPrisonerEncounter/DuviriPrisonerAvatar", + "/Lotus/Types/Gameplay/Duviri/SideActivities/Encounters/Shepherding/CattleAvatar", + "/Lotus/Types/Gameplay/EntratiLab/Quest/HumanLoidCombatAvatar", + "/Lotus/Types/Gameplay/InfestedMicroplanet/EncounterObjects/CorpusSurvivorsShieldDroneAvatar", + "/Lotus/Types/Gameplay/InfestedMicroplanet/EncounterObjects/EntratiMagicCableAvatar", + "/Lotus/Types/Gameplay/InfestedMicroplanet/EncounterObjects/InfestedMistDeviceAvatar", + "/Lotus/Types/Gameplay/InfestedMicroplanet/EncounterObjects/PurifierAvatar", + "/Lotus/Types/Gameplay/InfestedMicroplanet/EncounterObjects/ShrineDefenseAvatar", + "/Lotus/Types/Gameplay/Race/OrbiterMediumAvatar", + "/Lotus/Types/Gameplay/Race/OrbiterSmallAvatar", + "/Lotus/Types/Gameplay/Zariman/EncounterObjects/AssassinateEndless/Barracks", + "/Lotus/Types/Gameplay/Zariman/EncounterObjects/AssassinateEndless/DefenseAvatar", + "/Lotus/Types/Gameplay/Zariman/EncounterObjects/AssassinateEndless/TurretBeamAvatar", + "/Lotus/Types/Gameplay/Zariman/EncounterObjects/AssassinateEndless/TurretBossTetherAvatar", + "/Lotus/Types/Gameplay/Zariman/EncounterObjects/AssassinateEndless/TurretChainAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/BirdOfPreyAvatarResourceA", + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/CommonFemaleBirdOfPreyAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/CommonMaleBirdOfPreyAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/RareFemaleBirdOfPreyAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/RareMaleBirdOfPreyAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/UncommonFemaleBirdOfPreyAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/BirdOfPrey/UncommonMaleBirdOfPreyAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedCritter/CommonInfestedCritterAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedCritter/RareInfestedCritterAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedCritter/UncommonInfestedCritterAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedKDrive/GrottoInfKDriveAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedKDrive/HighlandInfKDriveAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedKDrive/SwampInfKDriveAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMergoo/CommonInfestedMergooAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMergoo/RareInfestedMergooAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedMergoo/UncommonInfestedMergooAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedNexifera/CommonInfestedNexiferaAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedNexifera/RareInfestedNexiferaAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedNexifera/UncommonInfestedNexiferaAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedUndazoa/CommonUndazoaAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedUndazoa/RareUndazoaAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Deimos/InfestedUndazoa/UncommonUndazoaAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Duviri/Kavat/DuviriConservationCatbrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Duviri/Rabbit/TeshinRabbitOnHandAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Duviri/Ram/DuviriRamConservationAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/Duviri/Wolf/DuviriWolfConservationAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/BaseKahlForestRodentAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/CommonMaleForestRodentAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/ForestRodentAvatarResourceA", + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/RareMaleForestRodentAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/UncommonFemaleForestRodentAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/ForestRodent/UncommonMaleForestRodentAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/CommonFemaleLegendaryKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/CommonMaleLegendaryKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/CommonPupLegendaryKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/RareFemaleLegendaryKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/RareMaleLegendaryKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/RarePupLegendaryKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/UncommonFemaleLegendaryKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/UncommonMaleLegendaryKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/LegendaryKubrow/UncommonPupLegendaryKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/CommonFemaleOrokinKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/CommonMaleOrokinKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/CommonPupOrokinKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/RareFemaleOrokinKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/RareMaleOrokinKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/RarePupOrokinKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/UncommonFemaleOrokinKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/UncommonMaleOrokinKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OrokinKubrow/UncommonPupOrokinKubrowAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/CommonFemaleOstronSeaBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/CommonMaleOstronSeaBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/RareFemaleOstronSeaBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/RareMaleOstronSeaBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/UncommonFemaleOstronSeaBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/OstronSeaBird/UncommonMaleOstronSeaBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/CommonFemaleSnowArmadilloAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/CommonMaleSnowArmadilloAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/CommonPupSnowArmadilloAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/RareFemaleSnowArmadilloAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/RareMaleSnowArmadilloAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/RarePupSnowArmadilloAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/UncommonFemaleSnowArmadilloAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/UncommonMaleSnowArmadilloAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowArmadillo/UncommonPupSnowArmadilloAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/CommonFemaleSnowBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/CommonMaleSnowBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/CommonPupSnowBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/RareFemaleSnowBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/RareMaleSnowBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/RarePupSnowBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/UncommonFemaleSnowBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/UncommonMaleSnowBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowBird/UncommonPupSnowBirdAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/CommonFemaleSnowCritterAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/CommonMaleSnowCritterAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/CommonPupSnowCritterAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/RareFemaleSnowCritterAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/RareMaleSnowCritterAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/RarePupSnowCritterAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/UncommonFemaleSnowCritterAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/UncommonMaleSnowCritterAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowCritter/UncommonPupSnowCritterAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/CommonFemaleSnowPredatorAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/CommonMaleSnowPredatorAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/CommonPupSnowPredatorAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/RareFemaleSnowPredatorAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/RareMaleSnowPredatorAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/RarePupSnowPredatorAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/UncommonFemaleSnowPredatorAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/UncommonMaleSnowPredatorAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowPredator/UncommonPupSnowPredatorAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/CommonFemaleSnowRodentAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/CommonMaleSnowRodentAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/RareFemaleSnowRodentAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/RareMaleSnowRodentAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/UncommonFemaleSnowRodentAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/SnowRodent/UncommonMaleSnowRodentAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/CommonVampireKavatCubAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/CommonVampireKavatFemaleAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/CommonVampireKavatMaleAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/RareVampireKavatCubAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/RareVampireKavatFemaleAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/RareVampireKavatMaleAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/UncommonVampireKavatCubAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/UncommonVampireKavatFemaleAvatar", + "/Lotus/Types/NeutralCreatures/Conservation/VampireKavat/UncommonVampireKavatMaleAvatar", + "/Lotus/Types/NeutralCreatures/CreatureAvatars/SandRayAvatar", + "/Lotus/Types/NeutralCreatures/ErsatzHorse/ErsatzHorseBAvatar", + "/Lotus/Types/NeutralCreatures/ErsatzHorse/ErsatzHorseCAvatar", + "/Lotus/Types/NeutralCreatures/ErsatzHorse/ErsatzHorseDAvatar", + "/Lotus/Types/NeutralCreatures/ErsatzHorse/ErsatzHorseGearSummonAvatar", + "/Lotus/Types/NeutralCreatures/ErsatzHorse/ErsatzHorseNPCAvatar", + "/Lotus/Types/NeutralCreatures/ErsatzHorse/ErsatzHorseSummonGroundDragonAvatar", + "/Lotus/Types/NeutralCreatures/ErsatzHorse/ErsatzHorseSummonItemAvatar", + "/Lotus/Types/NeutralCreatures/Kubrow/KubrowTutorialAvatar", + "/Lotus/Types/PvpBots/BasePvpBotAvatar", + "/Lotus/Types/Restoratives/Deployables/LisetLaserTurretAvatar", + "/Lotus/Types/Restoratives/Deployables/LisetTurretAvatar", + "/Lotus/Types/Sentinels/SentinelPrecepts/VoidBond/VoidCloneCatbrowPetAvatar", + "/Lotus/Types/Sentinels/SentinelPrecepts/VoidBond/VoidCloneKubrowPetAvatar", + "/Lotus/Types/Sentinels/SentinelPrecepts/VoidBond/VoidCloneSentinelAvatar", + "/Lotus/Types/Sentinels/SentinelHubAvatar", + "/Lotus/Types/Sentinels/SentinelMainMenuAvatar", + "/Lotus/Types/Vehicles/TNWBalloon/OstBalloonUnmannedAvatar", + "/Lotus/Upgrades/Mods/Sets/Strain/StrainMaggotAvatar", + "/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/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/allShipFeatures.json b/static/fixed_responses/allShipFeatures.json new file mode 100644 index 00000000..1a8a819e --- /dev/null +++ b/static/fixed_responses/allShipFeatures.json @@ -0,0 +1,57 @@ +[ + "/Lotus/Types/Items/ShipFeatureItems/AdvancedOrdisFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/AlchemyRoomFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/AlertsFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/ArsenalMeleeFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/CeresNavigationFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/ClanFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/EarthNavigationFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/EidolonArchwingFoundryUpgradeFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/ErisNavigationFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/EuropaNavigationFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/FoundryConcurrentBuildFormaFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/FoundryFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/FoundryVesselUpgradeFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/GeneticFoundryCatbrowUpgradeFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/GeneticFoundryFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/GeneticFoundryUpgradeFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/InfestedFoundryArchonShardFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/InfestedFoundryArchonShardUpgradeFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/InfestedFoundryItem", + "/Lotus/Types/Items/ShipFeatureItems/InfestedFoundryUpgradeFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/JupiterNavigationFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/MarketTierOneFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/MarketTierTwoFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/MarsNavigationFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/MercuryNavigationFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/ModsFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/ModsFusionFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/ModsTransmuteFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/NeptuneNavigationFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/PersonalQuartersFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/PhobosNavigationFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/PlutoNavigationFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/Railjack/DamagedRailjackHoodBraceFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/Railjack/DamagedRailjackHoodFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/Railjack/DamagedRailjackHullFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/Railjack/DamagedRailjackNacelleLeftFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/Railjack/DamagedRailjackNacelleRightFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/Railjack/DamagedRailjackTailFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/Railjack/RailjackHoodBraceFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/Railjack/RailjackHoodFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/Railjack/RailjackHullFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/Railjack/RailjackNacelleLeftFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/Railjack/RailjackNacelleRightFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/Railjack/RailjackTailFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/RailjackCephalonShipFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/RailjackKeyShipFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/SaturnNavigationFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/SednaNavigationFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/ShipFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/SocialMenuFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/SolarChartFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/UranusNavigationFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/VenusNavigationFeatureItem", + "/Lotus/Types/Items/ShipFeatureItems/VoidProjectionFeatureItem" +] 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/favicon.ico b/static/fixed_responses/favicon.ico new file mode 100644 index 00000000..3e261ade Binary files /dev/null and b/static/fixed_responses/favicon.ico differ diff --git a/static/fixed_responses/getSkuCatalog.json b/static/fixed_responses/getSkuCatalog.json new file mode 100644 index 00000000..b6157ea0 --- /dev/null +++ b/static/fixed_responses/getSkuCatalog.json @@ -0,0 +1,3856 @@ +{ + "Prices": [ + { + "productId": 17, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 18, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 160, + "listPrice": 19.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 257, + "listPrice": 199.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 258, + "listPrice": 99.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 259, + "listPrice": 49.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 785, + "listPrice": 19.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 786, + "listPrice": 39.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 787, + "listPrice": 79.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 861, + "listPrice": 29.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 979, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2000, + "listPrice": 49.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2001, + "listPrice": 79.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2004, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2005, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2006, + "listPrice": 79.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2007, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2008, + "listPrice": 77.77, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2009, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2010, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2011, + "listPrice": 79.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2012, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2013, + "listPrice": 77.77, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2014, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2015, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2016, + "listPrice": 79.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2017, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2018, + "listPrice": 77.77, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2019, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2020, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2021, + "listPrice": 79.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2022, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2023, + "listPrice": 77.77, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2025, + "listPrice": 149.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2026, + "listPrice": 99.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2027, + "listPrice": 49.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2028, + "listPrice": 149.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2029, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2030, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2031, + "listPrice": 79.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2032, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2033, + "listPrice": 77.77, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2035, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2037, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2038, + "listPrice": 79.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2039, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2040, + "listPrice": 77.77, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2041, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2042, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2043, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2044, + "listPrice": 79.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2045, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2046, + "listPrice": 77.77, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2047, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2049, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2050, + "listPrice": 79.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2051, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2052, + "listPrice": 77.77, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2054, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2055, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2058, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2059, + "listPrice": 79.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2060, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2061, + "listPrice": 77.77, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2062, + "listPrice": 24.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2066, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2071, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2072, + "listPrice": 19.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2075, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2081, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2085, + "listPrice": 24.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2089, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2092, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2095, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2098, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2100, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2102, + "listPrice": 15.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 2105, + "listPrice": 19.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3000, + "listPrice": 79.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3001, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3002, + "listPrice": 79.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3003, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3004, + "listPrice": 79.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3005, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3006, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3007, + "listPrice": 79.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3008, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3009, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3010, + "listPrice": 149.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3011, + "listPrice": 99.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3012, + "listPrice": 49.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3013, + "listPrice": 79.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3014, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3015, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3016, + "listPrice": 79.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3017, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3018, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3019, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3020, + "listPrice": 79.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3021, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3022, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3023, + "listPrice": 79.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3024, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3025, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3026, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3027, + "listPrice": 79.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3028, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3029, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3033, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3038, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3041, + "listPrice": 24.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3044, + "listPrice": 19.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3047, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3050, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3053, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3054, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3057, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3060, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 3063, + "listPrice": 19.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4000, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4001, + "listPrice": 77.77, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4015, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4016, + "listPrice": 77.77, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4020, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4021, + "listPrice": 77.77, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4022, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4023, + "listPrice": 77.77, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4024, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4025, + "listPrice": 77.77, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4100, + "listPrice": 19.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4101, + "listPrice": 39.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4102, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4103, + "listPrice": 99.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4105, + "listPrice": 19.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4106, + "listPrice": 39.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4107, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4108, + "listPrice": 99.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4109, + "listPrice": 19.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4110, + "listPrice": 39.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4111, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4112, + "listPrice": 99.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4113, + "listPrice": 19.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4114, + "listPrice": 39.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4115, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4116, + "listPrice": 99.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4117, + "listPrice": 19.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4118, + "listPrice": 39.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4119, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4120, + "listPrice": 99.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4121, + "listPrice": 19.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4122, + "listPrice": 39.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4123, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 4124, + "listPrice": 99.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6000, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6002, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6003, + "listPrice": 2.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6004, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6005, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6006, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6007, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6008, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6009, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6010, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6011, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6012, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6013, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6014, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6015, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6016, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6017, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6018, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6019, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6020, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6021, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6022, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6024, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6025, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6026, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6027, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6028, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6029, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6030, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6031, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6032, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6033, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6034, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6035, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6036, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6038, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6039, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6040, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6041, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6042, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6043, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6044, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6045, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6046, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6047, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6048, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6049, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6050, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6051, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6052, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6053, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6054, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6055, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6056, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6057, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6058, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6059, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6060, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6061, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6062, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6063, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6064, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6065, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6066, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6067, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6068, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6069, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6070, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6071, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6072, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6073, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6074, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6075, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6076, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6077, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6078, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6079, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6080, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6081, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6082, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6083, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6084, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6085, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6086, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6087, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6088, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6089, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6090, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6091, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6092, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6093, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6094, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6095, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6096, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6097, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6098, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6099, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6100, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6101, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6102, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6103, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6104, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6105, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6106, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6107, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6108, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6109, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6110, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6111, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6112, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6113, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6114, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6115, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6116, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6117, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6118, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6119, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6120, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6121, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6122, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6123, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6124, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6125, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6126, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6127, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6128, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6129, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6130, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6131, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6132, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6133, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6134, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6135, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6136, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6137, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6138, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6139, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6140, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6141, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6142, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6143, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6144, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6145, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6146, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6147, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6148, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6149, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6150, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6151, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6152, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6153, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6154, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6155, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6156, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6157, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6158, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6159, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6160, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6161, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6162, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6163, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6164, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6165, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6166, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6167, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6168, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6169, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6170, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6171, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6172, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6173, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6174, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6175, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6176, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6177, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6178, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6179, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6180, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6181, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6182, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6183, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6184, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6185, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6186, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6187, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6188, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6195, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6190, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6191, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6192, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6193, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6194, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6196, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6197, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6198, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6199, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6200, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6201, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6202, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6203, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6204, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6205, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6206, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6207, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6208, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6209, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6210, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6211, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6212, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6213, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6214, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6215, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6216, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6217, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6218, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6219, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6220, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6221, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6222, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6223, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6224, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6225, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6226, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6227, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6228, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6229, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6230, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6231, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6232, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6233, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6234, + "listPrice": 2.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6235, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6236, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6237, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6238, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6239, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6240, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6241, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6242, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6250, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6251, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6252, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6253, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6254, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6255, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6256, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6257, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6258, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6259, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6260, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6261, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6262, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6263, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6264, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6265, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6266, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6267, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6268, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6269, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6270, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6271, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6272, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6273, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6274, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6275, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6276, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6277, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6278, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6279, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6280, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6281, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6282, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6283, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6284, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6285, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6286, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6287, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6288, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6289, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6290, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6291, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6292, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6293, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6294, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6295, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6296, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6297, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6298, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6299, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6300, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6301, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6302, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6303, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6304, + "listPrice": 2.49, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6305, + "listPrice": 2.49, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6306, + "listPrice": 2.49, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6307, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6308, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6309, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6310, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6311, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6312, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6313, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6314, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6315, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6316, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6317, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6318, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6319, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6320, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6321, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6322, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6323, + "listPrice": 2.49, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6324, + "listPrice": 2.49, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6325, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6326, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6327, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6328, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6329, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6330, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6331, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6332, + "listPrice": 2.49, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6333, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6334, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6335, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6336, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6337, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6338, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6339, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6340, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6341, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6342, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6343, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6344, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6345, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6346, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6347, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6348, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6349, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6350, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6351, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6352, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6353, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6354, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6355, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6356, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6357, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6358, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6359, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6360, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6361, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6362, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6363, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6364, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6365, + "listPrice": 2.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6366, + "listPrice": 2.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6367, + "listPrice": 2.49, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6368, + "listPrice": 2.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6369, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6370, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6371, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6372, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6373, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6374, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6375, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6376, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6377, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6378, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6379, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6380, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6381, + "listPrice": 2.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6382, + "listPrice": 2.49, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6383, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6384, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6385, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6386, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6387, + "listPrice": 2.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6388, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6389, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6390, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6391, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6392, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6393, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6394, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6395, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6396, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6397, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6398, + "listPrice": 2.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6399, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6400, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6401, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6402, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6403, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6404, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6405, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6406, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6407, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6408, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6409, + "listPrice": 2.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6410, + "listPrice": 2.49, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6411, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6412, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6413, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6414, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6415, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6416, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6417, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6418, + "listPrice": 2.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6419, + "listPrice": 2.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6420, + "listPrice": 2.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6421, + "listPrice": 2.49, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6422, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6423, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6424, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6425, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6426, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6427, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6428, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6429, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6430, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6431, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6432, + "listPrice": 2.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6433, + "listPrice": 2.49, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6434, + "listPrice": 2.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6435, + "listPrice": 2.49, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6436, + "listPrice": 2.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6437, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6438, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6439, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6440, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6441, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6442, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6443, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6444, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6445, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6446, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6447, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6448, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6449, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6450, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6451, + "listPrice": 2.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6452, + "listPrice": 2.49, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6453, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6454, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6455, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6456, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6457, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6458, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6459, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6460, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6461, + "listPrice": 5.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6462, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6463, + "listPrice": 2.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6464, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6465, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6466, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6467, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 6468, + "listPrice": 1.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 7000, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 7001, + "listPrice": 6.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 7002, + "listPrice": 14.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 7003, + "listPrice": 39.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 7004, + "listPrice": 59.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 7005, + "listPrice": 119.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 7008, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 9000, + "listPrice": 149.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 9001, + "listPrice": 99.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 9002, + "listPrice": 49.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 9003, + "listPrice": 19.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 9004, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 9005, + "listPrice": 4.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 10000, + "listPrice": 19.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 10003, + "listPrice": 24.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 10019, + "listPrice": 29.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 10020, + "listPrice": 9.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 10024, + "listPrice": 49.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 10025, + "listPrice": 79.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 10026, + "listPrice": 139.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 10027, + "listPrice": 49.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 10028, + "listPrice": 49.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 10029, + "listPrice": 30, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 10030, + "listPrice": 60, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 10031, + "listPrice": 90, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 10032, + "listPrice": 90, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 10033, + "listPrice": 40.01, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 10034, + "listPrice": 10.01, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 10035, + "listPrice": 19.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 10036, + "listPrice": 39.99, + "currencyCode": "USD", + "owned": false + }, + { + "productId": 10037, + "listPrice": 54.99, + "currencyCode": "USD", + "owned": false + } + ] +} 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/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/questCompletionRewards.json b/static/fixed_responses/questCompletionRewards.json new file mode 100644 index 00000000..8a04e2aa --- /dev/null +++ b/static/fixed_responses/questCompletionRewards.json @@ -0,0 +1,25 @@ +{ + "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain": [ + { + "ItemType": "/Lotus/Types/NeutralCreatures/ErsatzHorse/ErsatzHorsePowerSuit", + "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 + }, + { + "ItemType": "/Lotus/Types/Items/EmailItems/TennokaiEmailItem", + "ItemCount": 1 + } + ] +} diff --git a/static/fixed_responses/worldState/baro.json b/static/fixed_responses/worldState/baro.json new file mode 100644 index 00000000..7e11cf67 --- /dev/null +++ b/static/fixed_responses/worldState/baro.json @@ -0,0 +1,444 @@ +{ + "evergreen": [ + { "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/BoosterPacks/BaroTreasureBox", "PrimePrice": 0, "RegularPrice": 50000, "Limit": 1 } + ], + "armorSets": [ + [ + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmour/BaroArmourA", "PrimePrice": 350, "RegularPrice": 110000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmour/BaroArmourC", "PrimePrice": 150, "RegularPrice": 250000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmour/BaroArmourL", "PrimePrice": 300, "RegularPrice": 150000 } + ], + [ + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmourTwo/BaroArmourTwoA", "PrimePrice": 310, "RegularPrice": 100000 }, + { "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/BaroArmourThree/BaroArmourThreeA", "PrimePrice": 400, "RegularPrice": 350000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmourThree/BaroArmourThreeC", "PrimePrice": 350, "RegularPrice": 300000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/BaroArmourThree/BaroArmourThreeL", "PrimePrice": 400, "RegularPrice": 350000 } + ], + [ + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/FurisArmor/PrismaFurisAArmor", "PrimePrice": 300, "RegularPrice": 200000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/FurisArmor/PrismaFurisCArmor", "PrimePrice": 250, "RegularPrice": 220000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/FurisArmor/PrismaFurisLArmor", "PrimePrice": 225, "RegularPrice": 175000 } + ], + [ + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/GrineerTurbines/WraithTurbinesArmArmor", "PrimePrice": 350, "RegularPrice": 150000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/GrineerTurbines/WraithTurbinesChestArmor", "PrimePrice": 300, "RegularPrice": 250000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/GrineerTurbines/WraithTurbinesLegArmor", "PrimePrice": 350, "RegularPrice": 150000 } + ], + [ + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/NecraArmor/NecraArmorA", "PrimePrice": 315, "RegularPrice": 215000 }, + { "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/SetThreeWinged/VTSetThreeArmLeftArmor", "PrimePrice": 65, "RegularPrice": 75000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetThreeWinged/VTSetThreeArmRightArmor", "PrimePrice": 65, "RegularPrice": 75000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetThreeWinged/VTSetThreeChestArmor", "PrimePrice": 150, "RegularPrice": 100000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetThreeWinged/VTSetThreeLegLeftArmor", "PrimePrice": 65, "RegularPrice": 75000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetThreeWinged/VTSetThreeLegRightArmor", "PrimePrice": 65, "RegularPrice": 75000 } + ], + [ + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetTwoSamurai/VTSetTwoArmLeftArmor", "PrimePrice": 100, "RegularPrice": 55000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetTwoSamurai/VTSetTwoArmRightArmor", "PrimePrice": 100, "RegularPrice": 55000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetTwoSamurai/VTSetTwoChestArmor", "PrimePrice": 225, "RegularPrice": 250000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetTwoSamurai/VTSetTwoLegLeftArmor", "PrimePrice": 100, "RegularPrice": 55000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/SetTwoSamurai/VTSetTwoLegRightArmor", "PrimePrice": 100, "RegularPrice": 55000 } + ], + [ + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnLatronArmor/TnLatronArmArmorElixis", "PrimePrice": 325, "RegularPrice": 220000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnLatronArmor/TnLatronArmArmorPrisma", "PrimePrice": 325, "RegularPrice": 220000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnLatronArmor/TnLatronChestArmorElixis", "PrimePrice": 275, "RegularPrice": 200000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnLatronArmor/TnLatronChestArmorPrisma", "PrimePrice": 275, "RegularPrice": 200000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnLatronArmor/TnLatronLegArmorElixis", "PrimePrice": 300, "RegularPrice": 175000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnLatronArmor/TnLatronLegArmorPrisma", "PrimePrice": 300, "RegularPrice": 175000 } + ], + [ + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnShinaiArmor/TnShinaiArmorA", "PrimePrice": 315, "RegularPrice": 125000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnShinaiArmor/TnShinaiArmorC", "PrimePrice": 300, "RegularPrice": 150000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/TnShinaiArmor/TnShinaiArmorL", "PrimePrice": 275, "RegularPrice": 115000 } + ], + [ + [{ "ItemType": "/Lotus/Types/StoreItems/Packages/VTEosArmourBundle", "PrimePrice": 285, "RegularPrice": 260000 }], + [ + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/VTEos/VTEosALArmor", "PrimePrice": 50, "RegularPrice": 75000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/VTEos/VTEosARArmor", "PrimePrice": 50, "RegularPrice": 75000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/VTEos/VTEosChestArmor", "PrimePrice": 125, "RegularPrice": 75000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/VTEos/VTEosLLArmor", "PrimePrice": 65, "RegularPrice": 50000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/VTEos/VTEosLRArmor", "PrimePrice": 65, "RegularPrice": 50000 } + ] + ] + ], + "rest": [ + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Armor/Halloween2014Wings/PrismaNaberusArmArmor", "PrimePrice": 220, "RegularPrice": 140000 }, + { "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/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/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/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/Types/StoreItems/SuitCustomizations/ColourPickerTwitchBItemA", "PrimePrice": 220, "RegularPrice": 220000 }, + { "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/Scarves/WraithTurbinesScarf", "PrimePrice": 400, "RegularPrice": 500000 }, + { "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/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/Scarves/BaroCape2Scarf", "PrimePrice": 400, "RegularPrice": 350000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Clan/BaroQuantumBadgeItem", "PrimePrice": 400, "RegularPrice": 200000 }, + { "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/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/Scarves/VTHornSkullScarf", "PrimePrice": 250, "RegularPrice": 300000 }, + { "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/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/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/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 }, + { "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageAvaClemCommunityGlyph", "PrimePrice": 20, "RegularPrice": 33333 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TennoconConcert2025Display", "PrimePrice": 90, "RegularPrice": 125000 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/SummerGameFestPoster", "PrimePrice": 90, "RegularPrice": 125000 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/RathuumEventPoster", "PrimePrice": 90, "RegularPrice": 125000 }, + { "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/Factions/GlyphFactionCorpus", "PrimePrice": 70, "RegularPrice": 55000 }, + { "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/Factions/GlyphFactionEntrati", "PrimePrice": 99, "RegularPrice": 1900 }, + { "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/Factions/GlyphFactionScaldra", "PrimePrice": 93, "RegularPrice": 1906 }, + { "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/Factions/GlyphFactionTechrot", "PrimePrice": 98, "RegularPrice": 1901 }, + { "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/Warframes/VorunaActionGlyph", "PrimePrice": 75, "RegularPrice": 60000 }, + { "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageVoidAngelBaro", "PrimePrice": 80, "RegularPrice": 50000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/1999DrippySigil", "PrimePrice": 50, "RegularPrice": 45000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Weapons/Rapier/CrpRapierSkin", "PrimePrice": 375, "RegularPrice": 400000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Events/OgrisOldSchool", "PrimePrice": 350, "RegularPrice": 325000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/PrismaLotusFlamesSigil", "PrimePrice": 55, "RegularPrice": 60000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/Expert/WeaponMeleeFactionDamageMurmursExpert", "PrimePrice": 375, "RegularPrice": 130000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/WeaponRecoilReductionModExpert", "PrimePrice": 300, "RegularPrice": 220000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Expert/WeaponRecoilReductionModExpert", "PrimePrice": 300, "RegularPrice": 220000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponRecoilReductionModExpert", "PrimePrice": 300, "RegularPrice": 220000 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/TenthAnniversaryLoginSongItem", "PrimePrice": 145, "RegularPrice": 165000 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/AbyssofDagathSongItem", "PrimePrice": 150, "RegularPrice": 155000 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/ZarimanLoginSongItem", "PrimePrice": 160, "RegularPrice": 180000 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/DanteUnboundLoginSongItem", "PrimePrice": 150, "RegularPrice": 150000 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/EmpyreanSongItem", "PrimePrice": 160, "RegularPrice": 155000 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/DeimosLoginSongItem", "PrimePrice": 155, "RegularPrice": 160000 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/JadeShadowsLoginSongItem", "PrimePrice": 150, "RegularPrice": 170000 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/WhispersInTheWallLoginSongItem", "PrimePrice": 165, "RegularPrice": 170000 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/CorpusRailjackLoginSongItem", "PrimePrice": 150, "RegularPrice": 165000 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/LotusEatersSongItem", "PrimePrice": 165, "RegularPrice": 150000 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/KuvaLichLoginSongItem", "PrimePrice": 140, "RegularPrice": 170000 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyBaro", "PrimePrice": 100, "RegularPrice": 125000 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyInaros", "PrimePrice": 120, "RegularPrice": 90000 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/WeaponPistolFactionDamageMurmursExpert", "PrimePrice": 375, "RegularPrice": 130000 } + ], + "allIfAny": [ + [ + "/Lotus/StoreItems/Upgrades/Skins/Operator/Leggings/LeggingsNovaEngineer", + "/Lotus/StoreItems/Upgrades/Skins/Operator/BodySuits/BodySuitNovaEngineer", + "/Lotus/StoreItems/Upgrades/Skins/Operator/Sleeves/SleevesNovaEngineer", + "/Lotus/StoreItems/Upgrades/Skins/Operator/Hoods/HoodNovaEngineer" + ] + ] +} diff --git a/static/fixed_responses/worldState/darvoDeals.json b/static/fixed_responses/worldState/darvoDeals.json new file mode 100644 index 00000000..598846b9 --- /dev/null +++ b/static/fixed_responses/worldState/darvoDeals.json @@ -0,0 +1,158 @@ +[ + { "StoreItem": "/Lotus/StoreItems/Powersuits/Archwing/DemolitionJetPack/DemolitionJetPack", "Discount": 60, "OriginalPrice": 275, "SalePrice": 110, "AmountTotal": 300, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Powersuits/Bard/Bard", "Discount": 30, "OriginalPrice": 225, "SalePrice": 157, "AmountTotal": 100, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Powersuits/Ember/Ember", "Discount": 30, "OriginalPrice": 225, "SalePrice": 157, "AmountTotal": 100, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Powersuits/Ember/Ember", "Discount": 60, "OriginalPrice": 225, "SalePrice": 90, "AmountTotal": 100, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Powersuits/Magician/Magician", "Discount": 20, "OriginalPrice": 200, "SalePrice": 160, "AmountTotal": 200, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Powersuits/Magician/Magician", "Discount": 30, "OriginalPrice": 200, "SalePrice": 140, "AmountTotal": 200, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Powersuits/Magician/Magician", "Discount": 40, "OriginalPrice": 200, "SalePrice": 120, "AmountTotal": 200, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Powersuits/Magician/Magician", "Discount": 50, "OriginalPrice": 200, "SalePrice": 100, "AmountTotal": 200, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Powersuits/Sandman/Sandman", "Discount": 20, "OriginalPrice": 225, "SalePrice": 180, "AmountTotal": 100, "probability": 4 }, + { "StoreItem": "/Lotus/StoreItems/Powersuits/Sandman/Sandman", "Discount": 30, "OriginalPrice": 225, "SalePrice": 157, "AmountTotal": 100, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Powersuits/Trapper/Trapper", "Discount": 40, "OriginalPrice": 300, "SalePrice": 180, "AmountTotal": 150, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Powersuits/Trapper/Trapper", "Discount": 50, "OriginalPrice": 300, "SalePrice": 150, "AmountTotal": 100, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Types/Game/CatbrowPet/CatbrowGeneticSignature", "Discount": 20, "OriginalPrice": 5, "SalePrice": 4, "AmountTotal": 500, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Types/Game/CatbrowPet/CatbrowGeneticSignature", "Discount": 30, "OriginalPrice": 5, "SalePrice": 3, "AmountTotal": 415, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Types/Game/KubrowPet/Eggs/KubrowEgg", "Discount": 50, "OriginalPrice": 10, "SalePrice": 5, "AmountTotal": 100, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Types/Game/KubrowPet/Eggs/KubrowEgg", "Discount": 60, "OriginalPrice": 10, "SalePrice": 4, "AmountTotal": 100, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/Forma", "Discount": 30, "OriginalPrice": 20, "SalePrice": 14, "AmountTotal": 150, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/Forma", "Discount": 45, "OriginalPrice": 20, "SalePrice": 11, "AmountTotal": 150, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/OrokinReactor", "Discount": 40, "OriginalPrice": 20, "SalePrice": 12, "AmountTotal": 100, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Types/Items/Research/BioComponent", "Discount": 10, "OriginalPrice": 10, "SalePrice": 9, "AmountTotal": 200, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Types/Items/Research/BioComponent", "Discount": 20, "OriginalPrice": 10, "SalePrice": 8, "AmountTotal": 165, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Types/Items/Research/BioComponent", "Discount": 30, "OriginalPrice": 10, "SalePrice": 7, "AmountTotal": 135, "probability": 5 }, + { "StoreItem": "/Lotus/StoreItems/Types/Items/Research/BioComponent", "Discount": 40, "OriginalPrice": 10, "SalePrice": 6, "AmountTotal": 100, "probability": 5 }, + { "StoreItem": "/Lotus/StoreItems/Types/Items/Research/ChemComponent", "Discount": 10, "OriginalPrice": 10, "SalePrice": 9, "AmountTotal": 200, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Types/Items/Research/ChemComponent", "Discount": 20, "OriginalPrice": 10, "SalePrice": 8, "AmountTotal": 165, "probability": 5 }, + { "StoreItem": "/Lotus/StoreItems/Types/Items/Research/ChemComponent", "Discount": 30, "OriginalPrice": 10, "SalePrice": 7, "AmountTotal": 135, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Types/Items/Research/EnergyComponent", "Discount": 10, "OriginalPrice": 10, "SalePrice": 9, "AmountTotal": 200, "probability": 5 }, + { "StoreItem": "/Lotus/StoreItems/Types/Items/Research/EnergyComponent", "Discount": 20, "OriginalPrice": 10, "SalePrice": 8, "AmountTotal": 165, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Types/Items/Research/EnergyComponent", "Discount": 30, "OriginalPrice": 10, "SalePrice": 7, "AmountTotal": 135, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Types/Items/Research/EnergyComponent", "Discount": 40, "OriginalPrice": 10, "SalePrice": 6, "AmountTotal": 100, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Upgrades/Focus/AttackLensGreater", "Discount": 10, "OriginalPrice": 40, "SalePrice": 36, "AmountTotal": 150, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Upgrades/Focus/AttackLensGreater", "Discount": 20, "OriginalPrice": 40, "SalePrice": 32, "AmountTotal": 125, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Upgrades/Focus/AttackLensGreater", "Discount": 40, "OriginalPrice": 40, "SalePrice": 24, "AmountTotal": 75, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Upgrades/Focus/DefenseLensGreater", "Discount": 10, "OriginalPrice": 40, "SalePrice": 36, "AmountTotal": 150, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Upgrades/Focus/DefenseLensGreater", "Discount": 40, "OriginalPrice": 40, "SalePrice": 24, "AmountTotal": 75, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Upgrades/Focus/DefenseLensGreater", "Discount": 50, "OriginalPrice": 40, "SalePrice": 20, "AmountTotal": 50, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Upgrades/Focus/PowerLensGreater", "Discount": 50, "OriginalPrice": 40, "SalePrice": 20, "AmountTotal": 50, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Upgrades/Focus/TacticLensGreater", "Discount": 10, "OriginalPrice": 40, "SalePrice": 36, "AmountTotal": 150, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Upgrades/Focus/TacticLensGreater", "Discount": 20, "OriginalPrice": 40, "SalePrice": 32, "AmountTotal": 125, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Upgrades/Focus/TacticLensGreater", "Discount": 30, "OriginalPrice": 40, "SalePrice": 28, "AmountTotal": 100, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Upgrades/Focus/TacticLensGreater", "Discount": 40, "OriginalPrice": 40, "SalePrice": 24, "AmountTotal": 75, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Upgrades/Focus/TacticLensGreater", "Discount": 50, "OriginalPrice": 40, "SalePrice": 20, "AmountTotal": 50, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Upgrades/Focus/WardLensGreater", "Discount": 40, "OriginalPrice": 40, "SalePrice": 24, "AmountTotal": 75, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/Bow/Longbow/CrpBow", "Discount": 20, "OriginalPrice": 235, "SalePrice": 188, "AmountTotal": 300, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/Bow/Longbow/CrpBow", "Discount": 30, "OriginalPrice": 235, "SalePrice": 164, "AmountTotal": 250, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/Bow/Longbow/CrpBow", "Discount": 50, "OriginalPrice": 235, "SalePrice": 117, "AmountTotal": 150, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/Bow/Longbow/CrpBow", "Discount": 60, "OriginalPrice": 235, "SalePrice": 94, "AmountTotal": 100, "probability": 5 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/Melee/KickAndPunch/KickPunchWeapon", "Discount": 20, "OriginalPrice": 125, "SalePrice": 100, "AmountTotal": 100, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/Melee/KickAndPunch/KickPunchWeapon", "Discount": 30, "OriginalPrice": 125, "SalePrice": 87, "AmountTotal": 90, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/Melee/KickAndPunch/KickPunchWeapon", "Discount": 60, "OriginalPrice": 125, "SalePrice": 50, "AmountTotal": 65, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/Pistols/CorpusMinigun/CorpusMinigun", "Discount": 30, "OriginalPrice": 175, "SalePrice": 122, "AmountTotal": 100, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/Pistols/CorpusMinigun/CorpusMinigun", "Discount": 40, "OriginalPrice": 175, "SalePrice": 105, "AmountTotal": 90, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/Pistols/CorpusMinigun/CorpusMinigun", "Discount": 50, "OriginalPrice": 175, "SalePrice": 87, "AmountTotal": 80, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/Pistols/CorpusMinigun/CorpusMinigun", "Discount": 60, "OriginalPrice": 175, "SalePrice": 70, "AmountTotal": 70, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/Pistols/CorpusMinigun/CorpusMinigun", "Discount": 70, "OriginalPrice": 175, "SalePrice": 52, "AmountTotal": 60, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Grineer/GrineerPistol/GrineerLightPistol", "Discount": 10, "OriginalPrice": 75, "SalePrice": 67, "AmountTotal": 100, "probability": 6 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Grineer/GrineerPistol/GrineerLightPistol", "Discount": 20, "OriginalPrice": 75, "SalePrice": 60, "AmountTotal": 100, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Grineer/GrineerPistol/GrineerLightPistol", "Discount": 30, "OriginalPrice": 75, "SalePrice": 52, "AmountTotal": 100, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Grineer/LongGuns/BurstRifle/GrnBurstRifle", "Discount": 30, "OriginalPrice": 225, "SalePrice": 157, "AmountTotal": 500, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Grineer/LongGuns/BurstRifle/GrnBurstRifle", "Discount": 40, "OriginalPrice": 225, "SalePrice": 135, "AmountTotal": 500, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Grineer/LongGuns/BurstRifle/GrnBurstRifle", "Discount": 60, "OriginalPrice": 225, "SalePrice": 90, "AmountTotal": 500, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Grineer/LongGuns/GrnSpark/GrnSparkRifle", "Discount": 20, "OriginalPrice": 150, "SalePrice": 120, "AmountTotal": 300, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Grineer/LongGuns/GrnSpark/GrnSparkRifle", "Discount": 30, "OriginalPrice": 150, "SalePrice": 105, "AmountTotal": 250, "probability": 4 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Grineer/LongGuns/GrnSpark/GrnSparkRifle", "Discount": 50, "OriginalPrice": 150, "SalePrice": 75, "AmountTotal": 150, "probability": 4 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Grineer/Melee/GrineerMachetteAndCleaver/DualCleaverWeapon", "Discount": 30, "OriginalPrice": 225, "SalePrice": 157, "AmountTotal": 200, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Grineer/Melee/GrineerMachetteAndCleaver/DualCleaverWeapon", "Discount": 40, "OriginalPrice": 225, "SalePrice": 135, "AmountTotal": 175, "probability": 4 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Grineer/Melee/GrineerMachetteAndCleaver/DualCleaverWeapon", "Discount": 50, "OriginalPrice": 225, "SalePrice": 112, "AmountTotal": 150, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Grineer/Melee/GrineerMachetteAndCleaver/DualCleaverWeapon", "Discount": 60, "OriginalPrice": 225, "SalePrice": 90, "AmountTotal": 125, "probability": 5 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Grineer/Melee/GrineerMachetteAndCleaver/DualCleaverWeapon", "Discount": 70, "OriginalPrice": 225, "SalePrice": 67, "AmountTotal": 100, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Infested/Melee/Swords/Mire/MireSword", "Discount": 10, "OriginalPrice": 150, "SalePrice": 135, "AmountTotal": 300, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Infested/Melee/Swords/Mire/MireSword", "Discount": 20, "OriginalPrice": 150, "SalePrice": 120, "AmountTotal": 270, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Infested/Melee/Swords/Mire/MireSword", "Discount": 30, "OriginalPrice": 150, "SalePrice": 105, "AmountTotal": 240, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Infested/Melee/Swords/Mire/MireSword", "Discount": 40, "OriginalPrice": 150, "SalePrice": 90, "AmountTotal": 205, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Infested/Melee/Swords/Mire/MireSword", "Discount": 60, "OriginalPrice": 150, "SalePrice": 60, "AmountTotal": 145, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Infested/Melee/Swords/Mire/MireSword", "Discount": 80, "OriginalPrice": 150, "SalePrice": 30, "AmountTotal": 80, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Akimbo/AkimboShotGun", "Discount": 20, "OriginalPrice": 225, "SalePrice": 180, "AmountTotal": 200, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Akimbo/AkimboShotGun", "Discount": 40, "OriginalPrice": 225, "SalePrice": 135, "AmountTotal": 165, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Akimbo/AkimboShotGun", "Discount": 50, "OriginalPrice": 225, "SalePrice": 112, "AmountTotal": 150, "probability": 5 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Dagger/Dagger", "Discount": 30, "OriginalPrice": 75, "SalePrice": 52, "AmountTotal": 350, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Dagger/Dagger", "Discount": 40, "OriginalPrice": 75, "SalePrice": 45, "AmountTotal": 300, "probability": 7 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Dagger/Dagger", "Discount": 50, "OriginalPrice": 75, "SalePrice": 37, "AmountTotal": 250, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Dagger/Dagger", "Discount": 60, "OriginalPrice": 75, "SalePrice": 30, "AmountTotal": 200, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/DualShortSword/DualHeatSwords", "Discount": 30, "OriginalPrice": 175, "SalePrice": 122, "AmountTotal": 200, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/DualShortSword/DualHeatSwords", "Discount": 70, "OriginalPrice": 175, "SalePrice": 52, "AmountTotal": 200, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Fist/Fist", "Discount": 10, "OriginalPrice": 125, "SalePrice": 112, "AmountTotal": 500, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Fist/Fist", "Discount": 20, "OriginalPrice": 125, "SalePrice": 100, "AmountTotal": 250, "probability": 6 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Gauntlet/Gauntlet", "Discount": 20, "OriginalPrice": 125, "SalePrice": 100, "AmountTotal": 100, "probability": 5 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Gauntlet/Gauntlet", "Discount": 30, "OriginalPrice": 125, "SalePrice": 87, "AmountTotal": 125, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Gauntlet/Gauntlet", "Discount": 40, "OriginalPrice": 125, "SalePrice": 75, "AmountTotal": 150, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Glaives/Boomerang/BoomerangWeapon", "Discount": 30, "OriginalPrice": 150, "SalePrice": 105, "AmountTotal": 300, "probability": 4 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Glaives/Boomerang/BoomerangWeapon", "Discount": 40, "OriginalPrice": 150, "SalePrice": 90, "AmountTotal": 250, "probability": 4 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Glaives/Boomerang/BoomerangWeapon", "Discount": 50, "OriginalPrice": 150, "SalePrice": 75, "AmountTotal": 200, "probability": 5 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Hammer/IceHammer/IceHammer", "Discount": 20, "OriginalPrice": 165, "SalePrice": 132, "AmountTotal": 300, "probability": 4 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Hammer/IceHammer/IceHammer", "Discount": 30, "OriginalPrice": 165, "SalePrice": 115, "AmountTotal": 250, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Hammer/IceHammer/IceHammer", "Discount": 40, "OriginalPrice": 165, "SalePrice": 99, "AmountTotal": 200, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Hammer/IceHammer/IceHammer", "Discount": 50, "OriginalPrice": 165, "SalePrice": 82, "AmountTotal": 150, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Hammer/IceHammer/IceHammer", "Discount": 60, "OriginalPrice": 165, "SalePrice": 66, "AmountTotal": 100, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/LongSword/LongSword", "Discount": 50, "OriginalPrice": 150, "SalePrice": 75, "AmountTotal": 300, "probability": 4 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/LongSword/LongSword", "Discount": 60, "OriginalPrice": 150, "SalePrice": 60, "AmountTotal": 265, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/LongSword/LongSword", "Discount": 70, "OriginalPrice": 150, "SalePrice": 45, "AmountTotal": 225, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/LongSword/LongSword", "Discount": 90, "OriginalPrice": 150, "SalePrice": 15, "AmountTotal": 150, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Scythe/EtherScytheWeapon", "Discount": 40, "OriginalPrice": 230, "SalePrice": 138, "AmountTotal": 250, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Scythe/EtherScytheWeapon", "Discount": 60, "OriginalPrice": 230, "SalePrice": 92, "AmountTotal": 150, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/GreatSword/TennoGreatSword", "Discount": 20, "OriginalPrice": 175, "SalePrice": 140, "AmountTotal": 100, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/GreatSword/TennoGreatSword", "Discount": 30, "OriginalPrice": 175, "SalePrice": 122, "AmountTotal": 100, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/GreatSword/TennoGreatSword", "Discount": 40, "OriginalPrice": 175, "SalePrice": 105, "AmountTotal": 100, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/GreatSword/TennoGreatSword", "Discount": 90, "OriginalPrice": 175, "SalePrice": 17, "AmountTotal": 100, "probability": 1 }, + { + "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/SwordsAndBoards/MeleeContestWinnerOne/TennoSwordShield", + "Discount": 30, + "OriginalPrice": 150, + "SalePrice": 105, + "AmountTotal": 100, + "probability": 1 + }, + { + "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/SwordsAndBoards/MeleeContestWinnerOne/TennoSwordShield", + "Discount": 70, + "OriginalPrice": 150, + "SalePrice": 45, + "AmountTotal": 100, + "probability": 1 + }, + { + "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/SwordsAndBoards/MeleeContestWinnerOne/TennoSwordShield", + "Discount": 90, + "OriginalPrice": 150, + "SalePrice": 15, + "AmountTotal": 100, + "probability": 1 + }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Pistol/CrossBow", "Discount": 30, "OriginalPrice": 175, "SalePrice": 122, "AmountTotal": 300, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Pistol/CrossBow", "Discount": 40, "OriginalPrice": 175, "SalePrice": 105, "AmountTotal": 250, "probability": 6 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Pistol/CrossBow", "Discount": 50, "OriginalPrice": 175, "SalePrice": 87, "AmountTotal": 200, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Pistol/CrossBow", "Discount": 60, "OriginalPrice": 175, "SalePrice": 70, "AmountTotal": 150, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Pistol/HandShotGun", "Discount": 20, "OriginalPrice": 190, "SalePrice": 152, "AmountTotal": 300, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Pistol/HandShotGun", "Discount": 30, "OriginalPrice": 190, "SalePrice": 133, "AmountTotal": 200, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Pistol/HandShotGun", "Discount": 40, "OriginalPrice": 190, "SalePrice": 114, "AmountTotal": 100, "probability": 5 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Pistol/RevolverPistol", "Discount": 20, "OriginalPrice": 190, "SalePrice": 152, "AmountTotal": 200, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Pistol/RevolverPistol", "Discount": 30, "OriginalPrice": 190, "SalePrice": 133, "AmountTotal": 150, "probability": 4 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Pistol/RevolverPistol", "Discount": 40, "OriginalPrice": 190, "SalePrice": 114, "AmountTotal": 100, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Pistol/RevolverPistol", "Discount": 50, "OriginalPrice": 190, "SalePrice": 95, "AmountTotal": 50, "probability": 4 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Pistols/TnBardPistol/TnBardPistolGun", "Discount": 20, "OriginalPrice": 190, "SalePrice": 152, "AmountTotal": 300, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Pistols/TnBardPistol/TnBardPistolGun", "Discount": 30, "OriginalPrice": 190, "SalePrice": 133, "AmountTotal": 250, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Pistols/TnBardPistol/TnBardPistolGun", "Discount": 40, "OriginalPrice": 190, "SalePrice": 114, "AmountTotal": 200, "probability": 2 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Pistols/TnBardPistol/TnBardPistolGun", "Discount": 50, "OriginalPrice": 190, "SalePrice": 95, "AmountTotal": 150, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Pistols/TnBardPistol/TnBardPistolGun", "Discount": 60, "OriginalPrice": 190, "SalePrice": 76, "AmountTotal": 100, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Rifle/TennoSniperRifle", "Discount": 10, "OriginalPrice": 250, "SalePrice": 225, "AmountTotal": 100, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Rifle/TennoSniperRifle", "Discount": 30, "OriginalPrice": 250, "SalePrice": 175, "AmountTotal": 100, "probability": 3 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Rifle/TennoSniperRifle", "Discount": 50, "OriginalPrice": 250, "SalePrice": 125, "AmountTotal": 100, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Shotgun/QuadShotgun", "Discount": 50, "OriginalPrice": 225, "SalePrice": 112, "AmountTotal": 100, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Shotgun/QuadShotgun", "Discount": 70, "OriginalPrice": 225, "SalePrice": 67, "AmountTotal": 100, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/ThrowingWeapons/Kunai", "Discount": 10, "OriginalPrice": 175, "SalePrice": 157, "AmountTotal": 100, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/ThrowingWeapons/Kunai", "Discount": 20, "OriginalPrice": 175, "SalePrice": 140, "AmountTotal": 100, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/ThrowingWeapons/Kunai", "Discount": 30, "OriginalPrice": 175, "SalePrice": 122, "AmountTotal": 100, "probability": 1 }, + { "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/ThrowingWeapons/Kunai", "Discount": 40, "OriginalPrice": 175, "SalePrice": 105, "AmountTotal": 100, "probability": 2 } +] diff --git a/static/fixed_responses/worldState/fissureMissions.json b/static/fixed_responses/worldState/fissureMissions.json new file mode 100644 index 00000000..9b5c85e3 --- /dev/null +++ b/static/fixed_responses/worldState/fissureMissions.json @@ -0,0 +1,154 @@ +{ + "VoidT1": [ + "SolNode23", + "SolNode66", + "SolNode45", + "SolNode41", + "SolNode59", + "SolNode39", + "SolNode75", + "SolNode113", + "SolNode85", + "SolNode58", + "SolNode101", + "SolNode109", + "SolNode26", + "SolNode15", + "SolNode61", + "SolNode123", + "SolNode16", + "SolNode79", + "SolNode2", + "SolNode22", + "SolNode68", + "SolNode89", + "SolNode11", + "SolNode46", + "SolNode36", + "SolNode27", + "SolNode14", + "SolNode106", + "SolNode30", + "SolNode107", + "SolNode63", + "SolNode128" + ], + "VoidT2": [ + "SolNode141", + "SolNode149", + "SolNode10", + "SolNode93", + "SettlementNode11", + "SolNode137", + "SolNode132", + "SolNode73", + "SolNode82", + "SolNode25", + "SolNode88", + "SolNode126", + "SolNode135", + "SolNode74", + "SettlementNode15", + "SolNode147", + "SolNode67", + "SolNode20", + "SolNode42", + "SolNode18", + "SolNode31", + "SolNode139", + "SettlementNode12", + "SolNode100", + "SolNode140", + "SolNode70", + "SettlementNode1", + "SettlementNode14", + "SolNode50", + "SettlementNode2", + "SolNode146", + "SettlementNode3", + "SolNode97", + "SolNode125", + "SolNode19", + "SolNode121", + "SolNode96", + "SolNode131" + ], + "VoidT3": [ + "SolNode62", + "SolNode17", + "SolNode403", + "SolNode6", + "SolNode118", + "SolNode211", + "SolNode217", + "SolNode401", + "SolNode64", + "SolNode405", + "SolNode84", + "SolNode402", + "SolNode408", + "SolNode122", + "SolNode57", + "SolNode216", + "SolNode205", + "SolNode215", + "SolNode404", + "SolNode209", + "SolNode406", + "SolNode204", + "SolNode203", + "SolNode409", + "SolNode400", + "SolNode212", + "SolNode1", + "SolNode412", + "SolNode49", + "SolNode78", + "SolNode410", + "SolNode407", + "SolNode220" + ], + "VoidT4": [ + "SolNode188", + "SolNode403", + "SolNode189", + "SolNode21", + "SolNode102", + "SolNode171", + "SolNode196", + "SolNode184", + "SolNode185", + "SolNode76", + "SolNode195", + "SolNode164", + "SolNode401", + "SolNode405", + "SolNode56", + "SolNode402", + "SolNode408", + "SolNode4", + "SolNode181", + "SolNode406", + "SolNode162", + "SolNode72", + "SolNode407", + "SolNode177", + "SolNode404", + "SolNode400", + "SolNode409", + "SolNode43", + "SolNode166", + "SolNode172", + "SolNode412", + "SolNode187", + "SolNode38", + "SolNode175", + "SolNode81", + "SolNode48", + "SolNode410", + "SolNode153", + "SolNode173" + ], + "VoidT5": ["SolNode747", "SolNode743", "SolNode742", "SolNode744", "SolNode745", "SolNode748", "SolNode746", "SolNode741"], + "VoidT6": ["SolNode717", "SolNode309", "SolNode718", "SolNode232", "SolNode230", "SolNode310"] +} diff --git a/static/fixed_responses/worldState/invasionNodes.json b/static/fixed_responses/worldState/invasionNodes.json new file mode 100644 index 00000000..47429eac --- /dev/null +++ b/static/fixed_responses/worldState/invasionNodes.json @@ -0,0 +1,114 @@ +{ + "FC_CORPUS": [ + "SettlementNode1", + "SettlementNode2", + "SettlementNode3", + "SettlementNode11", + "SettlementNode12", + "SettlementNode14", + "SettlementNode15", + "SettlementNode20", + "SolNode1", + "SolNode2", + "SolNode4", + "SolNode6", + "SolNode10", + "SolNode17", + "SolNode21", + "SolNode22", + "SolNode23", + "SolNode25", + "SolNode38", + "SolNode43", + "SolNode48", + "SolNode49", + "SolNode51", + "SolNode53", + "SolNode56", + "SolNode57", + "SolNode61", + "SolNode62", + "SolNode65", + "SolNode66", + "SolNode72", + "SolNode73", + "SolNode74", + "SolNode76", + "SolNode78", + "SolNode81", + "SolNode84", + "SolNode88", + "SolNode97", + "SolNode100", + "SolNode101", + "SolNode102", + "SolNode104", + "SolNode107", + "SolNode109", + "SolNode118", + "SolNode121", + "SolNode123", + "SolNode125", + "SolNode126", + "SolNode127", + "SolNode128", + "SolNode203", + "SolNode205", + "SolNode209", + "SolNode210", + "SolNode211", + "SolNode212", + "SolNode214", + "SolNode216", + "SolNode217", + "SolNode220" + ], + "FC_GRINEER": [ + "SolNode11", + "SolNode16", + "SolNode18", + "SolNode19", + "SolNode20", + "SolNode30", + "SolNode31", + "SolNode32", + "SolNode36", + "SolNode41", + "SolNode42", + "SolNode45", + "SolNode46", + "SolNode50", + "SolNode58", + "SolNode67", + "SolNode68", + "SolNode70", + "SolNode82", + "SolNode93", + "SolNode96", + "SolNode99", + "SolNode106", + "SolNode113", + "SolNode131", + "SolNode132", + "SolNode135", + "SolNode137", + "SolNode138", + "SolNode139", + "SolNode140", + "SolNode141", + "SolNode144", + "SolNode146", + "SolNode147", + "SolNode149", + "SolNode177", + "SolNode181", + "SolNode184", + "SolNode185", + "SolNode187", + "SolNode188", + "SolNode189", + "SolNode191", + "SolNode195", + "SolNode196" + ] +} diff --git a/static/fixed_responses/worldState/invasionRewards.json b/static/fixed_responses/worldState/invasionRewards.json new file mode 100644 index 00000000..0812c342 --- /dev/null +++ b/static/fixed_responses/worldState/invasionRewards.json @@ -0,0 +1,190 @@ +{ + "FC_GRINEER": { + "COMMON": [ + { + "ItemType": "/Lotus/Types/Items/Research/ChemComponent", + "ItemCount": 3 + } + ], + "UNCOMMON": [ + { + "ItemType": "/Lotus/Types/Recipes/Weapons/KarakWraithBlueprint", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/KarakWraithBarrel", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/KarakWraithReceiver", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/KarakWraithStock", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/StrunWraithBlueprint", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/StrunWraithBarrel", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/StrunWraithReceiver", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/StrunWraithStock", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/LatronWraithBlueprint", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/LatronWraithBarrel", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/LatronWraithReceiver", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/LatronWraithStock", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/TwinVipersWraithBlueprint", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/TwinVipersWraithBarrel", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/TwinVipersWraithLink", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/TwinVipersWraithReceiver", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/GrineerCombatKnifeSortieBlueprint", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/GrineerCombatKnifeHilt", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/GrineerCombatKnifeBlade", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/GrineerCombatKnifeHeatsink", + "ItemCount": 1 + } + ], + "RARE": [ + { + "ItemType": "/Lotus/Types/Recipes/Components/OrokinCatalystBlueprint", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Components/OrokinReactorBlueprint", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Components/FormaBlueprint", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Components/UtilityUnlockerBlueprint", + "ItemCount": 1 + } + ] + }, + "FC_CORPUS": { + "COMMON": [ + { + "ItemType": "/Lotus/Types/Items/Research/EnergyComponent", + "ItemCount": 3 + } + ], + "UNCOMMON": [ + { + "ItemType": "/Lotus/Types/Recipes/Weapons/DeraVandalBlueprint", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/DeraVandalBarrel", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/DeraVandalReceiver", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/DeraVandalStock", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/SnipetronVandalBlueprint", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/SnipetronVandalStock", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/SnipetronVandalReceiver", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/SnipetronVandalBarrel", + "ItemCount": 1 + } + ], + "RARE": [ + { + "ItemType": "/Lotus/Types/Recipes/Components/OrokinCatalystBlueprint", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Components/OrokinReactorBlueprint", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Components/FormaBlueprint", + "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Recipes/Components/UtilityUnlockerBlueprint", + "ItemCount": 1 + } + ] + }, + "FC_INFESTATION": { + "COMMON": [ + { + "ItemType": "/Lotus/Types/Items/Research/BioComponent", + "ItemCount": 1 + } + ], + "UNCOMMON": [ + { + "ItemType": "/Lotus/Types/Items/Research/BioComponent", + "ItemCount": 2 + } + ], + "RARE": [ + { + "ItemType": "/Lotus/Types/Items/MiscItems/InfestedAladCoordinate", + "ItemCount": 1 + } + ] + } +} diff --git a/static/fixed_responses/worldState/pvpChallenges.json b/static/fixed_responses/worldState/pvpChallenges.json new file mode 100644 index 00000000..36cf9288 --- /dev/null +++ b/static/fixed_responses/worldState/pvpChallenges.json @@ -0,0 +1,290 @@ +{ + "/Lotus/PVPChallengeTypes/PVPTimedChallengeFlagCaptureEASY": { + "ScriptParamValue": 1, + "PVPModeAllowed": ["PVPMODE_CAPTURETHEFLAG"], + "SyndicateXP": 500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeFlagCaptureMEDIUM": { + "ScriptParamValue": 4, + "PVPModeAllowed": ["PVPMODE_CAPTURETHEFLAG"], + "SyndicateXP": 1500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeFlagReturnEASY": { + "ScriptParamValue": 1, + "PVPModeAllowed": ["PVPMODE_CAPTURETHEFLAG"], + "SyndicateXP": 500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsComboEASY": { + "ScriptParamValue": 1, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsComboMEDIUM": { + "ScriptParamValue": 4, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 1500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsHeadShotsEASY": { + "ScriptParamValue": 1, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsHeadShotsMEDIUM": { + "ScriptParamValue": 4, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 1500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsMeleeEASY": { + "ScriptParamValue": 1, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsMeleeMEDIUM": { + "ScriptParamValue": 4, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 1500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsMeleeHARD": { + "ScriptParamValue": 3, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 3000, + "DuringSingleMatch": true + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsMultiMEDIUM": { + "ScriptParamValue": 4, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 1500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsPaybackEASY": { + "ScriptParamValue": 1, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsPayback_MEDIUM": { + "ScriptParamValue": 3, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 1500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsPowerEASY": { + "ScriptParamValue": 1, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsPowerMEDIUM": { + "ScriptParamValue": 4, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 1500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsPowerHARD": { + "ScriptParamValue": 3, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 3000, + "DuringSingleMatch": true + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsPrimaryEASY": { + "ScriptParamValue": 1, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsPrimaryMEDIUM": { + "ScriptParamValue": 4, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 1500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsPrimaryHARD": { + "ScriptParamValue": 3, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 3000, + "DuringSingleMatch": true + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsSecondaryEASY": { + "ScriptParamValue": 1, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsSecondaryMEDIUM": { + "ScriptParamValue": 4, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 1500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsSecondaryHARD": { + "ScriptParamValue": 3, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 3000, + "DuringSingleMatch": true + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsStreak_MEDIUM": { + "ScriptParamValue": 3, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 1500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsStreakDominationEASY": { + "ScriptParamValue": 1, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsStreakDomination_MEDIUM": { + "ScriptParamValue": 3, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 1500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsStreakDominationHARD": { + "ScriptParamValue": 3, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 3000, + "DuringSingleMatch": true + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsStreakStoppedEASY": { + "ScriptParamValue": 1, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsStreakStopped_MEDIUM": { + "ScriptParamValue": 3, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 1500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsStreakHARD": { + "ScriptParamValue": 2, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 3000, + "DuringSingleMatch": true + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsTargetInAirEASY": { + "ScriptParamValue": 1, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsTargetInAirMEDIUM": { + "ScriptParamValue": 4, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 1500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsTargetInAirHARD": { + "ScriptParamValue": 3, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 3000, + "DuringSingleMatch": true + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsWhileSlidingEASY": { + "ScriptParamValue": 1, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsWhileSlidingMEDIUM": { + "ScriptParamValue": 4, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 1500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsWhileSlidingHARD": { + "ScriptParamValue": 3, + "PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 3000, + "DuringSingleMatch": true + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeMatchCompleteEASY": { + "ScriptParamValue": 1, + "PVPModeAllowed": ["PVPMODE_CAPTURETHEFLAG", "PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeMatchCompleteMEDIUM": { + "ScriptParamValue": 4, + "PVPModeAllowed": ["PVPMODE_CAPTURETHEFLAG", "PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"], + "SyndicateXP": 1500 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballCatchesEASY": { + "ScriptParamValue": 3, + "PVPModeAllowed": ["PVPMODE_SPEEDBALL"], + "SyndicateXP": 1000 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballCatchesMEDIUM": { + "ScriptParamValue": 10, + "PVPModeAllowed": ["PVPMODE_SPEEDBALL"], + "SyndicateXP": 3000 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballCatchesHARD": { + "ScriptParamValue": 6, + "PVPModeAllowed": ["PVPMODE_SPEEDBALL"], + "SyndicateXP": 6000, + "DuringSingleMatch": true + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballChecksEASY": { + "ScriptParamValue": 3, + "PVPModeAllowed": ["PVPMODE_SPEEDBALL"], + "SyndicateXP": 1000 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballChecksMEDIUM": { + "ScriptParamValue": 10, + "PVPModeAllowed": ["PVPMODE_SPEEDBALL"], + "SyndicateXP": 3000 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballChecksHARD": { + "ScriptParamValue": 6, + "PVPModeAllowed": ["PVPMODE_SPEEDBALL"], + "SyndicateXP": 6000, + "DuringSingleMatch": true + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballGoalsEASY": { + "ScriptParamValue": 2, + "PVPModeAllowed": ["PVPMODE_SPEEDBALL"], + "SyndicateXP": 1000 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballGoalsMEDIUM": { + "ScriptParamValue": 6, + "PVPModeAllowed": ["PVPMODE_SPEEDBALL"], + "SyndicateXP": 3000 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballGoalsHARD": { + "ScriptParamValue": 4, + "PVPModeAllowed": ["PVPMODE_SPEEDBALL"], + "SyndicateXP": 6000, + "DuringSingleMatch": true + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballInterceptionsEASY": { + "ScriptParamValue": 3, + "PVPModeAllowed": ["PVPMODE_SPEEDBALL"], + "SyndicateXP": 1000 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballInterceptionsMEDIUM": { + "ScriptParamValue": 6, + "PVPModeAllowed": ["PVPMODE_SPEEDBALL"], + "SyndicateXP": 3000 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballInterceptionsHARD": { + "ScriptParamValue": 6, + "PVPModeAllowed": ["PVPMODE_SPEEDBALL"], + "SyndicateXP": 6000, + "DuringSingleMatch": true + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballPassesEASY": { + "ScriptParamValue": 3, + "PVPModeAllowed": ["PVPMODE_SPEEDBALL"], + "SyndicateXP": 1000 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballPassesMEDIUM": { + "ScriptParamValue": 6, + "PVPModeAllowed": ["PVPMODE_SPEEDBALL"], + "SyndicateXP": 3000 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballPassesHARD": { + "ScriptParamValue": 3, + "PVPModeAllowed": ["PVPMODE_SPEEDBALL"], + "SyndicateXP": 6000, + "DuringSingleMatch": true + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballStealsEASY": { + "ScriptParamValue": 3, + "PVPModeAllowed": ["PVPMODE_SPEEDBALL"], + "SyndicateXP": 1000 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballStealsMEDIUM": { + "ScriptParamValue": 6, + "PVPModeAllowed": ["PVPMODE_SPEEDBALL"], + "SyndicateXP": 3000 + }, + "/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballStealsHARD": { + "ScriptParamValue": 3, + "PVPModeAllowed": ["PVPMODE_SPEEDBALL"], + "SyndicateXP": 6000 + } +} 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/varzia.json b/static/fixed_responses/worldState/varzia.json new file mode 100644 index 00000000..8073d7e2 --- /dev/null +++ b/static/fixed_responses/worldState/varzia.json @@ -0,0 +1,1392 @@ +{ + "evergreen": [ + { "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Rifle/BratonPrime", "PrimePrice": 1 }, + { "ItemType": "/Lotus/StoreItems/Weapons/Tenno/LongGuns/PrimeBurston/PrimeBurston", "PrimePrice": 2 }, + { "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/DualDagger/FangPrimeDagger", "PrimePrice": 2 }, + { "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Pistols/PrimeLex/PrimeLex", "PrimePrice": 1 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/PrimeTwitchScarf", "PrimePrice": 2 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/TwitchPrimeScarf", "PrimePrice": 2 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MeleeDangles/TwitchPrimeMeleeDangle", "PrimePrice": 1 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Liset/LisetSkinTwitchPrime", "RegularPrice": 10 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/TwitchPrimeSigil", "PrimePrice": 1 }, + { "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVNecraloidBundle", "RegularPrice": 10 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Clan/TwitchNecraloidBadgeItem", "RegularPrice": 5 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/InfMembraneCape", "RegularPrice": 10 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/AmazonOniSyandana", "RegularPrice": 10 }, + { "ItemType": "/Lotus/StoreItems/Types/Game/ShipScenes/PrimeLisetFiligreeScene", "PrimePrice": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/Game/ShipScenes/CorpusShipScene", "RegularPrice": 7 }, + { "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVVayasPrimeAccessories", "PrimePrice": 2 }, + { "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVAviaPrimeArmorSet", "PrimePrice": 2 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/PrimeAviaSyandana", "PrimePrice": 2 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/LasBackpackMedkitSyandana", "PrimePrice": 1 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Effects/TwitchEphemera", "PrimePrice": 1 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Loki/LokiTwitchSkin", "PrimePrice": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageLokiActionTwitch", "RegularPrice": 5 }, + { "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVVervArmorSet", "PrimePrice": 1 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Promo/Twitch/LisetSkinTwitch", "RegularPrice": 10 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/TwitchPromo2021Sigil", "RegularPrice": 5 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Clan/TwitchPromo2021BadgeItem", "RegularPrice": 5 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Weapons/Redeemer/RedeemerTwitchSkin", "RegularPrice": 7 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Promo/Twitch/Twitch2021AfurisSkin", "RegularPrice": 7 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Promo/Twitch/TwitchRubicoSkin", "RegularPrice": 7 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Promo/Twitch/TwitchPentaSkin", "RegularPrice": 7 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/Twitch2021Syandana", "RegularPrice": 10 }, + { "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVVervSentrexSentAccessories", "RegularPrice": 7 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Kubrows/Armor/Twitch2021IfritKubrowArmor", "RegularPrice": 7 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Catbrows/Armor/Twitch2021MyrdinCatbrowArmor", "RegularPrice": 7 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropCleaningDroneTwitch", "RegularPrice": 7 }, + { "ItemType": "/Lotus/StoreItems/Types/Game/QuartersWallpapers/TwitchPrimeWallpaper", "RegularPrice": 5 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/FlameScarfRefresh", "RegularPrice": 10 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MeleeDangles/FireMeleeDangleRefresh", "RegularPrice": 6 }, + { "ItemType": "/Lotus/Types/StoreItems/Packages/PrimeColorPackA", "PrimePrice": 1 }, + { "ItemType": "/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/ColourPickerPrimeDayItemA", "RegularPrice": 5 }, + { "ItemType": "/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/ColourPickerTwitchItemC", "RegularPrice": 5 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Promo/Twitch/TigrisTwitchSkin", "RegularPrice": 7 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Promo/Twitch/TwitchAnkyros", "RegularPrice": 7 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/TwitchProminenceSigil", "RegularPrice": 5 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Promo/Twitch/ExcaliburTwitchSkin", "RegularPrice": 12 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Volt/VoltTwitchSkin", "RegularPrice": 12 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/TnLargeCapeTwitch", "RegularPrice": 10 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Bard/BardTwitchSkin", "PrimePrice": 1 }, + { "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVIridosArmorSet", "RegularPrice": 10 }, + { "ItemType": "/Lotus/StoreItems/Characters/Tenno/Accessory/Scarves/U17IntermScarf/IridosUdyatSkin/UdyatPrimeGamingSyandana", "RegularPrice": 10 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Promo/Twitch/PyranaTwitchSkin", "RegularPrice": 7 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Promo/Twitch/OgrisTwitchSkin", "RegularPrice": 7 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Weapons/Tonfa/KronenTwitchSkin", "RegularPrice": 7 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Promo/Twitch/AkjagaraIridosSkin", "RegularPrice": 7 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Liset/LisetInsectSkinIridos", "RegularPrice": 10 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Necramech/TefilahIridosSkin", "RegularPrice": 10 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropShawzinTwitch", "RegularPrice": 7 }, + { "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageOctaviaActionTwitch", "RegularPrice": 5 }, + { "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Effects/OvergrowthEphemera", "RegularPrice": 10 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/ResourceDecoItemCetusWispTwitch", "RegularPrice": 7 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/GaussPrimeSongItem", "RegularPrice": 5 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/GaraPrimeSongItem", "RegularPrice": 5 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/GrendelPrimeSongItem", "RegularPrice": 5 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/HildrynPrimeSongItem", "RegularPrice": 5 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/HydroidPrimeSongItem", "RegularPrice": 5 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/KhoraPrimeSongItem", "RegularPrice": 5 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/NekrosPrimeSongItem", "RegularPrice": 5 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/NidusPrimeSongItem", "RegularPrice": 5 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/OberonPrimeSongItem", "RegularPrice": 5 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/OctaviaPrimeSongItem", "RegularPrice": 5 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/RevenantPrimeSongItem", "RegularPrice": 5 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/VaubanPrimeSongItem", "RegularPrice": 5 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/ProteaPrimeSongItem", "RegularPrice": 5 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/WispPrimeSongItem", "RegularPrice": 5 }, + { "ItemType": "/Lotus/StoreItems/Types/Items/MiscItems/PrimeBucks", "RegularPrice": 1 }, + { "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVVoidTraceBundle", "RegularPrice": 1 } + ], + "primeSinglePacks": [ + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVRevenantPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Revenant/RevenantPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/LongGuns/PrimePhantasma/PhantasmaPrimeShotgun", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/PrimeTatsu/PrimeTatsuWeapon", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/PrimeRevenantCape", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVVetalaPrimeArmorSet", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/RevenantPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVBaruukPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Pacifist/BaruukPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Pistols/PrimeAfuris/PrimeAFurisWeapon", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/SwordsAndBoards/PrimeCobraAndCrane/PrimeCobraAndCraneWeapon", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/PrimeAkrabuSyandana", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Effects/BaruukPrimeEphemera", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaruukPrimePattern", + "PrimePrice": 1 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/BaruukPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVNekrosPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Necro/NekrosPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/LongGuns/PrimeTigris/PrimeTigris", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/PrimeGalatine/PrimeGalatine", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVAcanthusPrimeArmorSet", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/UruPrimeScarf", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/NekrosPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVOberonPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Paladin/PaladinPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/LongGuns/PrimeSybaris/PrimeSybarisRifle", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/PrimeSilvaAegis/PrimeSilvaAegis", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MeleeDangles/SurakaPrimeDangle", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/PrimeOberonCape", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/OberonPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVBansheePrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Banshee/BansheePrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/PrimeScarfF", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVIctusPrimeSentAccessories", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Sentinels/SentinelPowersuits/PrimeHeliosPowerSuit", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Pistols/AllNew1hSG/AllNew1hSG", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/BansheePrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVMiragePrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Banshee/MiragePrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/PrimeScarfG", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVAtavistPrimeArmorSet", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/PrimeKogake/KogakePrimeKnuckles", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Pistols/PrimeAkbolto/PrimeAkBoltoWeapon", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/MiragePrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVNidusPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Infestation/InfestationPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Game/ShipScenes/NidusPrimeScene", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/NidusPrimeSyandana", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Necramech/InfestedNecraMechSkin", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/LongGuns/PrimeStrun/PrimeStrunWeapon", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Pistols/PrimeMagnus/PrimeMagnusWeapon", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/InfestationPrimeShipMaggot", + "PrimePrice": 1 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/NidusPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVSarynPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Saryn/SarynPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Characters/Tenno/Accessory/Scarves/PrimeScarfD/Cloth/PrimeScarfDItem", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/PrimeAccessSigilSaryn", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/PrimeKatana/PrimeNikana", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/ThrowingWeapons/PrimeLiDagger/PrimeLiDagger", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/SarynPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVEmberPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Ember/EmberPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVTitanPrimeSet", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Glaives/PrimeGlaive/PrimeGlaiveWeapon", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/PrimeFlameScarf", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Pistols/PrimeSicarus/PrimeSicarusPistol", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/EmberPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVRhinoPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Rhino/RhinoPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/NoruPrimeScarf", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVDistillingExtractorPrimeSet", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/LongGuns/PrimeBoltor/PrimeBoltor", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Gauntlet/PrimeAnkyros/PrimeAnkyros", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/RhinoPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVInarosPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Sandman/InarosPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVMittahkPrimeArmorSet", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/PrimeInarosSyandana", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/LongGuns/PrimePanthera/PrimePanthera", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/PrimeKaryst/PrimeKrisDagger", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/InarosPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVAshPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Ninja/AshPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVUndaPrimeSentAccessories", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/YamakoPrimeScarf", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Sentinels/SentinelPowersuits/PrimeCarrierPowerSuit", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/LongGuns/PrimeVectis/PrimeVectisRifle", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/AshPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVMagPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Mag/MagPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Shotgun/PrimeBoar", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/CronusSword/PrimeCronusLongSword", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVTargisPrimeArmorSet", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVDistillingExtractorPrimeSet", + "PrimePrice": 1 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/MagPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVFrostPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Frost/FrostPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVTitanPrimeSet", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/PrimeScarf", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Scythe/ReaperWeapon", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Rifle/LatronPrime", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/FrostPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVEquinoxPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/YinYang/EquinoxPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/PrimeCapeEquinox", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVNarvarrPrimeArmorSet", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/LongGuns/PrimeStradavar/PrimeStradavarGun", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Staff/TipedoPrime/TipedoPrimeWeapon", + "PrimePrice": 2 + } + ], + "BobbleHeads": [ + "/Lotus/StoreItems/Types/Items/ShipDecos/EquinoxPrimeBobbleHead", + "/Lotus/StoreItems/Types/Items/ShipDecos/EquinoxPrimeNightBobbleHead", + "/Lotus/StoreItems/Types/Items/ShipDecos/EquinoxPrimeDayBobbleHead" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVWukongPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/MonkeyKing/WukongPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/PrimeWukongSyandana", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVWukongPrimeKubrowArmor", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Effects/EphemeraPrimeA", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/LongGuns/PrimeZhuge/PrimeZhugeCrossbow", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/PrimeNinkondi/PrimeNikondi", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/WukongPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVTitaniaPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Fairy/TitaniaPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/TitaniaPrimeSyandana", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Archwing/TitaniaPrimeArchwingSkin", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/PrimePangolinSword/PrimePangolinSword", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/LongGuns/PrimeCorinth/PrimeCorinth", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/TitaniaPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVGaraPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Glass/GaraPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Effects/EphemeraGaraPrime", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVCastellanPrimeKavatArmor", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/LongGuns/PrimeAstilla/AstillaPrimeWeapon", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/PrimeVolnus/VolnusPrimeWeapon", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/GaraPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVMesaPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Cowgirl/MesaPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVOperatorPrimeAccessories", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Cowgirl/MesaPrimeAltHelmet", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MeleeDangles/PrimeDangleF", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Pistols/PrimeAkjagara/AkJagaraPrime", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Gunblade/RedeemerPrime/RedeemerPrimeWep", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/MesaPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVHydroidPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Pirate/HydroidPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVSpritsailPrimeArmorSet", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MeleeDangles/PrimeDangleEMeleeDangle", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/PrimeNamiSkyla/PrimeNamiSkyla", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Pistols/PrimeBallistica/PrimeBallistica", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/HydroidPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVAtlasPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Brawler/AtlasPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/PrimeScarfAtlas", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVOrcusPrimeSentAccessories", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/PrimeTekko/PrimeTekko", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Sentinels/SentinelPowersuits/PrimeDethCubePowerSuit", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/AtlasPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVVaubanPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Trapper/TrapperPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/PrimeScarfV", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MeleeDangles/CatenoPrimeMeleeDangle", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/PrimeFragor/PrimeFragor", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Pistols/PrimeAkstiletto/PrimeAkstiletto", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/VaubanPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVVoltPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Volt/VoltPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MeleeDangles/KazeruPrimeMeleeDangle", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVEdoPrimeArmorSet", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Powersuits/Archwing/PrimeJetPack/PrimeJetPack", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/VoltPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVLokiPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Loki/LokiPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MeleeDangles/PrimeMeleeDangle", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVSummusPrimeSentAccessories", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Staff/PrimeBo/PrimeBoWeapon", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Sentinels/SentinelPowersuits/PrimeWyrmPowerSuit", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/LokiPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVNyxPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Jade/NyxPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVTargisPrimeArmorSet", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MeleeDangles/ValaPrimeMeleeDangle", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Axe/PrimeScindo/PrimeScindoWeapon", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/ThrowingWeapons/PrimeThrowingStar/PrimeHikou", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/NyxPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVLimboPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Magician/LimboPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/PrimeLimboCape", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/PrimeAccessSigilLimbo", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/PRapier/DestrezaPrime", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Pistols/PrimePyrana/PrimePyranaPistol", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/LimboPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVTrinityPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Trinity/TrinityPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVKavasaPrimeKubrowArmor", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MeleeDangles/ScrollingPrimeMeleeDangle", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/PrimeDualKamas/PrimeDualKamas", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/TrinityPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVIvaraPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Ranger/IvaraPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/IvaraPrimeCape", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVAnasaAyatanPrimeArmorSet", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/LongGuns/PrimeBaza/PrimeBazaGun", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Pistols/PrimeAksomati/PrimeAksomati", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/IvaraPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVGarudaPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Garuda/GarudaPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Effects/GarudaPrimeEphemera", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVKukriPrimeArmorSet", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/LongGuns/PrimeNagantaka/PrimeNagantakaWeapon", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Archwing/Primary/PrimeCorvas/PrimeCorvasWeapon", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/GarudaPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVKhoraPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Khora/KhoraPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Khora/KhoraPrimeAltHelmet", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/KhoraPrimeSyandana", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/Emotes/KhoraPrimeEmote", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Pistols/PrimeHystrix/PrimeHystrixWeapon", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/PrimeDualKeres/PrimeDualKeresWeapon", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/KhoraPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVZephyrPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Tengu/ZephyrPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/LongGuns/PrimeTiberon/PrimeTiberonRifle", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Tonfa/TonfaContestWinnerPrime/TonfaContestWinnerPrimeWeapon", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVCommodorePrimeSuit", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVTiborPrimeKavatArmor", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/ZephyrPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVChromaPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Dragon/ChromaPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/LongGuns/RubicoPrime/RubicoPrimeWeapon", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/PrimeGram/PrimeGram", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/PrimeChromaCape", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVImugiPrimeArmorSet", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/ChromaPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVNovaPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/AntiMatter/NovaPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVEdoPrimeArmorSet", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/PrimeAccessSigilFive", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Pistols/PrimeVasto/PrimeVastoPistol", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/LongGuns/PrimeSoma/PrimeSomaRifle", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/NovaPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVTrinityPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Trinity/TrinityPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVKavasaPrimeKubrowArmor", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/MeleeDangles/ScrollingPrimeMeleeDangle", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/PrimeDualKamas/PrimeDualKamas", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/TrinityPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVNezhaPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Nezha/NezhaPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVRanshaPrimeArmorSet", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Effects/EphemeraNezhaPrime", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/Polearms/PrimeGuandao/PrimeGuandaoWeapon", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Pistols/PrimeZakti/PrimeZaktiPistol", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/NezhaPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVOctaviaPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Bard/OctaviaPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVGlissandaPrimeArmorSet", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/PrimeOctaviaSyandana", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/LisetPropShawzinPrime", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/LongGuns/PrimeTenora/TenoraPrimeWeapon", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Pistols/PrimePandero/PanderoPrimeWeapon", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/OctaviaPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVHarrowPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Priest/HarrowPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/PrimeNaveScarf", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVTemplarPrimeSuit", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/LongGuns/PrimeScourge/PrimeScourgeWeapon", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Pistols/PrimeKnell/PrimeKnellWeapon", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/HarrowPrimeBobbleHead"] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVValkyrPrimeSinglePack", + "Items": [ + { + "ItemType": "/Lotus/StoreItems/Powersuits/Berserker/ValkyrPrime", + "PrimePrice": 3 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Bows/PrimeCernos/PrimeCernos", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Weapons/Tenno/Melee/PrimeVenKa/PrimeVenkaClaws", + "PrimePrice": 2 + }, + { + "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Liset/LisetSkinPrime", + "PrimePrice": 1 + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVSaitaPrimeSuit", + "PrimePrice": 2 + } + ], + "BobbleHeads": ["/Lotus/StoreItems/Types/Items/ShipDecos/ValkyrPrimeBobbleHead"] + } + ], + "primeDualPacks": [ + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVRevenantBaruukPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVRevenantPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVBaruukPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionRevenantBaruukVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionRevenantBaruukVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionRevenantBaruukVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionRevenantBaruukVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionRevenantBaruukVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionRevenantBaruukVaultBBronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVNekrosOberonPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVNekrosPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVOberonPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionOberonNekrosVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionOberonNekrosVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionOberonNekrosVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionOberonNekrosVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionOberonNekrosVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionOberonNekrosVaultABronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVBansheeMiragePrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVBansheePrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVMiragePrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionBansheeMirageVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionBansheeMirageVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionBansheeMirageVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionBansheeMirageVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionBansheeMirageVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionBansheeMirageVaultBBronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVNidusSarynPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVNidusPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVSarynPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionNidusSarynVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionNidusSarynVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionNidusSarynVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionNidusSarynVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionNidusSarynVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionNidusSarynVaultABronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVEmberRhinoPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVEmberPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVRhinoPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionEmberRhinoVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionEmberRhinosVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionEmberRhinosVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionEmberRhinoVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionEmberRhinoVaultABronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVInarosAshPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVInarosPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVAshPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionInarosAshVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionInarosAshVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionInarosAshVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionInarosAshVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionInarosAshVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionInarosAshVaultBBronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVFrostMagPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVMagPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVFrostPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionFrostMagVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionFrostMagVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionFrostMagVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionFrostMagVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionFrostMagVaultABronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVEquinoxWukongPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVEquinoxPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVWukongPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionWukongEquinoxVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionWukongEquinoxVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionWukongEquinoxVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionWukongEquinoxVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionWukongEquinoxVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionWukongEquinoxVaultABronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVTitaniaGaraPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVTitaniaPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVGaraPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionTitaniaGaraVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionTitaniaGaraVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionTitaniaGaraVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionTitaniaGaraVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionTitaniaGaraVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionTitaniaGaraVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionTitaniaGaraVaultBBronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVMesaHydroidPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVMesaPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVHydroidPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionMesaHydroidVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionMesaHydroidVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionMesaHydroidVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionMesaHydroidVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionMesaHydroidVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionMesaHydroidVaultABronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVAtlasVaubanPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVAtlasPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVVaubanPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionAtlasVaubanVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionAtlasVaubanVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionAtlasVaubanVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionAtlasVaubanVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionAtlasVaubanVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionAtlasVaubanVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionAtlasVaubanVaultABronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVVoltLokiPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVVoltPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVLokiPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionLokiVoltVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionLokiVoltVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionLokiVoltVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionLokiVoltVaultABronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVRhinoNyxPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVRhinoPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVNyxPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionRhinoNyxVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionRhinoNyxVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionRhinoNyxVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionRhinoNyxVaultABronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVLimboTrinityPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVLimboPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVTrinityPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionLimboTrinityVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionLimboTrinityVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionLimboTrinityVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionLimboTrinityVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionLimboTrinityVaultABronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVIvaraOberonPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVIvaraPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVOberonPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionIvaraOberonVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionIvaraOberonVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionIvaraOberonVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionIvaraOberonVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionIvaraOberonVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionIvaraOberonVaultBBronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVGarudaKhoraPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVGarudaPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVKhoraPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionGarudaKhoraVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionGarudaKhoraVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionGarudaKhoraVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionGarudaKhoraVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionGarudaKhoraVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionGarudaKhoraVaultBBronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVZephyrChromaPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVZephyrPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVChromaPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionChromaZephyrVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionChromaZephyrVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionChromaZephyrVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionChromaZephyrVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionChromaZephyrVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionChromaZephyrVaultABronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVNovaTrinityPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVNovaPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVTrinityPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionNovaTrinityVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionNovaTrinityVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionNovaTrinityVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionNovaTrinityVaultABronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVNezhaOctaviaPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVNezhaPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVOctaviaPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionNezhaOctaviaVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionNezhaOctaviaVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionNezhaOctaviaVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionNezhaOctaviaVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionNezhaOctaviaVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionNezhaOctaviaVaultABronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVMesaLimboPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVMesaPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVLimboPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionMesaLimboVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionMesaLimboVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionMesaLimboVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionMesaLimboVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionMesaLimboVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionMesaLimboVaultABronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVHarrowNekrosPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVHarrowPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVNekrosPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionHarrowNekrosVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionHarrowNekrosVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionHarrowNekrosVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionHarrowNekrosVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionHarrowNekrosVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionHarrowNekrosVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionHarrowNekrosVaultBBronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVValkyrSarynPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVValkyrPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVSarynPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionSarynValkyrVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionSarynValkyrVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionSarynValkyrVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionSarynValkyrVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionSarynValkyrVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionSarynValkyrVaultBBronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVMagRhinoPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVRhinoPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVMagPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionYVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionYVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionYVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionYVaultABronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVEmberFrostPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVEmberPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVFrostPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionQVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionQVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionQVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionQVaultABronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVAshVaubanPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVAshPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVVaubanPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionAshVaubanVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionAshVaubanVaultBBronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionAshVaubanVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionAshVaubanVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionAshVaubanVaultABronze" + ] + }, + { + "ItemType": "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVMagNovaPrimeDualPack", + "SinglePacks": ["/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVMagPrimeSinglePack", "/Lotus/Types/StoreItems/Packages/MegaPrimeVault/MPVNovaPrimeSinglePack"], + "Relics": [ + "/Lotus/StoreItems/Types/Game/Projections/T1VoidProjectionMagNovaVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T2VoidProjectionMagNovaVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T3VoidProjectionMagNovaVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionMagNovaVaultABronze", + "/Lotus/StoreItems/Types/Game/Projections/T4VoidProjectionMagNovaVaultBBronze" + ] + } + ] +} diff --git a/static/fixed_responses/worldState/worldState.json b/static/fixed_responses/worldState/worldState.json new file mode 100644 index 00000000..e25d1bf1 --- /dev/null +++ b/static/fixed_responses/worldState/worldState.json @@ -0,0 +1,322 @@ +{ + "Version": 10, + "Events": [ + { + "Msg": "Join the OpenWF Discord!", + "Messages": [ + { "LanguageCode": "fr", "Message": "Rejoignez le Discord OpenWF!" }, + { "LanguageCode": "it", "Message": "Unisciti al Discord di OpenWF!" }, + { "LanguageCode": "de", "Message": "Tritt dem OpenWF Discord bei!" }, + { "LanguageCode": "es", "Message": "Únete al Discord de OpenWF!" }, + { "LanguageCode": "pt", "Message": "Junte-se ao Discord do OpenWF!" }, + { "LanguageCode": "ru", "Message": "Присоединяйтесь к OpenWF Discord!" }, + { "LanguageCode": "pl", "Message": "Dołącz do Discord OpenWF!" }, + { "LanguageCode": "uk", "Message": "Приєднуйтесь до OpenWF Discord!" }, + { "LanguageCode": "tr", "Message": "OpenWF Discord'a katıl!" }, + { "LanguageCode": "ja", "Message": "OpenWFのDiscordに参加しよう!" }, + { "LanguageCode": "zh", "Message": "加入OpenWF Discord!" }, + { "LanguageCode": "ko", "Message": "OpenWF Discord에 가입하세요!" }, + { "LanguageCode": "tc", "Message": "加入OpenWF Discord!" } + ], + "Prop": "https://discord.gg/PNNZ3asUuY", + "Icon": "/Lotus/Interface/Icons/DiscordIconNoBacker.png" + } + ], + "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": [] + } + ] + } + }, + "SyndicateMissions": [ + { + "_id": { "$oid": "663a4fc5ba6f84724fa4804c" }, + "Activation": { "$date": { "$numberLong": "1715097541439" } }, + "Expiry": { "$date": { "$numberLong": "2000000000000" } }, + "Tag": "EventSyndicate", + "Seed": 47385, + "Nodes": [] + }, + { + "_id": { "$oid": "663a4fc5ba6f84724fa4804b" }, + "Activation": { "$date": { "$numberLong": "1715097541439" } }, + "Expiry": { "$date": { "$numberLong": "2000000000000" } }, + "Tag": "NecraloidSyndicate", + "Seed": 73038, + "Nodes": [] + }, + { + "_id": { "$oid": "663a4fc5ba6f84724fa4804d" }, + "Activation": { "$date": { "$numberLong": "1715097541439" } }, + "Expiry": { "$date": { "$numberLong": "2000000000000" } }, + "Tag": "KahlSyndicate", + "Seed": 50102, + "Nodes": [] + }, + { + "_id": { "$oid": "663a4fc5ba6f84724fa4804f" }, + "Activation": { "$date": { "$numberLong": "1715097541439" } }, + "Expiry": { "$date": { "$numberLong": "2000000000000" } }, + "Tag": "QuillsSyndicate", + "Seed": 77721, + "Nodes": [] + }, + { + "_id": { "$oid": "663a4fc5ba6f84724fa48052" }, + "Activation": { "$date": { "$numberLong": "1715097541439" } }, + "Expiry": { "$date": { "$numberLong": "2000000000000" } }, + "Tag": "RadioLegion3Syndicate", + "Seed": 95995, + "Nodes": [] + }, + { + "_id": { "$oid": "663a4fc5ba6f84724fa48051" }, + "Activation": { "$date": { "$numberLong": "1715097541439" } }, + "Expiry": { "$date": { "$numberLong": "2000000000000" } }, + "Tag": "RadioLegion2Syndicate", + "Seed": 32091, + "Nodes": [] + }, + { + "_id": { "$oid": "663a4fc5ba6f84724fa48053" }, + "Activation": { "$date": { "$numberLong": "1715097541439" } }, + "Expiry": { "$date": { "$numberLong": "2000000000000" } }, + "Tag": "RadioLegionIntermission10Syndicate", + "Seed": 74072, + "Nodes": [] + }, + { + "_id": { "$oid": "663a4fc5ba6f84724fa48060" }, + "Activation": { "$date": { "$numberLong": "1715097541439" } }, + "Expiry": { "$date": { "$numberLong": "2000000000000" } }, + "Tag": "RadioLegionIntermission11Syndicate", + "Seed": 353, + "Nodes": [] + }, + { + "_id": { "$oid": "663a4fc5ba6f84724fa48060" }, + "Activation": { "$date": { "$numberLong": "1715097541439" } }, + "Expiry": { "$date": { "$numberLong": "2000000000000" } }, + "Tag": "RadioLegionIntermission12Syndicate", + "Seed": 353, + "Nodes": [] + }, + { + "_id": { "$oid": "663a4fc5ba6f84724fa48057" }, + "Activation": { "$date": { "$numberLong": "1715097541439" } }, + "Expiry": { "$date": { "$numberLong": "2000000000000" } }, + "Tag": "RadioLegionIntermission5Syndicate", + "Seed": 95997, + "Nodes": [] + }, + { + "_id": { "$oid": "663a4fc5ba6f84724fa48055" }, + "Activation": { "$date": { "$numberLong": "1715097541439" } }, + "Expiry": { "$date": { "$numberLong": "2000000000000" } }, + "Tag": "RadioLegionIntermission3Syndicate", + "Seed": 71506, + "Nodes": [] + }, + { + "_id": { "$oid": "663a4fc5ba6f84724fa48056" }, + "Activation": { "$date": { "$numberLong": "1715097541439" } }, + "Expiry": { "$date": { "$numberLong": "2000000000000" } }, + "Tag": "RadioLegionIntermission4Syndicate", + "Seed": 97653, + "Nodes": [] + }, + { + "_id": { "$oid": "663a4fc5ba6f84724fa48054" }, + "Activation": { "$date": { "$numberLong": "1715097541439" } }, + "Expiry": { "$date": { "$numberLong": "2000000000000" } }, + "Tag": "RadioLegionIntermission2Syndicate", + "Seed": 64160, + "Nodes": [] + }, + { + "_id": { "$oid": "663a4fc5ba6f84724fa48058" }, + "Activation": { "$date": { "$numberLong": "1715097541439" } }, + "Expiry": { "$date": { "$numberLong": "2000000000000" } }, + "Tag": "RadioLegionIntermission6Syndicate", + "Seed": 94007, + "Nodes": [] + }, + { + "_id": { "$oid": "663a4fc5ba6f84724fa4805c" }, + "Activation": { "$date": { "$numberLong": "1715097541439" } }, + "Expiry": { "$date": { "$numberLong": "2000000000000" } }, + "Tag": "RadioLegionIntermissionSyndicate", + "Seed": 69122, + "Nodes": [] + }, + { + "_id": { "$oid": "663a4fc5ba6f84724fa4805a" }, + "Activation": { "$date": { "$numberLong": "1715097541439" } }, + "Expiry": { "$date": { "$numberLong": "2000000000000" } }, + "Tag": "RadioLegionIntermission8Syndicate", + "Seed": 69270, + "Nodes": [] + }, + { + "_id": { "$oid": "663a4fc5ba6f84724fa4805b" }, + "Activation": { "$date": { "$numberLong": "1715097541439" } }, + "Expiry": { "$date": { "$numberLong": "2000000000000" } }, + "Tag": "RadioLegionIntermission9Syndicate", + "Seed": 5166, + "Nodes": [] + }, + { + "_id": { "$oid": "663a4fc5ba6f84724fa48059" }, + "Activation": { "$date": { "$numberLong": "1715097541439" } }, + "Expiry": { "$date": { "$numberLong": "2000000000000" } }, + "Tag": "RadioLegionIntermission7Syndicate", + "Seed": 82114, + "Nodes": [] + }, + { + "_id": { "$oid": "663a4fc5ba6f84724fa4805d" }, + "Activation": { "$date": { "$numberLong": "1715097541439" } }, + "Expiry": { "$date": { "$numberLong": "2000000000000" } }, + "Tag": "RadioLegionSyndicate", + "Seed": 25645, + "Nodes": [] + }, + { + "_id": { "$oid": "663a4fc5ba6f84724fa4805f" }, + "Activation": { "$date": { "$numberLong": "1715097541439" } }, + "Expiry": { "$date": { "$numberLong": "2000000000000" } }, + "Tag": "VentKidsSyndicate", + "Seed": 67257, + "Nodes": [] + }, + { + "_id": { "$oid": "663a4fc5ba6f84724fa48060" }, + "Activation": { "$date": { "$numberLong": "1715097541439" } }, + "Expiry": { "$date": { "$numberLong": "2000000000000" } }, + "Tag": "VoxSyndicate", + "Seed": 77972, + "Nodes": [] + } + ], + "NodeOverrides": [ + { "_id": { "$oid": "549b18e9b029cef5991d6aec" }, "Node": "EuropaHUB", "Hide": true }, + { "_id": { "$oid": "54a1737aeb658f6cbccf70ff" }, "Node": "ErisHUB", "Hide": true }, + { "_id": { "$oid": "54a736ddec12f80bd6e9e326" }, "Node": "VenusHUB", "Hide": true }, + { "_id": { "$oid": "5ad9f9bb6df82a56eabf3d44" }, "Node": "SolNode802", "Seed": 9969639 }, + { + "_id": { "$oid": "5b8817c2bd4f253264d6aa91" }, + "Node": "EarthHUB", + "Hide": false, + "LevelOverride": "/Lotus/Levels/Proc/Hub/RelayStationHubTwoB", + "Activation": { "$date": { "$numberLong": "1535646600000" } } + }, + { + "_id": { "$oid": "5d24d1f674491d51f8d44473" }, + "Node": "MercuryHUB", + "Hide": true, + "LevelOverride": "/Lotus/Levels/Proc/Hub/RelayStationHubHydroid", + "Activation": { "$date": { "$numberLong": "1563030000000" } } + } + ], + "PrimeAccessAvailability": { "State": "PRIME1" }, + "PrimeVaultAvailabilities": [false, false, false, false, false], + "PrimeTokenAvailability": true, + "LibraryInfo": { "LastCompletedTargetType": "/Lotus/Types/Game/Library/Targets/Research7Target" }, + "PersistentEnemies": [], + "PVPAlternativeModes": [], + "PVPActiveTournaments": [], + "ProjectPct": [47.16325544797147, 70.88794362074432, 0], + "ConstructionProjects": [], + "TwitchPromos": [], + "ExperimentRecommended": [], + "ForceLogoutVersion": 0 +} diff --git a/static/webui/index.html b/static/webui/index.html new file mode 100644 index 00000000..7a4146df --- /dev/null +++ b/static/webui/index.html @@ -0,0 +1,1312 @@ + + + + OpenWF WebUI + + + + + + +
+ +
+
+

+
+ + +
+ + +
+ + +
+
+
+

+
+
+ +
+
+
+
+
+ + + +
+
+
+
+

+
+ + +
+
+
+
+
+
+
+
+
+
+
+

+
+ + +
+
+
+
+
+
+
+
+

+
+ + +
+
+
+
+
+
+
+
+

+
+ + +
+
+
+
+
+
+
+
+

+
+ + +
+
+
+
+
+
+
+
+
+
+
+ + +
+ + +
+
+
+
+
+
+
+
+
+ + +
+ + + +
+
+
+
+
+
+
+
+
+
+
+ + +
+ + + +
+
+
+
+
+
+
+
+
+ + +
+ + + +
+
+
+
+
+
+
+
+
+
+
+ + +
+ + +
+
+
+
+
+
+
+
+
+ + +
+ + +
+
+
+
+
+
+
+
+
+
+
+ + +
+ + +
+
+
+
+
+
+
+
+
+ + +
+ + +
+
+
+
+
+
+
+
+
+
+
+ + +
+ + +
+
+
+
+
+
+
+
+
+ + +
+ + + + +
+
+
+
+
+
+
+
+
+
+
+ + +
+ + + + +
+
+
+
+
+
+
+
+
+ + +
+ + +
+
+
+
+
+
+
+
+
+
+
+ + +
+ + + +
+
+
+
+
+
+
+
+
+ + + + + +
+ + +
+
+
+
+
+
+
+
+
+
+
+ + +
+ + +
+
+
+
+
+
+
+
+
+ + +
+ + +
+
+
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+
+
+
+
+

+

+

+
+
+
+ +
+
+
+
+
+
+

+ + +

+
+ + x + + +
+ + +
+
+
+
+
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+ + + + +
+
+
+
+
+

+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+ + + + +
+ + +
+
+
+
+
+
+ + + +
+
+
+
+
+
+

+
+
+
+
+
+
+ + +
+ + +
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + + + + + +
+
+ +
+ + +
+
+
+
+
+
+
+
+
+
+

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+ + + +
+
+ + +
+
+
+
+ + + +
+
+ + +
+
+
+ + + +
+
+ + + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + +
+
+ + +
+
+
+
+ + +
+
+
+ +
+ + +
+
+
+
+
+
+ + +
+
+
+ +
+ + +
+
+
+
+
+
+ + +
+
+
+ +
+ + +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+
+
+
+
+

+ + +

+ + +

+
    +
  • +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/webui/libs/bootstrap.bundle.min.js b/static/webui/libs/bootstrap.bundle.min.js new file mode 100644 index 00000000..3d91751d --- /dev/null +++ b/static/webui/libs/bootstrap.bundle.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v5.3.7 (https://getbootstrap.com/) + * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t=new Map,e={set(e,i,n){t.has(e)||t.set(e,new Map);const s=t.get(e);s.has(i)||0===s.size?s.set(i,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(e,i)=>t.has(e)&&t.get(e).get(i)||null,remove(e,i){if(!t.has(e))return;const n=t.get(e);n.delete(i),0===n.size&&t.delete(e)}},i="transitionend",n=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),s=t=>{t.dispatchEvent(new Event(i))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(n(t)):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,m=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},g=(t,e=[],i=t)=>"function"==typeof t?t.call(...e):i,_=(t,e,n=!0)=>{if(!n)return void g(t);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let r=!1;const a=({target:n})=>{n===e&&(r=!0,e.removeEventListener(i,a),g(t))};e.addEventListener(i,a),setTimeout((()=>{r||s(e)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=I(t);return C.has(o)||(o=t),[n,s,o]}function S(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return P(s,{delegateTarget:r}),n.oneOff&&N.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return P(n,{delegateTarget:t}),i.oneOff&&N.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function D(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function $(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&D(t,e,i,r.callable,r.delegationSelector)}function I(t){return t=t.replace(y,""),T[t]||t}const N={on(t,e,i,n){S(t,e,i,n,!1)},one(t,e,i,n){S(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))$(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(w,"");a&&!e.includes(s)||D(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;D(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=u();let s=null,o=!0,r=!0,a=!1;e!==I(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=P(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function P(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function j(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function M(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const F={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${M(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${M(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1),e[i]=j(t.dataset[n])}return e},getDataAttribute:(t,e)=>j(t.getAttribute(`data-bs-${M(e)}`))};class H{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=o(e)?F.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...o(e)?F.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],r=o(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(r))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${r}" but expected type "${s}".`)}var i}}class W extends H{constructor(t,i){super(),(t=r(t))&&(this._element=t,this._config=this._getConfig(i),e.set(this._element,this.constructor.DATA_KEY,this))}dispose(){e.remove(this._element,this.constructor.DATA_KEY),N.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){_(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return e.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.7"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const B=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e?e.split(",").map((t=>n(t))).join(","):null},z={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!l(t)&&a(t)))},getSelectorFromElement(t){const e=B(t);return e&&z.findOne(e)?e:null},getElementFromSelector(t){const e=B(t);return e?z.findOne(e):null},getMultipleElementsFromSelector(t){const e=B(t);return e?z.find(e):[]}},R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;N.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),l(this))return;const s=z.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))},q=".bs.alert",V=`close${q}`,K=`closed${q}`;class Q extends W{static get NAME(){return"alert"}close(){if(N.trigger(this._element,V).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),N.trigger(this._element,K),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Q.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(Q,"close"),m(Q);const X='[data-bs-toggle="button"]';class Y extends W{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=Y.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}N.on(document,"click.bs.button.data-api",X,(t=>{t.preventDefault();const e=t.target.closest(X);Y.getOrCreateInstance(e).toggle()})),m(Y);const U=".bs.swipe",G=`touchstart${U}`,J=`touchmove${U}`,Z=`touchend${U}`,tt=`pointerdown${U}`,et=`pointerup${U}`,it={endCallback:null,leftCallback:null,rightCallback:null},nt={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class st extends H{constructor(t,e){super(),this._element=t,t&&st.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return it}static get DefaultType(){return nt}static get NAME(){return"swipe"}dispose(){N.off(this._element,U)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),g(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&g(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(N.on(this._element,tt,(t=>this._start(t))),N.on(this._element,et,(t=>this._end(t))),this._element.classList.add("pointer-event")):(N.on(this._element,G,(t=>this._start(t))),N.on(this._element,J,(t=>this._move(t))),N.on(this._element,Z,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const ot=".bs.carousel",rt=".data-api",at="ArrowLeft",lt="ArrowRight",ct="next",ht="prev",dt="left",ut="right",ft=`slide${ot}`,pt=`slid${ot}`,mt=`keydown${ot}`,gt=`mouseenter${ot}`,_t=`mouseleave${ot}`,bt=`dragstart${ot}`,vt=`load${ot}${rt}`,yt=`click${ot}${rt}`,wt="carousel",At="active",Et=".active",Tt=".carousel-item",Ct=Et+Tt,Ot={[at]:ut,[lt]:dt},xt={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},kt={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class Lt extends W{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=z.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===wt&&this.cycle()}static get Default(){return xt}static get DefaultType(){return kt}static get NAME(){return"carousel"}next(){this._slide(ct)}nextWhenVisible(){!document.hidden&&a(this._element)&&this.next()}prev(){this._slide(ht)}pause(){this._isSliding&&s(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?N.one(this._element,pt,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void N.one(this._element,pt,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?ct:ht;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&N.on(this._element,mt,(t=>this._keydown(t))),"hover"===this._config.pause&&(N.on(this._element,gt,(()=>this.pause())),N.on(this._element,_t,(()=>this._maybeEnableCycle()))),this._config.touch&&st.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of z.find(".carousel-item img",this._element))N.on(t,bt,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(dt)),rightCallback:()=>this._slide(this._directionToOrder(ut)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new st(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=Ot[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=z.findOne(Et,this._indicatorsElement);e.classList.remove(At),e.removeAttribute("aria-current");const i=z.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(At),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===ct,s=e||b(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>N.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(ft).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),d(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(At),i.classList.remove(At,c,l),this._isSliding=!1,r(pt)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return z.findOne(Ct,this._element)}_getItems(){return z.find(Tt,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===dt?ht:ct:t===dt?ct:ht}_orderToDirection(t){return p()?t===ht?dt:ut:t===ht?ut:dt}static jQueryInterface(t){return this.each((function(){const e=Lt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}N.on(document,yt,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=z.getElementFromSelector(this);if(!e||!e.classList.contains(wt))return;t.preventDefault();const i=Lt.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===F.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),N.on(window,vt,(()=>{const t=z.find('[data-bs-ride="carousel"]');for(const e of t)Lt.getOrCreateInstance(e)})),m(Lt);const St=".bs.collapse",Dt=`show${St}`,$t=`shown${St}`,It=`hide${St}`,Nt=`hidden${St}`,Pt=`click${St}.data-api`,jt="show",Mt="collapse",Ft="collapsing",Ht=`:scope .${Mt} .${Mt}`,Wt='[data-bs-toggle="collapse"]',Bt={parent:null,toggle:!0},zt={parent:"(null|element)",toggle:"boolean"};class Rt extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=z.find(Wt);for(const t of i){const e=z.getSelectorFromElement(t),i=z.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Bt}static get DefaultType(){return zt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Rt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(N.trigger(this._element,Dt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Mt),this._element.classList.add(Ft),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Ft),this._element.classList.add(Mt,jt),this._element.style[e]="",N.trigger(this._element,$t)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(N.trigger(this._element,It).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,d(this._element),this._element.classList.add(Ft),this._element.classList.remove(Mt,jt);for(const t of this._triggerArray){const e=z.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Ft),this._element.classList.add(Mt),N.trigger(this._element,Nt)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(jt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=r(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Wt);for(const e of t){const t=z.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=z.find(Ht,this._config.parent);return z.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Rt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}N.on(document,Pt,Wt,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of z.getMultipleElementsFromSelector(this))Rt.getOrCreateInstance(t,{toggle:!1}).toggle()})),m(Rt);var qt="top",Vt="bottom",Kt="right",Qt="left",Xt="auto",Yt=[qt,Vt,Kt,Qt],Ut="start",Gt="end",Jt="clippingParents",Zt="viewport",te="popper",ee="reference",ie=Yt.reduce((function(t,e){return t.concat([e+"-"+Ut,e+"-"+Gt])}),[]),ne=[].concat(Yt,[Xt]).reduce((function(t,e){return t.concat([e,e+"-"+Ut,e+"-"+Gt])}),[]),se="beforeRead",oe="read",re="afterRead",ae="beforeMain",le="main",ce="afterMain",he="beforeWrite",de="write",ue="afterWrite",fe=[se,oe,re,ae,le,ce,he,de,ue];function pe(t){return t?(t.nodeName||"").toLowerCase():null}function me(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function ge(t){return t instanceof me(t).Element||t instanceof Element}function _e(t){return t instanceof me(t).HTMLElement||t instanceof HTMLElement}function be(t){return"undefined"!=typeof ShadowRoot&&(t instanceof me(t).ShadowRoot||t instanceof ShadowRoot)}const ve={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];_e(s)&&pe(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});_e(n)&&pe(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function ye(t){return t.split("-")[0]}var we=Math.max,Ae=Math.min,Ee=Math.round;function Te(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function Ce(){return!/^((?!chrome|android).)*safari/i.test(Te())}function Oe(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&_e(t)&&(s=t.offsetWidth>0&&Ee(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&Ee(n.height)/t.offsetHeight||1);var r=(ge(t)?me(t):window).visualViewport,a=!Ce()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function xe(t){var e=Oe(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function ke(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&be(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function Le(t){return me(t).getComputedStyle(t)}function Se(t){return["table","td","th"].indexOf(pe(t))>=0}function De(t){return((ge(t)?t.ownerDocument:t.document)||window.document).documentElement}function $e(t){return"html"===pe(t)?t:t.assignedSlot||t.parentNode||(be(t)?t.host:null)||De(t)}function Ie(t){return _e(t)&&"fixed"!==Le(t).position?t.offsetParent:null}function Ne(t){for(var e=me(t),i=Ie(t);i&&Se(i)&&"static"===Le(i).position;)i=Ie(i);return i&&("html"===pe(i)||"body"===pe(i)&&"static"===Le(i).position)?e:i||function(t){var e=/firefox/i.test(Te());if(/Trident/i.test(Te())&&_e(t)&&"fixed"===Le(t).position)return null;var i=$e(t);for(be(i)&&(i=i.host);_e(i)&&["html","body"].indexOf(pe(i))<0;){var n=Le(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Pe(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function je(t,e,i){return we(t,Ae(e,i))}function Me(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function Fe(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const He={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=ye(i.placement),l=Pe(a),c=[Qt,Kt].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return Me("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:Fe(t,Yt))}(s.padding,i),d=xe(o),u="y"===l?qt:Qt,f="y"===l?Vt:Kt,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=Ne(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=je(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&ke(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function We(t){return t.split("-")[1]}var Be={top:"auto",right:"auto",bottom:"auto",left:"auto"};function ze(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u,p=r.y,m=void 0===p?0:p,g="function"==typeof h?h({x:f,y:m}):{x:f,y:m};f=g.x,m=g.y;var _=r.hasOwnProperty("x"),b=r.hasOwnProperty("y"),v=Qt,y=qt,w=window;if(c){var A=Ne(i),E="clientHeight",T="clientWidth";A===me(i)&&"static"!==Le(A=De(i)).position&&"absolute"===a&&(E="scrollHeight",T="scrollWidth"),(s===qt||(s===Qt||s===Kt)&&o===Gt)&&(y=Vt,m-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,m*=l?1:-1),s!==Qt&&(s!==qt&&s!==Vt||o!==Gt)||(v=Kt,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&Be),x=!0===h?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:Ee(i*s)/s||0,y:Ee(n*s)/s||0}}({x:f,y:m},me(i)):{x:f,y:m};return f=x.x,m=x.y,l?Object.assign({},O,((C={})[y]=b?"0":"",C[v]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",C)):Object.assign({},O,((e={})[y]=b?m+"px":"",e[v]=_?f+"px":"",e.transform="",e))}const Re={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:ye(e.placement),variation:We(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,ze(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,ze(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var qe={passive:!0};const Ve={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=me(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,qe)})),a&&l.addEventListener("resize",i.update,qe),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,qe)})),a&&l.removeEventListener("resize",i.update,qe)}},data:{}};var Ke={left:"right",right:"left",bottom:"top",top:"bottom"};function Qe(t){return t.replace(/left|right|bottom|top/g,(function(t){return Ke[t]}))}var Xe={start:"end",end:"start"};function Ye(t){return t.replace(/start|end/g,(function(t){return Xe[t]}))}function Ue(t){var e=me(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Ge(t){return Oe(De(t)).left+Ue(t).scrollLeft}function Je(t){var e=Le(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ze(t){return["html","body","#document"].indexOf(pe(t))>=0?t.ownerDocument.body:_e(t)&&Je(t)?t:Ze($e(t))}function ti(t,e){var i;void 0===e&&(e=[]);var n=Ze(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=me(n),r=s?[o].concat(o.visualViewport||[],Je(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(ti($e(r)))}function ei(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function ii(t,e,i){return e===Zt?ei(function(t,e){var i=me(t),n=De(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Ce();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+Ge(t),y:l}}(t,i)):ge(e)?function(t,e){var i=Oe(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):ei(function(t){var e,i=De(t),n=Ue(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=we(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=we(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+Ge(t),l=-n.scrollTop;return"rtl"===Le(s||i).direction&&(a+=we(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(De(t)))}function ni(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?ye(s):null,r=s?We(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case qt:e={x:a,y:i.y-n.height};break;case Vt:e={x:a,y:i.y+i.height};break;case Kt:e={x:i.x+i.width,y:l};break;case Qt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?Pe(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case Ut:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Gt:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function si(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.strategy,r=void 0===o?t.strategy:o,a=i.boundary,l=void 0===a?Jt:a,c=i.rootBoundary,h=void 0===c?Zt:c,d=i.elementContext,u=void 0===d?te:d,f=i.altBoundary,p=void 0!==f&&f,m=i.padding,g=void 0===m?0:m,_=Me("number"!=typeof g?g:Fe(g,Yt)),b=u===te?ee:te,v=t.rects.popper,y=t.elements[p?b:u],w=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=ti($e(t)),i=["absolute","fixed"].indexOf(Le(t).position)>=0&&_e(t)?Ne(t):t;return ge(i)?e.filter((function(t){return ge(t)&&ke(t,i)&&"body"!==pe(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=ii(t,i,n);return e.top=we(s.top,e.top),e.right=Ae(s.right,e.right),e.bottom=Ae(s.bottom,e.bottom),e.left=we(s.left,e.left),e}),ii(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(ge(y)?y:y.contextElement||De(t.elements.popper),l,h,r),A=Oe(t.elements.reference),E=ni({reference:A,element:v,placement:s}),T=ei(Object.assign({},v,E)),C=u===te?T:A,O={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=t.modifiersData.offset;if(u===te&&x){var k=x[s];Object.keys(O).forEach((function(t){var e=[Kt,Vt].indexOf(t)>=0?1:-1,i=[qt,Vt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e}))}return O}function oi(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?ne:l,h=We(n),d=h?a?ie:ie.filter((function(t){return We(t)===h})):Yt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=si(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[ye(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const ri={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=ye(g),b=l||(_!==g&&p?function(t){if(ye(t)===Xt)return[];var e=Qe(t);return[Ye(t),e,Ye(e)]}(g):[Qe(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(ye(i)===Xt?oi(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,A=new Map,E=!0,T=v[0],C=0;C=0,S=L?"width":"height",D=si(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),$=L?k?Kt:Qt:k?Vt:qt;y[S]>w[S]&&($=Qe($));var I=Qe($),N=[];if(o&&N.push(D[x]<=0),a&&N.push(D[$]<=0,D[I]<=0),N.every((function(t){return t}))){T=O,E=!1;break}A.set(O,N)}if(E)for(var P=function(t){var e=v.find((function(e){var i=A.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},j=p?3:1;j>0&&"break"!==P(j);j--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function ai(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function li(t){return[qt,Kt,Vt,Qt].some((function(e){return t[e]>=0}))}const ci={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=si(e,{elementContext:"reference"}),a=si(e,{altBoundary:!0}),l=ai(r,n),c=ai(a,s,o),h=li(l),d=li(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},hi={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=ne.reduce((function(t,i){return t[i]=function(t,e,i){var n=ye(t),s=[Qt,qt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[Qt,Kt].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},di={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=ni({reference:e.rects.reference,element:e.rects.popper,placement:e.placement})},data:{}},ui={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=si(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=ye(e.placement),b=We(e.placement),v=!b,y=Pe(_),w="x"===y?"y":"x",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,O="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,S="y"===y?qt:Qt,D="y"===y?Vt:Kt,$="y"===y?"height":"width",I=A[y],N=I+g[S],P=I-g[D],j=f?-T[$]/2:0,M=b===Ut?E[$]:T[$],F=b===Ut?-T[$]:-E[$],H=e.elements.arrow,W=f&&H?xe(H):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},z=B[S],R=B[D],q=je(0,E[$],W[$]),V=v?E[$]/2-j-q-z-O.mainAxis:M-q-z-O.mainAxis,K=v?-E[$]/2+j+q+R+O.mainAxis:F+q+R+O.mainAxis,Q=e.elements.arrow&&Ne(e.elements.arrow),X=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=I+K-Y,G=je(f?Ae(N,I+V-Y-X):N,I,f?we(P,U):P);A[y]=G,k[y]=G-I}if(a){var J,Z="x"===y?qt:Qt,tt="x"===y?Vt:Kt,et=A[w],it="y"===w?"height":"width",nt=et+g[Z],st=et-g[tt],ot=-1!==[qt,Qt].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=je(t,e,i);return n>i?i:n}(at,et,lt):je(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:["offset"]};function fi(t,e,i){void 0===i&&(i=!1);var n,s,o=_e(e),r=_e(e)&&function(t){var e=t.getBoundingClientRect(),i=Ee(e.width)/t.offsetWidth||1,n=Ee(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=De(e),l=Oe(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==pe(e)||Je(a))&&(c=(n=e)!==me(n)&&_e(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:Ue(n)),_e(e)?((h=Oe(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=Ge(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function pi(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var mi={placement:"bottom",modifiers:[],strategy:"absolute"};function gi(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(F.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...g(this._config.popperConfig,[void 0,t])}}_selectMenuItem({key:t,target:e}){const i=z.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>a(t)));i.length&&b(i,e,t===Oi,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=Ki.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=z.find(ji);for(const i of e){const e=Ki.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Ci,Oi].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Pi)?this:z.prev(this,Pi)[0]||z.next(this,Pi)[0]||z.findOne(Pi,t.delegateTarget.parentNode),o=Ki.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}N.on(document,$i,Pi,Ki.dataApiKeydownHandler),N.on(document,$i,Mi,Ki.dataApiKeydownHandler),N.on(document,Di,Ki.clearMenus),N.on(document,Ii,Ki.clearMenus),N.on(document,Di,Pi,(function(t){t.preventDefault(),Ki.getOrCreateInstance(this).toggle()})),m(Ki);const Qi="backdrop",Xi="show",Yi=`mousedown.bs.${Qi}`,Ui={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Gi={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Ji extends H{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Ui}static get DefaultType(){return Gi}static get NAME(){return Qi}show(t){if(!this._config.isVisible)return void g(t);this._append();const e=this._getElement();this._config.isAnimated&&d(e),e.classList.add(Xi),this._emulateAnimation((()=>{g(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Xi),this._emulateAnimation((()=>{this.dispose(),g(t)}))):g(t)}dispose(){this._isAppended&&(N.off(this._element,Yi),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=r(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),N.on(t,Yi,(()=>{g(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){_(t,this._getElement(),this._config.isAnimated)}}const Zi=".bs.focustrap",tn=`focusin${Zi}`,en=`keydown.tab${Zi}`,nn="backward",sn={autofocus:!0,trapElement:null},on={autofocus:"boolean",trapElement:"element"};class rn extends H{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return sn}static get DefaultType(){return on}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),N.off(document,Zi),N.on(document,tn,(t=>this._handleFocusin(t))),N.on(document,en,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,N.off(document,Zi))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=z.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===nn?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?nn:"forward")}}const an=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",ln=".sticky-top",cn="padding-right",hn="margin-right";class dn{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,cn,(e=>e+t)),this._setElementAttributes(an,cn,(e=>e+t)),this._setElementAttributes(ln,hn,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,cn),this._resetElementAttributes(an,cn),this._resetElementAttributes(ln,hn)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&F.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=F.getDataAttribute(t,e);null!==i?(F.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(o(t))e(t);else for(const i of z.find(t,this._element))e(i)}}const un=".bs.modal",fn=`hide${un}`,pn=`hidePrevented${un}`,mn=`hidden${un}`,gn=`show${un}`,_n=`shown${un}`,bn=`resize${un}`,vn=`click.dismiss${un}`,yn=`mousedown.dismiss${un}`,wn=`keydown.dismiss${un}`,An=`click${un}.data-api`,En="modal-open",Tn="show",Cn="modal-static",On={backdrop:!0,focus:!0,keyboard:!0},xn={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class kn extends W{constructor(t,e){super(t,e),this._dialog=z.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new dn,this._addEventListeners()}static get Default(){return On}static get DefaultType(){return xn}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||N.trigger(this._element,gn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(En),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(N.trigger(this._element,fn).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Tn),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){N.off(window,un),N.off(this._dialog,un),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ji({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new rn({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=z.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),d(this._element),this._element.classList.add(Tn),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,N.trigger(this._element,_n,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){N.on(this._element,wn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),N.on(window,bn,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),N.on(this._element,yn,(t=>{N.one(this._element,vn,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(En),this._resetAdjustments(),this._scrollBar.reset(),N.trigger(this._element,mn)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(N.trigger(this._element,pn).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(Cn)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(Cn),this._queueCallback((()=>{this._element.classList.remove(Cn),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=kn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}N.on(document,An,'[data-bs-toggle="modal"]',(function(t){const e=z.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),N.one(e,gn,(t=>{t.defaultPrevented||N.one(e,mn,(()=>{a(this)&&this.focus()}))}));const i=z.findOne(".modal.show");i&&kn.getInstance(i).hide(),kn.getOrCreateInstance(e).toggle(this)})),R(kn),m(kn);const Ln=".bs.offcanvas",Sn=".data-api",Dn=`load${Ln}${Sn}`,$n="show",In="showing",Nn="hiding",Pn=".offcanvas.show",jn=`show${Ln}`,Mn=`shown${Ln}`,Fn=`hide${Ln}`,Hn=`hidePrevented${Ln}`,Wn=`hidden${Ln}`,Bn=`resize${Ln}`,zn=`click${Ln}${Sn}`,Rn=`keydown.dismiss${Ln}`,qn={backdrop:!0,keyboard:!0,scroll:!1},Vn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class Kn extends W{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return qn}static get DefaultType(){return Vn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||N.trigger(this._element,jn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new dn).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(In),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add($n),this._element.classList.remove(In),N.trigger(this._element,Mn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(N.trigger(this._element,Fn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(Nn),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove($n,Nn),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new dn).reset(),N.trigger(this._element,Wn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Ji({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():N.trigger(this._element,Hn)}:null})}_initializeFocusTrap(){return new rn({trapElement:this._element})}_addEventListeners(){N.on(this._element,Rn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():N.trigger(this._element,Hn))}))}static jQueryInterface(t){return this.each((function(){const e=Kn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}N.on(document,zn,'[data-bs-toggle="offcanvas"]',(function(t){const e=z.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this))return;N.one(e,Wn,(()=>{a(this)&&this.focus()}));const i=z.findOne(Pn);i&&i!==e&&Kn.getInstance(i).hide(),Kn.getOrCreateInstance(e).toggle(this)})),N.on(window,Dn,(()=>{for(const t of z.find(Pn))Kn.getOrCreateInstance(t).show()})),N.on(window,Bn,(()=>{for(const t of z.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&Kn.getOrCreateInstance(t).hide()})),R(Kn),m(Kn);const Qn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Xn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Yn=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Un=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Xn.has(i)||Boolean(Yn.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Gn={allowList:Qn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Jn={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Zn={entry:"(string|element|function|null)",selector:"(string|element)"};class ts extends H{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Gn}static get DefaultType(){return Jn}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Zn)}_setContent(t,e,i){const n=z.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?o(e)?this._putElementInTemplate(r(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Un(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return g(t,[void 0,this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const es=new Set(["sanitize","allowList","sanitizeFn"]),is="fade",ns="show",ss=".tooltip-inner",os=".modal",rs="hide.bs.modal",as="hover",ls="focus",cs="click",hs={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},ds={allowList:Qn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},us={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class fs extends W{constructor(t,e){if(void 0===wi)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org/docs/v2/)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return ds}static get DefaultType(){return us}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),N.off(this._element.closest(os),rs,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=N.trigger(this._element,this.constructor.eventName("show")),e=(c(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),N.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(ns),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.on(t,"mouseover",h);this._queueCallback((()=>{N.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!N.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(ns),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.off(t,"mouseover",h);this._activeTrigger[cs]=!1,this._activeTrigger[ls]=!1,this._activeTrigger[as]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),N.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(is,ns),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(is),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new ts({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[ss]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(is)}_isShown(){return this.tip&&this.tip.classList.contains(ns)}_createPopper(t){const e=g(this._config.placement,[this,t,this._element]),i=hs[e.toUpperCase()];return yi(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return g(t,[this._element,this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...g(this._config.popperConfig,[void 0,e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)N.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger[cs]=!(e._isShown()&&e._activeTrigger[cs]),e.toggle()}));else if("manual"!==e){const t=e===as?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===as?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");N.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?ls:as]=!0,e._enter()})),N.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?ls:as]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},N.on(this._element.closest(os),rs,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=F.getDataAttributes(this._element);for(const t of Object.keys(e))es.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=fs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(fs);const ps=".popover-header",ms=".popover-body",gs={...fs.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},_s={...fs.DefaultType,content:"(null|string|element|function)"};class bs extends fs{static get Default(){return gs}static get DefaultType(){return _s}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[ps]:this._getTitle(),[ms]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=bs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(bs);const vs=".bs.scrollspy",ys=`activate${vs}`,ws=`click${vs}`,As=`load${vs}.data-api`,Es="active",Ts="[href]",Cs=".nav-link",Os=`${Cs}, .nav-item > ${Cs}, .list-group-item`,xs={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},ks={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Ls extends W{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return xs}static get DefaultType(){return ks}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=r(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(N.off(this._config.target,ws),N.on(this._config.target,ws,Ts,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=z.find(Ts,this._config.target);for(const e of t){if(!e.hash||l(e))continue;const t=z.findOne(decodeURI(e.hash),this._element);a(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(Es),this._activateParents(t),N.trigger(this._element,ys,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))z.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(Es);else for(const e of z.parents(t,".nav, .list-group"))for(const t of z.prev(e,Os))t.classList.add(Es)}_clearActiveClass(t){t.classList.remove(Es);const e=z.find(`${Ts}.${Es}`,t);for(const t of e)t.classList.remove(Es)}static jQueryInterface(t){return this.each((function(){const e=Ls.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(window,As,(()=>{for(const t of z.find('[data-bs-spy="scroll"]'))Ls.getOrCreateInstance(t)})),m(Ls);const Ss=".bs.tab",Ds=`hide${Ss}`,$s=`hidden${Ss}`,Is=`show${Ss}`,Ns=`shown${Ss}`,Ps=`click${Ss}`,js=`keydown${Ss}`,Ms=`load${Ss}`,Fs="ArrowLeft",Hs="ArrowRight",Ws="ArrowUp",Bs="ArrowDown",zs="Home",Rs="End",qs="active",Vs="fade",Ks="show",Qs=".dropdown-toggle",Xs=`:not(${Qs})`,Ys='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',Us=`.nav-link${Xs}, .list-group-item${Xs}, [role="tab"]${Xs}, ${Ys}`,Gs=`.${qs}[data-bs-toggle="tab"], .${qs}[data-bs-toggle="pill"], .${qs}[data-bs-toggle="list"]`;class Js extends W{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),N.on(this._element,js,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?N.trigger(e,Ds,{relatedTarget:t}):null;N.trigger(t,Is,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(qs),this._activate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),N.trigger(t,Ns,{relatedTarget:e})):t.classList.add(Ks)}),t,t.classList.contains(Vs)))}_deactivate(t,e){t&&(t.classList.remove(qs),t.blur(),this._deactivate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),N.trigger(t,$s,{relatedTarget:e})):t.classList.remove(Ks)}),t,t.classList.contains(Vs)))}_keydown(t){if(![Fs,Hs,Ws,Bs,zs,Rs].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!l(t)));let i;if([zs,Rs].includes(t.key))i=e[t.key===zs?0:e.length-1];else{const n=[Hs,Bs].includes(t.key);i=b(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Js.getOrCreateInstance(i).show())}_getChildren(){return z.find(Us,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=z.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=z.findOne(t,i);s&&s.classList.toggle(n,e)};n(Qs,qs),n(".dropdown-menu",Ks),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(qs)}_getInnerElement(t){return t.matches(Us)?t:z.findOne(Us,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Js.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(document,Ps,Ys,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this)||Js.getOrCreateInstance(this).show()})),N.on(window,Ms,(()=>{for(const t of z.find(Gs))Js.getOrCreateInstance(t)})),m(Js);const Zs=".bs.toast",to=`mouseover${Zs}`,eo=`mouseout${Zs}`,io=`focusin${Zs}`,no=`focusout${Zs}`,so=`hide${Zs}`,oo=`hidden${Zs}`,ro=`show${Zs}`,ao=`shown${Zs}`,lo="hide",co="show",ho="showing",uo={animation:"boolean",autohide:"boolean",delay:"number"},fo={animation:!0,autohide:!0,delay:5e3};class po extends W{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return fo}static get DefaultType(){return uo}static get NAME(){return"toast"}show(){N.trigger(this._element,ro).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(lo),d(this._element),this._element.classList.add(co,ho),this._queueCallback((()=>{this._element.classList.remove(ho),N.trigger(this._element,ao),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(N.trigger(this._element,so).defaultPrevented||(this._element.classList.add(ho),this._queueCallback((()=>{this._element.classList.add(lo),this._element.classList.remove(ho,co),N.trigger(this._element,oo)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(co),super.dispose()}isShown(){return this._element.classList.contains(co)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){N.on(this._element,to,(t=>this._onInteraction(t,!0))),N.on(this._element,eo,(t=>this._onInteraction(t,!1))),N.on(this._element,io,(t=>this._onInteraction(t,!0))),N.on(this._element,no,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=po.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(po),m(po),{Alert:Q,Button:Y,Carousel:Lt,Collapse:Rt,Dropdown:Ki,Modal:kn,Offcanvas:Kn,Popover:bs,ScrollSpy:Ls,Tab:Js,Toast:po,Tooltip:fs}})); +//# sourceMappingURL=bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/static/webui/libs/bootstrap.bundle.min.js.map b/static/webui/libs/bootstrap.bundle.min.js.map new file mode 100644 index 00000000..27957ead --- /dev/null +++ b/static/webui/libs/bootstrap.bundle.min.js.map @@ -0,0 +1 @@ +{"version":3,"names":["elementMap","Map","Data","set","element","key","instance","has","instanceMap","get","size","console","error","Array","from","keys","remove","delete","TRANSITION_END","parseSelector","selector","window","CSS","escape","replace","match","id","triggerTransitionEnd","dispatchEvent","Event","isElement","object","jquery","nodeType","getElement","length","document","querySelector","isVisible","getClientRects","elementIsVisible","getComputedStyle","getPropertyValue","closedDetails","closest","summary","parentNode","isDisabled","Node","ELEMENT_NODE","classList","contains","disabled","hasAttribute","getAttribute","findShadowRoot","documentElement","attachShadow","getRootNode","root","ShadowRoot","noop","reflow","offsetHeight","getjQuery","jQuery","body","DOMContentLoadedCallbacks","isRTL","dir","defineJQueryPlugin","plugin","callback","$","name","NAME","JQUERY_NO_CONFLICT","fn","jQueryInterface","Constructor","noConflict","readyState","addEventListener","push","execute","possibleCallback","args","defaultValue","call","executeAfterTransition","transitionElement","waitForTransition","emulatedDuration","transitionDuration","transitionDelay","floatTransitionDuration","Number","parseFloat","floatTransitionDelay","split","getTransitionDurationFromElement","called","handler","target","removeEventListener","setTimeout","getNextActiveElement","list","activeElement","shouldGetNext","isCycleAllowed","listLength","index","indexOf","Math","max","min","namespaceRegex","stripNameRegex","stripUidRegex","eventRegistry","uidEvent","customEvents","mouseenter","mouseleave","nativeEvents","Set","makeEventUid","uid","getElementEvents","findHandler","events","callable","delegationSelector","Object","values","find","event","normalizeParameters","originalTypeEvent","delegationFunction","isDelegated","typeEvent","getTypeEvent","addHandler","oneOff","wrapFunction","relatedTarget","delegateTarget","this","handlers","previousFunction","domElements","querySelectorAll","domElement","hydrateObj","EventHandler","off","type","apply","bootstrapDelegationHandler","bootstrapHandler","removeHandler","Boolean","removeNamespacedHandlers","namespace","storeElementEvent","handlerKey","entries","includes","on","one","inNamespace","isNamespace","startsWith","elementEvent","slice","keyHandlers","trigger","jQueryEvent","bubbles","nativeDispatch","defaultPrevented","isPropagationStopped","isImmediatePropagationStopped","isDefaultPrevented","evt","cancelable","preventDefault","obj","meta","value","_unused","defineProperty","configurable","normalizeData","toString","JSON","parse","decodeURIComponent","normalizeDataKey","chr","toLowerCase","Manipulator","setDataAttribute","setAttribute","removeDataAttribute","removeAttribute","getDataAttributes","attributes","bsKeys","dataset","filter","pureKey","charAt","getDataAttribute","Config","Default","DefaultType","Error","_getConfig","config","_mergeConfigObj","_configAfterMerge","_typeCheckConfig","jsonConfig","constructor","configTypes","property","expectedTypes","valueType","prototype","RegExp","test","TypeError","toUpperCase","BaseComponent","super","_element","_config","DATA_KEY","dispose","EVENT_KEY","propertyName","getOwnPropertyNames","_queueCallback","isAnimated","getInstance","getOrCreateInstance","VERSION","eventName","getSelector","hrefAttribute","trim","map","sel","join","SelectorEngine","concat","Element","findOne","children","child","matches","parents","ancestor","prev","previous","previousElementSibling","next","nextElementSibling","focusableChildren","focusables","el","getSelectorFromElement","getElementFromSelector","getMultipleElementsFromSelector","enableDismissTrigger","component","method","clickEvent","tagName","EVENT_CLOSE","EVENT_CLOSED","Alert","close","_destroyElement","each","data","undefined","SELECTOR_DATA_TOGGLE","Button","toggle","button","EVENT_TOUCHSTART","EVENT_TOUCHMOVE","EVENT_TOUCHEND","EVENT_POINTERDOWN","EVENT_POINTERUP","endCallback","leftCallback","rightCallback","Swipe","isSupported","_deltaX","_supportPointerEvents","PointerEvent","_initEvents","_start","_eventIsPointerPenTouch","clientX","touches","_end","_handleSwipe","_move","absDeltaX","abs","direction","add","pointerType","navigator","maxTouchPoints","DATA_API_KEY","ARROW_LEFT_KEY","ARROW_RIGHT_KEY","ORDER_NEXT","ORDER_PREV","DIRECTION_LEFT","DIRECTION_RIGHT","EVENT_SLIDE","EVENT_SLID","EVENT_KEYDOWN","EVENT_MOUSEENTER","EVENT_MOUSELEAVE","EVENT_DRAG_START","EVENT_LOAD_DATA_API","EVENT_CLICK_DATA_API","CLASS_NAME_CAROUSEL","CLASS_NAME_ACTIVE","SELECTOR_ACTIVE","SELECTOR_ITEM","SELECTOR_ACTIVE_ITEM","KEY_TO_DIRECTION","ARROW_LEFT_KEY$1","ARROW_RIGHT_KEY$1","interval","keyboard","pause","ride","touch","wrap","Carousel","_interval","_activeElement","_isSliding","touchTimeout","_swipeHelper","_indicatorsElement","_addEventListeners","cycle","_slide","nextWhenVisible","hidden","_clearInterval","_updateInterval","setInterval","_maybeEnableCycle","to","items","_getItems","activeIndex","_getItemIndex","_getActive","order","defaultInterval","_keydown","_addTouchEventListeners","img","swipeConfig","_directionToOrder","endCallBack","clearTimeout","_setActiveIndicatorElement","activeIndicator","newActiveIndicator","elementInterval","parseInt","isNext","nextElement","nextElementIndex","triggerEvent","_orderToDirection","isCycling","directionalClassName","orderClassName","completeCallBack","_isAnimated","clearInterval","carousel","slideIndex","carousels","EVENT_SHOW","EVENT_SHOWN","EVENT_HIDE","EVENT_HIDDEN","CLASS_NAME_SHOW","CLASS_NAME_COLLAPSE","CLASS_NAME_COLLAPSING","CLASS_NAME_DEEPER_CHILDREN","parent","Collapse","_isTransitioning","_triggerArray","toggleList","elem","filterElement","foundElement","_initializeChildren","_addAriaAndCollapsedClass","_isShown","hide","show","activeChildren","_getFirstLevelChildren","activeInstance","dimension","_getDimension","style","scrollSize","complete","getBoundingClientRect","selected","triggerArray","isOpen","top","bottom","right","left","auto","basePlacements","start","end","clippingParents","viewport","popper","reference","variationPlacements","reduce","acc","placement","placements","beforeRead","read","afterRead","beforeMain","main","afterMain","beforeWrite","write","afterWrite","modifierPhases","getNodeName","nodeName","getWindow","node","ownerDocument","defaultView","isHTMLElement","HTMLElement","isShadowRoot","applyStyles$1","enabled","phase","_ref","state","elements","forEach","styles","assign","effect","_ref2","initialStyles","position","options","strategy","margin","arrow","hasOwnProperty","attribute","requires","getBasePlacement","round","getUAString","uaData","userAgentData","brands","isArray","item","brand","version","userAgent","isLayoutViewport","includeScale","isFixedStrategy","clientRect","scaleX","scaleY","offsetWidth","width","height","visualViewport","addVisualOffsets","x","offsetLeft","y","offsetTop","getLayoutRect","rootNode","isSameNode","host","isTableElement","getDocumentElement","getParentNode","assignedSlot","getTrueOffsetParent","offsetParent","getOffsetParent","isFirefox","currentNode","css","transform","perspective","contain","willChange","getContainingBlock","getMainAxisFromPlacement","within","mathMax","mathMin","mergePaddingObject","paddingObject","expandToHashMap","hashMap","arrow$1","_state$modifiersData$","arrowElement","popperOffsets","modifiersData","basePlacement","axis","len","padding","rects","toPaddingObject","arrowRect","minProp","maxProp","endDiff","startDiff","arrowOffsetParent","clientSize","clientHeight","clientWidth","centerToReference","center","offset","axisProp","centerOffset","_options$element","requiresIfExists","getVariation","unsetSides","mapToStyles","_Object$assign2","popperRect","variation","offsets","gpuAcceleration","adaptive","roundOffsets","isFixed","_offsets$x","_offsets$y","_ref3","hasX","hasY","sideX","sideY","win","heightProp","widthProp","_Object$assign","commonStyles","_ref4","dpr","devicePixelRatio","roundOffsetsByDPR","computeStyles$1","_ref5","_options$gpuAccelerat","_options$adaptive","_options$roundOffsets","passive","eventListeners","_options$scroll","scroll","_options$resize","resize","scrollParents","scrollParent","update","hash","getOppositePlacement","matched","getOppositeVariationPlacement","getWindowScroll","scrollLeft","pageXOffset","scrollTop","pageYOffset","getWindowScrollBarX","isScrollParent","_getComputedStyle","overflow","overflowX","overflowY","getScrollParent","listScrollParents","_element$ownerDocumen","isBody","updatedList","rectToClientRect","rect","getClientRectFromMixedType","clippingParent","html","layoutViewport","getViewportRect","clientTop","clientLeft","getInnerBoundingClientRect","winScroll","scrollWidth","scrollHeight","getDocumentRect","computeOffsets","commonX","commonY","mainAxis","detectOverflow","_options","_options$placement","_options$strategy","_options$boundary","boundary","_options$rootBoundary","rootBoundary","_options$elementConte","elementContext","_options$altBoundary","altBoundary","_options$padding","altContext","clippingClientRect","mainClippingParents","clipperElement","getClippingParents","firstClippingParent","clippingRect","accRect","getClippingRect","contextElement","referenceClientRect","popperClientRect","elementClientRect","overflowOffsets","offsetData","multiply","computeAutoPlacement","flipVariations","_options$allowedAutoP","allowedAutoPlacements","allPlacements","allowedPlacements","overflows","sort","a","b","flip$1","_skip","_options$mainAxis","checkMainAxis","_options$altAxis","altAxis","checkAltAxis","specifiedFallbackPlacements","fallbackPlacements","_options$flipVariatio","preferredPlacement","oppositePlacement","getExpandedFallbackPlacements","referenceRect","checksMap","makeFallbackChecks","firstFittingPlacement","i","_basePlacement","isStartVariation","isVertical","mainVariationSide","altVariationSide","checks","every","check","_loop","_i","fittingPlacement","reset","getSideOffsets","preventedOffsets","isAnySideFullyClipped","some","side","hide$1","preventOverflow","referenceOverflow","popperAltOverflow","referenceClippingOffsets","popperEscapeOffsets","isReferenceHidden","hasPopperEscaped","offset$1","_options$offset","invertDistance","skidding","distance","distanceAndSkiddingToXY","_data$state$placement","popperOffsets$1","preventOverflow$1","_options$tether","tether","_options$tetherOffset","tetherOffset","isBasePlacement","tetherOffsetValue","normalizedTetherOffsetValue","offsetModifierState","_offsetModifierState$","mainSide","altSide","additive","minLen","maxLen","arrowPaddingObject","arrowPaddingMin","arrowPaddingMax","arrowLen","minOffset","maxOffset","clientOffset","offsetModifierValue","tetherMax","preventedOffset","_offsetModifierState$2","_mainSide","_altSide","_offset","_len","_min","_max","isOriginSide","_offsetModifierValue","_tetherMin","_tetherMax","_preventedOffset","v","withinMaxClamp","getCompositeRect","elementOrVirtualElement","isOffsetParentAnElement","offsetParentIsScaled","isElementScaled","modifiers","visited","result","modifier","dep","depModifier","DEFAULT_OPTIONS","areValidElements","arguments","_key","popperGenerator","generatorOptions","_generatorOptions","_generatorOptions$def","defaultModifiers","_generatorOptions$def2","defaultOptions","pending","orderedModifiers","effectCleanupFns","isDestroyed","setOptions","setOptionsAction","cleanupModifierEffects","merged","orderModifiers","current","existing","m","_ref$options","cleanupFn","forceUpdate","_state$elements","_state$orderedModifie","_state$orderedModifie2","Promise","resolve","then","destroy","onFirstUpdate","createPopper","computeStyles","applyStyles","flip","ARROW_UP_KEY","ARROW_DOWN_KEY","EVENT_KEYDOWN_DATA_API","EVENT_KEYUP_DATA_API","SELECTOR_DATA_TOGGLE_SHOWN","SELECTOR_MENU","PLACEMENT_TOP","PLACEMENT_TOPEND","PLACEMENT_BOTTOM","PLACEMENT_BOTTOMEND","PLACEMENT_RIGHT","PLACEMENT_LEFT","autoClose","display","popperConfig","Dropdown","_popper","_parent","_menu","_inNavbar","_detectNavbar","_createPopper","focus","_completeHide","Popper","referenceElement","_getPopperConfig","_getPlacement","parentDropdown","isEnd","_getOffset","popperData","defaultBsPopperConfig","_selectMenuItem","clearMenus","openToggles","context","composedPath","isMenuTarget","dataApiKeydownHandler","isInput","isEscapeEvent","isUpOrDownEvent","getToggleButton","stopPropagation","EVENT_MOUSEDOWN","className","clickCallback","rootElement","Backdrop","_isAppended","_append","_getElement","_emulateAnimation","backdrop","createElement","append","EVENT_FOCUSIN","EVENT_KEYDOWN_TAB","TAB_NAV_BACKWARD","autofocus","trapElement","FocusTrap","_isActive","_lastTabNavDirection","activate","_handleFocusin","_handleKeydown","deactivate","shiftKey","SELECTOR_FIXED_CONTENT","SELECTOR_STICKY_CONTENT","PROPERTY_PADDING","PROPERTY_MARGIN","ScrollBarHelper","getWidth","documentWidth","innerWidth","_disableOverFlow","_setElementAttributes","calculatedValue","_resetElementAttributes","isOverflowing","_saveInitialAttribute","styleProperty","scrollbarWidth","_applyManipulationCallback","setProperty","actualValue","removeProperty","callBack","EVENT_HIDE_PREVENTED","EVENT_RESIZE","EVENT_CLICK_DISMISS","EVENT_MOUSEDOWN_DISMISS","EVENT_KEYDOWN_DISMISS","CLASS_NAME_OPEN","CLASS_NAME_STATIC","Modal","_dialog","_backdrop","_initializeBackDrop","_focustrap","_initializeFocusTrap","_scrollBar","_adjustDialog","_showElement","_hideModal","handleUpdate","modalBody","transitionComplete","_triggerBackdropTransition","event2","_resetAdjustments","isModalOverflowing","initialOverflowY","isBodyOverflowing","paddingLeft","paddingRight","showEvent","alreadyOpen","CLASS_NAME_SHOWING","CLASS_NAME_HIDING","OPEN_SELECTOR","Offcanvas","blur","completeCallback","DefaultAllowlist","area","br","col","code","dd","div","dl","dt","em","hr","h1","h2","h3","h4","h5","h6","li","ol","p","pre","s","small","span","sub","sup","strong","u","ul","uriAttributes","SAFE_URL_PATTERN","allowedAttribute","allowedAttributeList","attributeName","nodeValue","attributeRegex","regex","allowList","content","extraClass","sanitize","sanitizeFn","template","DefaultContentType","entry","TemplateFactory","getContent","_resolvePossibleFunction","hasContent","changeContent","_checkContent","toHtml","templateWrapper","innerHTML","_maybeSanitize","text","_setContent","arg","templateElement","_putElementInTemplate","textContent","unsafeHtml","sanitizeFunction","createdDocument","DOMParser","parseFromString","elementName","attributeList","allowedAttributes","sanitizeHtml","DISALLOWED_ATTRIBUTES","CLASS_NAME_FADE","SELECTOR_TOOLTIP_INNER","SELECTOR_MODAL","EVENT_MODAL_HIDE","TRIGGER_HOVER","TRIGGER_FOCUS","TRIGGER_CLICK","AttachmentMap","AUTO","TOP","RIGHT","BOTTOM","LEFT","animation","container","customClass","delay","title","Tooltip","_isEnabled","_timeout","_isHovered","_activeTrigger","_templateFactory","_newContent","tip","_setListeners","_fixTitle","enable","disable","toggleEnabled","_leave","_enter","_hideModalHandler","_disposePopper","_isWithContent","isInTheDom","_getTipElement","_isWithActiveTrigger","_getTitle","_createTipElement","_getContentForTemplate","_getTemplateFactory","tipId","prefix","floor","random","getElementById","getUID","setContent","_initializeOnDelegatedTarget","_getDelegateConfig","attachment","triggers","eventIn","eventOut","_setTimeout","timeout","dataAttributes","dataAttribute","SELECTOR_TITLE","SELECTOR_CONTENT","Popover","_getContent","EVENT_ACTIVATE","EVENT_CLICK","SELECTOR_TARGET_LINKS","SELECTOR_NAV_LINKS","SELECTOR_LINK_ITEMS","rootMargin","smoothScroll","threshold","ScrollSpy","_targetLinks","_observableSections","_rootElement","_activeTarget","_observer","_previousScrollData","visibleEntryTop","parentScrollTop","refresh","_initializeTargetsAndObservables","_maybeEnableSmoothScroll","disconnect","_getNewObserver","section","observe","observableSection","scrollTo","behavior","IntersectionObserver","_observerCallback","targetElement","_process","userScrollsDown","isIntersecting","_clearActiveClass","entryIsLowerThanPrevious","targetLinks","anchor","decodeURI","_activateParents","listGroup","activeNodes","spy","HOME_KEY","END_KEY","SELECTOR_DROPDOWN_TOGGLE","NOT_SELECTOR_DROPDOWN_TOGGLE","SELECTOR_INNER_ELEM","SELECTOR_DATA_TOGGLE_ACTIVE","Tab","_setInitialAttributes","_getChildren","innerElem","_elemIsActive","active","_getActiveElem","hideEvent","_deactivate","_activate","relatedElem","_toggleDropDown","nextActiveElement","preventScroll","_setAttributeIfNotExists","_setInitialAttributesOnChild","_getInnerElement","isActive","outerElem","_getOuterElement","_setInitialAttributesOnTargetPanel","open","EVENT_MOUSEOVER","EVENT_MOUSEOUT","EVENT_FOCUSOUT","CLASS_NAME_HIDE","autohide","Toast","_hasMouseInteraction","_hasKeyboardInteraction","_clearTimeout","_maybeScheduleHide","isShown","_onInteraction","isInteracting"],"sources":["../../js/src/dom/data.js","../../js/src/util/index.js","../../js/src/dom/event-handler.js","../../js/src/dom/manipulator.js","../../js/src/util/config.js","../../js/src/base-component.js","../../js/src/dom/selector-engine.js","../../js/src/util/component-functions.js","../../js/src/alert.js","../../js/src/button.js","../../js/src/util/swipe.js","../../js/src/carousel.js","../../js/src/collapse.js","../../node_modules/@popperjs/core/lib/enums.js","../../node_modules/@popperjs/core/lib/dom-utils/getNodeName.js","../../node_modules/@popperjs/core/lib/dom-utils/getWindow.js","../../node_modules/@popperjs/core/lib/dom-utils/instanceOf.js","../../node_modules/@popperjs/core/lib/modifiers/applyStyles.js","../../node_modules/@popperjs/core/lib/utils/getBasePlacement.js","../../node_modules/@popperjs/core/lib/utils/math.js","../../node_modules/@popperjs/core/lib/utils/userAgent.js","../../node_modules/@popperjs/core/lib/dom-utils/isLayoutViewport.js","../../node_modules/@popperjs/core/lib/dom-utils/getBoundingClientRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getLayoutRect.js","../../node_modules/@popperjs/core/lib/dom-utils/contains.js","../../node_modules/@popperjs/core/lib/dom-utils/getComputedStyle.js","../../node_modules/@popperjs/core/lib/dom-utils/isTableElement.js","../../node_modules/@popperjs/core/lib/dom-utils/getDocumentElement.js","../../node_modules/@popperjs/core/lib/dom-utils/getParentNode.js","../../node_modules/@popperjs/core/lib/dom-utils/getOffsetParent.js","../../node_modules/@popperjs/core/lib/utils/getMainAxisFromPlacement.js","../../node_modules/@popperjs/core/lib/utils/within.js","../../node_modules/@popperjs/core/lib/utils/mergePaddingObject.js","../../node_modules/@popperjs/core/lib/utils/getFreshSideObject.js","../../node_modules/@popperjs/core/lib/utils/expandToHashMap.js","../../node_modules/@popperjs/core/lib/modifiers/arrow.js","../../node_modules/@popperjs/core/lib/utils/getVariation.js","../../node_modules/@popperjs/core/lib/modifiers/computeStyles.js","../../node_modules/@popperjs/core/lib/modifiers/eventListeners.js","../../node_modules/@popperjs/core/lib/utils/getOppositePlacement.js","../../node_modules/@popperjs/core/lib/utils/getOppositeVariationPlacement.js","../../node_modules/@popperjs/core/lib/dom-utils/getWindowScroll.js","../../node_modules/@popperjs/core/lib/dom-utils/getWindowScrollBarX.js","../../node_modules/@popperjs/core/lib/dom-utils/isScrollParent.js","../../node_modules/@popperjs/core/lib/dom-utils/getScrollParent.js","../../node_modules/@popperjs/core/lib/dom-utils/listScrollParents.js","../../node_modules/@popperjs/core/lib/utils/rectToClientRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getClippingRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getViewportRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getDocumentRect.js","../../node_modules/@popperjs/core/lib/utils/computeOffsets.js","../../node_modules/@popperjs/core/lib/utils/detectOverflow.js","../../node_modules/@popperjs/core/lib/utils/computeAutoPlacement.js","../../node_modules/@popperjs/core/lib/modifiers/flip.js","../../node_modules/@popperjs/core/lib/modifiers/hide.js","../../node_modules/@popperjs/core/lib/modifiers/offset.js","../../node_modules/@popperjs/core/lib/modifiers/popperOffsets.js","../../node_modules/@popperjs/core/lib/modifiers/preventOverflow.js","../../node_modules/@popperjs/core/lib/utils/getAltAxis.js","../../node_modules/@popperjs/core/lib/dom-utils/getCompositeRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getNodeScroll.js","../../node_modules/@popperjs/core/lib/dom-utils/getHTMLElementScroll.js","../../node_modules/@popperjs/core/lib/utils/orderModifiers.js","../../node_modules/@popperjs/core/lib/createPopper.js","../../node_modules/@popperjs/core/lib/utils/debounce.js","../../node_modules/@popperjs/core/lib/utils/mergeByName.js","../../node_modules/@popperjs/core/lib/popper-lite.js","../../node_modules/@popperjs/core/lib/popper.js","../../js/src/dropdown.js","../../js/src/util/backdrop.js","../../js/src/util/focustrap.js","../../js/src/util/scrollbar.js","../../js/src/modal.js","../../js/src/offcanvas.js","../../js/src/util/sanitizer.js","../../js/src/util/template-factory.js","../../js/src/tooltip.js","../../js/src/popover.js","../../js/src/scrollspy.js","../../js/src/tab.js","../../js/src/toast.js","../../js/index.umd.js"],"sourcesContent":["/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/data.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n/**\n * Constants\n */\n\nconst elementMap = new Map()\n\nexport default {\n set(element, key, instance) {\n if (!elementMap.has(element)) {\n elementMap.set(element, new Map())\n }\n\n const instanceMap = elementMap.get(element)\n\n // make it clear we only want one instance per element\n // can be removed later when multiple key/instances are fine to be used\n if (!instanceMap.has(key) && instanceMap.size !== 0) {\n // eslint-disable-next-line no-console\n console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`)\n return\n }\n\n instanceMap.set(key, instance)\n },\n\n get(element, key) {\n if (elementMap.has(element)) {\n return elementMap.get(element).get(key) || null\n }\n\n return null\n },\n\n remove(element, key) {\n if (!elementMap.has(element)) {\n return\n }\n\n const instanceMap = elementMap.get(element)\n\n instanceMap.delete(key)\n\n // free up element references if there are no instances left for an element\n if (instanceMap.size === 0) {\n elementMap.delete(element)\n }\n }\n}\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/index.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst MAX_UID = 1_000_000\nconst MILLISECONDS_MULTIPLIER = 1000\nconst TRANSITION_END = 'transitionend'\n\n/**\n * Properly escape IDs selectors to handle weird IDs\n * @param {string} selector\n * @returns {string}\n */\nconst parseSelector = selector => {\n if (selector && window.CSS && window.CSS.escape) {\n // document.querySelector needs escaping to handle IDs (html5+) containing for instance /\n selector = selector.replace(/#([^\\s\"#']+)/g, (match, id) => `#${CSS.escape(id)}`)\n }\n\n return selector\n}\n\n// Shout-out Angus Croll (https://goo.gl/pxwQGp)\nconst toType = object => {\n if (object === null || object === undefined) {\n return `${object}`\n }\n\n return Object.prototype.toString.call(object).match(/\\s([a-z]+)/i)[1].toLowerCase()\n}\n\n/**\n * Public Util API\n */\n\nconst getUID = prefix => {\n do {\n prefix += Math.floor(Math.random() * MAX_UID)\n } while (document.getElementById(prefix))\n\n return prefix\n}\n\nconst getTransitionDurationFromElement = element => {\n if (!element) {\n return 0\n }\n\n // Get transition-duration of the element\n let { transitionDuration, transitionDelay } = window.getComputedStyle(element)\n\n const floatTransitionDuration = Number.parseFloat(transitionDuration)\n const floatTransitionDelay = Number.parseFloat(transitionDelay)\n\n // Return 0 if element or transition duration is not found\n if (!floatTransitionDuration && !floatTransitionDelay) {\n return 0\n }\n\n // If multiple durations are defined, take the first\n transitionDuration = transitionDuration.split(',')[0]\n transitionDelay = transitionDelay.split(',')[0]\n\n return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER\n}\n\nconst triggerTransitionEnd = element => {\n element.dispatchEvent(new Event(TRANSITION_END))\n}\n\nconst isElement = object => {\n if (!object || typeof object !== 'object') {\n return false\n }\n\n if (typeof object.jquery !== 'undefined') {\n object = object[0]\n }\n\n return typeof object.nodeType !== 'undefined'\n}\n\nconst getElement = object => {\n // it's a jQuery object or a node element\n if (isElement(object)) {\n return object.jquery ? object[0] : object\n }\n\n if (typeof object === 'string' && object.length > 0) {\n return document.querySelector(parseSelector(object))\n }\n\n return null\n}\n\nconst isVisible = element => {\n if (!isElement(element) || element.getClientRects().length === 0) {\n return false\n }\n\n const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible'\n // Handle `details` element as its content may falsie appear visible when it is closed\n const closedDetails = element.closest('details:not([open])')\n\n if (!closedDetails) {\n return elementIsVisible\n }\n\n if (closedDetails !== element) {\n const summary = element.closest('summary')\n if (summary && summary.parentNode !== closedDetails) {\n return false\n }\n\n if (summary === null) {\n return false\n }\n }\n\n return elementIsVisible\n}\n\nconst isDisabled = element => {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return true\n }\n\n if (element.classList.contains('disabled')) {\n return true\n }\n\n if (typeof element.disabled !== 'undefined') {\n return element.disabled\n }\n\n return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'\n}\n\nconst findShadowRoot = element => {\n if (!document.documentElement.attachShadow) {\n return null\n }\n\n // Can find the shadow root otherwise it'll return the document\n if (typeof element.getRootNode === 'function') {\n const root = element.getRootNode()\n return root instanceof ShadowRoot ? root : null\n }\n\n if (element instanceof ShadowRoot) {\n return element\n }\n\n // when we don't find a shadow root\n if (!element.parentNode) {\n return null\n }\n\n return findShadowRoot(element.parentNode)\n}\n\nconst noop = () => {}\n\n/**\n * Trick to restart an element's animation\n *\n * @param {HTMLElement} element\n * @return void\n *\n * @see https://www.harrytheo.com/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation\n */\nconst reflow = element => {\n element.offsetHeight // eslint-disable-line no-unused-expressions\n}\n\nconst getjQuery = () => {\n if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {\n return window.jQuery\n }\n\n return null\n}\n\nconst DOMContentLoadedCallbacks = []\n\nconst onDOMContentLoaded = callback => {\n if (document.readyState === 'loading') {\n // add listener on the first call when the document is in loading state\n if (!DOMContentLoadedCallbacks.length) {\n document.addEventListener('DOMContentLoaded', () => {\n for (const callback of DOMContentLoadedCallbacks) {\n callback()\n }\n })\n }\n\n DOMContentLoadedCallbacks.push(callback)\n } else {\n callback()\n }\n}\n\nconst isRTL = () => document.documentElement.dir === 'rtl'\n\nconst defineJQueryPlugin = plugin => {\n onDOMContentLoaded(() => {\n const $ = getjQuery()\n /* istanbul ignore if */\n if ($) {\n const name = plugin.NAME\n const JQUERY_NO_CONFLICT = $.fn[name]\n $.fn[name] = plugin.jQueryInterface\n $.fn[name].Constructor = plugin\n $.fn[name].noConflict = () => {\n $.fn[name] = JQUERY_NO_CONFLICT\n return plugin.jQueryInterface\n }\n }\n })\n}\n\nconst execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {\n return typeof possibleCallback === 'function' ? possibleCallback.call(...args) : defaultValue\n}\n\nconst executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {\n if (!waitForTransition) {\n execute(callback)\n return\n }\n\n const durationPadding = 5\n const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding\n\n let called = false\n\n const handler = ({ target }) => {\n if (target !== transitionElement) {\n return\n }\n\n called = true\n transitionElement.removeEventListener(TRANSITION_END, handler)\n execute(callback)\n }\n\n transitionElement.addEventListener(TRANSITION_END, handler)\n setTimeout(() => {\n if (!called) {\n triggerTransitionEnd(transitionElement)\n }\n }, emulatedDuration)\n}\n\n/**\n * Return the previous/next element of a list.\n *\n * @param {array} list The list of elements\n * @param activeElement The active element\n * @param shouldGetNext Choose to get next or previous element\n * @param isCycleAllowed\n * @return {Element|elem} The proper element\n */\nconst getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {\n const listLength = list.length\n let index = list.indexOf(activeElement)\n\n // if the element does not exist in the list return an element\n // depending on the direction and if cycle is allowed\n if (index === -1) {\n return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0]\n }\n\n index += shouldGetNext ? 1 : -1\n\n if (isCycleAllowed) {\n index = (index + listLength) % listLength\n }\n\n return list[Math.max(0, Math.min(index, listLength - 1))]\n}\n\nexport {\n defineJQueryPlugin,\n execute,\n executeAfterTransition,\n findShadowRoot,\n getElement,\n getjQuery,\n getNextActiveElement,\n getTransitionDurationFromElement,\n getUID,\n isDisabled,\n isElement,\n isRTL,\n isVisible,\n noop,\n onDOMContentLoaded,\n parseSelector,\n reflow,\n triggerTransitionEnd,\n toType\n}\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/event-handler.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport { getjQuery } from '../util/index.js'\n\n/**\n * Constants\n */\n\nconst namespaceRegex = /[^.]*(?=\\..*)\\.|.*/\nconst stripNameRegex = /\\..*/\nconst stripUidRegex = /::\\d+$/\nconst eventRegistry = {} // Events storage\nlet uidEvent = 1\nconst customEvents = {\n mouseenter: 'mouseover',\n mouseleave: 'mouseout'\n}\n\nconst nativeEvents = new Set([\n 'click',\n 'dblclick',\n 'mouseup',\n 'mousedown',\n 'contextmenu',\n 'mousewheel',\n 'DOMMouseScroll',\n 'mouseover',\n 'mouseout',\n 'mousemove',\n 'selectstart',\n 'selectend',\n 'keydown',\n 'keypress',\n 'keyup',\n 'orientationchange',\n 'touchstart',\n 'touchmove',\n 'touchend',\n 'touchcancel',\n 'pointerdown',\n 'pointermove',\n 'pointerup',\n 'pointerleave',\n 'pointercancel',\n 'gesturestart',\n 'gesturechange',\n 'gestureend',\n 'focus',\n 'blur',\n 'change',\n 'reset',\n 'select',\n 'submit',\n 'focusin',\n 'focusout',\n 'load',\n 'unload',\n 'beforeunload',\n 'resize',\n 'move',\n 'DOMContentLoaded',\n 'readystatechange',\n 'error',\n 'abort',\n 'scroll'\n])\n\n/**\n * Private methods\n */\n\nfunction makeEventUid(element, uid) {\n return (uid && `${uid}::${uidEvent++}`) || element.uidEvent || uidEvent++\n}\n\nfunction getElementEvents(element) {\n const uid = makeEventUid(element)\n\n element.uidEvent = uid\n eventRegistry[uid] = eventRegistry[uid] || {}\n\n return eventRegistry[uid]\n}\n\nfunction bootstrapHandler(element, fn) {\n return function handler(event) {\n hydrateObj(event, { delegateTarget: element })\n\n if (handler.oneOff) {\n EventHandler.off(element, event.type, fn)\n }\n\n return fn.apply(element, [event])\n }\n}\n\nfunction bootstrapDelegationHandler(element, selector, fn) {\n return function handler(event) {\n const domElements = element.querySelectorAll(selector)\n\n for (let { target } = event; target && target !== this; target = target.parentNode) {\n for (const domElement of domElements) {\n if (domElement !== target) {\n continue\n }\n\n hydrateObj(event, { delegateTarget: target })\n\n if (handler.oneOff) {\n EventHandler.off(element, event.type, selector, fn)\n }\n\n return fn.apply(target, [event])\n }\n }\n }\n}\n\nfunction findHandler(events, callable, delegationSelector = null) {\n return Object.values(events)\n .find(event => event.callable === callable && event.delegationSelector === delegationSelector)\n}\n\nfunction normalizeParameters(originalTypeEvent, handler, delegationFunction) {\n const isDelegated = typeof handler === 'string'\n // TODO: tooltip passes `false` instead of selector, so we need to check\n const callable = isDelegated ? delegationFunction : (handler || delegationFunction)\n let typeEvent = getTypeEvent(originalTypeEvent)\n\n if (!nativeEvents.has(typeEvent)) {\n typeEvent = originalTypeEvent\n }\n\n return [isDelegated, callable, typeEvent]\n}\n\nfunction addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return\n }\n\n let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction)\n\n // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position\n // this prevents the handler from being dispatched the same way as mouseover or mouseout does\n if (originalTypeEvent in customEvents) {\n const wrapFunction = fn => {\n return function (event) {\n if (!event.relatedTarget || (event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget))) {\n return fn.call(this, event)\n }\n }\n }\n\n callable = wrapFunction(callable)\n }\n\n const events = getElementEvents(element)\n const handlers = events[typeEvent] || (events[typeEvent] = {})\n const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null)\n\n if (previousFunction) {\n previousFunction.oneOff = previousFunction.oneOff && oneOff\n\n return\n }\n\n const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, ''))\n const fn = isDelegated ?\n bootstrapDelegationHandler(element, handler, callable) :\n bootstrapHandler(element, callable)\n\n fn.delegationSelector = isDelegated ? handler : null\n fn.callable = callable\n fn.oneOff = oneOff\n fn.uidEvent = uid\n handlers[uid] = fn\n\n element.addEventListener(typeEvent, fn, isDelegated)\n}\n\nfunction removeHandler(element, events, typeEvent, handler, delegationSelector) {\n const fn = findHandler(events[typeEvent], handler, delegationSelector)\n\n if (!fn) {\n return\n }\n\n element.removeEventListener(typeEvent, fn, Boolean(delegationSelector))\n delete events[typeEvent][fn.uidEvent]\n}\n\nfunction removeNamespacedHandlers(element, events, typeEvent, namespace) {\n const storeElementEvent = events[typeEvent] || {}\n\n for (const [handlerKey, event] of Object.entries(storeElementEvent)) {\n if (handlerKey.includes(namespace)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector)\n }\n }\n}\n\nfunction getTypeEvent(event) {\n // allow to get the native events from namespaced events ('click.bs.button' --> 'click')\n event = event.replace(stripNameRegex, '')\n return customEvents[event] || event\n}\n\nconst EventHandler = {\n on(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, false)\n },\n\n one(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, true)\n },\n\n off(element, originalTypeEvent, handler, delegationFunction) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return\n }\n\n const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction)\n const inNamespace = typeEvent !== originalTypeEvent\n const events = getElementEvents(element)\n const storeElementEvent = events[typeEvent] || {}\n const isNamespace = originalTypeEvent.startsWith('.')\n\n if (typeof callable !== 'undefined') {\n // Simplest case: handler is passed, remove that listener ONLY.\n if (!Object.keys(storeElementEvent).length) {\n return\n }\n\n removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null)\n return\n }\n\n if (isNamespace) {\n for (const elementEvent of Object.keys(events)) {\n removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1))\n }\n }\n\n for (const [keyHandlers, event] of Object.entries(storeElementEvent)) {\n const handlerKey = keyHandlers.replace(stripUidRegex, '')\n\n if (!inNamespace || originalTypeEvent.includes(handlerKey)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector)\n }\n }\n },\n\n trigger(element, event, args) {\n if (typeof event !== 'string' || !element) {\n return null\n }\n\n const $ = getjQuery()\n const typeEvent = getTypeEvent(event)\n const inNamespace = event !== typeEvent\n\n let jQueryEvent = null\n let bubbles = true\n let nativeDispatch = true\n let defaultPrevented = false\n\n if (inNamespace && $) {\n jQueryEvent = $.Event(event, args)\n\n $(element).trigger(jQueryEvent)\n bubbles = !jQueryEvent.isPropagationStopped()\n nativeDispatch = !jQueryEvent.isImmediatePropagationStopped()\n defaultPrevented = jQueryEvent.isDefaultPrevented()\n }\n\n const evt = hydrateObj(new Event(event, { bubbles, cancelable: true }), args)\n\n if (defaultPrevented) {\n evt.preventDefault()\n }\n\n if (nativeDispatch) {\n element.dispatchEvent(evt)\n }\n\n if (evt.defaultPrevented && jQueryEvent) {\n jQueryEvent.preventDefault()\n }\n\n return evt\n }\n}\n\nfunction hydrateObj(obj, meta = {}) {\n for (const [key, value] of Object.entries(meta)) {\n try {\n obj[key] = value\n } catch {\n Object.defineProperty(obj, key, {\n configurable: true,\n get() {\n return value\n }\n })\n }\n }\n\n return obj\n}\n\nexport default EventHandler\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/manipulator.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nfunction normalizeData(value) {\n if (value === 'true') {\n return true\n }\n\n if (value === 'false') {\n return false\n }\n\n if (value === Number(value).toString()) {\n return Number(value)\n }\n\n if (value === '' || value === 'null') {\n return null\n }\n\n if (typeof value !== 'string') {\n return value\n }\n\n try {\n return JSON.parse(decodeURIComponent(value))\n } catch {\n return value\n }\n}\n\nfunction normalizeDataKey(key) {\n return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`)\n}\n\nconst Manipulator = {\n setDataAttribute(element, key, value) {\n element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value)\n },\n\n removeDataAttribute(element, key) {\n element.removeAttribute(`data-bs-${normalizeDataKey(key)}`)\n },\n\n getDataAttributes(element) {\n if (!element) {\n return {}\n }\n\n const attributes = {}\n const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'))\n\n for (const key of bsKeys) {\n let pureKey = key.replace(/^bs/, '')\n pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1)\n attributes[pureKey] = normalizeData(element.dataset[key])\n }\n\n return attributes\n },\n\n getDataAttribute(element, key) {\n return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`))\n }\n}\n\nexport default Manipulator\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/config.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport Manipulator from '../dom/manipulator.js'\nimport { isElement, toType } from './index.js'\n\n/**\n * Class definition\n */\n\nclass Config {\n // Getters\n static get Default() {\n return {}\n }\n\n static get DefaultType() {\n return {}\n }\n\n static get NAME() {\n throw new Error('You have to implement the static method \"NAME\", for each component!')\n }\n\n _getConfig(config) {\n config = this._mergeConfigObj(config)\n config = this._configAfterMerge(config)\n this._typeCheckConfig(config)\n return config\n }\n\n _configAfterMerge(config) {\n return config\n }\n\n _mergeConfigObj(config, element) {\n const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {} // try to parse\n\n return {\n ...this.constructor.Default,\n ...(typeof jsonConfig === 'object' ? jsonConfig : {}),\n ...(isElement(element) ? Manipulator.getDataAttributes(element) : {}),\n ...(typeof config === 'object' ? config : {})\n }\n }\n\n _typeCheckConfig(config, configTypes = this.constructor.DefaultType) {\n for (const [property, expectedTypes] of Object.entries(configTypes)) {\n const value = config[property]\n const valueType = isElement(value) ? 'element' : toType(value)\n\n if (!new RegExp(expectedTypes).test(valueType)) {\n throw new TypeError(\n `${this.constructor.NAME.toUpperCase()}: Option \"${property}\" provided type \"${valueType}\" but expected type \"${expectedTypes}\".`\n )\n }\n }\n }\n}\n\nexport default Config\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap base-component.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport Data from './dom/data.js'\nimport EventHandler from './dom/event-handler.js'\nimport Config from './util/config.js'\nimport { executeAfterTransition, getElement } from './util/index.js'\n\n/**\n * Constants\n */\n\nconst VERSION = '5.3.7'\n\n/**\n * Class definition\n */\n\nclass BaseComponent extends Config {\n constructor(element, config) {\n super()\n\n element = getElement(element)\n if (!element) {\n return\n }\n\n this._element = element\n this._config = this._getConfig(config)\n\n Data.set(this._element, this.constructor.DATA_KEY, this)\n }\n\n // Public\n dispose() {\n Data.remove(this._element, this.constructor.DATA_KEY)\n EventHandler.off(this._element, this.constructor.EVENT_KEY)\n\n for (const propertyName of Object.getOwnPropertyNames(this)) {\n this[propertyName] = null\n }\n }\n\n // Private\n _queueCallback(callback, element, isAnimated = true) {\n executeAfterTransition(callback, element, isAnimated)\n }\n\n _getConfig(config) {\n config = this._mergeConfigObj(config, this._element)\n config = this._configAfterMerge(config)\n this._typeCheckConfig(config)\n return config\n }\n\n // Static\n static getInstance(element) {\n return Data.get(getElement(element), this.DATA_KEY)\n }\n\n static getOrCreateInstance(element, config = {}) {\n return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null)\n }\n\n static get VERSION() {\n return VERSION\n }\n\n static get DATA_KEY() {\n return `bs.${this.NAME}`\n }\n\n static get EVENT_KEY() {\n return `.${this.DATA_KEY}`\n }\n\n static eventName(name) {\n return `${name}${this.EVENT_KEY}`\n }\n}\n\nexport default BaseComponent\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/selector-engine.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport { isDisabled, isVisible, parseSelector } from '../util/index.js'\n\nconst getSelector = element => {\n let selector = element.getAttribute('data-bs-target')\n\n if (!selector || selector === '#') {\n let hrefAttribute = element.getAttribute('href')\n\n // The only valid content that could double as a selector are IDs or classes,\n // so everything starting with `#` or `.`. If a \"real\" URL is used as the selector,\n // `document.querySelector` will rightfully complain it is invalid.\n // See https://github.com/twbs/bootstrap/issues/32273\n if (!hrefAttribute || (!hrefAttribute.includes('#') && !hrefAttribute.startsWith('.'))) {\n return null\n }\n\n // Just in case some CMS puts out a full URL with the anchor appended\n if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) {\n hrefAttribute = `#${hrefAttribute.split('#')[1]}`\n }\n\n selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null\n }\n\n return selector ? selector.split(',').map(sel => parseSelector(sel)).join(',') : null\n}\n\nconst SelectorEngine = {\n find(selector, element = document.documentElement) {\n return [].concat(...Element.prototype.querySelectorAll.call(element, selector))\n },\n\n findOne(selector, element = document.documentElement) {\n return Element.prototype.querySelector.call(element, selector)\n },\n\n children(element, selector) {\n return [].concat(...element.children).filter(child => child.matches(selector))\n },\n\n parents(element, selector) {\n const parents = []\n let ancestor = element.parentNode.closest(selector)\n\n while (ancestor) {\n parents.push(ancestor)\n ancestor = ancestor.parentNode.closest(selector)\n }\n\n return parents\n },\n\n prev(element, selector) {\n let previous = element.previousElementSibling\n\n while (previous) {\n if (previous.matches(selector)) {\n return [previous]\n }\n\n previous = previous.previousElementSibling\n }\n\n return []\n },\n // TODO: this is now unused; remove later along with prev()\n next(element, selector) {\n let next = element.nextElementSibling\n\n while (next) {\n if (next.matches(selector)) {\n return [next]\n }\n\n next = next.nextElementSibling\n }\n\n return []\n },\n\n focusableChildren(element) {\n const focusables = [\n 'a',\n 'button',\n 'input',\n 'textarea',\n 'select',\n 'details',\n '[tabindex]',\n '[contenteditable=\"true\"]'\n ].map(selector => `${selector}:not([tabindex^=\"-\"])`).join(',')\n\n return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el))\n },\n\n getSelectorFromElement(element) {\n const selector = getSelector(element)\n\n if (selector) {\n return SelectorEngine.findOne(selector) ? selector : null\n }\n\n return null\n },\n\n getElementFromSelector(element) {\n const selector = getSelector(element)\n\n return selector ? SelectorEngine.findOne(selector) : null\n },\n\n getMultipleElementsFromSelector(element) {\n const selector = getSelector(element)\n\n return selector ? SelectorEngine.find(selector) : []\n }\n}\n\nexport default SelectorEngine\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/component-functions.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport EventHandler from '../dom/event-handler.js'\nimport SelectorEngine from '../dom/selector-engine.js'\nimport { isDisabled } from './index.js'\n\nconst enableDismissTrigger = (component, method = 'hide') => {\n const clickEvent = `click.dismiss${component.EVENT_KEY}`\n const name = component.NAME\n\n EventHandler.on(document, clickEvent, `[data-bs-dismiss=\"${name}\"]`, function (event) {\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault()\n }\n\n if (isDisabled(this)) {\n return\n }\n\n const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`)\n const instance = component.getOrCreateInstance(target)\n\n // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method\n instance[method]()\n })\n}\n\nexport {\n enableDismissTrigger\n}\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap alert.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport { enableDismissTrigger } from './util/component-functions.js'\nimport { defineJQueryPlugin } from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'alert'\nconst DATA_KEY = 'bs.alert'\nconst EVENT_KEY = `.${DATA_KEY}`\n\nconst EVENT_CLOSE = `close${EVENT_KEY}`\nconst EVENT_CLOSED = `closed${EVENT_KEY}`\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_SHOW = 'show'\n\n/**\n * Class definition\n */\n\nclass Alert extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME\n }\n\n // Public\n close() {\n const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE)\n\n if (closeEvent.defaultPrevented) {\n return\n }\n\n this._element.classList.remove(CLASS_NAME_SHOW)\n\n const isAnimated = this._element.classList.contains(CLASS_NAME_FADE)\n this._queueCallback(() => this._destroyElement(), this._element, isAnimated)\n }\n\n // Private\n _destroyElement() {\n this._element.remove()\n EventHandler.trigger(this._element, EVENT_CLOSED)\n this.dispose()\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Alert.getOrCreateInstance(this)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config](this)\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nenableDismissTrigger(Alert, 'close')\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Alert)\n\nexport default Alert\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap button.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport { defineJQueryPlugin } from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'button'\nconst DATA_KEY = 'bs.button'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst CLASS_NAME_ACTIVE = 'active'\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"button\"]'\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\n/**\n * Class definition\n */\n\nclass Button extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME\n }\n\n // Public\n toggle() {\n // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method\n this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE))\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Button.getOrCreateInstance(this)\n\n if (config === 'toggle') {\n data[config]()\n }\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => {\n event.preventDefault()\n\n const button = event.target.closest(SELECTOR_DATA_TOGGLE)\n const data = Button.getOrCreateInstance(button)\n\n data.toggle()\n})\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Button)\n\nexport default Button\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/swipe.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport EventHandler from '../dom/event-handler.js'\nimport Config from './config.js'\nimport { execute } from './index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'swipe'\nconst EVENT_KEY = '.bs.swipe'\nconst EVENT_TOUCHSTART = `touchstart${EVENT_KEY}`\nconst EVENT_TOUCHMOVE = `touchmove${EVENT_KEY}`\nconst EVENT_TOUCHEND = `touchend${EVENT_KEY}`\nconst EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`\nconst EVENT_POINTERUP = `pointerup${EVENT_KEY}`\nconst POINTER_TYPE_TOUCH = 'touch'\nconst POINTER_TYPE_PEN = 'pen'\nconst CLASS_NAME_POINTER_EVENT = 'pointer-event'\nconst SWIPE_THRESHOLD = 40\n\nconst Default = {\n endCallback: null,\n leftCallback: null,\n rightCallback: null\n}\n\nconst DefaultType = {\n endCallback: '(function|null)',\n leftCallback: '(function|null)',\n rightCallback: '(function|null)'\n}\n\n/**\n * Class definition\n */\n\nclass Swipe extends Config {\n constructor(element, config) {\n super()\n this._element = element\n\n if (!element || !Swipe.isSupported()) {\n return\n }\n\n this._config = this._getConfig(config)\n this._deltaX = 0\n this._supportPointerEvents = Boolean(window.PointerEvent)\n this._initEvents()\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n dispose() {\n EventHandler.off(this._element, EVENT_KEY)\n }\n\n // Private\n _start(event) {\n if (!this._supportPointerEvents) {\n this._deltaX = event.touches[0].clientX\n\n return\n }\n\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX\n }\n }\n\n _end(event) {\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX - this._deltaX\n }\n\n this._handleSwipe()\n execute(this._config.endCallback)\n }\n\n _move(event) {\n this._deltaX = event.touches && event.touches.length > 1 ?\n 0 :\n event.touches[0].clientX - this._deltaX\n }\n\n _handleSwipe() {\n const absDeltaX = Math.abs(this._deltaX)\n\n if (absDeltaX <= SWIPE_THRESHOLD) {\n return\n }\n\n const direction = absDeltaX / this._deltaX\n\n this._deltaX = 0\n\n if (!direction) {\n return\n }\n\n execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback)\n }\n\n _initEvents() {\n if (this._supportPointerEvents) {\n EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event))\n EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event))\n\n this._element.classList.add(CLASS_NAME_POINTER_EVENT)\n } else {\n EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event))\n EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event))\n EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event))\n }\n }\n\n _eventIsPointerPenTouch(event) {\n return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH)\n }\n\n // Static\n static isSupported() {\n return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0\n }\n}\n\nexport default Swipe\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap carousel.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport Manipulator from './dom/manipulator.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport {\n defineJQueryPlugin,\n getNextActiveElement,\n isRTL,\n isVisible,\n reflow,\n triggerTransitionEnd\n} from './util/index.js'\nimport Swipe from './util/swipe.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'carousel'\nconst DATA_KEY = 'bs.carousel'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst ARROW_LEFT_KEY = 'ArrowLeft'\nconst ARROW_RIGHT_KEY = 'ArrowRight'\nconst TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch\n\nconst ORDER_NEXT = 'next'\nconst ORDER_PREV = 'prev'\nconst DIRECTION_LEFT = 'left'\nconst DIRECTION_RIGHT = 'right'\n\nconst EVENT_SLIDE = `slide${EVENT_KEY}`\nconst EVENT_SLID = `slid${EVENT_KEY}`\nconst EVENT_KEYDOWN = `keydown${EVENT_KEY}`\nconst EVENT_MOUSEENTER = `mouseenter${EVENT_KEY}`\nconst EVENT_MOUSELEAVE = `mouseleave${EVENT_KEY}`\nconst EVENT_DRAG_START = `dragstart${EVENT_KEY}`\nconst EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_CAROUSEL = 'carousel'\nconst CLASS_NAME_ACTIVE = 'active'\nconst CLASS_NAME_SLIDE = 'slide'\nconst CLASS_NAME_END = 'carousel-item-end'\nconst CLASS_NAME_START = 'carousel-item-start'\nconst CLASS_NAME_NEXT = 'carousel-item-next'\nconst CLASS_NAME_PREV = 'carousel-item-prev'\n\nconst SELECTOR_ACTIVE = '.active'\nconst SELECTOR_ITEM = '.carousel-item'\nconst SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM\nconst SELECTOR_ITEM_IMG = '.carousel-item img'\nconst SELECTOR_INDICATORS = '.carousel-indicators'\nconst SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'\nconst SELECTOR_DATA_RIDE = '[data-bs-ride=\"carousel\"]'\n\nconst KEY_TO_DIRECTION = {\n [ARROW_LEFT_KEY]: DIRECTION_RIGHT,\n [ARROW_RIGHT_KEY]: DIRECTION_LEFT\n}\n\nconst Default = {\n interval: 5000,\n keyboard: true,\n pause: 'hover',\n ride: false,\n touch: true,\n wrap: true\n}\n\nconst DefaultType = {\n interval: '(number|boolean)', // TODO:v6 remove boolean support\n keyboard: 'boolean',\n pause: '(string|boolean)',\n ride: '(boolean|string)',\n touch: 'boolean',\n wrap: 'boolean'\n}\n\n/**\n * Class definition\n */\n\nclass Carousel extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n this._interval = null\n this._activeElement = null\n this._isSliding = false\n this.touchTimeout = null\n this._swipeHelper = null\n\n this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element)\n this._addEventListeners()\n\n if (this._config.ride === CLASS_NAME_CAROUSEL) {\n this.cycle()\n }\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n next() {\n this._slide(ORDER_NEXT)\n }\n\n nextWhenVisible() {\n // FIXME TODO use `document.visibilityState`\n // Don't call next when the page isn't visible\n // or the carousel or its parent isn't visible\n if (!document.hidden && isVisible(this._element)) {\n this.next()\n }\n }\n\n prev() {\n this._slide(ORDER_PREV)\n }\n\n pause() {\n if (this._isSliding) {\n triggerTransitionEnd(this._element)\n }\n\n this._clearInterval()\n }\n\n cycle() {\n this._clearInterval()\n this._updateInterval()\n\n this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval)\n }\n\n _maybeEnableCycle() {\n if (!this._config.ride) {\n return\n }\n\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.cycle())\n return\n }\n\n this.cycle()\n }\n\n to(index) {\n const items = this._getItems()\n if (index > items.length - 1 || index < 0) {\n return\n }\n\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.to(index))\n return\n }\n\n const activeIndex = this._getItemIndex(this._getActive())\n if (activeIndex === index) {\n return\n }\n\n const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV\n\n this._slide(order, items[index])\n }\n\n dispose() {\n if (this._swipeHelper) {\n this._swipeHelper.dispose()\n }\n\n super.dispose()\n }\n\n // Private\n _configAfterMerge(config) {\n config.defaultInterval = config.interval\n return config\n }\n\n _addEventListeners() {\n if (this._config.keyboard) {\n EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event))\n }\n\n if (this._config.pause === 'hover') {\n EventHandler.on(this._element, EVENT_MOUSEENTER, () => this.pause())\n EventHandler.on(this._element, EVENT_MOUSELEAVE, () => this._maybeEnableCycle())\n }\n\n if (this._config.touch && Swipe.isSupported()) {\n this._addTouchEventListeners()\n }\n }\n\n _addTouchEventListeners() {\n for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {\n EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault())\n }\n\n const endCallBack = () => {\n if (this._config.pause !== 'hover') {\n return\n }\n\n // If it's a touch-enabled device, mouseenter/leave are fired as\n // part of the mouse compatibility events on first tap - the carousel\n // would stop cycling until user tapped out of it;\n // here, we listen for touchend, explicitly pause the carousel\n // (as if it's the second time we tap on it, mouseenter compat event\n // is NOT fired) and after a timeout (to allow for mouse compatibility\n // events to fire) we explicitly restart cycling\n\n this.pause()\n if (this.touchTimeout) {\n clearTimeout(this.touchTimeout)\n }\n\n this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval)\n }\n\n const swipeConfig = {\n leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)),\n rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)),\n endCallback: endCallBack\n }\n\n this._swipeHelper = new Swipe(this._element, swipeConfig)\n }\n\n _keydown(event) {\n if (/input|textarea/i.test(event.target.tagName)) {\n return\n }\n\n const direction = KEY_TO_DIRECTION[event.key]\n if (direction) {\n event.preventDefault()\n this._slide(this._directionToOrder(direction))\n }\n }\n\n _getItemIndex(element) {\n return this._getItems().indexOf(element)\n }\n\n _setActiveIndicatorElement(index) {\n if (!this._indicatorsElement) {\n return\n }\n\n const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement)\n\n activeIndicator.classList.remove(CLASS_NAME_ACTIVE)\n activeIndicator.removeAttribute('aria-current')\n\n const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to=\"${index}\"]`, this._indicatorsElement)\n\n if (newActiveIndicator) {\n newActiveIndicator.classList.add(CLASS_NAME_ACTIVE)\n newActiveIndicator.setAttribute('aria-current', 'true')\n }\n }\n\n _updateInterval() {\n const element = this._activeElement || this._getActive()\n\n if (!element) {\n return\n }\n\n const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10)\n\n this._config.interval = elementInterval || this._config.defaultInterval\n }\n\n _slide(order, element = null) {\n if (this._isSliding) {\n return\n }\n\n const activeElement = this._getActive()\n const isNext = order === ORDER_NEXT\n const nextElement = element || getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap)\n\n if (nextElement === activeElement) {\n return\n }\n\n const nextElementIndex = this._getItemIndex(nextElement)\n\n const triggerEvent = eventName => {\n return EventHandler.trigger(this._element, eventName, {\n relatedTarget: nextElement,\n direction: this._orderToDirection(order),\n from: this._getItemIndex(activeElement),\n to: nextElementIndex\n })\n }\n\n const slideEvent = triggerEvent(EVENT_SLIDE)\n\n if (slideEvent.defaultPrevented) {\n return\n }\n\n if (!activeElement || !nextElement) {\n // Some weirdness is happening, so we bail\n // TODO: change tests that use empty divs to avoid this check\n return\n }\n\n const isCycling = Boolean(this._interval)\n this.pause()\n\n this._isSliding = true\n\n this._setActiveIndicatorElement(nextElementIndex)\n this._activeElement = nextElement\n\n const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END\n const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV\n\n nextElement.classList.add(orderClassName)\n\n reflow(nextElement)\n\n activeElement.classList.add(directionalClassName)\n nextElement.classList.add(directionalClassName)\n\n const completeCallBack = () => {\n nextElement.classList.remove(directionalClassName, orderClassName)\n nextElement.classList.add(CLASS_NAME_ACTIVE)\n\n activeElement.classList.remove(CLASS_NAME_ACTIVE, orderClassName, directionalClassName)\n\n this._isSliding = false\n\n triggerEvent(EVENT_SLID)\n }\n\n this._queueCallback(completeCallBack, activeElement, this._isAnimated())\n\n if (isCycling) {\n this.cycle()\n }\n }\n\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_SLIDE)\n }\n\n _getActive() {\n return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element)\n }\n\n _getItems() {\n return SelectorEngine.find(SELECTOR_ITEM, this._element)\n }\n\n _clearInterval() {\n if (this._interval) {\n clearInterval(this._interval)\n this._interval = null\n }\n }\n\n _directionToOrder(direction) {\n if (isRTL()) {\n return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT\n }\n\n return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV\n }\n\n _orderToDirection(order) {\n if (isRTL()) {\n return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT\n }\n\n return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Carousel.getOrCreateInstance(this, config)\n\n if (typeof config === 'number') {\n data.to(config)\n return\n }\n\n if (typeof config === 'string') {\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n }\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, function (event) {\n const target = SelectorEngine.getElementFromSelector(this)\n\n if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {\n return\n }\n\n event.preventDefault()\n\n const carousel = Carousel.getOrCreateInstance(target)\n const slideIndex = this.getAttribute('data-bs-slide-to')\n\n if (slideIndex) {\n carousel.to(slideIndex)\n carousel._maybeEnableCycle()\n return\n }\n\n if (Manipulator.getDataAttribute(this, 'slide') === 'next') {\n carousel.next()\n carousel._maybeEnableCycle()\n return\n }\n\n carousel.prev()\n carousel._maybeEnableCycle()\n})\n\nEventHandler.on(window, EVENT_LOAD_DATA_API, () => {\n const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE)\n\n for (const carousel of carousels) {\n Carousel.getOrCreateInstance(carousel)\n }\n})\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Carousel)\n\nexport default Carousel\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap collapse.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport {\n defineJQueryPlugin,\n getElement,\n reflow\n} from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'collapse'\nconst DATA_KEY = 'bs.collapse'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_COLLAPSE = 'collapse'\nconst CLASS_NAME_COLLAPSING = 'collapsing'\nconst CLASS_NAME_COLLAPSED = 'collapsed'\nconst CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`\nconst CLASS_NAME_HORIZONTAL = 'collapse-horizontal'\n\nconst WIDTH = 'width'\nconst HEIGHT = 'height'\n\nconst SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing'\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"collapse\"]'\n\nconst Default = {\n parent: null,\n toggle: true\n}\n\nconst DefaultType = {\n parent: '(null|element)',\n toggle: 'boolean'\n}\n\n/**\n * Class definition\n */\n\nclass Collapse extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n this._isTransitioning = false\n this._triggerArray = []\n\n const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE)\n\n for (const elem of toggleList) {\n const selector = SelectorEngine.getSelectorFromElement(elem)\n const filterElement = SelectorEngine.find(selector)\n .filter(foundElement => foundElement === this._element)\n\n if (selector !== null && filterElement.length) {\n this._triggerArray.push(elem)\n }\n }\n\n this._initializeChildren()\n\n if (!this._config.parent) {\n this._addAriaAndCollapsedClass(this._triggerArray, this._isShown())\n }\n\n if (this._config.toggle) {\n this.toggle()\n }\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n toggle() {\n if (this._isShown()) {\n this.hide()\n } else {\n this.show()\n }\n }\n\n show() {\n if (this._isTransitioning || this._isShown()) {\n return\n }\n\n let activeChildren = []\n\n // find active children\n if (this._config.parent) {\n activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES)\n .filter(element => element !== this._element)\n .map(element => Collapse.getOrCreateInstance(element, { toggle: false }))\n }\n\n if (activeChildren.length && activeChildren[0]._isTransitioning) {\n return\n }\n\n const startEvent = EventHandler.trigger(this._element, EVENT_SHOW)\n if (startEvent.defaultPrevented) {\n return\n }\n\n for (const activeInstance of activeChildren) {\n activeInstance.hide()\n }\n\n const dimension = this._getDimension()\n\n this._element.classList.remove(CLASS_NAME_COLLAPSE)\n this._element.classList.add(CLASS_NAME_COLLAPSING)\n\n this._element.style[dimension] = 0\n\n this._addAriaAndCollapsedClass(this._triggerArray, true)\n this._isTransitioning = true\n\n const complete = () => {\n this._isTransitioning = false\n\n this._element.classList.remove(CLASS_NAME_COLLAPSING)\n this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW)\n\n this._element.style[dimension] = ''\n\n EventHandler.trigger(this._element, EVENT_SHOWN)\n }\n\n const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1)\n const scrollSize = `scroll${capitalizedDimension}`\n\n this._queueCallback(complete, this._element, true)\n this._element.style[dimension] = `${this._element[scrollSize]}px`\n }\n\n hide() {\n if (this._isTransitioning || !this._isShown()) {\n return\n }\n\n const startEvent = EventHandler.trigger(this._element, EVENT_HIDE)\n if (startEvent.defaultPrevented) {\n return\n }\n\n const dimension = this._getDimension()\n\n this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`\n\n reflow(this._element)\n\n this._element.classList.add(CLASS_NAME_COLLAPSING)\n this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW)\n\n for (const trigger of this._triggerArray) {\n const element = SelectorEngine.getElementFromSelector(trigger)\n\n if (element && !this._isShown(element)) {\n this._addAriaAndCollapsedClass([trigger], false)\n }\n }\n\n this._isTransitioning = true\n\n const complete = () => {\n this._isTransitioning = false\n this._element.classList.remove(CLASS_NAME_COLLAPSING)\n this._element.classList.add(CLASS_NAME_COLLAPSE)\n EventHandler.trigger(this._element, EVENT_HIDDEN)\n }\n\n this._element.style[dimension] = ''\n\n this._queueCallback(complete, this._element, true)\n }\n\n // Private\n _isShown(element = this._element) {\n return element.classList.contains(CLASS_NAME_SHOW)\n }\n\n _configAfterMerge(config) {\n config.toggle = Boolean(config.toggle) // Coerce string values\n config.parent = getElement(config.parent)\n return config\n }\n\n _getDimension() {\n return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT\n }\n\n _initializeChildren() {\n if (!this._config.parent) {\n return\n }\n\n const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE)\n\n for (const element of children) {\n const selected = SelectorEngine.getElementFromSelector(element)\n\n if (selected) {\n this._addAriaAndCollapsedClass([element], this._isShown(selected))\n }\n }\n }\n\n _getFirstLevelChildren(selector) {\n const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent)\n // remove children if greater depth\n return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element))\n }\n\n _addAriaAndCollapsedClass(triggerArray, isOpen) {\n if (!triggerArray.length) {\n return\n }\n\n for (const element of triggerArray) {\n element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen)\n element.setAttribute('aria-expanded', isOpen)\n }\n }\n\n // Static\n static jQueryInterface(config) {\n const _config = {}\n if (typeof config === 'string' && /show|hide/.test(config)) {\n _config.toggle = false\n }\n\n return this.each(function () {\n const data = Collapse.getOrCreateInstance(this, _config)\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n }\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n // preventDefault only for elements (which change the URL) not inside the collapsible element\n if (event.target.tagName === 'A' || (event.delegateTarget && event.delegateTarget.tagName === 'A')) {\n event.preventDefault()\n }\n\n for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) {\n Collapse.getOrCreateInstance(element, { toggle: false }).toggle()\n }\n})\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Collapse)\n\nexport default Collapse\n","export var top = 'top';\nexport var bottom = 'bottom';\nexport var right = 'right';\nexport var left = 'left';\nexport var auto = 'auto';\nexport var basePlacements = [top, bottom, right, left];\nexport var start = 'start';\nexport var end = 'end';\nexport var clippingParents = 'clippingParents';\nexport var viewport = 'viewport';\nexport var popper = 'popper';\nexport var reference = 'reference';\nexport var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) {\n return acc.concat([placement + \"-\" + start, placement + \"-\" + end]);\n}, []);\nexport var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) {\n return acc.concat([placement, placement + \"-\" + start, placement + \"-\" + end]);\n}, []); // modifiers that need to read the DOM\n\nexport var beforeRead = 'beforeRead';\nexport var read = 'read';\nexport var afterRead = 'afterRead'; // pure-logic modifiers\n\nexport var beforeMain = 'beforeMain';\nexport var main = 'main';\nexport var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state)\n\nexport var beforeWrite = 'beforeWrite';\nexport var write = 'write';\nexport var afterWrite = 'afterWrite';\nexport var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite];","export default function getNodeName(element) {\n return element ? (element.nodeName || '').toLowerCase() : null;\n}","export default function getWindow(node) {\n if (node == null) {\n return window;\n }\n\n if (node.toString() !== '[object Window]') {\n var ownerDocument = node.ownerDocument;\n return ownerDocument ? ownerDocument.defaultView || window : window;\n }\n\n return node;\n}","import getWindow from \"./getWindow.js\";\n\nfunction isElement(node) {\n var OwnElement = getWindow(node).Element;\n return node instanceof OwnElement || node instanceof Element;\n}\n\nfunction isHTMLElement(node) {\n var OwnElement = getWindow(node).HTMLElement;\n return node instanceof OwnElement || node instanceof HTMLElement;\n}\n\nfunction isShadowRoot(node) {\n // IE 11 has no ShadowRoot\n if (typeof ShadowRoot === 'undefined') {\n return false;\n }\n\n var OwnElement = getWindow(node).ShadowRoot;\n return node instanceof OwnElement || node instanceof ShadowRoot;\n}\n\nexport { isElement, isHTMLElement, isShadowRoot };","import getNodeName from \"../dom-utils/getNodeName.js\";\nimport { isHTMLElement } from \"../dom-utils/instanceOf.js\"; // This modifier takes the styles prepared by the `computeStyles` modifier\n// and applies them to the HTMLElements such as popper and arrow\n\nfunction applyStyles(_ref) {\n var state = _ref.state;\n Object.keys(state.elements).forEach(function (name) {\n var style = state.styles[name] || {};\n var attributes = state.attributes[name] || {};\n var element = state.elements[name]; // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n } // Flow doesn't support to extend this property, but it's the most\n // effective way to apply styles to an HTMLElement\n // $FlowFixMe[cannot-write]\n\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (name) {\n var value = attributes[name];\n\n if (value === false) {\n element.removeAttribute(name);\n } else {\n element.setAttribute(name, value === true ? '' : value);\n }\n });\n });\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state;\n var initialStyles = {\n popper: {\n position: state.options.strategy,\n left: '0',\n top: '0',\n margin: '0'\n },\n arrow: {\n position: 'absolute'\n },\n reference: {}\n };\n Object.assign(state.elements.popper.style, initialStyles.popper);\n state.styles = initialStyles;\n\n if (state.elements.arrow) {\n Object.assign(state.elements.arrow.style, initialStyles.arrow);\n }\n\n return function () {\n Object.keys(state.elements).forEach(function (name) {\n var element = state.elements[name];\n var attributes = state.attributes[name] || {};\n var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); // Set all values to an empty string to unset them\n\n var style = styleProperties.reduce(function (style, property) {\n style[property] = '';\n return style;\n }, {}); // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n }\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (attribute) {\n element.removeAttribute(attribute);\n });\n });\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'applyStyles',\n enabled: true,\n phase: 'write',\n fn: applyStyles,\n effect: effect,\n requires: ['computeStyles']\n};","import { auto } from \"../enums.js\";\nexport default function getBasePlacement(placement) {\n return placement.split('-')[0];\n}","export var max = Math.max;\nexport var min = Math.min;\nexport var round = Math.round;","export default function getUAString() {\n var uaData = navigator.userAgentData;\n\n if (uaData != null && uaData.brands && Array.isArray(uaData.brands)) {\n return uaData.brands.map(function (item) {\n return item.brand + \"/\" + item.version;\n }).join(' ');\n }\n\n return navigator.userAgent;\n}","import getUAString from \"../utils/userAgent.js\";\nexport default function isLayoutViewport() {\n return !/^((?!chrome|android).)*safari/i.test(getUAString());\n}","import { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport { round } from \"../utils/math.js\";\nimport getWindow from \"./getWindow.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getBoundingClientRect(element, includeScale, isFixedStrategy) {\n if (includeScale === void 0) {\n includeScale = false;\n }\n\n if (isFixedStrategy === void 0) {\n isFixedStrategy = false;\n }\n\n var clientRect = element.getBoundingClientRect();\n var scaleX = 1;\n var scaleY = 1;\n\n if (includeScale && isHTMLElement(element)) {\n scaleX = element.offsetWidth > 0 ? round(clientRect.width) / element.offsetWidth || 1 : 1;\n scaleY = element.offsetHeight > 0 ? round(clientRect.height) / element.offsetHeight || 1 : 1;\n }\n\n var _ref = isElement(element) ? getWindow(element) : window,\n visualViewport = _ref.visualViewport;\n\n var addVisualOffsets = !isLayoutViewport() && isFixedStrategy;\n var x = (clientRect.left + (addVisualOffsets && visualViewport ? visualViewport.offsetLeft : 0)) / scaleX;\n var y = (clientRect.top + (addVisualOffsets && visualViewport ? visualViewport.offsetTop : 0)) / scaleY;\n var width = clientRect.width / scaleX;\n var height = clientRect.height / scaleY;\n return {\n width: width,\n height: height,\n top: y,\n right: x + width,\n bottom: y + height,\n left: x,\n x: x,\n y: y\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\"; // Returns the layout rect of an element relative to its offsetParent. Layout\n// means it doesn't take into account transforms.\n\nexport default function getLayoutRect(element) {\n var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed.\n // Fixes https://github.com/popperjs/popper-core/issues/1223\n\n var width = element.offsetWidth;\n var height = element.offsetHeight;\n\n if (Math.abs(clientRect.width - width) <= 1) {\n width = clientRect.width;\n }\n\n if (Math.abs(clientRect.height - height) <= 1) {\n height = clientRect.height;\n }\n\n return {\n x: element.offsetLeft,\n y: element.offsetTop,\n width: width,\n height: height\n };\n}","import { isShadowRoot } from \"./instanceOf.js\";\nexport default function contains(parent, child) {\n var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method\n\n if (parent.contains(child)) {\n return true;\n } // then fallback to custom implementation with Shadow DOM support\n else if (rootNode && isShadowRoot(rootNode)) {\n var next = child;\n\n do {\n if (next && parent.isSameNode(next)) {\n return true;\n } // $FlowFixMe[prop-missing]: need a better way to handle this...\n\n\n next = next.parentNode || next.host;\n } while (next);\n } // Give up, the result is false\n\n\n return false;\n}","import getWindow from \"./getWindow.js\";\nexport default function getComputedStyle(element) {\n return getWindow(element).getComputedStyle(element);\n}","import getNodeName from \"./getNodeName.js\";\nexport default function isTableElement(element) {\n return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0;\n}","import { isElement } from \"./instanceOf.js\";\nexport default function getDocumentElement(element) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return ((isElement(element) ? element.ownerDocument : // $FlowFixMe[prop-missing]\n element.document) || window.document).documentElement;\n}","import getNodeName from \"./getNodeName.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport { isShadowRoot } from \"./instanceOf.js\";\nexport default function getParentNode(element) {\n if (getNodeName(element) === 'html') {\n return element;\n }\n\n return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle\n // $FlowFixMe[incompatible-return]\n // $FlowFixMe[prop-missing]\n element.assignedSlot || // step into the shadow DOM of the parent of a slotted node\n element.parentNode || ( // DOM Element detected\n isShadowRoot(element) ? element.host : null) || // ShadowRoot detected\n // $FlowFixMe[incompatible-call]: HTMLElement is a Node\n getDocumentElement(element) // fallback\n\n );\n}","import getWindow from \"./getWindow.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isHTMLElement, isShadowRoot } from \"./instanceOf.js\";\nimport isTableElement from \"./isTableElement.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getUAString from \"../utils/userAgent.js\";\n\nfunction getTrueOffsetParent(element) {\n if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837\n getComputedStyle(element).position === 'fixed') {\n return null;\n }\n\n return element.offsetParent;\n} // `.offsetParent` reports `null` for fixed elements, while absolute elements\n// return the containing block\n\n\nfunction getContainingBlock(element) {\n var isFirefox = /firefox/i.test(getUAString());\n var isIE = /Trident/i.test(getUAString());\n\n if (isIE && isHTMLElement(element)) {\n // In IE 9, 10 and 11 fixed elements containing block is always established by the viewport\n var elementCss = getComputedStyle(element);\n\n if (elementCss.position === 'fixed') {\n return null;\n }\n }\n\n var currentNode = getParentNode(element);\n\n if (isShadowRoot(currentNode)) {\n currentNode = currentNode.host;\n }\n\n while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) {\n var css = getComputedStyle(currentNode); // This is non-exhaustive but covers the most common CSS properties that\n // create a containing block.\n // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\n\n if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') {\n return currentNode;\n } else {\n currentNode = currentNode.parentNode;\n }\n }\n\n return null;\n} // Gets the closest ancestor positioned element. Handles some edge cases,\n// such as table ancestors and cross browser bugs.\n\n\nexport default function getOffsetParent(element) {\n var window = getWindow(element);\n var offsetParent = getTrueOffsetParent(element);\n\n while (offsetParent && isTableElement(offsetParent) && getComputedStyle(offsetParent).position === 'static') {\n offsetParent = getTrueOffsetParent(offsetParent);\n }\n\n if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle(offsetParent).position === 'static')) {\n return window;\n }\n\n return offsetParent || getContainingBlock(element) || window;\n}","export default function getMainAxisFromPlacement(placement) {\n return ['top', 'bottom'].indexOf(placement) >= 0 ? 'x' : 'y';\n}","import { max as mathMax, min as mathMin } from \"./math.js\";\nexport function within(min, value, max) {\n return mathMax(min, mathMin(value, max));\n}\nexport function withinMaxClamp(min, value, max) {\n var v = within(min, value, max);\n return v > max ? max : v;\n}","import getFreshSideObject from \"./getFreshSideObject.js\";\nexport default function mergePaddingObject(paddingObject) {\n return Object.assign({}, getFreshSideObject(), paddingObject);\n}","export default function getFreshSideObject() {\n return {\n top: 0,\n right: 0,\n bottom: 0,\n left: 0\n };\n}","export default function expandToHashMap(value, keys) {\n return keys.reduce(function (hashMap, key) {\n hashMap[key] = value;\n return hashMap;\n }, {});\n}","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport contains from \"../dom-utils/contains.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport { within } from \"../utils/within.js\";\nimport mergePaddingObject from \"../utils/mergePaddingObject.js\";\nimport expandToHashMap from \"../utils/expandToHashMap.js\";\nimport { left, right, basePlacements, top, bottom } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar toPaddingObject = function toPaddingObject(padding, state) {\n padding = typeof padding === 'function' ? padding(Object.assign({}, state.rects, {\n placement: state.placement\n })) : padding;\n return mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n};\n\nfunction arrow(_ref) {\n var _state$modifiersData$;\n\n var state = _ref.state,\n name = _ref.name,\n options = _ref.options;\n var arrowElement = state.elements.arrow;\n var popperOffsets = state.modifiersData.popperOffsets;\n var basePlacement = getBasePlacement(state.placement);\n var axis = getMainAxisFromPlacement(basePlacement);\n var isVertical = [left, right].indexOf(basePlacement) >= 0;\n var len = isVertical ? 'height' : 'width';\n\n if (!arrowElement || !popperOffsets) {\n return;\n }\n\n var paddingObject = toPaddingObject(options.padding, state);\n var arrowRect = getLayoutRect(arrowElement);\n var minProp = axis === 'y' ? top : left;\n var maxProp = axis === 'y' ? bottom : right;\n var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets[axis] - state.rects.popper[len];\n var startDiff = popperOffsets[axis] - state.rects.reference[axis];\n var arrowOffsetParent = getOffsetParent(arrowElement);\n var clientSize = arrowOffsetParent ? axis === 'y' ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0;\n var centerToReference = endDiff / 2 - startDiff / 2; // Make sure the arrow doesn't overflow the popper if the center point is\n // outside of the popper bounds\n\n var min = paddingObject[minProp];\n var max = clientSize - arrowRect[len] - paddingObject[maxProp];\n var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference;\n var offset = within(min, center, max); // Prevents breaking syntax highlighting...\n\n var axisProp = axis;\n state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset, _state$modifiersData$.centerOffset = offset - center, _state$modifiersData$);\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state,\n options = _ref2.options;\n var _options$element = options.element,\n arrowElement = _options$element === void 0 ? '[data-popper-arrow]' : _options$element;\n\n if (arrowElement == null) {\n return;\n } // CSS selector\n\n\n if (typeof arrowElement === 'string') {\n arrowElement = state.elements.popper.querySelector(arrowElement);\n\n if (!arrowElement) {\n return;\n }\n }\n\n if (!contains(state.elements.popper, arrowElement)) {\n return;\n }\n\n state.elements.arrow = arrowElement;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'arrow',\n enabled: true,\n phase: 'main',\n fn: arrow,\n effect: effect,\n requires: ['popperOffsets'],\n requiresIfExists: ['preventOverflow']\n};","export default function getVariation(placement) {\n return placement.split('-')[1];\n}","import { top, left, right, bottom, end } from \"../enums.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getWindow from \"../dom-utils/getWindow.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getComputedStyle from \"../dom-utils/getComputedStyle.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport { round } from \"../utils/math.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar unsetSides = {\n top: 'auto',\n right: 'auto',\n bottom: 'auto',\n left: 'auto'\n}; // Round the offsets to the nearest suitable subpixel based on the DPR.\n// Zooming can change the DPR, but it seems to report a value that will\n// cleanly divide the values into the appropriate subpixels.\n\nfunction roundOffsetsByDPR(_ref, win) {\n var x = _ref.x,\n y = _ref.y;\n var dpr = win.devicePixelRatio || 1;\n return {\n x: round(x * dpr) / dpr || 0,\n y: round(y * dpr) / dpr || 0\n };\n}\n\nexport function mapToStyles(_ref2) {\n var _Object$assign2;\n\n var popper = _ref2.popper,\n popperRect = _ref2.popperRect,\n placement = _ref2.placement,\n variation = _ref2.variation,\n offsets = _ref2.offsets,\n position = _ref2.position,\n gpuAcceleration = _ref2.gpuAcceleration,\n adaptive = _ref2.adaptive,\n roundOffsets = _ref2.roundOffsets,\n isFixed = _ref2.isFixed;\n var _offsets$x = offsets.x,\n x = _offsets$x === void 0 ? 0 : _offsets$x,\n _offsets$y = offsets.y,\n y = _offsets$y === void 0 ? 0 : _offsets$y;\n\n var _ref3 = typeof roundOffsets === 'function' ? roundOffsets({\n x: x,\n y: y\n }) : {\n x: x,\n y: y\n };\n\n x = _ref3.x;\n y = _ref3.y;\n var hasX = offsets.hasOwnProperty('x');\n var hasY = offsets.hasOwnProperty('y');\n var sideX = left;\n var sideY = top;\n var win = window;\n\n if (adaptive) {\n var offsetParent = getOffsetParent(popper);\n var heightProp = 'clientHeight';\n var widthProp = 'clientWidth';\n\n if (offsetParent === getWindow(popper)) {\n offsetParent = getDocumentElement(popper);\n\n if (getComputedStyle(offsetParent).position !== 'static' && position === 'absolute') {\n heightProp = 'scrollHeight';\n widthProp = 'scrollWidth';\n }\n } // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it\n\n\n offsetParent = offsetParent;\n\n if (placement === top || (placement === left || placement === right) && variation === end) {\n sideY = bottom;\n var offsetY = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.height : // $FlowFixMe[prop-missing]\n offsetParent[heightProp];\n y -= offsetY - popperRect.height;\n y *= gpuAcceleration ? 1 : -1;\n }\n\n if (placement === left || (placement === top || placement === bottom) && variation === end) {\n sideX = right;\n var offsetX = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.width : // $FlowFixMe[prop-missing]\n offsetParent[widthProp];\n x -= offsetX - popperRect.width;\n x *= gpuAcceleration ? 1 : -1;\n }\n }\n\n var commonStyles = Object.assign({\n position: position\n }, adaptive && unsetSides);\n\n var _ref4 = roundOffsets === true ? roundOffsetsByDPR({\n x: x,\n y: y\n }, getWindow(popper)) : {\n x: x,\n y: y\n };\n\n x = _ref4.x;\n y = _ref4.y;\n\n if (gpuAcceleration) {\n var _Object$assign;\n\n return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? '0' : '', _Object$assign[sideX] = hasX ? '0' : '', _Object$assign.transform = (win.devicePixelRatio || 1) <= 1 ? \"translate(\" + x + \"px, \" + y + \"px)\" : \"translate3d(\" + x + \"px, \" + y + \"px, 0)\", _Object$assign));\n }\n\n return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + \"px\" : '', _Object$assign2[sideX] = hasX ? x + \"px\" : '', _Object$assign2.transform = '', _Object$assign2));\n}\n\nfunction computeStyles(_ref5) {\n var state = _ref5.state,\n options = _ref5.options;\n var _options$gpuAccelerat = options.gpuAcceleration,\n gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat,\n _options$adaptive = options.adaptive,\n adaptive = _options$adaptive === void 0 ? true : _options$adaptive,\n _options$roundOffsets = options.roundOffsets,\n roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets;\n var commonStyles = {\n placement: getBasePlacement(state.placement),\n variation: getVariation(state.placement),\n popper: state.elements.popper,\n popperRect: state.rects.popper,\n gpuAcceleration: gpuAcceleration,\n isFixed: state.options.strategy === 'fixed'\n };\n\n if (state.modifiersData.popperOffsets != null) {\n state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.popperOffsets,\n position: state.options.strategy,\n adaptive: adaptive,\n roundOffsets: roundOffsets\n })));\n }\n\n if (state.modifiersData.arrow != null) {\n state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.arrow,\n position: 'absolute',\n adaptive: false,\n roundOffsets: roundOffsets\n })));\n }\n\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-placement': state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'computeStyles',\n enabled: true,\n phase: 'beforeWrite',\n fn: computeStyles,\n data: {}\n};","import getWindow from \"../dom-utils/getWindow.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar passive = {\n passive: true\n};\n\nfunction effect(_ref) {\n var state = _ref.state,\n instance = _ref.instance,\n options = _ref.options;\n var _options$scroll = options.scroll,\n scroll = _options$scroll === void 0 ? true : _options$scroll,\n _options$resize = options.resize,\n resize = _options$resize === void 0 ? true : _options$resize;\n var window = getWindow(state.elements.popper);\n var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper);\n\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.addEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.addEventListener('resize', instance.update, passive);\n }\n\n return function () {\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.removeEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.removeEventListener('resize', instance.update, passive);\n }\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'eventListeners',\n enabled: true,\n phase: 'write',\n fn: function fn() {},\n effect: effect,\n data: {}\n};","var hash = {\n left: 'right',\n right: 'left',\n bottom: 'top',\n top: 'bottom'\n};\nexport default function getOppositePlacement(placement) {\n return placement.replace(/left|right|bottom|top/g, function (matched) {\n return hash[matched];\n });\n}","var hash = {\n start: 'end',\n end: 'start'\n};\nexport default function getOppositeVariationPlacement(placement) {\n return placement.replace(/start|end/g, function (matched) {\n return hash[matched];\n });\n}","import getWindow from \"./getWindow.js\";\nexport default function getWindowScroll(node) {\n var win = getWindow(node);\n var scrollLeft = win.pageXOffset;\n var scrollTop = win.pageYOffset;\n return {\n scrollLeft: scrollLeft,\n scrollTop: scrollTop\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nexport default function getWindowScrollBarX(element) {\n // If has a CSS width greater than the viewport, then this will be\n // incorrect for RTL.\n // Popper 1 is broken in this case and never had a bug report so let's assume\n // it's not an issue. I don't think anyone ever specifies width on \n // anyway.\n // Browsers where the left scrollbar doesn't cause an issue report `0` for\n // this (e.g. Edge 2019, IE11, Safari)\n return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft;\n}","import getComputedStyle from \"./getComputedStyle.js\";\nexport default function isScrollParent(element) {\n // Firefox wants us to check `-x` and `-y` variations as well\n var _getComputedStyle = getComputedStyle(element),\n overflow = _getComputedStyle.overflow,\n overflowX = _getComputedStyle.overflowX,\n overflowY = _getComputedStyle.overflowY;\n\n return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX);\n}","import getParentNode from \"./getParentNode.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nexport default function getScrollParent(node) {\n if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return node.ownerDocument.body;\n }\n\n if (isHTMLElement(node) && isScrollParent(node)) {\n return node;\n }\n\n return getScrollParent(getParentNode(node));\n}","import getScrollParent from \"./getScrollParent.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getWindow from \"./getWindow.js\";\nimport isScrollParent from \"./isScrollParent.js\";\n/*\ngiven a DOM element, return the list of all scroll parents, up the list of ancesors\nuntil we get to the top window object. This list is what we attach scroll listeners\nto, because if any of these parent elements scroll, we'll need to re-calculate the\nreference element's position.\n*/\n\nexport default function listScrollParents(element, list) {\n var _element$ownerDocumen;\n\n if (list === void 0) {\n list = [];\n }\n\n var scrollParent = getScrollParent(element);\n var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body);\n var win = getWindow(scrollParent);\n var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent;\n var updatedList = list.concat(target);\n return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here\n updatedList.concat(listScrollParents(getParentNode(target)));\n}","export default function rectToClientRect(rect) {\n return Object.assign({}, rect, {\n left: rect.x,\n top: rect.y,\n right: rect.x + rect.width,\n bottom: rect.y + rect.height\n });\n}","import { viewport } from \"../enums.js\";\nimport getViewportRect from \"./getViewportRect.js\";\nimport getDocumentRect from \"./getDocumentRect.js\";\nimport listScrollParents from \"./listScrollParents.js\";\nimport getOffsetParent from \"./getOffsetParent.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport contains from \"./contains.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport rectToClientRect from \"../utils/rectToClientRect.js\";\nimport { max, min } from \"../utils/math.js\";\n\nfunction getInnerBoundingClientRect(element, strategy) {\n var rect = getBoundingClientRect(element, false, strategy === 'fixed');\n rect.top = rect.top + element.clientTop;\n rect.left = rect.left + element.clientLeft;\n rect.bottom = rect.top + element.clientHeight;\n rect.right = rect.left + element.clientWidth;\n rect.width = element.clientWidth;\n rect.height = element.clientHeight;\n rect.x = rect.left;\n rect.y = rect.top;\n return rect;\n}\n\nfunction getClientRectFromMixedType(element, clippingParent, strategy) {\n return clippingParent === viewport ? rectToClientRect(getViewportRect(element, strategy)) : isElement(clippingParent) ? getInnerBoundingClientRect(clippingParent, strategy) : rectToClientRect(getDocumentRect(getDocumentElement(element)));\n} // A \"clipping parent\" is an overflowable container with the characteristic of\n// clipping (or hiding) overflowing elements with a position different from\n// `initial`\n\n\nfunction getClippingParents(element) {\n var clippingParents = listScrollParents(getParentNode(element));\n var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle(element).position) >= 0;\n var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element;\n\n if (!isElement(clipperElement)) {\n return [];\n } // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414\n\n\n return clippingParents.filter(function (clippingParent) {\n return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body';\n });\n} // Gets the maximum area that the element is visible in due to any number of\n// clipping parents\n\n\nexport default function getClippingRect(element, boundary, rootBoundary, strategy) {\n var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary);\n var clippingParents = [].concat(mainClippingParents, [rootBoundary]);\n var firstClippingParent = clippingParents[0];\n var clippingRect = clippingParents.reduce(function (accRect, clippingParent) {\n var rect = getClientRectFromMixedType(element, clippingParent, strategy);\n accRect.top = max(rect.top, accRect.top);\n accRect.right = min(rect.right, accRect.right);\n accRect.bottom = min(rect.bottom, accRect.bottom);\n accRect.left = max(rect.left, accRect.left);\n return accRect;\n }, getClientRectFromMixedType(element, firstClippingParent, strategy));\n clippingRect.width = clippingRect.right - clippingRect.left;\n clippingRect.height = clippingRect.bottom - clippingRect.top;\n clippingRect.x = clippingRect.left;\n clippingRect.y = clippingRect.top;\n return clippingRect;\n}","import getWindow from \"./getWindow.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getViewportRect(element, strategy) {\n var win = getWindow(element);\n var html = getDocumentElement(element);\n var visualViewport = win.visualViewport;\n var width = html.clientWidth;\n var height = html.clientHeight;\n var x = 0;\n var y = 0;\n\n if (visualViewport) {\n width = visualViewport.width;\n height = visualViewport.height;\n var layoutViewport = isLayoutViewport();\n\n if (layoutViewport || !layoutViewport && strategy === 'fixed') {\n x = visualViewport.offsetLeft;\n y = visualViewport.offsetTop;\n }\n }\n\n return {\n width: width,\n height: height,\n x: x + getWindowScrollBarX(element),\n y: y\n };\n}","import getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nimport { max } from \"../utils/math.js\"; // Gets the entire size of the scrollable document area, even extending outside\n// of the `` and `` rect bounds if horizontally scrollable\n\nexport default function getDocumentRect(element) {\n var _element$ownerDocumen;\n\n var html = getDocumentElement(element);\n var winScroll = getWindowScroll(element);\n var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body;\n var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0);\n var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0);\n var x = -winScroll.scrollLeft + getWindowScrollBarX(element);\n var y = -winScroll.scrollTop;\n\n if (getComputedStyle(body || html).direction === 'rtl') {\n x += max(html.clientWidth, body ? body.clientWidth : 0) - width;\n }\n\n return {\n width: width,\n height: height,\n x: x,\n y: y\n };\n}","import getBasePlacement from \"./getBasePlacement.js\";\nimport getVariation from \"./getVariation.js\";\nimport getMainAxisFromPlacement from \"./getMainAxisFromPlacement.js\";\nimport { top, right, bottom, left, start, end } from \"../enums.js\";\nexport default function computeOffsets(_ref) {\n var reference = _ref.reference,\n element = _ref.element,\n placement = _ref.placement;\n var basePlacement = placement ? getBasePlacement(placement) : null;\n var variation = placement ? getVariation(placement) : null;\n var commonX = reference.x + reference.width / 2 - element.width / 2;\n var commonY = reference.y + reference.height / 2 - element.height / 2;\n var offsets;\n\n switch (basePlacement) {\n case top:\n offsets = {\n x: commonX,\n y: reference.y - element.height\n };\n break;\n\n case bottom:\n offsets = {\n x: commonX,\n y: reference.y + reference.height\n };\n break;\n\n case right:\n offsets = {\n x: reference.x + reference.width,\n y: commonY\n };\n break;\n\n case left:\n offsets = {\n x: reference.x - element.width,\n y: commonY\n };\n break;\n\n default:\n offsets = {\n x: reference.x,\n y: reference.y\n };\n }\n\n var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null;\n\n if (mainAxis != null) {\n var len = mainAxis === 'y' ? 'height' : 'width';\n\n switch (variation) {\n case start:\n offsets[mainAxis] = offsets[mainAxis] - (reference[len] / 2 - element[len] / 2);\n break;\n\n case end:\n offsets[mainAxis] = offsets[mainAxis] + (reference[len] / 2 - element[len] / 2);\n break;\n\n default:\n }\n }\n\n return offsets;\n}","import getClippingRect from \"../dom-utils/getClippingRect.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getBoundingClientRect from \"../dom-utils/getBoundingClientRect.js\";\nimport computeOffsets from \"./computeOffsets.js\";\nimport rectToClientRect from \"./rectToClientRect.js\";\nimport { clippingParents, reference, popper, bottom, top, right, basePlacements, viewport } from \"../enums.js\";\nimport { isElement } from \"../dom-utils/instanceOf.js\";\nimport mergePaddingObject from \"./mergePaddingObject.js\";\nimport expandToHashMap from \"./expandToHashMap.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport default function detectOverflow(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n _options$placement = _options.placement,\n placement = _options$placement === void 0 ? state.placement : _options$placement,\n _options$strategy = _options.strategy,\n strategy = _options$strategy === void 0 ? state.strategy : _options$strategy,\n _options$boundary = _options.boundary,\n boundary = _options$boundary === void 0 ? clippingParents : _options$boundary,\n _options$rootBoundary = _options.rootBoundary,\n rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary,\n _options$elementConte = _options.elementContext,\n elementContext = _options$elementConte === void 0 ? popper : _options$elementConte,\n _options$altBoundary = _options.altBoundary,\n altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary,\n _options$padding = _options.padding,\n padding = _options$padding === void 0 ? 0 : _options$padding;\n var paddingObject = mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n var altContext = elementContext === popper ? reference : popper;\n var popperRect = state.rects.popper;\n var element = state.elements[altBoundary ? altContext : elementContext];\n var clippingClientRect = getClippingRect(isElement(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary, strategy);\n var referenceClientRect = getBoundingClientRect(state.elements.reference);\n var popperOffsets = computeOffsets({\n reference: referenceClientRect,\n element: popperRect,\n strategy: 'absolute',\n placement: placement\n });\n var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets));\n var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; // positive = overflowing the clipping rect\n // 0 or negative = within the clipping rect\n\n var overflowOffsets = {\n top: clippingClientRect.top - elementClientRect.top + paddingObject.top,\n bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom,\n left: clippingClientRect.left - elementClientRect.left + paddingObject.left,\n right: elementClientRect.right - clippingClientRect.right + paddingObject.right\n };\n var offsetData = state.modifiersData.offset; // Offsets can be applied only to the popper element\n\n if (elementContext === popper && offsetData) {\n var offset = offsetData[placement];\n Object.keys(overflowOffsets).forEach(function (key) {\n var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1;\n var axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x';\n overflowOffsets[key] += offset[axis] * multiply;\n });\n }\n\n return overflowOffsets;\n}","import getVariation from \"./getVariation.js\";\nimport { variationPlacements, basePlacements, placements as allPlacements } from \"../enums.js\";\nimport detectOverflow from \"./detectOverflow.js\";\nimport getBasePlacement from \"./getBasePlacement.js\";\nexport default function computeAutoPlacement(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n placement = _options.placement,\n boundary = _options.boundary,\n rootBoundary = _options.rootBoundary,\n padding = _options.padding,\n flipVariations = _options.flipVariations,\n _options$allowedAutoP = _options.allowedAutoPlacements,\n allowedAutoPlacements = _options$allowedAutoP === void 0 ? allPlacements : _options$allowedAutoP;\n var variation = getVariation(placement);\n var placements = variation ? flipVariations ? variationPlacements : variationPlacements.filter(function (placement) {\n return getVariation(placement) === variation;\n }) : basePlacements;\n var allowedPlacements = placements.filter(function (placement) {\n return allowedAutoPlacements.indexOf(placement) >= 0;\n });\n\n if (allowedPlacements.length === 0) {\n allowedPlacements = placements;\n } // $FlowFixMe[incompatible-type]: Flow seems to have problems with two array unions...\n\n\n var overflows = allowedPlacements.reduce(function (acc, placement) {\n acc[placement] = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding\n })[getBasePlacement(placement)];\n return acc;\n }, {});\n return Object.keys(overflows).sort(function (a, b) {\n return overflows[a] - overflows[b];\n });\n}","import getOppositePlacement from \"../utils/getOppositePlacement.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getOppositeVariationPlacement from \"../utils/getOppositeVariationPlacement.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport computeAutoPlacement from \"../utils/computeAutoPlacement.js\";\nimport { bottom, top, start, right, left, auto } from \"../enums.js\";\nimport getVariation from \"../utils/getVariation.js\"; // eslint-disable-next-line import/no-unused-modules\n\nfunction getExpandedFallbackPlacements(placement) {\n if (getBasePlacement(placement) === auto) {\n return [];\n }\n\n var oppositePlacement = getOppositePlacement(placement);\n return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)];\n}\n\nfunction flip(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n\n if (state.modifiersData[name]._skip) {\n return;\n }\n\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis,\n specifiedFallbackPlacements = options.fallbackPlacements,\n padding = options.padding,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n _options$flipVariatio = options.flipVariations,\n flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio,\n allowedAutoPlacements = options.allowedAutoPlacements;\n var preferredPlacement = state.options.placement;\n var basePlacement = getBasePlacement(preferredPlacement);\n var isBasePlacement = basePlacement === preferredPlacement;\n var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement));\n var placements = [preferredPlacement].concat(fallbackPlacements).reduce(function (acc, placement) {\n return acc.concat(getBasePlacement(placement) === auto ? computeAutoPlacement(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n flipVariations: flipVariations,\n allowedAutoPlacements: allowedAutoPlacements\n }) : placement);\n }, []);\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var checksMap = new Map();\n var makeFallbackChecks = true;\n var firstFittingPlacement = placements[0];\n\n for (var i = 0; i < placements.length; i++) {\n var placement = placements[i];\n\n var _basePlacement = getBasePlacement(placement);\n\n var isStartVariation = getVariation(placement) === start;\n var isVertical = [top, bottom].indexOf(_basePlacement) >= 0;\n var len = isVertical ? 'width' : 'height';\n var overflow = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n altBoundary: altBoundary,\n padding: padding\n });\n var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top;\n\n if (referenceRect[len] > popperRect[len]) {\n mainVariationSide = getOppositePlacement(mainVariationSide);\n }\n\n var altVariationSide = getOppositePlacement(mainVariationSide);\n var checks = [];\n\n if (checkMainAxis) {\n checks.push(overflow[_basePlacement] <= 0);\n }\n\n if (checkAltAxis) {\n checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0);\n }\n\n if (checks.every(function (check) {\n return check;\n })) {\n firstFittingPlacement = placement;\n makeFallbackChecks = false;\n break;\n }\n\n checksMap.set(placement, checks);\n }\n\n if (makeFallbackChecks) {\n // `2` may be desired in some cases – research later\n var numberOfChecks = flipVariations ? 3 : 1;\n\n var _loop = function _loop(_i) {\n var fittingPlacement = placements.find(function (placement) {\n var checks = checksMap.get(placement);\n\n if (checks) {\n return checks.slice(0, _i).every(function (check) {\n return check;\n });\n }\n });\n\n if (fittingPlacement) {\n firstFittingPlacement = fittingPlacement;\n return \"break\";\n }\n };\n\n for (var _i = numberOfChecks; _i > 0; _i--) {\n var _ret = _loop(_i);\n\n if (_ret === \"break\") break;\n }\n }\n\n if (state.placement !== firstFittingPlacement) {\n state.modifiersData[name]._skip = true;\n state.placement = firstFittingPlacement;\n state.reset = true;\n }\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'flip',\n enabled: true,\n phase: 'main',\n fn: flip,\n requiresIfExists: ['offset'],\n data: {\n _skip: false\n }\n};","import { top, bottom, left, right } from \"../enums.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\n\nfunction getSideOffsets(overflow, rect, preventedOffsets) {\n if (preventedOffsets === void 0) {\n preventedOffsets = {\n x: 0,\n y: 0\n };\n }\n\n return {\n top: overflow.top - rect.height - preventedOffsets.y,\n right: overflow.right - rect.width + preventedOffsets.x,\n bottom: overflow.bottom - rect.height + preventedOffsets.y,\n left: overflow.left - rect.width - preventedOffsets.x\n };\n}\n\nfunction isAnySideFullyClipped(overflow) {\n return [top, right, bottom, left].some(function (side) {\n return overflow[side] >= 0;\n });\n}\n\nfunction hide(_ref) {\n var state = _ref.state,\n name = _ref.name;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var preventedOffsets = state.modifiersData.preventOverflow;\n var referenceOverflow = detectOverflow(state, {\n elementContext: 'reference'\n });\n var popperAltOverflow = detectOverflow(state, {\n altBoundary: true\n });\n var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect);\n var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets);\n var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets);\n var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets);\n state.modifiersData[name] = {\n referenceClippingOffsets: referenceClippingOffsets,\n popperEscapeOffsets: popperEscapeOffsets,\n isReferenceHidden: isReferenceHidden,\n hasPopperEscaped: hasPopperEscaped\n };\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-reference-hidden': isReferenceHidden,\n 'data-popper-escaped': hasPopperEscaped\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'hide',\n enabled: true,\n phase: 'main',\n requiresIfExists: ['preventOverflow'],\n fn: hide\n};","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport { top, left, right, placements } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport function distanceAndSkiddingToXY(placement, rects, offset) {\n var basePlacement = getBasePlacement(placement);\n var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1;\n\n var _ref = typeof offset === 'function' ? offset(Object.assign({}, rects, {\n placement: placement\n })) : offset,\n skidding = _ref[0],\n distance = _ref[1];\n\n skidding = skidding || 0;\n distance = (distance || 0) * invertDistance;\n return [left, right].indexOf(basePlacement) >= 0 ? {\n x: distance,\n y: skidding\n } : {\n x: skidding,\n y: distance\n };\n}\n\nfunction offset(_ref2) {\n var state = _ref2.state,\n options = _ref2.options,\n name = _ref2.name;\n var _options$offset = options.offset,\n offset = _options$offset === void 0 ? [0, 0] : _options$offset;\n var data = placements.reduce(function (acc, placement) {\n acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset);\n return acc;\n }, {});\n var _data$state$placement = data[state.placement],\n x = _data$state$placement.x,\n y = _data$state$placement.y;\n\n if (state.modifiersData.popperOffsets != null) {\n state.modifiersData.popperOffsets.x += x;\n state.modifiersData.popperOffsets.y += y;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'offset',\n enabled: true,\n phase: 'main',\n requires: ['popperOffsets'],\n fn: offset\n};","import computeOffsets from \"../utils/computeOffsets.js\";\n\nfunction popperOffsets(_ref) {\n var state = _ref.state,\n name = _ref.name;\n // Offsets are the actual position the popper needs to have to be\n // properly positioned near its reference element\n // This is the most basic placement, and will be adjusted by\n // the modifiers in the next step\n state.modifiersData[name] = computeOffsets({\n reference: state.rects.reference,\n element: state.rects.popper,\n strategy: 'absolute',\n placement: state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'popperOffsets',\n enabled: true,\n phase: 'read',\n fn: popperOffsets,\n data: {}\n};","import { top, left, right, bottom, start } from \"../enums.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport getAltAxis from \"../utils/getAltAxis.js\";\nimport { within, withinMaxClamp } from \"../utils/within.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport getFreshSideObject from \"../utils/getFreshSideObject.js\";\nimport { min as mathMin, max as mathMax } from \"../utils/math.js\";\n\nfunction preventOverflow(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? false : _options$altAxis,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n padding = options.padding,\n _options$tether = options.tether,\n tether = _options$tether === void 0 ? true : _options$tether,\n _options$tetherOffset = options.tetherOffset,\n tetherOffset = _options$tetherOffset === void 0 ? 0 : _options$tetherOffset;\n var overflow = detectOverflow(state, {\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n altBoundary: altBoundary\n });\n var basePlacement = getBasePlacement(state.placement);\n var variation = getVariation(state.placement);\n var isBasePlacement = !variation;\n var mainAxis = getMainAxisFromPlacement(basePlacement);\n var altAxis = getAltAxis(mainAxis);\n var popperOffsets = state.modifiersData.popperOffsets;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var tetherOffsetValue = typeof tetherOffset === 'function' ? tetherOffset(Object.assign({}, state.rects, {\n placement: state.placement\n })) : tetherOffset;\n var normalizedTetherOffsetValue = typeof tetherOffsetValue === 'number' ? {\n mainAxis: tetherOffsetValue,\n altAxis: tetherOffsetValue\n } : Object.assign({\n mainAxis: 0,\n altAxis: 0\n }, tetherOffsetValue);\n var offsetModifierState = state.modifiersData.offset ? state.modifiersData.offset[state.placement] : null;\n var data = {\n x: 0,\n y: 0\n };\n\n if (!popperOffsets) {\n return;\n }\n\n if (checkMainAxis) {\n var _offsetModifierState$;\n\n var mainSide = mainAxis === 'y' ? top : left;\n var altSide = mainAxis === 'y' ? bottom : right;\n var len = mainAxis === 'y' ? 'height' : 'width';\n var offset = popperOffsets[mainAxis];\n var min = offset + overflow[mainSide];\n var max = offset - overflow[altSide];\n var additive = tether ? -popperRect[len] / 2 : 0;\n var minLen = variation === start ? referenceRect[len] : popperRect[len];\n var maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; // We need to include the arrow in the calculation so the arrow doesn't go\n // outside the reference bounds\n\n var arrowElement = state.elements.arrow;\n var arrowRect = tether && arrowElement ? getLayoutRect(arrowElement) : {\n width: 0,\n height: 0\n };\n var arrowPaddingObject = state.modifiersData['arrow#persistent'] ? state.modifiersData['arrow#persistent'].padding : getFreshSideObject();\n var arrowPaddingMin = arrowPaddingObject[mainSide];\n var arrowPaddingMax = arrowPaddingObject[altSide]; // If the reference length is smaller than the arrow length, we don't want\n // to include its full size in the calculation. If the reference is small\n // and near the edge of a boundary, the popper can overflow even if the\n // reference is not overflowing as well (e.g. virtual elements with no\n // width or height)\n\n var arrowLen = within(0, referenceRect[len], arrowRect[len]);\n var minOffset = isBasePlacement ? referenceRect[len] / 2 - additive - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis : minLen - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis;\n var maxOffset = isBasePlacement ? -referenceRect[len] / 2 + additive + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis : maxLen + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis;\n var arrowOffsetParent = state.elements.arrow && getOffsetParent(state.elements.arrow);\n var clientOffset = arrowOffsetParent ? mainAxis === 'y' ? arrowOffsetParent.clientTop || 0 : arrowOffsetParent.clientLeft || 0 : 0;\n var offsetModifierValue = (_offsetModifierState$ = offsetModifierState == null ? void 0 : offsetModifierState[mainAxis]) != null ? _offsetModifierState$ : 0;\n var tetherMin = offset + minOffset - offsetModifierValue - clientOffset;\n var tetherMax = offset + maxOffset - offsetModifierValue;\n var preventedOffset = within(tether ? mathMin(min, tetherMin) : min, offset, tether ? mathMax(max, tetherMax) : max);\n popperOffsets[mainAxis] = preventedOffset;\n data[mainAxis] = preventedOffset - offset;\n }\n\n if (checkAltAxis) {\n var _offsetModifierState$2;\n\n var _mainSide = mainAxis === 'x' ? top : left;\n\n var _altSide = mainAxis === 'x' ? bottom : right;\n\n var _offset = popperOffsets[altAxis];\n\n var _len = altAxis === 'y' ? 'height' : 'width';\n\n var _min = _offset + overflow[_mainSide];\n\n var _max = _offset - overflow[_altSide];\n\n var isOriginSide = [top, left].indexOf(basePlacement) !== -1;\n\n var _offsetModifierValue = (_offsetModifierState$2 = offsetModifierState == null ? void 0 : offsetModifierState[altAxis]) != null ? _offsetModifierState$2 : 0;\n\n var _tetherMin = isOriginSide ? _min : _offset - referenceRect[_len] - popperRect[_len] - _offsetModifierValue + normalizedTetherOffsetValue.altAxis;\n\n var _tetherMax = isOriginSide ? _offset + referenceRect[_len] + popperRect[_len] - _offsetModifierValue - normalizedTetherOffsetValue.altAxis : _max;\n\n var _preventedOffset = tether && isOriginSide ? withinMaxClamp(_tetherMin, _offset, _tetherMax) : within(tether ? _tetherMin : _min, _offset, tether ? _tetherMax : _max);\n\n popperOffsets[altAxis] = _preventedOffset;\n data[altAxis] = _preventedOffset - _offset;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'preventOverflow',\n enabled: true,\n phase: 'main',\n fn: preventOverflow,\n requiresIfExists: ['offset']\n};","export default function getAltAxis(axis) {\n return axis === 'x' ? 'y' : 'x';\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getNodeScroll from \"./getNodeScroll.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport { round } from \"../utils/math.js\";\n\nfunction isElementScaled(element) {\n var rect = element.getBoundingClientRect();\n var scaleX = round(rect.width) / element.offsetWidth || 1;\n var scaleY = round(rect.height) / element.offsetHeight || 1;\n return scaleX !== 1 || scaleY !== 1;\n} // Returns the composite rect of an element relative to its offsetParent.\n// Composite means it takes into account transforms as well as layout.\n\n\nexport default function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) {\n if (isFixed === void 0) {\n isFixed = false;\n }\n\n var isOffsetParentAnElement = isHTMLElement(offsetParent);\n var offsetParentIsScaled = isHTMLElement(offsetParent) && isElementScaled(offsetParent);\n var documentElement = getDocumentElement(offsetParent);\n var rect = getBoundingClientRect(elementOrVirtualElement, offsetParentIsScaled, isFixed);\n var scroll = {\n scrollLeft: 0,\n scrollTop: 0\n };\n var offsets = {\n x: 0,\n y: 0\n };\n\n if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {\n if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078\n isScrollParent(documentElement)) {\n scroll = getNodeScroll(offsetParent);\n }\n\n if (isHTMLElement(offsetParent)) {\n offsets = getBoundingClientRect(offsetParent, true);\n offsets.x += offsetParent.clientLeft;\n offsets.y += offsetParent.clientTop;\n } else if (documentElement) {\n offsets.x = getWindowScrollBarX(documentElement);\n }\n }\n\n return {\n x: rect.left + scroll.scrollLeft - offsets.x,\n y: rect.top + scroll.scrollTop - offsets.y,\n width: rect.width,\n height: rect.height\n };\n}","import getWindowScroll from \"./getWindowScroll.js\";\nimport getWindow from \"./getWindow.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getHTMLElementScroll from \"./getHTMLElementScroll.js\";\nexport default function getNodeScroll(node) {\n if (node === getWindow(node) || !isHTMLElement(node)) {\n return getWindowScroll(node);\n } else {\n return getHTMLElementScroll(node);\n }\n}","export default function getHTMLElementScroll(element) {\n return {\n scrollLeft: element.scrollLeft,\n scrollTop: element.scrollTop\n };\n}","import { modifierPhases } from \"../enums.js\"; // source: https://stackoverflow.com/questions/49875255\n\nfunction order(modifiers) {\n var map = new Map();\n var visited = new Set();\n var result = [];\n modifiers.forEach(function (modifier) {\n map.set(modifier.name, modifier);\n }); // On visiting object, check for its dependencies and visit them recursively\n\n function sort(modifier) {\n visited.add(modifier.name);\n var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []);\n requires.forEach(function (dep) {\n if (!visited.has(dep)) {\n var depModifier = map.get(dep);\n\n if (depModifier) {\n sort(depModifier);\n }\n }\n });\n result.push(modifier);\n }\n\n modifiers.forEach(function (modifier) {\n if (!visited.has(modifier.name)) {\n // check for visited object\n sort(modifier);\n }\n });\n return result;\n}\n\nexport default function orderModifiers(modifiers) {\n // order based on dependencies\n var orderedModifiers = order(modifiers); // order based on phase\n\n return modifierPhases.reduce(function (acc, phase) {\n return acc.concat(orderedModifiers.filter(function (modifier) {\n return modifier.phase === phase;\n }));\n }, []);\n}","import getCompositeRect from \"./dom-utils/getCompositeRect.js\";\nimport getLayoutRect from \"./dom-utils/getLayoutRect.js\";\nimport listScrollParents from \"./dom-utils/listScrollParents.js\";\nimport getOffsetParent from \"./dom-utils/getOffsetParent.js\";\nimport orderModifiers from \"./utils/orderModifiers.js\";\nimport debounce from \"./utils/debounce.js\";\nimport mergeByName from \"./utils/mergeByName.js\";\nimport detectOverflow from \"./utils/detectOverflow.js\";\nimport { isElement } from \"./dom-utils/instanceOf.js\";\nvar DEFAULT_OPTIONS = {\n placement: 'bottom',\n modifiers: [],\n strategy: 'absolute'\n};\n\nfunction areValidElements() {\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n return !args.some(function (element) {\n return !(element && typeof element.getBoundingClientRect === 'function');\n });\n}\n\nexport function popperGenerator(generatorOptions) {\n if (generatorOptions === void 0) {\n generatorOptions = {};\n }\n\n var _generatorOptions = generatorOptions,\n _generatorOptions$def = _generatorOptions.defaultModifiers,\n defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def,\n _generatorOptions$def2 = _generatorOptions.defaultOptions,\n defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2;\n return function createPopper(reference, popper, options) {\n if (options === void 0) {\n options = defaultOptions;\n }\n\n var state = {\n placement: 'bottom',\n orderedModifiers: [],\n options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions),\n modifiersData: {},\n elements: {\n reference: reference,\n popper: popper\n },\n attributes: {},\n styles: {}\n };\n var effectCleanupFns = [];\n var isDestroyed = false;\n var instance = {\n state: state,\n setOptions: function setOptions(setOptionsAction) {\n var options = typeof setOptionsAction === 'function' ? setOptionsAction(state.options) : setOptionsAction;\n cleanupModifierEffects();\n state.options = Object.assign({}, defaultOptions, state.options, options);\n state.scrollParents = {\n reference: isElement(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [],\n popper: listScrollParents(popper)\n }; // Orders the modifiers based on their dependencies and `phase`\n // properties\n\n var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers\n\n state.orderedModifiers = orderedModifiers.filter(function (m) {\n return m.enabled;\n });\n runModifierEffects();\n return instance.update();\n },\n // Sync update – it will always be executed, even if not necessary. This\n // is useful for low frequency updates where sync behavior simplifies the\n // logic.\n // For high frequency updates (e.g. `resize` and `scroll` events), always\n // prefer the async Popper#update method\n forceUpdate: function forceUpdate() {\n if (isDestroyed) {\n return;\n }\n\n var _state$elements = state.elements,\n reference = _state$elements.reference,\n popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements\n // anymore\n\n if (!areValidElements(reference, popper)) {\n return;\n } // Store the reference and popper rects to be read by modifiers\n\n\n state.rects = {\n reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'),\n popper: getLayoutRect(popper)\n }; // Modifiers have the ability to reset the current update cycle. The\n // most common use case for this is the `flip` modifier changing the\n // placement, which then needs to re-run all the modifiers, because the\n // logic was previously ran for the previous placement and is therefore\n // stale/incorrect\n\n state.reset = false;\n state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier\n // is filled with the initial data specified by the modifier. This means\n // it doesn't persist and is fresh on each update.\n // To ensure persistent data, use `${name}#persistent`\n\n state.orderedModifiers.forEach(function (modifier) {\n return state.modifiersData[modifier.name] = Object.assign({}, modifier.data);\n });\n\n for (var index = 0; index < state.orderedModifiers.length; index++) {\n if (state.reset === true) {\n state.reset = false;\n index = -1;\n continue;\n }\n\n var _state$orderedModifie = state.orderedModifiers[index],\n fn = _state$orderedModifie.fn,\n _state$orderedModifie2 = _state$orderedModifie.options,\n _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2,\n name = _state$orderedModifie.name;\n\n if (typeof fn === 'function') {\n state = fn({\n state: state,\n options: _options,\n name: name,\n instance: instance\n }) || state;\n }\n }\n },\n // Async and optimistically optimized update – it will not be executed if\n // not necessary (debounced to run at most once-per-tick)\n update: debounce(function () {\n return new Promise(function (resolve) {\n instance.forceUpdate();\n resolve(state);\n });\n }),\n destroy: function destroy() {\n cleanupModifierEffects();\n isDestroyed = true;\n }\n };\n\n if (!areValidElements(reference, popper)) {\n return instance;\n }\n\n instance.setOptions(options).then(function (state) {\n if (!isDestroyed && options.onFirstUpdate) {\n options.onFirstUpdate(state);\n }\n }); // Modifiers have the ability to execute arbitrary code before the first\n // update cycle runs. They will be executed in the same order as the update\n // cycle. This is useful when a modifier adds some persistent data that\n // other modifiers need to use, but the modifier is run after the dependent\n // one.\n\n function runModifierEffects() {\n state.orderedModifiers.forEach(function (_ref) {\n var name = _ref.name,\n _ref$options = _ref.options,\n options = _ref$options === void 0 ? {} : _ref$options,\n effect = _ref.effect;\n\n if (typeof effect === 'function') {\n var cleanupFn = effect({\n state: state,\n name: name,\n instance: instance,\n options: options\n });\n\n var noopFn = function noopFn() {};\n\n effectCleanupFns.push(cleanupFn || noopFn);\n }\n });\n }\n\n function cleanupModifierEffects() {\n effectCleanupFns.forEach(function (fn) {\n return fn();\n });\n effectCleanupFns = [];\n }\n\n return instance;\n };\n}\nexport var createPopper = /*#__PURE__*/popperGenerator(); // eslint-disable-next-line import/no-unused-modules\n\nexport { detectOverflow };","export default function debounce(fn) {\n var pending;\n return function () {\n if (!pending) {\n pending = new Promise(function (resolve) {\n Promise.resolve().then(function () {\n pending = undefined;\n resolve(fn());\n });\n });\n }\n\n return pending;\n };\n}","export default function mergeByName(modifiers) {\n var merged = modifiers.reduce(function (merged, current) {\n var existing = merged[current.name];\n merged[current.name] = existing ? Object.assign({}, existing, current, {\n options: Object.assign({}, existing.options, current.options),\n data: Object.assign({}, existing.data, current.data)\n }) : current;\n return merged;\n }, {}); // IE11 does not support Object.values\n\n return Object.keys(merged).map(function (key) {\n return merged[key];\n });\n}","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow };","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nimport offset from \"./modifiers/offset.js\";\nimport flip from \"./modifiers/flip.js\";\nimport preventOverflow from \"./modifiers/preventOverflow.js\";\nimport arrow from \"./modifiers/arrow.js\";\nimport hide from \"./modifiers/hide.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles, offset, flip, preventOverflow, arrow, hide];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow }; // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper as createPopperLite } from \"./popper-lite.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport * from \"./modifiers/index.js\";","/**\n * --------------------------------------------------------------------------\n * Bootstrap dropdown.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport * as Popper from '@popperjs/core'\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport Manipulator from './dom/manipulator.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport {\n defineJQueryPlugin,\n execute,\n getElement,\n getNextActiveElement,\n isDisabled,\n isElement,\n isRTL,\n isVisible,\n noop\n} from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'dropdown'\nconst DATA_KEY = 'bs.dropdown'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst ESCAPE_KEY = 'Escape'\nconst TAB_KEY = 'Tab'\nconst ARROW_UP_KEY = 'ArrowUp'\nconst ARROW_DOWN_KEY = 'ArrowDown'\nconst RIGHT_MOUSE_BUTTON = 2 // MouseEvent.button value for the secondary button, usually the right button\n\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_DROPUP = 'dropup'\nconst CLASS_NAME_DROPEND = 'dropend'\nconst CLASS_NAME_DROPSTART = 'dropstart'\nconst CLASS_NAME_DROPUP_CENTER = 'dropup-center'\nconst CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center'\n\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"dropdown\"]:not(.disabled):not(:disabled)'\nconst SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE}.${CLASS_NAME_SHOW}`\nconst SELECTOR_MENU = '.dropdown-menu'\nconst SELECTOR_NAVBAR = '.navbar'\nconst SELECTOR_NAVBAR_NAV = '.navbar-nav'\nconst SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'\n\nconst PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start'\nconst PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end'\nconst PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start'\nconst PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end'\nconst PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start'\nconst PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start'\nconst PLACEMENT_TOPCENTER = 'top'\nconst PLACEMENT_BOTTOMCENTER = 'bottom'\n\nconst Default = {\n autoClose: true,\n boundary: 'clippingParents',\n display: 'dynamic',\n offset: [0, 2],\n popperConfig: null,\n reference: 'toggle'\n}\n\nconst DefaultType = {\n autoClose: '(boolean|string)',\n boundary: '(string|element)',\n display: 'string',\n offset: '(array|string|function)',\n popperConfig: '(null|object|function)',\n reference: '(string|element|object)'\n}\n\n/**\n * Class definition\n */\n\nclass Dropdown extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n this._popper = null\n this._parent = this._element.parentNode // dropdown wrapper\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] ||\n SelectorEngine.prev(this._element, SELECTOR_MENU)[0] ||\n SelectorEngine.findOne(SELECTOR_MENU, this._parent)\n this._inNavbar = this._detectNavbar()\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n toggle() {\n return this._isShown() ? this.hide() : this.show()\n }\n\n show() {\n if (isDisabled(this._element) || this._isShown()) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, relatedTarget)\n\n if (showEvent.defaultPrevented) {\n return\n }\n\n this._createPopper()\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop)\n }\n }\n\n this._element.focus()\n this._element.setAttribute('aria-expanded', true)\n\n this._menu.classList.add(CLASS_NAME_SHOW)\n this._element.classList.add(CLASS_NAME_SHOW)\n EventHandler.trigger(this._element, EVENT_SHOWN, relatedTarget)\n }\n\n hide() {\n if (isDisabled(this._element) || !this._isShown()) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n\n this._completeHide(relatedTarget)\n }\n\n dispose() {\n if (this._popper) {\n this._popper.destroy()\n }\n\n super.dispose()\n }\n\n update() {\n this._inNavbar = this._detectNavbar()\n if (this._popper) {\n this._popper.update()\n }\n }\n\n // Private\n _completeHide(relatedTarget) {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE, relatedTarget)\n if (hideEvent.defaultPrevented) {\n return\n }\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop)\n }\n }\n\n if (this._popper) {\n this._popper.destroy()\n }\n\n this._menu.classList.remove(CLASS_NAME_SHOW)\n this._element.classList.remove(CLASS_NAME_SHOW)\n this._element.setAttribute('aria-expanded', 'false')\n Manipulator.removeDataAttribute(this._menu, 'popper')\n EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget)\n\n // Explicitly return focus to the trigger element\n this._element.focus()\n }\n\n _getConfig(config) {\n config = super._getConfig(config)\n\n if (typeof config.reference === 'object' && !isElement(config.reference) &&\n typeof config.reference.getBoundingClientRect !== 'function'\n ) {\n // Popper virtual elements require a getBoundingClientRect method\n throw new TypeError(`${NAME.toUpperCase()}: Option \"reference\" provided type \"object\" without a required \"getBoundingClientRect\" method.`)\n }\n\n return config\n }\n\n _createPopper() {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s dropdowns require Popper (https://popper.js.org/docs/v2/)')\n }\n\n let referenceElement = this._element\n\n if (this._config.reference === 'parent') {\n referenceElement = this._parent\n } else if (isElement(this._config.reference)) {\n referenceElement = getElement(this._config.reference)\n } else if (typeof this._config.reference === 'object') {\n referenceElement = this._config.reference\n }\n\n const popperConfig = this._getPopperConfig()\n this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig)\n }\n\n _isShown() {\n return this._menu.classList.contains(CLASS_NAME_SHOW)\n }\n\n _getPlacement() {\n const parentDropdown = this._parent\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {\n return PLACEMENT_RIGHT\n }\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) {\n return PLACEMENT_LEFT\n }\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) {\n return PLACEMENT_TOPCENTER\n }\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) {\n return PLACEMENT_BOTTOMCENTER\n }\n\n // We need to trim the value because custom properties can also include spaces\n const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end'\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) {\n return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP\n }\n\n return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM\n }\n\n _detectNavbar() {\n return this._element.closest(SELECTOR_NAVBAR) !== null\n }\n\n _getOffset() {\n const { offset } = this._config\n\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10))\n }\n\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element)\n }\n\n return offset\n }\n\n _getPopperConfig() {\n const defaultBsPopperConfig = {\n placement: this._getPlacement(),\n modifiers: [{\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n },\n {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n }]\n }\n\n // Disable Popper if we have a static display or Dropdown is in Navbar\n if (this._inNavbar || this._config.display === 'static') {\n Manipulator.setDataAttribute(this._menu, 'popper', 'static') // TODO: v6 remove\n defaultBsPopperConfig.modifiers = [{\n name: 'applyStyles',\n enabled: false\n }]\n }\n\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])\n }\n }\n\n _selectMenuItem({ key, target }) {\n const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element))\n\n if (!items.length) {\n return\n }\n\n // if target isn't included in items (e.g. when expanding the dropdown)\n // allow cycling to get the last item in case key equals ARROW_UP_KEY\n getNextActiveElement(items, target, key === ARROW_DOWN_KEY, !items.includes(target)).focus()\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Dropdown.getOrCreateInstance(this, config)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n })\n }\n\n static clearMenus(event) {\n if (event.button === RIGHT_MOUSE_BUTTON || (event.type === 'keyup' && event.key !== TAB_KEY)) {\n return\n }\n\n const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN)\n\n for (const toggle of openToggles) {\n const context = Dropdown.getInstance(toggle)\n if (!context || context._config.autoClose === false) {\n continue\n }\n\n const composedPath = event.composedPath()\n const isMenuTarget = composedPath.includes(context._menu)\n if (\n composedPath.includes(context._element) ||\n (context._config.autoClose === 'inside' && !isMenuTarget) ||\n (context._config.autoClose === 'outside' && isMenuTarget)\n ) {\n continue\n }\n\n // Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu\n if (context._menu.contains(event.target) && ((event.type === 'keyup' && event.key === TAB_KEY) || /input|select|option|textarea|form/i.test(event.target.tagName))) {\n continue\n }\n\n const relatedTarget = { relatedTarget: context._element }\n\n if (event.type === 'click') {\n relatedTarget.clickEvent = event\n }\n\n context._completeHide(relatedTarget)\n }\n }\n\n static dataApiKeydownHandler(event) {\n // If not an UP | DOWN | ESCAPE key => not a dropdown command\n // If input/textarea && if key is other than ESCAPE => not a dropdown command\n\n const isInput = /input|textarea/i.test(event.target.tagName)\n const isEscapeEvent = event.key === ESCAPE_KEY\n const isUpOrDownEvent = [ARROW_UP_KEY, ARROW_DOWN_KEY].includes(event.key)\n\n if (!isUpOrDownEvent && !isEscapeEvent) {\n return\n }\n\n if (isInput && !isEscapeEvent) {\n return\n }\n\n event.preventDefault()\n\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE) ?\n this :\n (SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0] ||\n SelectorEngine.next(this, SELECTOR_DATA_TOGGLE)[0] ||\n SelectorEngine.findOne(SELECTOR_DATA_TOGGLE, event.delegateTarget.parentNode))\n\n const instance = Dropdown.getOrCreateInstance(getToggleButton)\n\n if (isUpOrDownEvent) {\n event.stopPropagation()\n instance.show()\n instance._selectMenuItem(event)\n return\n }\n\n if (instance._isShown()) { // else is escape and we check if it is shown\n event.stopPropagation()\n instance.hide()\n getToggleButton.focus()\n }\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Dropdown.dataApiKeydownHandler)\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler)\nEventHandler.on(document, EVENT_CLICK_DATA_API, Dropdown.clearMenus)\nEventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus)\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n event.preventDefault()\n Dropdown.getOrCreateInstance(this).toggle()\n})\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Dropdown)\n\nexport default Dropdown\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/backdrop.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport EventHandler from '../dom/event-handler.js'\nimport Config from './config.js'\nimport {\n execute, executeAfterTransition, getElement, reflow\n} from './index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'backdrop'\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_SHOW = 'show'\nconst EVENT_MOUSEDOWN = `mousedown.bs.${NAME}`\n\nconst Default = {\n className: 'modal-backdrop',\n clickCallback: null,\n isAnimated: false,\n isVisible: true, // if false, we use the backdrop helper without adding any element to the dom\n rootElement: 'body' // give the choice to place backdrop under different elements\n}\n\nconst DefaultType = {\n className: 'string',\n clickCallback: '(function|null)',\n isAnimated: 'boolean',\n isVisible: 'boolean',\n rootElement: '(element|string)'\n}\n\n/**\n * Class definition\n */\n\nclass Backdrop extends Config {\n constructor(config) {\n super()\n this._config = this._getConfig(config)\n this._isAppended = false\n this._element = null\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n show(callback) {\n if (!this._config.isVisible) {\n execute(callback)\n return\n }\n\n this._append()\n\n const element = this._getElement()\n if (this._config.isAnimated) {\n reflow(element)\n }\n\n element.classList.add(CLASS_NAME_SHOW)\n\n this._emulateAnimation(() => {\n execute(callback)\n })\n }\n\n hide(callback) {\n if (!this._config.isVisible) {\n execute(callback)\n return\n }\n\n this._getElement().classList.remove(CLASS_NAME_SHOW)\n\n this._emulateAnimation(() => {\n this.dispose()\n execute(callback)\n })\n }\n\n dispose() {\n if (!this._isAppended) {\n return\n }\n\n EventHandler.off(this._element, EVENT_MOUSEDOWN)\n\n this._element.remove()\n this._isAppended = false\n }\n\n // Private\n _getElement() {\n if (!this._element) {\n const backdrop = document.createElement('div')\n backdrop.className = this._config.className\n if (this._config.isAnimated) {\n backdrop.classList.add(CLASS_NAME_FADE)\n }\n\n this._element = backdrop\n }\n\n return this._element\n }\n\n _configAfterMerge(config) {\n // use getElement() with the default \"body\" to get a fresh Element on each instantiation\n config.rootElement = getElement(config.rootElement)\n return config\n }\n\n _append() {\n if (this._isAppended) {\n return\n }\n\n const element = this._getElement()\n this._config.rootElement.append(element)\n\n EventHandler.on(element, EVENT_MOUSEDOWN, () => {\n execute(this._config.clickCallback)\n })\n\n this._isAppended = true\n }\n\n _emulateAnimation(callback) {\n executeAfterTransition(callback, this._getElement(), this._config.isAnimated)\n }\n}\n\nexport default Backdrop\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/focustrap.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport EventHandler from '../dom/event-handler.js'\nimport SelectorEngine from '../dom/selector-engine.js'\nimport Config from './config.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'focustrap'\nconst DATA_KEY = 'bs.focustrap'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst EVENT_FOCUSIN = `focusin${EVENT_KEY}`\nconst EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY}`\n\nconst TAB_KEY = 'Tab'\nconst TAB_NAV_FORWARD = 'forward'\nconst TAB_NAV_BACKWARD = 'backward'\n\nconst Default = {\n autofocus: true,\n trapElement: null // The element to trap focus inside of\n}\n\nconst DefaultType = {\n autofocus: 'boolean',\n trapElement: 'element'\n}\n\n/**\n * Class definition\n */\n\nclass FocusTrap extends Config {\n constructor(config) {\n super()\n this._config = this._getConfig(config)\n this._isActive = false\n this._lastTabNavDirection = null\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n activate() {\n if (this._isActive) {\n return\n }\n\n if (this._config.autofocus) {\n this._config.trapElement.focus()\n }\n\n EventHandler.off(document, EVENT_KEY) // guard against infinite focus loop\n EventHandler.on(document, EVENT_FOCUSIN, event => this._handleFocusin(event))\n EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event))\n\n this._isActive = true\n }\n\n deactivate() {\n if (!this._isActive) {\n return\n }\n\n this._isActive = false\n EventHandler.off(document, EVENT_KEY)\n }\n\n // Private\n _handleFocusin(event) {\n const { trapElement } = this._config\n\n if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) {\n return\n }\n\n const elements = SelectorEngine.focusableChildren(trapElement)\n\n if (elements.length === 0) {\n trapElement.focus()\n } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) {\n elements[elements.length - 1].focus()\n } else {\n elements[0].focus()\n }\n }\n\n _handleKeydown(event) {\n if (event.key !== TAB_KEY) {\n return\n }\n\n this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD\n }\n}\n\nexport default FocusTrap\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/scrollBar.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport Manipulator from '../dom/manipulator.js'\nimport SelectorEngine from '../dom/selector-engine.js'\nimport { isElement } from './index.js'\n\n/**\n * Constants\n */\n\nconst SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top'\nconst SELECTOR_STICKY_CONTENT = '.sticky-top'\nconst PROPERTY_PADDING = 'padding-right'\nconst PROPERTY_MARGIN = 'margin-right'\n\n/**\n * Class definition\n */\n\nclass ScrollBarHelper {\n constructor() {\n this._element = document.body\n }\n\n // Public\n getWidth() {\n // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes\n const documentWidth = document.documentElement.clientWidth\n return Math.abs(window.innerWidth - documentWidth)\n }\n\n hide() {\n const width = this.getWidth()\n this._disableOverFlow()\n // give padding to element to balance the hidden scrollbar width\n this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width)\n // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth\n this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width)\n this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width)\n }\n\n reset() {\n this._resetElementAttributes(this._element, 'overflow')\n this._resetElementAttributes(this._element, PROPERTY_PADDING)\n this._resetElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING)\n this._resetElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN)\n }\n\n isOverflowing() {\n return this.getWidth() > 0\n }\n\n // Private\n _disableOverFlow() {\n this._saveInitialAttribute(this._element, 'overflow')\n this._element.style.overflow = 'hidden'\n }\n\n _setElementAttributes(selector, styleProperty, callback) {\n const scrollbarWidth = this.getWidth()\n const manipulationCallBack = element => {\n if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) {\n return\n }\n\n this._saveInitialAttribute(element, styleProperty)\n const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty)\n element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`)\n }\n\n this._applyManipulationCallback(selector, manipulationCallBack)\n }\n\n _saveInitialAttribute(element, styleProperty) {\n const actualValue = element.style.getPropertyValue(styleProperty)\n if (actualValue) {\n Manipulator.setDataAttribute(element, styleProperty, actualValue)\n }\n }\n\n _resetElementAttributes(selector, styleProperty) {\n const manipulationCallBack = element => {\n const value = Manipulator.getDataAttribute(element, styleProperty)\n // We only want to remove the property if the value is `null`; the value can also be zero\n if (value === null) {\n element.style.removeProperty(styleProperty)\n return\n }\n\n Manipulator.removeDataAttribute(element, styleProperty)\n element.style.setProperty(styleProperty, value)\n }\n\n this._applyManipulationCallback(selector, manipulationCallBack)\n }\n\n _applyManipulationCallback(selector, callBack) {\n if (isElement(selector)) {\n callBack(selector)\n return\n }\n\n for (const sel of SelectorEngine.find(selector, this._element)) {\n callBack(sel)\n }\n }\n}\n\nexport default ScrollBarHelper\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap modal.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport Backdrop from './util/backdrop.js'\nimport { enableDismissTrigger } from './util/component-functions.js'\nimport FocusTrap from './util/focustrap.js'\nimport {\n defineJQueryPlugin, isRTL, isVisible, reflow\n} from './util/index.js'\nimport ScrollBarHelper from './util/scrollbar.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'modal'\nconst DATA_KEY = 'bs.modal'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst ESCAPE_KEY = 'Escape'\n\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_RESIZE = `resize${EVENT_KEY}`\nconst EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`\nconst EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}`\nconst EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_OPEN = 'modal-open'\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_STATIC = 'modal-static'\n\nconst OPEN_SELECTOR = '.modal.show'\nconst SELECTOR_DIALOG = '.modal-dialog'\nconst SELECTOR_MODAL_BODY = '.modal-body'\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"modal\"]'\n\nconst Default = {\n backdrop: true,\n focus: true,\n keyboard: true\n}\n\nconst DefaultType = {\n backdrop: '(boolean|string)',\n focus: 'boolean',\n keyboard: 'boolean'\n}\n\n/**\n * Class definition\n */\n\nclass Modal extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element)\n this._backdrop = this._initializeBackDrop()\n this._focustrap = this._initializeFocusTrap()\n this._isShown = false\n this._isTransitioning = false\n this._scrollBar = new ScrollBarHelper()\n\n this._addEventListeners()\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget)\n }\n\n show(relatedTarget) {\n if (this._isShown || this._isTransitioning) {\n return\n }\n\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, {\n relatedTarget\n })\n\n if (showEvent.defaultPrevented) {\n return\n }\n\n this._isShown = true\n this._isTransitioning = true\n\n this._scrollBar.hide()\n\n document.body.classList.add(CLASS_NAME_OPEN)\n\n this._adjustDialog()\n\n this._backdrop.show(() => this._showElement(relatedTarget))\n }\n\n hide() {\n if (!this._isShown || this._isTransitioning) {\n return\n }\n\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)\n\n if (hideEvent.defaultPrevented) {\n return\n }\n\n this._isShown = false\n this._isTransitioning = true\n this._focustrap.deactivate()\n\n this._element.classList.remove(CLASS_NAME_SHOW)\n\n this._queueCallback(() => this._hideModal(), this._element, this._isAnimated())\n }\n\n dispose() {\n EventHandler.off(window, EVENT_KEY)\n EventHandler.off(this._dialog, EVENT_KEY)\n\n this._backdrop.dispose()\n this._focustrap.deactivate()\n\n super.dispose()\n }\n\n handleUpdate() {\n this._adjustDialog()\n }\n\n // Private\n _initializeBackDrop() {\n return new Backdrop({\n isVisible: Boolean(this._config.backdrop), // 'static' option will be translated to true, and booleans will keep their value,\n isAnimated: this._isAnimated()\n })\n }\n\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n })\n }\n\n _showElement(relatedTarget) {\n // try to append dynamic modal\n if (!document.body.contains(this._element)) {\n document.body.append(this._element)\n }\n\n this._element.style.display = 'block'\n this._element.removeAttribute('aria-hidden')\n this._element.setAttribute('aria-modal', true)\n this._element.setAttribute('role', 'dialog')\n this._element.scrollTop = 0\n\n const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog)\n if (modalBody) {\n modalBody.scrollTop = 0\n }\n\n reflow(this._element)\n\n this._element.classList.add(CLASS_NAME_SHOW)\n\n const transitionComplete = () => {\n if (this._config.focus) {\n this._focustrap.activate()\n }\n\n this._isTransitioning = false\n EventHandler.trigger(this._element, EVENT_SHOWN, {\n relatedTarget\n })\n }\n\n this._queueCallback(transitionComplete, this._dialog, this._isAnimated())\n }\n\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n if (event.key !== ESCAPE_KEY) {\n return\n }\n\n if (this._config.keyboard) {\n this.hide()\n return\n }\n\n this._triggerBackdropTransition()\n })\n\n EventHandler.on(window, EVENT_RESIZE, () => {\n if (this._isShown && !this._isTransitioning) {\n this._adjustDialog()\n }\n })\n\n EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => {\n // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks\n EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => {\n if (this._element !== event.target || this._element !== event2.target) {\n return\n }\n\n if (this._config.backdrop === 'static') {\n this._triggerBackdropTransition()\n return\n }\n\n if (this._config.backdrop) {\n this.hide()\n }\n })\n })\n }\n\n _hideModal() {\n this._element.style.display = 'none'\n this._element.setAttribute('aria-hidden', true)\n this._element.removeAttribute('aria-modal')\n this._element.removeAttribute('role')\n this._isTransitioning = false\n\n this._backdrop.hide(() => {\n document.body.classList.remove(CLASS_NAME_OPEN)\n this._resetAdjustments()\n this._scrollBar.reset()\n EventHandler.trigger(this._element, EVENT_HIDDEN)\n })\n }\n\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_FADE)\n }\n\n _triggerBackdropTransition() {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)\n if (hideEvent.defaultPrevented) {\n return\n }\n\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight\n const initialOverflowY = this._element.style.overflowY\n // return if the following background transition hasn't yet completed\n if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {\n return\n }\n\n if (!isModalOverflowing) {\n this._element.style.overflowY = 'hidden'\n }\n\n this._element.classList.add(CLASS_NAME_STATIC)\n this._queueCallback(() => {\n this._element.classList.remove(CLASS_NAME_STATIC)\n this._queueCallback(() => {\n this._element.style.overflowY = initialOverflowY\n }, this._dialog)\n }, this._dialog)\n\n this._element.focus()\n }\n\n /**\n * The following methods are used to handle overflowing modals\n */\n\n _adjustDialog() {\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight\n const scrollbarWidth = this._scrollBar.getWidth()\n const isBodyOverflowing = scrollbarWidth > 0\n\n if (isBodyOverflowing && !isModalOverflowing) {\n const property = isRTL() ? 'paddingLeft' : 'paddingRight'\n this._element.style[property] = `${scrollbarWidth}px`\n }\n\n if (!isBodyOverflowing && isModalOverflowing) {\n const property = isRTL() ? 'paddingRight' : 'paddingLeft'\n this._element.style[property] = `${scrollbarWidth}px`\n }\n }\n\n _resetAdjustments() {\n this._element.style.paddingLeft = ''\n this._element.style.paddingRight = ''\n }\n\n // Static\n static jQueryInterface(config, relatedTarget) {\n return this.each(function () {\n const data = Modal.getOrCreateInstance(this, config)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config](relatedTarget)\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n const target = SelectorEngine.getElementFromSelector(this)\n\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault()\n }\n\n EventHandler.one(target, EVENT_SHOW, showEvent => {\n if (showEvent.defaultPrevented) {\n // only register focus restorer if modal will actually get shown\n return\n }\n\n EventHandler.one(target, EVENT_HIDDEN, () => {\n if (isVisible(this)) {\n this.focus()\n }\n })\n })\n\n // avoid conflict when clicking modal toggler while another one is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR)\n if (alreadyOpen) {\n Modal.getInstance(alreadyOpen).hide()\n }\n\n const data = Modal.getOrCreateInstance(target)\n\n data.toggle(this)\n})\n\nenableDismissTrigger(Modal)\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Modal)\n\nexport default Modal\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap offcanvas.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport Backdrop from './util/backdrop.js'\nimport { enableDismissTrigger } from './util/component-functions.js'\nimport FocusTrap from './util/focustrap.js'\nimport {\n defineJQueryPlugin,\n isDisabled,\n isVisible\n} from './util/index.js'\nimport ScrollBarHelper from './util/scrollbar.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'offcanvas'\nconst DATA_KEY = 'bs.offcanvas'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`\nconst ESCAPE_KEY = 'Escape'\n\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_SHOWING = 'showing'\nconst CLASS_NAME_HIDING = 'hiding'\nconst CLASS_NAME_BACKDROP = 'offcanvas-backdrop'\nconst OPEN_SELECTOR = '.offcanvas.show'\n\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_RESIZE = `resize${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`\n\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"offcanvas\"]'\n\nconst Default = {\n backdrop: true,\n keyboard: true,\n scroll: false\n}\n\nconst DefaultType = {\n backdrop: '(boolean|string)',\n keyboard: 'boolean',\n scroll: 'boolean'\n}\n\n/**\n * Class definition\n */\n\nclass Offcanvas extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n this._isShown = false\n this._backdrop = this._initializeBackDrop()\n this._focustrap = this._initializeFocusTrap()\n this._addEventListeners()\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget)\n }\n\n show(relatedTarget) {\n if (this._isShown) {\n return\n }\n\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, { relatedTarget })\n\n if (showEvent.defaultPrevented) {\n return\n }\n\n this._isShown = true\n this._backdrop.show()\n\n if (!this._config.scroll) {\n new ScrollBarHelper().hide()\n }\n\n this._element.setAttribute('aria-modal', true)\n this._element.setAttribute('role', 'dialog')\n this._element.classList.add(CLASS_NAME_SHOWING)\n\n const completeCallBack = () => {\n if (!this._config.scroll || this._config.backdrop) {\n this._focustrap.activate()\n }\n\n this._element.classList.add(CLASS_NAME_SHOW)\n this._element.classList.remove(CLASS_NAME_SHOWING)\n EventHandler.trigger(this._element, EVENT_SHOWN, { relatedTarget })\n }\n\n this._queueCallback(completeCallBack, this._element, true)\n }\n\n hide() {\n if (!this._isShown) {\n return\n }\n\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)\n\n if (hideEvent.defaultPrevented) {\n return\n }\n\n this._focustrap.deactivate()\n this._element.blur()\n this._isShown = false\n this._element.classList.add(CLASS_NAME_HIDING)\n this._backdrop.hide()\n\n const completeCallback = () => {\n this._element.classList.remove(CLASS_NAME_SHOW, CLASS_NAME_HIDING)\n this._element.removeAttribute('aria-modal')\n this._element.removeAttribute('role')\n\n if (!this._config.scroll) {\n new ScrollBarHelper().reset()\n }\n\n EventHandler.trigger(this._element, EVENT_HIDDEN)\n }\n\n this._queueCallback(completeCallback, this._element, true)\n }\n\n dispose() {\n this._backdrop.dispose()\n this._focustrap.deactivate()\n super.dispose()\n }\n\n // Private\n _initializeBackDrop() {\n const clickCallback = () => {\n if (this._config.backdrop === 'static') {\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)\n return\n }\n\n this.hide()\n }\n\n // 'static' option will be translated to true, and booleans will keep their value\n const isVisible = Boolean(this._config.backdrop)\n\n return new Backdrop({\n className: CLASS_NAME_BACKDROP,\n isVisible,\n isAnimated: true,\n rootElement: this._element.parentNode,\n clickCallback: isVisible ? clickCallback : null\n })\n }\n\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n })\n }\n\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n if (event.key !== ESCAPE_KEY) {\n return\n }\n\n if (this._config.keyboard) {\n this.hide()\n return\n }\n\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)\n })\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Offcanvas.getOrCreateInstance(this, config)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config](this)\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n const target = SelectorEngine.getElementFromSelector(this)\n\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault()\n }\n\n if (isDisabled(this)) {\n return\n }\n\n EventHandler.one(target, EVENT_HIDDEN, () => {\n // focus on trigger when it is closed\n if (isVisible(this)) {\n this.focus()\n }\n })\n\n // avoid conflict when clicking a toggler of an offcanvas, while another is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR)\n if (alreadyOpen && alreadyOpen !== target) {\n Offcanvas.getInstance(alreadyOpen).hide()\n }\n\n const data = Offcanvas.getOrCreateInstance(target)\n data.toggle(this)\n})\n\nEventHandler.on(window, EVENT_LOAD_DATA_API, () => {\n for (const selector of SelectorEngine.find(OPEN_SELECTOR)) {\n Offcanvas.getOrCreateInstance(selector).show()\n }\n})\n\nEventHandler.on(window, EVENT_RESIZE, () => {\n for (const element of SelectorEngine.find('[aria-modal][class*=show][class*=offcanvas-]')) {\n if (getComputedStyle(element).position !== 'fixed') {\n Offcanvas.getOrCreateInstance(element).hide()\n }\n }\n})\n\nenableDismissTrigger(Offcanvas)\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Offcanvas)\n\nexport default Offcanvas\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/sanitizer.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n// js-docs-start allow-list\nconst ARIA_ATTRIBUTE_PATTERN = /^aria-[\\w-]*$/i\n\nexport const DefaultAllowlist = {\n // Global attributes allowed on any supplied element below.\n '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],\n a: ['target', 'href', 'title', 'rel'],\n area: [],\n b: [],\n br: [],\n col: [],\n code: [],\n dd: [],\n div: [],\n dl: [],\n dt: [],\n em: [],\n hr: [],\n h1: [],\n h2: [],\n h3: [],\n h4: [],\n h5: [],\n h6: [],\n i: [],\n img: ['src', 'srcset', 'alt', 'title', 'width', 'height'],\n li: [],\n ol: [],\n p: [],\n pre: [],\n s: [],\n small: [],\n span: [],\n sub: [],\n sup: [],\n strong: [],\n u: [],\n ul: []\n}\n// js-docs-end allow-list\n\nconst uriAttributes = new Set([\n 'background',\n 'cite',\n 'href',\n 'itemtype',\n 'longdesc',\n 'poster',\n 'src',\n 'xlink:href'\n])\n\n/**\n * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation\n * contexts.\n *\n * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38\n */\nconst SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i\n\nconst allowedAttribute = (attribute, allowedAttributeList) => {\n const attributeName = attribute.nodeName.toLowerCase()\n\n if (allowedAttributeList.includes(attributeName)) {\n if (uriAttributes.has(attributeName)) {\n return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue))\n }\n\n return true\n }\n\n // Check if a regular expression validates the attribute.\n return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp)\n .some(regex => regex.test(attributeName))\n}\n\nexport function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {\n if (!unsafeHtml.length) {\n return unsafeHtml\n }\n\n if (sanitizeFunction && typeof sanitizeFunction === 'function') {\n return sanitizeFunction(unsafeHtml)\n }\n\n const domParser = new window.DOMParser()\n const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html')\n const elements = [].concat(...createdDocument.body.querySelectorAll('*'))\n\n for (const element of elements) {\n const elementName = element.nodeName.toLowerCase()\n\n if (!Object.keys(allowList).includes(elementName)) {\n element.remove()\n continue\n }\n\n const attributeList = [].concat(...element.attributes)\n const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || [])\n\n for (const attribute of attributeList) {\n if (!allowedAttribute(attribute, allowedAttributes)) {\n element.removeAttribute(attribute.nodeName)\n }\n }\n }\n\n return createdDocument.body.innerHTML\n}\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/template-factory.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport SelectorEngine from '../dom/selector-engine.js'\nimport Config from './config.js'\nimport { DefaultAllowlist, sanitizeHtml } from './sanitizer.js'\nimport { execute, getElement, isElement } from './index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'TemplateFactory'\n\nconst Default = {\n allowList: DefaultAllowlist,\n content: {}, // { selector : text , selector2 : text2 , }\n extraClass: '',\n html: false,\n sanitize: true,\n sanitizeFn: null,\n template: '
'\n}\n\nconst DefaultType = {\n allowList: 'object',\n content: 'object',\n extraClass: '(string|function)',\n html: 'boolean',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n template: 'string'\n}\n\nconst DefaultContentType = {\n entry: '(string|element|function|null)',\n selector: '(string|element)'\n}\n\n/**\n * Class definition\n */\n\nclass TemplateFactory extends Config {\n constructor(config) {\n super()\n this._config = this._getConfig(config)\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n getContent() {\n return Object.values(this._config.content)\n .map(config => this._resolvePossibleFunction(config))\n .filter(Boolean)\n }\n\n hasContent() {\n return this.getContent().length > 0\n }\n\n changeContent(content) {\n this._checkContent(content)\n this._config.content = { ...this._config.content, ...content }\n return this\n }\n\n toHtml() {\n const templateWrapper = document.createElement('div')\n templateWrapper.innerHTML = this._maybeSanitize(this._config.template)\n\n for (const [selector, text] of Object.entries(this._config.content)) {\n this._setContent(templateWrapper, text, selector)\n }\n\n const template = templateWrapper.children[0]\n const extraClass = this._resolvePossibleFunction(this._config.extraClass)\n\n if (extraClass) {\n template.classList.add(...extraClass.split(' '))\n }\n\n return template\n }\n\n // Private\n _typeCheckConfig(config) {\n super._typeCheckConfig(config)\n this._checkContent(config.content)\n }\n\n _checkContent(arg) {\n for (const [selector, content] of Object.entries(arg)) {\n super._typeCheckConfig({ selector, entry: content }, DefaultContentType)\n }\n }\n\n _setContent(template, content, selector) {\n const templateElement = SelectorEngine.findOne(selector, template)\n\n if (!templateElement) {\n return\n }\n\n content = this._resolvePossibleFunction(content)\n\n if (!content) {\n templateElement.remove()\n return\n }\n\n if (isElement(content)) {\n this._putElementInTemplate(getElement(content), templateElement)\n return\n }\n\n if (this._config.html) {\n templateElement.innerHTML = this._maybeSanitize(content)\n return\n }\n\n templateElement.textContent = content\n }\n\n _maybeSanitize(arg) {\n return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg\n }\n\n _resolvePossibleFunction(arg) {\n return execute(arg, [undefined, this])\n }\n\n _putElementInTemplate(element, templateElement) {\n if (this._config.html) {\n templateElement.innerHTML = ''\n templateElement.append(element)\n return\n }\n\n templateElement.textContent = element.textContent\n }\n}\n\nexport default TemplateFactory\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap tooltip.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport * as Popper from '@popperjs/core'\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport Manipulator from './dom/manipulator.js'\nimport {\n defineJQueryPlugin, execute, findShadowRoot, getElement, getUID, isRTL, noop\n} from './util/index.js'\nimport { DefaultAllowlist } from './util/sanitizer.js'\nimport TemplateFactory from './util/template-factory.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'tooltip'\nconst DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn'])\n\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_MODAL = 'modal'\nconst CLASS_NAME_SHOW = 'show'\n\nconst SELECTOR_TOOLTIP_INNER = '.tooltip-inner'\nconst SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`\n\nconst EVENT_MODAL_HIDE = 'hide.bs.modal'\n\nconst TRIGGER_HOVER = 'hover'\nconst TRIGGER_FOCUS = 'focus'\nconst TRIGGER_CLICK = 'click'\nconst TRIGGER_MANUAL = 'manual'\n\nconst EVENT_HIDE = 'hide'\nconst EVENT_HIDDEN = 'hidden'\nconst EVENT_SHOW = 'show'\nconst EVENT_SHOWN = 'shown'\nconst EVENT_INSERTED = 'inserted'\nconst EVENT_CLICK = 'click'\nconst EVENT_FOCUSIN = 'focusin'\nconst EVENT_FOCUSOUT = 'focusout'\nconst EVENT_MOUSEENTER = 'mouseenter'\nconst EVENT_MOUSELEAVE = 'mouseleave'\n\nconst AttachmentMap = {\n AUTO: 'auto',\n TOP: 'top',\n RIGHT: isRTL() ? 'left' : 'right',\n BOTTOM: 'bottom',\n LEFT: isRTL() ? 'right' : 'left'\n}\n\nconst Default = {\n allowList: DefaultAllowlist,\n animation: true,\n boundary: 'clippingParents',\n container: false,\n customClass: '',\n delay: 0,\n fallbackPlacements: ['top', 'right', 'bottom', 'left'],\n html: false,\n offset: [0, 6],\n placement: 'top',\n popperConfig: null,\n sanitize: true,\n sanitizeFn: null,\n selector: false,\n template: '
' +\n '
' +\n '
' +\n '
',\n title: '',\n trigger: 'hover focus'\n}\n\nconst DefaultType = {\n allowList: 'object',\n animation: 'boolean',\n boundary: '(string|element)',\n container: '(string|element|boolean)',\n customClass: '(string|function)',\n delay: '(number|object)',\n fallbackPlacements: 'array',\n html: 'boolean',\n offset: '(array|string|function)',\n placement: '(string|function)',\n popperConfig: '(null|object|function)',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n selector: '(string|boolean)',\n template: 'string',\n title: '(string|element|function)',\n trigger: 'string'\n}\n\n/**\n * Class definition\n */\n\nclass Tooltip extends BaseComponent {\n constructor(element, config) {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s tooltips require Popper (https://popper.js.org/docs/v2/)')\n }\n\n super(element, config)\n\n // Private\n this._isEnabled = true\n this._timeout = 0\n this._isHovered = null\n this._activeTrigger = {}\n this._popper = null\n this._templateFactory = null\n this._newContent = null\n\n // Protected\n this.tip = null\n\n this._setListeners()\n\n if (!this._config.selector) {\n this._fixTitle()\n }\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n enable() {\n this._isEnabled = true\n }\n\n disable() {\n this._isEnabled = false\n }\n\n toggleEnabled() {\n this._isEnabled = !this._isEnabled\n }\n\n toggle() {\n if (!this._isEnabled) {\n return\n }\n\n if (this._isShown()) {\n this._leave()\n return\n }\n\n this._enter()\n }\n\n dispose() {\n clearTimeout(this._timeout)\n\n EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler)\n\n if (this._element.getAttribute('data-bs-original-title')) {\n this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title'))\n }\n\n this._disposePopper()\n super.dispose()\n }\n\n show() {\n if (this._element.style.display === 'none') {\n throw new Error('Please use show on visible elements')\n }\n\n if (!(this._isWithContent() && this._isEnabled)) {\n return\n }\n\n const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW))\n const shadowRoot = findShadowRoot(this._element)\n const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element)\n\n if (showEvent.defaultPrevented || !isInTheDom) {\n return\n }\n\n // TODO: v6 remove this or make it optional\n this._disposePopper()\n\n const tip = this._getTipElement()\n\n this._element.setAttribute('aria-describedby', tip.getAttribute('id'))\n\n const { container } = this._config\n\n if (!this._element.ownerDocument.documentElement.contains(this.tip)) {\n container.append(tip)\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED))\n }\n\n this._popper = this._createPopper(tip)\n\n tip.classList.add(CLASS_NAME_SHOW)\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop)\n }\n }\n\n const complete = () => {\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN))\n\n if (this._isHovered === false) {\n this._leave()\n }\n\n this._isHovered = false\n }\n\n this._queueCallback(complete, this.tip, this._isAnimated())\n }\n\n hide() {\n if (!this._isShown()) {\n return\n }\n\n const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE))\n if (hideEvent.defaultPrevented) {\n return\n }\n\n const tip = this._getTipElement()\n tip.classList.remove(CLASS_NAME_SHOW)\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop)\n }\n }\n\n this._activeTrigger[TRIGGER_CLICK] = false\n this._activeTrigger[TRIGGER_FOCUS] = false\n this._activeTrigger[TRIGGER_HOVER] = false\n this._isHovered = null // it is a trick to support manual triggering\n\n const complete = () => {\n if (this._isWithActiveTrigger()) {\n return\n }\n\n if (!this._isHovered) {\n this._disposePopper()\n }\n\n this._element.removeAttribute('aria-describedby')\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN))\n }\n\n this._queueCallback(complete, this.tip, this._isAnimated())\n }\n\n update() {\n if (this._popper) {\n this._popper.update()\n }\n }\n\n // Protected\n _isWithContent() {\n return Boolean(this._getTitle())\n }\n\n _getTipElement() {\n if (!this.tip) {\n this.tip = this._createTipElement(this._newContent || this._getContentForTemplate())\n }\n\n return this.tip\n }\n\n _createTipElement(content) {\n const tip = this._getTemplateFactory(content).toHtml()\n\n // TODO: remove this check in v6\n if (!tip) {\n return null\n }\n\n tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW)\n // TODO: v6 the following can be achieved with CSS only\n tip.classList.add(`bs-${this.constructor.NAME}-auto`)\n\n const tipId = getUID(this.constructor.NAME).toString()\n\n tip.setAttribute('id', tipId)\n\n if (this._isAnimated()) {\n tip.classList.add(CLASS_NAME_FADE)\n }\n\n return tip\n }\n\n setContent(content) {\n this._newContent = content\n if (this._isShown()) {\n this._disposePopper()\n this.show()\n }\n }\n\n _getTemplateFactory(content) {\n if (this._templateFactory) {\n this._templateFactory.changeContent(content)\n } else {\n this._templateFactory = new TemplateFactory({\n ...this._config,\n // the `content` var has to be after `this._config`\n // to override config.content in case of popover\n content,\n extraClass: this._resolvePossibleFunction(this._config.customClass)\n })\n }\n\n return this._templateFactory\n }\n\n _getContentForTemplate() {\n return {\n [SELECTOR_TOOLTIP_INNER]: this._getTitle()\n }\n }\n\n _getTitle() {\n return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title')\n }\n\n // Private\n _initializeOnDelegatedTarget(event) {\n return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig())\n }\n\n _isAnimated() {\n return this._config.animation || (this.tip && this.tip.classList.contains(CLASS_NAME_FADE))\n }\n\n _isShown() {\n return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW)\n }\n\n _createPopper(tip) {\n const placement = execute(this._config.placement, [this, tip, this._element])\n const attachment = AttachmentMap[placement.toUpperCase()]\n return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment))\n }\n\n _getOffset() {\n const { offset } = this._config\n\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10))\n }\n\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element)\n }\n\n return offset\n }\n\n _resolvePossibleFunction(arg) {\n return execute(arg, [this._element, this._element])\n }\n\n _getPopperConfig(attachment) {\n const defaultBsPopperConfig = {\n placement: attachment,\n modifiers: [\n {\n name: 'flip',\n options: {\n fallbackPlacements: this._config.fallbackPlacements\n }\n },\n {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n },\n {\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n },\n {\n name: 'arrow',\n options: {\n element: `.${this.constructor.NAME}-arrow`\n }\n },\n {\n name: 'preSetPlacement',\n enabled: true,\n phase: 'beforeMain',\n fn: data => {\n // Pre-set Popper's placement attribute in order to read the arrow sizes properly.\n // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement\n this._getTipElement().setAttribute('data-popper-placement', data.state.placement)\n }\n }\n ]\n }\n\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])\n }\n }\n\n _setListeners() {\n const triggers = this._config.trigger.split(' ')\n\n for (const trigger of triggers) {\n if (trigger === 'click') {\n EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK), this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event)\n context._activeTrigger[TRIGGER_CLICK] = !(context._isShown() && context._activeTrigger[TRIGGER_CLICK])\n context.toggle()\n })\n } else if (trigger !== TRIGGER_MANUAL) {\n const eventIn = trigger === TRIGGER_HOVER ?\n this.constructor.eventName(EVENT_MOUSEENTER) :\n this.constructor.eventName(EVENT_FOCUSIN)\n const eventOut = trigger === TRIGGER_HOVER ?\n this.constructor.eventName(EVENT_MOUSELEAVE) :\n this.constructor.eventName(EVENT_FOCUSOUT)\n\n EventHandler.on(this._element, eventIn, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event)\n context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true\n context._enter()\n })\n EventHandler.on(this._element, eventOut, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event)\n context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] =\n context._element.contains(event.relatedTarget)\n\n context._leave()\n })\n }\n }\n\n this._hideModalHandler = () => {\n if (this._element) {\n this.hide()\n }\n }\n\n EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler)\n }\n\n _fixTitle() {\n const title = this._element.getAttribute('title')\n\n if (!title) {\n return\n }\n\n if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) {\n this._element.setAttribute('aria-label', title)\n }\n\n this._element.setAttribute('data-bs-original-title', title) // DO NOT USE IT. Is only for backwards compatibility\n this._element.removeAttribute('title')\n }\n\n _enter() {\n if (this._isShown() || this._isHovered) {\n this._isHovered = true\n return\n }\n\n this._isHovered = true\n\n this._setTimeout(() => {\n if (this._isHovered) {\n this.show()\n }\n }, this._config.delay.show)\n }\n\n _leave() {\n if (this._isWithActiveTrigger()) {\n return\n }\n\n this._isHovered = false\n\n this._setTimeout(() => {\n if (!this._isHovered) {\n this.hide()\n }\n }, this._config.delay.hide)\n }\n\n _setTimeout(handler, timeout) {\n clearTimeout(this._timeout)\n this._timeout = setTimeout(handler, timeout)\n }\n\n _isWithActiveTrigger() {\n return Object.values(this._activeTrigger).includes(true)\n }\n\n _getConfig(config) {\n const dataAttributes = Manipulator.getDataAttributes(this._element)\n\n for (const dataAttribute of Object.keys(dataAttributes)) {\n if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {\n delete dataAttributes[dataAttribute]\n }\n }\n\n config = {\n ...dataAttributes,\n ...(typeof config === 'object' && config ? config : {})\n }\n config = this._mergeConfigObj(config)\n config = this._configAfterMerge(config)\n this._typeCheckConfig(config)\n return config\n }\n\n _configAfterMerge(config) {\n config.container = config.container === false ? document.body : getElement(config.container)\n\n if (typeof config.delay === 'number') {\n config.delay = {\n show: config.delay,\n hide: config.delay\n }\n }\n\n if (typeof config.title === 'number') {\n config.title = config.title.toString()\n }\n\n if (typeof config.content === 'number') {\n config.content = config.content.toString()\n }\n\n return config\n }\n\n _getDelegateConfig() {\n const config = {}\n\n for (const [key, value] of Object.entries(this._config)) {\n if (this.constructor.Default[key] !== value) {\n config[key] = value\n }\n }\n\n config.selector = false\n config.trigger = 'manual'\n\n // In the future can be replaced with:\n // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]])\n // `Object.fromEntries(keysWithDifferentValues)`\n return config\n }\n\n _disposePopper() {\n if (this._popper) {\n this._popper.destroy()\n this._popper = null\n }\n\n if (this.tip) {\n this.tip.remove()\n this.tip = null\n }\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Tooltip.getOrCreateInstance(this, config)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n })\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Tooltip)\n\nexport default Tooltip\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap popover.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport Tooltip from './tooltip.js'\nimport { defineJQueryPlugin } from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'popover'\n\nconst SELECTOR_TITLE = '.popover-header'\nconst SELECTOR_CONTENT = '.popover-body'\n\nconst Default = {\n ...Tooltip.Default,\n content: '',\n offset: [0, 8],\n placement: 'right',\n template: '
' +\n '
' +\n '

' +\n '
' +\n '
',\n trigger: 'click'\n}\n\nconst DefaultType = {\n ...Tooltip.DefaultType,\n content: '(null|string|element|function)'\n}\n\n/**\n * Class definition\n */\n\nclass Popover extends Tooltip {\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Overrides\n _isWithContent() {\n return this._getTitle() || this._getContent()\n }\n\n // Private\n _getContentForTemplate() {\n return {\n [SELECTOR_TITLE]: this._getTitle(),\n [SELECTOR_CONTENT]: this._getContent()\n }\n }\n\n _getContent() {\n return this._resolvePossibleFunction(this._config.content)\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Popover.getOrCreateInstance(this, config)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n })\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Popover)\n\nexport default Popover\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap scrollspy.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport {\n defineJQueryPlugin, getElement, isDisabled, isVisible\n} from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'scrollspy'\nconst DATA_KEY = 'bs.scrollspy'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst EVENT_ACTIVATE = `activate${EVENT_KEY}`\nconst EVENT_CLICK = `click${EVENT_KEY}`\nconst EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item'\nconst CLASS_NAME_ACTIVE = 'active'\n\nconst SELECTOR_DATA_SPY = '[data-bs-spy=\"scroll\"]'\nconst SELECTOR_TARGET_LINKS = '[href]'\nconst SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'\nconst SELECTOR_NAV_LINKS = '.nav-link'\nconst SELECTOR_NAV_ITEMS = '.nav-item'\nconst SELECTOR_LIST_ITEMS = '.list-group-item'\nconst SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`\nconst SELECTOR_DROPDOWN = '.dropdown'\nconst SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'\n\nconst Default = {\n offset: null, // TODO: v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: '0px 0px -25%',\n smoothScroll: false,\n target: null,\n threshold: [0.1, 0.5, 1]\n}\n\nconst DefaultType = {\n offset: '(number|null)', // TODO v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: 'string',\n smoothScroll: 'boolean',\n target: 'element',\n threshold: 'array'\n}\n\n/**\n * Class definition\n */\n\nclass ScrollSpy extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n // this._element is the observablesContainer and config.target the menu links wrapper\n this._targetLinks = new Map()\n this._observableSections = new Map()\n this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element\n this._activeTarget = null\n this._observer = null\n this._previousScrollData = {\n visibleEntryTop: 0,\n parentScrollTop: 0\n }\n this.refresh() // initialize\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n refresh() {\n this._initializeTargetsAndObservables()\n this._maybeEnableSmoothScroll()\n\n if (this._observer) {\n this._observer.disconnect()\n } else {\n this._observer = this._getNewObserver()\n }\n\n for (const section of this._observableSections.values()) {\n this._observer.observe(section)\n }\n }\n\n dispose() {\n this._observer.disconnect()\n super.dispose()\n }\n\n // Private\n _configAfterMerge(config) {\n // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case\n config.target = getElement(config.target) || document.body\n\n // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only\n config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin\n\n if (typeof config.threshold === 'string') {\n config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value))\n }\n\n return config\n }\n\n _maybeEnableSmoothScroll() {\n if (!this._config.smoothScroll) {\n return\n }\n\n // unregister any previous listeners\n EventHandler.off(this._config.target, EVENT_CLICK)\n\n EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => {\n const observableSection = this._observableSections.get(event.target.hash)\n if (observableSection) {\n event.preventDefault()\n const root = this._rootElement || window\n const height = observableSection.offsetTop - this._element.offsetTop\n if (root.scrollTo) {\n root.scrollTo({ top: height, behavior: 'smooth' })\n return\n }\n\n // Chrome 60 doesn't support `scrollTo`\n root.scrollTop = height\n }\n })\n }\n\n _getNewObserver() {\n const options = {\n root: this._rootElement,\n threshold: this._config.threshold,\n rootMargin: this._config.rootMargin\n }\n\n return new IntersectionObserver(entries => this._observerCallback(entries), options)\n }\n\n // The logic of selection\n _observerCallback(entries) {\n const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`)\n const activate = entry => {\n this._previousScrollData.visibleEntryTop = entry.target.offsetTop\n this._process(targetElement(entry))\n }\n\n const parentScrollTop = (this._rootElement || document.documentElement).scrollTop\n const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop\n this._previousScrollData.parentScrollTop = parentScrollTop\n\n for (const entry of entries) {\n if (!entry.isIntersecting) {\n this._activeTarget = null\n this._clearActiveClass(targetElement(entry))\n\n continue\n }\n\n const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop\n // if we are scrolling down, pick the bigger offsetTop\n if (userScrollsDown && entryIsLowerThanPrevious) {\n activate(entry)\n // if parent isn't scrolled, let's keep the first visible item, breaking the iteration\n if (!parentScrollTop) {\n return\n }\n\n continue\n }\n\n // if we are scrolling up, pick the smallest offsetTop\n if (!userScrollsDown && !entryIsLowerThanPrevious) {\n activate(entry)\n }\n }\n }\n\n _initializeTargetsAndObservables() {\n this._targetLinks = new Map()\n this._observableSections = new Map()\n\n const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target)\n\n for (const anchor of targetLinks) {\n // ensure that the anchor has an id and is not disabled\n if (!anchor.hash || isDisabled(anchor)) {\n continue\n }\n\n const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element)\n\n // ensure that the observableSection exists & is visible\n if (isVisible(observableSection)) {\n this._targetLinks.set(decodeURI(anchor.hash), anchor)\n this._observableSections.set(anchor.hash, observableSection)\n }\n }\n }\n\n _process(target) {\n if (this._activeTarget === target) {\n return\n }\n\n this._clearActiveClass(this._config.target)\n this._activeTarget = target\n target.classList.add(CLASS_NAME_ACTIVE)\n this._activateParents(target)\n\n EventHandler.trigger(this._element, EVENT_ACTIVATE, { relatedTarget: target })\n }\n\n _activateParents(target) {\n // Activate dropdown parents\n if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {\n SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE, target.closest(SELECTOR_DROPDOWN))\n .classList.add(CLASS_NAME_ACTIVE)\n return\n }\n\n for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) {\n // Set triggered links parents as active\n // With both