forked from OpenWF/SpaceNinjaServer
Initial commit
This commit is contained in:
commit
0b6f9bb026
19
.coderabbit.yaml
Normal file
19
.coderabbit.yaml
Normal file
@ -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
|
||||||
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
**/.dockerignore
|
||||||
|
**/.git
|
||||||
|
Dockerfile*
|
||||||
|
.*
|
||||||
|
docker-data/
|
||||||
|
node_modules/
|
||||||
|
static/data/
|
||||||
|
logs/
|
||||||
47
.eslintrc
Normal file
47
.eslintrc
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
static/webui/libs/ linguist-vendored
|
||||||
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
29
.github/workflows/build.yml
vendored
Normal file
29
.github/workflows/build.yml
vendored
Normal file
@ -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
|
||||||
44
.github/workflows/docker.yml
vendored
Normal file
44
.github/workflows/docker.yml
vendored
Normal file
@ -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
|
||||||
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
@ -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
|
||||||
5
.prettierignore
Normal file
5
.prettierignore
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
src/routes/api.ts
|
||||||
|
static/webui/libs/
|
||||||
|
*.html
|
||||||
|
*.md
|
||||||
|
config-vanilla.json
|
||||||
27
.prettierrc
Normal file
27
.prettierrc
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["dbaeumer.vscode-eslint"]
|
||||||
|
}
|
||||||
18
.vscode/launch.json
vendored
Normal file
18
.vscode/launch.json
vendored
Normal file
@ -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
|
||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"typescript.preferences.preferTypeOnlyAutoImports": true
|
||||||
|
}
|
||||||
17
AGENTS.md
Normal file
17
AGENTS.md
Normal file
@ -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.
|
||||||
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@ -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"]
|
||||||
674
LICENSE
Normal file
674
LICENSE
Normal file
@ -0,0 +1,674 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
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.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
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
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
38
README.md
Normal file
38
README.md
Normal file
@ -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 <https://onlyg.it/OpenWF/SpaceNinjaServer>.
|
||||||
|
|
||||||
|
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`.
|
||||||
28
UPDATE AND START SERVER.bat
Normal file
28
UPDATE AND START SERVER.bat
Normal file
@ -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
|
||||||
24
UPDATE AND START SERVER.sh
Normal file
24
UPDATE AND START SERVER.sh
Normal file
@ -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
|
||||||
77
config-vanilla.json
Normal file
77
config-vanilla.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
27
docker-compose.yml
Normal file
27
docker-compose.yml
Normal file
@ -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
|
||||||
8
docker-entrypoint.sh
Normal file
8
docker-entrypoint.sh
Normal file
@ -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
|
||||||
5776
package-lock.json
generated
Normal file
5776
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
65
package.json
Normal file
65
package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
58
scripts/dev.cjs
Normal file
58
scripts/dev.cjs
Normal file
@ -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) {}
|
||||||
|
});
|
||||||
47
scripts/update-translations.cjs
Normal file
47
scripts/update-translations.cjs
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
51
src/app.ts
Normal file
51
src/app.ts
Normal file
@ -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 };
|
||||||
19
src/constants/timeConstants.ts
Normal file
19
src/constants/timeConstants.ts
Normal file
@ -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
|
||||||
|
};
|
||||||
11
src/controllers/api/abandonLibraryDailyTaskController.ts
Normal file
11
src/controllers/api/abandonLibraryDailyTaskController.ts
Normal file
@ -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();
|
||||||
|
};
|
||||||
46
src/controllers/api/abortDojoComponentController.ts
Normal file
46
src/controllers/api/abortDojoComponentController.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
@ -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));
|
||||||
|
};
|
||||||
46
src/controllers/api/activateRandomModController.ts
Normal file
46
src/controllers/api/activateRandomModController.ts
Normal file
@ -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<IActiveRandomModRequest>(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;
|
||||||
|
}
|
||||||
60
src/controllers/api/addFriendController.ts
Normal file
60
src/controllers/api/addFriendController.ts
Normal file
@ -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<IAddFriendRequest>(String(req.body));
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
|
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<void>
|
||||||
|
);
|
||||||
|
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<void>
|
||||||
|
);
|
||||||
|
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
|
||||||
|
}
|
||||||
25
src/controllers/api/addFriendImageController.ts
Normal file
25
src/controllers/api/addFriendImageController.ts
Normal file
@ -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<IUpdateGlyphRequest>(String(req.body));
|
||||||
|
|
||||||
|
await Inventory.updateOne(
|
||||||
|
{
|
||||||
|
accountOwnerId: accountId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ActiveAvatarImageType: json.AvatarImageType
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({});
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IUpdateGlyphRequest {
|
||||||
|
AvatarImageType: string;
|
||||||
|
AvatarImage: string;
|
||||||
|
}
|
||||||
30
src/controllers/api/addIgnoredUserController.ts
Normal file
30
src/controllers/api/addIgnoredUserController.ts
Normal file
@ -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<IAddIgnoredUserRequest>(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;
|
||||||
|
}
|
||||||
52
src/controllers/api/addPendingFriendController.ts
Normal file
52
src/controllers/api/addPendingFriendController.ts
Normal file
@ -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<IAddPendingFriendRequest>(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;
|
||||||
|
}
|
||||||
117
src/controllers/api/addToAllianceController.ts
Normal file
117
src/controllers/api/addToAllianceController.ts
Normal file
@ -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<IAddToAllianceRequest>(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;
|
||||||
|
}
|
||||||
112
src/controllers/api/addToGuildController.ts
Normal file
112
src/controllers/api/addToGuildController.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
27
src/controllers/api/adoptPetController.ts
Normal file
27
src/controllers/api/adoptPetController.ts
Normal file
@ -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<IAdoptPetRequest>(String(req.body));
|
||||||
|
const details = inventory.KubrowPets.id(data.petId)!.Details!;
|
||||||
|
details.Name = data.name;
|
||||||
|
await inventory.save();
|
||||||
|
res.json({
|
||||||
|
petId: data.petId,
|
||||||
|
newName: data.name
|
||||||
|
} satisfies IAdoptPetResponse);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IAdoptPetRequest {
|
||||||
|
petId: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAdoptPetResponse {
|
||||||
|
petId: string;
|
||||||
|
newName: string;
|
||||||
|
}
|
||||||
22
src/controllers/api/apartmentController.ts
Normal file
22
src/controllers/api/apartmentController.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
76
src/controllers/api/arcaneCommonController.ts
Normal file
76
src/controllers/api/arcaneCommonController.ts
Normal file
@ -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<IArcaneCommonRequest>(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;
|
||||||
|
}
|
||||||
51
src/controllers/api/archonFusionController.ts
Normal file
51
src/controllers/api/archonFusionController.ts
Normal file
@ -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"; // ???
|
||||||
|
}
|
||||||
169
src/controllers/api/artifactTransmutationController.ts
Normal file
169
src/controllers/api/artifactTransmutationController.ts
Normal file
@ -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<TRarity, number> = {
|
||||||
|
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<TRarity, number> = {
|
||||||
|
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"
|
||||||
|
]
|
||||||
|
];
|
||||||
68
src/controllers/api/artifactsController.ts
Normal file
68
src/controllers/api/artifactsController.ts
Normal file
@ -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<IArtifactsRequest>(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<IInventoryClient>().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;
|
||||||
|
}
|
||||||
20
src/controllers/api/cancelGuildAdvertisementController.ts
Normal file
20
src/controllers/api/cancelGuildAdvertisementController.ts
Normal file
@ -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();
|
||||||
|
};
|
||||||
99
src/controllers/api/changeDojoRootController.ts
Normal file
99
src/controllers/api/changeDojoRootController.ts
Normal file
@ -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<string, INode> = {};
|
||||||
|
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!);
|
||||||
|
};
|
||||||
38
src/controllers/api/changeGuildRankController.ts
Normal file
38
src/controllers/api/changeGuildRankController.ts
Normal file
@ -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
|
||||||
|
});
|
||||||
|
};
|
||||||
12
src/controllers/api/checkDailyMissionBonusController.ts
Normal file
12
src/controllers/api/checkDailyMissionBonusController.ts
Normal file
@ -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");
|
||||||
|
}
|
||||||
|
};
|
||||||
307
src/controllers/api/claimCompletedRecipeController.ts
Normal file
307
src/controllers/api/claimCompletedRecipeController.ts
Normal file
@ -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<IClaimCompletedRecipeRequest>(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<void> => {
|
||||||
|
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<void> => {
|
||||||
|
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<IEquipmentClient>());
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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<IClaimJunctionChallengeRewardRequest>(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;
|
||||||
|
}
|
||||||
31
src/controllers/api/claimLibraryDailyTaskRewardController.ts
Normal file
31
src/controllers/api/claimLibraryDailyTaskRewardController.ts
Normal file
@ -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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
25
src/controllers/api/clearDialogueHistoryController.ts
Normal file
25
src/controllers/api/clearDialogueHistoryController.ts
Normal file
@ -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[];
|
||||||
|
}
|
||||||
6
src/controllers/api/clearNewEpisodeRewardController.ts
Normal file
6
src/controllers/api/clearNewEpisodeRewardController.ts
Normal file
@ -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();
|
||||||
|
};
|
||||||
37
src/controllers/api/completeCalendarEventController.ts
Normal file
37
src/controllers/api/completeCalendarEventController.ts
Normal file
@ -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
|
||||||
|
});
|
||||||
|
};
|
||||||
45
src/controllers/api/completeRandomModChallengeController.ts
Normal file
45
src/controllers/api/completeRandomModChallengeController.ts
Normal file
@ -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<ICompleteRandomModChallengeRequest>(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;
|
||||||
|
}
|
||||||
37
src/controllers/api/confirmAllianceInvitationController.ts
Normal file
37
src/controllers/api/confirmAllianceInvitationController.ts
Normal file
@ -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
|
||||||
|
});
|
||||||
|
};
|
||||||
121
src/controllers/api/confirmGuildInvitationController.ts
Normal file
121
src/controllers/api/confirmGuildInvitationController.ts
Normal file
@ -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
|
||||||
|
});
|
||||||
|
};
|
||||||
67
src/controllers/api/contributeGuildClassController.ts
Normal file
67
src/controllers/api/contributeGuildClassController.ts
Normal file
@ -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<IContributeGuildClassRequest>(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;
|
||||||
|
};
|
||||||
170
src/controllers/api/contributeToDojoComponentController.ts
Normal file
170
src/controllers/api/contributeToDojoComponentController.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
109
src/controllers/api/contributeToVaultController.ts
Normal file
109
src/controllers/api/contributeToVaultController.ts
Normal file
@ -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<unknown>[] = [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;
|
||||||
|
}
|
||||||
50
src/controllers/api/createAllianceController.ts
Normal file
50
src/controllers/api/createAllianceController.ts
Normal file
@ -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<ICreateAllianceRequest>(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;
|
||||||
|
}
|
||||||
44
src/controllers/api/createGuildController.ts
Normal file
44
src/controllers/api/createGuildController.ts
Normal file
@ -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<ICreateGuildRequest>(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;
|
||||||
|
}
|
||||||
32
src/controllers/api/creditsController.ts
Normal file
32
src/controllers/api/creditsController.ts
Normal file
@ -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);
|
||||||
|
};
|
||||||
54
src/controllers/api/crewMembersController.ts
Normal file
54
src/controllers/api/crewMembersController.ts
Normal file
@ -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<ICrewMembersRequest>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
107
src/controllers/api/crewShipFusionController.ts
Normal file
107
src/controllers/api/crewShipFusionController.ts
Normal file
@ -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<ICrewShipFusionRequest>(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];
|
||||||
87
src/controllers/api/crewShipIdentifySalvageController.ts
Normal file
87
src/controllers/api/crewShipIdentifySalvageController.ts
Normal file
@ -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<ICrewShipIdentifySalvageRequest>(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<IEquipmentDatabase> | 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;
|
||||||
|
}
|
||||||
@ -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<ICustomObstacleCourseLeaderboardRequest>(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
|
||||||
|
}
|
||||||
22
src/controllers/api/customizeGuildRanksController.ts
Normal file
22
src/controllers/api/customizeGuildRanksController.ts
Normal file
@ -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[];
|
||||||
|
}
|
||||||
17
src/controllers/api/declineAllianceInviteController.ts
Normal file
17
src/controllers/api/declineAllianceInviteController.ts
Normal file
@ -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();
|
||||||
|
};
|
||||||
14
src/controllers/api/declineGuildInviteController.ts
Normal file
14
src/controllers/api/declineGuildInviteController.ts
Normal file
@ -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();
|
||||||
|
};
|
||||||
9
src/controllers/api/deleteSessionController.ts
Normal file
9
src/controllers/api/deleteSessionController.ts
Normal file
@ -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 };
|
||||||
51
src/controllers/api/destroyDojoDecoController.ts
Normal file
51
src/controllers/api/destroyDojoDecoController.ts
Normal file
@ -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";
|
||||||
|
}
|
||||||
67
src/controllers/api/divvyAllianceVaultController.ts
Normal file
67
src/controllers/api/divvyAllianceVaultController.ts
Normal file
@ -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<string, number> = {};
|
||||||
|
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();
|
||||||
|
};
|
||||||
83
src/controllers/api/dojoComponentRushController.ts
Normal file
83
src/controllers/api/dojoComponentRushController.ts
Normal file
@ -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;
|
||||||
|
};
|
||||||
11
src/controllers/api/dojoController.ts
Normal file
11
src/controllers/api/dojoController.ts
Normal file
@ -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();
|
||||||
|
};
|
||||||
145
src/controllers/api/dronesController.ts
Normal file
145
src/controllers/api/dronesController.ts
Normal file
@ -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<IDroneClient>()];
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
534
src/controllers/api/endlessXpController.ts
Normal file
534
src/controllers/api/endlessXpController.ts
Normal file
@ -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<IEndlessXpRequest>(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<IInventoryClient>().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<string, string[]> = {
|
||||||
|
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<string, string> = {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
52
src/controllers/api/entratiLabConquestModeController.ts
Normal file
52
src/controllers/api/entratiLabConquestModeController.ts
Normal file
@ -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<IEntratiLabConquestModeRequest>(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[];
|
||||||
|
}
|
||||||
59
src/controllers/api/evolveWeaponController.ts
Normal file
59
src/controllers/api/evolveWeaponController.ts
Normal file
@ -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<IEvolveWeaponRequest>(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"
|
||||||
|
}
|
||||||
28
src/controllers/api/findSessionsController.ts
Normal file
28
src/controllers/api/findSessionsController.ts
Normal file
@ -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({});
|
||||||
|
}
|
||||||
|
};
|
||||||
46
src/controllers/api/fishmongerController.ts
Normal file
46
src/controllers/api/fishmongerController.ts
Normal file
@ -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<IFishmongerRequest>(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[];
|
||||||
|
}
|
||||||
242
src/controllers/api/focusController.ts
Normal file
242
src/controllers/api/focusController.ts
Normal file
@ -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
|
||||||
|
};
|
||||||
44
src/controllers/api/fusionTreasuresController.ts
Normal file
44
src/controllers/api/fusionTreasuresController.ts
Normal file
@ -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
|
||||||
|
});
|
||||||
|
};
|
||||||
83
src/controllers/api/gardeningController.ts
Normal file
83
src/controllers/api/gardeningController.ts
Normal file
@ -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<IGardeningRequest>(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<string, IMissionReward[][]> = {};
|
||||||
|
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<IPersonalRoomsClient>().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<string, IMissionReward[][]>;
|
||||||
|
}
|
||||||
16
src/controllers/api/genericUpdateController.ts
Normal file
16
src/controllers/api/genericUpdateController.ts
Normal file
@ -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<IGenericUpdate>(String(request.body));
|
||||||
|
response.json(await updateGeneric(update, accountId));
|
||||||
|
};
|
||||||
|
|
||||||
|
export { genericUpdateController };
|
||||||
26
src/controllers/api/getAllianceController.ts
Normal file
26
src/controllers/api/getAllianceController.ts
Normal file
@ -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;
|
||||||
|
}*/
|
||||||
10
src/controllers/api/getDailyDealStockLevelsController.ts
Normal file
10
src/controllers/api/getDailyDealStockLevelsController.ts
Normal file
@ -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
|
||||||
|
});
|
||||||
|
};
|
||||||
54
src/controllers/api/getFriendsController.ts
Normal file
54
src/controllers/api/getFriendsController.ts
Normal file
@ -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<void>[] = [];
|
||||||
|
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[]>;
|
||||||
19
src/controllers/api/getGuildContributionsController.ts
Normal file
19
src/controllers/api/getGuildContributionsController.ts
Normal file
@ -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<IGuildMemberClient>);
|
||||||
|
};
|
||||||
32
src/controllers/api/getGuildController.ts
Normal file
32
src/controllers/api/getGuildController.ts
Normal file
@ -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();
|
||||||
|
};
|
||||||
35
src/controllers/api/getGuildDojoController.ts
Normal file
35
src/controllers/api/getGuildDojoController.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
26
src/controllers/api/getGuildEventScoreController.ts
Normal file
26
src/controllers/api/getGuildEventScoreController.ts
Normal file
@ -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({});
|
||||||
|
};
|
||||||
60
src/controllers/api/getGuildLogController.ts
Normal file
60
src/controllers/api/getGuildLogController.ts
Normal file
@ -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<string, IGuildLogEntryClient[]> = {
|
||||||
|
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;
|
||||||
|
}
|
||||||
20
src/controllers/api/getIgnoredUsersController.ts
Normal file
20
src/controllers/api/getIgnoredUsersController.ts
Normal file
@ -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 });
|
||||||
|
};
|
||||||
19
src/controllers/api/getNewRewardSeedController.ts
Normal file
19
src/controllers/api/getNewRewardSeedController.ts
Normal file
@ -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 });
|
||||||
|
};
|
||||||
62
src/controllers/api/getPastWeeklyChallengesController.ts
Normal file
62
src/controllers/api/getPastWeeklyChallengesController.ts
Normal file
@ -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<string>();
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
39
src/controllers/api/getShipController.ts
Normal file
39
src/controllers/api/getShipController.ts
Normal file
@ -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<IPersonalRoomsClient>();
|
||||||
|
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);
|
||||||
|
};
|
||||||
29
src/controllers/api/getVendorInfoController.ts
Normal file
29
src/controllers/api/getVendorInfoController.ts
Normal file
@ -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);
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user