forked from OpenWF/SpaceNinjaServer
		
	Compare commits
	
		
			247 Commits
		
	
	
		
			3a904753f2
			...
			8b0ba0b84a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8b0ba0b84a | |||
| cdead6fdf8 | |||
| da6067ec43 | |||
| a98e18d511 | |||
| 6394adb0f0 | |||
| bc5dc02fc9 | |||
| de5fd5fce0 | |||
| a6d4fab595 | |||
| f549b042d6 | |||
| 0c34c87d75 | |||
| 3baf6ad015 | |||
| 196182f9a8 | |||
| 379f57be2c | |||
| 0d8f5ee66c | |||
| 79492efbb4 | |||
| decbbdc81b | |||
| 41d976d362 | |||
| f94ecbfbfc | |||
| f4f1e11b31 | |||
| e38d52fb1b | |||
| 419096f603 | |||
| 76a53bb1f6 | |||
| 435aafeaae | |||
| 8a1603a661 | |||
| 66e34b7be9 | |||
| 9940024a01 | |||
| ed217bae33 | |||
| 16e850e7ee | |||
| 850a073594 | |||
| 66dae6d3f8 | |||
| 9a50c05205 | |||
| 379e83a764 | |||
| 44da0eb50a | |||
| deb652ab37 | |||
| 0ea67ea89a | |||
| 51b82df5fd | |||
| 3d1b009bdb | |||
| 46aef2c00e | |||
| 7d607b7348 | |||
| 4cb1ea94e5 | |||
| 729061951f | |||
| 38502f10bf | |||
| a738dbfa9a | |||
| 95562a97ad | |||
| c13615c4df | |||
| eb6b1c1f57 | |||
| 64fbdf6064 | |||
| 47551e93b3 | |||
| 7a53363b1b | |||
| ea0ca8c88b | |||
| a10c3b061a | |||
| 3165d9f459 | |||
| 28d7ca8ca0 | |||
| 3f0a2bec48 | |||
| d28437b658 | |||
| a6d2c8b18a | |||
| 0c884576bd | |||
| 380f0662a4 | |||
| bd83738168 | |||
| fa68a1357d | |||
| 43f3917b09 | |||
| 8ebb749732 | |||
| 827ea47468 | |||
| c64d466ce1 | |||
| c8ae3d688f | |||
| 4a971841a1 | |||
| 9472f855b6 | |||
| 7736a2bf65 | |||
| bef3aeed72 | |||
| aacd089123 | |||
| d281e929ae | |||
| 729ea0abff | |||
| a75e0c59af | |||
| b429eed46c | |||
| 92d2616dda | |||
| 37ccd33d5c | |||
| 20326fdaa0 | |||
| 6a97a0c7c8 | |||
| 0928b842ad | |||
| e0200b2111 | |||
| 18a13911ba | |||
| 2c53d17489 | |||
| 2eb28c4e89 | |||
| 2187d9cd7e | |||
| e5e6f7963b | |||
| 900c6e9a26 | |||
| f0ee1e8aad | |||
| 5c6b4b5779 | |||
| 9b330ffd3e | |||
| 97d27e8110 | |||
| 525e3067c9 | |||
| 0c1fa05e9c | |||
| 946f3129b8 | |||
| 355de3fa04 | |||
| 61e168e444 | |||
| 70fa48ab07 | |||
| dde95c2b61 | |||
| 2ca79ef898 | |||
| 63e3c96671 | |||
| f0351489be | |||
| 5149d0e382 | |||
| ec8982a921 | |||
| cc338c2173 | |||
| 85b5bb438e | |||
| 9f727789ca | |||
| b308b91f44 | |||
| fc3ef3a126 | |||
| 3f47f89b56 | |||
| e784b2dfb8 | |||
| c0947b8822 | |||
| a0b61bec12 | |||
| d3620c00e2 | |||
| 0ffcee5faf | |||
| c2ed8b40f0 | |||
| feb1dd4715 | |||
| 540961ff9e | |||
| 5692a6201e | |||
| 541b8d32a8 | |||
| 74f9d1567f | |||
| 02a4d2b30a | |||
| a8f174bce1 | |||
| db1dd21924 | |||
| 005350bde0 | |||
| 5cd18db7a7 | |||
| bb315eaafe | |||
| 327b834b07 | |||
| 39be095818 | |||
| ef4973e694 | |||
| 7f69667171 | |||
| dcdeb0cd34 | |||
| 8ce86ad4fd | |||
| a2f1469779 | |||
| dd32e082f3 | |||
| 74c7d86090 | |||
| 7fd4d50e07 | |||
| 7f805a1dcc | |||
| 919f12b8f9 | |||
| 94993a16aa | |||
| 002b0cb93f | |||
| 4362a842ff | |||
| 5702ab5f3b | |||
| fac52bfda1 | |||
| 2ff535e7ab | |||
| 9698baa979 | |||
| ceb7deec06 | |||
| fe0b745066 | |||
| 8f41d3c13f | |||
| f906cdb5e8 | |||
| ea6facf3fc | |||
| b93a4a6dae | |||
| 64da8c2e50 | |||
| b2497ded19 | |||
| 5f6b2330af | |||
| 2bdb722986 | |||
| 3c79f910a2 | |||
| 65306e0478 | |||
| 49edebc1eb | |||
| 62263efde3 | |||
| f66c958a3c | |||
| 2ef59cd570 | |||
| 6bb74b026a | |||
| 743a905de4 | |||
| 5c22949c6b | |||
| 651640c4d7 | |||
| 1d1abf5550 | |||
| 23267aa641 | |||
| d94b4fd946 | |||
| d5ff349746 | |||
| 2746e243c9 | |||
| b3374eb66e | |||
| c18abab9c4 | |||
| 61062e433f | |||
| 92e8ffd709 | |||
| 0b18932dd8 | |||
| abeb17ce44 | |||
| 710470ca2d | |||
| 5cc991baca | |||
| 9eadc7fa21 | |||
| ed10a89c1d | |||
| 0c2f72f9b1 | |||
| d918b0c982 | |||
| 05c0c9909c | |||
| 92cf85084f | |||
| cfa9ec775e | |||
| d4d887a5a4 | |||
| 1b7b5a28bc | |||
| 6dc54ed893 | |||
| 2173bdb8b8 | |||
| 74d9428a66 | |||
| dd7805cfb2 | |||
| c55aa8a0e1 | |||
| 24ed580a97 | |||
| 158310bda2 | |||
| 2b451a19e6 | |||
| bf67a4391d | |||
| ea9333279b | |||
| 9e1a5d50af | |||
| 2091dabfc3 | |||
| 3a26d788a2 | |||
| 367dd3f22d | |||
| 404c747642 | |||
| 1a4ad8b7a5 | |||
| 4a3a3de300 | |||
| 7d5ea680e4 | |||
| e2879a7808 | |||
| d033c2bc12 | |||
| fb58aeb07f | |||
| 3d69828610 | |||
| a0fa41cd58 | |||
| 9162522962 | |||
| 42e08faaaf | |||
| 9e0dd3e0a5 | |||
| 054abee62c | |||
| 04d39ed973 | |||
| b0f0b61d49 | |||
| 23f8901505 | |||
| d3d966a503 | |||
| 48598c145f | |||
| 01f04c287a | |||
| 9e99d0370c | |||
| 516f822e43 | |||
| fccdbf4a8e | |||
| cfc1524619 | |||
| 3beb1ecc42 | |||
| 779bc34082 | |||
| b6167165fe | |||
| b07e89ed72 | |||
| f34e1615e2 | |||
| c82cad7b02 | |||
| 725efcc72e | |||
| fa34b99976 | |||
| d3819c25c5 | |||
| 4cb883dabf | |||
| c376ff25f3 | |||
| f7ada5a7e5 | |||
| 1bdc5126b3 | |||
| a7899d1c18 | |||
| 895b9381ca | |||
| 9de0aee6f0 | |||
| 69f544c8d1 | |||
| 8cdcb209ae | |||
| ab0d472c75 | |||
| e266f9e36c | |||
| 6a1e508109 | |||
| a167216730 | |||
| 30ae95bec8 | |||
| dcc2b903ac | 
@ -15,17 +15,16 @@
 | 
			
		||||
        "@typescript-eslint/restrict-template-expressions": "warn",
 | 
			
		||||
        "@typescript-eslint/restrict-plus-operands": "warn",
 | 
			
		||||
        "@typescript-eslint/no-unsafe-member-access": "warn",
 | 
			
		||||
        "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
 | 
			
		||||
        "@typescript-eslint/no-misused-promises": "warn",
 | 
			
		||||
        "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "caughtErrors": "none" }],
 | 
			
		||||
        "@typescript-eslint/no-unsafe-argument": "error",
 | 
			
		||||
        "@typescript-eslint/no-unsafe-call": "warn",
 | 
			
		||||
        "@typescript-eslint/no-unsafe-assignment": "warn",
 | 
			
		||||
        "@typescript-eslint/no-explicit-any": "warn",
 | 
			
		||||
        "@typescript-eslint/no-loss-of-precision": "warn",
 | 
			
		||||
        "no-loss-of-precision": "warn",
 | 
			
		||||
        "@typescript-eslint/no-unnecessary-condition": "warn",
 | 
			
		||||
        "@typescript-eslint/no-base-to-string": "off",
 | 
			
		||||
        "no-case-declarations": "error",
 | 
			
		||||
        "prettier/prettier": "error",
 | 
			
		||||
        "@typescript-eslint/semi": "error",
 | 
			
		||||
        "no-mixed-spaces-and-tabs": "error",
 | 
			
		||||
        "require-await": "off",
 | 
			
		||||
        "@typescript-eslint/require-await": "error"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							@ -1,4 +1,4 @@
 | 
			
		||||
# Auto detect text files and perform LF normalization
 | 
			
		||||
* text=auto
 | 
			
		||||
* text=auto eol=lf
 | 
			
		||||
 | 
			
		||||
static/webui/libs/ linguist-vendored
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										19
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							@ -5,17 +5,22 @@ on:
 | 
			
		||||
jobs:
 | 
			
		||||
    build:
 | 
			
		||||
        runs-on: ubuntu-latest
 | 
			
		||||
        strategy:
 | 
			
		||||
            matrix:
 | 
			
		||||
                version: [18, 20, 22]
 | 
			
		||||
        steps:
 | 
			
		||||
            - name: Checkout
 | 
			
		||||
              uses: actions/checkout@v4.1.2
 | 
			
		||||
            - name: Setup Node.js environment
 | 
			
		||||
              uses: actions/setup-node@v4.0.2
 | 
			
		||||
              with:
 | 
			
		||||
                  node-version: ${{ matrix.version }}
 | 
			
		||||
            - run: npm ci
 | 
			
		||||
            - run: cp config.json.example config.json
 | 
			
		||||
            - run: npm run build
 | 
			
		||||
            - run: npm run lint
 | 
			
		||||
            - run: npm run verify
 | 
			
		||||
            - run: npm run lint:ci
 | 
			
		||||
            - run: npm run prettier
 | 
			
		||||
            - run: npm run update-translations
 | 
			
		||||
            - name: Fail if there are uncommitted changes
 | 
			
		||||
              run: |
 | 
			
		||||
                  if [[ -n "$(git status --porcelain)" ]]; then
 | 
			
		||||
                    echo "Uncommitted changes detected:"
 | 
			
		||||
                    git status
 | 
			
		||||
                    git --no-pager diff
 | 
			
		||||
                    exit 1
 | 
			
		||||
                  fi
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
src/routes/api.ts
 | 
			
		||||
static/webui/libs/
 | 
			
		||||
*.html
 | 
			
		||||
*.md
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
{
 | 
			
		||||
  "recommendations": ["dbaeumer.vscode-eslint"]
 | 
			
		||||
}
 | 
			
		||||
@ -10,8 +10,6 @@ ENV APP_SKIP_TUTORIAL=true
 | 
			
		||||
ENV APP_SKIP_ALL_DIALOGUE=true
 | 
			
		||||
ENV APP_UNLOCK_ALL_SCANS=true
 | 
			
		||||
ENV APP_UNLOCK_ALL_MISSIONS=true
 | 
			
		||||
ENV APP_UNLOCK_ALL_QUESTS=true
 | 
			
		||||
ENV APP_COMPLETE_ALL_QUESTS=true
 | 
			
		||||
ENV APP_INFINITE_RESOURCES=true
 | 
			
		||||
ENV APP_UNLOCK_ALL_SHIP_FEATURES=true
 | 
			
		||||
ENV APP_UNLOCK_ALL_SHIP_DECORATIONS=true
 | 
			
		||||
 | 
			
		||||
@ -10,5 +10,8 @@ To get an idea of what functionality you can expect to be missing [have a look t
 | 
			
		||||
 | 
			
		||||
## config.json
 | 
			
		||||
 | 
			
		||||
SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [config.json.example](config.json.example), which has most cheats disabled.
 | 
			
		||||
 | 
			
		||||
- `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`.
 | 
			
		||||
- `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`.
 | 
			
		||||
- `worldState.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE.
 | 
			
		||||
 | 
			
		||||
@ -29,19 +29,26 @@
 | 
			
		||||
  "unlockExilusEverywhere": false,
 | 
			
		||||
  "unlockArcanesEverywhere": false,
 | 
			
		||||
  "noDailyStandingLimits": false,
 | 
			
		||||
  "noDailyFocusLimit": false,
 | 
			
		||||
  "noArgonCrystalDecay": false,
 | 
			
		||||
  "noMasteryRankUpCooldown": false,
 | 
			
		||||
  "noVendorPurchaseLimits": true,
 | 
			
		||||
  "noDeathMarks": false,
 | 
			
		||||
  "noKimCooldowns": false,
 | 
			
		||||
  "instantResourceExtractorDrones": false,
 | 
			
		||||
  "noResourceExtractorDronesDamage": false,
 | 
			
		||||
  "noDojoRoomBuildStage": false,
 | 
			
		||||
  "noDecoBuildStage": false,
 | 
			
		||||
  "fastDojoRoomDestruction": false,
 | 
			
		||||
  "noDojoResearchCosts": false,
 | 
			
		||||
  "noDojoResearchTime": false,
 | 
			
		||||
  "fastClanAscension": false,
 | 
			
		||||
  "spoofMasteryRank": -1,
 | 
			
		||||
  "events": {
 | 
			
		||||
  "worldState": {
 | 
			
		||||
    "creditBoost": false,
 | 
			
		||||
    "affinityBoost": false,
 | 
			
		||||
    "resourceBoost": false,
 | 
			
		||||
    "starDays": true
 | 
			
		||||
    "starDays": true,
 | 
			
		||||
    "lockTime": 0
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										638
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										638
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										23
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								package.json
									
									
									
									
									
								
							@ -4,10 +4,12 @@
 | 
			
		||||
  "description": "WF Emulator",
 | 
			
		||||
  "main": "index.ts",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "start": "node --import ./build/src/pathman.js build/src/index.js",
 | 
			
		||||
    "start": "node --enable-source-maps --import ./build/src/pathman.js build/src/index.js",
 | 
			
		||||
    "dev": "ts-node-dev --openssl-legacy-provider -r tsconfig-paths/register src/index.ts ",
 | 
			
		||||
    "build": "tsc && copyfiles static/webui/** build",
 | 
			
		||||
    "build": "tsc --incremental --sourceMap && ncp static/webui build/static/webui",
 | 
			
		||||
    "verify": "tsgo --noEmit",
 | 
			
		||||
    "lint": "eslint --ext .ts .",
 | 
			
		||||
    "lint:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .",
 | 
			
		||||
    "lint:fix": "eslint --fix --ext .ts .",
 | 
			
		||||
    "prettier": "prettier --write .",
 | 
			
		||||
    "update-translations": "cd scripts && node update-translations.js"
 | 
			
		||||
@ -16,24 +18,25 @@
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@types/express": "^5",
 | 
			
		||||
    "@types/morgan": "^1.9.9",
 | 
			
		||||
    "copyfiles": "^2.4.1",
 | 
			
		||||
    "crc-32": "^1.2.2",
 | 
			
		||||
    "express": "^5",
 | 
			
		||||
    "json-with-bigint": "^3.2.2",
 | 
			
		||||
    "mongoose": "^8.11.0",
 | 
			
		||||
    "morgan": "^1.10.0",
 | 
			
		||||
    "typescript": ">=5.5 <5.6.0",
 | 
			
		||||
    "warframe-public-export-plus": "^0.5.48",
 | 
			
		||||
    "ncp": "^2.0.0",
 | 
			
		||||
    "typescript": "^5.5",
 | 
			
		||||
    "warframe-public-export-plus": "^0.5.56",
 | 
			
		||||
    "warframe-riven-info": "^0.1.2",
 | 
			
		||||
    "winston": "^3.17.0",
 | 
			
		||||
    "winston-daily-rotate-file": "^5.0.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^7.18",
 | 
			
		||||
    "@typescript-eslint/parser": "^7.18",
 | 
			
		||||
    "eslint": "^8.56.0",
 | 
			
		||||
    "eslint-plugin-prettier": "^5.2.3",
 | 
			
		||||
    "prettier": "^3.4.2",
 | 
			
		||||
    "@rxliuli/tsgo": "^2025.3.31",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^8.28.0",
 | 
			
		||||
    "@typescript-eslint/parser": "^8.28.0",
 | 
			
		||||
    "eslint": "^8",
 | 
			
		||||
    "eslint-plugin-prettier": "^5.2.5",
 | 
			
		||||
    "prettier": "^3.5.3",
 | 
			
		||||
    "ts-node-dev": "^2.0.0",
 | 
			
		||||
    "tsconfig-paths": "^4.2.0"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@
 | 
			
		||||
const fs = require("fs");
 | 
			
		||||
 | 
			
		||||
function extractStrings(content) {
 | 
			
		||||
    const regex = /([a-zA-Z_]+): `([^`]*)`,/g;
 | 
			
		||||
    const regex = /([a-zA-Z0-9_]+): `([^`]*)`,/g;
 | 
			
		||||
    let matches;
 | 
			
		||||
    const strings = {};
 | 
			
		||||
    while ((matches = regex.exec(content)) !== null) {
 | 
			
		||||
@ -15,7 +15,7 @@ function extractStrings(content) {
 | 
			
		||||
 | 
			
		||||
const source = fs.readFileSync("../static/webui/translations/en.js", "utf8");
 | 
			
		||||
const sourceStrings = extractStrings(source);
 | 
			
		||||
const sourceLines = source.split("\n");
 | 
			
		||||
const sourceLines = source.substring(0, source.length - 1).split("\n");
 | 
			
		||||
 | 
			
		||||
fs.readdirSync("../static/webui/translations").forEach(file => {
 | 
			
		||||
    if (fs.lstatSync(`../static/webui/translations/${file}`).isFile() && file !== "en.js") {
 | 
			
		||||
@ -36,7 +36,7 @@ fs.readdirSync("../static/webui/translations").forEach(file => {
 | 
			
		||||
                        fs.writeSync(fileHandle, `    ${key}: \`[UNTRANSLATED] ${value}\`,\n`);
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            } else if (line.length) {
 | 
			
		||||
            } else {
 | 
			
		||||
                fs.writeSync(fileHandle, line + "\n");
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@ -16,9 +16,9 @@ import { webuiRouter } from "@/src/routes/webui";
 | 
			
		||||
const app = express();
 | 
			
		||||
 | 
			
		||||
app.use((req, _res, next) => {
 | 
			
		||||
    // 38.5.0 introduced "ezip" for encrypted body blobs.
 | 
			
		||||
    // 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") {
 | 
			
		||||
    if (req.headers["content-encoding"] == "ezip" || req.headers["content-encoding"] == "e") {
 | 
			
		||||
        req.headers["content-encoding"] = undefined;
 | 
			
		||||
    }
 | 
			
		||||
    next();
 | 
			
		||||
 | 
			
		||||
@ -32,7 +32,7 @@ export const abortDojoComponentController: RequestHandler = async (req, res) =>
 | 
			
		||||
    if (request.DecoId) {
 | 
			
		||||
        removeDojoDeco(guild, request.ComponentId, request.DecoId);
 | 
			
		||||
    } else {
 | 
			
		||||
        removeDojoRoom(guild, request.ComponentId);
 | 
			
		||||
        await removeDojoRoom(guild, request.ComponentId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await guild.save();
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										30
									
								
								src/controllers/api/addIgnoredUserController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/controllers/api/addIgnoredUserController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
import { toOid } from "@/src/helpers/inventoryHelpers";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { Account, Ignore } from "@/src/models/loginModel";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { IFriendInfo } from "@/src/types/guildTypes";
 | 
			
		||||
import { 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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										117
									
								
								src/controllers/api/addToAllianceController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								src/controllers/api/addToAllianceController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,117 @@
 | 
			
		||||
import { getJSONfromString, regexEscape } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { createMessage } from "@/src/services/inboxService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
 | 
			
		||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { ExportFlavour } from "warframe-public-export-plus";
 | 
			
		||||
 | 
			
		||||
export const addToAllianceController: RequestHandler = async (req, res) => {
 | 
			
		||||
    // Check requester is a warlord in their guild
 | 
			
		||||
    const account = await getAccountForRequest(req);
 | 
			
		||||
    const guildMember = (await GuildMember.findOne({ accountId: account._id, status: 0 }))!;
 | 
			
		||||
    if (guildMember.rank > 1) {
 | 
			
		||||
        res.status(400).json({ Error: 104 });
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check guild has invite permissions in the alliance
 | 
			
		||||
    const allianceMember = (await AllianceMember.findOne({
 | 
			
		||||
        allianceId: req.query.allianceId,
 | 
			
		||||
        guildId: guildMember.guildId
 | 
			
		||||
    }))!;
 | 
			
		||||
    if (!(allianceMember.Permissions & GuildPermission.Recruiter)) {
 | 
			
		||||
        res.status(400).json({ Error: 104 });
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Find clan to invite
 | 
			
		||||
    const payload = getJSONfromString<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, "Name"))!;
 | 
			
		||||
    await createMessage(invitedClanOwnerMember.accountId, [
 | 
			
		||||
        {
 | 
			
		||||
            sndr: getSuffixedName(account),
 | 
			
		||||
            msg: "/Lotus/Language/Menu/Mailbox_AllianceInvite_Body",
 | 
			
		||||
            arg: [
 | 
			
		||||
                {
 | 
			
		||||
                    Key: "THEIR_CLAN",
 | 
			
		||||
                    Tag: senderGuild.Name
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    Key: "CLAN",
 | 
			
		||||
                    Tag: guilds[0].Name
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    Key: "ALLIANCE",
 | 
			
		||||
                    Tag: alliance.Name
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            sub: "/Lotus/Language/Menu/Mailbox_AllianceInvite_Title",
 | 
			
		||||
            icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon,
 | 
			
		||||
            contextInfo: alliance._id.toString(),
 | 
			
		||||
            highPriority: true,
 | 
			
		||||
            acceptAction: "ALLIANCE_INVITE",
 | 
			
		||||
            declineAction: "ALLIANCE_INVITE",
 | 
			
		||||
            hasAccountAction: true
 | 
			
		||||
        }
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IAddToAllianceRequest {
 | 
			
		||||
    clanName: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IGuildChoice {
 | 
			
		||||
    OriginalPlatform: number;
 | 
			
		||||
    Name: string;
 | 
			
		||||
}
 | 
			
		||||
@ -3,15 +3,19 @@ import { Account } from "@/src/models/loginModel";
 | 
			
		||||
import { fillInInventoryDataForGuildMember, hasGuildPermission } from "@/src/services/guildService";
 | 
			
		||||
import { createMessage } from "@/src/services/inboxService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
 | 
			
		||||
import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService";
 | 
			
		||||
import { IOid } from "@/src/types/commonTypes";
 | 
			
		||||
import { GuildPermission, IGuildMemberClient } from "@/src/types/guildTypes";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { ExportFlavour } from "warframe-public-export-plus";
 | 
			
		||||
 | 
			
		||||
export const addToGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const payload = JSON.parse(String(req.body)) as IAddToGuildRequest;
 | 
			
		||||
 | 
			
		||||
    if ("UserName" in payload) {
 | 
			
		||||
        // Clan recruiter sending an invite
 | 
			
		||||
 | 
			
		||||
        const account = await Account.findOne({ DisplayName: payload.UserName });
 | 
			
		||||
        if (!account) {
 | 
			
		||||
            res.status(400).json("Username does not exist");
 | 
			
		||||
@ -25,30 +29,26 @@ export const addToGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    const guild = (await Guild.findById(payload.GuildId.$oid, "Name"))!;
 | 
			
		||||
        const guild = (await Guild.findById(payload.GuildId.$oid, "Name Ranks"))!;
 | 
			
		||||
        const senderAccount = await getAccountForRequest(req);
 | 
			
		||||
        if (!(await hasGuildPermission(guild, senderAccount._id.toString(), GuildPermission.Recruiter))) {
 | 
			
		||||
            res.status(400).json("Invalid permission");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        await GuildMember.exists({
 | 
			
		||||
            accountId: account._id,
 | 
			
		||||
            guildId: payload.GuildId.$oid
 | 
			
		||||
        })
 | 
			
		||||
    ) {
 | 
			
		||||
        res.status(400).json("User already invited to clan");
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        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.toString(), [
 | 
			
		||||
        await createMessage(account._id, [
 | 
			
		||||
            {
 | 
			
		||||
                sndr: getSuffixedName(senderAccount),
 | 
			
		||||
                msg: "/Lotus/Language/Menu/Mailbox_ClanInvite_Body",
 | 
			
		||||
@ -76,9 +76,30 @@ export const addToGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
        };
 | 
			
		||||
        await fillInInventoryDataForGuildMember(member);
 | 
			
		||||
        res.json({ NewMember: member });
 | 
			
		||||
    } else if ("RequestMsg" in payload) {
 | 
			
		||||
        // Player applying to join a clan
 | 
			
		||||
        const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
        try {
 | 
			
		||||
            await GuildMember.insertOne({
 | 
			
		||||
                accountId,
 | 
			
		||||
                guildId: payload.GuildId.$oid,
 | 
			
		||||
                status: 1, // incoming invite
 | 
			
		||||
                RequestMsg: payload.RequestMsg,
 | 
			
		||||
                RequestExpiry: new Date(Date.now() + 14 * 86400 * 1000) // TOVERIFY: I can't find any good information about this with regards to live, but 2 weeks seem reasonable.
 | 
			
		||||
            });
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            logger.debug(`guild invite failed due to ${String(e)}`);
 | 
			
		||||
            res.status(400).send("Already requested");
 | 
			
		||||
        }
 | 
			
		||||
        res.end();
 | 
			
		||||
    } else {
 | 
			
		||||
        logger.error(`data provided to ${req.path}: ${String(req.body)}`);
 | 
			
		||||
        res.status(400).end();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IAddToGuildRequest {
 | 
			
		||||
    UserName: string;
 | 
			
		||||
    UserName?: string;
 | 
			
		||||
    GuildId: IOid;
 | 
			
		||||
    RequestMsg?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -57,12 +57,16 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
 | 
			
		||||
        payload.Consumed.forEach(upgrade => {
 | 
			
		||||
            const meta = ExportUpgrades[upgrade.ItemType];
 | 
			
		||||
            counts[meta.rarity] += upgrade.ItemCount;
 | 
			
		||||
            if (upgrade.ItemId.$oid != "000000000000000000000000") {
 | 
			
		||||
                inventory.Upgrades.pull({ _id: upgrade.ItemId.$oid });
 | 
			
		||||
            } 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") {
 | 
			
		||||
@ -72,6 +76,15 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        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,
 | 
			
		||||
@ -87,7 +100,9 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        const newModType = getRandomWeightedReward(options, weights)!.uniqueName;
 | 
			
		||||
            newModType = getRandomWeightedReward(options, weights)!.uniqueName;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        addMods(inventory, [
 | 
			
		||||
            {
 | 
			
		||||
                ItemType: newModType,
 | 
			
		||||
@ -130,3 +145,34 @@ interface IAgnosticUpgradeClient {
 | 
			
		||||
    ItemCount: number;
 | 
			
		||||
    LastAdded: IOid;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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"
 | 
			
		||||
    ]
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										20
									
								
								src/controllers/api/cancelGuildAdvertisementController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/controllers/api/cancelGuildAdvertisementController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
			
		||||
import { GuildAd } from "@/src/models/guildModel";
 | 
			
		||||
import { getGuildForRequestEx, hasGuildPermission } from "@/src/services/guildService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const cancelGuildAdvertisementController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "GuildId");
 | 
			
		||||
    const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
    if (!(await hasGuildPermission(guild, accountId, GuildPermission.Advertiser))) {
 | 
			
		||||
        res.status(400).end();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await GuildAd.deleteOne({ GuildId: guild._id });
 | 
			
		||||
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
@ -15,6 +15,12 @@ export const changeDojoRootController: RequestHandler = async (req, res) => {
 | 
			
		||||
        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()] = {
 | 
			
		||||
@ -43,23 +49,13 @@ export const changeDojoRootController: RequestHandler = async (req, res) => {
 | 
			
		||||
    newRoot.component.pp = undefined;
 | 
			
		||||
    newRoot.parent = undefined;
 | 
			
		||||
 | 
			
		||||
    // Don't even ask me why this is needed because I don't know either
 | 
			
		||||
    // Set/update SortId in top-to-bottom order
 | 
			
		||||
    const stack: INode[] = [newRoot];
 | 
			
		||||
    let i = 0;
 | 
			
		||||
    const idMap: Record<string, Types.ObjectId> = {};
 | 
			
		||||
    while (stack.length != 0) {
 | 
			
		||||
        const top = stack.shift()!;
 | 
			
		||||
        idMap[top.component._id.toString()] = new Types.ObjectId(
 | 
			
		||||
            (++i).toString(16).padStart(8, "0") + top.component._id.toString().substr(8)
 | 
			
		||||
        );
 | 
			
		||||
        top.component.SortId = new Types.ObjectId();
 | 
			
		||||
        top.children.forEach(x => stack.push(x));
 | 
			
		||||
    }
 | 
			
		||||
    guild.DojoComponents.forEach(x => {
 | 
			
		||||
        x._id = idMap[x._id.toString()];
 | 
			
		||||
        if (x.pi) {
 | 
			
		||||
            x.pi = idMap[x.pi.toString()];
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    logger.debug("New tree:\n" + treeToString(newRoot));
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -18,8 +18,9 @@ import {
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
 | 
			
		||||
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { toOid } from "@/src/helpers/inventoryHelpers";
 | 
			
		||||
 | 
			
		||||
export interface IClaimCompletedRecipeRequest {
 | 
			
		||||
interface IClaimCompletedRecipeRequest {
 | 
			
		||||
    RecipeIds: IOid[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -80,6 +81,7 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
 | 
			
		||||
    } else {
 | 
			
		||||
        logger.debug("Claiming Recipe", { recipe, pendingRecipe });
 | 
			
		||||
 | 
			
		||||
        let BrandedSuits: undefined | IOid[];
 | 
			
		||||
        if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
 | 
			
		||||
            inventory.PendingSpectreLoadouts ??= [];
 | 
			
		||||
            inventory.SpectreLoadouts ??= [];
 | 
			
		||||
@ -99,9 +101,15 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
 | 
			
		||||
                inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]);
 | 
			
		||||
                inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1);
 | 
			
		||||
            }
 | 
			
		||||
        } else if (recipe.secretIngredientAction == "SIA_UNBRAND") {
 | 
			
		||||
            inventory.BrandedSuits!.splice(
 | 
			
		||||
                inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)),
 | 
			
		||||
                1
 | 
			
		||||
            );
 | 
			
		||||
            BrandedSuits = [toOid(pendingRecipe.SuitToUnbrand!)];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let InventoryChanges = {};
 | 
			
		||||
        let InventoryChanges: IInventoryChanges = {};
 | 
			
		||||
        if (recipe.consumeOnUse) {
 | 
			
		||||
            addRecipes(inventory, [
 | 
			
		||||
                {
 | 
			
		||||
@ -111,16 +119,24 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
 | 
			
		||||
            ]);
 | 
			
		||||
        }
 | 
			
		||||
        if (req.query.rush) {
 | 
			
		||||
            const end = Math.trunc(pendingRecipe.CompletionDate.getTime() / 1000);
 | 
			
		||||
            const start = end - recipe.buildTime;
 | 
			
		||||
            const secondsElapsed = Math.trunc(Date.now() / 1000) - start;
 | 
			
		||||
            const progress = secondsElapsed / recipe.buildTime;
 | 
			
		||||
            logger.debug(`rushing recipe at ${Math.trunc(progress * 100)}% completion`);
 | 
			
		||||
            const cost = Math.round(recipe.skipBuildTimePrice * (1 - (progress - 0.5)));
 | 
			
		||||
            InventoryChanges = {
 | 
			
		||||
                ...InventoryChanges,
 | 
			
		||||
                ...updateCurrency(inventory, recipe.skipBuildTimePrice, true)
 | 
			
		||||
                ...updateCurrency(inventory, cost, true)
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        if (recipe.secretIngredientAction != "SIA_UNBRAND") {
 | 
			
		||||
            InventoryChanges = {
 | 
			
		||||
                ...InventoryChanges,
 | 
			
		||||
                ...(await addItem(inventory, recipe.resultType, recipe.num, false))
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
        res.json({ InventoryChanges });
 | 
			
		||||
        res.json({ InventoryChanges, BrandedSuits });
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,8 @@ export const clearDialogueHistoryController: RequestHandler = async (req, res) =
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
    const request = JSON.parse(String(req.body)) as IClearDialogueRequest;
 | 
			
		||||
    if (inventory.DialogueHistory && inventory.DialogueHistory.Dialogues) {
 | 
			
		||||
        inventory.DialogueHistory.Resets ??= 0;
 | 
			
		||||
        inventory.DialogueHistory.Resets += 1;
 | 
			
		||||
        for (const dialogueName of request.Dialogues) {
 | 
			
		||||
            const index = inventory.DialogueHistory.Dialogues.findIndex(x => x.DialogueName == dialogueName);
 | 
			
		||||
            if (index != -1) {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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 "@/src/models/guildModel";
 | 
			
		||||
import { getAllianceClient } from "@/src/services/guildService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const confirmAllianceInvitationController: RequestHandler = async (req, res) => {
 | 
			
		||||
    // Check requester is a warlord in their guild
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const guildMember = (await GuildMember.findOne({ accountId, status: 0 }))!;
 | 
			
		||||
    if (guildMember.rank > 1) {
 | 
			
		||||
        res.status(400).json({ Error: 104 });
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const allianceMember = await AllianceMember.findOne({
 | 
			
		||||
        allianceId: req.query.allianceId,
 | 
			
		||||
        guildId: guildMember.guildId
 | 
			
		||||
    });
 | 
			
		||||
    if (!allianceMember || !allianceMember.Pending) {
 | 
			
		||||
        res.status(400);
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    allianceMember.Pending = false;
 | 
			
		||||
 | 
			
		||||
    const guild = (await Guild.findById(guildMember.guildId))!;
 | 
			
		||||
    guild.AllianceId = allianceMember.allianceId;
 | 
			
		||||
 | 
			
		||||
    await Promise.all([allianceMember.save(), guild.save()]);
 | 
			
		||||
 | 
			
		||||
    // Give client the new alliance data which uses "AllianceId" instead of "_id" in this response
 | 
			
		||||
    const alliance = (await Alliance.findById(allianceMember.allianceId))!;
 | 
			
		||||
    const { _id, ...rest } = await getAllianceClient(alliance, guild);
 | 
			
		||||
    res.json({
 | 
			
		||||
        AllianceId: _id,
 | 
			
		||||
        ...rest
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
@ -1,26 +1,59 @@
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { Guild, GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { getGuildClient, updateInventoryForConfirmedGuildJoin } from "@/src/services/guildService";
 | 
			
		||||
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
 | 
			
		||||
import { Account } from "@/src/models/loginModel";
 | 
			
		||||
import { deleteGuild, getGuildClient, hasGuildPermission, removeDojoKeyItems } from "@/src/services/guildService";
 | 
			
		||||
import { addRecipes, combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService";
 | 
			
		||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { Types } from "mongoose";
 | 
			
		||||
 | 
			
		||||
export const confirmGuildInvitationController: RequestHandler = async (req, res) => {
 | 
			
		||||
// 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 guildMember = await GuildMember.findOne({
 | 
			
		||||
    const invitedGuildMember = await GuildMember.findOne({
 | 
			
		||||
        accountId: account._id,
 | 
			
		||||
        guildId: req.query.clanId as string
 | 
			
		||||
    });
 | 
			
		||||
    if (guildMember) {
 | 
			
		||||
        guildMember.status = 0;
 | 
			
		||||
        await guildMember.save();
 | 
			
		||||
    if (invitedGuildMember && invitedGuildMember.status == 2) {
 | 
			
		||||
        let inventoryChanges: IInventoryChanges = {};
 | 
			
		||||
 | 
			
		||||
        await updateInventoryForConfirmedGuildJoin(
 | 
			
		||||
            account._id.toString(),
 | 
			
		||||
            new Types.ObjectId(req.query.clanId as string)
 | 
			
		||||
        );
 | 
			
		||||
        // If this account is already in a guild, we need to do cleanup first.
 | 
			
		||||
        const guildMember = await GuildMember.findOneAndDelete({ accountId: account._id, status: 0 });
 | 
			
		||||
        if (guildMember) {
 | 
			
		||||
            const inventory = await getInventory(account._id.toString(), "LevelKeys Recipes");
 | 
			
		||||
            inventoryChanges = removeDojoKeyItems(inventory);
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
 | 
			
		||||
            if (guildMember.rank == 0) {
 | 
			
		||||
                await deleteGuild(guildMember.guildId);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Now that we're sure this account is not in a guild right now, we can just proceed with the normal updates.
 | 
			
		||||
        invitedGuildMember.status = 0;
 | 
			
		||||
        await invitedGuildMember.save();
 | 
			
		||||
 | 
			
		||||
        // Remove pending applications for this account
 | 
			
		||||
        await GuildMember.deleteMany({ accountId: account._id, status: 1 });
 | 
			
		||||
 | 
			
		||||
        // Update inventory of new member
 | 
			
		||||
        const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes");
 | 
			
		||||
        inventory.GuildId = new Types.ObjectId(req.query.clanId as string);
 | 
			
		||||
        const recipeChanges = [
 | 
			
		||||
            {
 | 
			
		||||
                ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
 | 
			
		||||
                ItemCount: 1
 | 
			
		||||
            }
 | 
			
		||||
        ];
 | 
			
		||||
        addRecipes(inventory, recipeChanges);
 | 
			
		||||
        combineInventoryChanges(inventoryChanges, { Recipes: recipeChanges });
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
 | 
			
		||||
        const guild = (await Guild.findById(req.query.clanId as string))!;
 | 
			
		||||
 | 
			
		||||
        // Add join to clan log
 | 
			
		||||
        guild.RosterActivity ??= [];
 | 
			
		||||
        guild.RosterActivity.push({
 | 
			
		||||
            dateTime: new Date(),
 | 
			
		||||
@ -31,16 +64,61 @@ export const confirmGuildInvitationController: RequestHandler = async (req, res)
 | 
			
		||||
 | 
			
		||||
        res.json({
 | 
			
		||||
            ...(await getGuildClient(guild, account._id.toString())),
 | 
			
		||||
            InventoryChanges: {
 | 
			
		||||
                Recipes: [
 | 
			
		||||
                    {
 | 
			
		||||
                        ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
 | 
			
		||||
                        ItemCount: 1
 | 
			
		||||
                    }
 | 
			
		||||
                ]
 | 
			
		||||
            }
 | 
			
		||||
            InventoryChanges: inventoryChanges
 | 
			
		||||
        });
 | 
			
		||||
    } else {
 | 
			
		||||
        res.end();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// POST request: Clan representative accepting invite(s).
 | 
			
		||||
export const confirmGuildInvitationPostController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const guild = (await Guild.findById(req.query.clanId as string, "Ranks RosterActivity"))!;
 | 
			
		||||
    if (!(await hasGuildPermission(guild, accountId, GuildPermission.Recruiter))) {
 | 
			
		||||
        res.status(400).json("Invalid permission");
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    const payload = getJSONfromString<{ userId: string }>(String(req.body));
 | 
			
		||||
    const filter: { accountId?: string; status: number } = { status: 1 };
 | 
			
		||||
    if (payload.userId != "all") {
 | 
			
		||||
        filter.accountId = payload.userId;
 | 
			
		||||
    }
 | 
			
		||||
    const guildMembers = await GuildMember.find(filter);
 | 
			
		||||
    const newMembers: string[] = [];
 | 
			
		||||
    for (const guildMember of guildMembers) {
 | 
			
		||||
        guildMember.status = 0;
 | 
			
		||||
        guildMember.RequestMsg = undefined;
 | 
			
		||||
        guildMember.RequestExpiry = undefined;
 | 
			
		||||
        await guildMember.save();
 | 
			
		||||
 | 
			
		||||
        // Remove other pending applications for this account
 | 
			
		||||
        await GuildMember.deleteMany({ accountId: guildMember.accountId, status: 1 });
 | 
			
		||||
 | 
			
		||||
        // Update inventory of new member
 | 
			
		||||
        const inventory = await getInventory(guildMember.accountId.toString(), "GuildId Recipes");
 | 
			
		||||
        inventory.GuildId = new Types.ObjectId(req.query.clanId as string);
 | 
			
		||||
        addRecipes(inventory, [
 | 
			
		||||
            {
 | 
			
		||||
                ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
 | 
			
		||||
                ItemCount: 1
 | 
			
		||||
            }
 | 
			
		||||
        ]);
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
 | 
			
		||||
        // Add join to clan log
 | 
			
		||||
        const account = (await Account.findOne({ _id: guildMember.accountId }))!;
 | 
			
		||||
        guild.RosterActivity ??= [];
 | 
			
		||||
        guild.RosterActivity.push({
 | 
			
		||||
            dateTime: new Date(),
 | 
			
		||||
            entryType: 6,
 | 
			
		||||
            details: getSuffixedName(account)
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        newMembers.push(account._id.toString());
 | 
			
		||||
    }
 | 
			
		||||
    await guild.save();
 | 
			
		||||
    res.json({
 | 
			
		||||
        NewMembers: newMembers
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,7 @@
 | 
			
		||||
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { Guild, GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { config } from "@/src/services/configService";
 | 
			
		||||
import { createMessage } from "@/src/services/inboxService";
 | 
			
		||||
import { Guild } from "@/src/models/guildModel";
 | 
			
		||||
import { checkClanAscensionHasRequiredContributors } from "@/src/services/guildService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
@ -31,43 +30,7 @@ export const contributeGuildClassController: RequestHandler = async (req, res) =
 | 
			
		||||
 | 
			
		||||
    guild.CeremonyContributors.push(new Types.ObjectId(accountId));
 | 
			
		||||
 | 
			
		||||
    // Once required contributor count is hit, the class is committed and there's 72 hours to claim endo.
 | 
			
		||||
    if (guild.CeremonyContributors.length == payload.RequiredContributors) {
 | 
			
		||||
        guild.Class = guild.CeremonyClass!;
 | 
			
		||||
        guild.CeremonyClass = undefined;
 | 
			
		||||
        guild.CeremonyResetDate = new Date(Date.now() + (config.fastClanAscension ? 5_000 : 72 * 3600_000));
 | 
			
		||||
        if (!config.fastClanAscension) {
 | 
			
		||||
            // Send message to all active guild members
 | 
			
		||||
            const members = await GuildMember.find({ guildId: payload.GuildId, status: 0 }, "accountId");
 | 
			
		||||
            for (const member of members) {
 | 
			
		||||
                // somewhat unfaithful as on live the "msg" is not a loctag, but since we don't have the string, we'll let the client fill it in with "arg".
 | 
			
		||||
                await createMessage(member.accountId.toString(), [
 | 
			
		||||
                    {
 | 
			
		||||
                        sndr: guild.Name,
 | 
			
		||||
                        msg: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgressDetails",
 | 
			
		||||
                        arg: [
 | 
			
		||||
                            {
 | 
			
		||||
                                Key: "RESETDATE",
 | 
			
		||||
                                Tag:
 | 
			
		||||
                                    guild.CeremonyResetDate.getUTCMonth() +
 | 
			
		||||
                                    "/" +
 | 
			
		||||
                                    guild.CeremonyResetDate.getUTCDate() +
 | 
			
		||||
                                    "/" +
 | 
			
		||||
                                    (guild.CeremonyResetDate.getUTCFullYear() % 100) +
 | 
			
		||||
                                    " " +
 | 
			
		||||
                                    guild.CeremonyResetDate.getUTCHours().toString().padStart(2, "0") +
 | 
			
		||||
                                    ":" +
 | 
			
		||||
                                    guild.CeremonyResetDate.getUTCMinutes().toString().padStart(2, "0")
 | 
			
		||||
                            }
 | 
			
		||||
                        ],
 | 
			
		||||
                        sub: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgress",
 | 
			
		||||
                        icon: "/Lotus/Interface/Graphics/ClanTileImages/ClanEnterDojo.png",
 | 
			
		||||
                        highPriority: true
 | 
			
		||||
                    }
 | 
			
		||||
                ]);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    await checkClanAscensionHasRequiredContributors(guild);
 | 
			
		||||
 | 
			
		||||
    await guild.save();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel";
 | 
			
		||||
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
 | 
			
		||||
import {
 | 
			
		||||
    addGuildMemberMiscItemContribution,
 | 
			
		||||
    getDojoClient,
 | 
			
		||||
    getGuildForRequestEx,
 | 
			
		||||
    hasAccessToDojo,
 | 
			
		||||
@ -63,9 +64,7 @@ export const contributeToDojoComponentController: RequestHandler = async (req, r
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await guild.save();
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    await guildMember.save();
 | 
			
		||||
    await Promise.all([guild.save(), inventory.save(), guildMember.save()]);
 | 
			
		||||
    res.json({
 | 
			
		||||
        ...(await getDojoClient(guild, 0, component._id)),
 | 
			
		||||
        InventoryChanges: inventoryChanges
 | 
			
		||||
@ -94,10 +93,10 @@ const processContribution = (
 | 
			
		||||
        component.RegularCredits += request.VaultCredits;
 | 
			
		||||
        guild.VaultRegularCredits! -= request.VaultCredits;
 | 
			
		||||
    }
 | 
			
		||||
    if (component.RegularCredits > scaleRequiredCount(meta.price)) {
 | 
			
		||||
    if (component.RegularCredits > scaleRequiredCount(guild.Tier, meta.price)) {
 | 
			
		||||
        guild.VaultRegularCredits ??= 0;
 | 
			
		||||
        guild.VaultRegularCredits += component.RegularCredits - scaleRequiredCount(meta.price);
 | 
			
		||||
        component.RegularCredits = scaleRequiredCount(meta.price);
 | 
			
		||||
        guild.VaultRegularCredits += component.RegularCredits - scaleRequiredCount(guild.Tier, meta.price);
 | 
			
		||||
        component.RegularCredits = scaleRequiredCount(guild.Tier, meta.price);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    component.MiscItems ??= [];
 | 
			
		||||
@ -108,10 +107,10 @@ const processContribution = (
 | 
			
		||||
                const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!;
 | 
			
		||||
                if (
 | 
			
		||||
                    componentMiscItem.ItemCount + ingredientContribution.ItemCount >
 | 
			
		||||
                    scaleRequiredCount(ingredientMeta.ItemCount)
 | 
			
		||||
                    scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount)
 | 
			
		||||
                ) {
 | 
			
		||||
                    ingredientContribution.ItemCount =
 | 
			
		||||
                        scaleRequiredCount(ingredientMeta.ItemCount) - componentMiscItem.ItemCount;
 | 
			
		||||
                        scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) - componentMiscItem.ItemCount;
 | 
			
		||||
                }
 | 
			
		||||
                componentMiscItem.ItemCount += ingredientContribution.ItemCount;
 | 
			
		||||
            } else {
 | 
			
		||||
@ -129,10 +128,10 @@ const processContribution = (
 | 
			
		||||
                const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!;
 | 
			
		||||
                if (
 | 
			
		||||
                    componentMiscItem.ItemCount + ingredientContribution.ItemCount >
 | 
			
		||||
                    scaleRequiredCount(ingredientMeta.ItemCount)
 | 
			
		||||
                    scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount)
 | 
			
		||||
                ) {
 | 
			
		||||
                    ingredientContribution.ItemCount =
 | 
			
		||||
                        scaleRequiredCount(ingredientMeta.ItemCount) - componentMiscItem.ItemCount;
 | 
			
		||||
                        scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) - componentMiscItem.ItemCount;
 | 
			
		||||
                }
 | 
			
		||||
                componentMiscItem.ItemCount += ingredientContribution.ItemCount;
 | 
			
		||||
            } else {
 | 
			
		||||
@ -143,18 +142,20 @@ const processContribution = (
 | 
			
		||||
                ItemCount: ingredientContribution.ItemCount * -1
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            guildMember.MiscItemsContributed ??= [];
 | 
			
		||||
            guildMember.MiscItemsContributed.push(ingredientContribution);
 | 
			
		||||
            addGuildMemberMiscItemContribution(guildMember, ingredientContribution);
 | 
			
		||||
        }
 | 
			
		||||
        addMiscItems(inventory, miscItemChanges);
 | 
			
		||||
        inventoryChanges.MiscItems = miscItemChanges;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (component.RegularCredits >= scaleRequiredCount(meta.price)) {
 | 
			
		||||
    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(ingredient.ItemCount)) {
 | 
			
		||||
            if (
 | 
			
		||||
                !componentMiscItem ||
 | 
			
		||||
                componentMiscItem.ItemCount < scaleRequiredCount(guild.Tier, ingredient.ItemCount)
 | 
			
		||||
            ) {
 | 
			
		||||
                fullyFunded = false;
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -1,56 +1,104 @@
 | 
			
		||||
import { GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { getGuildForRequestEx } from "@/src/services/guildService";
 | 
			
		||||
import { addFusionTreasures, addMiscItems, addShipDecorations, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import {
 | 
			
		||||
    Alliance,
 | 
			
		||||
    Guild,
 | 
			
		||||
    GuildMember,
 | 
			
		||||
    TGuildDatabaseDocument,
 | 
			
		||||
    TGuildMemberDatabaseDocument
 | 
			
		||||
} from "@/src/models/guildModel";
 | 
			
		||||
import {
 | 
			
		||||
    addGuildMemberMiscItemContribution,
 | 
			
		||||
    addGuildMemberShipDecoContribution,
 | 
			
		||||
    addVaultFusionTreasures,
 | 
			
		||||
    addVaultMiscItems,
 | 
			
		||||
    addVaultShipDecos,
 | 
			
		||||
    getGuildForRequestEx
 | 
			
		||||
} from "@/src/services/guildService";
 | 
			
		||||
import {
 | 
			
		||||
    addFusionTreasures,
 | 
			
		||||
    addMiscItems,
 | 
			
		||||
    addShipDecorations,
 | 
			
		||||
    getInventory,
 | 
			
		||||
    updateCurrency
 | 
			
		||||
} from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const contributeToVaultController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
    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 guildMember = (await GuildMember.findOne(
 | 
			
		||||
        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"
 | 
			
		||||
        ))!;
 | 
			
		||||
    const request = JSON.parse(String(req.body)) as IContributeToVaultRequest;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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) {
 | 
			
		||||
        guild.VaultMiscItems ??= [];
 | 
			
		||||
        guildMember.MiscItemsContributed ??= [];
 | 
			
		||||
        addVaultMiscItems(guild, request.MiscItems);
 | 
			
		||||
        for (const item of request.MiscItems) {
 | 
			
		||||
            guild.VaultMiscItems.push(item);
 | 
			
		||||
            guildMember.MiscItemsContributed.push(item);
 | 
			
		||||
            if (guildMember) {
 | 
			
		||||
                addGuildMemberMiscItemContribution(guildMember, item);
 | 
			
		||||
            }
 | 
			
		||||
            addMiscItems(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if (request.ShipDecorations.length) {
 | 
			
		||||
        guild.VaultShipDecorations ??= [];
 | 
			
		||||
        guildMember.ShipDecorationsContributed ??= [];
 | 
			
		||||
        addVaultShipDecos(guild, request.ShipDecorations);
 | 
			
		||||
        for (const item of request.ShipDecorations) {
 | 
			
		||||
            guild.VaultShipDecorations.push(item);
 | 
			
		||||
            guildMember.ShipDecorationsContributed.push(item);
 | 
			
		||||
            if (guildMember) {
 | 
			
		||||
                addGuildMemberShipDecoContribution(guildMember, item);
 | 
			
		||||
            }
 | 
			
		||||
            addShipDecorations(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if (request.FusionTreasures.length) {
 | 
			
		||||
        guild.VaultFusionTreasures ??= [];
 | 
			
		||||
        addVaultFusionTreasures(guild, request.FusionTreasures);
 | 
			
		||||
        for (const item of request.FusionTreasures) {
 | 
			
		||||
            guild.VaultFusionTreasures.push(item);
 | 
			
		||||
            addFusionTreasures(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await guild.save();
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    await guildMember.save();
 | 
			
		||||
    const promises: Promise<unknown>[] = [guild.save(), inventory.save()];
 | 
			
		||||
    if (guildMember) {
 | 
			
		||||
        promises.push(guildMember.save());
 | 
			
		||||
    }
 | 
			
		||||
    await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -59,4 +107,7 @@ interface IContributeToVaultRequest {
 | 
			
		||||
    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 "@/src/helpers/stringHelpers";
 | 
			
		||||
import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { getAllianceClient } from "@/src/services/guildService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const createAllianceController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "GuildId");
 | 
			
		||||
    const guild = (await Guild.findById(inventory.GuildId!, "Name Tier AllianceId"))!;
 | 
			
		||||
    if (guild.AllianceId) {
 | 
			
		||||
        res.status(400).send("Guild is already in an alliance").end();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    const guildMember = (await GuildMember.findOne({ guildId: guild._id, accountId }, "rank"))!;
 | 
			
		||||
    if (guildMember.rank > 1) {
 | 
			
		||||
        res.status(400).send("Invalid permission").end();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    const data = getJSONfromString<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;
 | 
			
		||||
}
 | 
			
		||||
@ -2,16 +2,16 @@ import { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { Guild, GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import {
 | 
			
		||||
    createUniqueClanName,
 | 
			
		||||
    getGuildClient,
 | 
			
		||||
    updateInventoryForConfirmedGuildJoin
 | 
			
		||||
} from "@/src/services/guildService";
 | 
			
		||||
import { createUniqueClanName, getGuildClient } from "@/src/services/guildService";
 | 
			
		||||
import { addRecipes, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
 | 
			
		||||
export const createGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const payload = getJSONfromString<ICreateGuildRequest>(String(req.body));
 | 
			
		||||
 | 
			
		||||
    // Remove pending applications for this account
 | 
			
		||||
    await GuildMember.deleteMany({ accountId, status: 1 });
 | 
			
		||||
 | 
			
		||||
    // Create guild on database
 | 
			
		||||
    const guild = new Guild({
 | 
			
		||||
        Name: await createUniqueClanName(payload.guildName)
 | 
			
		||||
@ -26,9 +26,27 @@ export const createGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
        rank: 0
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await updateInventoryForConfirmedGuildJoin(accountId, guild._id);
 | 
			
		||||
    const inventory = await getInventory(accountId, "GuildId Recipes");
 | 
			
		||||
    inventory.GuildId = guild._id;
 | 
			
		||||
    addRecipes(inventory, [
 | 
			
		||||
        {
 | 
			
		||||
            ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
 | 
			
		||||
            ItemCount: 1
 | 
			
		||||
        }
 | 
			
		||||
    ]);
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
 | 
			
		||||
    res.json(await getGuildClient(guild, accountId));
 | 
			
		||||
    res.json({
 | 
			
		||||
        ...(await getGuildClient(guild, accountId)),
 | 
			
		||||
        InventoryChanges: {
 | 
			
		||||
            Recipes: [
 | 
			
		||||
                {
 | 
			
		||||
                    ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
 | 
			
		||||
                    ItemCount: 1
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ICreateGuildRequest {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										79
									
								
								src/controllers/api/crewShipIdentifySalvageController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/controllers/api/crewShipIdentifySalvageController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,79 @@
 | 
			
		||||
import {
 | 
			
		||||
    addCrewShipSalvagedWeaponSkin,
 | 
			
		||||
    addCrewShipRawSalvage,
 | 
			
		||||
    getInventory,
 | 
			
		||||
    addEquipment
 | 
			
		||||
} from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { ICrewShipComponentFingerprint, IInnateDamageFingerprint } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { ExportCustoms, ExportRailjackWeapons, ExportUpgrades } from "warframe-public-export-plus";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
import { getRandomInt } from "@/src/services/rngService";
 | 
			
		||||
import { IFingerprintStat } from "@/src/helpers/rivenHelper";
 | 
			
		||||
 | 
			
		||||
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];
 | 
			
		||||
        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)
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        addEquipment(inventory, "CrewShipSalvagedWeapons", payload.ItemType, undefined, inventoryChanges, {
 | 
			
		||||
            UpgradeType: upgradeType,
 | 
			
		||||
            UpgradeFingerprint: JSON.stringify({
 | 
			
		||||
                compat: payload.ItemType,
 | 
			
		||||
                buffs
 | 
			
		||||
            } satisfies IInnateDamageFingerprint)
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    inventoryChanges.CrewShipRawSalvage = [
 | 
			
		||||
        {
 | 
			
		||||
            ItemType: payload.ItemType,
 | 
			
		||||
            ItemCount: -1
 | 
			
		||||
        }
 | 
			
		||||
    ];
 | 
			
		||||
    addCrewShipRawSalvage(inventory, inventoryChanges.CrewShipRawSalvage);
 | 
			
		||||
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json({
 | 
			
		||||
        InventoryChanges: inventoryChanges
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ICrewShipIdentifySalvageRequest {
 | 
			
		||||
    ItemType: string;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								src/controllers/api/declineAllianceInviteController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/controllers/api/declineAllianceInviteController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
import { AllianceMember, GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const declineAllianceInviteController: RequestHandler = async (req, res) => {
 | 
			
		||||
    // Check requester is a warlord in their guild
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const guildMember = (await GuildMember.findOne({ accountId, status: 0 }))!;
 | 
			
		||||
    if (guildMember.rank > 1) {
 | 
			
		||||
        res.status(400).json({ Error: 104 });
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await AllianceMember.deleteOne({ allianceId: req.query.allianceId, guildId: guildMember.guildId });
 | 
			
		||||
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										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 "@/src/models/guildModel";
 | 
			
		||||
import { getAccountForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
			
		||||
import { parallelForeach } from "@/src/utils/async-utils";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const divvyAllianceVaultController: RequestHandler = async (req, res) => {
 | 
			
		||||
    // Afaict, there's no way to put anything other than credits in the alliance vault (anymore?), so just no-op if this is not a request to divvy credits.
 | 
			
		||||
    if (req.query.credits == "1") {
 | 
			
		||||
        // Check requester is a warlord in their guild
 | 
			
		||||
        const account = await getAccountForRequest(req);
 | 
			
		||||
        const guildMember = (await GuildMember.findOne({ accountId: account._id, status: 0 }))!;
 | 
			
		||||
        if (guildMember.rank > 1) {
 | 
			
		||||
            res.status(400).end();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check guild has treasurer permissions in the alliance
 | 
			
		||||
        const allianceMember = (await AllianceMember.findOne({
 | 
			
		||||
            allianceId: req.query.allianceId,
 | 
			
		||||
            guildId: guildMember.guildId
 | 
			
		||||
        }))!;
 | 
			
		||||
        if (!(allianceMember.Permissions & GuildPermission.Treasurer)) {
 | 
			
		||||
            res.status(400).end();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const allianceMembers = await AllianceMember.find({ allianceId: req.query.allianceId });
 | 
			
		||||
        const memberCounts: Record<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();
 | 
			
		||||
};
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel";
 | 
			
		||||
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, scaleRequiredCount } from "@/src/services/guildService";
 | 
			
		||||
import { getInventory, updateCurrency } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
@ -36,10 +36,10 @@ export const dojoComponentRushController: RequestHandler = async (req, res) => {
 | 
			
		||||
    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(deco, meta, platinumDonated);
 | 
			
		||||
        processContribution(guild, deco, meta, platinumDonated);
 | 
			
		||||
    } else {
 | 
			
		||||
        const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!;
 | 
			
		||||
        processContribution(component, meta, platinumDonated);
 | 
			
		||||
        processContribution(guild, component, meta, platinumDonated);
 | 
			
		||||
 | 
			
		||||
        const entry = guild.RoomChanges?.find(x => x.componentId.equals(component._id));
 | 
			
		||||
        if (entry) {
 | 
			
		||||
@ -47,13 +47,11 @@ export const dojoComponentRushController: RequestHandler = async (req, res) => {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await guild.save();
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
 | 
			
		||||
    const guildMember = (await GuildMember.findOne({ accountId, guildId: guild._id }, "PremiumCreditsContributed"))!;
 | 
			
		||||
    guildMember.PremiumCreditsContributed ??= 0;
 | 
			
		||||
    guildMember.PremiumCreditsContributed += request.Amount;
 | 
			
		||||
    await guildMember.save();
 | 
			
		||||
 | 
			
		||||
    await Promise.all([guild.save(), inventory.save(), guildMember.save()]);
 | 
			
		||||
 | 
			
		||||
    res.json({
 | 
			
		||||
        ...(await getDojoClient(guild, 0, component._id)),
 | 
			
		||||
@ -61,8 +59,13 @@ export const dojoComponentRushController: RequestHandler = async (req, res) => {
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const processContribution = (component: IDojoContributable, meta: IDojoBuild, platinumDonated: number): void => {
 | 
			
		||||
    const fullPlatinumCost = scaleRequiredCount(meta.skipTimePrice);
 | 
			
		||||
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(
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,11 @@
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
// Arbiter Dojo endpoints, not really used by us as we don't provide a ContentURL.
 | 
			
		||||
 | 
			
		||||
export const dojoController: RequestHandler = (_req, res) => {
 | 
			
		||||
    res.json("-1"); // Tell client to use authorised request.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const setDojoURLController: RequestHandler = (_req, res) => {
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -55,7 +55,7 @@ export const dronesController: RequestHandler = async (req, res) => {
 | 
			
		||||
            ? new Date()
 | 
			
		||||
            : new Date(Date.now() + getRandomInt(3 * 3600 * 1000, 4 * 3600 * 1000));
 | 
			
		||||
        drone.PendingDamage =
 | 
			
		||||
            Math.random() < system.damageChance
 | 
			
		||||
            !config.noResourceExtractorDronesDamage && Math.random() < system.damageChance
 | 
			
		||||
                ? getRandomInt(system.droneDamage.minValue, system.droneDamage.maxValue)
 | 
			
		||||
                : 0;
 | 
			
		||||
        const resource = getRandomWeightedRewardUc(system.resources, droneMeta.probabilities)!;
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,7 @@ export const evolveWeaponController: RequestHandler = async (req, res) => {
 | 
			
		||||
            recipe.ingredients.map(x => ({ ItemType: x.ItemType, ItemCount: x.ItemCount * -1 }))
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const item = inventory[payload.Category].find(item => item._id.toString() == (req.query.ItemId as string))!;
 | 
			
		||||
        const item = inventory[payload.Category].id(req.query.ItemId as string)!;
 | 
			
		||||
        item.Features ??= 0;
 | 
			
		||||
        item.Features |= EquipmentFeatures.INCARNON_GENESIS;
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,7 @@ export const evolveWeaponController: RequestHandler = async (req, res) => {
 | 
			
		||||
            }
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        const item = inventory[payload.Category].find(item => item._id.toString() == (req.query.ItemId as string))!;
 | 
			
		||||
        const item = inventory[payload.Category].id(req.query.ItemId as string)!;
 | 
			
		||||
        item.Features! &= ~EquipmentFeatures.INCARNON_GENESIS;
 | 
			
		||||
    } else {
 | 
			
		||||
        throw new Error(`unexpected evolve weapon action: ${payload.Action}`);
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,9 @@
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
 | 
			
		||||
import { addMiscItems, getInventory, getStandingLimit, updateStandingLimit } from "@/src/services/inventoryService";
 | 
			
		||||
import { addMiscItems, addStanding, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { ExportResources, ExportSyndicates } from "warframe-public-export-plus";
 | 
			
		||||
import { ExportResources } from "warframe-public-export-plus";
 | 
			
		||||
 | 
			
		||||
export const fishmongerController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -31,32 +30,15 @@ export const fishmongerController: RequestHandler = async (req, res) => {
 | 
			
		||||
        miscItemChanges.push({ ItemType: fish.ItemType, ItemCount: fish.ItemCount * -1 });
 | 
			
		||||
    }
 | 
			
		||||
    addMiscItems(inventory, miscItemChanges);
 | 
			
		||||
    if (gainedStanding && syndicateTag) {
 | 
			
		||||
        let syndicate = inventory.Affiliations.find(x => x.Tag == syndicateTag);
 | 
			
		||||
        if (!syndicate) {
 | 
			
		||||
            syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: syndicateTag, Standing: 0 }) - 1];
 | 
			
		||||
        }
 | 
			
		||||
        const syndicateMeta = ExportSyndicates[syndicateTag];
 | 
			
		||||
 | 
			
		||||
        const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0);
 | 
			
		||||
        if (syndicate.Standing + gainedStanding > max) {
 | 
			
		||||
            gainedStanding = max - syndicate.Standing;
 | 
			
		||||
        }
 | 
			
		||||
        if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) {
 | 
			
		||||
            gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        syndicate.Standing += gainedStanding;
 | 
			
		||||
 | 
			
		||||
        updateStandingLimit(inventory, syndicateMeta.dailyLimitBin, gainedStanding);
 | 
			
		||||
    }
 | 
			
		||||
    let affiliationMod;
 | 
			
		||||
    if (gainedStanding && syndicateTag) affiliationMod = addStanding(inventory, syndicateTag, gainedStanding);
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json({
 | 
			
		||||
        InventoryChanges: {
 | 
			
		||||
            MiscItems: miscItemChanges
 | 
			
		||||
        },
 | 
			
		||||
        SyndicateTag: syndicateTag,
 | 
			
		||||
        StandingChange: gainedStanding
 | 
			
		||||
        StandingChange: affiliationMod?.Standing || 0
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -18,8 +18,8 @@ export const focusController: RequestHandler = async (req, res) => {
 | 
			
		||||
        case FocusOperation.InstallLens: {
 | 
			
		||||
            const request = JSON.parse(String(req.body)) as ILensInstallRequest;
 | 
			
		||||
            const inventory = await getInventory(accountId);
 | 
			
		||||
            for (const item of inventory[request.Category]) {
 | 
			
		||||
                if (item._id.toString() == request.WeaponId) {
 | 
			
		||||
            const item = inventory[request.Category].id(request.WeaponId);
 | 
			
		||||
            if (item) {
 | 
			
		||||
                item.FocusLens = request.LensType;
 | 
			
		||||
                addMiscItems(inventory, [
 | 
			
		||||
                    {
 | 
			
		||||
@ -27,8 +27,6 @@ export const focusController: RequestHandler = async (req, res) => {
 | 
			
		||||
                        ItemCount: -1
 | 
			
		||||
                    } satisfies IMiscItem
 | 
			
		||||
                ]);
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            res.json({
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,25 @@
 | 
			
		||||
import { Alliance, Guild } from "@/src/models/guildModel";
 | 
			
		||||
import { getAllianceClient } from "@/src/services/guildService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
const getAllianceController: RequestHandler = (_req, res) => {
 | 
			
		||||
    res.sendStatus(200);
 | 
			
		||||
export const getAllianceController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "GuildId");
 | 
			
		||||
    if (inventory.GuildId) {
 | 
			
		||||
        const guild = (await Guild.findById(inventory.GuildId, "Name Tier AllianceId"))!;
 | 
			
		||||
        if (guild.AllianceId) {
 | 
			
		||||
            const alliance = (await Alliance.findById(guild.AllianceId))!;
 | 
			
		||||
            res.json(await getAllianceClient(alliance, guild));
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export { getAllianceController };
 | 
			
		||||
/*interface IGetAllianceRequest {
 | 
			
		||||
    memberCount: number;
 | 
			
		||||
    clanLeaderName: string;
 | 
			
		||||
    clanLeaderId: string;
 | 
			
		||||
}*/
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import { GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { IGuildMemberClient } from "@/src/types/guildTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const getGuildContributionsController: RequestHandler = async (req, res) => {
 | 
			
		||||
@ -8,11 +9,11 @@ export const getGuildContributionsController: RequestHandler = async (req, res)
 | 
			
		||||
    const guildId = (await getInventory(accountId, "GuildId")).GuildId;
 | 
			
		||||
    const guildMember = (await GuildMember.findOne({ guildId, accountId: req.query.buddyId }))!;
 | 
			
		||||
    res.json({
 | 
			
		||||
        _id: { $oid: req.query.buddyId },
 | 
			
		||||
        _id: { $oid: req.query.buddyId as string },
 | 
			
		||||
        RegularCreditsContributed: guildMember.RegularCreditsContributed,
 | 
			
		||||
        PremiumCreditsContributed: guildMember.PremiumCreditsContributed,
 | 
			
		||||
        MiscItemsContributed: guildMember.MiscItemsContributed,
 | 
			
		||||
        ConsumablesContributed: [], // ???
 | 
			
		||||
        ShipDecorationsContributed: guildMember.ShipDecorationsContributed
 | 
			
		||||
    });
 | 
			
		||||
    } satisfies Partial<IGuildMemberClient>);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { createUniqueClanName, getGuildClient } from "@/src/services/guildService";
 | 
			
		||||
 | 
			
		||||
const getGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
export const getGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "GuildId");
 | 
			
		||||
    if (inventory.GuildId) {
 | 
			
		||||
@ -28,7 +28,5 @@ const getGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    res.sendStatus(200);
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export { getGuildController };
 | 
			
		||||
 | 
			
		||||
@ -1,16 +1,20 @@
 | 
			
		||||
import { toOid } from "@/src/helpers/inventoryHelpers";
 | 
			
		||||
import { Account, Ignore } from "@/src/models/loginModel";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { IFriendInfo } from "@/src/types/guildTypes";
 | 
			
		||||
import { parallelForeach } from "@/src/utils/async-utils";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
const getIgnoredUsersController: RequestHandler = (_req, res) => {
 | 
			
		||||
    res.writeHead(200, {
 | 
			
		||||
        "Content-Type": "text/html",
 | 
			
		||||
        "Content-Length": "3"
 | 
			
		||||
export const getIgnoredUsersController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const ignores = await Ignore.find({ ignorer: accountId });
 | 
			
		||||
    const ignoredUsers: IFriendInfo[] = [];
 | 
			
		||||
    await parallelForeach(ignores, async ignore => {
 | 
			
		||||
        const ignoreeAccount = (await Account.findById(ignore.ignoree, "DisplayName"))!;
 | 
			
		||||
        ignoredUsers.push({
 | 
			
		||||
            _id: toOid(ignore.ignoree),
 | 
			
		||||
            DisplayName: ignoreeAccount.DisplayName + ""
 | 
			
		||||
        });
 | 
			
		||||
    res.end(
 | 
			
		||||
        Buffer.from([
 | 
			
		||||
            0x7b, 0x22, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x22, 0x3a, 0x38, 0x33, 0x30, 0x34, 0x30, 0x37, 0x37, 0x32, 0x32,
 | 
			
		||||
            0x34, 0x30, 0x32, 0x32, 0x32, 0x36, 0x31, 0x35, 0x30, 0x31, 0x7d
 | 
			
		||||
        ])
 | 
			
		||||
    );
 | 
			
		||||
    });
 | 
			
		||||
    res.json({ IgnoredUsers: ignoredUsers });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export { getIgnoredUsersController };
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,12 @@
 | 
			
		||||
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
 | 
			
		||||
import { generateRewardSeed } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const getNewRewardSeedController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
 | 
			
		||||
    const rewardSeed = generateRewardSeed();
 | 
			
		||||
    logger.debug(`generated new reward seed: ${rewardSeed}`);
 | 
			
		||||
    await Inventory.updateOne(
 | 
			
		||||
        {
 | 
			
		||||
            accountOwnerId: accountId
 | 
			
		||||
@ -18,9 +17,3 @@ export const getNewRewardSeedController: RequestHandler = async (req, res) => {
 | 
			
		||||
    );
 | 
			
		||||
    res.json({ rewardSeed: rewardSeed });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function generateRewardSeed(): number {
 | 
			
		||||
    const min = -Number.MAX_SAFE_INTEGER;
 | 
			
		||||
    const max = Number.MAX_SAFE_INTEGER;
 | 
			
		||||
    return Math.floor(Math.random() * (max - min + 1)) + min;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -26,7 +26,10 @@ export const getShipController: RequestHandler = async (req, res) => {
 | 
			
		||||
                Colors: personalRooms.ShipInteriorColors,
 | 
			
		||||
                ShipAttachments: ship.ShipAttachments,
 | 
			
		||||
                SkinFlavourItem: ship.SkinFlavourItem
 | 
			
		||||
            }
 | 
			
		||||
            },
 | 
			
		||||
            FavouriteLoadoutId: personalRooms.Ship.FavouriteLoadoutId
 | 
			
		||||
                ? toOid(personalRooms.Ship.FavouriteLoadoutId)
 | 
			
		||||
                : undefined
 | 
			
		||||
        },
 | 
			
		||||
        Apartment: personalRooms.Apartment,
 | 
			
		||||
        TailorShop: personalRooms.TailorShop
 | 
			
		||||
 | 
			
		||||
@ -56,7 +56,7 @@ export const giftingController: RequestHandler = async (req, res) => {
 | 
			
		||||
    await senderInventory.save();
 | 
			
		||||
 | 
			
		||||
    const senderName = getSuffixedName(senderAccount);
 | 
			
		||||
    await createMessage(account._id.toString(), [
 | 
			
		||||
    await createMessage(account._id, [
 | 
			
		||||
        {
 | 
			
		||||
            sndr: senderName,
 | 
			
		||||
            msg: data.Message || "/Lotus/Language/Menu/GiftReceivedBody_NoCustomMessage",
 | 
			
		||||
 | 
			
		||||
@ -2,36 +2,25 @@ import { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { addMiscItems, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { WeaponTypeInternal } from "@/src/services/itemDataService";
 | 
			
		||||
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { ArtifactPolarity, EquipmentFeatures, IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
 | 
			
		||||
import { ExportRecipes } from "warframe-public-export-plus";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
 | 
			
		||||
const modularWeaponCategory: (WeaponTypeInternal | "Hoverboards")[] = [
 | 
			
		||||
    "LongGuns",
 | 
			
		||||
    "Pistols",
 | 
			
		||||
    "Melee",
 | 
			
		||||
    "OperatorAmps",
 | 
			
		||||
    "Hoverboards"
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
interface IGildWeaponRequest {
 | 
			
		||||
    ItemName: string;
 | 
			
		||||
    Recipe: string; // e.g. /Lotus/Weapons/SolarisUnited/LotusGildKitgunBlueprint
 | 
			
		||||
    PolarizeSlot?: number;
 | 
			
		||||
    PolarizeValue?: ArtifactPolarity;
 | 
			
		||||
    ItemId: string;
 | 
			
		||||
    Category: WeaponTypeInternal | "Hoverboards";
 | 
			
		||||
    Category: TEquipmentKey;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const gildWeaponController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const data = getJSONfromString<IGildWeaponRequest>(String(req.body));
 | 
			
		||||
    data.ItemId = String(req.query.ItemId);
 | 
			
		||||
    if (!modularWeaponCategory.includes(req.query.Category as WeaponTypeInternal | "Hoverboards")) {
 | 
			
		||||
        throw new Error(`Unknown modular weapon Category: ${String(req.query.Category)}`);
 | 
			
		||||
    }
 | 
			
		||||
    data.Category = req.query.Category as WeaponTypeInternal | "Hoverboards";
 | 
			
		||||
    data.Category = req.query.Category as TEquipmentKey;
 | 
			
		||||
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
    const weaponIndex = inventory[data.Category].findIndex(x => String(x._id) === data.ItemId);
 | 
			
		||||
@ -42,8 +31,10 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const weapon = inventory[data.Category][weaponIndex];
 | 
			
		||||
    weapon.Features ??= 0;
 | 
			
		||||
    weapon.Features |= EquipmentFeatures.GILDED;
 | 
			
		||||
    if (data.Recipe != "webui") {
 | 
			
		||||
        weapon.ItemName = data.ItemName;
 | 
			
		||||
        weapon.XP = 0;
 | 
			
		||||
    }
 | 
			
		||||
    if (data.Category != "OperatorAmps" && data.PolarizeSlot && data.PolarizeValue) {
 | 
			
		||||
        weapon.Polarity = [
 | 
			
		||||
            {
 | 
			
		||||
@ -56,6 +47,9 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const inventoryChanges: IInventoryChanges = {};
 | 
			
		||||
    inventoryChanges[data.Category] = [weapon.toJSON<IEquipmentClient>()];
 | 
			
		||||
 | 
			
		||||
    const affiliationMods = [];
 | 
			
		||||
 | 
			
		||||
    if (data.Recipe != "webui") {
 | 
			
		||||
        const recipe = ExportRecipes[data.Recipe];
 | 
			
		||||
        inventoryChanges.MiscItems = recipe.secretIngredients!.map(ingredient => ({
 | 
			
		||||
            ItemType: ingredient.ItemType,
 | 
			
		||||
@ -63,7 +57,6 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
 | 
			
		||||
        }));
 | 
			
		||||
        addMiscItems(inventory, inventoryChanges.MiscItems);
 | 
			
		||||
 | 
			
		||||
    const affiliationMods = [];
 | 
			
		||||
        if (recipe.syndicateStandingChange) {
 | 
			
		||||
            const affiliation = inventory.Affiliations.find(x => x.Tag == recipe.syndicateStandingChange!.tag)!;
 | 
			
		||||
            affiliation.Standing += recipe.syndicateStandingChange.value;
 | 
			
		||||
@ -72,6 +65,7 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
 | 
			
		||||
                Standing: recipe.syndicateStandingChange.value
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json({
 | 
			
		||||
 | 
			
		||||
@ -2,8 +2,8 @@ import { RequestHandler } from "express";
 | 
			
		||||
import { parseString } from "@/src/helpers/general";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { IGroup } from "@/src/types/loginTypes";
 | 
			
		||||
import { giveKeyChainItem } from "@/src/services/questService";
 | 
			
		||||
import { IKeyChainRequest } from "@/src/types/requestTypes";
 | 
			
		||||
 | 
			
		||||
export const giveKeyChainTriggeredItemsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = parseString(req.query.accountId);
 | 
			
		||||
@ -15,9 +15,3 @@ export const giveKeyChainTriggeredItemsController: RequestHandler = async (req,
 | 
			
		||||
 | 
			
		||||
    res.send(inventoryChanges);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface IKeyChainRequest {
 | 
			
		||||
    KeyChain: string;
 | 
			
		||||
    ChainStage: number;
 | 
			
		||||
    Groups?: IGroup[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { giveKeyChainMessage } from "@/src/services/questService";
 | 
			
		||||
import { IKeyChainRequest } from "@/src/types/requestTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const giveKeyChainTriggeredMessageController: RequestHandler = async (req, res) => {
 | 
			
		||||
 | 
			
		||||
@ -16,15 +16,15 @@ export const giveQuestKeyRewardController: RequestHandler = async (req, res) =>
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
    const inventoryChanges = await addItem(inventory, reward.ItemType, reward.Amount);
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json(inventoryChanges.InventoryChanges);
 | 
			
		||||
    res.json(inventoryChanges);
 | 
			
		||||
    //TODO: consider whishlist changes
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface IQuestKeyRewardRequest {
 | 
			
		||||
interface IQuestKeyRewardRequest {
 | 
			
		||||
    reward: IQuestKeyReward;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IQuestKeyReward {
 | 
			
		||||
interface IQuestKeyReward {
 | 
			
		||||
    RewardType: string;
 | 
			
		||||
    CouponType: string;
 | 
			
		||||
    Icon: string;
 | 
			
		||||
							
								
								
									
										20
									
								
								src/controllers/api/giveShipDecoAndLoreFragmentController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/controllers/api/giveShipDecoAndLoreFragmentController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { addLoreFragmentScans, addShipDecorations, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { ILoreFragmentScan, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const giveShipDecoAndLoreFragmentController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "LoreFragmentScans ShipDecorations");
 | 
			
		||||
    const data = getJSONfromString<IGiveShipDecoAndLoreFragmentRequest>(String(req.body));
 | 
			
		||||
    addLoreFragmentScans(inventory, data.LoreFragmentScans);
 | 
			
		||||
    addShipDecorations(inventory, data.ShipDecorations);
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IGiveShipDecoAndLoreFragmentRequest {
 | 
			
		||||
    LoreFragmentScans: ILoreFragmentScan[];
 | 
			
		||||
    ShipDecorations: ITypeCount[];
 | 
			
		||||
}
 | 
			
		||||
@ -1,20 +1,8 @@
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { InventoryDocumentProps } from "@/src/models/inventoryModels/inventoryModel";
 | 
			
		||||
import {
 | 
			
		||||
    addEquipment,
 | 
			
		||||
    addItem,
 | 
			
		||||
    addPowerSuit,
 | 
			
		||||
    combineInventoryChanges,
 | 
			
		||||
    getInventory,
 | 
			
		||||
    updateSlots
 | 
			
		||||
} from "@/src/services/inventoryService";
 | 
			
		||||
import { addStartingGear, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { IInventoryClient, IInventoryDatabase, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
import { TPartialStartingGear } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { HydratedDocument } from "mongoose";
 | 
			
		||||
 | 
			
		||||
type TPartialStartingGear = Pick<IInventoryClient, "LongGuns" | "Suits" | "Pistols" | "Melee">;
 | 
			
		||||
 | 
			
		||||
export const giveStartingGearController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -26,72 +14,3 @@ export const giveStartingGearController: RequestHandler = async (req, res) => {
 | 
			
		||||
 | 
			
		||||
    res.send(inventoryChanges);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
//TODO: RawUpgrades might need to return a LastAdded
 | 
			
		||||
const awakeningRewards = [
 | 
			
		||||
    "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem1",
 | 
			
		||||
    "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem2",
 | 
			
		||||
    "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem3",
 | 
			
		||||
    "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem4",
 | 
			
		||||
    "/Lotus/Types/Restoratives/LisetAutoHack",
 | 
			
		||||
    "/Lotus/Upgrades/Mods/Warframe/AvatarShieldMaxMod"
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const addStartingGear = async (
 | 
			
		||||
    inventory: HydratedDocument<IInventoryDatabase, InventoryDocumentProps>,
 | 
			
		||||
    startingGear: TPartialStartingGear | undefined = undefined
 | 
			
		||||
): Promise<IInventoryChanges> => {
 | 
			
		||||
    const { LongGuns, Pistols, Suits, Melee } = startingGear || {
 | 
			
		||||
        LongGuns: [{ ItemType: "/Lotus/Weapons/Tenno/Rifle/Rifle" }],
 | 
			
		||||
        Pistols: [{ ItemType: "/Lotus/Weapons/Tenno/Pistol/Pistol" }],
 | 
			
		||||
        Suits: [{ ItemType: "/Lotus/Powersuits/Excalibur/Excalibur" }],
 | 
			
		||||
        Melee: [{ ItemType: "/Lotus/Weapons/Tenno/Melee/LongSword/LongSword" }]
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    //TODO: properly merge weapon bin changes it is currently static here
 | 
			
		||||
    const inventoryChanges: IInventoryChanges = {};
 | 
			
		||||
    addEquipment(inventory, "LongGuns", LongGuns[0].ItemType, undefined, inventoryChanges);
 | 
			
		||||
    addEquipment(inventory, "Pistols", Pistols[0].ItemType, undefined, inventoryChanges);
 | 
			
		||||
    addEquipment(inventory, "Melee", Melee[0].ItemType, undefined, inventoryChanges);
 | 
			
		||||
    addPowerSuit(inventory, Suits[0].ItemType, inventoryChanges);
 | 
			
		||||
    addEquipment(
 | 
			
		||||
        inventory,
 | 
			
		||||
        "DataKnives",
 | 
			
		||||
        "/Lotus/Weapons/Tenno/HackingDevices/TnHackingDevice/TnHackingDeviceWeapon",
 | 
			
		||||
        undefined,
 | 
			
		||||
        inventoryChanges,
 | 
			
		||||
        { XP: 450_000 }
 | 
			
		||||
    );
 | 
			
		||||
    addEquipment(
 | 
			
		||||
        inventory,
 | 
			
		||||
        "Scoops",
 | 
			
		||||
        "/Lotus/Weapons/Tenno/Speedball/SpeedballWeaponTest",
 | 
			
		||||
        undefined,
 | 
			
		||||
        inventoryChanges
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    updateSlots(inventory, InventorySlot.SUITS, 0, 1);
 | 
			
		||||
    updateSlots(inventory, InventorySlot.WEAPONS, 0, 3);
 | 
			
		||||
    inventoryChanges.SuitBin = { count: 1, platinum: 0, Slots: -1 };
 | 
			
		||||
    inventoryChanges.WeaponBin = { count: 3, platinum: 0, Slots: -3 };
 | 
			
		||||
 | 
			
		||||
    await addItem(inventory, "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain");
 | 
			
		||||
    inventory.ActiveQuest = "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain";
 | 
			
		||||
 | 
			
		||||
    inventory.PremiumCredits = 50;
 | 
			
		||||
    inventory.PremiumCreditsFree = 50;
 | 
			
		||||
    inventoryChanges.PremiumCredits = 50;
 | 
			
		||||
    inventoryChanges.PremiumCreditsFree = 50;
 | 
			
		||||
    inventory.RegularCredits = 3000;
 | 
			
		||||
    inventoryChanges.RegularCredits = 3000;
 | 
			
		||||
 | 
			
		||||
    for (const item of awakeningRewards) {
 | 
			
		||||
        const inventoryDelta = await addItem(inventory, item);
 | 
			
		||||
        combineInventoryChanges(inventoryChanges, inventoryDelta);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    inventory.PlayedParkourTutorial = true;
 | 
			
		||||
    inventory.ReceivedStartingGear = true;
 | 
			
		||||
 | 
			
		||||
    return inventoryChanges;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,38 +1,46 @@
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import {
 | 
			
		||||
    addGuildMemberMiscItemContribution,
 | 
			
		||||
    getGuildForRequestEx,
 | 
			
		||||
    getGuildVault,
 | 
			
		||||
    hasAccessToDojo,
 | 
			
		||||
    hasGuildPermission,
 | 
			
		||||
    processFundedGuildTechProject,
 | 
			
		||||
    processGuildTechProjectContributionsUpdate,
 | 
			
		||||
    removePigmentsFromGuildMembers,
 | 
			
		||||
    scaleRequiredCount
 | 
			
		||||
    scaleRequiredCount,
 | 
			
		||||
    setGuildTechLogState
 | 
			
		||||
} from "@/src/services/guildService";
 | 
			
		||||
import { ExportDojoRecipes, IDojoResearch } from "warframe-public-export-plus";
 | 
			
		||||
import { ExportDojoRecipes } from "warframe-public-export-plus";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import {
 | 
			
		||||
    addCrewShipWeaponSkin,
 | 
			
		||||
    addEquipment,
 | 
			
		||||
    addItem,
 | 
			
		||||
    addMiscItems,
 | 
			
		||||
    addRecipes,
 | 
			
		||||
    combineInventoryChanges,
 | 
			
		||||
    getInventory,
 | 
			
		||||
    occupySlot,
 | 
			
		||||
    updateCurrency
 | 
			
		||||
} from "@/src/services/inventoryService";
 | 
			
		||||
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
import { config } from "@/src/services/configService";
 | 
			
		||||
import { GuildPermission, ITechProjectClient, ITechProjectDatabase } from "@/src/types/guildTypes";
 | 
			
		||||
import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel";
 | 
			
		||||
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
 | 
			
		||||
import { GuildPermission, ITechProjectClient } from "@/src/types/guildTypes";
 | 
			
		||||
import { GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
 | 
			
		||||
 | 
			
		||||
export const guildTechController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
    const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
    const data = JSON.parse(String(req.body)) as TGuildTechRequest;
 | 
			
		||||
    if (data.Action == "Sync") {
 | 
			
		||||
        let needSave = false;
 | 
			
		||||
        const techProjects: ITechProjectClient[] = [];
 | 
			
		||||
        const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
        if (guild.TechProjects) {
 | 
			
		||||
            for (const project of guild.TechProjects) {
 | 
			
		||||
                const techProject: ITechProjectClient = {
 | 
			
		||||
@ -44,7 +52,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
 | 
			
		||||
                if (project.CompletionDate) {
 | 
			
		||||
                    techProject.CompletionDate = toMongoDate(project.CompletionDate);
 | 
			
		||||
                    if (Date.now() >= project.CompletionDate.getTime()) {
 | 
			
		||||
                        needSave ||= setTechLogState(guild, project.ItemType, 4, project.CompletionDate);
 | 
			
		||||
                        needSave ||= setGuildTechLogState(guild, project.ItemType, 4, project.CompletionDate);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                techProjects.push(techProject);
 | 
			
		||||
@ -55,6 +63,8 @@ export const guildTechController: RequestHandler = async (req, res) => {
 | 
			
		||||
        }
 | 
			
		||||
        res.json({ TechProjects: techProjects });
 | 
			
		||||
    } else if (data.Action == "Start") {
 | 
			
		||||
        if (data.Mode == "Guild") {
 | 
			
		||||
            const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
            if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
 | 
			
		||||
                res.status(400).send("-1").end();
 | 
			
		||||
                return;
 | 
			
		||||
@ -66,17 +76,17 @@ export const guildTechController: RequestHandler = async (req, res) => {
 | 
			
		||||
                    guild.TechProjects[
 | 
			
		||||
                        guild.TechProjects.push({
 | 
			
		||||
                            ItemType: data.RecipeType,
 | 
			
		||||
                        ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(recipe.price),
 | 
			
		||||
                            ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price),
 | 
			
		||||
                            ReqItems: recipe.ingredients.map(x => ({
 | 
			
		||||
                                ItemType: x.ItemType,
 | 
			
		||||
                            ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(x.ItemCount)
 | 
			
		||||
                                ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount)
 | 
			
		||||
                            })),
 | 
			
		||||
                            State: 0
 | 
			
		||||
                        }) - 1
 | 
			
		||||
                    ];
 | 
			
		||||
            setTechLogState(guild, techProject.ItemType, 5);
 | 
			
		||||
                setGuildTechLogState(guild, techProject.ItemType, 5);
 | 
			
		||||
                if (config.noDojoResearchCosts) {
 | 
			
		||||
                processFundedProject(guild, techProject, recipe);
 | 
			
		||||
                    processFundedGuildTechProject(guild, techProject, recipe);
 | 
			
		||||
                } else {
 | 
			
		||||
                    if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") {
 | 
			
		||||
                        guild.ActiveDojoColorResearch = data.RecipeType;
 | 
			
		||||
@ -85,38 +95,109 @@ export const guildTechController: RequestHandler = async (req, res) => {
 | 
			
		||||
            }
 | 
			
		||||
            await guild.save();
 | 
			
		||||
            res.end();
 | 
			
		||||
        } else {
 | 
			
		||||
            const recipe = ExportDojoRecipes.research[data.RecipeType];
 | 
			
		||||
            if (data.TechProductCategory) {
 | 
			
		||||
                if (
 | 
			
		||||
                    data.TechProductCategory != "CrewShipWeapons" &&
 | 
			
		||||
                    data.TechProductCategory != "CrewShipWeaponSkins"
 | 
			
		||||
                ) {
 | 
			
		||||
                    throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`);
 | 
			
		||||
                }
 | 
			
		||||
                if (!inventory[getSalvageCategory(data.TechProductCategory)].id(data.CategoryItemId)) {
 | 
			
		||||
                    throw new Error(
 | 
			
		||||
                        `no item with id ${data.CategoryItemId} in ${getSalvageCategory(data.TechProductCategory)} array`
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            const techProject =
 | 
			
		||||
                inventory.PersonalTechProjects[
 | 
			
		||||
                    inventory.PersonalTechProjects.push({
 | 
			
		||||
                        State: 0,
 | 
			
		||||
                        ReqCredits: recipe.price,
 | 
			
		||||
                        ItemType: data.RecipeType,
 | 
			
		||||
                        ProductCategory: data.TechProductCategory,
 | 
			
		||||
                        CategoryItemId: data.CategoryItemId,
 | 
			
		||||
                        ReqItems: recipe.ingredients
 | 
			
		||||
                    }) - 1
 | 
			
		||||
                ];
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            res.json({
 | 
			
		||||
                isPersonal: true,
 | 
			
		||||
                action: "Start",
 | 
			
		||||
                personalTech: techProject.toJSON()
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    } else if (data.Action == "Contribute") {
 | 
			
		||||
        if ((req.query.guildId as string) == "000000000000000000000000") {
 | 
			
		||||
            const techProject = inventory.PersonalTechProjects.id(data.ResearchId)!;
 | 
			
		||||
 | 
			
		||||
            techProject.ReqCredits -= data.RegularCredits;
 | 
			
		||||
            const inventoryChanges: IInventoryChanges = updateCurrency(inventory, data.RegularCredits, false);
 | 
			
		||||
 | 
			
		||||
            const miscItemChanges = [];
 | 
			
		||||
            for (const miscItem of data.MiscItems) {
 | 
			
		||||
                const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
 | 
			
		||||
                if (reqItem) {
 | 
			
		||||
                    if (miscItem.ItemCount > reqItem.ItemCount) {
 | 
			
		||||
                        miscItem.ItemCount = reqItem.ItemCount;
 | 
			
		||||
                    }
 | 
			
		||||
                    reqItem.ItemCount -= miscItem.ItemCount;
 | 
			
		||||
                    miscItemChanges.push({
 | 
			
		||||
                        ItemType: miscItem.ItemType,
 | 
			
		||||
                        ItemCount: miscItem.ItemCount * -1
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            addMiscItems(inventory, miscItemChanges);
 | 
			
		||||
            inventoryChanges.MiscItems = miscItemChanges;
 | 
			
		||||
 | 
			
		||||
            techProject.HasContributions = true;
 | 
			
		||||
 | 
			
		||||
            if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) {
 | 
			
		||||
                techProject.State = 1;
 | 
			
		||||
                const recipe = ExportDojoRecipes.research[techProject.ItemType];
 | 
			
		||||
                techProject.CompletionDate = new Date(Date.now() + recipe.time * 1000);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            res.json({
 | 
			
		||||
                InventoryChanges: inventoryChanges,
 | 
			
		||||
                PersonalResearch: { $oid: data.ResearchId },
 | 
			
		||||
                PersonalResearchDate: techProject.CompletionDate ? toMongoDate(techProject.CompletionDate) : undefined
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            if (!hasAccessToDojo(inventory)) {
 | 
			
		||||
                res.status(400).send("-1").end();
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
            const guildMember = (await GuildMember.findOne(
 | 
			
		||||
                { accountId, guildId: guild._id },
 | 
			
		||||
                "RegularCreditsContributed MiscItemsContributed"
 | 
			
		||||
            ))!;
 | 
			
		||||
 | 
			
		||||
        const contributions = data;
 | 
			
		||||
        const techProject = guild.TechProjects!.find(x => x.ItemType == contributions.RecipeType)!;
 | 
			
		||||
            const techProject = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!;
 | 
			
		||||
 | 
			
		||||
        if (contributions.VaultCredits) {
 | 
			
		||||
            if (contributions.VaultCredits > techProject.ReqCredits) {
 | 
			
		||||
                contributions.VaultCredits = techProject.ReqCredits;
 | 
			
		||||
            if (data.VaultCredits) {
 | 
			
		||||
                if (data.VaultCredits > techProject.ReqCredits) {
 | 
			
		||||
                    data.VaultCredits = techProject.ReqCredits;
 | 
			
		||||
                }
 | 
			
		||||
            techProject.ReqCredits -= contributions.VaultCredits;
 | 
			
		||||
            guild.VaultRegularCredits! -= contributions.VaultCredits;
 | 
			
		||||
                techProject.ReqCredits -= data.VaultCredits;
 | 
			
		||||
                guild.VaultRegularCredits! -= data.VaultCredits;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        if (contributions.RegularCredits > techProject.ReqCredits) {
 | 
			
		||||
            contributions.RegularCredits = techProject.ReqCredits;
 | 
			
		||||
            if (data.RegularCredits > techProject.ReqCredits) {
 | 
			
		||||
                data.RegularCredits = techProject.ReqCredits;
 | 
			
		||||
            }
 | 
			
		||||
        techProject.ReqCredits -= contributions.RegularCredits;
 | 
			
		||||
            techProject.ReqCredits -= data.RegularCredits;
 | 
			
		||||
 | 
			
		||||
            guildMember.RegularCreditsContributed ??= 0;
 | 
			
		||||
        guildMember.RegularCreditsContributed += contributions.RegularCredits;
 | 
			
		||||
            guildMember.RegularCreditsContributed += data.RegularCredits;
 | 
			
		||||
 | 
			
		||||
        if (contributions.VaultMiscItems.length) {
 | 
			
		||||
            for (const miscItem of contributions.VaultMiscItems) {
 | 
			
		||||
            if (data.VaultMiscItems.length) {
 | 
			
		||||
                for (const miscItem of data.VaultMiscItems) {
 | 
			
		||||
                    const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
 | 
			
		||||
                    if (reqItem) {
 | 
			
		||||
                        if (miscItem.ItemCount > reqItem.ItemCount) {
 | 
			
		||||
@ -131,7 +212,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const miscItemChanges = [];
 | 
			
		||||
        for (const miscItem of contributions.MiscItems) {
 | 
			
		||||
            for (const miscItem of data.MiscItems) {
 | 
			
		||||
                const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
 | 
			
		||||
                if (reqItem) {
 | 
			
		||||
                    if (miscItem.ItemCount > reqItem.ItemCount) {
 | 
			
		||||
@ -143,37 +224,33 @@ export const guildTechController: RequestHandler = async (req, res) => {
 | 
			
		||||
                        ItemCount: miscItem.ItemCount * -1
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                guildMember.MiscItemsContributed ??= [];
 | 
			
		||||
                guildMember.MiscItemsContributed.push(miscItem);
 | 
			
		||||
                    addGuildMemberMiscItemContribution(guildMember, miscItem);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            addMiscItems(inventory, miscItemChanges);
 | 
			
		||||
        const inventoryChanges: IInventoryChanges = updateCurrency(inventory, contributions.RegularCredits, false);
 | 
			
		||||
            const inventoryChanges: IInventoryChanges = updateCurrency(inventory, data.RegularCredits, false);
 | 
			
		||||
            inventoryChanges.MiscItems = miscItemChanges;
 | 
			
		||||
 | 
			
		||||
        if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) {
 | 
			
		||||
            // This research is now fully funded.
 | 
			
		||||
            const recipe = ExportDojoRecipes.research[data.RecipeType];
 | 
			
		||||
            processFundedProject(guild, techProject, recipe);
 | 
			
		||||
            if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") {
 | 
			
		||||
                guild.ActiveDojoColorResearch = "";
 | 
			
		||||
                await removePigmentsFromGuildMembers(guild._id);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
            // Check if research is fully funded now.
 | 
			
		||||
            await processGuildTechProjectContributionsUpdate(guild, techProject);
 | 
			
		||||
 | 
			
		||||
        await guild.save();
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
        await guildMember.save();
 | 
			
		||||
            await Promise.all([guild.save(), inventory.save(), guildMember.save()]);
 | 
			
		||||
            res.json({
 | 
			
		||||
                InventoryChanges: inventoryChanges,
 | 
			
		||||
                Vault: getGuildVault(guild)
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    } else if (data.Action.split(",")[0] == "Buy") {
 | 
			
		||||
        if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) {
 | 
			
		||||
        const purchase = data as IGuildTechBuyRequest;
 | 
			
		||||
        if (purchase.Mode == "Guild") {
 | 
			
		||||
            const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
            if (
 | 
			
		||||
                !hasAccessToDojo(inventory) ||
 | 
			
		||||
                !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))
 | 
			
		||||
            ) {
 | 
			
		||||
                res.status(400).send("-1").end();
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        const purchase = data as IGuildTechBuyRequest;
 | 
			
		||||
            const quantity = parseInt(data.Action.split(",")[1]);
 | 
			
		||||
            const recipeChanges = [
 | 
			
		||||
                {
 | 
			
		||||
@ -195,7 +272,15 @@ export const guildTechController: RequestHandler = async (req, res) => {
 | 
			
		||||
                    Recipes: recipeChanges
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            const inventoryChanges = claimSalvagedComponent(inventory, purchase.CategoryItemId!);
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            res.json({
 | 
			
		||||
                inventoryChanges: inventoryChanges
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    } else if (data.Action == "Fabricate") {
 | 
			
		||||
        const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
        if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) {
 | 
			
		||||
            res.status(400).send("-1").end();
 | 
			
		||||
            return;
 | 
			
		||||
@ -212,6 +297,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
 | 
			
		||||
        // Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`.
 | 
			
		||||
        res.json({ inventoryChanges: inventoryChanges });
 | 
			
		||||
    } else if (data.Action == "Pause") {
 | 
			
		||||
        const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
        if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
 | 
			
		||||
            res.status(400).send("-1").end();
 | 
			
		||||
            return;
 | 
			
		||||
@ -223,6 +309,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
 | 
			
		||||
        await removePigmentsFromGuildMembers(guild._id);
 | 
			
		||||
        res.end();
 | 
			
		||||
    } else if (data.Action == "Unpause") {
 | 
			
		||||
        const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
        if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
 | 
			
		||||
            res.status(400).send("-1").end();
 | 
			
		||||
            return;
 | 
			
		||||
@ -232,47 +319,51 @@ export const guildTechController: RequestHandler = async (req, res) => {
 | 
			
		||||
        guild.ActiveDojoColorResearch = data.RecipeType;
 | 
			
		||||
        await guild.save();
 | 
			
		||||
        res.end();
 | 
			
		||||
    } else {
 | 
			
		||||
        logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
 | 
			
		||||
        throw new Error(`unknown guildTech action: ${data.Action}`);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
    } else if (data.Action == "Cancel" && data.CategoryItemId) {
 | 
			
		||||
        const personalTechProjectIndex = inventory.PersonalTechProjects.findIndex(x =>
 | 
			
		||||
            x.CategoryItemId?.equals(data.CategoryItemId)
 | 
			
		||||
        );
 | 
			
		||||
        const personalTechProject = inventory.PersonalTechProjects[personalTechProjectIndex];
 | 
			
		||||
        inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1);
 | 
			
		||||
 | 
			
		||||
const processFundedProject = (
 | 
			
		||||
    guild: TGuildDatabaseDocument,
 | 
			
		||||
    techProject: ITechProjectDatabase,
 | 
			
		||||
    recipe: IDojoResearch
 | 
			
		||||
): void => {
 | 
			
		||||
    techProject.State = 1;
 | 
			
		||||
    techProject.CompletionDate = new Date(Date.now() + (config.noDojoResearchTime ? 0 : recipe.time) * 1000);
 | 
			
		||||
    if (recipe.guildXpValue) {
 | 
			
		||||
        guild.XP += recipe.guildXpValue;
 | 
			
		||||
    }
 | 
			
		||||
    setTechLogState(guild, techProject.ItemType, config.noDojoResearchTime ? 4 : 3, techProject.CompletionDate);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const setTechLogState = (
 | 
			
		||||
    guild: TGuildDatabaseDocument,
 | 
			
		||||
    type: string,
 | 
			
		||||
    state: number,
 | 
			
		||||
    dateTime: Date | undefined = undefined
 | 
			
		||||
): boolean => {
 | 
			
		||||
    guild.TechChanges ??= [];
 | 
			
		||||
    const entry = guild.TechChanges.find(x => x.details == type);
 | 
			
		||||
    if (entry) {
 | 
			
		||||
        if (entry.entryType == state) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        entry.dateTime = dateTime;
 | 
			
		||||
        entry.entryType = state;
 | 
			
		||||
    } else {
 | 
			
		||||
        guild.TechChanges.push({
 | 
			
		||||
            dateTime: dateTime,
 | 
			
		||||
            entryType: state,
 | 
			
		||||
            details: type
 | 
			
		||||
        const meta = ExportDojoRecipes.research[personalTechProject.ItemType];
 | 
			
		||||
        const contributedCredits = meta.price - personalTechProject.ReqCredits;
 | 
			
		||||
        const inventoryChanges = updateCurrency(inventory, contributedCredits * -1, false);
 | 
			
		||||
        inventoryChanges.MiscItems = [];
 | 
			
		||||
        for (const ingredient of meta.ingredients) {
 | 
			
		||||
            const reqItem = personalTechProject.ReqItems.find(x => x.ItemType == ingredient.ItemType);
 | 
			
		||||
            if (reqItem) {
 | 
			
		||||
                const contributedItems = ingredient.ItemCount - reqItem.ItemCount;
 | 
			
		||||
                inventoryChanges.MiscItems.push({
 | 
			
		||||
                    ItemType: ingredient.ItemType,
 | 
			
		||||
                    ItemCount: contributedItems
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
    return true;
 | 
			
		||||
        }
 | 
			
		||||
        addMiscItems(inventory, inventoryChanges.MiscItems);
 | 
			
		||||
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
        res.json({
 | 
			
		||||
            action: "Cancel",
 | 
			
		||||
            isPersonal: true,
 | 
			
		||||
            inventoryChanges: inventoryChanges,
 | 
			
		||||
            personalTech: {
 | 
			
		||||
                ItemId: toOid(personalTechProject._id)
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    } else if (data.Action == "Rush" && data.CategoryItemId) {
 | 
			
		||||
        const inventoryChanges: IInventoryChanges = {
 | 
			
		||||
            ...updateCurrency(inventory, 20, true),
 | 
			
		||||
            ...claimSalvagedComponent(inventory, data.CategoryItemId)
 | 
			
		||||
        };
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
        res.json({
 | 
			
		||||
            inventoryChanges: inventoryChanges
 | 
			
		||||
        });
 | 
			
		||||
    } else {
 | 
			
		||||
        logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
 | 
			
		||||
        throw new Error(`unhandled guildTech request`);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type TGuildTechRequest =
 | 
			
		||||
@ -281,23 +372,69 @@ type TGuildTechRequest =
 | 
			
		||||
    | IGuildTechContributeRequest;
 | 
			
		||||
 | 
			
		||||
interface IGuildTechBasicRequest {
 | 
			
		||||
    Action: "Start" | "Fabricate" | "Pause" | "Unpause";
 | 
			
		||||
    Mode: "Guild";
 | 
			
		||||
    Action: "Start" | "Fabricate" | "Pause" | "Unpause" | "Cancel" | "Rush";
 | 
			
		||||
    Mode: "Guild" | "Personal";
 | 
			
		||||
    RecipeType: string;
 | 
			
		||||
    TechProductCategory?: string;
 | 
			
		||||
    CategoryItemId?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IGuildTechBuyRequest {
 | 
			
		||||
interface IGuildTechBuyRequest extends Omit<IGuildTechBasicRequest, "Action"> {
 | 
			
		||||
    Action: string;
 | 
			
		||||
    Mode: "Guild";
 | 
			
		||||
    RecipeType: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IGuildTechContributeRequest {
 | 
			
		||||
    Action: "Contribute";
 | 
			
		||||
    ResearchId: "";
 | 
			
		||||
    ResearchId: string;
 | 
			
		||||
    RecipeType: string;
 | 
			
		||||
    RegularCredits: number;
 | 
			
		||||
    MiscItems: IMiscItem[];
 | 
			
		||||
    VaultCredits: number;
 | 
			
		||||
    VaultMiscItems: IMiscItem[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const getSalvageCategory = (
 | 
			
		||||
    category: "CrewShipWeapons" | "CrewShipWeaponSkins"
 | 
			
		||||
): "CrewShipSalvagedWeapons" | "CrewShipSalvagedWeaponSkins" => {
 | 
			
		||||
    return category == "CrewShipWeapons" ? "CrewShipSalvagedWeapons" : "CrewShipSalvagedWeaponSkins";
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const claimSalvagedComponent = (inventory: TInventoryDatabaseDocument, itemId: string): IInventoryChanges => {
 | 
			
		||||
    // delete personal tech project
 | 
			
		||||
    const personalTechProjectIndex = inventory.PersonalTechProjects.findIndex(x => x.CategoryItemId?.equals(itemId));
 | 
			
		||||
    const personalTechProject = inventory.PersonalTechProjects[personalTechProjectIndex];
 | 
			
		||||
    inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1);
 | 
			
		||||
 | 
			
		||||
    const category = personalTechProject.ProductCategory! as "CrewShipWeapons" | "CrewShipWeaponSkins";
 | 
			
		||||
    const salvageCategory = getSalvageCategory(category);
 | 
			
		||||
 | 
			
		||||
    // find salved part & delete it
 | 
			
		||||
    const salvageIndex = inventory[salvageCategory].findIndex(x => x._id.equals(itemId));
 | 
			
		||||
    const salvageItem = inventory[category][salvageIndex];
 | 
			
		||||
    inventory[salvageCategory].splice(salvageIndex, 1);
 | 
			
		||||
 | 
			
		||||
    // add final item
 | 
			
		||||
    const inventoryChanges = {
 | 
			
		||||
        ...(category == "CrewShipWeaponSkins"
 | 
			
		||||
            ? addCrewShipWeaponSkin(inventory, salvageItem.ItemType, salvageItem.UpgradeFingerprint)
 | 
			
		||||
            : addEquipment(
 | 
			
		||||
                  inventory,
 | 
			
		||||
                  category,
 | 
			
		||||
                  salvageItem.ItemType,
 | 
			
		||||
                  undefined,
 | 
			
		||||
                  {},
 | 
			
		||||
                  {
 | 
			
		||||
                      UpgradeFingerprint: salvageItem.UpgradeFingerprint
 | 
			
		||||
                  }
 | 
			
		||||
              )),
 | 
			
		||||
        ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false)
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    inventoryChanges.RemovedIdItems = [
 | 
			
		||||
        {
 | 
			
		||||
            ItemId: { $oid: itemId }
 | 
			
		||||
        }
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    return inventoryChanges;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -34,8 +34,8 @@ export const inboxController: RequestHandler = async (req, res) => {
 | 
			
		||||
        message.r = true;
 | 
			
		||||
        await message.save();
 | 
			
		||||
 | 
			
		||||
        const attachmentItems = message.att;
 | 
			
		||||
        const attachmentCountedItems = message.countedAtt;
 | 
			
		||||
        const attachmentItems = message.attVisualOnly ? undefined : message.att;
 | 
			
		||||
        const attachmentCountedItems = message.attVisualOnly ? undefined : message.countedAtt;
 | 
			
		||||
 | 
			
		||||
        if (!attachmentItems && !attachmentCountedItems && !message.gifts) {
 | 
			
		||||
            res.status(200).end();
 | 
			
		||||
@ -67,7 +67,7 @@ export const inboxController: RequestHandler = async (req, res) => {
 | 
			
		||||
                    (await handleStoreItemAcquisition(gift.GiftType, inventory, giftQuantity)).InventoryChanges
 | 
			
		||||
                );
 | 
			
		||||
                if (sender) {
 | 
			
		||||
                    await createMessage(sender._id.toString(), [
 | 
			
		||||
                    await createMessage(sender._id, [
 | 
			
		||||
                        {
 | 
			
		||||
                            sndr: recipientName,
 | 
			
		||||
                            msg: "/Lotus/Language/Menu/GiftReceivedConfirmationBody",
 | 
			
		||||
 | 
			
		||||
@ -6,20 +6,21 @@ import { IOid } from "@/src/types/commonTypes";
 | 
			
		||||
import {
 | 
			
		||||
    IConsumedSuit,
 | 
			
		||||
    IHelminthFoodRecord,
 | 
			
		||||
    IInfestedFoundryClient,
 | 
			
		||||
    IInfestedFoundryDatabase,
 | 
			
		||||
    IInventoryClient,
 | 
			
		||||
    IMiscItem,
 | 
			
		||||
    InventorySlot,
 | 
			
		||||
    ITypeCount
 | 
			
		||||
    InventorySlot
 | 
			
		||||
} from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { ExportMisc, ExportRecipes } from "warframe-public-export-plus";
 | 
			
		||||
import { ExportMisc } from "warframe-public-export-plus";
 | 
			
		||||
import { getRecipe } from "@/src/services/itemDataService";
 | 
			
		||||
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
 | 
			
		||||
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { colorToShard } from "@/src/helpers/shardHelper";
 | 
			
		||||
import { config } from "@/src/services/configService";
 | 
			
		||||
import {
 | 
			
		||||
    addInfestedFoundryXP,
 | 
			
		||||
    applyCheatsToInfestedFoundry,
 | 
			
		||||
    handleSubsumeCompletion
 | 
			
		||||
} from "@/src/services/infestedFoundryService";
 | 
			
		||||
 | 
			
		||||
export const infestedFoundryController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -28,7 +29,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
 | 
			
		||||
            // shard installation
 | 
			
		||||
            const request = getJSONfromString<IShardInstallRequest>(String(req.body));
 | 
			
		||||
            const inventory = await getInventory(accountId);
 | 
			
		||||
            const suit = inventory.Suits.find(suit => suit._id.toString() == request.SuitId.$oid)!;
 | 
			
		||||
            const suit = inventory.Suits.id(request.SuitId.$oid)!;
 | 
			
		||||
            if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) {
 | 
			
		||||
                suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}];
 | 
			
		||||
            }
 | 
			
		||||
@ -56,7 +57,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
 | 
			
		||||
            // shard removal
 | 
			
		||||
            const request = getJSONfromString<IShardUninstallRequest>(String(req.body));
 | 
			
		||||
            const inventory = await getInventory(accountId);
 | 
			
		||||
            const suit = inventory.Suits.find(suit => suit._id.toString() == request.SuitId.$oid)!;
 | 
			
		||||
            const suit = inventory.Suits.id(request.SuitId.$oid)!;
 | 
			
		||||
 | 
			
		||||
            const miscItemChanges: IMiscItem[] = [];
 | 
			
		||||
            if (suit.ArchonCrystalUpgrades![request.Slot].Color) {
 | 
			
		||||
@ -383,116 +384,11 @@ interface IHelminthFeedRequest {
 | 
			
		||||
    }[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const addInfestedFoundryXP = (infestedFoundry: IInfestedFoundryDatabase, delta: number): ITypeCount[] => {
 | 
			
		||||
    const recipeChanges: ITypeCount[] = [];
 | 
			
		||||
    infestedFoundry.XP ??= 0;
 | 
			
		||||
    const prevXP = infestedFoundry.XP;
 | 
			
		||||
    infestedFoundry.XP += delta;
 | 
			
		||||
    if (prevXP < 2250_00 && infestedFoundry.XP >= 2250_00) {
 | 
			
		||||
        infestedFoundry.Slots ??= 0;
 | 
			
		||||
        infestedFoundry.Slots += 3;
 | 
			
		||||
    }
 | 
			
		||||
    if (prevXP < 5625_00 && infestedFoundry.XP >= 5625_00) {
 | 
			
		||||
        recipeChanges.push({
 | 
			
		||||
            ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthShieldsBlueprint",
 | 
			
		||||
            ItemCount: 1
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    if (prevXP < 10125_00 && infestedFoundry.XP >= 10125_00) {
 | 
			
		||||
        recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthHackBlueprint", ItemCount: 1 });
 | 
			
		||||
    }
 | 
			
		||||
    if (prevXP < 15750_00 && infestedFoundry.XP >= 15750_00) {
 | 
			
		||||
        infestedFoundry.Slots ??= 0;
 | 
			
		||||
        infestedFoundry.Slots += 10;
 | 
			
		||||
    }
 | 
			
		||||
    if (prevXP < 22500_00 && infestedFoundry.XP >= 22500_00) {
 | 
			
		||||
        recipeChanges.push({
 | 
			
		||||
            ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthAmmoEfficiencyBlueprint",
 | 
			
		||||
            ItemCount: 1
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    if (prevXP < 30375_00 && infestedFoundry.XP >= 30375_00) {
 | 
			
		||||
        recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthStunBlueprint", ItemCount: 1 });
 | 
			
		||||
    }
 | 
			
		||||
    if (prevXP < 39375_00 && infestedFoundry.XP >= 39375_00) {
 | 
			
		||||
        infestedFoundry.Slots ??= 0;
 | 
			
		||||
        infestedFoundry.Slots += 20;
 | 
			
		||||
    }
 | 
			
		||||
    if (prevXP < 60750_00 && infestedFoundry.XP >= 60750_00) {
 | 
			
		||||
        recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthStatusBlueprint", ItemCount: 1 });
 | 
			
		||||
    }
 | 
			
		||||
    if (prevXP < 73125_00 && infestedFoundry.XP >= 73125_00) {
 | 
			
		||||
        infestedFoundry.Slots = 1;
 | 
			
		||||
    }
 | 
			
		||||
    if (prevXP < 86625_00 && infestedFoundry.XP >= 86625_00) {
 | 
			
		||||
        recipeChanges.push({
 | 
			
		||||
            ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthShieldArmorBlueprint",
 | 
			
		||||
            ItemCount: 1
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    if (prevXP < 101250_00 && infestedFoundry.XP >= 101250_00) {
 | 
			
		||||
        recipeChanges.push({
 | 
			
		||||
            ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthProcBlockBlueprint",
 | 
			
		||||
            ItemCount: 1
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    if (prevXP < 117000_00 && infestedFoundry.XP >= 117000_00) {
 | 
			
		||||
        recipeChanges.push({
 | 
			
		||||
            ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthEnergyShareBlueprint",
 | 
			
		||||
            ItemCount: 1
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    if (prevXP < 133875_00 && infestedFoundry.XP >= 133875_00) {
 | 
			
		||||
        recipeChanges.push({
 | 
			
		||||
            ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthMaxStatusBlueprint",
 | 
			
		||||
            ItemCount: 1
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    if (prevXP < 151875_00 && infestedFoundry.XP >= 151875_00) {
 | 
			
		||||
        recipeChanges.push({
 | 
			
		||||
            ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthTreasureBlueprint",
 | 
			
		||||
            ItemCount: 1
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    return recipeChanges;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IHelminthSubsumeRequest {
 | 
			
		||||
    SuitId: IOid;
 | 
			
		||||
    Recipe: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const handleSubsumeCompletion = (inventory: TInventoryDatabaseDocument): ITypeCount[] => {
 | 
			
		||||
    const [recipeType] = Object.entries(ExportRecipes).find(
 | 
			
		||||
        ([_recipeType, recipe]) =>
 | 
			
		||||
            recipe.secretIngredientAction == "SIA_WARFRAME_ABILITY" &&
 | 
			
		||||
            recipe.secretIngredients![0].ItemType == inventory.InfestedFoundry!.LastConsumedSuit!.ItemType
 | 
			
		||||
    )!;
 | 
			
		||||
    inventory.InfestedFoundry!.LastConsumedSuit = undefined;
 | 
			
		||||
    inventory.InfestedFoundry!.AbilityOverrideUnlockCooldown = undefined;
 | 
			
		||||
    const recipeChanges: ITypeCount[] = [
 | 
			
		||||
        {
 | 
			
		||||
            ItemType: recipeType,
 | 
			
		||||
            ItemCount: 1
 | 
			
		||||
        }
 | 
			
		||||
    ];
 | 
			
		||||
    addRecipes(inventory, recipeChanges);
 | 
			
		||||
    return recipeChanges;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const applyCheatsToInfestedFoundry = (infestedFoundry: IInfestedFoundryClient): void => {
 | 
			
		||||
    if (config.infiniteHelminthMaterials) {
 | 
			
		||||
        infestedFoundry.Resources = [
 | 
			
		||||
            { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthCalx", Count: 1000 },
 | 
			
		||||
            { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthBiotics", Count: 1000 },
 | 
			
		||||
            { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthSynthetics", Count: 1000 },
 | 
			
		||||
            { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthPheromones", Count: 1000 },
 | 
			
		||||
            { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthBile", Count: 1000 },
 | 
			
		||||
            { ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthOxides", Count: 1000 }
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IHelminthOfferingsUpdate {
 | 
			
		||||
    OfferingsIndex: number;
 | 
			
		||||
    SuitTypes: string[];
 | 
			
		||||
 | 
			
		||||
@ -13,9 +13,10 @@ import {
 | 
			
		||||
    ExportResources,
 | 
			
		||||
    ExportVirtuals
 | 
			
		||||
} from "warframe-public-export-plus";
 | 
			
		||||
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "./infestedFoundryController";
 | 
			
		||||
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService";
 | 
			
		||||
import { addMiscItems, allDailyAffiliationKeys, createLibraryDailyTask } from "@/src/services/inventoryService";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { catBreadHash } from "@/src/helpers/stringHelpers";
 | 
			
		||||
 | 
			
		||||
export const inventoryController: RequestHandler = async (request, response) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(request);
 | 
			
		||||
@ -34,6 +35,7 @@ export const inventoryController: RequestHandler = async (request, response) =>
 | 
			
		||||
        }
 | 
			
		||||
        inventory.DailyFocus = 250000 + inventory.PlayerLevel * 5000;
 | 
			
		||||
        inventory.GiftsRemaining = Math.max(8, inventory.PlayerLevel);
 | 
			
		||||
        inventory.TradesRemaining = inventory.PlayerLevel;
 | 
			
		||||
 | 
			
		||||
        inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask();
 | 
			
		||||
 | 
			
		||||
@ -51,9 +53,11 @@ export const inventoryController: RequestHandler = async (request, response) =>
 | 
			
		||||
                    if (numArgonCrystals == 0) {
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
                    const numStableArgonCrystals =
 | 
			
		||||
                    const numStableArgonCrystals = Math.min(
 | 
			
		||||
                        numArgonCrystals,
 | 
			
		||||
                        inventory.FoundToday?.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal")
 | 
			
		||||
                            ?.ItemCount ?? 0;
 | 
			
		||||
                            ?.ItemCount ?? 0
 | 
			
		||||
                    );
 | 
			
		||||
                    const numDecayingArgonCrystals = numArgonCrystals - numStableArgonCrystals;
 | 
			
		||||
                    const numDecayingArgonCrystalsToRemove = Math.ceil(numDecayingArgonCrystals / 2);
 | 
			
		||||
                    logger.debug(`ticking argon crystals for day ${i + 1} of ${daysPassed}`, {
 | 
			
		||||
@ -145,7 +149,7 @@ export const getInventoryResponse = async (
 | 
			
		||||
        inventoryResponse.ShipDecorations = [];
 | 
			
		||||
        for (const [uniqueName, item] of Object.entries(ExportResources)) {
 | 
			
		||||
            if (item.productCategory == "ShipDecorations") {
 | 
			
		||||
                inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 1 });
 | 
			
		||||
                inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 999_999 });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -198,7 +202,8 @@ export const getInventoryResponse = async (
 | 
			
		||||
 | 
			
		||||
    if (config.universalPolarityEverywhere) {
 | 
			
		||||
        const Polarity: IPolarity[] = [];
 | 
			
		||||
        for (let i = 0; i != 12; ++i) {
 | 
			
		||||
        // 12 is needed for necramechs. 15 is needed for plexus/crewshipharness.
 | 
			
		||||
        for (let i = 0; i != 15; ++i) {
 | 
			
		||||
            Polarity.push({
 | 
			
		||||
                Slot: i,
 | 
			
		||||
                Value: ArtifactPolarity.Any
 | 
			
		||||
@ -253,6 +258,10 @@ export const getInventoryResponse = async (
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (config.noDailyFocusLimit) {
 | 
			
		||||
        inventoryResponse.DailyFocus = Math.max(999_999, 250000 + inventoryResponse.PlayerLevel * 5000);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (inventoryResponse.InfestedFoundry) {
 | 
			
		||||
        applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry);
 | 
			
		||||
    }
 | 
			
		||||
@ -266,7 +275,7 @@ export const getInventoryResponse = async (
 | 
			
		||||
    return inventoryResponse;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const addString = (arr: string[], str: string): void => {
 | 
			
		||||
const addString = (arr: string[], str: string): void => {
 | 
			
		||||
    if (!arr.find(x => x == str)) {
 | 
			
		||||
        arr.push(str);
 | 
			
		||||
    }
 | 
			
		||||
@ -296,13 +305,3 @@ const resourceGetParent = (resourceName: string): string | undefined => {
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 | 
			
		||||
    return ExportVirtuals[resourceName]?.parentName;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// This is FNV1a-32 except operating under modulus 2^31 because JavaScript is stinky and likes producing negative integers out of nowhere.
 | 
			
		||||
export const catBreadHash = (name: string): number => {
 | 
			
		||||
    let hash = 2166136261;
 | 
			
		||||
    for (let i = 0; i != name.length; ++i) {
 | 
			
		||||
        hash = (hash ^ name.charCodeAt(i)) & 0x7fffffff;
 | 
			
		||||
        hash = (hash * 16777619) & 0x7fffffff;
 | 
			
		||||
    }
 | 
			
		||||
    return hash;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -54,8 +54,12 @@ export const loginController: RequestHandler = async (request, response) => {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //email not found or incorrect password
 | 
			
		||||
    if (!account || !isCorrectPassword(loginRequest.password, account.password)) {
 | 
			
		||||
    if (!account) {
 | 
			
		||||
        response.status(400).json({ error: "unknown user" });
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!isCorrectPassword(loginRequest.password, account.password)) {
 | 
			
		||||
        response.status(400).json({ error: "incorrect login data" });
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,8 @@ import {
 | 
			
		||||
    claimLoginReward,
 | 
			
		||||
    getRandomLoginRewards,
 | 
			
		||||
    ILoginRewardsReponse,
 | 
			
		||||
    isLoginRewardAChoice
 | 
			
		||||
    isLoginRewardAChoice,
 | 
			
		||||
    setAccountGotLoginRewardToday
 | 
			
		||||
} from "@/src/services/loginRewardService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
 | 
			
		||||
@ -44,8 +45,11 @@ export const loginRewardsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    if (!isMilestoneDay && randomRewards.length == 1) {
 | 
			
		||||
        response.DailyTributeInfo.HasChosenReward = true;
 | 
			
		||||
        response.DailyTributeInfo.ChosenReward = randomRewards[0];
 | 
			
		||||
        response.DailyTributeInfo.NewInventory = await claimLoginReward(account, inventory, randomRewards[0]);
 | 
			
		||||
        response.DailyTributeInfo.NewInventory = await claimLoginReward(inventory, randomRewards[0]);
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
 | 
			
		||||
        setAccountGotLoginRewardToday(account);
 | 
			
		||||
        await account.save();
 | 
			
		||||
    }
 | 
			
		||||
    res.json(response);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,9 @@
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { claimLoginReward, getRandomLoginRewards } from "@/src/services/loginRewardService";
 | 
			
		||||
import {
 | 
			
		||||
    claimLoginReward,
 | 
			
		||||
    getRandomLoginRewards,
 | 
			
		||||
    setAccountGotLoginRewardToday
 | 
			
		||||
} from "@/src/services/loginRewardService";
 | 
			
		||||
import { getAccountForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
@ -28,9 +32,13 @@ export const loginRewardsSelectionController: RequestHandler = async (req, res)
 | 
			
		||||
    } else {
 | 
			
		||||
        const randomRewards = getRandomLoginRewards(account, inventory);
 | 
			
		||||
        chosenReward = randomRewards.find(x => x.StoreItemType == body.ChosenReward)!;
 | 
			
		||||
        inventoryChanges = await claimLoginReward(account, inventory, chosenReward);
 | 
			
		||||
        inventoryChanges = await claimLoginReward(inventory, chosenReward);
 | 
			
		||||
    }
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
 | 
			
		||||
    setAccountGotLoginRewardToday(account);
 | 
			
		||||
    await account.save();
 | 
			
		||||
 | 
			
		||||
    res.json({
 | 
			
		||||
        DailyTributeInfo: {
 | 
			
		||||
            NewInventory: inventoryChanges,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										27
									
								
								src/controllers/api/maturePetController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/controllers/api/maturePetController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const maturePetController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "KubrowPets");
 | 
			
		||||
    const data = getJSONfromString<IMaturePetRequest>(String(req.body));
 | 
			
		||||
    const details = inventory.KubrowPets.id(data.petId)!.Details!;
 | 
			
		||||
    details.IsPuppy = data.revert;
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json({
 | 
			
		||||
        petId: data.petId,
 | 
			
		||||
        updateCollar: true,
 | 
			
		||||
        armorSkins: ["", "", ""],
 | 
			
		||||
        furPatterns: data.revert
 | 
			
		||||
            ? ["", "", ""]
 | 
			
		||||
            : [details.DominantTraits.FurPattern, details.DominantTraits.FurPattern, details.DominantTraits.FurPattern],
 | 
			
		||||
        unmature: data.revert
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IMaturePetRequest {
 | 
			
		||||
    petId: string;
 | 
			
		||||
    revert: boolean;
 | 
			
		||||
}
 | 
			
		||||
@ -47,16 +47,21 @@ import { logger } from "@/src/utils/logger";
 | 
			
		||||
- [ ]  FpsSamples
 | 
			
		||||
*/
 | 
			
		||||
//move credit calc in here, return MissionRewards: [] if no reward info
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
			
		||||
export const missionInventoryUpdateController: RequestHandler = async (req, res): Promise<void> => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const missionReport = getJSONfromString<IMissionInventoryUpdateRequest>((req.body as string).toString());
 | 
			
		||||
    logger.debug("mission report:", missionReport);
 | 
			
		||||
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
    const firstCompletion = missionReport.SortieId
 | 
			
		||||
        ? inventory.CompletedSorties.indexOf(missionReport.SortieId) == -1
 | 
			
		||||
        : false;
 | 
			
		||||
    const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport);
 | 
			
		||||
 | 
			
		||||
    if (missionReport.MissionStatus !== "GS_SUCCESS") {
 | 
			
		||||
    if (
 | 
			
		||||
        missionReport.MissionStatus !== "GS_SUCCESS" &&
 | 
			
		||||
        !(missionReport.RewardInfo?.jobId || missionReport.RewardInfo?.challengeMissionId)
 | 
			
		||||
    ) {
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
        const inventoryResponse = await getInventoryResponse(inventory, true);
 | 
			
		||||
        res.json({
 | 
			
		||||
@ -66,7 +71,8 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const { MissionRewards, inventoryChanges, credits } = await addMissionRewards(inventory, missionReport);
 | 
			
		||||
    const { MissionRewards, inventoryChanges, credits, AffiliationMods, SyndicateXPItemReward } =
 | 
			
		||||
        await addMissionRewards(inventory, missionReport, firstCompletion);
 | 
			
		||||
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    const inventoryResponse = await getInventoryResponse(inventory, true);
 | 
			
		||||
@ -78,7 +84,9 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
 | 
			
		||||
        MissionRewards,
 | 
			
		||||
        ...credits,
 | 
			
		||||
        ...inventoryUpdates,
 | 
			
		||||
        FusionPoints: inventoryChanges?.FusionPoints
 | 
			
		||||
        FusionPoints: inventoryChanges?.FusionPoints,
 | 
			
		||||
        SyndicateXPItemReward,
 | 
			
		||||
        AffiliationMods
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,7 @@ import { getDefaultUpgrades } from "@/src/services/itemDataService";
 | 
			
		||||
import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper";
 | 
			
		||||
import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes";
 | 
			
		||||
import { getRandomInt } from "@/src/services/rngService";
 | 
			
		||||
import { ExportSentinels } from "warframe-public-export-plus";
 | 
			
		||||
import { ExportSentinels, IDefaultUpgrade } from "warframe-public-export-plus";
 | 
			
		||||
import { Status } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
 | 
			
		||||
interface IModularCraftRequest {
 | 
			
		||||
@ -34,10 +34,8 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res)
 | 
			
		||||
    const category = modularWeaponTypes[data.WeaponType];
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
 | 
			
		||||
    const defaultUpgrades = getDefaultUpgrades(data.Parts);
 | 
			
		||||
    const defaultOverwrites: Partial<IEquipmentDatabase> = {
 | 
			
		||||
        Configs: applyDefaultUpgrades(inventory, defaultUpgrades)
 | 
			
		||||
    };
 | 
			
		||||
    let defaultUpgrades: IDefaultUpgrade[] | undefined;
 | 
			
		||||
    const defaultOverwrites: Partial<IEquipmentDatabase> = {};
 | 
			
		||||
    const inventoryChanges: IInventoryChanges = {};
 | 
			
		||||
    if (category == "KubrowPets") {
 | 
			
		||||
        const traits = {
 | 
			
		||||
@ -129,10 +127,17 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res)
 | 
			
		||||
        // Only save mutagen & antigen in the ModularParts.
 | 
			
		||||
        defaultOverwrites.ModularParts = [data.Parts[1], data.Parts[2]];
 | 
			
		||||
 | 
			
		||||
        for (const specialItem of ExportSentinels[data.WeaponType].exalted!) {
 | 
			
		||||
        const meta = ExportSentinels[data.WeaponType];
 | 
			
		||||
 | 
			
		||||
        for (const specialItem of meta.exalted!) {
 | 
			
		||||
            addSpecialItem(inventory, specialItem, inventoryChanges);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        defaultUpgrades = meta.defaultUpgrades;
 | 
			
		||||
    } else {
 | 
			
		||||
        defaultUpgrades = getDefaultUpgrades(data.Parts);
 | 
			
		||||
    }
 | 
			
		||||
    defaultOverwrites.Configs = applyDefaultUpgrades(inventory, defaultUpgrades);
 | 
			
		||||
    addEquipment(inventory, category, data.WeaponType, data.Parts, inventoryChanges, defaultOverwrites);
 | 
			
		||||
    combineInventoryChanges(inventoryChanges, occupySlot(inventory, productCategoryToInventoryBin(category)!, false));
 | 
			
		||||
    if (defaultUpgrades) {
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,6 @@ export const modularWeaponSaleController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const partTypeToParts: Record<string, string[]> = {};
 | 
			
		||||
    for (const [uniqueName, data] of Object.entries(ExportWeapons)) {
 | 
			
		||||
        if (data.partType && data.premiumPrice) {
 | 
			
		||||
            // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 | 
			
		||||
            partTypeToParts[data.partType] ??= [];
 | 
			
		||||
            partTypeToParts[data.partType].push(uniqueName);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -12,15 +12,17 @@ export const nameWeaponController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
    const body = getJSONfromString<INameWeaponRequest>(String(req.body));
 | 
			
		||||
    const item = inventory[req.query.Category as string as TEquipmentKey].find(
 | 
			
		||||
        item => item._id.toString() == (req.query.ItemId as string)
 | 
			
		||||
    )!;
 | 
			
		||||
    const item = inventory[req.query.Category as string as TEquipmentKey].id(req.query.ItemId as string)!;
 | 
			
		||||
    if (body.ItemName != "") {
 | 
			
		||||
        item.ItemName = body.ItemName;
 | 
			
		||||
    } else {
 | 
			
		||||
        item.ItemName = undefined;
 | 
			
		||||
    }
 | 
			
		||||
    const currencyChanges = updateCurrency(inventory, "webui" in req.query ? 0 : 15, true);
 | 
			
		||||
    const currencyChanges = updateCurrency(
 | 
			
		||||
        inventory,
 | 
			
		||||
        req.query.Category == "Horses" || "webui" in req.query ? 0 : 15,
 | 
			
		||||
        true
 | 
			
		||||
    );
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json({
 | 
			
		||||
        InventoryChanges: currencyChanges
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,25 @@
 | 
			
		||||
import { getInfNodes, getNemesisPasscode } from "@/src/helpers/nemesisHelpers";
 | 
			
		||||
import {
 | 
			
		||||
    consumeModCharge,
 | 
			
		||||
    encodeNemesisGuess,
 | 
			
		||||
    getInfNodes,
 | 
			
		||||
    getNemesisPasscode,
 | 
			
		||||
    IKnifeResponse
 | 
			
		||||
} from "@/src/helpers/nemesisHelpers";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
 | 
			
		||||
import { freeUpSlot, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { SRng } from "@/src/services/rngService";
 | 
			
		||||
import { IMongoDate, IOid } from "@/src/types/commonTypes";
 | 
			
		||||
import { IInnateDamageFingerprint, InventorySlot, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
 | 
			
		||||
import {
 | 
			
		||||
    IInnateDamageFingerprint,
 | 
			
		||||
    InventorySlot,
 | 
			
		||||
    IUpgradeClient,
 | 
			
		||||
    IWeaponSkinClient,
 | 
			
		||||
    LoadoutIndex,
 | 
			
		||||
    TEquipmentKey
 | 
			
		||||
} from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
@ -18,7 +33,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
 | 
			
		||||
        const destFingerprint = JSON.parse(destWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint;
 | 
			
		||||
        const sourceFingerprint = JSON.parse(sourceWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint;
 | 
			
		||||
 | 
			
		||||
        // Upgrade destination damage type if desireed
 | 
			
		||||
        // Update destination damage type if desired
 | 
			
		||||
        if (body.UseSourceDmgType) {
 | 
			
		||||
            destFingerprint.buffs[0].Tag = sourceFingerprint.buffs[0].Tag;
 | 
			
		||||
        }
 | 
			
		||||
@ -27,7 +42,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
 | 
			
		||||
        const destDamage = 0.25 + (destFingerprint.buffs[0].Value / 0x3fffffff) * (0.6 - 0.25);
 | 
			
		||||
        const sourceDamage = 0.25 + (sourceFingerprint.buffs[0].Value / 0x3fffffff) * (0.6 - 0.25);
 | 
			
		||||
        let newDamage = Math.max(destDamage, sourceDamage) * 1.1;
 | 
			
		||||
        if (newDamage >= 0.58) {
 | 
			
		||||
        if (newDamage >= 0.5794998) {
 | 
			
		||||
            newDamage = 0.6;
 | 
			
		||||
        }
 | 
			
		||||
        destFingerprint.buffs[0].Value = Math.trunc(((newDamage - 0.25) / (0.6 - 0.25)) * 0x3fffffff);
 | 
			
		||||
@ -42,13 +57,14 @@ export const nemesisController: RequestHandler = async (req, res) => {
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
        res.json({
 | 
			
		||||
            InventoryChanges: {
 | 
			
		||||
                [body.Category]: [destWeapon.toJSON()]
 | 
			
		||||
                [body.Category]: [destWeapon.toJSON()],
 | 
			
		||||
                RemovedIdItems: [{ ItemId: body.SourceWeapon }]
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    } else if ((req.query.mode as string) == "p") {
 | 
			
		||||
        const inventory = await getInventory(accountId, "Nemesis");
 | 
			
		||||
        const body = getJSONfromString<INemesisPrespawnCheckRequest>(String(req.body));
 | 
			
		||||
        const passcode = getNemesisPasscode(inventory.Nemesis!.fp, inventory.Nemesis!.Faction);
 | 
			
		||||
        const passcode = getNemesisPasscode(inventory.Nemesis!);
 | 
			
		||||
        let guessResult = 0;
 | 
			
		||||
        if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
 | 
			
		||||
            for (let i = 0; i != 3; ++i) {
 | 
			
		||||
@ -65,6 +81,88 @@ export const nemesisController: RequestHandler = async (req, res) => {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        res.json({ GuessResult: guessResult });
 | 
			
		||||
    } else if (req.query.mode == "r") {
 | 
			
		||||
        const inventory = await getInventory(
 | 
			
		||||
            accountId,
 | 
			
		||||
            "Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades"
 | 
			
		||||
        );
 | 
			
		||||
        const body = getJSONfromString<INemesisRequiemRequest>(String(req.body));
 | 
			
		||||
        if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
 | 
			
		||||
            const guess: number[] = [body.guess & 0xf, (body.guess >> 4) & 0xf, (body.guess >> 8) & 0xf];
 | 
			
		||||
            const passcode = getNemesisPasscode(inventory.Nemesis!)[0];
 | 
			
		||||
 | 
			
		||||
            // Add to GuessHistory
 | 
			
		||||
            const result1 = passcode == guess[0] ? 0 : 1;
 | 
			
		||||
            const result2 = passcode == guess[1] ? 0 : 1;
 | 
			
		||||
            const result3 = passcode == guess[2] ? 0 : 1;
 | 
			
		||||
            inventory.Nemesis!.GuessHistory.push(
 | 
			
		||||
                encodeNemesisGuess(guess[0], result1, guess[1], result2, guess[2], result3)
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            // Increase antivirus
 | 
			
		||||
            let antivirusGain = 5;
 | 
			
		||||
            const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
 | 
			
		||||
            const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid);
 | 
			
		||||
            const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0;
 | 
			
		||||
            const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!;
 | 
			
		||||
            const response: IKnifeResponse = {};
 | 
			
		||||
            for (const upgrade of body.knife!.AttachedUpgrades) {
 | 
			
		||||
                switch (upgrade.ItemType) {
 | 
			
		||||
                    case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod":
 | 
			
		||||
                        antivirusGain += 10;
 | 
			
		||||
                        consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
 | 
			
		||||
                        break;
 | 
			
		||||
                    case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod":
 | 
			
		||||
                        antivirusGain += 10;
 | 
			
		||||
                        consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
 | 
			
		||||
                        break;
 | 
			
		||||
                    case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod": // Instant Secure
 | 
			
		||||
                        antivirusGain += 15;
 | 
			
		||||
                        consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
 | 
			
		||||
                        break;
 | 
			
		||||
                    case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod": // Immuno Shield
 | 
			
		||||
                        antivirusGain += 15;
 | 
			
		||||
                        consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
 | 
			
		||||
                        break;
 | 
			
		||||
                    case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod":
 | 
			
		||||
                        antivirusGain += 10;
 | 
			
		||||
                        consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
 | 
			
		||||
                        break;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            inventory.Nemesis!.HenchmenKilled += antivirusGain;
 | 
			
		||||
            if (inventory.Nemesis!.HenchmenKilled >= 100) {
 | 
			
		||||
                inventory.Nemesis!.HenchmenKilled = 100;
 | 
			
		||||
                inventory.Nemesis!.InfNodes = [
 | 
			
		||||
                    {
 | 
			
		||||
                        Node: "CrewBattleNode559",
 | 
			
		||||
                        Influence: 1
 | 
			
		||||
                    }
 | 
			
		||||
                ];
 | 
			
		||||
                inventory.Nemesis!.Weakened = true;
 | 
			
		||||
            } else {
 | 
			
		||||
                inventory.Nemesis!.InfNodes = getInfNodes("FC_INFESTATION", 0);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            res.json(response);
 | 
			
		||||
        } else {
 | 
			
		||||
            const passcode = getNemesisPasscode(inventory.Nemesis!);
 | 
			
		||||
            if (passcode[body.position] != body.guess) {
 | 
			
		||||
                res.end();
 | 
			
		||||
            } else {
 | 
			
		||||
                inventory.Nemesis!.Rank += 1;
 | 
			
		||||
                inventory.Nemesis!.InfNodes = getInfNodes(inventory.Nemesis!.Faction, inventory.Nemesis!.Rank);
 | 
			
		||||
                await inventory.save();
 | 
			
		||||
                res.json({ RankIncrease: 1 });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    } else if ((req.query.mode as string) == "rs") {
 | 
			
		||||
        // report spawn; POST but no application data in body
 | 
			
		||||
        const inventory = await getInventory(accountId, "Nemesis");
 | 
			
		||||
        inventory.Nemesis!.LastEnc = inventory.Nemesis!.MissionCount;
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
        res.json({ LastEnc: inventory.Nemesis!.LastEnc });
 | 
			
		||||
    } else if ((req.query.mode as string) == "s") {
 | 
			
		||||
        const inventory = await getInventory(accountId, "Nemesis");
 | 
			
		||||
        const body = getJSONfromString<INemesisStartRequest>(String(req.body));
 | 
			
		||||
@ -172,6 +270,20 @@ interface INemesisPrespawnCheckRequest {
 | 
			
		||||
    potency?: number[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface INemesisRequiemRequest {
 | 
			
		||||
    guess: number; // grn/crp: 4 bits | coda: 3x 4 bits
 | 
			
		||||
    position: number; // grn/crp: 0-2 | coda: 0
 | 
			
		||||
    // knife field provided for coda only
 | 
			
		||||
    knife?: {
 | 
			
		||||
        Item: IEquipmentClient;
 | 
			
		||||
        Skins: IWeaponSkinClient[];
 | 
			
		||||
        ModSlot: number;
 | 
			
		||||
        CustSlot: number;
 | 
			
		||||
        AttachedUpgrades: IUpgradeClient[];
 | 
			
		||||
        HiddenWhenHolstered: boolean;
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const kuvaLichVersionSixWeapons = [
 | 
			
		||||
    "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon",
 | 
			
		||||
    "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak",
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,19 @@
 | 
			
		||||
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService";
 | 
			
		||||
import {
 | 
			
		||||
    getDojoClient,
 | 
			
		||||
    getGuildForRequestEx,
 | 
			
		||||
    getVaultMiscItemCount,
 | 
			
		||||
    hasAccessToDojo,
 | 
			
		||||
    hasGuildPermission,
 | 
			
		||||
    processDojoBuildMaterialsGathered,
 | 
			
		||||
    scaleRequiredCount
 | 
			
		||||
} from "@/src/services/guildService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { Types } from "mongoose";
 | 
			
		||||
import { ExportDojoRecipes } from "warframe-public-export-plus";
 | 
			
		||||
import { ExportDojoRecipes, ExportResources } from "warframe-public-export-plus";
 | 
			
		||||
import { config } from "@/src/services/configService";
 | 
			
		||||
 | 
			
		||||
export const placeDecoInComponentController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -24,6 +33,11 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) =
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    component.Decos ??= [];
 | 
			
		||||
    if (request.MoveId) {
 | 
			
		||||
        const deco = component.Decos.find(x => x._id.equals(request.MoveId))!;
 | 
			
		||||
        deco.Pos = request.Pos;
 | 
			
		||||
        deco.Rot = request.Rot;
 | 
			
		||||
    } else {
 | 
			
		||||
        const deco =
 | 
			
		||||
            component.Decos[
 | 
			
		||||
                component.Decos.push({
 | 
			
		||||
@ -31,17 +45,61 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) =
 | 
			
		||||
                    Type: request.Type,
 | 
			
		||||
                    Pos: request.Pos,
 | 
			
		||||
                    Rot: request.Rot,
 | 
			
		||||
                Name: request.Name
 | 
			
		||||
                    Name: request.Name,
 | 
			
		||||
                    Sockets: request.Sockets
 | 
			
		||||
                }) - 1
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
        const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == request.Type);
 | 
			
		||||
        if (meta) {
 | 
			
		||||
            if (meta.capacityCost) {
 | 
			
		||||
                component.DecoCapacity -= meta.capacityCost;
 | 
			
		||||
            }
 | 
			
		||||
        if (meta.price == 0 && meta.ingredients.length == 0) {
 | 
			
		||||
        } else {
 | 
			
		||||
            const itemType = Object.entries(ExportResources).find(arr => arr[1].deco == deco.Type)![0];
 | 
			
		||||
            if (deco.Sockets !== undefined) {
 | 
			
		||||
                guild.VaultFusionTreasures!.find(x => x.ItemType == itemType && x.Sockets == deco.Sockets)!.ItemCount -=
 | 
			
		||||
                    1;
 | 
			
		||||
            } else {
 | 
			
		||||
                guild.VaultShipDecorations!.find(x => x.ItemType == itemType)!.ItemCount -= 1;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (deco.Type != "/Lotus/Objects/Tenno/Props/TnoPaintBotDojoDeco") {
 | 
			
		||||
            if (!meta || (meta.price == 0 && meta.ingredients.length == 0) || config.noDojoDecoBuildStage) {
 | 
			
		||||
                deco.CompletionTime = new Date();
 | 
			
		||||
                if (meta) {
 | 
			
		||||
                    processDojoBuildMaterialsGathered(guild, meta);
 | 
			
		||||
                }
 | 
			
		||||
            } else if (guild.AutoContributeFromVault && guild.VaultRegularCredits && guild.VaultMiscItems) {
 | 
			
		||||
                if (guild.VaultRegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) {
 | 
			
		||||
                    let enoughMiscItems = true;
 | 
			
		||||
                    for (const ingredient of meta.ingredients) {
 | 
			
		||||
                        if (
 | 
			
		||||
                            getVaultMiscItemCount(guild, ingredient.ItemType) <
 | 
			
		||||
                            scaleRequiredCount(guild.Tier, ingredient.ItemCount)
 | 
			
		||||
                        ) {
 | 
			
		||||
                            enoughMiscItems = false;
 | 
			
		||||
                            break;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    if (enoughMiscItems) {
 | 
			
		||||
                        guild.VaultRegularCredits -= scaleRequiredCount(guild.Tier, meta.price);
 | 
			
		||||
                        deco.RegularCredits = scaleRequiredCount(guild.Tier, meta.price);
 | 
			
		||||
 | 
			
		||||
                        deco.MiscItems = [];
 | 
			
		||||
                        for (const ingredient of meta.ingredients) {
 | 
			
		||||
                            guild.VaultMiscItems.find(x => x.ItemType == ingredient.ItemType)!.ItemCount -=
 | 
			
		||||
                                scaleRequiredCount(guild.Tier, ingredient.ItemCount);
 | 
			
		||||
                            deco.MiscItems.push({
 | 
			
		||||
                                ItemType: ingredient.ItemType,
 | 
			
		||||
                                ItemCount: scaleRequiredCount(guild.Tier, ingredient.ItemCount)
 | 
			
		||||
                            });
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        deco.CompletionTime = new Date(Date.now() + meta.time * 1000);
 | 
			
		||||
                        processDojoBuildMaterialsGathered(guild, meta);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -56,4 +114,9 @@ interface IPlaceDecoInComponentRequest {
 | 
			
		||||
    Pos: number[];
 | 
			
		||||
    Rot: number[];
 | 
			
		||||
    Name?: string;
 | 
			
		||||
    Sockets?: number;
 | 
			
		||||
    Scale?: number; // only provided alongside MoveId and seems to always be 1
 | 
			
		||||
    MoveId?: string;
 | 
			
		||||
    ShipDeco?: boolean;
 | 
			
		||||
    VaultDeco?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										9
									
								
								src/controllers/api/playedParkourTutorialController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/controllers/api/playedParkourTutorialController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const playedParkourTutorialController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    await Inventory.updateOne({ accountOwnerId: accountId }, { PlayedParkourTutorial: true });
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										75
									
								
								src/controllers/api/postGuildAdvertisementController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/controllers/api/postGuildAdvertisementController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,75 @@
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { GuildAd, GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import {
 | 
			
		||||
    addGuildMemberMiscItemContribution,
 | 
			
		||||
    addVaultMiscItems,
 | 
			
		||||
    getGuildForRequestEx,
 | 
			
		||||
    getVaultMiscItemCount,
 | 
			
		||||
    hasGuildPermissionEx
 | 
			
		||||
} from "@/src/services/guildService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { getVendorManifestByTypeName } from "@/src/services/serversideVendorsService";
 | 
			
		||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
			
		||||
import { IPurchaseParams } from "@/src/types/purchaseTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const postGuildAdvertisementController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "GuildId MiscItems");
 | 
			
		||||
    const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
    const guildMember = (await GuildMember.findOne({ accountId, guildId: guild._id }))!;
 | 
			
		||||
    if (!hasGuildPermissionEx(guild, guildMember, GuildPermission.Advertiser)) {
 | 
			
		||||
        res.status(400).end();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    const payload = getJSONfromString<IPostGuildAdvertisementRequest>(String(req.body));
 | 
			
		||||
 | 
			
		||||
    // Handle resource cost
 | 
			
		||||
    const vendor = getVendorManifestByTypeName(
 | 
			
		||||
        "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest"
 | 
			
		||||
    )!;
 | 
			
		||||
    const offer = vendor.VendorInfo.ItemManifest.find(x => x.StoreItem == payload.PurchaseParams.StoreItem)!;
 | 
			
		||||
    if (getVaultMiscItemCount(guild, offer.ItemPrices![0].ItemType) >= offer.ItemPrices![0].ItemCount) {
 | 
			
		||||
        addVaultMiscItems(guild, [
 | 
			
		||||
            {
 | 
			
		||||
                ItemType: offer.ItemPrices![0].ItemType,
 | 
			
		||||
                ItemCount: offer.ItemPrices![0].ItemCount * -1
 | 
			
		||||
            }
 | 
			
		||||
        ]);
 | 
			
		||||
    } else {
 | 
			
		||||
        const miscItem = inventory.MiscItems.find(x => x.ItemType == offer.ItemPrices![0].ItemType);
 | 
			
		||||
        if (!miscItem || miscItem.ItemCount < offer.ItemPrices![0].ItemCount) {
 | 
			
		||||
            res.status(400).json("Insufficient funds");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        miscItem.ItemCount -= offer.ItemPrices![0].ItemCount;
 | 
			
		||||
        addGuildMemberMiscItemContribution(guildMember, offer.ItemPrices![0]);
 | 
			
		||||
        await guildMember.save();
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Create or update ad
 | 
			
		||||
    await GuildAd.findOneAndUpdate(
 | 
			
		||||
        { GuildId: guild._id },
 | 
			
		||||
        {
 | 
			
		||||
            Emblem: guild.Emblem,
 | 
			
		||||
            Expiry: new Date(Date.now() + 12 * 3600 * 1000),
 | 
			
		||||
            Features: payload.Features,
 | 
			
		||||
            GuildName: guild.Name,
 | 
			
		||||
            MemberCount: await GuildMember.countDocuments({ guildId: guild._id, status: 0 }),
 | 
			
		||||
            RecruitMsg: payload.RecruitMsg,
 | 
			
		||||
            Tier: guild.Tier
 | 
			
		||||
        },
 | 
			
		||||
        { upsert: true }
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IPostGuildAdvertisementRequest {
 | 
			
		||||
    Features: number;
 | 
			
		||||
    RecruitMsg: string;
 | 
			
		||||
    Languages: string[];
 | 
			
		||||
    PurchaseParams: IPurchaseParams;
 | 
			
		||||
}
 | 
			
		||||
@ -16,7 +16,7 @@ export const queueDojoComponentDestructionController: RequestHandler = async (re
 | 
			
		||||
    const componentId = req.query.componentId as string;
 | 
			
		||||
 | 
			
		||||
    guild.DojoComponents.id(componentId)!.DestructionTime = new Date(
 | 
			
		||||
        Date.now() + (config.fastDojoRoomDestruction ? 5_000 : 2 * 3600_000)
 | 
			
		||||
        (Math.trunc(Date.now() / 1000) + (config.fastDojoRoomDestruction ? 5 : 2 * 3600)) * 1000
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    await guild.save();
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										23
									
								
								src/controllers/api/releasePetController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/controllers/api/releasePetController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { getInventory, updateCurrency } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const releasePetController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "RegularCredits KubrowPets");
 | 
			
		||||
    const payload = getJSONfromString<IReleasePetRequest>(String(req.body));
 | 
			
		||||
 | 
			
		||||
    const inventoryChanges = updateCurrency(inventory, 25000, false);
 | 
			
		||||
 | 
			
		||||
    inventoryChanges.RemovedIdItems = [{ ItemId: { $oid: payload.petId } }];
 | 
			
		||||
    inventory.KubrowPets.pull({ _id: payload.petId });
 | 
			
		||||
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json({ inventoryChanges }); // Not a mistake; it's "inventoryChanges" here.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IReleasePetRequest {
 | 
			
		||||
    recipeName: "/Lotus/Types/Game/KubrowPet/ReleasePetRecipe";
 | 
			
		||||
    petId: string;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										38
									
								
								src/controllers/api/removeFromAllianceController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/controllers/api/removeFromAllianceController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,38 @@
 | 
			
		||||
import { AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { deleteAlliance } from "@/src/services/guildService";
 | 
			
		||||
import { getAccountForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const removeFromAllianceController: RequestHandler = async (req, res) => {
 | 
			
		||||
    // Check requester is a warlord in their guild
 | 
			
		||||
    const account = await getAccountForRequest(req);
 | 
			
		||||
    const guildMember = (await GuildMember.findOne({ accountId: account._id, status: 0 }))!;
 | 
			
		||||
    if (guildMember.rank > 1) {
 | 
			
		||||
        res.status(400).json({ Error: 104 });
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let allianceMember = (await AllianceMember.findOne({ guildId: guildMember.guildId }))!;
 | 
			
		||||
    if (!guildMember.guildId.equals(req.query.guildId as string)) {
 | 
			
		||||
        // Removing a guild that is not our own needs additional permissions
 | 
			
		||||
        if (!(allianceMember.Permissions & GuildPermission.Ruler)) {
 | 
			
		||||
            res.status(400).json({ Error: 104 });
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Update allianceMember to point to the alliance to kick
 | 
			
		||||
        allianceMember = (await AllianceMember.findOne({ guildId: req.query.guildId }))!;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (allianceMember.Permissions & GuildPermission.Ruler) {
 | 
			
		||||
        await deleteAlliance(allianceMember.allianceId);
 | 
			
		||||
    } else {
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            await Guild.updateOne({ _id: allianceMember.guildId }, { $unset: { AllianceId: "" } }),
 | 
			
		||||
            await AllianceMember.deleteOne({ _id: allianceMember._id })
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
@ -1,6 +1,8 @@
 | 
			
		||||
import { GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { Inbox } from "@/src/models/inboxModel";
 | 
			
		||||
import { Account } from "@/src/models/loginModel";
 | 
			
		||||
import { getGuildForRequest, hasGuildPermission } from "@/src/services/guildService";
 | 
			
		||||
import { deleteGuild, getGuildForRequest, hasGuildPermission, removeDojoKeyItems } from "@/src/services/guildService";
 | 
			
		||||
import { createMessage } from "@/src/services/inboxService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
 | 
			
		||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
			
		||||
@ -17,26 +19,41 @@ export const removeFromGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const guildMember = (await GuildMember.findOne({ accountId: payload.userId, guildId: guild._id }))!;
 | 
			
		||||
    if (guildMember.status == 0) {
 | 
			
		||||
        const inventory = await getInventory(payload.userId);
 | 
			
		||||
        inventory.GuildId = undefined;
 | 
			
		||||
 | 
			
		||||
        // Remove clan key or blueprint from kicked member
 | 
			
		||||
        const itemIndex = inventory.LevelKeys.findIndex(x => x.ItemType == "/Lotus/Types/Keys/DojoKey");
 | 
			
		||||
        if (itemIndex != -1) {
 | 
			
		||||
            inventory.LevelKeys.splice(itemIndex, 1);
 | 
			
		||||
    if (guildMember.rank == 0) {
 | 
			
		||||
        await deleteGuild(guild._id);
 | 
			
		||||
    } else {
 | 
			
		||||
            const recipeIndex = inventory.Recipes.findIndex(x => x.ItemType == "/Lotus/Types/Keys/DojoKeyBlueprint");
 | 
			
		||||
            if (recipeIndex != -1) {
 | 
			
		||||
                inventory.Recipes.splice(itemIndex, 1);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (guildMember.status == 0) {
 | 
			
		||||
            const inventory = await getInventory(payload.userId, "GuildId LevelKeys Recipes");
 | 
			
		||||
            inventory.GuildId = undefined;
 | 
			
		||||
            removeDojoKeyItems(inventory);
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
 | 
			
		||||
        // TODO: Handle clan leader kicking themselves (guild should be deleted in this case, I think)
 | 
			
		||||
        } else if (guildMember.status == 1) {
 | 
			
		||||
            // TOVERIFY: Is this inbox message actually sent on live?
 | 
			
		||||
            await createMessage(guildMember.accountId, [
 | 
			
		||||
                {
 | 
			
		||||
                    sndr: "/Lotus/Language/Bosses/Ordis",
 | 
			
		||||
                    msg: "/Lotus/Language/Clan/RejectedFromClan",
 | 
			
		||||
                    sub: "/Lotus/Language/Clan/RejectedFromClanHeader",
 | 
			
		||||
                    arg: [
 | 
			
		||||
                        {
 | 
			
		||||
                            Key: "PLAYER_NAME",
 | 
			
		||||
                            Tag: (await Account.findOne({ _id: guildMember.accountId }, "DisplayName"))!.DisplayName
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            Key: "CLAN_NAME",
 | 
			
		||||
                            Tag: guild.Name
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                    // TOVERIFY: If this message is sent on live, is it highPriority?
 | 
			
		||||
                }
 | 
			
		||||
            ]);
 | 
			
		||||
        } else if (guildMember.status == 2) {
 | 
			
		||||
        // TODO: Maybe the inbox message for the sent invite should be deleted?
 | 
			
		||||
            // Delete the inbox message for the invite
 | 
			
		||||
            await Inbox.deleteOne({
 | 
			
		||||
                ownerId: guildMember.accountId,
 | 
			
		||||
                contextInfo: guild._id.toString(),
 | 
			
		||||
                acceptAction: "GUILD_INVITE"
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        await GuildMember.deleteOne({ _id: guildMember._id });
 | 
			
		||||
 | 
			
		||||
@ -56,6 +73,7 @@ export const removeFromGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        await guild.save();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    res.json({
 | 
			
		||||
        _id: payload.userId,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										21
									
								
								src/controllers/api/removeIgnoredUserController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/controllers/api/removeIgnoredUserController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { Account, Ignore } from "@/src/models/loginModel";
 | 
			
		||||
import { getAccountForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const removeIgnoredUserController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountForRequest(req);
 | 
			
		||||
    const data = getJSONfromString<IRemoveIgnoredUserRequest>(String(req.body));
 | 
			
		||||
    const ignoreeAccount = await Account.findOne(
 | 
			
		||||
        { DisplayName: data.playerName.substring(0, data.playerName.length - 1) },
 | 
			
		||||
        "_id"
 | 
			
		||||
    );
 | 
			
		||||
    if (ignoreeAccount) {
 | 
			
		||||
        await Ignore.deleteOne({ ignorer: accountId, ignoree: ignoreeAccount._id });
 | 
			
		||||
    }
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IRemoveIgnoredUserRequest {
 | 
			
		||||
    playerName: string;
 | 
			
		||||
}
 | 
			
		||||
@ -1,6 +1,9 @@
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
 | 
			
		||||
import { config } from "@/src/services/configService";
 | 
			
		||||
import { addEmailItem, getInventory, updateCurrency } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { ICompletedDialogue } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { ICompletedDialogue, IDialogueDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
@ -8,7 +11,7 @@ export const saveDialogueController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const request = JSON.parse(String(req.body)) as SaveDialogueRequest;
 | 
			
		||||
    if ("YearIteration" in request) {
 | 
			
		||||
        const inventory = await getInventory(accountId);
 | 
			
		||||
        const inventory = await getInventory(accountId, "DialogueHistory");
 | 
			
		||||
        if (inventory.DialogueHistory) {
 | 
			
		||||
            inventory.DialogueHistory.YearIteration = request.YearIteration;
 | 
			
		||||
        } else {
 | 
			
		||||
@ -21,33 +24,25 @@ export const saveDialogueController: RequestHandler = async (req, res) => {
 | 
			
		||||
        if (!inventory.DialogueHistory) {
 | 
			
		||||
            throw new Error("bad inventory state");
 | 
			
		||||
        }
 | 
			
		||||
        if (request.QueuedDialogues.length != 0 || request.OtherDialogueInfos.length != 0) {
 | 
			
		||||
            logger.error(`saveDialogue request not fully handled: ${String(req.body)}`);
 | 
			
		||||
        }
 | 
			
		||||
        const inventoryChanges: IInventoryChanges = {};
 | 
			
		||||
        const tomorrowAt0Utc = config.noKimCooldowns
 | 
			
		||||
            ? Date.now()
 | 
			
		||||
            : (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000;
 | 
			
		||||
        inventory.DialogueHistory.Dialogues ??= [];
 | 
			
		||||
        let dialogue = inventory.DialogueHistory.Dialogues.find(x => x.DialogueName == request.DialogueName);
 | 
			
		||||
        if (!dialogue) {
 | 
			
		||||
            dialogue =
 | 
			
		||||
                inventory.DialogueHistory.Dialogues[
 | 
			
		||||
                    inventory.DialogueHistory.Dialogues.push({
 | 
			
		||||
                        Rank: 0,
 | 
			
		||||
                        Chemistry: 0,
 | 
			
		||||
                        AvailableDate: new Date(0),
 | 
			
		||||
                        AvailableGiftDate: new Date(0),
 | 
			
		||||
                        RankUpExpiry: new Date(0),
 | 
			
		||||
                        BountyChemExpiry: new Date(0),
 | 
			
		||||
                        Gifts: [],
 | 
			
		||||
                        Booleans: [],
 | 
			
		||||
                        Completed: [],
 | 
			
		||||
                        DialogueName: request.DialogueName
 | 
			
		||||
                    }) - 1
 | 
			
		||||
                ];
 | 
			
		||||
        }
 | 
			
		||||
        const dialogue = getDialogue(inventory, request.DialogueName);
 | 
			
		||||
        dialogue.Rank = request.Rank;
 | 
			
		||||
        dialogue.Chemistry = request.Chemistry;
 | 
			
		||||
        //dialogue.QueuedDialogues = request.QueuedDialogues;
 | 
			
		||||
        if (request.Data) {
 | 
			
		||||
            dialogue.QueuedDialogues = request.QueuedDialogues;
 | 
			
		||||
            for (const bool of request.Booleans) {
 | 
			
		||||
                dialogue.Booleans.push(bool);
 | 
			
		||||
                if (bool == "LizzieShawzin") {
 | 
			
		||||
                    await addEmailItem(
 | 
			
		||||
                        inventory,
 | 
			
		||||
                        "/Lotus/Types/Items/EmailItems/LizzieShawzinSkinEmailItem",
 | 
			
		||||
                        inventoryChanges
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            for (const bool of request.ResetBooleans) {
 | 
			
		||||
                const index = dialogue.Booleans.findIndex(x => x == bool);
 | 
			
		||||
@ -56,13 +51,36 @@ export const saveDialogueController: RequestHandler = async (req, res) => {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            dialogue.Completed.push(request.Data);
 | 
			
		||||
        const tomorrowAt0Utc = (Math.trunc(Date.now() / (86400 * 1000)) + 1) * 86400 * 1000;
 | 
			
		||||
            dialogue.AvailableDate = new Date(tomorrowAt0Utc);
 | 
			
		||||
            for (const info of request.OtherDialogueInfos) {
 | 
			
		||||
                const otherDialogue = getDialogue(inventory, info.Dialogue);
 | 
			
		||||
                if (info.Tag != "") {
 | 
			
		||||
                    otherDialogue.QueuedDialogues.push(info.Tag);
 | 
			
		||||
                }
 | 
			
		||||
                otherDialogue.Chemistry += info.Value; // unsure
 | 
			
		||||
            }
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            res.json({
 | 
			
		||||
            InventoryChanges: [],
 | 
			
		||||
                InventoryChanges: inventoryChanges,
 | 
			
		||||
                AvailableDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } }
 | 
			
		||||
            });
 | 
			
		||||
        } else if (request.Gift) {
 | 
			
		||||
            const inventoryChanges = updateCurrency(inventory, request.Gift.Cost, false);
 | 
			
		||||
            const gift = dialogue.Gifts.find(x => x.Item == request.Gift!.Item);
 | 
			
		||||
            if (gift) {
 | 
			
		||||
                gift.GiftedQuantity += 1;
 | 
			
		||||
            } else {
 | 
			
		||||
                dialogue.Gifts.push({ Item: request.Gift.Item, GiftedQuantity: 1 });
 | 
			
		||||
            }
 | 
			
		||||
            dialogue.AvailableGiftDate = new Date(tomorrowAt0Utc);
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            res.json({
 | 
			
		||||
                InventoryChanges: inventoryChanges,
 | 
			
		||||
                AvailableGiftDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } }
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            logger.error(`saveDialogue request not fully handled: ${String(req.body)}`);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -77,11 +95,17 @@ interface SaveCompletedDialogueRequest {
 | 
			
		||||
    Rank: number;
 | 
			
		||||
    Chemistry: number;
 | 
			
		||||
    CompletionType: number;
 | 
			
		||||
    QueuedDialogues: string[]; // unsure
 | 
			
		||||
    QueuedDialogues: string[];
 | 
			
		||||
    Gift?: {
 | 
			
		||||
        Item: string;
 | 
			
		||||
        GainedChemistry: number;
 | 
			
		||||
        Cost: number;
 | 
			
		||||
        GiftedQuantity: number;
 | 
			
		||||
    };
 | 
			
		||||
    Booleans: string[];
 | 
			
		||||
    ResetBooleans: string[];
 | 
			
		||||
    Data: ICompletedDialogue;
 | 
			
		||||
    OtherDialogueInfos: IOtherDialogueInfo[]; // unsure
 | 
			
		||||
    Data?: ICompletedDialogue;
 | 
			
		||||
    OtherDialogueInfos: IOtherDialogueInfo[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IOtherDialogueInfo {
 | 
			
		||||
@ -89,3 +113,26 @@ interface IOtherDialogueInfo {
 | 
			
		||||
    Tag: string;
 | 
			
		||||
    Value: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const getDialogue = (inventory: TInventoryDatabaseDocument, dialogueName: string): IDialogueDatabase => {
 | 
			
		||||
    let dialogue = inventory.DialogueHistory!.Dialogues!.find(x => x.DialogueName == dialogueName);
 | 
			
		||||
    if (!dialogue) {
 | 
			
		||||
        dialogue =
 | 
			
		||||
            inventory.DialogueHistory!.Dialogues![
 | 
			
		||||
                inventory.DialogueHistory!.Dialogues!.push({
 | 
			
		||||
                    Rank: 0,
 | 
			
		||||
                    Chemistry: 0,
 | 
			
		||||
                    AvailableDate: new Date(0),
 | 
			
		||||
                    AvailableGiftDate: new Date(0),
 | 
			
		||||
                    RankUpExpiry: new Date(0),
 | 
			
		||||
                    BountyChemExpiry: new Date(0),
 | 
			
		||||
                    QueuedDialogues: [],
 | 
			
		||||
                    Gifts: [],
 | 
			
		||||
                    Booleans: [],
 | 
			
		||||
                    Completed: [],
 | 
			
		||||
                    DialogueName: dialogueName
 | 
			
		||||
                }) - 1
 | 
			
		||||
            ];
 | 
			
		||||
    }
 | 
			
		||||
    return dialogue;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,32 +0,0 @@
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { ISaveLoadoutRequest } from "@/src/types/saveLoadoutTypes";
 | 
			
		||||
import { handleInventoryItemConfigChange } from "@/src/services/saveLoadoutService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
 | 
			
		||||
export const saveLoadoutController: RequestHandler = async (req, res) => {
 | 
			
		||||
    //validate here
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        const body: ISaveLoadoutRequest = JSON.parse(req.body as string) as ISaveLoadoutRequest;
 | 
			
		||||
        // console.log(util.inspect(body, { showHidden: false, depth: null, colors: true }));
 | 
			
		||||
 | 
			
		||||
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
        const { UpgradeVer, ...equipmentChanges } = body;
 | 
			
		||||
        const newLoadoutId = await handleInventoryItemConfigChange(equipmentChanges, accountId);
 | 
			
		||||
 | 
			
		||||
        //send back new loadout id, if new loadout was added
 | 
			
		||||
        if (newLoadoutId) {
 | 
			
		||||
            res.send(newLoadoutId);
 | 
			
		||||
        }
 | 
			
		||||
        res.status(200).end();
 | 
			
		||||
    } catch (error: unknown) {
 | 
			
		||||
        if (error instanceof Error) {
 | 
			
		||||
            logger.error(`error in saveLoadoutController: ${error.message}`);
 | 
			
		||||
            res.status(400).json({ error: error.message });
 | 
			
		||||
        } else {
 | 
			
		||||
            res.status(400).json({ error: "unknown error" });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										21
									
								
								src/controllers/api/saveLoadoutController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/controllers/api/saveLoadoutController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { ISaveLoadoutRequest } from "@/src/types/saveLoadoutTypes";
 | 
			
		||||
import { handleInventoryItemConfigChange } from "@/src/services/saveLoadoutService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
 | 
			
		||||
export const saveLoadoutController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
 | 
			
		||||
    const body: ISaveLoadoutRequest = JSON.parse(req.body as string) as ISaveLoadoutRequest;
 | 
			
		||||
    // console.log(util.inspect(body, { showHidden: false, depth: null, colors: true }));
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    const { UpgradeVer, ...equipmentChanges } = body;
 | 
			
		||||
    const newLoadoutId = await handleInventoryItemConfigChange(equipmentChanges, accountId);
 | 
			
		||||
 | 
			
		||||
    //send back new loadout id, if new loadout was added
 | 
			
		||||
    if (newLoadoutId) {
 | 
			
		||||
        res.send(newLoadoutId);
 | 
			
		||||
    }
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										25
									
								
								src/controllers/api/saveVaultAutoContributeController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/controllers/api/saveVaultAutoContributeController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { Guild } from "@/src/models/guildModel";
 | 
			
		||||
import { hasGuildPermission } from "@/src/services/guildService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const saveVaultAutoContributeController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "GuildId");
 | 
			
		||||
    const guild = (await Guild.findById(inventory.GuildId!, "Ranks AutoContributeFromVault"))!;
 | 
			
		||||
    if (!(await hasGuildPermission(guild, accountId, GuildPermission.Treasurer))) {
 | 
			
		||||
        res.status(400).send("Invalid permission").end();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    const data = getJSONfromString<ISetVaultAutoContributeRequest>(String(req.body));
 | 
			
		||||
    guild.AutoContributeFromVault = data.autoContributeFromVault;
 | 
			
		||||
    await guild.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ISetVaultAutoContributeRequest {
 | 
			
		||||
    autoContributeFromVault: boolean;
 | 
			
		||||
}
 | 
			
		||||
@ -6,14 +6,64 @@ import {
 | 
			
		||||
    addRecipes,
 | 
			
		||||
    addMiscItems,
 | 
			
		||||
    addConsumables,
 | 
			
		||||
    freeUpSlot
 | 
			
		||||
    freeUpSlot,
 | 
			
		||||
    combineInventoryChanges,
 | 
			
		||||
    addCrewShipRawSalvage
 | 
			
		||||
} from "@/src/services/inventoryService";
 | 
			
		||||
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { ExportDojoRecipes } from "warframe-public-export-plus";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
 | 
			
		||||
 | 
			
		||||
export const sellController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const payload = JSON.parse(String(req.body)) as ISellRequest;
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
    const requiredFields = new Set<keyof TInventoryDatabaseDocument>();
 | 
			
		||||
    if (payload.SellCurrency == "SC_RegularCredits") {
 | 
			
		||||
        requiredFields.add("RegularCredits");
 | 
			
		||||
    } else if (payload.SellCurrency == "SC_FusionPoints") {
 | 
			
		||||
        requiredFields.add("FusionPoints");
 | 
			
		||||
    } else {
 | 
			
		||||
        requiredFields.add("MiscItems");
 | 
			
		||||
    }
 | 
			
		||||
    for (const key of Object.keys(payload.Items)) {
 | 
			
		||||
        requiredFields.add(key as keyof TInventoryDatabaseDocument);
 | 
			
		||||
    }
 | 
			
		||||
    if (requiredFields.has("Upgrades")) {
 | 
			
		||||
        requiredFields.add("RawUpgrades");
 | 
			
		||||
    }
 | 
			
		||||
    if (payload.Items.Suits) {
 | 
			
		||||
        requiredFields.add(InventorySlot.SUITS);
 | 
			
		||||
    }
 | 
			
		||||
    if (payload.Items.LongGuns || payload.Items.Pistols || payload.Items.Melee) {
 | 
			
		||||
        requiredFields.add(InventorySlot.WEAPONS);
 | 
			
		||||
    }
 | 
			
		||||
    if (payload.Items.SpaceSuits) {
 | 
			
		||||
        requiredFields.add(InventorySlot.SPACESUITS);
 | 
			
		||||
    }
 | 
			
		||||
    if (payload.Items.SpaceGuns || payload.Items.SpaceMelee) {
 | 
			
		||||
        requiredFields.add(InventorySlot.SPACEWEAPONS);
 | 
			
		||||
    }
 | 
			
		||||
    if (payload.Items.Sentinels || payload.Items.SentinelWeapons) {
 | 
			
		||||
        requiredFields.add(InventorySlot.SENTINELS);
 | 
			
		||||
    }
 | 
			
		||||
    if (payload.Items.OperatorAmps) {
 | 
			
		||||
        requiredFields.add(InventorySlot.AMPS);
 | 
			
		||||
    }
 | 
			
		||||
    if (payload.Items.Hoverboards) {
 | 
			
		||||
        requiredFields.add(InventorySlot.SPACESUITS);
 | 
			
		||||
    }
 | 
			
		||||
    if (payload.Items.CrewShipWeapons || payload.Items.CrewShipWeaponSkins) {
 | 
			
		||||
        requiredFields.add(InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
 | 
			
		||||
        requiredFields.add("CrewShipRawSalvage");
 | 
			
		||||
        if (payload.Items.CrewShipWeapons) {
 | 
			
		||||
            requiredFields.add("CrewShipSalvagedWeapons");
 | 
			
		||||
        }
 | 
			
		||||
        if (payload.Items.CrewShipWeaponSkins) {
 | 
			
		||||
            requiredFields.add("CrewShipSalvagedWeaponSkins");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    const inventory = await getInventory(accountId, Array.from(requiredFields).join(" "));
 | 
			
		||||
 | 
			
		||||
    // Give currency
 | 
			
		||||
    if (payload.SellCurrency == "SC_RegularCredits") {
 | 
			
		||||
@ -34,10 +84,14 @@ export const sellController: RequestHandler = async (req, res) => {
 | 
			
		||||
                ItemCount: payload.SellPrice
 | 
			
		||||
            }
 | 
			
		||||
        ]);
 | 
			
		||||
    } else if (payload.SellCurrency == "SC_Resources") {
 | 
			
		||||
        // Will add appropriate MiscItems from CrewShipWeapons or CrewShipWeaponSkins
 | 
			
		||||
    } else {
 | 
			
		||||
        throw new Error("Unknown SellCurrency: " + payload.SellCurrency);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const inventoryChanges: IInventoryChanges = {};
 | 
			
		||||
 | 
			
		||||
    // Remove item(s)
 | 
			
		||||
    if (payload.Items.Suits) {
 | 
			
		||||
        payload.Items.Suits.forEach(sellItem => {
 | 
			
		||||
@ -110,6 +164,56 @@ export const sellController: RequestHandler = async (req, res) => {
 | 
			
		||||
            inventory.Drones.pull({ _id: sellItem.String });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    if (payload.Items.CrewShipWeapons) {
 | 
			
		||||
        payload.Items.CrewShipWeapons.forEach(sellItem => {
 | 
			
		||||
            if (sellItem.String[0] == "/") {
 | 
			
		||||
                addCrewShipRawSalvage(inventory, [
 | 
			
		||||
                    {
 | 
			
		||||
                        ItemType: sellItem.String,
 | 
			
		||||
                        ItemCount: sellItem.Count * -1
 | 
			
		||||
                    }
 | 
			
		||||
                ]);
 | 
			
		||||
            } else {
 | 
			
		||||
                const index = inventory.CrewShipWeapons.findIndex(x => x._id.equals(sellItem.String));
 | 
			
		||||
                if (index != -1) {
 | 
			
		||||
                    if (payload.SellCurrency == "SC_Resources") {
 | 
			
		||||
                        refundPartialBuildCosts(inventory, inventory.CrewShipWeapons[index].ItemType, inventoryChanges);
 | 
			
		||||
                    }
 | 
			
		||||
                    inventory.CrewShipWeapons.splice(index, 1);
 | 
			
		||||
                    freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
 | 
			
		||||
                } else {
 | 
			
		||||
                    inventory.CrewShipSalvagedWeapons.pull({ _id: sellItem.String });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    if (payload.Items.CrewShipWeaponSkins) {
 | 
			
		||||
        payload.Items.CrewShipWeaponSkins.forEach(sellItem => {
 | 
			
		||||
            if (sellItem.String[0] == "/") {
 | 
			
		||||
                addCrewShipRawSalvage(inventory, [
 | 
			
		||||
                    {
 | 
			
		||||
                        ItemType: sellItem.String,
 | 
			
		||||
                        ItemCount: sellItem.Count * -1
 | 
			
		||||
                    }
 | 
			
		||||
                ]);
 | 
			
		||||
            } else {
 | 
			
		||||
                const index = inventory.CrewShipWeaponSkins.findIndex(x => x._id.equals(sellItem.String));
 | 
			
		||||
                if (index != -1) {
 | 
			
		||||
                    if (payload.SellCurrency == "SC_Resources") {
 | 
			
		||||
                        refundPartialBuildCosts(
 | 
			
		||||
                            inventory,
 | 
			
		||||
                            inventory.CrewShipWeaponSkins[index].ItemType,
 | 
			
		||||
                            inventoryChanges
 | 
			
		||||
                        );
 | 
			
		||||
                    }
 | 
			
		||||
                    inventory.CrewShipWeaponSkins.splice(index, 1);
 | 
			
		||||
                    freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
 | 
			
		||||
                } else {
 | 
			
		||||
                    inventory.CrewShipSalvagedWeaponSkins.pull({ _id: sellItem.String });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    if (payload.Items.Consumables) {
 | 
			
		||||
        const consumablesChanges = [];
 | 
			
		||||
        for (const sellItem of payload.Items.Consumables) {
 | 
			
		||||
@ -156,7 +260,9 @@ export const sellController: RequestHandler = async (req, res) => {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json({});
 | 
			
		||||
    res.json({
 | 
			
		||||
        inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges"
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ISellRequest {
 | 
			
		||||
@ -177,6 +283,8 @@ interface ISellRequest {
 | 
			
		||||
        OperatorAmps?: ISellItem[];
 | 
			
		||||
        Hoverboards?: ISellItem[];
 | 
			
		||||
        Drones?: ISellItem[];
 | 
			
		||||
        CrewShipWeapons?: ISellItem[];
 | 
			
		||||
        CrewShipWeaponSkins?: ISellItem[];
 | 
			
		||||
    };
 | 
			
		||||
    SellPrice: number;
 | 
			
		||||
    SellCurrency:
 | 
			
		||||
@ -193,3 +301,33 @@ interface ISellItem {
 | 
			
		||||
    String: string; // oid or uniqueName
 | 
			
		||||
    Count: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const refundPartialBuildCosts = (
 | 
			
		||||
    inventory: TInventoryDatabaseDocument,
 | 
			
		||||
    itemType: string,
 | 
			
		||||
    inventoryChanges: IInventoryChanges
 | 
			
		||||
): void => {
 | 
			
		||||
    // House versions
 | 
			
		||||
    const research = Object.values(ExportDojoRecipes.research).find(x => x.resultType == itemType);
 | 
			
		||||
    if (research) {
 | 
			
		||||
        const miscItemChanges = research.ingredients.map(x => ({
 | 
			
		||||
            ItemType: x.ItemType,
 | 
			
		||||
            ItemCount: Math.trunc(x.ItemCount * 0.8)
 | 
			
		||||
        }));
 | 
			
		||||
        addMiscItems(inventory, miscItemChanges);
 | 
			
		||||
        combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges });
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Sigma versions
 | 
			
		||||
    const recipe = Object.values(ExportDojoRecipes.fabrications).find(x => x.resultType == itemType);
 | 
			
		||||
    if (recipe) {
 | 
			
		||||
        const miscItemChanges = recipe.ingredients.map(x => ({
 | 
			
		||||
            ItemType: x.ItemType,
 | 
			
		||||
            ItemCount: Math.trunc(x.ItemCount * 0.8)
 | 
			
		||||
        }));
 | 
			
		||||
        addMiscItems(inventory, miscItemChanges);
 | 
			
		||||
        combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges });
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										31
									
								
								src/controllers/api/sendMsgToInBoxController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/controllers/api/sendMsgToInBoxController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { createMessage } from "@/src/services/inboxService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const sendMsgToInBoxController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const data = getJSONfromString<ISendMsgToInBoxRequest>(String(req.body));
 | 
			
		||||
    await createMessage(accountId, [
 | 
			
		||||
        {
 | 
			
		||||
            sub: data.title,
 | 
			
		||||
            msg: data.message,
 | 
			
		||||
            sndr: data.sender ?? "/Lotus/Language/Bosses/Ordis",
 | 
			
		||||
            icon: data.senderIcon,
 | 
			
		||||
            highPriority: data.highPriority,
 | 
			
		||||
            transmission: data.transmission,
 | 
			
		||||
            att: data.attachments
 | 
			
		||||
        }
 | 
			
		||||
    ]);
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ISendMsgToInBoxRequest {
 | 
			
		||||
    title: string;
 | 
			
		||||
    message: string;
 | 
			
		||||
    sender?: string;
 | 
			
		||||
    senderIcon?: string;
 | 
			
		||||
    highPriority?: boolean;
 | 
			
		||||
    transmission?: string;
 | 
			
		||||
    attachments?: string[];
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										38
									
								
								src/controllers/api/setAllianceGuildPermissionsController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/controllers/api/setAllianceGuildPermissionsController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,38 @@
 | 
			
		||||
import { AllianceMember, GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { getAccountForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const setAllianceGuildPermissionsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    // Check requester is a warlord in their guild
 | 
			
		||||
    const account = await getAccountForRequest(req);
 | 
			
		||||
    const guildMember = (await GuildMember.findOne({ accountId: account._id, status: 0 }))!;
 | 
			
		||||
    if (guildMember.rank > 1) {
 | 
			
		||||
        res.status(400).end();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check guild is the creator of the alliance and don't allow changing of own permissions. (Technically changing permissions requires the Promoter permission, but both are exclusive to the creator guild.)
 | 
			
		||||
    const allianceMember = (await AllianceMember.findOne({
 | 
			
		||||
        guildId: guildMember.guildId,
 | 
			
		||||
        Pending: false
 | 
			
		||||
    }))!;
 | 
			
		||||
    if (
 | 
			
		||||
        !(allianceMember.Permissions & GuildPermission.Ruler) ||
 | 
			
		||||
        allianceMember.guildId.equals(req.query.guildId as string)
 | 
			
		||||
    ) {
 | 
			
		||||
        res.status(400).end();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const targetAllianceMember = (await AllianceMember.findOne({
 | 
			
		||||
        allianceId: allianceMember.allianceId,
 | 
			
		||||
        guildId: req.query.guildId
 | 
			
		||||
    }))!;
 | 
			
		||||
    targetAllianceMember.Permissions =
 | 
			
		||||
        parseInt(req.query.perms as string) &
 | 
			
		||||
        (GuildPermission.Recruiter | GuildPermission.Treasurer | GuildPermission.ChatModerator);
 | 
			
		||||
    await targetAllianceMember.save();
 | 
			
		||||
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										34
									
								
								src/controllers/api/setDojoComponentColorsController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/controllers/api/setDojoComponentColorsController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,34 @@
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const setDojoComponentColorsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "GuildId LevelKeys");
 | 
			
		||||
    const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
    if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Decorator))) {
 | 
			
		||||
        res.json({ DojoRequestStatus: -1 });
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    const data = getJSONfromString<ISetDojoComponentColorsRequest>(String(req.body));
 | 
			
		||||
    const component = guild.DojoComponents.id(data.ComponentId)!;
 | 
			
		||||
    //const deco = component.Decos!.find(x => x._id.equals(data.DecoId))!;
 | 
			
		||||
    //deco.Pending = true;
 | 
			
		||||
    //component.PaintBot = new Types.ObjectId(data.DecoId);
 | 
			
		||||
    if ("lights" in req.query) {
 | 
			
		||||
        component.PendingLights = data.Colours;
 | 
			
		||||
    } else {
 | 
			
		||||
        component.PendingColors = data.Colours;
 | 
			
		||||
    }
 | 
			
		||||
    await guild.save();
 | 
			
		||||
    res.json(await getDojoClient(guild, 0, component._id));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ISetDojoComponentColorsRequest {
 | 
			
		||||
    ComponentId: string;
 | 
			
		||||
    DecoId: string;
 | 
			
		||||
    Colours: number[];
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										25
									
								
								src/controllers/api/setDojoComponentSettingsController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/controllers/api/setDojoComponentSettingsController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const setDojoComponentSettingsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "GuildId LevelKeys");
 | 
			
		||||
    const guild = await getGuildForRequestEx(req, inventory);
 | 
			
		||||
    if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Decorator))) {
 | 
			
		||||
        res.json({ DojoRequestStatus: -1 });
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    const component = guild.DojoComponents.id(req.query.componentId)!;
 | 
			
		||||
    const data = getJSONfromString<ISetDojoComponentSettingsRequest>(String(req.body));
 | 
			
		||||
    component.Settings = data.Settings;
 | 
			
		||||
    await guild.save();
 | 
			
		||||
    res.json(await getDojoClient(guild, 0, component._id));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ISetDojoComponentSettingsRequest {
 | 
			
		||||
    Settings: string;
 | 
			
		||||
}
 | 
			
		||||
@ -1,21 +1,44 @@
 | 
			
		||||
import { Guild } from "@/src/models/guildModel";
 | 
			
		||||
import { hasGuildPermission } from "@/src/services/guildService";
 | 
			
		||||
import { Alliance, Guild, GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { hasGuildPermissionEx } from "@/src/services/guildService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
 | 
			
		||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
			
		||||
import { GuildPermission, ILongMOTD } from "@/src/types/guildTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const setGuildMotdController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const account = await getAccountForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(account._id.toString(), "GuildId");
 | 
			
		||||
    const guild = (await Guild.findById(inventory.GuildId!))!;
 | 
			
		||||
    if (!(await hasGuildPermission(guild, account._id, GuildPermission.Herald))) {
 | 
			
		||||
    const member = (await GuildMember.findOne({ accountId: account._id, guildId: guild._id }))!;
 | 
			
		||||
 | 
			
		||||
    const IsLongMOTD = "longMOTD" in req.query;
 | 
			
		||||
    const MOTD = req.body ? String(req.body) : undefined;
 | 
			
		||||
 | 
			
		||||
    if ("alliance" in req.query) {
 | 
			
		||||
        if (member.rank > 1) {
 | 
			
		||||
            res.status(400).json("Invalid permission");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    const IsLongMOTD = "longMOTD" in req.query;
 | 
			
		||||
    const MOTD = req.body ? String(req.body) : undefined;
 | 
			
		||||
        const alliance = (await Alliance.findById(guild.AllianceId!))!;
 | 
			
		||||
        const motd = MOTD
 | 
			
		||||
            ? ({
 | 
			
		||||
                  message: MOTD,
 | 
			
		||||
                  authorName: getSuffixedName(account),
 | 
			
		||||
                  authorGuildName: guild.Name
 | 
			
		||||
              } satisfies ILongMOTD)
 | 
			
		||||
            : undefined;
 | 
			
		||||
        if (IsLongMOTD) {
 | 
			
		||||
            alliance.LongMOTD = motd;
 | 
			
		||||
        } else {
 | 
			
		||||
            alliance.MOTD = motd;
 | 
			
		||||
        }
 | 
			
		||||
        await alliance.save();
 | 
			
		||||
    } else {
 | 
			
		||||
        if (!hasGuildPermissionEx(guild, member, GuildPermission.Herald)) {
 | 
			
		||||
            res.status(400).json("Invalid permission");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (IsLongMOTD) {
 | 
			
		||||
            if (MOTD) {
 | 
			
		||||
@ -30,6 +53,7 @@ export const setGuildMotdController: RequestHandler = async (req, res) => {
 | 
			
		||||
            guild.MOTD = MOTD ?? "";
 | 
			
		||||
        }
 | 
			
		||||
        await guild.save();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    res.json({ IsLongMOTD, MOTD });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { ISetPlacedDecoInfoRequest } from "@/src/types/shipTypes";
 | 
			
		||||
import { IPictureFrameInfo, ISetPlacedDecoInfoRequest } from "@/src/types/shipTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { handleSetPlacedDecoInfo } from "@/src/services/shipCustomizationsService";
 | 
			
		||||
 | 
			
		||||
@ -7,5 +7,17 @@ export const setPlacedDecoInfoController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const payload = JSON.parse(req.body as string) as ISetPlacedDecoInfoRequest;
 | 
			
		||||
    await handleSetPlacedDecoInfo(accountId, payload);
 | 
			
		||||
    res.end();
 | 
			
		||||
    res.json({
 | 
			
		||||
        DecoId: payload.DecoId,
 | 
			
		||||
        IsPicture: true,
 | 
			
		||||
        PictureFrameInfo: payload.PictureFrameInfo,
 | 
			
		||||
        BootLocation: payload.BootLocation
 | 
			
		||||
    } satisfies ISetPlacedDecoInfoResponse);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ISetPlacedDecoInfoResponse {
 | 
			
		||||
    DecoId: string;
 | 
			
		||||
    IsPicture: boolean;
 | 
			
		||||
    PictureFrameInfo?: IPictureFrameInfo;
 | 
			
		||||
    BootLocation?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3,29 +3,40 @@ import { RequestHandler } from "express";
 | 
			
		||||
import { getPersonalRooms } from "@/src/services/personalRoomsService";
 | 
			
		||||
import { IOid } from "@/src/types/commonTypes";
 | 
			
		||||
import { Types } from "mongoose";
 | 
			
		||||
import { IFavouriteLoadoutDatabase, TBootLocation } from "@/src/types/shipTypes";
 | 
			
		||||
 | 
			
		||||
export const setShipFavouriteLoadoutController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const personalRooms = await getPersonalRooms(accountId);
 | 
			
		||||
    const body = JSON.parse(String(req.body)) as ISetShipFavouriteLoadoutRequest;
 | 
			
		||||
    if (body.BootLocation != "SHOP") {
 | 
			
		||||
        throw new Error(`unexpected BootLocation: ${body.BootLocation}`);
 | 
			
		||||
    }
 | 
			
		||||
    const display = personalRooms.TailorShop.FavouriteLoadouts.find(x => x.Tag == body.TagName);
 | 
			
		||||
    if (display) {
 | 
			
		||||
        display.LoadoutId = new Types.ObjectId(body.FavouriteLoadoutId.$oid);
 | 
			
		||||
    if (body.BootLocation == "LISET") {
 | 
			
		||||
        personalRooms.Ship.FavouriteLoadoutId = new Types.ObjectId(body.FavouriteLoadoutId.$oid);
 | 
			
		||||
    } else if (body.BootLocation == "APARTMENT") {
 | 
			
		||||
        updateTaggedDisplay(personalRooms.Apartment.FavouriteLoadouts, body);
 | 
			
		||||
    } else if (body.BootLocation == "SHOP") {
 | 
			
		||||
        updateTaggedDisplay(personalRooms.TailorShop.FavouriteLoadouts, body);
 | 
			
		||||
    } else {
 | 
			
		||||
        personalRooms.TailorShop.FavouriteLoadouts.push({
 | 
			
		||||
            Tag: body.TagName,
 | 
			
		||||
            LoadoutId: new Types.ObjectId(body.FavouriteLoadoutId.$oid)
 | 
			
		||||
        });
 | 
			
		||||
        console.log(body);
 | 
			
		||||
        throw new Error(`unexpected BootLocation: ${body.BootLocation}`);
 | 
			
		||||
    }
 | 
			
		||||
    await personalRooms.save();
 | 
			
		||||
    res.json({});
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ISetShipFavouriteLoadoutRequest {
 | 
			
		||||
    BootLocation: string;
 | 
			
		||||
    BootLocation: TBootLocation;
 | 
			
		||||
    FavouriteLoadoutId: IOid;
 | 
			
		||||
    TagName: string;
 | 
			
		||||
    TagName?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const updateTaggedDisplay = (arr: IFavouriteLoadoutDatabase[], body: ISetShipFavouriteLoadoutRequest): void => {
 | 
			
		||||
    const display = arr.find(x => x.Tag == body.TagName!);
 | 
			
		||||
    if (display) {
 | 
			
		||||
        display.LoadoutId = new Types.ObjectId(body.FavouriteLoadoutId.$oid);
 | 
			
		||||
    } else {
 | 
			
		||||
        arr.push({
 | 
			
		||||
            Tag: body.TagName!,
 | 
			
		||||
            LoadoutId: new Types.ObjectId(body.FavouriteLoadoutId.$oid)
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										48
									
								
								src/controllers/api/setShipVignetteController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/controllers/api/setShipVignetteController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,48 @@
 | 
			
		||||
import { addMiscItems, combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { getPersonalRooms } from "@/src/services/personalRoomsService";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const setShipVignetteController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "MiscItems");
 | 
			
		||||
    const personalRooms = await getPersonalRooms(accountId);
 | 
			
		||||
    const body = JSON.parse(String(req.body)) as ISetShipVignetteRequest;
 | 
			
		||||
    personalRooms.Ship.Wallpaper = body.Wallpaper;
 | 
			
		||||
    personalRooms.Ship.Vignette = body.Vignette;
 | 
			
		||||
    personalRooms.Ship.VignetteFish ??= [];
 | 
			
		||||
    const inventoryChanges: IInventoryChanges = {};
 | 
			
		||||
    for (let i = 0; i != body.Fish.length; ++i) {
 | 
			
		||||
        if (body.Fish[i] && !personalRooms.Ship.VignetteFish[i]) {
 | 
			
		||||
            logger.debug(`moving ${body.Fish[i]} from inventory to vignette slot ${i}`);
 | 
			
		||||
            const miscItemsDelta = [{ ItemType: body.Fish[i], ItemCount: -1 }];
 | 
			
		||||
            addMiscItems(inventory, miscItemsDelta);
 | 
			
		||||
            combineInventoryChanges(inventoryChanges, { MiscItems: miscItemsDelta });
 | 
			
		||||
        } else if (personalRooms.Ship.VignetteFish[i] && !body.Fish[i]) {
 | 
			
		||||
            logger.debug(`moving ${personalRooms.Ship.VignetteFish[i]} from vignette slot ${i} to inventory`);
 | 
			
		||||
            const miscItemsDelta = [{ ItemType: personalRooms.Ship.VignetteFish[i], ItemCount: +1 }];
 | 
			
		||||
            addMiscItems(inventory, miscItemsDelta);
 | 
			
		||||
            combineInventoryChanges(inventoryChanges, { MiscItems: miscItemsDelta });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    personalRooms.Ship.VignetteFish = body.Fish;
 | 
			
		||||
    if (body.VignetteDecos.length) {
 | 
			
		||||
        logger.error(`setShipVignette request not fully handled:`, body);
 | 
			
		||||
    }
 | 
			
		||||
    await Promise.all([inventory.save(), personalRooms.save()]);
 | 
			
		||||
    res.json({
 | 
			
		||||
        Wallpaper: body.Wallpaper,
 | 
			
		||||
        Vignette: body.Vignette,
 | 
			
		||||
        VignetteFish: body.Fish,
 | 
			
		||||
        InventoryChanges: inventoryChanges
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ISetShipVignetteRequest {
 | 
			
		||||
    Wallpaper: string;
 | 
			
		||||
    Vignette: string;
 | 
			
		||||
    Fish: string[];
 | 
			
		||||
    VignetteDecos: unknown[];
 | 
			
		||||
}
 | 
			
		||||
@ -1,20 +1,25 @@
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { WeaponTypeInternal } from "@/src/services/itemDataService";
 | 
			
		||||
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
 | 
			
		||||
import { equipmentKeys, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
 | 
			
		||||
export const setWeaponSkillTreeController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
    const payload = getJSONfromString<ISetWeaponSkillTreeRequest>(String(req.body));
 | 
			
		||||
 | 
			
		||||
    const item = inventory[req.query.Category as WeaponTypeInternal].find(
 | 
			
		||||
        item => item._id.toString() == (req.query.ItemId as string)
 | 
			
		||||
    )!;
 | 
			
		||||
    item.SkillTree = payload.SkillTree;
 | 
			
		||||
    if (equipmentKeys.indexOf(req.query.Category as TEquipmentKey) != -1) {
 | 
			
		||||
        await Inventory.updateOne(
 | 
			
		||||
            {
 | 
			
		||||
                accountOwnerId: accountId,
 | 
			
		||||
                [`${req.query.Category as string}._id`]: req.query.ItemId as string
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                [`${req.query.Category as string}.$.SkillTree`]: payload.SkillTree
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -90,7 +90,6 @@ export const startRecipeController: RequestHandler = async (req, res) => {
 | 
			
		||||
                spectreLoadout.LongGuns = item.ItemType;
 | 
			
		||||
                spectreLoadout.LongGunsModularParts = item.ModularParts;
 | 
			
		||||
            } else {
 | 
			
		||||
                // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 | 
			
		||||
                console.assert(type == "/Lotus/Types/Game/LotusMeleeWeapon");
 | 
			
		||||
                const item = inventory.Melee.id(oid)!;
 | 
			
		||||
                spectreLoadout.Melee = item.ItemType;
 | 
			
		||||
@ -111,6 +110,8 @@ export const startRecipeController: RequestHandler = async (req, res) => {
 | 
			
		||||
            inventory.PendingSpectreLoadouts.push(spectreLoadout);
 | 
			
		||||
            logger.debug("pending spectre loadout", spectreLoadout);
 | 
			
		||||
        }
 | 
			
		||||
    } else if (recipe.secretIngredientAction == "SIA_UNBRAND") {
 | 
			
		||||
        pr.SuitToUnbrand = new Types.ObjectId(startRecipeRequest.Ids[recipe.ingredients.length + 0]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
 | 
			
		||||
@ -3,15 +3,9 @@ import { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { ExportNightwave, ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus";
 | 
			
		||||
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
 | 
			
		||||
import {
 | 
			
		||||
    addItem,
 | 
			
		||||
    addMiscItems,
 | 
			
		||||
    combineInventoryChanges,
 | 
			
		||||
    getInventory,
 | 
			
		||||
    updateCurrency
 | 
			
		||||
} from "@/src/services/inventoryService";
 | 
			
		||||
import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
import { fromStoreItem, isStoreItem } from "@/src/services/itemDataService";
 | 
			
		||||
import { isStoreItem, toStoreItem } from "@/src/services/itemDataService";
 | 
			
		||||
 | 
			
		||||
export const syndicateSacrificeController: RequestHandler = async (request, response) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(request);
 | 
			
		||||
@ -57,7 +51,7 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
 | 
			
		||||
    syndicate.Title ??= 0;
 | 
			
		||||
    syndicate.Title += 1;
 | 
			
		||||
 | 
			
		||||
    if (syndicate.Title > 0 && manifest.favours.length != 0) {
 | 
			
		||||
    if (syndicate.Title > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == syndicate.Title)) {
 | 
			
		||||
        syndicate.FreeFavorsEarned ??= [];
 | 
			
		||||
        if (!syndicate.FreeFavorsEarned.includes(syndicate.Title)) {
 | 
			
		||||
            syndicate.FreeFavorsEarned.push(syndicate.Title);
 | 
			
		||||
@ -77,10 +71,13 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
 | 
			
		||||
            res.NewEpisodeReward = true;
 | 
			
		||||
            const reward = ExportNightwave.rewards[index];
 | 
			
		||||
            let rewardType = reward.uniqueName;
 | 
			
		||||
            if (isStoreItem(rewardType)) {
 | 
			
		||||
                rewardType = fromStoreItem(rewardType);
 | 
			
		||||
            if (!isStoreItem(rewardType)) {
 | 
			
		||||
                rewardType = toStoreItem(rewardType);
 | 
			
		||||
            }
 | 
			
		||||
            combineInventoryChanges(res.InventoryChanges, await addItem(inventory, rewardType, reward.itemCount ?? 1));
 | 
			
		||||
            combineInventoryChanges(
 | 
			
		||||
                res.InventoryChanges,
 | 
			
		||||
                (await handleStoreItemAcquisition(rewardType, inventory, reward.itemCount)).InventoryChanges
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,16 +1,9 @@
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import {
 | 
			
		||||
    addMiscItems,
 | 
			
		||||
    freeUpSlot,
 | 
			
		||||
    getInventory,
 | 
			
		||||
    getStandingLimit,
 | 
			
		||||
    updateStandingLimit
 | 
			
		||||
} from "@/src/services/inventoryService";
 | 
			
		||||
import { addMiscItems, addStanding, freeUpSlot, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { IOid } from "@/src/types/commonTypes";
 | 
			
		||||
import { ExportSyndicates, ExportWeapons } from "warframe-public-export-plus";
 | 
			
		||||
import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
 | 
			
		||||
@ -61,38 +54,13 @@ export const syndicateStandingBonusController: RequestHandler = async (req, res)
 | 
			
		||||
        inventoryChanges[slotBin] = { count: -1, platinum: 0, Slots: 1 };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let syndicate = inventory.Affiliations.find(x => x.Tag == request.Operation.AffiliationTag);
 | 
			
		||||
    if (!syndicate) {
 | 
			
		||||
        syndicate =
 | 
			
		||||
            inventory.Affiliations[
 | 
			
		||||
                inventory.Affiliations.push({ Tag: request.Operation.AffiliationTag, Standing: 0 }) - 1
 | 
			
		||||
            ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0);
 | 
			
		||||
    if (syndicate.Standing + gainedStanding > max) {
 | 
			
		||||
        gainedStanding = max - syndicate.Standing;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (syndicateMeta.medallionsCappedByDailyLimit) {
 | 
			
		||||
        if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) {
 | 
			
		||||
            gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin);
 | 
			
		||||
        }
 | 
			
		||||
        updateStandingLimit(inventory, syndicateMeta.dailyLimitBin, gainedStanding);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    syndicate.Standing += gainedStanding;
 | 
			
		||||
    const affiliationMod = addStanding(inventory, request.Operation.AffiliationTag, gainedStanding, true);
 | 
			
		||||
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
 | 
			
		||||
    res.json({
 | 
			
		||||
        InventoryChanges: inventoryChanges,
 | 
			
		||||
        AffiliationMods: [
 | 
			
		||||
            {
 | 
			
		||||
                Tag: request.Operation.AffiliationTag,
 | 
			
		||||
                Standing: gainedStanding
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
        AffiliationMods: [affiliationMod]
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ import { RequestHandler } from "express";
 | 
			
		||||
import { unixTimesInMs } from "@/src/constants/timeConstants";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
import { createMessage } from "@/src/services/inboxService";
 | 
			
		||||
import { config } from "@/src/services/configService";
 | 
			
		||||
 | 
			
		||||
interface ITrainingResultsRequest {
 | 
			
		||||
    numLevelsGained: number;
 | 
			
		||||
@ -22,11 +23,17 @@ const trainingResultController: RequestHandler = async (req, res): Promise<void>
 | 
			
		||||
 | 
			
		||||
    const trainingResults = getJSONfromString<ITrainingResultsRequest>(String(req.body));
 | 
			
		||||
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
    const inventory = await getInventory(accountId, "TrainingDate PlayerLevel TradesRemaining");
 | 
			
		||||
 | 
			
		||||
    if (trainingResults.numLevelsGained == 1) {
 | 
			
		||||
        inventory.TrainingDate = new Date(Date.now() + unixTimesInMs.hour * 23);
 | 
			
		||||
        let time = Date.now();
 | 
			
		||||
        if (!config.noMasteryRankUpCooldown) {
 | 
			
		||||
            time += unixTimesInMs.hour * 23;
 | 
			
		||||
        }
 | 
			
		||||
        inventory.TrainingDate = new Date(time);
 | 
			
		||||
 | 
			
		||||
        inventory.PlayerLevel += 1;
 | 
			
		||||
        inventory.TradesRemaining += 1;
 | 
			
		||||
 | 
			
		||||
        await createMessage(accountId, [
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,6 @@ import { updateShipFeature } from "@/src/services/personalRoomsService";
 | 
			
		||||
import { IUnlockShipFeatureRequest } from "@/src/types/requestTypes";
 | 
			
		||||
import { parseString } from "@/src/helpers/general";
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
			
		||||
export const unlockShipFeatureController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = parseString(req.query.accountId);
 | 
			
		||||
    const shipFeatureRequest = JSON.parse((req.body as string).toString()) as IUnlockShipFeatureRequest;
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,8 @@
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { addChallenges, addSeasonalChallengeHistory, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { addChallenges, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { ExportNightwave } from "warframe-public-export-plus";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { IAffiliationMods } from "@/src/types/purchaseTypes";
 | 
			
		||||
 | 
			
		||||
export const updateChallengeProgressController: RequestHandler = async (req, res) => {
 | 
			
		||||
@ -12,41 +10,19 @@ export const updateChallengeProgressController: RequestHandler = async (req, res
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
 | 
			
		||||
    const inventory = await getInventory(accountId, "ChallengeProgress SeasonChallengeHistory Affiliations");
 | 
			
		||||
    let affiliationMods: IAffiliationMods[] = [];
 | 
			
		||||
    if (challenges.ChallengeProgress) {
 | 
			
		||||
        addChallenges(inventory, challenges.ChallengeProgress);
 | 
			
		||||
        affiliationMods = addChallenges(inventory, challenges.ChallengeProgress, challenges.SeasonChallengeCompletions);
 | 
			
		||||
    }
 | 
			
		||||
    if (challenges.SeasonChallengeHistory) {
 | 
			
		||||
        addSeasonalChallengeHistory(inventory, challenges.SeasonChallengeHistory);
 | 
			
		||||
    }
 | 
			
		||||
    const affiliationMods: IAffiliationMods[] = [];
 | 
			
		||||
    if (challenges.ChallengeProgress && challenges.SeasonChallengeCompletions) {
 | 
			
		||||
        for (const challenge of challenges.SeasonChallengeCompletions) {
 | 
			
		||||
            // Ignore challenges that weren't completed just now
 | 
			
		||||
            if (!challenges.ChallengeProgress.find(x => challenge.challenge.indexOf(x.Name) != -1)) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const meta = ExportNightwave.challenges[challenge.challenge];
 | 
			
		||||
            logger.debug("Completed challenge", meta);
 | 
			
		||||
 | 
			
		||||
            let affiliation = inventory.Affiliations.find(x => x.Tag == ExportNightwave.affiliationTag);
 | 
			
		||||
            if (!affiliation) {
 | 
			
		||||
                affiliation =
 | 
			
		||||
                    inventory.Affiliations[
 | 
			
		||||
                        inventory.Affiliations.push({
 | 
			
		||||
                            Tag: ExportNightwave.affiliationTag,
 | 
			
		||||
                            Standing: 0
 | 
			
		||||
                        }) - 1
 | 
			
		||||
                    ];
 | 
			
		||||
            }
 | 
			
		||||
            affiliation.Standing += meta.standing;
 | 
			
		||||
 | 
			
		||||
            if (affiliationMods.length == 0) {
 | 
			
		||||
                affiliationMods.push({ Tag: ExportNightwave.affiliationTag });
 | 
			
		||||
            }
 | 
			
		||||
            affiliationMods[0].Standing ??= 0;
 | 
			
		||||
            affiliationMods[0].Standing += meta.standing;
 | 
			
		||||
        challenges.SeasonChallengeHistory.forEach(({ challenge, id }) => {
 | 
			
		||||
            const itemIndex = inventory.SeasonChallengeHistory.findIndex(i => i.challenge === challenge);
 | 
			
		||||
            if (itemIndex !== -1) {
 | 
			
		||||
                inventory.SeasonChallengeHistory[itemIndex].id = id;
 | 
			
		||||
            } else {
 | 
			
		||||
                inventory.SeasonChallengeHistory.push({ challenge, id });
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,6 @@ import { updateQuestKey, IUpdateQuestRequest } from "@/src/services/questService
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
			
		||||
export const updateQuestController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = parseString(req.query.accountId);
 | 
			
		||||
    const updateQuestRequest = getJSONfromString<IUpdateQuestRequest>((req.body as string).toString());
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { addMiscItems, addRecipes, getInventory, updateCurrency } from "@/src/services/inventoryService";
 | 
			
		||||
import { getRecipeByResult } from "@/src/services/itemDataService";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
import { addInfestedFoundryXP } from "./infestedFoundryController";
 | 
			
		||||
import { addInfestedFoundryXP, applyCheatsToInfestedFoundry } from "@/src/services/infestedFoundryService";
 | 
			
		||||
import { config } from "@/src/services/configService";
 | 
			
		||||
 | 
			
		||||
export const upgradesController: RequestHandler = async (req, res) => {
 | 
			
		||||
@ -25,7 +25,13 @@ export const upgradesController: RequestHandler = async (req, res) => {
 | 
			
		||||
            operation.UpgradeRequirement == "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker"
 | 
			
		||||
        ) {
 | 
			
		||||
            updateCurrency(inventory, 10, true);
 | 
			
		||||
        } else {
 | 
			
		||||
        } else if (
 | 
			
		||||
            operation.OperationType != "UOT_SWAP_POLARITY" &&
 | 
			
		||||
            operation.OperationType != "UOT_ABILITY_OVERRIDE"
 | 
			
		||||
        ) {
 | 
			
		||||
            if (!operation.UpgradeRequirement) {
 | 
			
		||||
                throw new Error(`${operation.OperationType} operation should be free?`);
 | 
			
		||||
            }
 | 
			
		||||
            addMiscItems(inventory, [
 | 
			
		||||
                {
 | 
			
		||||
                    ItemType: operation.UpgradeRequirement,
 | 
			
		||||
@ -66,6 +72,7 @@ export const upgradesController: RequestHandler = async (req, res) => {
 | 
			
		||||
 | 
			
		||||
            inventoryChanges.Recipes = recipeChanges;
 | 
			
		||||
            inventoryChanges.InfestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry;
 | 
			
		||||
            applyCheatsToInfestedFoundry(inventoryChanges.InfestedFoundry!);
 | 
			
		||||
        } else
 | 
			
		||||
            switch (operation.UpgradeRequirement) {
 | 
			
		||||
                case "/Lotus/Types/Items/MiscItems/OrokinReactor":
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										98
									
								
								src/controllers/custom/addModularEquipmentController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								src/controllers/custom/addModularEquipmentController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,98 @@
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import {
 | 
			
		||||
    getInventory,
 | 
			
		||||
    addEquipment,
 | 
			
		||||
    occupySlot,
 | 
			
		||||
    productCategoryToInventoryBin,
 | 
			
		||||
    applyDefaultUpgrades
 | 
			
		||||
} from "@/src/services/inventoryService";
 | 
			
		||||
import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper";
 | 
			
		||||
import { getDefaultUpgrades } from "@/src/services/itemDataService";
 | 
			
		||||
import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes";
 | 
			
		||||
import { ExportWeapons } from "warframe-public-export-plus";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const addModularEquipmentController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const requiredFields = new Set();
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const request = req.body as IAddModularEquipmentRequest;
 | 
			
		||||
    const category = modularWeaponTypes[request.ItemType];
 | 
			
		||||
    const inventoryBin = productCategoryToInventoryBin(category)!;
 | 
			
		||||
    requiredFields.add(category);
 | 
			
		||||
    requiredFields.add(inventoryBin);
 | 
			
		||||
 | 
			
		||||
    request.ModularParts.forEach(part => {
 | 
			
		||||
        if (ExportWeapons[part].gunType) {
 | 
			
		||||
            if (category == "LongGuns") {
 | 
			
		||||
                request.ItemType = {
 | 
			
		||||
                    GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary",
 | 
			
		||||
                    GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun",
 | 
			
		||||
                    GT_BEAM: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam"
 | 
			
		||||
                }[ExportWeapons[part].gunType];
 | 
			
		||||
            } else {
 | 
			
		||||
                request.ItemType = {
 | 
			
		||||
                    GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary",
 | 
			
		||||
                    GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun",
 | 
			
		||||
                    GT_BEAM: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam"
 | 
			
		||||
                }[ExportWeapons[part].gunType];
 | 
			
		||||
            }
 | 
			
		||||
        } else if (request.ItemType == "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetPowerSuit") {
 | 
			
		||||
            if (part.includes("ZanukaPetPartHead")) {
 | 
			
		||||
                request.ItemType = {
 | 
			
		||||
                    "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA":
 | 
			
		||||
                        "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit",
 | 
			
		||||
                    "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB":
 | 
			
		||||
                        "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit",
 | 
			
		||||
                    "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC":
 | 
			
		||||
                        "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit"
 | 
			
		||||
                }[part]!;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    const defaultUpgrades = getDefaultUpgrades(request.ModularParts);
 | 
			
		||||
    if (defaultUpgrades) {
 | 
			
		||||
        requiredFields.add("RawUpgrades");
 | 
			
		||||
    }
 | 
			
		||||
    const defaultWeaponsMap: Record<string, string[]> = {
 | 
			
		||||
        "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit": [
 | 
			
		||||
            "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetMeleeWeaponIP"
 | 
			
		||||
        ],
 | 
			
		||||
        "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit": [
 | 
			
		||||
            "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetMeleeWeaponIS"
 | 
			
		||||
        ],
 | 
			
		||||
        "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit": [
 | 
			
		||||
            "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetMeleeWeaponPS"
 | 
			
		||||
        ]
 | 
			
		||||
    };
 | 
			
		||||
    const defaultWeapons = defaultWeaponsMap[request.ItemType] as string[] | undefined;
 | 
			
		||||
    if (defaultWeapons) {
 | 
			
		||||
        for (const defaultWeapon of defaultWeapons) {
 | 
			
		||||
            const category = ExportWeapons[defaultWeapon].productCategory;
 | 
			
		||||
            requiredFields.add(category);
 | 
			
		||||
            requiredFields.add(productCategoryToInventoryBin(category));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const inventory = await getInventory(accountId, Array.from(requiredFields).join(" "));
 | 
			
		||||
    if (defaultWeapons) {
 | 
			
		||||
        for (const defaultWeapon of defaultWeapons) {
 | 
			
		||||
            const category = ExportWeapons[defaultWeapon].productCategory;
 | 
			
		||||
            addEquipment(inventory, category, defaultWeapon);
 | 
			
		||||
            occupySlot(inventory, productCategoryToInventoryBin(category)!, true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const defaultOverwrites: Partial<IEquipmentDatabase> = {
 | 
			
		||||
        Configs: applyDefaultUpgrades(inventory, defaultUpgrades)
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    addEquipment(inventory, category, request.ItemType, request.ModularParts, undefined, defaultOverwrites);
 | 
			
		||||
    occupySlot(inventory, inventoryBin, true);
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IAddModularEquipmentRequest {
 | 
			
		||||
    ItemType: string;
 | 
			
		||||
    ModularParts: string[];
 | 
			
		||||
}
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { Account } from "@/src/models/loginModel";
 | 
			
		||||
import { Account, Ignore } from "@/src/models/loginModel";
 | 
			
		||||
import { Inbox } from "@/src/models/inboxModel";
 | 
			
		||||
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
 | 
			
		||||
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
 | 
			
		||||
@ -9,13 +9,22 @@ import { Ship } from "@/src/models/shipModel";
 | 
			
		||||
import { Stats } from "@/src/models/statsModel";
 | 
			
		||||
import { GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { Leaderboard } from "@/src/models/leaderboardModel";
 | 
			
		||||
import { deleteGuild } from "@/src/services/guildService";
 | 
			
		||||
 | 
			
		||||
export const deleteAccountController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    // TODO: Handle the account being the creator of a guild
 | 
			
		||||
 | 
			
		||||
    // If account is the founding warlord of a guild, delete that guild as well.
 | 
			
		||||
    const guildMember = await GuildMember.findOne({ accountId, rank: 0, status: 0 });
 | 
			
		||||
    if (guildMember) {
 | 
			
		||||
        await deleteGuild(guildMember.guildId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        Account.deleteOne({ _id: accountId }),
 | 
			
		||||
        GuildMember.deleteMany({ accountId: accountId }),
 | 
			
		||||
        Ignore.deleteMany({ ignorer: accountId }),
 | 
			
		||||
        Ignore.deleteMany({ ignoree: accountId }),
 | 
			
		||||
        Inbox.deleteMany({ ownerId: accountId }),
 | 
			
		||||
        Inventory.deleteOne({ accountOwnerId: accountId }),
 | 
			
		||||
        Leaderboard.deleteMany({ ownerId: accountId }),
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { Guild, GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { getAccountForRequest, isAdministrator } from "@/src/services/loginService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
@ -12,9 +12,19 @@ export const getAccountInfoController: RequestHandler = async (req, res) => {
 | 
			
		||||
    }
 | 
			
		||||
    const guildMember = await GuildMember.findOne({ accountId: account._id, status: 0 }, "guildId rank");
 | 
			
		||||
    if (guildMember) {
 | 
			
		||||
        const guild = (await Guild.findById(guildMember.guildId, "Ranks"))!;
 | 
			
		||||
        const guild = (await Guild.findById(guildMember.guildId, "Ranks AllianceId"))!;
 | 
			
		||||
        info.GuildId = guildMember.guildId.toString();
 | 
			
		||||
        info.GuildPermissions = guild.Ranks[guildMember.rank].Permissions;
 | 
			
		||||
        info.GuildRank = guildMember.rank;
 | 
			
		||||
        if (guild.AllianceId) {
 | 
			
		||||
            //const alliance = (await Alliance.findById(guild.AllianceId))!;
 | 
			
		||||
            const allianceMember = (await AllianceMember.findOne({
 | 
			
		||||
                allianceId: guild.AllianceId,
 | 
			
		||||
                guildId: guild._id
 | 
			
		||||
            }))!;
 | 
			
		||||
            info.AllianceId = guild.AllianceId.toString();
 | 
			
		||||
            info.AlliancePermissions = allianceMember.Permissions;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    res.json(info);
 | 
			
		||||
};
 | 
			
		||||
@ -24,4 +34,7 @@ interface IAccountInfo {
 | 
			
		||||
    IsAdministrator?: boolean;
 | 
			
		||||
    GuildId?: string;
 | 
			
		||||
    GuildPermissions?: number;
 | 
			
		||||
    GuildRank?: number;
 | 
			
		||||
    AllianceId?: string;
 | 
			
		||||
    AlliancePermissions?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3,8 +3,10 @@ import { getDict, getItemName, getString } from "@/src/services/itemDataService"
 | 
			
		||||
import {
 | 
			
		||||
    ExportArcanes,
 | 
			
		||||
    ExportAvionics,
 | 
			
		||||
    ExportCustoms,
 | 
			
		||||
    ExportDrones,
 | 
			
		||||
    ExportGear,
 | 
			
		||||
    ExportKeys,
 | 
			
		||||
    ExportMisc,
 | 
			
		||||
    ExportRailjackWeapons,
 | 
			
		||||
    ExportRecipes,
 | 
			
		||||
@ -25,6 +27,8 @@ interface ListedItem {
 | 
			
		||||
    fusionLimit?: number;
 | 
			
		||||
    exalted?: string[];
 | 
			
		||||
    badReason?: "starter" | "frivolous" | "notraw";
 | 
			
		||||
    partType?: string;
 | 
			
		||||
    chainLength?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const relicQualitySuffixes: Record<TRelicQuality, string> = {
 | 
			
		||||
@ -50,6 +54,8 @@ const getItemListsController: RequestHandler = (req, response) => {
 | 
			
		||||
    res.MechSuits = [];
 | 
			
		||||
    res.miscitems = [];
 | 
			
		||||
    res.Syndicates = [];
 | 
			
		||||
    res.OperatorAmps = [];
 | 
			
		||||
    res.QuestKeys = [];
 | 
			
		||||
    for (const [uniqueName, item] of Object.entries(ExportWarframes)) {
 | 
			
		||||
        res[item.productCategory].push({
 | 
			
		||||
            uniqueName,
 | 
			
		||||
@ -66,20 +72,11 @@ const getItemListsController: RequestHandler = (req, response) => {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    for (const [uniqueName, item] of Object.entries(ExportWeapons)) {
 | 
			
		||||
        if (
 | 
			
		||||
            uniqueName.split("/")[4] == "OperatorAmplifiers" ||
 | 
			
		||||
            uniqueName.split("/")[5] == "SUModularSecondarySet1" ||
 | 
			
		||||
            uniqueName.split("/")[5] == "SUModularPrimarySet1" ||
 | 
			
		||||
            uniqueName.split("/")[5] == "InfKitGun" ||
 | 
			
		||||
            uniqueName.split("/")[5] == "HoverboardParts" ||
 | 
			
		||||
            uniqueName.split("/")[5] == "ModularMelee01" ||
 | 
			
		||||
            uniqueName.split("/")[5] == "ModularMelee02" ||
 | 
			
		||||
            uniqueName.split("/")[5] == "ModularMeleeInfested" ||
 | 
			
		||||
            uniqueName.split("/")[6] == "CreaturePetParts"
 | 
			
		||||
        ) {
 | 
			
		||||
        if (item.partType) {
 | 
			
		||||
            res.ModularParts.push({
 | 
			
		||||
                uniqueName,
 | 
			
		||||
                name: getString(item.name, lang)
 | 
			
		||||
                name: getString(item.name, lang),
 | 
			
		||||
                partType: item.partType
 | 
			
		||||
            });
 | 
			
		||||
            if (uniqueName.split("/")[5] != "SentTrainingAmplifier") {
 | 
			
		||||
                res.miscitems.push({
 | 
			
		||||
@ -94,7 +91,8 @@ const getItemListsController: RequestHandler = (req, response) => {
 | 
			
		||||
                item.productCategory == "Melee" ||
 | 
			
		||||
                item.productCategory == "SpaceGuns" ||
 | 
			
		||||
                item.productCategory == "SpaceMelee" ||
 | 
			
		||||
                item.productCategory == "SentinelWeapons"
 | 
			
		||||
                item.productCategory == "SentinelWeapons" ||
 | 
			
		||||
                item.productCategory == "OperatorAmps"
 | 
			
		||||
            ) {
 | 
			
		||||
                res[item.productCategory].push({
 | 
			
		||||
                    uniqueName,
 | 
			
		||||
@ -121,6 +119,7 @@ const getItemListsController: RequestHandler = (req, response) => {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (
 | 
			
		||||
            name &&
 | 
			
		||||
            uniqueName.substr(0, 30) != "/Lotus/Types/Game/Projections/" &&
 | 
			
		||||
            uniqueName != "/Lotus/Types/Gameplay/EntratiLab/Resources/EntratiLanthornBundle"
 | 
			
		||||
        ) {
 | 
			
		||||
@ -152,9 +151,11 @@ const getItemListsController: RequestHandler = (req, response) => {
 | 
			
		||||
        if (!item.hidden) {
 | 
			
		||||
            const resultName = getItemName(item.resultType);
 | 
			
		||||
            if (resultName) {
 | 
			
		||||
                let itemName = getString(resultName, lang);
 | 
			
		||||
                if (item.num > 1) itemName = `${itemName} X ${item.num}`;
 | 
			
		||||
                res.miscitems.push({
 | 
			
		||||
                    uniqueName: uniqueName,
 | 
			
		||||
                    name: recipeNameTemplate.replace("|ITEM|", getString(resultName, lang))
 | 
			
		||||
                    name: recipeNameTemplate.replace("|ITEM|", itemName)
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@ -171,6 +172,12 @@ const getItemListsController: RequestHandler = (req, response) => {
 | 
			
		||||
            name: getString(item.name, lang)
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    for (const [uniqueName, item] of Object.entries(ExportCustoms)) {
 | 
			
		||||
        res.miscitems.push({
 | 
			
		||||
            uniqueName: uniqueName,
 | 
			
		||||
            name: getString(item.name, lang)
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    res.mods = [];
 | 
			
		||||
    for (const [uniqueName, upgrade] of Object.entries(ExportUpgrades)) {
 | 
			
		||||
@ -213,6 +220,20 @@ const getItemListsController: RequestHandler = (req, response) => {
 | 
			
		||||
            name: getString(syndicate.name, lang)
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    for (const [uniqueName, key] of Object.entries(ExportKeys)) {
 | 
			
		||||
        if (key.chainStages) {
 | 
			
		||||
            res.QuestKeys.push({
 | 
			
		||||
                uniqueName,
 | 
			
		||||
                name: getString(key.name || "", lang),
 | 
			
		||||
                chainLength: key.chainStages.length
 | 
			
		||||
            });
 | 
			
		||||
        } else if (key.name) {
 | 
			
		||||
            res.miscitems.push({
 | 
			
		||||
                uniqueName,
 | 
			
		||||
                name: getString(key.name, lang)
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    response.json({
 | 
			
		||||
        archonCrystalUpgrades,
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,11 @@
 | 
			
		||||
import { addString } from "@/src/controllers/api/inventoryController";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { addQuestKey, completeQuest, IUpdateQuestRequest, updateQuestKey } from "@/src/services/questService";
 | 
			
		||||
import {
 | 
			
		||||
    addQuestKey,
 | 
			
		||||
    completeQuest,
 | 
			
		||||
    giveKeyChainMissionReward,
 | 
			
		||||
    giveKeyChainStageTriggered
 | 
			
		||||
} from "@/src/services/questService";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { ExportKeys } from "warframe-public-export-plus";
 | 
			
		||||
@ -9,13 +13,17 @@ import { ExportKeys } from "warframe-public-export-plus";
 | 
			
		||||
export const manageQuestsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const operation = req.query.operation as
 | 
			
		||||
        | "unlockAll"
 | 
			
		||||
        | "completeAll"
 | 
			
		||||
        | "ResetAll"
 | 
			
		||||
        | "completeAllUnlocked"
 | 
			
		||||
        | "updateKey"
 | 
			
		||||
        | "giveAll";
 | 
			
		||||
    const questKeyUpdate = req.body as IUpdateQuestRequest["QuestKeys"];
 | 
			
		||||
        | "resetAll"
 | 
			
		||||
        | "giveAll"
 | 
			
		||||
        | "completeKey"
 | 
			
		||||
        | "deleteKey"
 | 
			
		||||
        | "resetKey"
 | 
			
		||||
        | "prevStage"
 | 
			
		||||
        | "nextStage"
 | 
			
		||||
        | "setInactive";
 | 
			
		||||
 | 
			
		||||
    const questItemType = req.query.itemType as string;
 | 
			
		||||
 | 
			
		||||
    const allQuestKeys: string[] = [];
 | 
			
		||||
    for (const [k, v] of Object.entries(ExportKeys)) {
 | 
			
		||||
@ -26,47 +34,15 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
 | 
			
		||||
    switch (operation) {
 | 
			
		||||
        case "updateKey": {
 | 
			
		||||
            //TODO: if this is intended to be used, one needs to add a updateQuestKeyMultiple, the game does never intend to do it, so it errors for multiple keys.
 | 
			
		||||
            await updateQuestKey(inventory, questKeyUpdate);
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case "unlockAll": {
 | 
			
		||||
            for (const questKey of allQuestKeys) {
 | 
			
		||||
                addQuestKey(inventory, { ItemType: questKey, Completed: false, unlock: true, Progress: [] });
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case "completeAll": {
 | 
			
		||||
            logger.info("completing all quests..");
 | 
			
		||||
            for (const questKey of allQuestKeys) {
 | 
			
		||||
                try {
 | 
			
		||||
                    await completeQuest(inventory, questKey);
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    if (error instanceof Error) {
 | 
			
		||||
                        logger.error(
 | 
			
		||||
                            `Something went wrong completing quest ${questKey}, probably could not add some item`
 | 
			
		||||
                        );
 | 
			
		||||
                        logger.error(error.message);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                //Skip "Watch The Maker"
 | 
			
		||||
                if (questKey === "/Lotus/Types/Keys/NewWarIntroQuest/NewWarIntroKeyChain") {
 | 
			
		||||
                    addString(
 | 
			
		||||
                        inventory.NodeIntrosCompleted,
 | 
			
		||||
                        "/Lotus/Levels/Cinematics/NewWarIntro/NewWarStageTwo.level"
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (questKey === "/Lotus/Types/Keys/ArchwingQuest/ArchwingQuestKeyChain") {
 | 
			
		||||
                    inventory.ArchwingEnabled = true;
 | 
			
		||||
            if (allQuestKeys.includes(questItemType)) {
 | 
			
		||||
                for (const questKey of inventory.QuestKeys) {
 | 
			
		||||
                    await completeQuest(inventory, questKey.ItemType);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case "ResetAll": {
 | 
			
		||||
            logger.info("resetting all quests..");
 | 
			
		||||
        case "resetAll": {
 | 
			
		||||
            for (const questKey of inventory.QuestKeys) {
 | 
			
		||||
                questKey.Completed = false;
 | 
			
		||||
                questKey.Progress = [];
 | 
			
		||||
@ -75,40 +51,110 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
 | 
			
		||||
            inventory.ActiveQuest = "";
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case "completeAllUnlocked": {
 | 
			
		||||
            logger.info("completing all unlocked quests..");
 | 
			
		||||
            for (const questKey of inventory.QuestKeys) {
 | 
			
		||||
                try {
 | 
			
		||||
                    await completeQuest(inventory, questKey.ItemType);
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    if (error instanceof Error) {
 | 
			
		||||
                        logger.error(
 | 
			
		||||
                            `Something went wrong completing quest ${questKey.ItemType}, probably could not add some item`
 | 
			
		||||
                        );
 | 
			
		||||
                        logger.error(error.message);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                //Skip "Watch The Maker"
 | 
			
		||||
                if (questKey.ItemType === "/Lotus/Types/Keys/NewWarIntroQuest/NewWarIntroKeyChain") {
 | 
			
		||||
                    addString(
 | 
			
		||||
                        inventory.NodeIntrosCompleted,
 | 
			
		||||
                        "/Lotus/Levels/Cinematics/NewWarIntro/NewWarStageTwo.level"
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (questKey.ItemType === "/Lotus/Types/Keys/ArchwingQuest/ArchwingQuestKeyChain") {
 | 
			
		||||
                    inventory.ArchwingEnabled = true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case "giveAll": {
 | 
			
		||||
            for (const questKey of allQuestKeys) {
 | 
			
		||||
                addQuestKey(inventory, { ItemType: questKey });
 | 
			
		||||
            allQuestKeys.forEach(questKey => addQuestKey(inventory, { ItemType: questKey }));
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case "deleteKey": {
 | 
			
		||||
            const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType);
 | 
			
		||||
            if (!questKey) {
 | 
			
		||||
                logger.error(`Quest key not found in inventory: ${questItemType}`);
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
            inventory.QuestKeys.pull({ ItemType: questItemType });
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case "completeKey": {
 | 
			
		||||
            if (allQuestKeys.includes(questItemType)) {
 | 
			
		||||
                const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType);
 | 
			
		||||
                if (!questKey) {
 | 
			
		||||
                    logger.error(`Quest key not found in inventory: ${questItemType}`);
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await completeQuest(inventory, questItemType);
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case "resetKey": {
 | 
			
		||||
            if (allQuestKeys.includes(questItemType)) {
 | 
			
		||||
                const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType);
 | 
			
		||||
                if (!questKey) {
 | 
			
		||||
                    logger.error(`Quest key not found in inventory: ${questItemType}`);
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                questKey.Completed = false;
 | 
			
		||||
                questKey.Progress = [];
 | 
			
		||||
                questKey.CompletionDate = undefined;
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case "prevStage": {
 | 
			
		||||
            if (allQuestKeys.includes(questItemType)) {
 | 
			
		||||
                const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType);
 | 
			
		||||
                if (!questKey) {
 | 
			
		||||
                    logger.error(`Quest key not found in inventory: ${questItemType}`);
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
                if (!questKey.Progress) break;
 | 
			
		||||
 | 
			
		||||
                if (questKey.Completed) {
 | 
			
		||||
                    questKey.Completed = false;
 | 
			
		||||
                    questKey.CompletionDate = undefined;
 | 
			
		||||
                }
 | 
			
		||||
                questKey.Progress.pop();
 | 
			
		||||
                const stage = questKey.Progress.length - 1;
 | 
			
		||||
                if (stage > 0) {
 | 
			
		||||
                    await giveKeyChainStageTriggered(inventory, {
 | 
			
		||||
                        KeyChain: questKey.ItemType,
 | 
			
		||||
                        ChainStage: stage
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case "nextStage": {
 | 
			
		||||
            if (allQuestKeys.includes(questItemType)) {
 | 
			
		||||
                const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType);
 | 
			
		||||
                const questManifest = ExportKeys[questItemType];
 | 
			
		||||
                if (!questKey) {
 | 
			
		||||
                    logger.error(`Quest key not found in inventory: ${questItemType}`);
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
                if (!questKey.Progress) break;
 | 
			
		||||
 | 
			
		||||
                const currentStage = questKey.Progress.length;
 | 
			
		||||
                if (currentStage + 1 == questManifest.chainStages?.length) {
 | 
			
		||||
                    logger.debug(`Trying to complete last stage with nextStage, calling completeQuest instead`);
 | 
			
		||||
                    await completeQuest(inventory, questKey.ItemType);
 | 
			
		||||
                } else {
 | 
			
		||||
                    const progress = {
 | 
			
		||||
                        c: questManifest.chainStages![currentStage].key ? -1 : 0,
 | 
			
		||||
                        i: false,
 | 
			
		||||
                        m: false,
 | 
			
		||||
                        b: []
 | 
			
		||||
                    };
 | 
			
		||||
                    questKey.Progress.push(progress);
 | 
			
		||||
 | 
			
		||||
                    await giveKeyChainStageTriggered(inventory, {
 | 
			
		||||
                        KeyChain: questKey.ItemType,
 | 
			
		||||
                        ChainStage: currentStage
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    if (currentStage > 0) {
 | 
			
		||||
                        await giveKeyChainMissionReward(inventory, {
 | 
			
		||||
                            KeyChain: questKey.ItemType,
 | 
			
		||||
                            ChainStage: currentStage - 1
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case "setInactive":
 | 
			
		||||
            inventory.ActiveQuest = "";
 | 
			
		||||
            break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
export const popArchonCrystalUpgradeController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
    const suit = inventory.Suits.find(suit => suit._id.toString() == (req.query.oid as string));
 | 
			
		||||
    const suit = inventory.Suits.id(req.query.oid as string);
 | 
			
		||||
    if (suit && suit.ArchonCrystalUpgrades) {
 | 
			
		||||
        suit.ArchonCrystalUpgrades = suit.ArchonCrystalUpgrades.filter(
 | 
			
		||||
            x => x.UpgradeType != (req.query.type as string)
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
export const pushArchonCrystalUpgradeController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
    const suit = inventory.Suits.find(suit => suit._id.toString() == (req.query.oid as string));
 | 
			
		||||
    const suit = inventory.Suits.id(req.query.oid as string);
 | 
			
		||||
    if (suit) {
 | 
			
		||||
        suit.ArchonCrystalUpgrades ??= [];
 | 
			
		||||
        const count = (req.query.count as number | undefined) ?? 1;
 | 
			
		||||
 | 
			
		||||
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