merge upstream
This commit is contained in:
commit
862465ea3c
18
.eslintrc
18
.eslintrc
@ -11,17 +11,17 @@
|
||||
"node": true
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/explicit-function-return-type": "warn",
|
||||
"@typescript-eslint/restrict-template-expressions": "warn",
|
||||
"@typescript-eslint/restrict-plus-operands": "warn",
|
||||
"@typescript-eslint/no-unsafe-member-access": "warn",
|
||||
"@typescript-eslint/explicit-function-return-type": "error",
|
||||
"@typescript-eslint/restrict-template-expressions": "error",
|
||||
"@typescript-eslint/restrict-plus-operands": "error",
|
||||
"@typescript-eslint/no-unsafe-member-access": "error",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "caughtErrors": "none" }],
|
||||
"@typescript-eslint/no-unsafe-argument": "error",
|
||||
"@typescript-eslint/no-unsafe-call": "warn",
|
||||
"@typescript-eslint/no-unsafe-assignment": "warn",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"no-loss-of-precision": "warn",
|
||||
"@typescript-eslint/no-unnecessary-condition": "warn",
|
||||
"@typescript-eslint/no-unsafe-call": "error",
|
||||
"@typescript-eslint/no-unsafe-assignment": "error",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"no-loss-of-precision": "error",
|
||||
"@typescript-eslint/no-unnecessary-condition": "error",
|
||||
"@typescript-eslint/no-base-to-string": "off",
|
||||
"no-case-declarations": "error",
|
||||
"prettier/prettier": "error",
|
||||
|
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@ -1,6 +1,7 @@
|
||||
name: Build
|
||||
on:
|
||||
push: {}
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request: {}
|
||||
jobs:
|
||||
build:
|
||||
|
25
.github/workflows/docker.yml
vendored
25
.github/workflows/docker.yml
vendored
@ -4,9 +4,9 @@ on:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
docker:
|
||||
docker-amd64:
|
||||
if: github.repository == 'OpenWF/SpaceNinjaServer'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: amd64
|
||||
steps:
|
||||
- name: Set up Docker buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
@ -18,8 +18,27 @@ jobs:
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: |
|
||||
openwf/spaceninjaserver:latest
|
||||
openwf/spaceninjaserver:${{ github.sha }}
|
||||
docker-arm64:
|
||||
if: github.repository == 'OpenWF/SpaceNinjaServer'
|
||||
runs-on: arm64
|
||||
steps:
|
||||
- name: Set up Docker buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Log in to container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: openwf
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
openwf/spaceninjaserver:latest-arm64
|
||||
openwf/spaceninjaserver:${{ github.sha }}-arm64
|
||||
|
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@ -8,8 +8,7 @@
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug and Watch",
|
||||
"runtimeArgs": ["-r", "tsconfig-paths/register", "-r", "ts-node/register", "--watch-path", "src"],
|
||||
"args": ["${workspaceFolder}/src/index.ts"],
|
||||
"args": ["${workspaceFolder}/scripts/dev.js"],
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
|
17
AGENTS.md
Normal file
17
AGENTS.md
Normal file
@ -0,0 +1,17 @@
|
||||
## In General
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Use `npm i` or `npm ci` to install all dependencies.
|
||||
|
||||
### Testing
|
||||
|
||||
Use `npm run verify` to verify that your changes pass TypeScript's checks.
|
||||
|
||||
### Formatting
|
||||
|
||||
Use `npm run prettier` to ensure your formatting matches the expected format. Failing to do so will cause CI failure.
|
||||
|
||||
## WebUI Specific
|
||||
|
||||
The translation system is designed around additions being made to `static/webui/translations/en.js`. They are copied over for translation via `npm run update-translations`. DO NOT produce non-English strings; we want them to be translated by humans who can understand the full context.
|
52
Dockerfile
52
Dockerfile
@ -1,53 +1,11 @@
|
||||
FROM node:18-alpine3.19
|
||||
FROM node:24-alpine3.21
|
||||
|
||||
ENV APP_MONGODB_URL=mongodb://mongodb:27017/openWF
|
||||
ENV APP_MY_ADDRESS=localhost
|
||||
ENV APP_HTTP_PORT=80
|
||||
ENV APP_HTTPS_PORT=443
|
||||
ENV APP_AUTO_CREATE_ACCOUNT=true
|
||||
ENV APP_SKIP_TUTORIAL=false
|
||||
ENV APP_SKIP_ALL_DIALOGUE=false
|
||||
ENV APP_UNLOCK_ALL_SCANS=false
|
||||
ENV APP_UNLOCK_ALL_MISSIONS=false
|
||||
ENV APP_INFINITE_CREDITS=false
|
||||
ENV APP_INFINITE_PLATINUM=false
|
||||
ENV APP_INFINITE_ENDO=false
|
||||
ENV APP_INFINITE_REGAL_AYA=false
|
||||
ENV APP_INFINITE_HELMINTH_MATERIALS=false
|
||||
ENV APP_CLAIMING_BLUEPRINT_REFUNDS_INGREDIENTS=false
|
||||
ENV APP_DONT_SUBTRACT_VOIDTRACES=false
|
||||
ENV APP_DONT_SUBTRACT_CONSUMABLES=false
|
||||
ENV APP_UNLOCK_ALL_SHIP_FEATURES=false
|
||||
ENV APP_UNLOCK_ALL_SHIP_DECORATIONS=false
|
||||
ENV APP_UNLOCK_ALL_FLAVOUR_ITEMS=false
|
||||
ENV APP_UNLOCK_ALL_SKINS=false
|
||||
ENV APP_UNLOCK_ALL_CAPTURA_SCENES=false
|
||||
ENV APP_UNIVERSAL_POLARITY_EVERYWHERE=false
|
||||
ENV APP_UNLOCK_DOUBLE_CAPACITY_POTATOES_EVERYWHERE=false
|
||||
ENV APP_UNLOCK_EXILUS_EVERYWHERE=false
|
||||
ENV APP_UNLOCK_ARCANES_EVERYWHERE=false
|
||||
ENV APP_NO_DAILY_FOCUS_LIMIT=false
|
||||
ENV APP_NO_ARGON_CRYSTAL_DECAY=false
|
||||
ENV APP_NO_MASTERY_RANK_UP_COOLDOWN=false
|
||||
ENV APP_NO_VENDOR_PURCHASE_LIMITS=true
|
||||
ENV APP_NO_DEATH_MARKS=false
|
||||
ENV APP_NO_KIM_COOLDOWNS=false
|
||||
ENV APP_SYNDICATE_MISSIONS_REPEATABLE=false
|
||||
ENV APP_INSTANT_FINISH_RIVEN_CHALLENGE=false
|
||||
ENV APP_INSTANT_RESOURCE_EXTRACTOR_DRONES=false
|
||||
ENV APP_NO_RESOURCE_EXTRACTOR_DRONES_DAMAGE=false
|
||||
ENV APP_SKIP_CLAN_KEY_CRAFTING=false
|
||||
ENV APP_NO_DOJO_ROOM_BUILD_STAGE=false
|
||||
ENV APP_NO_DECO_BUILD_STAGE=false
|
||||
ENV APP_FAST_DOJO_ROOM_DESTRUCTION=false
|
||||
ENV APP_NO_DOJO_RESEARCH_COSTS=false
|
||||
ENV APP_NO_DOJO_RESEARCH_TIME=false
|
||||
ENV APP_FAST_CLAN_ASCENSION=false
|
||||
ENV APP_SPOOF_MASTERY_RANK=-1
|
||||
|
||||
RUN apk add --no-cache bash sed wget jq
|
||||
RUN apk add --no-cache bash jq
|
||||
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
|
||||
RUN npm i --omit=dev
|
||||
RUN npm run build
|
||||
|
||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||
|
@ -16,6 +16,7 @@ SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [confi
|
||||
- `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`.
|
||||
- `worldState.eidolonOverride` can be set to `day` or `night` to lock the time to day/fass and night/vome on Plains of Eidolon/Cambion Drift.
|
||||
- `worldState.vallisOverride` can be set to `warm` or `cold` to lock the temperature on Orb Vallis.
|
||||
- `worldState.duviriOverride` can be set to `joy`, `anger`, `envy`, `sorrow`, or `fear` to lock the Duviri spiral.
|
||||
- `worldState.nightwaveOverride` will lock the nightwave season, assuming the client is new enough for it. Valid values:
|
||||
- `RadioLegionIntermission13Syndicate` for Nora's Mix Vol. 9
|
||||
- `RadioLegionIntermission12Syndicate` for Nora's Mix Vol. 8
|
||||
@ -33,3 +34,5 @@ SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [confi
|
||||
- `RadioLegion2Syndicate` for The Emissary
|
||||
- `RadioLegionIntermissionSyndicate` for Intermission I
|
||||
- `RadioLegionSyndicate` for The Wolf of Saturn Six
|
||||
- `allTheFissures` can be set to `normal` or `hard` to enable all fissures either in normal or steel path, respectively.
|
||||
- `worldState.circuitGameModes` can be set to an array of game modes which will override the otherwise-random pattern in The Circuit. Valid element values are `Survival`, `VoidFlood`, `Excavation`, `Defense`, `Exterminate`, `Assassination`, and `Alchemy`.
|
||||
|
@ -13,13 +13,16 @@
|
||||
"skipTutorial": false,
|
||||
"skipAllDialogue": false,
|
||||
"unlockAllScans": false,
|
||||
"unlockAllMissions": false,
|
||||
"infiniteCredits": false,
|
||||
"infinitePlatinum": false,
|
||||
"infiniteEndo": false,
|
||||
"infiniteRegalAya": false,
|
||||
"infiniteHelminthMaterials": false,
|
||||
"claimingBlueprintRefundsIngredients": false,
|
||||
"dontSubtractPurchaseCreditCost": false,
|
||||
"dontSubtractPurchasePlatinumCost": false,
|
||||
"dontSubtractPurchaseItemCost": false,
|
||||
"dontSubtractPurchaseStandingCost": false,
|
||||
"dontSubtractVoidTraces": false,
|
||||
"dontSubtractConsumables": false,
|
||||
"unlockAllShipFeatures": false,
|
||||
@ -35,10 +38,14 @@
|
||||
"noDailyFocusLimit": false,
|
||||
"noArgonCrystalDecay": false,
|
||||
"noMasteryRankUpCooldown": false,
|
||||
"noVendorPurchaseLimits": true,
|
||||
"noVendorPurchaseLimits": false,
|
||||
"noDeathMarks": false,
|
||||
"noKimCooldowns": false,
|
||||
"fullyStockedVendors": false,
|
||||
"baroAlwaysAvailable": false,
|
||||
"baroFullyStocked": false,
|
||||
"syndicateMissionsRepeatable": false,
|
||||
"unlockAllProfitTakerStages": false,
|
||||
"instantFinishRivenChallenge": false,
|
||||
"instantResourceExtractorDrones": false,
|
||||
"noResourceExtractorDronesDamage": false,
|
||||
@ -49,14 +56,31 @@
|
||||
"noDojoResearchCosts": false,
|
||||
"noDojoResearchTime": false,
|
||||
"fastClanAscension": false,
|
||||
"missionsCanGiveAllRelics": false,
|
||||
"unlockAllSimarisResearchEntries": false,
|
||||
"disableDailyTribute": false,
|
||||
"spoofMasteryRank": -1,
|
||||
"relicRewardItemCountMultiplier": 1,
|
||||
"nightwaveStandingMultiplier": 1,
|
||||
"unfaithfulBugFixes": {
|
||||
"ignore1999LastRegionPlayed": false,
|
||||
"fixXtraCheeseTimer": false
|
||||
},
|
||||
"worldState": {
|
||||
"creditBoost": false,
|
||||
"affinityBoost": false,
|
||||
"resourceBoost": false,
|
||||
"starDays": true,
|
||||
"galleonOfGhouls": 0,
|
||||
"eidolonOverride": "",
|
||||
"vallisOverride": "",
|
||||
"nightwaveOverride": ""
|
||||
"duviriOverride": "",
|
||||
"nightwaveOverride": "",
|
||||
"allTheFissures": "",
|
||||
"circuitGameModes": null,
|
||||
"darvoStockMultiplier": 1
|
||||
},
|
||||
"dev": {
|
||||
"keepVendorsExpired": false
|
||||
}
|
||||
}
|
||||
|
@ -1,62 +1,20 @@
|
||||
services:
|
||||
spaceninjaserver:
|
||||
# build: .
|
||||
# The image to use. If you have an ARM CPU, replace 'latest' with 'latest-arm64'.
|
||||
image: openwf/spaceninjaserver:latest
|
||||
environment:
|
||||
APP_MONGODB_URL: mongodb://openwfagent:spaceninjaserver@mongodb:27017/
|
||||
|
||||
# Following environment variables are set to default image values.
|
||||
# Uncomment to edit.
|
||||
|
||||
# APP_MY_ADDRESS: localhost
|
||||
# APP_HTTP_PORT: 80
|
||||
# APP_HTTPS_PORT: 443
|
||||
# APP_AUTO_CREATE_ACCOUNT: true
|
||||
# APP_SKIP_TUTORIAL: false
|
||||
# APP_SKIP_ALL_DIALOGUE: false
|
||||
# APP_UNLOCK_ALL_SCANS: false
|
||||
# APP_UNLOCK_ALL_MISSIONS: false
|
||||
# APP_INFINITE_CREDITS: false
|
||||
# APP_INFINITE_PLATINUM: false
|
||||
# APP_INFINITE_ENDO: false
|
||||
# APP_INFINITE_REGAL_AYA: false
|
||||
# APP_INFINITE_HELMINTH_MATERIALS: false
|
||||
# APP_CLAIMING_BLUEPRINT_REFUNDS_INGREDIENTS: false
|
||||
# APP_DONT_SUBTRACT_VOIDTRACES: false
|
||||
# APP_DONT_SUBTRACT_CONSUMABLES: false
|
||||
# APP_UNLOCK_ALL_SHIP_FEATURES: false
|
||||
# APP_UNLOCK_ALL_SHIP_DECORATIONS: false
|
||||
# APP_UNLOCK_ALL_FLAVOUR_ITEMS: false
|
||||
# APP_UNLOCK_ALL_SKINS: false
|
||||
# APP_UNLOCK_ALL_CAPTURA_SCENES: false
|
||||
# APP_UNIVERSAL_POLARITY_EVERYWHERE: false
|
||||
# APP_UNLOCK_DOUBLE_CAPACITY_POTATOES_EVERYWHERE: false
|
||||
# APP_UNLOCK_EXILUS_EVERYWHERE: false
|
||||
# APP_UNLOCK_ARCANES_EVERYWHERE: false
|
||||
# APP_NO_DAILY_FOCUS_LIMIT: false
|
||||
# APP_NO_ARGON_CRYSTAL_DECAY: false
|
||||
# APP_NO_MASTERY_RANK_UP_COOLDOWN: false
|
||||
# APP_NO_VENDOR_PURCHASE_LIMITS: true
|
||||
# APP_NO_DEATH_MARKS: false
|
||||
# APP_NO_KIM_COOLDOWNS: false
|
||||
# APP_SYNDICATE_MISSIONS_REPEATABLE: false
|
||||
# APP_INSTANT_FINISH_RIVEN_CHALLENGE: false
|
||||
# APP_INSTANT_RESOURCE_EXTRACTOR_DRONES: false
|
||||
# APP_NO_RESOURCE_EXTRACTOR_DRONES_DAMAGE: false
|
||||
# APP_SKIP_CLAN_KEY_CRAFTING: false
|
||||
# APP_NO_DOJO_ROOM_BUILD_STAGE: false
|
||||
# APP_NO_DECO_BUILD_STAGE: false
|
||||
# APP_FAST_DOJO_ROOM_DESTRUCTION: false
|
||||
# APP_NO_DOJO_RESEARCH_COSTS: false
|
||||
# APP_NO_DOJO_RESEARCH_TIME: false
|
||||
# APP_FAST_CLAN_ASCENSION: false
|
||||
# APP_SPOOF_MASTERY_RANK: -1
|
||||
volumes:
|
||||
- ./docker-data/static:/app/static/data
|
||||
- ./docker-data/conf:/app/conf
|
||||
- ./docker-data/static-data:/app/static/data
|
||||
- ./docker-data/logs:/app/logs
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
|
||||
# Normally, the image is fetched from Docker Hub, but you can use the local Dockerfile by removing "image" above and adding this:
|
||||
#build: .
|
||||
# Works best when using `docker-compose up --force-recreate --build`.
|
||||
|
||||
depends_on:
|
||||
- mongodb
|
||||
mongodb:
|
||||
@ -66,3 +24,4 @@ services:
|
||||
MONGO_INITDB_ROOT_PASSWORD: spaceninjaserver
|
||||
volumes:
|
||||
- ./docker-data/database:/data/db
|
||||
command: mongod --quiet --logpath /dev/null
|
||||
|
@ -1,24 +1,8 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Set up the configuration file using environment variables.
|
||||
echo '{
|
||||
"logger": {
|
||||
"files": true,
|
||||
"level": "trace",
|
||||
"__valid_levels": "fatal, error, warn, info, http, debug, trace"
|
||||
}
|
||||
}
|
||||
' > config.json
|
||||
if [ ! -f conf/config.json ]; then
|
||||
jq --arg value "mongodb://openwfagent:spaceninjaserver@mongodb:27017/" '.mongodbUrl = $value' /app/config.json.example > /app/conf/config.json
|
||||
fi
|
||||
|
||||
for config in $(env | grep "APP_")
|
||||
do
|
||||
var=$(echo "${config}" | tr '[:upper:]' '[:lower:]' | sed 's/app_//g' | sed -E 's/_([a-z])/\U\1/g' | sed 's/=.*//g')
|
||||
val=$(echo "${config}" | sed 's/.*=//g')
|
||||
jq --arg variable "$var" --arg value "$val" '.[$variable] += try [$value|fromjson][] catch $value' config.json > config.tmp
|
||||
mv config.tmp config.json
|
||||
done
|
||||
|
||||
npm i --omit=dev
|
||||
npm run build
|
||||
exec npm run start
|
||||
exec npm run start -- --configPath conf/config.json
|
||||
|
729
package-lock.json
generated
729
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@ -5,9 +5,16 @@
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"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 --incremental --sourceMap && ncp static/webui build/static/webui",
|
||||
"build": "tsgo --sourceMap && ncp static/webui build/static/webui",
|
||||
"build:tsc": "tsc --incremental --sourceMap && ncp static/webui build/static/webui",
|
||||
"build:dev": "tsgo --sourceMap",
|
||||
"build:dev:tsc": "tsc --incremental --sourceMap",
|
||||
"build-and-start": "npm run build && npm run start",
|
||||
"build-and-start:bun": "npm run verify && npm run bun-run",
|
||||
"dev": "node scripts/dev.js",
|
||||
"dev:bun": "bun scripts/dev.js",
|
||||
"verify": "tsgo --noEmit",
|
||||
"bun-run": "bun src/index.ts",
|
||||
"lint": "eslint --ext .ts .",
|
||||
"lint:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .",
|
||||
"lint:fix": "eslint --fix --ext .ts .",
|
||||
@ -18,6 +25,10 @@
|
||||
"dependencies": {
|
||||
"@types/express": "^5",
|
||||
"@types/morgan": "^1.9.9",
|
||||
"@types/websocket": "^1.0.10",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20250625.1",
|
||||
"chokidar": "^4.0.3",
|
||||
"crc-32": "^1.2.2",
|
||||
"express": "^5",
|
||||
"json-with-bigint": "^3.4.4",
|
||||
@ -25,19 +36,19 @@
|
||||
"morgan": "^1.10.0",
|
||||
"ncp": "^2.0.0",
|
||||
"typescript": "^5.5",
|
||||
"warframe-public-export-plus": "^0.5.66",
|
||||
"undici": "^7.10.0",
|
||||
"warframe-public-export-plus": "^0.5.76",
|
||||
"warframe-riven-info": "^0.1.2",
|
||||
"winston": "^3.17.0",
|
||||
"winston-daily-rotate-file": "^5.0.0"
|
||||
"winston-daily-rotate-file": "^5.0.0",
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^8.28.0",
|
||||
"@typescript-eslint/parser": "^8.28.0",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20250523.1",
|
||||
"eslint": "^8",
|
||||
"eslint-plugin-prettier": "^5.2.5",
|
||||
"prettier": "^3.5.3",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"tsconfig-paths": "^4.2.0"
|
||||
"tree-kill": "^1.2.2"
|
||||
}
|
||||
}
|
||||
|
58
scripts/dev.js
Normal file
58
scripts/dev.js
Normal file
@ -0,0 +1,58 @@
|
||||
/* eslint-disable */
|
||||
const { spawn } = require("child_process");
|
||||
const chokidar = require("chokidar");
|
||||
const kill = require("tree-kill");
|
||||
|
||||
let secret = "";
|
||||
for (let i = 0; i != 10; ++i) {
|
||||
secret += String.fromCharCode(Math.floor(Math.random() * 26) + 0x41);
|
||||
}
|
||||
|
||||
const args = [...process.argv].splice(2);
|
||||
args.push("--dev");
|
||||
args.push("--secret");
|
||||
args.push(secret);
|
||||
|
||||
let buildproc, runproc;
|
||||
const spawnopts = { stdio: "inherit", shell: true };
|
||||
function run(changedFile) {
|
||||
if (changedFile) {
|
||||
console.log(`Change to ${changedFile} detected`);
|
||||
}
|
||||
|
||||
if (buildproc) {
|
||||
kill(buildproc.pid);
|
||||
buildproc = undefined;
|
||||
}
|
||||
if (runproc) {
|
||||
kill(runproc.pid);
|
||||
runproc = undefined;
|
||||
}
|
||||
|
||||
const thisbuildproc = spawn("npm", ["run", process.versions.bun ? "verify" : "build:dev"], spawnopts);
|
||||
const thisbuildstart = Date.now();
|
||||
buildproc = thisbuildproc;
|
||||
buildproc.on("exit", code => {
|
||||
if (buildproc !== thisbuildproc) {
|
||||
return;
|
||||
}
|
||||
buildproc = undefined;
|
||||
if (code === 0) {
|
||||
console.log(`${process.versions.bun ? "Verified" : "Built"} in ${Date.now() - thisbuildstart} ms`);
|
||||
runproc = spawn("npm", ["run", process.versions.bun ? "bun-run" : "start", "--", ...args], spawnopts);
|
||||
runproc.on("exit", () => {
|
||||
runproc = undefined;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
run();
|
||||
chokidar.watch("src").on("change", run);
|
||||
chokidar.watch("static/fixed_responses").on("change", run);
|
||||
|
||||
chokidar.watch("static/webui").on("change", async () => {
|
||||
try {
|
||||
await fetch("http://localhost/custom/webuiFileChangeDetected?secret=" + secret);
|
||||
} catch (e) {}
|
||||
});
|
@ -1,6 +1,7 @@
|
||||
// Based on https://onlyg.it/OpenWF/Translations/src/branch/main/update.php
|
||||
// Converted via ChatGPT-4o
|
||||
|
||||
/* eslint-disable */
|
||||
const fs = require("fs");
|
||||
|
||||
function extractStrings(content) {
|
||||
|
@ -1,16 +1,12 @@
|
||||
import { getAccountForRequest } from "@/src/services/loginService";
|
||||
import { RequestHandler } from "express";
|
||||
|
||||
const checkDailyMissionBonusController: RequestHandler = (_req, res) => {
|
||||
const data = Buffer.from([
|
||||
0x44, 0x61, 0x69, 0x6c, 0x79, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x6f, 0x6e, 0x75, 0x73, 0x3a,
|
||||
0x31, 0x2d, 0x44, 0x61, 0x69, 0x6c, 0x79, 0x50, 0x56, 0x50, 0x57, 0x69, 0x6e, 0x42, 0x6f, 0x6e, 0x75, 0x73,
|
||||
0x3a, 0x31, 0x0a
|
||||
]);
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/html",
|
||||
"Content-Length": data.length
|
||||
});
|
||||
res.end(data);
|
||||
export const checkDailyMissionBonusController: RequestHandler = async (req, res) => {
|
||||
const account = await getAccountForRequest(req);
|
||||
const today = Math.trunc(Date.now() / 86400000) * 86400;
|
||||
if (account.DailyFirstWinDate != today) {
|
||||
res.send("DailyMissionBonus:1-DailyPVPWinBonus:1\n");
|
||||
} else {
|
||||
res.send("DailyMissionBonus:0-DailyPVPWinBonus:1\n");
|
||||
}
|
||||
};
|
||||
|
||||
export { checkDailyMissionBonusController };
|
||||
|
@ -13,7 +13,8 @@ import {
|
||||
addItem,
|
||||
addRecipes,
|
||||
occupySlot,
|
||||
combineInventoryChanges
|
||||
combineInventoryChanges,
|
||||
addKubrowPetPrint
|
||||
} from "@/src/services/inventoryService";
|
||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
|
||||
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
|
||||
@ -119,6 +120,9 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
|
||||
}
|
||||
}
|
||||
pet.Details!.Status = canSetActive ? Status.StatusAvailable : Status.StatusStasis;
|
||||
} else if (recipe.secretIngredientAction == "SIA_DISTILL_PRINT") {
|
||||
const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!;
|
||||
addKubrowPetPrint(inventory, pet, InventoryChanges);
|
||||
} else if (recipe.secretIngredientAction != "SIA_UNBRAND") {
|
||||
InventoryChanges = {
|
||||
...InventoryChanges,
|
||||
|
@ -0,0 +1,35 @@
|
||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
|
||||
import { combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
|
||||
import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
|
||||
import { RequestHandler } from "express";
|
||||
import { ExportChallenges } from "warframe-public-export-plus";
|
||||
|
||||
export const claimJunctionChallengeRewardController: RequestHandler = async (req, res) => {
|
||||
const accountId = await getAccountIdForRequest(req);
|
||||
const inventory = await getInventory(accountId);
|
||||
const data = getJSONfromString<IClaimJunctionChallengeRewardRequest>(String(req.body));
|
||||
const challengeProgress = inventory.ChallengeProgress.find(x => x.Name == data.Challenge)!;
|
||||
if (challengeProgress.ReceivedJunctionReward) {
|
||||
throw new Error(`attempt to double-claim junction reward`);
|
||||
}
|
||||
challengeProgress.ReceivedJunctionReward = true;
|
||||
inventory.ClaimedJunctionChallengeRewards ??= [];
|
||||
inventory.ClaimedJunctionChallengeRewards.push(data.Challenge);
|
||||
const challengeMeta = Object.entries(ExportChallenges).find(arr => arr[0].endsWith("/" + data.Challenge))![1];
|
||||
const inventoryChanges = {};
|
||||
for (const reward of challengeMeta.countedRewards!) {
|
||||
combineInventoryChanges(
|
||||
inventoryChanges,
|
||||
(await handleStoreItemAcquisition(reward.StoreItem, inventory, reward.ItemCount)).InventoryChanges
|
||||
);
|
||||
}
|
||||
await inventory.save();
|
||||
res.json({
|
||||
inventoryChanges: inventoryChanges // Yeah, it's "inventoryChanges" in the response here.
|
||||
});
|
||||
};
|
||||
|
||||
interface IClaimJunctionChallengeRewardRequest {
|
||||
Challenge: string;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { getCalendarProgress, getInventory } from "@/src/services/inventoryService";
|
||||
import { checkCalendarChallengeCompletion, getCalendarProgress, getInventory } from "@/src/services/inventoryService";
|
||||
import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
|
||||
import { getWorldState } from "@/src/services/worldStateService";
|
||||
@ -12,27 +12,23 @@ export const completeCalendarEventController: RequestHandler = async (req, res)
|
||||
const calendarProgress = getCalendarProgress(inventory);
|
||||
const currentSeason = getWorldState().KnownCalendarSeasons[0];
|
||||
let inventoryChanges: IInventoryChanges = {};
|
||||
let dayIndex = 0;
|
||||
for (const day of currentSeason.Days) {
|
||||
if (day.events.length == 0 || day.events[0].type != "CET_CHALLENGE") {
|
||||
if (dayIndex == calendarProgress.SeasonProgress.LastCompletedDayIdx) {
|
||||
const dayIndex = calendarProgress.SeasonProgress.LastCompletedDayIdx + 1;
|
||||
const day = currentSeason.Days[dayIndex];
|
||||
if (day.events.length != 0) {
|
||||
if (day.events[0].type == "CET_CHALLENGE") {
|
||||
throw new Error(`completeCalendarEvent should not be used for challenges`);
|
||||
}
|
||||
const selection = day.events[parseInt(req.query.CompletedEventIdx as string)];
|
||||
if (selection.type == "CET_REWARD") {
|
||||
inventoryChanges = (await handleStoreItemAcquisition(selection.reward!, inventory))
|
||||
.InventoryChanges;
|
||||
inventoryChanges = (await handleStoreItemAcquisition(selection.reward!, inventory)).InventoryChanges;
|
||||
} else if (selection.type == "CET_UPGRADE") {
|
||||
calendarProgress.YearProgress.Upgrades.push(selection.upgrade!);
|
||||
} else if (selection.type != "CET_PLOT") {
|
||||
throw new Error(`unexpected selection type: ${selection.type}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
++dayIndex;
|
||||
}
|
||||
}
|
||||
calendarProgress.SeasonProgress.LastCompletedDayIdx++;
|
||||
calendarProgress.SeasonProgress.LastCompletedDayIdx = dayIndex;
|
||||
checkCalendarChallengeCompletion(calendarProgress, currentSeason);
|
||||
await inventory.save();
|
||||
res.json({
|
||||
InventoryChanges: inventoryChanges,
|
||||
|
@ -4,9 +4,15 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import { getInventory } from "@/src/services/inventoryService";
|
||||
|
||||
export const creditsController: RequestHandler = async (req, res) => {
|
||||
const accountId = await getAccountIdForRequest(req);
|
||||
|
||||
const inventory = await getInventory(accountId, "RegularCredits TradesRemaining PremiumCreditsFree PremiumCredits");
|
||||
const inventory = (
|
||||
await Promise.all([
|
||||
getAccountIdForRequest(req),
|
||||
getInventory(
|
||||
req.query.accountId as string,
|
||||
"RegularCredits TradesRemaining PremiumCreditsFree PremiumCredits"
|
||||
)
|
||||
])
|
||||
)[1];
|
||||
|
||||
const response = {
|
||||
RegularCredits: inventory.RegularCredits,
|
||||
|
107
src/controllers/api/crewShipFusionController.ts
Normal file
107
src/controllers/api/crewShipFusionController.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
|
||||
import { addMiscItems, freeUpSlot, getInventory, updateCurrency } from "@/src/services/inventoryService";
|
||||
import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import { IOid } from "@/src/types/commonTypes";
|
||||
import { ICrewShipComponentFingerprint, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
|
||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
|
||||
import { RequestHandler } from "express";
|
||||
import { ExportCustoms, ExportDojoRecipes } from "warframe-public-export-plus";
|
||||
|
||||
export const crewShipFusionController: RequestHandler = async (req, res) => {
|
||||
const accountId = await getAccountIdForRequest(req);
|
||||
const inventory = await getInventory(accountId);
|
||||
const payload = getJSONfromString<ICrewShipFusionRequest>(String(req.body));
|
||||
|
||||
const isWeapon = inventory.CrewShipWeapons.id(payload.PartA.$oid);
|
||||
const itemA = isWeapon ?? inventory.CrewShipWeaponSkins.id(payload.PartA.$oid)!;
|
||||
const category = isWeapon ? "CrewShipWeapons" : "CrewShipWeaponSkins";
|
||||
const salvageCategory = isWeapon ? "CrewShipSalvagedWeapons" : "CrewShipSalvagedWeaponSkins";
|
||||
const itemB = inventory[payload.SourceRecipe ? salvageCategory : category].id(payload.PartB.$oid)!;
|
||||
const tierA = itemA.ItemType.charCodeAt(itemA.ItemType.length - 1) - 65;
|
||||
const tierB = itemB.ItemType.charCodeAt(itemB.ItemType.length - 1) - 65;
|
||||
|
||||
const inventoryChanges: IInventoryChanges = {};
|
||||
|
||||
// Charge partial repair cost if fusing with an identified but unrepaired part
|
||||
if (payload.SourceRecipe) {
|
||||
const recipe = ExportDojoRecipes.research[payload.SourceRecipe];
|
||||
updateCurrency(inventory, Math.round(recipe.price * 0.4), false, inventoryChanges);
|
||||
const miscItemChanges = recipe.ingredients.map(x => ({ ...x, ItemCount: Math.round(x.ItemCount * -0.4) }));
|
||||
addMiscItems(inventory, miscItemChanges);
|
||||
inventoryChanges.MiscItems = miscItemChanges;
|
||||
}
|
||||
|
||||
// Remove inferior item
|
||||
if (payload.SourceRecipe) {
|
||||
inventory[salvageCategory].pull({ _id: payload.PartB.$oid });
|
||||
inventoryChanges.RemovedIdItems = [{ ItemId: payload.PartB }];
|
||||
} else {
|
||||
const inferiorId = tierA < tierB ? payload.PartA : payload.PartB;
|
||||
inventory[category].pull({ _id: inferiorId.$oid });
|
||||
inventoryChanges.RemovedIdItems = [{ ItemId: inferiorId }];
|
||||
freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
|
||||
inventoryChanges[InventorySlot.RJ_COMPONENT_AND_ARMAMENTS] = { count: -1, platinum: 0, Slots: 1 };
|
||||
}
|
||||
|
||||
// Upgrade superior item
|
||||
const superiorItem = tierA < tierB ? itemB : itemA;
|
||||
const inferiorItem = tierA < tierB ? itemA : itemB;
|
||||
const fingerprint: ICrewShipComponentFingerprint = JSON.parse(
|
||||
superiorItem.UpgradeFingerprint!
|
||||
) as ICrewShipComponentFingerprint;
|
||||
const inferiorFingerprint: ICrewShipComponentFingerprint = inferiorItem.UpgradeFingerprint
|
||||
? (JSON.parse(inferiorItem.UpgradeFingerprint) as ICrewShipComponentFingerprint)
|
||||
: { compat: "", buffs: [] };
|
||||
if (isWeapon) {
|
||||
for (let i = 0; i != fingerprint.buffs.length; ++i) {
|
||||
const buffA = fingerprint.buffs[i];
|
||||
const buffB = i < inferiorFingerprint.buffs.length ? inferiorFingerprint.buffs[i] : undefined;
|
||||
const fvalA = buffA.Value / 0x3fffffff;
|
||||
const fvalB = (buffB?.Value ?? 0) / 0x3fffffff;
|
||||
const percA = 0.3 + fvalA * (0.6 - 0.3);
|
||||
const percB = 0.3 + fvalB * (0.6 - 0.3);
|
||||
const newPerc = Math.min(0.6, Math.max(percA, percB) * FUSE_MULTIPLIERS[Math.abs(tierA - tierB)]);
|
||||
const newFval = (newPerc - 0.3) / (0.6 - 0.3);
|
||||
buffA.Value = Math.trunc(newFval * 0x3fffffff);
|
||||
}
|
||||
} else {
|
||||
const superiorMeta = ExportCustoms[superiorItem.ItemType].randomisedUpgrades ?? [];
|
||||
const inferiorMeta = ExportCustoms[inferiorItem.ItemType].randomisedUpgrades ?? [];
|
||||
for (let i = 0; i != inferiorFingerprint.buffs.length; ++i) {
|
||||
const buffA = fingerprint.buffs[i];
|
||||
const buffB = inferiorFingerprint.buffs[i];
|
||||
const fvalA = buffA.Value / 0x3fffffff;
|
||||
const fvalB = buffB.Value / 0x3fffffff;
|
||||
const rangeA = superiorMeta[i].range;
|
||||
const rangeB = inferiorMeta[i].range;
|
||||
const percA = rangeA[0] + fvalA * (rangeA[1] - rangeA[0]);
|
||||
const percB = rangeB[0] + fvalB * (rangeB[1] - rangeB[0]);
|
||||
const newPerc = Math.min(rangeA[1], Math.max(percA, percB) * FUSE_MULTIPLIERS[Math.abs(tierA - tierB)]);
|
||||
const newFval = (newPerc - rangeA[0]) / (rangeA[1] - rangeA[0]);
|
||||
buffA.Value = Math.trunc(newFval * 0x3fffffff);
|
||||
}
|
||||
if (inferiorFingerprint.SubroutineIndex) {
|
||||
const useSuperiorSubroutine = tierA < tierB ? !payload.UseSubroutineA : payload.UseSubroutineA;
|
||||
if (!useSuperiorSubroutine) {
|
||||
fingerprint.SubroutineIndex = inferiorFingerprint.SubroutineIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
superiorItem.UpgradeFingerprint = JSON.stringify(fingerprint);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
inventoryChanges[category] = [superiorItem.toJSON() as any];
|
||||
|
||||
await inventory.save();
|
||||
res.json({
|
||||
InventoryChanges: inventoryChanges
|
||||
});
|
||||
};
|
||||
|
||||
interface ICrewShipFusionRequest {
|
||||
PartA: IOid;
|
||||
PartB: IOid;
|
||||
SourceRecipe: string;
|
||||
UseSubroutineA: boolean;
|
||||
}
|
||||
|
||||
const FUSE_MULTIPLIERS = [1.1, 1.05, 1.02];
|
@ -30,15 +30,14 @@ export const fishmongerController: RequestHandler = async (req, res) => {
|
||||
miscItemChanges.push({ ItemType: fish.ItemType, ItemCount: fish.ItemCount * -1 });
|
||||
}
|
||||
addMiscItems(inventory, miscItemChanges);
|
||||
let affiliationMod;
|
||||
if (gainedStanding && syndicateTag) affiliationMod = addStanding(inventory, syndicateTag, gainedStanding);
|
||||
if (gainedStanding && syndicateTag) addStanding(inventory, syndicateTag, gainedStanding);
|
||||
await inventory.save();
|
||||
res.json({
|
||||
InventoryChanges: {
|
||||
MiscItems: miscItemChanges
|
||||
},
|
||||
SyndicateTag: syndicateTag,
|
||||
StandingChange: affiliationMod?.Standing || 0
|
||||
StandingChange: gainedStanding
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -43,7 +43,7 @@ export const focusController: RequestHandler = async (req, res) => {
|
||||
inventory.FocusAbility ??= focusType;
|
||||
inventory.FocusUpgrades.push({ ItemType: focusType });
|
||||
if (inventory.FocusXP) {
|
||||
inventory.FocusXP[focusPolarity] -= cost;
|
||||
inventory.FocusXP[focusPolarity]! -= cost;
|
||||
}
|
||||
await inventory.save();
|
||||
res.json({
|
||||
@ -78,7 +78,7 @@ export const focusController: RequestHandler = async (req, res) => {
|
||||
cost += ExportFocusUpgrades[focusType].baseFocusPointCost;
|
||||
inventory.FocusUpgrades.push({ ItemType: focusType, Level: 0 });
|
||||
}
|
||||
inventory.FocusXP![focusPolarity] -= cost;
|
||||
inventory.FocusXP![focusPolarity]! -= cost;
|
||||
await inventory.save();
|
||||
res.json({
|
||||
FocusTypes: request.FocusTypes,
|
||||
@ -96,7 +96,7 @@ export const focusController: RequestHandler = async (req, res) => {
|
||||
const focusUpgradeDb = inventory.FocusUpgrades.find(entry => entry.ItemType == focusUpgrade.ItemType)!;
|
||||
focusUpgradeDb.Level = focusUpgrade.Level;
|
||||
}
|
||||
inventory.FocusXP![focusPolarity] -= cost;
|
||||
inventory.FocusXP![focusPolarity]! -= cost;
|
||||
await inventory.save();
|
||||
res.json({
|
||||
FocusInfos: request.FocusInfos,
|
||||
@ -123,7 +123,7 @@ export const focusController: RequestHandler = async (req, res) => {
|
||||
const request = JSON.parse(String(req.body)) as IUnbindUpgradeRequest;
|
||||
const focusPolarity = focusTypeToPolarity(request.FocusTypes[0]);
|
||||
const inventory = await getInventory(accountId);
|
||||
inventory.FocusXP![focusPolarity] -= 750_000 * request.FocusTypes.length;
|
||||
inventory.FocusXP![focusPolarity]! -= 750_000 * request.FocusTypes.length;
|
||||
addMiscItems(inventory, [
|
||||
{
|
||||
ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantItem",
|
||||
@ -168,8 +168,10 @@ export const focusController: RequestHandler = async (req, res) => {
|
||||
shard.ItemCount *= -1;
|
||||
}
|
||||
const inventory = await getInventory(accountId);
|
||||
inventory.FocusXP ??= { AP_POWER: 0, AP_TACTIC: 0, AP_DEFENSE: 0, AP_ATTACK: 0, AP_WARD: 0 };
|
||||
inventory.FocusXP[request.Polarity] += xp;
|
||||
const polarity = request.Polarity;
|
||||
inventory.FocusXP ??= {};
|
||||
inventory.FocusXP[polarity] ??= 0;
|
||||
inventory.FocusXP[polarity] += xp;
|
||||
addMiscItems(inventory, request.Shards);
|
||||
await inventory.save();
|
||||
break;
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { DailyDeal } from "@/src/models/worldStateModel";
|
||||
import { RequestHandler } from "express";
|
||||
|
||||
export const getDailyDealStockLevelsController: RequestHandler = (req, res) => {
|
||||
export const getDailyDealStockLevelsController: RequestHandler = async (req, res) => {
|
||||
const dailyDeal = (await DailyDeal.findOne({ StoreItem: req.query.productName }, "AmountSold"))!;
|
||||
res.json({
|
||||
StoreItem: req.query.productName,
|
||||
AmountSold: 0
|
||||
AmountSold: dailyDeal.AmountSold
|
||||
});
|
||||
};
|
||||
|
@ -2,6 +2,7 @@ import { RequestHandler } from "express";
|
||||
import { applyStandingToVendorManifest, getVendorManifestByTypeName } from "@/src/services/serversideVendorsService";
|
||||
import { getInventory } from "@/src/services/inventoryService";
|
||||
import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import { config } from "@/src/services/configService";
|
||||
|
||||
export const getVendorInfoController: RequestHandler = async (req, res) => {
|
||||
let manifest = getVendorManifestByTypeName(req.query.vendor as string);
|
||||
@ -14,6 +15,14 @@ export const getVendorInfoController: RequestHandler = async (req, res) => {
|
||||
const accountId = await getAccountIdForRequest(req);
|
||||
const inventory = await getInventory(accountId);
|
||||
manifest = applyStandingToVendorManifest(inventory, manifest);
|
||||
if (config.dev?.keepVendorsExpired) {
|
||||
manifest = {
|
||||
VendorInfo: {
|
||||
...manifest.VendorInfo,
|
||||
Expiry: { $date: { $numberLong: "0" } }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
res.json(manifest);
|
||||
|
@ -9,15 +9,26 @@ import {
|
||||
updateCurrency
|
||||
} from "@/src/services/inventoryService";
|
||||
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
|
||||
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
|
||||
import { handleDailyDealPurchase, handleStoreItemAcquisition } from "@/src/services/purchaseService";
|
||||
import { IOid } from "@/src/types/commonTypes";
|
||||
import { IInventoryChanges, IPurchaseParams } from "@/src/types/purchaseTypes";
|
||||
import { IPurchaseParams, IPurchaseResponse, PurchaseSource } from "@/src/types/purchaseTypes";
|
||||
import { RequestHandler } from "express";
|
||||
import { ExportBundles, ExportFlavour } from "warframe-public-export-plus";
|
||||
|
||||
const checkPurchaseParams = (params: IPurchaseParams): boolean => {
|
||||
switch (params.Source) {
|
||||
case PurchaseSource.Market:
|
||||
return params.UsePremium;
|
||||
|
||||
case PurchaseSource.DailyDeal:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const giftingController: RequestHandler = async (req, res) => {
|
||||
const data = getJSONfromString<IGiftingRequest>(String(req.body));
|
||||
if (data.PurchaseParams.Source != 0 || !data.PurchaseParams.UsePremium) {
|
||||
if (!checkPurchaseParams(data.PurchaseParams)) {
|
||||
throw new Error(`unexpected purchase params in gifting request: ${String(req.body)}`);
|
||||
}
|
||||
|
||||
@ -58,16 +69,19 @@ export const giftingController: RequestHandler = async (req, res) => {
|
||||
}
|
||||
senderInventory.GiftsRemaining -= 1;
|
||||
|
||||
const inventoryChanges: IInventoryChanges = updateCurrency(
|
||||
senderInventory,
|
||||
data.PurchaseParams.ExpectedPrice,
|
||||
true
|
||||
);
|
||||
const response: IPurchaseResponse = {
|
||||
InventoryChanges: {}
|
||||
};
|
||||
if (data.PurchaseParams.Source == PurchaseSource.DailyDeal) {
|
||||
await handleDailyDealPurchase(senderInventory, data.PurchaseParams, response);
|
||||
} else {
|
||||
updateCurrency(senderInventory, data.PurchaseParams.ExpectedPrice, true, response.InventoryChanges);
|
||||
}
|
||||
if (data.PurchaseParams.StoreItem in ExportBundles) {
|
||||
const bundle = ExportBundles[data.PurchaseParams.StoreItem];
|
||||
if (bundle.giftingBonus) {
|
||||
combineInventoryChanges(
|
||||
inventoryChanges,
|
||||
response.InventoryChanges,
|
||||
(await handleStoreItemAcquisition(bundle.giftingBonus, senderInventory)).InventoryChanges
|
||||
);
|
||||
}
|
||||
@ -99,9 +113,7 @@ export const giftingController: RequestHandler = async (req, res) => {
|
||||
}
|
||||
]);
|
||||
|
||||
res.json({
|
||||
InventoryChanges: inventoryChanges
|
||||
});
|
||||
res.json(response);
|
||||
};
|
||||
|
||||
interface IGiftingRequest {
|
||||
|
@ -5,13 +5,14 @@ import {
|
||||
getGuildVault,
|
||||
hasAccessToDojo,
|
||||
hasGuildPermission,
|
||||
processCompletedGuildTechProject,
|
||||
processFundedGuildTechProject,
|
||||
processGuildTechProjectContributionsUpdate,
|
||||
removePigmentsFromGuildMembers,
|
||||
scaleRequiredCount,
|
||||
setGuildTechLogState
|
||||
} from "@/src/services/guildService";
|
||||
import { ExportDojoRecipes } from "warframe-public-export-plus";
|
||||
import { ExportDojoRecipes, ExportRailjackWeapons } from "warframe-public-export-plus";
|
||||
import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import {
|
||||
addCrewShipWeaponSkin,
|
||||
@ -51,8 +52,12 @@ export const guildTechController: RequestHandler = async (req, res) => {
|
||||
};
|
||||
if (project.CompletionDate) {
|
||||
techProject.CompletionDate = toMongoDate(project.CompletionDate);
|
||||
if (Date.now() >= project.CompletionDate.getTime()) {
|
||||
needSave ||= setGuildTechLogState(guild, project.ItemType, 4, project.CompletionDate);
|
||||
if (
|
||||
Date.now() >= project.CompletionDate.getTime() &&
|
||||
setGuildTechLogState(guild, project.ItemType, 4, project.CompletionDate)
|
||||
) {
|
||||
processCompletedGuildTechProject(guild, project.ItemType);
|
||||
needSave = true;
|
||||
}
|
||||
}
|
||||
techProjects.push(techProject);
|
||||
@ -442,6 +447,7 @@ const finishComponentRepair = (
|
||||
...(category == "CrewShipWeaponSkins"
|
||||
? addCrewShipWeaponSkin(inventory, salvageItem.ItemType, salvageItem.UpgradeFingerprint)
|
||||
: addEquipment(inventory, category, salvageItem.ItemType, {
|
||||
UpgradeType: ExportRailjackWeapons[salvageItem.ItemType].defaultUpgrades?.[0].ItemType,
|
||||
UpgradeFingerprint: salvageItem.UpgradeFingerprint
|
||||
})),
|
||||
...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false)
|
||||
|
@ -30,8 +30,9 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
|
||||
const request = getJSONfromString<IShardInstallRequest>(String(req.body));
|
||||
const inventory = await getInventory(account._id.toString());
|
||||
const suit = inventory.Suits.id(request.SuitId.$oid)!;
|
||||
if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) {
|
||||
suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}];
|
||||
suit.ArchonCrystalUpgrades ??= [];
|
||||
while (suit.ArchonCrystalUpgrades.length < request.Slot) {
|
||||
suit.ArchonCrystalUpgrades.push({});
|
||||
}
|
||||
suit.ArchonCrystalUpgrades[request.Slot] = {
|
||||
UpgradeType: request.UpgradeType,
|
||||
@ -92,7 +93,8 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
|
||||
}
|
||||
|
||||
// remove from suit
|
||||
suit.ArchonCrystalUpgrades![request.Slot] = {};
|
||||
suit.ArchonCrystalUpgrades![request.Slot].UpgradeType = undefined;
|
||||
suit.ArchonCrystalUpgrades![request.Slot].Color = undefined;
|
||||
|
||||
await inventory.save();
|
||||
|
||||
|
@ -6,29 +6,28 @@ import allDialogue from "@/static/fixed_responses/allDialogue.json";
|
||||
import { ILoadoutDatabase } from "@/src/types/saveLoadoutTypes";
|
||||
import { IInventoryClient, IShipInventory, equipmentKeys } from "@/src/types/inventoryTypes/inventoryTypes";
|
||||
import { IPolarity, ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
|
||||
import {
|
||||
ExportCustoms,
|
||||
ExportFlavour,
|
||||
ExportRegions,
|
||||
ExportResources,
|
||||
ExportVirtuals
|
||||
} from "warframe-public-export-plus";
|
||||
import { ExportCustoms, ExportFlavour, ExportResources, ExportVirtuals } from "warframe-public-export-plus";
|
||||
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService";
|
||||
import {
|
||||
addEmailItem,
|
||||
addMiscItems,
|
||||
allDailyAffiliationKeys,
|
||||
cleanupInventory,
|
||||
createLibraryDailyTask,
|
||||
generateRewardSeed
|
||||
generateRewardSeed,
|
||||
getCalendarProgress
|
||||
} from "@/src/services/inventoryService";
|
||||
import { logger } from "@/src/utils/logger";
|
||||
import { catBreadHash } from "@/src/helpers/stringHelpers";
|
||||
import { addString, catBreadHash } from "@/src/helpers/stringHelpers";
|
||||
import { Types } from "mongoose";
|
||||
import { getNemesisManifest } from "@/src/helpers/nemesisHelpers";
|
||||
import { getPersonalRooms } from "@/src/services/personalRoomsService";
|
||||
import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
|
||||
import { Ship } from "@/src/models/shipModel";
|
||||
import { toLegacyOid, version_compare } from "@/src/helpers/inventoryHelpers";
|
||||
import { toLegacyOid, toOid, version_compare } from "@/src/helpers/inventoryHelpers";
|
||||
import { Inbox } from "@/src/models/inboxModel";
|
||||
import { unixTimesInMs } from "@/src/constants/timeConstants";
|
||||
import { DailyDeal } from "@/src/models/worldStateModel";
|
||||
|
||||
export const inventoryController: RequestHandler = async (request, response) => {
|
||||
const account = await getAccountForRequest(request);
|
||||
@ -42,6 +41,8 @@ export const inventoryController: RequestHandler = async (request, response) =>
|
||||
|
||||
// Handle daily reset
|
||||
if (!inventory.NextRefill || Date.now() >= inventory.NextRefill.getTime()) {
|
||||
const today = Math.trunc(Date.now() / 86400000);
|
||||
|
||||
for (const key of allDailyAffiliationKeys) {
|
||||
inventory[key] = 16000 + inventory.PlayerLevel * 500;
|
||||
}
|
||||
@ -52,12 +53,12 @@ export const inventoryController: RequestHandler = async (request, response) =>
|
||||
inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask();
|
||||
|
||||
if (inventory.NextRefill) {
|
||||
const lastLoginDay = Math.trunc(inventory.NextRefill.getTime() / 86400000) - 1;
|
||||
const daysPassed = today - lastLoginDay;
|
||||
|
||||
if (config.noArgonCrystalDecay) {
|
||||
inventory.FoundToday = undefined;
|
||||
} else {
|
||||
const lastLoginDay = Math.trunc(inventory.NextRefill.getTime() / 86400000) - 1;
|
||||
const today = Math.trunc(Date.now() / 86400000);
|
||||
const daysPassed = today - lastLoginDay;
|
||||
for (let i = 0; i != daysPassed; ++i) {
|
||||
const numArgonCrystals =
|
||||
inventory.MiscItems.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal")
|
||||
@ -89,11 +90,84 @@ export const inventoryController: RequestHandler = async (request, response) =>
|
||||
inventory.FoundToday = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (inventory.UsedDailyDeals.length != 0) {
|
||||
if (daysPassed == 1) {
|
||||
const todayAt0Utc = today * 86400000;
|
||||
const darvoIndex = Math.trunc((todayAt0Utc - 25200000) / (26 * unixTimesInMs.hour));
|
||||
const darvoStart = darvoIndex * (26 * unixTimesInMs.hour) + 25200000;
|
||||
const darvoOid =
|
||||
((darvoStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "adc51a72f7324d95";
|
||||
const deal = await DailyDeal.findById(darvoOid);
|
||||
if (deal) {
|
||||
inventory.UsedDailyDeals = inventory.UsedDailyDeals.filter(x => x == deal.StoreItem); // keep only the deal that came into this new day with us
|
||||
} else {
|
||||
inventory.UsedDailyDeals = [];
|
||||
}
|
||||
} else {
|
||||
inventory.UsedDailyDeals = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inventory.CalendarProgress) {
|
||||
const previousYearIteration = inventory.CalendarProgress.Iteration;
|
||||
getCalendarProgress(inventory); // handle year rollover; the client expects to receive an inventory with an up-to-date CalendarProgress
|
||||
|
||||
// also handle sending of kiss cinematic at year rollover
|
||||
if (
|
||||
inventory.CalendarProgress.Iteration != previousYearIteration &&
|
||||
inventory.DialogueHistory &&
|
||||
inventory.DialogueHistory.Dialogues
|
||||
) {
|
||||
let kalymos = false;
|
||||
for (const { dialogueName, kissEmail } of [
|
||||
{
|
||||
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue",
|
||||
kissEmail: "/Lotus/Types/Items/EmailItems/ArthurKissEmailItem"
|
||||
},
|
||||
{
|
||||
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue",
|
||||
kissEmail: "/Lotus/Types/Items/EmailItems/EleanorKissEmailItem"
|
||||
},
|
||||
{
|
||||
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue",
|
||||
kissEmail: "/Lotus/Types/Items/EmailItems/LettieKissEmailItem"
|
||||
},
|
||||
{
|
||||
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue",
|
||||
kissEmail: "/Lotus/Types/Items/EmailItems/AmirKissEmailItem"
|
||||
},
|
||||
{
|
||||
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue",
|
||||
kissEmail: "/Lotus/Types/Items/EmailItems/AoiKissEmailItem"
|
||||
},
|
||||
{
|
||||
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue",
|
||||
kissEmail: "/Lotus/Types/Items/EmailItems/QuincyKissEmailItem"
|
||||
}
|
||||
]) {
|
||||
const dialogue = inventory.DialogueHistory.Dialogues.find(x => x.DialogueName == dialogueName);
|
||||
if (dialogue) {
|
||||
if (dialogue.Rank == 7) {
|
||||
await addEmailItem(inventory, kissEmail);
|
||||
kalymos = false;
|
||||
break;
|
||||
}
|
||||
if (dialogue.Rank == 6) {
|
||||
kalymos = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (kalymos) {
|
||||
await addEmailItem(inventory, "/Lotus/Types/Items/EmailItems/KalymosKissEmailItem");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cleanupInventory(inventory);
|
||||
|
||||
inventory.NextRefill = new Date((Math.trunc(Date.now() / 86400000) + 1) * 86400000);
|
||||
inventory.NextRefill = new Date((today + 1) * 86400000); // tomorrow at 0 UTC
|
||||
//await inventory.save();
|
||||
}
|
||||
|
||||
@ -134,13 +208,21 @@ export const getInventoryResponse = async (
|
||||
xpBasedLevelCapDisabled: boolean,
|
||||
buildLabel: string | undefined
|
||||
): Promise<IInventoryClient> => {
|
||||
const [inventoryWithLoadOutPresets, ships] = await Promise.all([
|
||||
const [inventoryWithLoadOutPresets, ships, latestMessage] = await Promise.all([
|
||||
inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>("LoadOutPresets"),
|
||||
Ship.find({ ShipOwnerId: inventory.accountOwnerId })
|
||||
Ship.find({ ShipOwnerId: inventory.accountOwnerId }),
|
||||
Inbox.findOne({ ownerId: inventory.accountOwnerId }, "_id").sort({ date: -1 })
|
||||
]);
|
||||
const inventoryResponse = inventoryWithLoadOutPresets.toJSON<IInventoryClient>();
|
||||
inventoryResponse.Ships = ships.map(x => x.toJSON<IShipInventory>());
|
||||
|
||||
// In case mission inventory update added an inbox message, we need to send the Mailbox part so the client knows to refresh it.
|
||||
if (latestMessage) {
|
||||
inventoryResponse.Mailbox = {
|
||||
LastInboxId: toOid(latestMessage._id)
|
||||
};
|
||||
}
|
||||
|
||||
if (config.infiniteCredits) {
|
||||
inventoryResponse.RegularCredits = 999999999;
|
||||
}
|
||||
@ -167,18 +249,6 @@ export const getInventoryResponse = async (
|
||||
}
|
||||
}
|
||||
|
||||
if (config.unlockAllMissions) {
|
||||
inventoryResponse.Missions = [];
|
||||
for (const tag of Object.keys(ExportRegions)) {
|
||||
inventoryResponse.Missions.push({
|
||||
Completes: 1,
|
||||
Tier: 1,
|
||||
Tag: tag
|
||||
});
|
||||
}
|
||||
addString(inventoryResponse.NodeIntrosCompleted, "TeshinHardModeUnlocked");
|
||||
}
|
||||
|
||||
if (config.unlockAllShipDecorations) {
|
||||
inventoryResponse.ShipDecorations = [];
|
||||
for (const [uniqueName, item] of Object.entries(ExportResources)) {
|
||||
@ -300,9 +370,6 @@ export const getInventoryResponse = async (
|
||||
applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry);
|
||||
}
|
||||
|
||||
// Omitting this field so opening the navigation resyncs the inventory which is more desirable for typical usage.
|
||||
inventoryResponse.LastInventorySync = undefined;
|
||||
|
||||
// Set 2FA enabled so trading post can be used
|
||||
inventoryResponse.HWIDProtectEnabled = true;
|
||||
|
||||
@ -339,14 +406,41 @@ export const getInventoryResponse = async (
|
||||
}
|
||||
}
|
||||
|
||||
if (config.unlockAllProfitTakerStages) {
|
||||
inventoryResponse.CompletedJobChains ??= [];
|
||||
const EudicoHeists = inventoryResponse.CompletedJobChains.find(x => x.LocationTag == "EudicoHeists");
|
||||
if (EudicoHeists) {
|
||||
EudicoHeists.Jobs = allEudicoHeistJobs;
|
||||
} else {
|
||||
inventoryResponse.CompletedJobChains.push({
|
||||
LocationTag: "EudicoHeists",
|
||||
Jobs: allEudicoHeistJobs
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (config.unlockAllSimarisResearchEntries) {
|
||||
inventoryResponse.LibraryPersonalTarget = undefined;
|
||||
inventoryResponse.LibraryPersonalProgress = [
|
||||
"/Lotus/Types/Game/Library/Targets/Research1Target",
|
||||
"/Lotus/Types/Game/Library/Targets/Research2Target",
|
||||
"/Lotus/Types/Game/Library/Targets/Research3Target",
|
||||
"/Lotus/Types/Game/Library/Targets/Research4Target",
|
||||
"/Lotus/Types/Game/Library/Targets/Research5Target",
|
||||
"/Lotus/Types/Game/Library/Targets/Research6Target",
|
||||
"/Lotus/Types/Game/Library/Targets/Research7Target"
|
||||
].map(type => ({ TargetType: type, Scans: 10, Completed: true }));
|
||||
}
|
||||
|
||||
return inventoryResponse;
|
||||
};
|
||||
|
||||
const addString = (arr: string[], str: string): void => {
|
||||
if (arr.indexOf(str) == -1) {
|
||||
arr.push(str);
|
||||
}
|
||||
};
|
||||
const allEudicoHeistJobs = [
|
||||
"/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyOne",
|
||||
"/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyTwo",
|
||||
"/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyThree",
|
||||
"/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyFour"
|
||||
];
|
||||
|
||||
const getExpRequiredForMr = (rank: number): number => {
|
||||
if (rank <= 30) {
|
||||
|
@ -4,16 +4,16 @@ import { config } from "@/src/services/configService";
|
||||
import { buildConfig } from "@/src/services/buildConfigService";
|
||||
|
||||
import { Account } from "@/src/models/loginModel";
|
||||
import { createAccount, isCorrectPassword, isNameTaken } from "@/src/services/loginService";
|
||||
import { createAccount, createNonce, getUsernameFromEmail, isCorrectPassword } from "@/src/services/loginService";
|
||||
import { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "@/src/types/loginTypes";
|
||||
import { logger } from "@/src/utils/logger";
|
||||
import { version_compare } from "@/src/helpers/inventoryHelpers";
|
||||
import { sendWsBroadcastTo } from "@/src/services/webService";
|
||||
|
||||
export const loginController: RequestHandler = async (request, response) => {
|
||||
const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object
|
||||
|
||||
const account = await Account.findOne({ email: loginRequest.email });
|
||||
const nonce = Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
|
||||
|
||||
const buildLabel: string =
|
||||
typeof request.query.buildLabel == "string"
|
||||
@ -42,26 +42,14 @@ export const loginController: RequestHandler = async (request, response) => {
|
||||
loginRequest.ClientType == "webui-register")
|
||||
) {
|
||||
try {
|
||||
const nameFromEmail = loginRequest.email.substring(0, loginRequest.email.indexOf("@"));
|
||||
let name = nameFromEmail || loginRequest.email.substring(1) || "SpaceNinja";
|
||||
if (await isNameTaken(name)) {
|
||||
let suffix = 0;
|
||||
do {
|
||||
++suffix;
|
||||
name = nameFromEmail + suffix;
|
||||
} while (await isNameTaken(name));
|
||||
}
|
||||
const name = await getUsernameFromEmail(loginRequest.email);
|
||||
const newAccount = await createAccount({
|
||||
email: loginRequest.email,
|
||||
password: loginRequest.password,
|
||||
DisplayName: name,
|
||||
CountryCode: loginRequest.lang?.toUpperCase() ?? "EN",
|
||||
ClientType: loginRequest.ClientType == "webui-register" ? "webui" : loginRequest.ClientType,
|
||||
CrossPlatformAllowed: true,
|
||||
ForceLogoutVersion: 0,
|
||||
ConsentNeeded: false,
|
||||
TrackedSettings: [],
|
||||
Nonce: nonce,
|
||||
ClientType: loginRequest.ClientType,
|
||||
Nonce: createNonce(),
|
||||
BuildLabel: buildLabel,
|
||||
LastLogin: new Date()
|
||||
});
|
||||
@ -80,22 +68,11 @@ export const loginController: RequestHandler = async (request, response) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (loginRequest.ClientType == "webui-register") {
|
||||
response.status(400).json({ error: "account already exists" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isCorrectPassword(loginRequest.password, account.password)) {
|
||||
response.status(400).json({ error: "incorrect login data" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (loginRequest.ClientType == "webui") {
|
||||
if (!account.Nonce) {
|
||||
account.ClientType = "webui";
|
||||
account.Nonce = nonce;
|
||||
}
|
||||
} else {
|
||||
if (account.Nonce && account.ClientType != "webui" && !account.Dropped && !loginRequest.kick) {
|
||||
// U17 seems to handle "nonce still set" like a login failure.
|
||||
if (version_compare(buildLabel, "2015.12.05.18.07") >= 0) {
|
||||
@ -105,13 +82,15 @@ export const loginController: RequestHandler = async (request, response) => {
|
||||
}
|
||||
|
||||
account.ClientType = loginRequest.ClientType;
|
||||
account.Nonce = nonce;
|
||||
account.Nonce = createNonce();
|
||||
account.CountryCode = loginRequest.lang?.toUpperCase() ?? "EN";
|
||||
account.BuildLabel = buildLabel;
|
||||
account.LastLogin = new Date();
|
||||
}
|
||||
await account.save();
|
||||
|
||||
// Tell WebUI its nonce has been invalidated
|
||||
sendWsBroadcastTo(account._id.toString(), { logged_out: true });
|
||||
|
||||
response.json(createLoginResponse(myAddress, myUrlBase, account.toJSON(), buildLabel));
|
||||
};
|
||||
|
||||
|
@ -8,6 +8,8 @@ import {
|
||||
setAccountGotLoginRewardToday
|
||||
} from "@/src/services/loginRewardService";
|
||||
import { getInventory } from "@/src/services/inventoryService";
|
||||
import { config } from "@/src/services/configService";
|
||||
import { sendWsBroadcastTo } from "@/src/services/webService";
|
||||
|
||||
export const loginRewardsController: RequestHandler = async (req, res) => {
|
||||
const account = await getAccountForRequest(req);
|
||||
@ -15,7 +17,7 @@ export const loginRewardsController: RequestHandler = async (req, res) => {
|
||||
const isMilestoneDay = account.LoginDays == 5 || account.LoginDays % 50 == 0;
|
||||
const nextMilestoneDay = account.LoginDays < 5 ? 5 : (Math.trunc(account.LoginDays / 50) + 1) * 50;
|
||||
|
||||
if (today == account.LastLoginRewardDate) {
|
||||
if (today == account.LastLoginRewardDate || config.disableDailyTribute) {
|
||||
res.json({
|
||||
DailyTributeInfo: {
|
||||
IsMilestoneDay: isMilestoneDay,
|
||||
@ -46,10 +48,10 @@ export const loginRewardsController: RequestHandler = async (req, res) => {
|
||||
response.DailyTributeInfo.HasChosenReward = true;
|
||||
response.DailyTributeInfo.ChosenReward = randomRewards[0];
|
||||
response.DailyTributeInfo.NewInventory = await claimLoginReward(inventory, randomRewards[0]);
|
||||
await inventory.save();
|
||||
|
||||
setAccountGotLoginRewardToday(account);
|
||||
await account.save();
|
||||
await Promise.all([inventory.save(), account.save()]);
|
||||
|
||||
sendWsBroadcastTo(account._id.toString(), { update_inventory: true });
|
||||
}
|
||||
res.json(response);
|
||||
};
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
} from "@/src/services/loginRewardService";
|
||||
import { getAccountForRequest } from "@/src/services/loginService";
|
||||
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
|
||||
import { sendWsBroadcastTo } from "@/src/services/webService";
|
||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
|
||||
import { logger } from "@/src/utils/logger";
|
||||
import { RequestHandler } from "express";
|
||||
@ -34,11 +35,10 @@ export const loginRewardsSelectionController: RequestHandler = async (req, res)
|
||||
chosenReward = randomRewards.find(x => x.StoreItemType == body.ChosenReward)!;
|
||||
inventoryChanges = await claimLoginReward(inventory, chosenReward);
|
||||
}
|
||||
await inventory.save();
|
||||
|
||||
setAccountGotLoginRewardToday(account);
|
||||
await account.save();
|
||||
await Promise.all([inventory.save(), account.save()]);
|
||||
|
||||
sendWsBroadcastTo(account._id.toString(), { update_inventory: true });
|
||||
res.json({
|
||||
DailyTributeInfo: {
|
||||
NewInventory: inventoryChanges,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { RequestHandler } from "express";
|
||||
import { Account } from "@/src/models/loginModel";
|
||||
import { sendWsBroadcastTo } from "@/src/services/webService";
|
||||
|
||||
export const logoutController: RequestHandler = async (req, res) => {
|
||||
if (!req.query.accountId) {
|
||||
@ -10,7 +11,7 @@ export const logoutController: RequestHandler = async (req, res) => {
|
||||
throw new Error("Request is missing nonce parameter");
|
||||
}
|
||||
|
||||
await Account.updateOne(
|
||||
const stat = await Account.updateOne(
|
||||
{
|
||||
_id: req.query.accountId,
|
||||
Nonce: nonce
|
||||
@ -19,6 +20,10 @@ export const logoutController: RequestHandler = async (req, res) => {
|
||||
Nonce: 0
|
||||
}
|
||||
);
|
||||
if (stat.modifiedCount) {
|
||||
// Tell WebUI its nonce has been invalidated
|
||||
sendWsBroadcastTo(req.query.accountId as string, { logged_out: true });
|
||||
}
|
||||
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/html",
|
||||
|
@ -7,6 +7,7 @@ import { generateRewardSeed, getInventory } from "@/src/services/inventoryServic
|
||||
import { getInventoryResponse } from "./inventoryController";
|
||||
import { logger } from "@/src/utils/logger";
|
||||
import { IMissionInventoryUpdateResponse } from "@/src/types/missionTypes";
|
||||
import { sendWsBroadcastTo } from "@/src/services/webService";
|
||||
|
||||
/*
|
||||
**** INPUT ****
|
||||
@ -76,6 +77,7 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
|
||||
InventoryJson: JSON.stringify(inventoryResponse),
|
||||
MissionRewards: []
|
||||
});
|
||||
sendWsBroadcastTo(account._id.toString(), { update_inventory: true });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -86,7 +88,7 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
|
||||
AffiliationMods,
|
||||
SyndicateXPItemReward,
|
||||
ConquestCompletedMissionsCount
|
||||
} = await addMissionRewards(inventory, missionReport, firstCompletion);
|
||||
} = await addMissionRewards(account, inventory, missionReport, firstCompletion);
|
||||
|
||||
if (missionReport.EndOfMatchUpload) {
|
||||
inventory.RewardSeed = generateRewardSeed();
|
||||
@ -106,6 +108,7 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
|
||||
AffiliationMods,
|
||||
ConquestCompletedMissionsCount
|
||||
} satisfies IMissionInventoryUpdateResponse);
|
||||
sendWsBroadcastTo(account._id.toString(), { update_inventory: true });
|
||||
};
|
||||
|
||||
/*
|
||||
|
@ -3,6 +3,7 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import { getInventory, updateCurrency } from "@/src/services/inventoryService";
|
||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
|
||||
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
|
||||
import { sendWsBroadcastTo } from "@/src/services/webService";
|
||||
|
||||
interface INameWeaponRequest {
|
||||
ItemName: string;
|
||||
@ -27,4 +28,5 @@ export const nameWeaponController: RequestHandler = async (req, res) => {
|
||||
res.json({
|
||||
InventoryChanges: currencyChanges
|
||||
});
|
||||
sendWsBroadcastTo(accountId, { update_inventory: true });
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { version_compare } from "@/src/helpers/inventoryHelpers";
|
||||
import {
|
||||
antivirusMods,
|
||||
consumeModCharge,
|
||||
decodeNemesisGuess,
|
||||
encodeNemesisGuess,
|
||||
@ -7,13 +8,13 @@ import {
|
||||
getKnifeUpgrade,
|
||||
getNemesisManifest,
|
||||
getNemesisPasscode,
|
||||
getNemesisPasscodeModTypes,
|
||||
GUESS_CORRECT,
|
||||
GUESS_INCORRECT,
|
||||
GUESS_NEUTRAL,
|
||||
GUESS_NONE,
|
||||
GUESS_WILDCARD,
|
||||
IKnifeResponse
|
||||
IKnifeResponse,
|
||||
parseUpgrade
|
||||
} from "@/src/helpers/nemesisHelpers";
|
||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
|
||||
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
|
||||
@ -134,34 +135,38 @@ export const nemesisController: RequestHandler = async (req, res) => {
|
||||
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":
|
||||
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod":
|
||||
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;
|
||||
|
||||
// Weaken nemesis now.
|
||||
inventory.Nemesis!.InfNodes = [
|
||||
{
|
||||
Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode,
|
||||
Influence: 1
|
||||
}
|
||||
];
|
||||
inventory.Nemesis!.Weakened = true;
|
||||
const upgrade = getKnifeUpgrade(inventory, dataknifeUpgrades, antivirusMods[passcode]);
|
||||
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
|
||||
}
|
||||
}
|
||||
|
||||
if (inventory.Nemesis!.HenchmenKilled < 100) {
|
||||
inventory.Nemesis!.InfNodes = getInfNodes(getNemesisManifest(inventory.Nemesis!.manifest), 0);
|
||||
}
|
||||
|
||||
await inventory.save();
|
||||
res.json(response);
|
||||
@ -198,16 +203,40 @@ export const nemesisController: RequestHandler = async (req, res) => {
|
||||
guess[body.position].result = correct ? GUESS_CORRECT : GUESS_INCORRECT;
|
||||
inventory.Nemesis!.GuessHistory[inventory.Nemesis!.GuessHistory.length - 1] = encodeNemesisGuess(guess);
|
||||
|
||||
// Increase rank if incorrect
|
||||
let RankIncrease: number | undefined;
|
||||
if (!correct) {
|
||||
RankIncrease = 1;
|
||||
const response: INemesisRequiemResponse = {};
|
||||
if (correct) {
|
||||
if (body.position == 2) {
|
||||
// That was all 3 guesses correct, nemesis is now weakened.
|
||||
inventory.Nemesis!.InfNodes = [
|
||||
{
|
||||
Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode,
|
||||
Influence: 1
|
||||
}
|
||||
];
|
||||
inventory.Nemesis!.Weakened = true;
|
||||
|
||||
// Subtract a charge from all requiem mods installed on parazon
|
||||
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
|
||||
const dataknifeLoadout = loadout.DATAKNIFE.id(
|
||||
inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid
|
||||
);
|
||||
const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0;
|
||||
const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!;
|
||||
for (let i = 3; i != 6; ++i) {
|
||||
//logger.debug(`subtracting a charge from ${dataknifeUpgrades[i]}`);
|
||||
const upgrade = parseUpgrade(inventory, dataknifeUpgrades[i]);
|
||||
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Guess was incorrect, increase rank
|
||||
response.RankIncrease = 1;
|
||||
const manifest = getNemesisManifest(inventory.Nemesis!.manifest);
|
||||
inventory.Nemesis!.Rank = Math.min(inventory.Nemesis!.Rank + 1, manifest.systemIndexes.length - 1);
|
||||
inventory.Nemesis!.InfNodes = getInfNodes(manifest, inventory.Nemesis!.Rank);
|
||||
}
|
||||
await inventory.save();
|
||||
res.json({ RankIncrease });
|
||||
res.json(response);
|
||||
}
|
||||
} else if ((req.query.mode as string) == "rs") {
|
||||
// report spawn; POST but no application data in body
|
||||
@ -277,36 +306,15 @@ export const nemesisController: RequestHandler = async (req, res) => {
|
||||
target: inventory.toJSON().Nemesis
|
||||
});
|
||||
} else if ((req.query.mode as string) == "w") {
|
||||
const inventory = await getInventory(
|
||||
account._id.toString(),
|
||||
"Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades"
|
||||
);
|
||||
const inventory = await getInventory(account._id.toString(), "Nemesis");
|
||||
//const body = getJSONfromString<INemesisWeakenRequest>(String(req.body));
|
||||
|
||||
inventory.Nemesis!.InfNodes = [
|
||||
{
|
||||
Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode,
|
||||
Influence: 1
|
||||
}
|
||||
];
|
||||
inventory.Nemesis!.Weakened = true;
|
||||
// As of 38.6.0, this request is no longer sent, instead mode=r already weakens the nemesis if appropriate.
|
||||
// We always weaken the nemesis in mode=r so simply giving the client back the nemesis.
|
||||
|
||||
const response: IKnifeResponse & { target: INemesisClient } = {
|
||||
const response: INemesisWeakenResponse = {
|
||||
target: inventory.toJSON<IInventoryClient>().Nemesis!
|
||||
};
|
||||
|
||||
// Consume charge of the correct requiem mod(s)
|
||||
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
|
||||
const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid);
|
||||
const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0;
|
||||
const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!;
|
||||
const modTypes = getNemesisPasscodeModTypes(inventory.Nemesis!);
|
||||
for (const modType of modTypes) {
|
||||
const upgrade = getKnifeUpgrade(inventory, dataknifeUpgrades, modType);
|
||||
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
|
||||
}
|
||||
|
||||
await inventory.save();
|
||||
res.json(response);
|
||||
} else {
|
||||
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
|
||||
@ -362,11 +370,19 @@ interface INemesisRequiemRequest {
|
||||
knife?: IKnife;
|
||||
}
|
||||
|
||||
interface INemesisRequiemResponse extends IKnifeResponse {
|
||||
RankIncrease?: number;
|
||||
}
|
||||
|
||||
// interface INemesisWeakenRequest {
|
||||
// target: INemesisClient;
|
||||
// knife: IKnife;
|
||||
// }
|
||||
|
||||
interface INemesisWeakenResponse extends IKnifeResponse {
|
||||
target: INemesisClient;
|
||||
}
|
||||
|
||||
interface IKnife {
|
||||
Item: IEquipmentClient;
|
||||
Skins: IWeaponSkinClient[];
|
||||
|
@ -57,7 +57,15 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) =
|
||||
component.DecoCapacity -= meta.capacityCost;
|
||||
}
|
||||
} else {
|
||||
const itemType = Object.entries(ExportResources).find(arr => arr[1].deco == deco.Type)![0];
|
||||
const entry = Object.entries(ExportResources).find(arr => arr[1].deco == deco.Type);
|
||||
if (!entry) {
|
||||
throw new Error(`unknown deco type: ${deco.Type}`);
|
||||
}
|
||||
const [itemType, meta] = entry;
|
||||
if (meta.dojoCapacityCost === undefined) {
|
||||
throw new Error(`unknown deco type: ${deco.Type}`);
|
||||
}
|
||||
component.DecoCapacity -= meta.dojoCapacityCost;
|
||||
if (deco.Sockets !== undefined) {
|
||||
guild.VaultFusionTreasures!.find(x => x.ItemType == itemType && x.Sockets == deco.Sockets)!.ItemCount -=
|
||||
1;
|
||||
@ -71,7 +79,13 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) =
|
||||
if (meta) {
|
||||
processDojoBuildMaterialsGathered(guild, meta);
|
||||
}
|
||||
} else if (guild.AutoContributeFromVault && guild.VaultRegularCredits && guild.VaultMiscItems) {
|
||||
} else if (
|
||||
deco.Type.startsWith("/Lotus/Objects/Tenno/Dojo/NpcPlaceables/") ||
|
||||
(guild.AutoContributeFromVault && guild.VaultRegularCredits && guild.VaultMiscItems)
|
||||
) {
|
||||
if (!guild.VaultRegularCredits || !guild.VaultMiscItems) {
|
||||
throw new Error(`dojo visitor placed without anything in vault?!`);
|
||||
}
|
||||
if (guild.VaultRegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) {
|
||||
let enoughMiscItems = true;
|
||||
for (const ingredient of meta.ingredients) {
|
||||
|
@ -3,6 +3,7 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import { IPurchaseRequest } from "@/src/types/purchaseTypes";
|
||||
import { handlePurchase } from "@/src/services/purchaseService";
|
||||
import { getInventory } from "@/src/services/inventoryService";
|
||||
import { sendWsBroadcastTo } from "@/src/services/webService";
|
||||
|
||||
export const purchaseController: RequestHandler = async (req, res) => {
|
||||
const purchaseRequest = JSON.parse(String(req.body)) as IPurchaseRequest;
|
||||
@ -10,5 +11,7 @@ export const purchaseController: RequestHandler = async (req, res) => {
|
||||
const inventory = await getInventory(accountId);
|
||||
const response = await handlePurchase(purchaseRequest, inventory);
|
||||
await inventory.save();
|
||||
//console.log(JSON.stringify(response, null, 2));
|
||||
res.json(response);
|
||||
sendWsBroadcastTo(accountId, { update_inventory: true });
|
||||
};
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
|
||||
import { getInventory, updateCurrency } from "@/src/services/inventoryService";
|
||||
import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import { sendWsBroadcastTo } from "@/src/services/webService";
|
||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
|
||||
import { RequestHandler } from "express";
|
||||
|
||||
export const renamePetController: RequestHandler = async (req, res) => {
|
||||
@ -8,13 +10,20 @@ export const renamePetController: RequestHandler = async (req, res) => {
|
||||
const inventory = await getInventory(accountId, "KubrowPets PremiumCredits PremiumCreditsFree");
|
||||
const data = getJSONfromString<IRenamePetRequest>(String(req.body));
|
||||
const details = inventory.KubrowPets.id(data.petId)!.Details!;
|
||||
|
||||
details.Name = data.name;
|
||||
const currencyChanges = updateCurrency(inventory, 15, true);
|
||||
|
||||
const inventoryChanges: IInventoryChanges = {};
|
||||
if (!("webui" in req.query)) {
|
||||
updateCurrency(inventory, 15, true, inventoryChanges);
|
||||
}
|
||||
|
||||
await inventory.save();
|
||||
res.json({
|
||||
...data,
|
||||
inventoryChanges: currencyChanges
|
||||
inventoryChanges: inventoryChanges
|
||||
});
|
||||
sendWsBroadcastTo(accountId, { update_inventory: true });
|
||||
};
|
||||
|
||||
interface IRenamePetRequest {
|
||||
|
@ -24,7 +24,7 @@ export const saveDialogueController: RequestHandler = async (req, res) => {
|
||||
inventory.DialogueHistory.Dialogues ??= [];
|
||||
const dialogue = getDialogue(inventory, request.DialogueName);
|
||||
dialogue.Rank = request.Rank;
|
||||
dialogue.Chemistry = request.Chemistry;
|
||||
dialogue.Chemistry += request.Chemistry;
|
||||
dialogue.QueuedDialogues = request.QueuedDialogues;
|
||||
for (const bool of request.Booleans) {
|
||||
dialogue.Booleans.push(bool);
|
||||
|
@ -15,9 +15,11 @@ 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";
|
||||
import { sendWsBroadcastTo } from "@/src/services/webService";
|
||||
|
||||
export const sellController: RequestHandler = async (req, res) => {
|
||||
const payload = JSON.parse(String(req.body)) as ISellRequest;
|
||||
//console.log(JSON.stringify(payload, null, 2));
|
||||
const accountId = await getAccountIdForRequest(req);
|
||||
const requiredFields = new Set<keyof TInventoryDatabaseDocument>();
|
||||
if (payload.SellCurrency == "SC_RegularCredits") {
|
||||
@ -57,6 +59,9 @@ export const sellController: RequestHandler = async (req, res) => {
|
||||
if (payload.Items.Hoverboards) {
|
||||
requiredFields.add(InventorySlot.SPACESUITS);
|
||||
}
|
||||
if (payload.Items.CrewMembers) {
|
||||
requiredFields.add(InventorySlot.CREWMEMBERS);
|
||||
}
|
||||
if (payload.Items.CrewShipWeapons || payload.Items.CrewShipWeaponSkins) {
|
||||
requiredFields.add(InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
|
||||
requiredFields.add("CrewShipRawSalvage");
|
||||
@ -180,6 +185,17 @@ export const sellController: RequestHandler = async (req, res) => {
|
||||
inventory.Drones.pull({ _id: sellItem.String });
|
||||
});
|
||||
}
|
||||
if (payload.Items.KubrowPetPrints) {
|
||||
payload.Items.KubrowPetPrints.forEach(sellItem => {
|
||||
inventory.KubrowPetPrints.pull({ _id: sellItem.String });
|
||||
});
|
||||
}
|
||||
if (payload.Items.CrewMembers) {
|
||||
payload.Items.CrewMembers.forEach(sellItem => {
|
||||
inventory.CrewMembers.pull({ _id: sellItem.String });
|
||||
freeUpSlot(inventory, InventorySlot.CREWMEMBERS);
|
||||
});
|
||||
}
|
||||
if (payload.Items.CrewShipWeapons) {
|
||||
payload.Items.CrewShipWeapons.forEach(sellItem => {
|
||||
if (sellItem.String[0] == "/") {
|
||||
@ -279,6 +295,7 @@ export const sellController: RequestHandler = async (req, res) => {
|
||||
res.json({
|
||||
inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges"
|
||||
});
|
||||
sendWsBroadcastTo(accountId, { update_inventory: true });
|
||||
};
|
||||
|
||||
interface ISellRequest {
|
||||
@ -301,6 +318,8 @@ interface ISellRequest {
|
||||
OperatorAmps?: ISellItem[];
|
||||
Hoverboards?: ISellItem[];
|
||||
Drones?: ISellItem[];
|
||||
KubrowPetPrints?: ISellItem[];
|
||||
CrewMembers?: ISellItem[];
|
||||
CrewShipWeapons?: ISellItem[];
|
||||
CrewShipWeaponSkins?: ISellItem[];
|
||||
};
|
||||
|
22
src/controllers/api/setSuitInfectionController.ts
Normal file
22
src/controllers/api/setSuitInfectionController.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { fromMongoDate, fromOid } from "@/src/helpers/inventoryHelpers";
|
||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
|
||||
import { getInventory } from "@/src/services/inventoryService";
|
||||
import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
|
||||
import { RequestHandler } from "express";
|
||||
|
||||
export const setSuitInfectionController: RequestHandler = async (req, res) => {
|
||||
const accountId = await getAccountIdForRequest(req);
|
||||
const inventory = await getInventory(accountId, "Suits");
|
||||
const payload = getJSONfromString<ISetSuitInfectionRequest>(String(req.body));
|
||||
for (const clientSuit of payload.Suits) {
|
||||
const dbSuit = inventory.Suits.id(fromOid(clientSuit.ItemId))!;
|
||||
dbSuit.InfestationDate = fromMongoDate(clientSuit.InfestationDate!);
|
||||
}
|
||||
await inventory.save();
|
||||
res.end();
|
||||
};
|
||||
|
||||
interface ISetSuitInfectionRequest {
|
||||
Suits: IEquipmentClient[];
|
||||
}
|
@ -45,9 +45,9 @@ export const startRecipeController: RequestHandler = async (req, res) => {
|
||||
for (let i = 0; i != recipe.ingredients.length; ++i) {
|
||||
if (startRecipeRequest.Ids[i] && startRecipeRequest.Ids[i][0] != "/") {
|
||||
if (recipe.ingredients[i].ItemType == "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem") {
|
||||
const index = inventory.KubrowPetEggs!.findIndex(x => x._id.equals(startRecipeRequest.Ids[i]));
|
||||
const index = inventory.KubrowPetEggs.findIndex(x => x._id.equals(startRecipeRequest.Ids[i]));
|
||||
if (index != -1) {
|
||||
inventory.KubrowPetEggs!.splice(index, 1);
|
||||
inventory.KubrowPetEggs.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
const category = ExportWeapons[recipe.ingredients[i].ItemType].productCategory;
|
||||
@ -72,6 +72,10 @@ export const startRecipeController: RequestHandler = async (req, res) => {
|
||||
if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") {
|
||||
inventoryChanges = addKubrowPet(inventory, getRandomElement(recipe.secretIngredients!)!.ItemType);
|
||||
pr.KubrowPet = new Types.ObjectId(fromOid(inventoryChanges.KubrowPets![0].ItemId));
|
||||
} else if (recipe.secretIngredientAction == "SIA_DISTILL_PRINT") {
|
||||
pr.KubrowPet = new Types.ObjectId(startRecipeRequest.Ids[recipe.ingredients.length]);
|
||||
const pet = inventory.KubrowPets.id(pr.KubrowPet)!;
|
||||
pet.Details!.PrintsRemaining -= 1;
|
||||
} else if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
|
||||
const spectreLoadout: ISpectreLoadout = {
|
||||
ItemType: recipe.resultType,
|
||||
|
@ -31,13 +31,13 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
|
||||
AffiliationTag: data.AffiliationTag,
|
||||
InventoryChanges: {},
|
||||
Level: data.SacrificeLevel,
|
||||
LevelIncrease: levelIncrease,
|
||||
LevelIncrease: data.SacrificeLevel < 0 ? 1 : levelIncrease,
|
||||
NewEpisodeReward: false
|
||||
};
|
||||
|
||||
// Process sacrifices and rewards for every level we're reaching
|
||||
const manifest = ExportSyndicates[data.AffiliationTag];
|
||||
for (let level = oldLevel + levelIncrease; level <= data.SacrificeLevel; ++level) {
|
||||
for (let level = oldLevel + Math.min(levelIncrease, 1); level <= data.SacrificeLevel; ++level) {
|
||||
let sacrifice: ISyndicateSacrifice | undefined;
|
||||
if (level == 0) {
|
||||
sacrifice = manifest.initiationSacrifice;
|
||||
@ -94,7 +94,7 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
|
||||
}
|
||||
|
||||
// Commit
|
||||
syndicate.Title = data.SacrificeLevel;
|
||||
syndicate.Title = data.SacrificeLevel < 0 ? data.SacrificeLevel + 1 : data.SacrificeLevel;
|
||||
await inventory.save();
|
||||
|
||||
response.json(res);
|
||||
|
@ -5,7 +5,7 @@ import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTy
|
||||
import { IOid } from "@/src/types/commonTypes";
|
||||
import { ExportSyndicates, ExportWeapons } from "warframe-public-export-plus";
|
||||
import { logger } from "@/src/utils/logger";
|
||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
|
||||
import { IAffiliationMods, IInventoryChanges } from "@/src/types/purchaseTypes";
|
||||
import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
|
||||
|
||||
export const syndicateStandingBonusController: RequestHandler = async (req, res) => {
|
||||
@ -54,13 +54,14 @@ export const syndicateStandingBonusController: RequestHandler = async (req, res)
|
||||
inventoryChanges[slotBin] = { count: -1, platinum: 0, Slots: 1 };
|
||||
}
|
||||
|
||||
const affiliationMod = addStanding(inventory, request.Operation.AffiliationTag, gainedStanding, true);
|
||||
const affiliationMods: IAffiliationMods[] = [];
|
||||
addStanding(inventory, request.Operation.AffiliationTag, gainedStanding, affiliationMods, true);
|
||||
|
||||
await inventory.save();
|
||||
|
||||
res.json({
|
||||
InventoryChanges: inventoryChanges,
|
||||
AffiliationMods: [affiliationMod]
|
||||
AffiliationMods: affiliationMods
|
||||
});
|
||||
};
|
||||
|
||||
|
27
src/controllers/api/umbraController.ts
Normal file
27
src/controllers/api/umbraController.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { fromMongoDate, fromOid } from "@/src/helpers/inventoryHelpers";
|
||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
|
||||
import { addMiscItem, getInventory } from "@/src/services/inventoryService";
|
||||
import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
|
||||
import { RequestHandler } from "express";
|
||||
|
||||
export const umbraController: RequestHandler = async (req, res) => {
|
||||
const accountId = await getAccountIdForRequest(req);
|
||||
const inventory = await getInventory(accountId, "Suits MiscItems");
|
||||
const payload = getJSONfromString<IUmbraRequest>(String(req.body));
|
||||
for (const clientSuit of payload.Suits) {
|
||||
const dbSuit = inventory.Suits.id(fromOid(clientSuit.ItemId))!;
|
||||
if (clientSuit.UmbraDate) {
|
||||
addMiscItem(inventory, "/Lotus/Types/Items/MiscItems/UmbraEchoes", -1);
|
||||
dbSuit.UmbraDate = fromMongoDate(clientSuit.UmbraDate);
|
||||
} else {
|
||||
dbSuit.UmbraDate = undefined;
|
||||
}
|
||||
}
|
||||
await inventory.save();
|
||||
res.end();
|
||||
};
|
||||
|
||||
interface IUmbraRequest {
|
||||
Suits: IEquipmentClient[];
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
import { RequestHandler } from "express";
|
||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
|
||||
import { getAccountForRequest } from "@/src/services/loginService";
|
||||
import { addChallenges, getInventory } from "@/src/services/inventoryService";
|
||||
import { addCalendarProgress, addChallenges, getInventory } from "@/src/services/inventoryService";
|
||||
import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes";
|
||||
import { IAffiliationMods } from "@/src/types/purchaseTypes";
|
||||
import { getEntriesUnsafe } from "@/src/utils/ts-utils";
|
||||
import { logger } from "@/src/utils/logger";
|
||||
|
||||
export const updateChallengeProgressController: RequestHandler = async (req, res) => {
|
||||
const challenges = getJSONfromString<IUpdateChallengeProgressRequest>(String(req.body));
|
||||
@ -11,7 +13,7 @@ export const updateChallengeProgressController: RequestHandler = async (req, res
|
||||
|
||||
const inventory = await getInventory(
|
||||
account._id.toString(),
|
||||
"ChallengeProgress SeasonChallengeHistory Affiliations"
|
||||
"ChallengesFixVersion ChallengeProgress SeasonChallengeHistory Affiliations CalendarProgress"
|
||||
);
|
||||
let affiliationMods: IAffiliationMods[] = [];
|
||||
if (challenges.ChallengeProgress) {
|
||||
@ -22,8 +24,18 @@ export const updateChallengeProgressController: RequestHandler = async (req, res
|
||||
challenges.SeasonChallengeCompletions
|
||||
);
|
||||
}
|
||||
if (challenges.SeasonChallengeHistory) {
|
||||
challenges.SeasonChallengeHistory.forEach(({ challenge, id }) => {
|
||||
for (const [key, value] of getEntriesUnsafe(challenges)) {
|
||||
if (value === undefined) {
|
||||
logger.error(`Challenge progress update key ${key} has no value`);
|
||||
continue;
|
||||
}
|
||||
switch (key) {
|
||||
case "ChallengesFixVersion":
|
||||
inventory.ChallengesFixVersion = value;
|
||||
break;
|
||||
|
||||
case "SeasonChallengeHistory":
|
||||
value.forEach(({ challenge, id }) => {
|
||||
const itemIndex = inventory.SeasonChallengeHistory.findIndex(i => i.challenge === challenge);
|
||||
if (itemIndex !== -1) {
|
||||
inventory.SeasonChallengeHistory[itemIndex].id = id;
|
||||
@ -31,6 +43,20 @@ export const updateChallengeProgressController: RequestHandler = async (req, res
|
||||
inventory.SeasonChallengeHistory.push({ challenge, id });
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case "CalendarProgress":
|
||||
addCalendarProgress(inventory, value);
|
||||
break;
|
||||
|
||||
case "ChallengeProgress":
|
||||
case "SeasonChallengeCompletions":
|
||||
case "ChallengePTS":
|
||||
case "crossPlaySetting":
|
||||
break;
|
||||
default:
|
||||
logger.warn(`unknown challenge progress entry`, { key, value });
|
||||
}
|
||||
}
|
||||
await inventory.save();
|
||||
|
||||
@ -40,7 +66,11 @@ export const updateChallengeProgressController: RequestHandler = async (req, res
|
||||
};
|
||||
|
||||
interface IUpdateChallengeProgressRequest {
|
||||
ChallengePTS?: number;
|
||||
ChallengesFixVersion?: number;
|
||||
ChallengeProgress?: IChallengeProgress[];
|
||||
SeasonChallengeHistory?: ISeasonChallenge[];
|
||||
SeasonChallengeCompletions?: ISeasonChallenge[];
|
||||
CalendarProgress?: { challenge: string }[];
|
||||
crossPlaySetting?: string;
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import { getInventory, addRecipes } from "@/src/services/inventoryService";
|
||||
import { RequestHandler } from "express";
|
||||
import { ExportRecipes } from "warframe-public-export-plus";
|
||||
|
||||
export const addMissingHelminthBlueprintsController: RequestHandler = async (req, res) => {
|
||||
const accountId = await getAccountIdForRequest(req);
|
||||
const inventory = await getInventory(accountId, "Recipes");
|
||||
const allHelminthRecipes = Object.keys(ExportRecipes).filter(
|
||||
key => ExportRecipes[key].secretIngredientAction === "SIA_WARFRAME_ABILITY"
|
||||
);
|
||||
const inventoryHelminthRecipes = inventory.Recipes.filter(recipe =>
|
||||
recipe.ItemType.startsWith("/Lotus/Types/Recipes/AbilityOverrides/")
|
||||
).map(recipe => recipe.ItemType);
|
||||
|
||||
const missingHelminthRecipes = allHelminthRecipes
|
||||
.filter(key => !inventoryHelminthRecipes.includes(key))
|
||||
.map(ItemType => ({ ItemType, ItemCount: 1 }));
|
||||
|
||||
addRecipes(inventory, missingHelminthRecipes);
|
||||
|
||||
await inventory.save();
|
||||
res.end();
|
||||
};
|
40
src/controllers/custom/completeAllMissionsController.ts
Normal file
40
src/controllers/custom/completeAllMissionsController.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { addString } from "@/src/helpers/stringHelpers";
|
||||
import { getInventory } from "@/src/services/inventoryService";
|
||||
import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import { addFixedLevelRewards } from "@/src/services/missionInventoryUpdateService";
|
||||
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
|
||||
import { IMissionReward } from "@/src/types/missionTypes";
|
||||
import { RequestHandler } from "express";
|
||||
import { ExportRegions } from "warframe-public-export-plus";
|
||||
|
||||
export const completeAllMissionsController: RequestHandler = async (req, res) => {
|
||||
const accountId = await getAccountIdForRequest(req);
|
||||
const inventory = await getInventory(accountId);
|
||||
const MissionRewards: IMissionReward[] = [];
|
||||
for (const [tag, node] of Object.entries(ExportRegions)) {
|
||||
let mission = inventory.Missions.find(x => x.Tag == tag);
|
||||
if (!mission) {
|
||||
mission =
|
||||
inventory.Missions[
|
||||
inventory.Missions.push({
|
||||
Completes: 0,
|
||||
Tier: 0,
|
||||
Tag: tag
|
||||
}) - 1
|
||||
];
|
||||
}
|
||||
if (mission.Completes == 0) {
|
||||
mission.Completes++;
|
||||
if (node.missionReward) {
|
||||
addFixedLevelRewards(node.missionReward, MissionRewards);
|
||||
}
|
||||
}
|
||||
mission.Tier = 1;
|
||||
}
|
||||
for (const reward of MissionRewards) {
|
||||
await handleStoreItemAcquisition(reward.StoreItem, inventory, reward.ItemCount, undefined, true);
|
||||
}
|
||||
addString(inventory.NodeIntrosCompleted, "TeshinHardModeUnlocked");
|
||||
await inventory.save();
|
||||
res.end();
|
||||
};
|
44
src/controllers/custom/configController.ts
Normal file
44
src/controllers/custom/configController.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { RequestHandler } from "express";
|
||||
import { config } from "@/src/services/configService";
|
||||
import { getAccountForRequest, isAdministrator } from "@/src/services/loginService";
|
||||
import { saveConfig } from "@/src/services/configWatcherService";
|
||||
import { sendWsBroadcastExcept } from "@/src/services/webService";
|
||||
|
||||
export const getConfigController: RequestHandler = async (req, res) => {
|
||||
const account = await getAccountForRequest(req);
|
||||
if (isAdministrator(account)) {
|
||||
const responseData: Record<string, boolean | string | number | null> = {};
|
||||
for (const id of req.body as string[]) {
|
||||
const [obj, idx] = configIdToIndexable(id);
|
||||
responseData[id] = obj[idx] ?? null;
|
||||
}
|
||||
res.json(responseData);
|
||||
} else {
|
||||
res.status(401).end();
|
||||
}
|
||||
};
|
||||
|
||||
export const setConfigController: RequestHandler = async (req, res) => {
|
||||
const account = await getAccountForRequest(req);
|
||||
if (isAdministrator(account)) {
|
||||
for (const [id, value] of Object.entries(req.body as Record<string, boolean | string | number>)) {
|
||||
const [obj, idx] = configIdToIndexable(id);
|
||||
obj[idx] = value;
|
||||
}
|
||||
sendWsBroadcastExcept(parseInt(String(req.query.wsid)), { config_reloaded: true });
|
||||
await saveConfig();
|
||||
res.end();
|
||||
} else {
|
||||
res.status(401).end();
|
||||
}
|
||||
};
|
||||
|
||||
const configIdToIndexable = (id: string): [Record<string, boolean | string | number | undefined>, string] => {
|
||||
let obj = config as unknown as Record<string, never>;
|
||||
const arr = id.split(".");
|
||||
while (arr.length > 1) {
|
||||
obj = obj[arr[0]];
|
||||
arr.splice(0, 1);
|
||||
}
|
||||
return [obj, arr[0]];
|
||||
};
|
@ -1,14 +0,0 @@
|
||||
import { RequestHandler } from "express";
|
||||
import { config } from "@/src/services/configService";
|
||||
import { getAccountForRequest, isAdministrator } from "@/src/services/loginService";
|
||||
|
||||
const getConfigDataController: RequestHandler = async (req, res) => {
|
||||
const account = await getAccountForRequest(req);
|
||||
if (isAdministrator(account)) {
|
||||
res.json(config);
|
||||
} else {
|
||||
res.status(401).end();
|
||||
}
|
||||
};
|
||||
|
||||
export { getConfigDataController };
|
@ -20,7 +20,6 @@ import {
|
||||
ExportWeapons,
|
||||
TRelicQuality
|
||||
} from "warframe-public-export-plus";
|
||||
import archonCrystalUpgrades from "@/static/fixed_responses/webuiArchonCrystalUpgrades.json";
|
||||
import allIncarnons from "@/static/fixed_responses/allIncarnonList.json";
|
||||
|
||||
interface ListedItem {
|
||||
@ -36,7 +35,6 @@ interface ListedItem {
|
||||
}
|
||||
|
||||
interface ItemLists {
|
||||
archonCrystalUpgrades: Record<string, string>;
|
||||
uniqueLevelCaps: Record<string, number>;
|
||||
Suits: ListedItem[];
|
||||
LongGuns: ListedItem[];
|
||||
@ -57,6 +55,7 @@ interface ItemLists {
|
||||
EvolutionProgress: ListedItem[];
|
||||
mods: ListedItem[];
|
||||
Boosters: ListedItem[];
|
||||
//circuitGameModes: ListedItem[];
|
||||
}
|
||||
|
||||
const relicQualitySuffixes: Record<TRelicQuality, string> = {
|
||||
@ -66,10 +65,13 @@ const relicQualitySuffixes: Record<TRelicQuality, string> = {
|
||||
VPQ_PLATINUM: " [Exceptional]"
|
||||
};
|
||||
|
||||
/*const toTitleCase = (str: string): string => {
|
||||
return str.replace(/[^\s-]+/g, word => word.charAt(0).toUpperCase() + word.substr(1).toLowerCase());
|
||||
};*/
|
||||
|
||||
const getItemListsController: RequestHandler = (req, response) => {
|
||||
const lang = getDict(typeof req.query.lang == "string" ? req.query.lang : "en");
|
||||
const res: ItemLists = {
|
||||
archonCrystalUpgrades,
|
||||
uniqueLevelCaps: ExportMisc.uniqueLevelCaps,
|
||||
Suits: [],
|
||||
LongGuns: [],
|
||||
@ -90,6 +92,36 @@ const getItemListsController: RequestHandler = (req, response) => {
|
||||
EvolutionProgress: [],
|
||||
mods: [],
|
||||
Boosters: []
|
||||
/*circuitGameModes: [
|
||||
{
|
||||
uniqueName: "Survival",
|
||||
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Survival", lang))
|
||||
},
|
||||
{
|
||||
uniqueName: "VoidFlood",
|
||||
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Corruption", lang))
|
||||
},
|
||||
{
|
||||
uniqueName: "Excavation",
|
||||
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Excavation", lang))
|
||||
},
|
||||
{
|
||||
uniqueName: "Defense",
|
||||
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Defense", lang))
|
||||
},
|
||||
{
|
||||
uniqueName: "Exterminate",
|
||||
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Exterminate", lang))
|
||||
},
|
||||
{
|
||||
uniqueName: "Assassination",
|
||||
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Assassination", lang))
|
||||
},
|
||||
{
|
||||
uniqueName: "Alchemy",
|
||||
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Alchemy", lang))
|
||||
}
|
||||
]*/
|
||||
};
|
||||
for (const [uniqueName, item] of Object.entries(ExportWarframes)) {
|
||||
res[item.productCategory].push({
|
||||
|
@ -128,7 +128,7 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
|
||||
await completeQuest(inventory, questKey.ItemType);
|
||||
} else {
|
||||
const progress = {
|
||||
c: questManifest.chainStages![currentStage].key ? -1 : 0,
|
||||
c: 0,
|
||||
i: false,
|
||||
m: false,
|
||||
b: []
|
||||
|
@ -12,6 +12,7 @@ export const popArchonCrystalUpgradeController: RequestHandler = async (req, res
|
||||
);
|
||||
await inventory.save();
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
res.status(400).end();
|
||||
};
|
||||
|
@ -15,6 +15,7 @@ export const pushArchonCrystalUpgradeController: RequestHandler = async (req, re
|
||||
}
|
||||
await inventory.save();
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
}
|
||||
res.status(400).end();
|
||||
|
@ -23,9 +23,9 @@ export const setBoosterController: RequestHandler = async (req, res) => {
|
||||
res.status(400).send("Invalid ItemType provided.");
|
||||
return;
|
||||
}
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const now = Math.trunc(Date.now() / 1000);
|
||||
for (const { ItemType, ExpiryDate } of requests) {
|
||||
if (ExpiryDate < now) {
|
||||
if (ExpiryDate <= now) {
|
||||
// remove expired boosters
|
||||
const index = boosters.findIndex(item => item.ItemType === ItemType);
|
||||
if (index !== -1) {
|
||||
|
@ -1,15 +0,0 @@
|
||||
import { RequestHandler } from "express";
|
||||
import { updateConfig } from "@/src/services/configWatcherService";
|
||||
import { getAccountForRequest, isAdministrator } from "@/src/services/loginService";
|
||||
|
||||
const updateConfigDataController: RequestHandler = async (req, res) => {
|
||||
const account = await getAccountForRequest(req);
|
||||
if (isAdministrator(account)) {
|
||||
await updateConfig(String(req.body));
|
||||
res.end();
|
||||
} else {
|
||||
res.status(401).end();
|
||||
}
|
||||
};
|
||||
|
||||
export { updateConfigDataController };
|
39
src/controllers/custom/updateFingerprintController.ts
Normal file
39
src/controllers/custom/updateFingerprintController.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { getInventory } from "@/src/services/inventoryService";
|
||||
import { WeaponTypeInternal } from "@/src/services/itemDataService";
|
||||
import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import { RequestHandler } from "express";
|
||||
|
||||
export const updateFingerprintController: RequestHandler = async (req, res) => {
|
||||
const accountId = await getAccountIdForRequest(req);
|
||||
const request = req.body as IUpdateFingerPrintRequest;
|
||||
const inventory = await getInventory(accountId, request.category);
|
||||
const item = inventory[request.category].id(request.oid);
|
||||
if (item) {
|
||||
if (request.action == "set" && request.upgradeFingerprint.buffs[0].Tag) {
|
||||
const newUpgradeFingerprint = request.upgradeFingerprint;
|
||||
if (!newUpgradeFingerprint.compact) newUpgradeFingerprint.compact = item.ItemType;
|
||||
|
||||
item.UpgradeType = request.upgradeType;
|
||||
item.UpgradeFingerprint = JSON.stringify(newUpgradeFingerprint);
|
||||
} else if (request.action == "remove") {
|
||||
item.UpgradeFingerprint = undefined;
|
||||
item.UpgradeType = undefined;
|
||||
}
|
||||
await inventory.save();
|
||||
}
|
||||
res.end();
|
||||
};
|
||||
|
||||
interface IUpdateFingerPrintRequest {
|
||||
category: WeaponTypeInternal;
|
||||
oid: string;
|
||||
action: "set" | "remove";
|
||||
upgradeType: string;
|
||||
upgradeFingerprint: {
|
||||
compact?: string;
|
||||
buffs: {
|
||||
Tag: string;
|
||||
Value: number;
|
||||
}[];
|
||||
};
|
||||
}
|
10
src/controllers/custom/webuiFileChangeDetectedController.ts
Normal file
10
src/controllers/custom/webuiFileChangeDetectedController.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { args } from "@/src/helpers/commandLineArguments";
|
||||
import { sendWsBroadcast } from "@/src/services/webService";
|
||||
import { RequestHandler } from "express";
|
||||
|
||||
export const webuiFileChangeDetectedController: RequestHandler = (req, res) => {
|
||||
if (args.dev && args.secret && req.query.secret == args.secret) {
|
||||
sendWsBroadcast({ reload: true });
|
||||
}
|
||||
res.end();
|
||||
};
|
@ -1,6 +1,19 @@
|
||||
import { RequestHandler } from "express";
|
||||
import { getWorldState } from "@/src/services/worldStateService";
|
||||
import { getWorldState, populateDailyDeal, populateFissures } from "@/src/services/worldStateService";
|
||||
import { version_compare } from "@/src/helpers/inventoryHelpers";
|
||||
|
||||
export const worldStateController: RequestHandler = (req, res) => {
|
||||
res.json(getWorldState(req.query.buildLabel as string | undefined));
|
||||
export const worldStateController: RequestHandler = async (req, res) => {
|
||||
const buildLabel = req.query.buildLabel as string | undefined;
|
||||
const worldState = getWorldState(buildLabel);
|
||||
|
||||
const populatePromises = [populateDailyDeal(worldState)];
|
||||
|
||||
// Omitting void fissures for versions prior to Dante Unbound to avoid script errors.
|
||||
if (!buildLabel || version_compare(buildLabel, "2024.03.24.20.00") >= 0) {
|
||||
populatePromises.push(populateFissures(worldState));
|
||||
}
|
||||
|
||||
await Promise.all(populatePromises);
|
||||
|
||||
res.json(worldState);
|
||||
};
|
||||
|
23
src/helpers/commandLineArguments.ts
Normal file
23
src/helpers/commandLineArguments.ts
Normal file
@ -0,0 +1,23 @@
|
||||
interface IArguments {
|
||||
configPath?: string;
|
||||
dev?: boolean;
|
||||
secret?: string;
|
||||
}
|
||||
|
||||
export const args: IArguments = {};
|
||||
|
||||
for (let i = 2; i < process.argv.length; ) {
|
||||
switch (process.argv[i++]) {
|
||||
case "--configPath":
|
||||
args.configPath = process.argv[i++];
|
||||
break;
|
||||
|
||||
case "--dev":
|
||||
args.dev = true;
|
||||
break;
|
||||
|
||||
case "--secret":
|
||||
args.secret = process.argv[i++];
|
||||
break;
|
||||
}
|
||||
}
|
@ -237,7 +237,7 @@ export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: TNemesisFacti
|
||||
return passcode;
|
||||
};
|
||||
|
||||
const requiemMods: readonly string[] = [
|
||||
/*const requiemMods: readonly string[] = [
|
||||
"/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod",
|
||||
"/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod",
|
||||
"/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod",
|
||||
@ -246,9 +246,9 @@ const requiemMods: readonly string[] = [
|
||||
"/Lotus/Upgrades/Mods/Immortal/ImmortalSixMod",
|
||||
"/Lotus/Upgrades/Mods/Immortal/ImmortalSevenMod",
|
||||
"/Lotus/Upgrades/Mods/Immortal/ImmortalEightMod"
|
||||
];
|
||||
];*/
|
||||
|
||||
const antivirusMods: readonly string[] = [
|
||||
export const antivirusMods: readonly string[] = [
|
||||
"/Lotus/Upgrades/Mods/Immortal/AntivirusOneMod",
|
||||
"/Lotus/Upgrades/Mods/Immortal/AntivirusTwoMod",
|
||||
"/Lotus/Upgrades/Mods/Immortal/AntivirusThreeMod",
|
||||
@ -259,12 +259,12 @@ const antivirusMods: readonly string[] = [
|
||||
"/Lotus/Upgrades/Mods/Immortal/AntivirusEightMod"
|
||||
];
|
||||
|
||||
export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: TNemesisFaction }): string[] => {
|
||||
/*export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: TNemesisFaction }): string[] => {
|
||||
const passcode = getNemesisPasscode(nemesis);
|
||||
return nemesis.Faction == "FC_INFESTATION"
|
||||
? passcode.map(i => antivirusMods[i])
|
||||
: passcode.map(i => requiemMods[i]);
|
||||
};
|
||||
};*/
|
||||
|
||||
// Symbols; 0-7 are the normal requiem mods.
|
||||
export const GUESS_NONE = 8;
|
||||
@ -343,6 +343,27 @@ export const getKnifeUpgrade = (
|
||||
throw new Error(`${type} does not seem to be installed on parazon?!`);
|
||||
};
|
||||
|
||||
export const parseUpgrade = (
|
||||
inventory: TInventoryDatabaseDocument,
|
||||
str: string
|
||||
): { ItemId: IOid; ItemType: string } => {
|
||||
if (str.length == 24) {
|
||||
const upgrade = inventory.Upgrades.id(str);
|
||||
if (upgrade) {
|
||||
return {
|
||||
ItemId: { $oid: str },
|
||||
ItemType: upgrade.ItemType
|
||||
};
|
||||
}
|
||||
throw new Error(`Could not resolve oid ${str}`);
|
||||
} else {
|
||||
return {
|
||||
ItemId: { $oid: "000000000000000000000000" },
|
||||
ItemType: str
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const consumeModCharge = (
|
||||
response: IKnifeResponse,
|
||||
inventory: TInventoryDatabaseDocument,
|
||||
|
@ -1,5 +1,4 @@
|
||||
import path from "path";
|
||||
|
||||
export const rootDir = path.join(__dirname, "../..");
|
||||
export const isDev = path.basename(rootDir) != "build";
|
||||
export const repoDir = isDev ? rootDir : path.join(rootDir, "..");
|
||||
export const repoDir = path.basename(rootDir) != "build" ? rootDir : path.join(rootDir, "..");
|
||||
|
@ -6,6 +6,7 @@ import { logger } from "@/src/utils/logger";
|
||||
import { addMiscItems, combineInventoryChanges } from "@/src/services/inventoryService";
|
||||
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
|
||||
import { IInventoryChanges } from "../types/purchaseTypes";
|
||||
import { config } from "../services/configService";
|
||||
|
||||
export const crackRelic = async (
|
||||
inventory: TInventoryDatabaseDocument,
|
||||
@ -35,7 +36,13 @@ export const crackRelic = async (
|
||||
// Give reward
|
||||
combineInventoryChanges(
|
||||
inventoryChanges,
|
||||
(await handleStoreItemAcquisition(reward.type, inventory, reward.itemCount)).InventoryChanges
|
||||
(
|
||||
await handleStoreItemAcquisition(
|
||||
reward.type,
|
||||
inventory,
|
||||
reward.itemCount * (config.relicRewardItemCountMultiplier ?? 1)
|
||||
)
|
||||
).InventoryChanges
|
||||
);
|
||||
|
||||
return reward;
|
||||
|
@ -54,3 +54,9 @@ export const regexEscape = (str: string): string => {
|
||||
str = str.split("}").join("\\}");
|
||||
return str;
|
||||
};
|
||||
|
||||
export const addString = (arr: string[], str: string): void => {
|
||||
if (arr.indexOf(str) == -1) {
|
||||
arr.push(str);
|
||||
}
|
||||
};
|
||||
|
@ -10,3 +10,14 @@ export const getMaxStanding = (syndicate: ISyndicate, title: number): number =>
|
||||
}
|
||||
return syndicate.titles.find(x => x.level == title)!.maxStanding;
|
||||
};
|
||||
|
||||
export const getMinStanding = (syndicate: ISyndicate, title: number): number => {
|
||||
if (!syndicate.titles) {
|
||||
// LibrarySyndicate
|
||||
return 0;
|
||||
}
|
||||
if (title == 0) {
|
||||
return syndicate.titles.find(x => x.level == -1)!.maxStanding;
|
||||
}
|
||||
return syndicate.titles.find(x => x.level == title)!.minStanding;
|
||||
};
|
||||
|
19
src/index.ts
19
src/index.ts
@ -1,9 +1,14 @@
|
||||
// First, init config.
|
||||
import { config, loadConfig } from "@/src/services/configService";
|
||||
import { config, configPath, loadConfig } from "@/src/services/configService";
|
||||
import fs from "fs";
|
||||
try {
|
||||
loadConfig();
|
||||
} catch (e) {
|
||||
console.log("ERROR: Failed to load config.json. You can copy config.json.example to create your config.json.");
|
||||
if (fs.existsSync("config.json")) {
|
||||
console.log("Failed to load " + configPath + ": " + (e as Error).message);
|
||||
} else {
|
||||
console.log("Failed to load " + configPath + ". You can copy config.json.example to create your config file.");
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@ -16,7 +21,8 @@ import mongoose from "mongoose";
|
||||
import { JSONStringify } from "json-with-bigint";
|
||||
import { startWebServer } from "./services/webService";
|
||||
|
||||
import { validateConfig } from "@/src/services/configWatcherService";
|
||||
import { syncConfigWithDatabase, validateConfig } from "@/src/services/configWatcherService";
|
||||
import { updateWorldStateCollections } from "./services/worldStateService";
|
||||
|
||||
// Patch JSON.stringify to work flawlessly with Bigints.
|
||||
JSON.stringify = JSONStringify;
|
||||
@ -27,7 +33,14 @@ mongoose
|
||||
.connect(config.mongodbUrl)
|
||||
.then(() => {
|
||||
logger.info("Connected to MongoDB");
|
||||
syncConfigWithDatabase();
|
||||
|
||||
startWebServer();
|
||||
|
||||
void updateWorldStateCollections();
|
||||
setInterval(() => {
|
||||
void updateWorldStateCollections();
|
||||
}, 60_000);
|
||||
})
|
||||
.catch(error => {
|
||||
if (error instanceof Error) {
|
||||
|
@ -17,22 +17,25 @@ export interface IMessageDatabase extends IMessage {
|
||||
ownerId: Types.ObjectId;
|
||||
date: Date; //created at
|
||||
attVisualOnly?: boolean;
|
||||
expiry?: Date;
|
||||
_id: Types.ObjectId;
|
||||
}
|
||||
|
||||
export interface IMessage {
|
||||
sndr: string;
|
||||
msg: string;
|
||||
cinematic?: string;
|
||||
sub: string;
|
||||
customData?: string;
|
||||
icon?: string;
|
||||
highPriority?: boolean;
|
||||
lowPrioNewPlayers?: boolean;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
transmission?: string;
|
||||
att?: string[];
|
||||
countedAtt?: ITypeCount[];
|
||||
transmission?: string;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
goalTag?: string;
|
||||
CrossPlatform?: boolean;
|
||||
arg?: Arg[];
|
||||
gifts?: IGift[];
|
||||
r?: boolean;
|
||||
@ -101,13 +104,18 @@ const messageSchema = new Schema<IMessageDatabase>(
|
||||
ownerId: Schema.Types.ObjectId,
|
||||
sndr: String,
|
||||
msg: String,
|
||||
cinematic: String,
|
||||
sub: String,
|
||||
customData: String,
|
||||
icon: String,
|
||||
highPriority: Boolean,
|
||||
lowPrioNewPlayers: Boolean,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
goalTag: String,
|
||||
date: { type: Date, required: true },
|
||||
r: Boolean,
|
||||
CrossPlatform: Boolean,
|
||||
att: { type: [String], default: undefined },
|
||||
gifts: { type: [giftSchema], default: undefined },
|
||||
countedAtt: { type: [typeCountSchema], default: undefined },
|
||||
@ -128,7 +136,7 @@ const messageSchema = new Schema<IMessageDatabase>(
|
||||
declineAction: String,
|
||||
hasAccountAction: Boolean
|
||||
},
|
||||
{ timestamps: { createdAt: "date", updatedAt: false }, id: false }
|
||||
{ id: false }
|
||||
);
|
||||
|
||||
messageSchema.virtual("messageId").get(function (this: IMessageDatabase) {
|
||||
@ -151,13 +159,15 @@ messageSchema.set("toJSON", {
|
||||
|
||||
if (messageDatabase.startDate && messageDatabase.endDate) {
|
||||
messageClient.startDate = toMongoDate(messageDatabase.startDate);
|
||||
|
||||
messageClient.endDate = toMongoDate(messageDatabase.endDate);
|
||||
} else {
|
||||
delete messageClient.startDate;
|
||||
delete messageClient.endDate;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
messageSchema.index({ ownerId: 1 });
|
||||
messageSchema.index({ expiry: 1 }, { expireAfterSeconds: 0 });
|
||||
messageSchema.index({ endDate: 1 }, { expireAfterSeconds: 0 });
|
||||
|
||||
export const Inbox = model<IMessageDatabase>("Inbox", messageSchema, "inbox");
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Document, HydratedDocument, Model, Schema, Types, model } from "mongoose";
|
||||
import { Document, Model, Schema, Types, model } from "mongoose";
|
||||
import {
|
||||
IFlavourItem,
|
||||
IRawUpgrade,
|
||||
@ -7,7 +7,6 @@ import {
|
||||
IBooster,
|
||||
IInventoryClient,
|
||||
ISlots,
|
||||
IMailboxDatabase,
|
||||
IDuviriInfo,
|
||||
IPendingRecipeDatabase,
|
||||
IPendingRecipeClient,
|
||||
@ -54,7 +53,6 @@ import {
|
||||
IUpgradeDatabase,
|
||||
ICrewShipMemberDatabase,
|
||||
ICrewShipMemberClient,
|
||||
IMailboxClient,
|
||||
TEquipmentKey,
|
||||
equipmentKeys,
|
||||
IKubrowPetDetailsDatabase,
|
||||
@ -93,13 +91,17 @@ import {
|
||||
ICrewMemberSkillEfficiency,
|
||||
ICrewMemberDatabase,
|
||||
ICrewMemberClient,
|
||||
ISortieRewardAttenuation,
|
||||
IRewardAttenuation,
|
||||
IInvasionProgressDatabase,
|
||||
IInvasionProgressClient,
|
||||
IAccolades,
|
||||
IHubNpcCustomization,
|
||||
ILotusCustomization,
|
||||
IEndlessXpReward
|
||||
IEndlessXpReward,
|
||||
IPersonalGoalProgressDatabase,
|
||||
IPersonalGoalProgressClient,
|
||||
IKubrowPetPrintClient,
|
||||
IKubrowPetPrintDatabase
|
||||
} from "../../types/inventoryTypes/inventoryTypes";
|
||||
import { IOid } from "../../types/commonTypes";
|
||||
import {
|
||||
@ -251,12 +253,6 @@ const ArchonCrystalUpgradeSchema = new Schema<IArchonCrystalUpgrade>(
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
ArchonCrystalUpgradeSchema.set("toJSON", {
|
||||
transform(_document, returnedObject) {
|
||||
delete returnedObject.__v;
|
||||
}
|
||||
});
|
||||
|
||||
const boosterSchema = new Schema<IBooster>(
|
||||
{
|
||||
ExpiryDate: Number,
|
||||
@ -377,7 +373,7 @@ FlavourItemSchema.set("toJSON", {
|
||||
}
|
||||
});
|
||||
|
||||
const MailboxSchema = new Schema<IMailboxDatabase>(
|
||||
/*const MailboxSchema = new Schema<IMailboxDatabase>(
|
||||
{
|
||||
LastInboxId: Schema.Types.ObjectId
|
||||
},
|
||||
@ -390,7 +386,7 @@ MailboxSchema.set("toJSON", {
|
||||
delete mailboxDatabase.__v;
|
||||
(returnedObject as IMailboxClient).LastInboxId = toOid(mailboxDatabase.LastInboxId);
|
||||
}
|
||||
});
|
||||
});*/
|
||||
|
||||
const DuviriInfoSchema = new Schema<IDuviriInfo>(
|
||||
{
|
||||
@ -463,11 +459,35 @@ const discoveredMarkerSchema = new Schema<IDiscoveredMarker>(
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const personalGoalProgressSchema = new Schema<IPersonalGoalProgressDatabase>(
|
||||
{
|
||||
Best: Number,
|
||||
Count: Number,
|
||||
Tag: String,
|
||||
goalId: Types.ObjectId
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
personalGoalProgressSchema.set("toJSON", {
|
||||
virtuals: true,
|
||||
transform(_doc, obj) {
|
||||
const db = obj as IPersonalGoalProgressDatabase;
|
||||
const client = obj as IPersonalGoalProgressClient;
|
||||
|
||||
client._id = toOid(db.goalId);
|
||||
|
||||
delete obj.goalId;
|
||||
delete obj.__v;
|
||||
}
|
||||
});
|
||||
|
||||
const challengeProgressSchema = new Schema<IChallengeProgress>(
|
||||
{
|
||||
Progress: Number,
|
||||
Name: String,
|
||||
Completed: [String]
|
||||
Completed: { type: [String], default: undefined },
|
||||
ReceivedJunctionReward: Boolean,
|
||||
Name: { type: String, required: true }
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
@ -990,6 +1010,27 @@ const traitsSchema = new Schema<ITraits>(
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const kubrowPetPrintSchema = new Schema<IKubrowPetPrintDatabase>({
|
||||
ItemType: String,
|
||||
Name: String,
|
||||
IsMale: Boolean,
|
||||
Size: Number,
|
||||
DominantTraits: traitsSchema,
|
||||
RecessiveTraits: traitsSchema
|
||||
});
|
||||
kubrowPetPrintSchema.set("toJSON", {
|
||||
virtuals: true,
|
||||
transform(_doc, obj) {
|
||||
const db = obj as IKubrowPetPrintDatabase;
|
||||
const client = obj as IKubrowPetPrintClient;
|
||||
|
||||
client.ItemId = toOid(db._id);
|
||||
|
||||
delete obj._id;
|
||||
delete obj.__v;
|
||||
}
|
||||
});
|
||||
|
||||
const detailsSchema = new Schema<IKubrowPetDetailsDatabase>(
|
||||
{
|
||||
Name: String,
|
||||
@ -1079,6 +1120,11 @@ EquipmentSchema.set("toJSON", {
|
||||
if (db.UmbraDate) {
|
||||
client.UmbraDate = toMongoDate(db.UmbraDate);
|
||||
}
|
||||
|
||||
if (client.ArchonCrystalUpgrades) {
|
||||
// For some reason, mongoose turns empty objects here into nulls, so we have to fix it.
|
||||
client.ArchonCrystalUpgrades = client.ArchonCrystalUpgrades.map(x => (x as unknown) ?? {});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1371,10 +1417,10 @@ lastSortieRewardSchema.set("toJSON", {
|
||||
}
|
||||
});
|
||||
|
||||
const sortieRewardAttenutationSchema = new Schema<ISortieRewardAttenuation>(
|
||||
const rewardAttenutationSchema = new Schema<IRewardAttenuation>(
|
||||
{
|
||||
Tag: String,
|
||||
Atten: Number
|
||||
Tag: { type: String, required: true },
|
||||
Atten: { type: Number, required: true }
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
@ -1488,7 +1534,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
|
||||
|
||||
KubrowPetEggs: [kubrowPetEggSchema],
|
||||
//Prints Cat(3 Prints)\Kubrow(2 Prints) Pets
|
||||
//KubrowPetPrints: [Schema.Types.Mixed],
|
||||
KubrowPetPrints: [kubrowPetPrintSchema],
|
||||
|
||||
//Item for EquippedGear example:Scaner,LoadoutTechSummon etc
|
||||
Consumables: [typeCountSchema],
|
||||
@ -1602,6 +1648,9 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
|
||||
PendingSpectreLoadouts: { type: [spectreLoadoutsSchema], default: undefined },
|
||||
SpectreLoadouts: { type: [spectreLoadoutsSchema], default: undefined },
|
||||
|
||||
//Darvo Deal
|
||||
UsedDailyDeals: [String],
|
||||
|
||||
//New Quest Email
|
||||
EmailItems: [typeCountSchema],
|
||||
|
||||
@ -1617,7 +1666,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
|
||||
CompletedSorties: [String],
|
||||
LastSortieReward: { type: [lastSortieRewardSchema], default: undefined },
|
||||
LastLiteSortieReward: { type: [lastSortieRewardSchema], default: undefined },
|
||||
SortieRewardAttenuation: { type: [sortieRewardAttenutationSchema], default: undefined },
|
||||
SortieRewardAttenuation: { type: [rewardAttenutationSchema], default: undefined },
|
||||
|
||||
// Resource Extractor Drones
|
||||
Drones: [droneSchema],
|
||||
@ -1631,7 +1680,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
|
||||
//CompletedJobs: [Schema.Types.Mixed],
|
||||
|
||||
//Game mission\ivent score example "Tag": "WaterFight", "Best": 170, "Count": 1258,
|
||||
//PersonalGoalProgress: [Schema.Types.Mixed],
|
||||
PersonalGoalProgress: { type: [personalGoalProgressSchema], default: undefined },
|
||||
|
||||
//Setting interface Style
|
||||
ThemeStyle: String,
|
||||
@ -1702,9 +1751,9 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
|
||||
//Unknown and system
|
||||
DuviriInfo: DuviriInfoSchema,
|
||||
LastInventorySync: Schema.Types.ObjectId,
|
||||
Mailbox: MailboxSchema,
|
||||
//Mailbox: MailboxSchema,
|
||||
HandlerPoints: Number,
|
||||
ChallengesFixVersion: { type: Number, default: 6 },
|
||||
ChallengesFixVersion: Number,
|
||||
PlayedParkourTutorial: Boolean,
|
||||
//ActiveLandscapeTraps: [Schema.Types.Mixed],
|
||||
//RepVotes: [Schema.Types.Mixed],
|
||||
@ -1718,7 +1767,6 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
|
||||
//ChallengeInstanceStates: [Schema.Types.Mixed],
|
||||
RecentVendorPurchases: { type: [recentVendorPurchaseSchema], default: undefined },
|
||||
//Robotics: [Schema.Types.Mixed],
|
||||
//UsedDailyDeals: [Schema.Types.Mixed],
|
||||
CollectibleSeries: { type: [collectibleEntrySchema], default: undefined },
|
||||
HasResetAccount: { type: Boolean, default: false },
|
||||
|
||||
@ -1755,7 +1803,11 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
|
||||
BrandedSuits: { type: [Schema.Types.ObjectId], default: undefined },
|
||||
LockedWeaponGroup: { type: lockedWeaponGroupSchema, default: undefined },
|
||||
|
||||
HubNpcCustomizations: { type: [hubNpcCustomizationSchema], default: undefined }
|
||||
HubNpcCustomizations: { type: [hubNpcCustomizationSchema], default: undefined },
|
||||
|
||||
ClaimedJunctionChallengeRewards: { type: [String], default: undefined },
|
||||
|
||||
SpecialItemRewardAttenuation: { type: [rewardAttenutationSchema], default: undefined }
|
||||
},
|
||||
{ timestamps: { createdAt: "Created", updatedAt: false } }
|
||||
);
|
||||
@ -1825,6 +1877,7 @@ export type InventoryDocumentProps = {
|
||||
CrewShipSalvagedWeaponSkins: Types.DocumentArray<IUpgradeDatabase>;
|
||||
PersonalTechProjects: Types.DocumentArray<IPersonalTechProjectDatabase>;
|
||||
CrewMembers: Types.DocumentArray<ICrewMemberDatabase>;
|
||||
KubrowPetPrints: Types.DocumentArray<IKubrowPetPrintDatabase>;
|
||||
} & { [K in TEquipmentKey]: Types.DocumentArray<IEquipmentDatabase> };
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
|
@ -11,13 +11,13 @@ const databaseAccountSchema = new Schema<IDatabaseAccountJson>(
|
||||
email: { type: String, required: true, unique: true },
|
||||
password: { type: String, required: true },
|
||||
DisplayName: { type: String, required: true, unique: true },
|
||||
CountryCode: { type: String, required: true },
|
||||
CountryCode: { type: String, default: "" },
|
||||
ClientType: { type: String },
|
||||
CrossPlatformAllowed: { type: Boolean, required: true },
|
||||
ForceLogoutVersion: { type: Number, required: true },
|
||||
CrossPlatformAllowed: { type: Boolean, default: true },
|
||||
ForceLogoutVersion: { type: Number, default: 0 },
|
||||
AmazonAuthToken: { type: String },
|
||||
AmazonRefreshToken: { type: String },
|
||||
ConsentNeeded: { type: Boolean, required: true },
|
||||
ConsentNeeded: { type: Boolean, default: false },
|
||||
TrackedSettings: { type: [String], default: [] },
|
||||
Nonce: { type: Number, default: 0 },
|
||||
BuildLabel: String,
|
||||
@ -25,7 +25,8 @@ const databaseAccountSchema = new Schema<IDatabaseAccountJson>(
|
||||
LastLogin: { type: Date, default: 0 },
|
||||
LatestEventMessageDate: { type: Date, default: 0 },
|
||||
LastLoginRewardDate: { type: Number, default: 0 },
|
||||
LoginDays: { type: Number, default: 1 }
|
||||
LoginDays: { type: Number, default: 1 },
|
||||
DailyFirstWinDate: { type: Number, default: 0 }
|
||||
},
|
||||
opts
|
||||
);
|
||||
|
@ -40,6 +40,7 @@ const placedDecosSchema = new Schema<IPlacedDecosDatabase>(
|
||||
Pos: [Number],
|
||||
Rot: [Number],
|
||||
Scale: Number,
|
||||
Sockets: Number,
|
||||
PictureFrameInfo: { type: pictureFrameInfoSchema, default: undefined }
|
||||
},
|
||||
{ id: false }
|
||||
|
30
src/models/worldStateModel.ts
Normal file
30
src/models/worldStateModel.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { IDailyDealDatabase, IFissureDatabase } from "@/src/types/worldStateTypes";
|
||||
import { model, Schema } from "mongoose";
|
||||
|
||||
const fissureSchema = new Schema<IFissureDatabase>({
|
||||
Activation: Date,
|
||||
Expiry: Date,
|
||||
Node: String, // must be unique
|
||||
Modifier: String,
|
||||
Hard: Boolean
|
||||
});
|
||||
|
||||
fissureSchema.index({ Expiry: 1 }, { expireAfterSeconds: 0 }); // With this, MongoDB will automatically delete expired entries.
|
||||
|
||||
export const Fissure = model<IFissureDatabase>("Fissure", fissureSchema);
|
||||
|
||||
const dailyDealSchema = new Schema<IDailyDealDatabase>({
|
||||
StoreItem: { type: String, required: true },
|
||||
Activation: { type: Date, required: true },
|
||||
Expiry: { type: Date, required: true },
|
||||
Discount: { type: Number, required: true },
|
||||
OriginalPrice: { type: Number, required: true },
|
||||
SalePrice: { type: Number, required: true },
|
||||
AmountTotal: { type: Number, required: true },
|
||||
AmountSold: { type: Number, required: true }
|
||||
});
|
||||
|
||||
dailyDealSchema.index({ StoreItem: 1 }, { unique: true });
|
||||
dailyDealSchema.index({ Expiry: 1 }, { expireAfterSeconds: 86400 });
|
||||
|
||||
export const DailyDeal = model<IDailyDealDatabase>("DailyDeal", dailyDealSchema);
|
@ -19,6 +19,7 @@ import { changeDojoRootController } from "@/src/controllers/api/changeDojoRootCo
|
||||
import { changeGuildRankController } from "@/src/controllers/api/changeGuildRankController";
|
||||
import { checkDailyMissionBonusController } from "@/src/controllers/api/checkDailyMissionBonusController";
|
||||
import { claimCompletedRecipeController } from "@/src/controllers/api/claimCompletedRecipeController";
|
||||
import { claimJunctionChallengeRewardController } from "@/src/controllers/api/claimJunctionChallengeRewardController";
|
||||
import { claimLibraryDailyTaskRewardController } from "@/src/controllers/api/claimLibraryDailyTaskRewardController";
|
||||
import { clearDialogueHistoryController } from "@/src/controllers/api/clearDialogueHistoryController";
|
||||
import { clearNewEpisodeRewardController } from "@/src/controllers/api/clearNewEpisodeRewardController";
|
||||
@ -33,6 +34,7 @@ import { createAllianceController } from "@/src/controllers/api/createAllianceCo
|
||||
import { createGuildController } from "@/src/controllers/api/createGuildController";
|
||||
import { creditsController } from "@/src/controllers/api/creditsController";
|
||||
import { crewMembersController } from "@/src/controllers/api/crewMembersController";
|
||||
import { crewShipFusionController } from "@/src/controllers/api/crewShipFusionController";
|
||||
import { crewShipIdentifySalvageController } from "@/src/controllers/api/crewShipIdentifySalvageController";
|
||||
import { customizeGuildRanksController } from "@/src/controllers/api/customizeGuildRanksController";
|
||||
import { customObstacleCourseLeaderboardController } from "@/src/controllers/api/customObstacleCourseLeaderboardController";
|
||||
@ -132,6 +134,7 @@ import { setPlacedDecoInfoController } from "@/src/controllers/api/setPlacedDeco
|
||||
import { setShipCustomizationsController } from "@/src/controllers/api/setShipCustomizationsController";
|
||||
import { setShipFavouriteLoadoutController } from "@/src/controllers/api/setShipFavouriteLoadoutController";
|
||||
import { setShipVignetteController } from "@/src/controllers/api/setShipVignetteController";
|
||||
import { setSuitInfectionController } from "@/src/controllers/api/setSuitInfectionController";
|
||||
import { setSupportedSyndicateController } from "@/src/controllers/api/setSupportedSyndicateController";
|
||||
import { setWeaponSkillTreeController } from "@/src/controllers/api/setWeaponSkillTreeController";
|
||||
import { shipDecorationsController } from "@/src/controllers/api/shipDecorationsController";
|
||||
@ -147,6 +150,7 @@ import { syndicateStandingBonusController } from "@/src/controllers/api/syndicat
|
||||
import { tauntHistoryController } from "@/src/controllers/api/tauntHistoryController";
|
||||
import { tradingController } from "@/src/controllers/api/tradingController";
|
||||
import { trainingResultController } from "@/src/controllers/api/trainingResultController";
|
||||
import { umbraController } from "@/src/controllers/api/umbraController";
|
||||
import { unlockShipFeatureController } from "@/src/controllers/api/unlockShipFeatureController";
|
||||
import { updateAlignmentController } from "@/src/controllers/api/updateAlignmentController";
|
||||
import { updateChallengeProgressController } from "@/src/controllers/api/updateChallengeProgressController";
|
||||
@ -234,6 +238,7 @@ apiRouter.post("/artifacts.php", artifactsController);
|
||||
apiRouter.post("/artifactTransmutation.php", artifactTransmutationController);
|
||||
apiRouter.post("/changeDojoRoot.php", changeDojoRootController);
|
||||
apiRouter.post("/claimCompletedRecipe.php", claimCompletedRecipeController);
|
||||
apiRouter.post("/claimJunctionChallengeReward.php", claimJunctionChallengeRewardController);
|
||||
apiRouter.post("/clearDialogueHistory.php", clearDialogueHistoryController);
|
||||
apiRouter.post("/clearNewEpisodeReward.php", clearNewEpisodeRewardController);
|
||||
apiRouter.post("/commitStoryModeDecision.php", (_req, res) => { res.end(); }); // U14 (maybe wanna actually unlock the ship features?)
|
||||
@ -245,6 +250,7 @@ apiRouter.post("/contributeToVault.php", contributeToVaultController);
|
||||
apiRouter.post("/createAlliance.php", createAllianceController);
|
||||
apiRouter.post("/createGuild.php", createGuildController);
|
||||
apiRouter.post("/crewMembers.php", crewMembersController);
|
||||
apiRouter.post("/crewShipFusion.php", crewShipFusionController);
|
||||
apiRouter.post("/crewShipIdentifySalvage.php", crewShipIdentifySalvageController);
|
||||
apiRouter.post("/customizeGuildRanks.php", customizeGuildRanksController);
|
||||
apiRouter.post("/customObstacleCourseLeaderboard.php", customObstacleCourseLeaderboardController);
|
||||
@ -280,6 +286,7 @@ apiRouter.post("/inventorySlots.php", inventorySlotsController);
|
||||
apiRouter.post("/joinSession.php", joinSessionController);
|
||||
apiRouter.post("/login.php", loginController);
|
||||
apiRouter.post("/loginRewardsSelection.php", loginRewardsSelectionController);
|
||||
apiRouter.post("/logout.php", logoutController); // from ~U16, don't know when they changed it to GET
|
||||
apiRouter.post("/maturePet.php", maturePetController);
|
||||
apiRouter.post("/missionInventoryUpdate.php", missionInventoryUpdateController);
|
||||
apiRouter.post("/modularWeaponCrafting.php", modularWeaponCraftingController);
|
||||
@ -317,6 +324,7 @@ apiRouter.post("/setPlacedDecoInfo.php", setPlacedDecoInfoController);
|
||||
apiRouter.post("/setShipCustomizations.php", setShipCustomizationsController);
|
||||
apiRouter.post("/setShipFavouriteLoadout.php", setShipFavouriteLoadoutController);
|
||||
apiRouter.post("/setShipVignette.php", setShipVignetteController);
|
||||
apiRouter.post("/setSuitInfection.php", setSuitInfectionController);
|
||||
apiRouter.post("/setWeaponSkillTree.php", setWeaponSkillTreeController);
|
||||
apiRouter.post("/shipDecorations.php", shipDecorationsController);
|
||||
apiRouter.post("/startCollectibleEntry.php", startCollectibleEntryController);
|
||||
@ -327,6 +335,7 @@ apiRouter.post("/syndicateSacrifice.php", syndicateSacrificeController);
|
||||
apiRouter.post("/syndicateStandingBonus.php", syndicateStandingBonusController);
|
||||
apiRouter.post("/tauntHistory.php", tauntHistoryController);
|
||||
apiRouter.post("/trainingResult.php", trainingResultController);
|
||||
apiRouter.post("/umbra.php", umbraController);
|
||||
apiRouter.post("/unlockShipFeature.php", unlockShipFeatureController);
|
||||
apiRouter.post("/updateAlignment.php", updateAlignmentController);
|
||||
apiRouter.post("/updateChallengeProgress.php", updateChallengeProgressController);
|
||||
|
@ -11,6 +11,9 @@ import { renameAccountController } from "@/src/controllers/custom/renameAccountC
|
||||
import { ircDroppedController } from "@/src/controllers/custom/ircDroppedController";
|
||||
import { unlockAllIntrinsicsController } from "@/src/controllers/custom/unlockAllIntrinsicsController";
|
||||
import { addMissingMaxRankModsController } from "@/src/controllers/custom/addMissingMaxRankModsController";
|
||||
import { webuiFileChangeDetectedController } from "@/src/controllers/custom/webuiFileChangeDetectedController";
|
||||
import { completeAllMissionsController } from "@/src/controllers/custom/completeAllMissionsController";
|
||||
import { addMissingHelminthBlueprintsController } from "@/src/controllers/custom/addMissingHelminthBlueprintsController";
|
||||
|
||||
import { createAccountController } from "@/src/controllers/custom/createAccountController";
|
||||
import { createMessageController } from "@/src/controllers/custom/createMessageController";
|
||||
@ -20,10 +23,10 @@ import { addXpController } from "@/src/controllers/custom/addXpController";
|
||||
import { importController } from "@/src/controllers/custom/importController";
|
||||
import { manageQuestsController } from "@/src/controllers/custom/manageQuestsController";
|
||||
import { setEvolutionProgressController } from "@/src/controllers/custom/setEvolutionProgressController";
|
||||
import { setBoosterController } from "@/src/controllers/custom/setBoosterController";
|
||||
import { updateFingerprintController } from "@/src/controllers/custom/updateFingerprintController";
|
||||
|
||||
import { getConfigDataController } from "@/src/controllers/custom/getConfigDataController";
|
||||
import { updateConfigDataController } from "@/src/controllers/custom/updateConfigDataController";
|
||||
import { setBoosterController } from "../controllers/custom/setBoosterController";
|
||||
import { getConfigController, setConfigController } from "@/src/controllers/custom/configController";
|
||||
|
||||
const customRouter = express.Router();
|
||||
|
||||
@ -38,6 +41,9 @@ customRouter.get("/renameAccount", renameAccountController);
|
||||
customRouter.get("/ircDropped", ircDroppedController);
|
||||
customRouter.get("/unlockAllIntrinsics", unlockAllIntrinsicsController);
|
||||
customRouter.get("/addMissingMaxRankMods", addMissingMaxRankModsController);
|
||||
customRouter.get("/webuiFileChangeDetected", webuiFileChangeDetectedController);
|
||||
customRouter.get("/completeAllMissions", completeAllMissionsController);
|
||||
customRouter.get("/addMissingHelminthBlueprints", addMissingHelminthBlueprintsController);
|
||||
|
||||
customRouter.post("/createAccount", createAccountController);
|
||||
customRouter.post("/createMessage", createMessageController);
|
||||
@ -48,8 +54,9 @@ customRouter.post("/import", importController);
|
||||
customRouter.post("/manageQuests", manageQuestsController);
|
||||
customRouter.post("/setEvolutionProgress", setEvolutionProgressController);
|
||||
customRouter.post("/setBooster", setBoosterController);
|
||||
customRouter.post("/updateFingerprint", updateFingerprintController);
|
||||
|
||||
customRouter.get("/config", getConfigDataController);
|
||||
customRouter.post("/config", updateConfigDataController);
|
||||
customRouter.post("/getConfig", getConfigController);
|
||||
customRouter.post("/setConfig", setConfigController);
|
||||
|
||||
export { customRouter };
|
||||
|
@ -1,6 +1,9 @@
|
||||
import express from "express";
|
||||
import path from "path";
|
||||
import { repoDir, rootDir } from "@/src/helpers/pathHelper";
|
||||
import { args } from "@/src/helpers/commandLineArguments";
|
||||
|
||||
const baseDir = args.dev ? repoDir : rootDir;
|
||||
|
||||
const webuiRouter = express.Router();
|
||||
|
||||
@ -19,29 +22,29 @@ webuiRouter.use("/webui", (req, res, next) => {
|
||||
|
||||
// Serve virtual routes
|
||||
webuiRouter.get("/webui/inventory", (_req, res) => {
|
||||
res.sendFile(path.join(rootDir, "static/webui/index.html"));
|
||||
res.sendFile(path.join(baseDir, "static/webui/index.html"));
|
||||
});
|
||||
webuiRouter.get(/webui\/powersuit\/(.+)/, (_req, res) => {
|
||||
res.sendFile(path.join(rootDir, "static/webui/index.html"));
|
||||
webuiRouter.get("/webui/detailedView", (_req, res) => {
|
||||
res.sendFile(path.join(baseDir, "static/webui/index.html"));
|
||||
});
|
||||
webuiRouter.get("/webui/mods", (_req, res) => {
|
||||
res.sendFile(path.join(rootDir, "static/webui/index.html"));
|
||||
res.sendFile(path.join(baseDir, "static/webui/index.html"));
|
||||
});
|
||||
webuiRouter.get("/webui/settings", (_req, res) => {
|
||||
res.sendFile(path.join(rootDir, "static/webui/index.html"));
|
||||
res.sendFile(path.join(baseDir, "static/webui/index.html"));
|
||||
});
|
||||
webuiRouter.get("/webui/quests", (_req, res) => {
|
||||
res.sendFile(path.join(rootDir, "static/webui/index.html"));
|
||||
res.sendFile(path.join(baseDir, "static/webui/index.html"));
|
||||
});
|
||||
webuiRouter.get("/webui/cheats", (_req, res) => {
|
||||
res.sendFile(path.join(rootDir, "static/webui/index.html"));
|
||||
res.sendFile(path.join(baseDir, "static/webui/index.html"));
|
||||
});
|
||||
webuiRouter.get("/webui/import", (_req, res) => {
|
||||
res.sendFile(path.join(rootDir, "static/webui/index.html"));
|
||||
res.sendFile(path.join(baseDir, "static/webui/index.html"));
|
||||
});
|
||||
|
||||
// Serve static files
|
||||
webuiRouter.use("/webui", express.static(path.join(rootDir, "static/webui")));
|
||||
webuiRouter.use("/webui", express.static(path.join(baseDir, "static/webui")));
|
||||
|
||||
// Serve favicon
|
||||
webuiRouter.get("/favicon.ico", (_req, res) => {
|
||||
@ -58,7 +61,7 @@ webuiRouter.get("/webui/riven-tool/RivenParser.js", (_req, res) => {
|
||||
|
||||
// Serve translations
|
||||
webuiRouter.get("/translations/:file", (req, res) => {
|
||||
res.sendFile(path.join(rootDir, `static/webui/translations/${req.params.file}`));
|
||||
res.sendFile(path.join(baseDir, `static/webui/translations/${req.params.file}`));
|
||||
});
|
||||
|
||||
export { webuiRouter };
|
||||
|
@ -1,8 +1,9 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { repoDir } from "@/src/helpers/pathHelper";
|
||||
import { args } from "@/src/helpers/commandLineArguments";
|
||||
|
||||
interface IConfig {
|
||||
export interface IConfig {
|
||||
mongodbUrl: string;
|
||||
logger: {
|
||||
files: boolean;
|
||||
@ -18,13 +19,16 @@ interface IConfig {
|
||||
skipTutorial?: boolean;
|
||||
skipAllDialogue?: boolean;
|
||||
unlockAllScans?: boolean;
|
||||
unlockAllMissions?: boolean;
|
||||
infiniteCredits?: boolean;
|
||||
infinitePlatinum?: boolean;
|
||||
infiniteEndo?: boolean;
|
||||
infiniteRegalAya?: boolean;
|
||||
infiniteHelminthMaterials?: boolean;
|
||||
claimingBlueprintRefundsIngredients?: boolean;
|
||||
dontSubtractPurchaseCreditCost?: boolean;
|
||||
dontSubtractPurchasePlatinumCost?: boolean;
|
||||
dontSubtractPurchaseItemCost?: boolean;
|
||||
dontSubtractPurchaseStandingCost?: boolean;
|
||||
dontSubtractVoidTraces?: boolean;
|
||||
dontSubtractConsumables?: boolean;
|
||||
unlockAllShipFeatures?: boolean;
|
||||
@ -44,7 +48,11 @@ interface IConfig {
|
||||
noVendorPurchaseLimits?: boolean;
|
||||
noDeathMarks?: boolean;
|
||||
noKimCooldowns?: boolean;
|
||||
fullyStockedVendors?: boolean;
|
||||
baroAlwaysAvailable?: boolean;
|
||||
baroFullyStocked?: boolean;
|
||||
syndicateMissionsRepeatable?: boolean;
|
||||
unlockAllProfitTakerStages?: boolean;
|
||||
instantFinishRivenChallenge?: boolean;
|
||||
instantResourceExtractorDrones?: boolean;
|
||||
noResourceExtractorDronesDamage?: boolean;
|
||||
@ -55,20 +63,36 @@ interface IConfig {
|
||||
noDojoResearchCosts?: boolean;
|
||||
noDojoResearchTime?: boolean;
|
||||
fastClanAscension?: boolean;
|
||||
missionsCanGiveAllRelics?: boolean;
|
||||
unlockAllSimarisResearchEntries?: boolean;
|
||||
disableDailyTribute?: boolean;
|
||||
spoofMasteryRank?: number;
|
||||
relicRewardItemCountMultiplier?: number;
|
||||
nightwaveStandingMultiplier?: number;
|
||||
unfaithfulBugFixes?: {
|
||||
ignore1999LastRegionPlayed?: boolean;
|
||||
fixXtraCheeseTimer?: boolean;
|
||||
};
|
||||
worldState?: {
|
||||
creditBoost?: boolean;
|
||||
affinityBoost?: boolean;
|
||||
resourceBoost?: boolean;
|
||||
starDays?: boolean;
|
||||
galleonOfGhouls?: number;
|
||||
eidolonOverride?: string;
|
||||
vallisOverride?: string;
|
||||
duviriOverride?: string;
|
||||
nightwaveOverride?: string;
|
||||
allTheFissures?: string;
|
||||
circuitGameModes?: string[];
|
||||
darvoStockMultiplier?: number;
|
||||
};
|
||||
dev?: {
|
||||
keepVendorsExpired?: boolean;
|
||||
};
|
||||
nightwaveStandingMultiplier?: number;
|
||||
}
|
||||
|
||||
export const configPath = path.join(repoDir, "config.json");
|
||||
export const configPath = path.join(repoDir, args.configPath ?? "config.json");
|
||||
|
||||
export const config: IConfig = {
|
||||
mongodbUrl: "mongodb://127.0.0.1:27017/openWF",
|
||||
@ -80,11 +104,13 @@ export const config: IConfig = {
|
||||
};
|
||||
|
||||
export const loadConfig = (): void => {
|
||||
const newConfig = JSON.parse(fs.readFileSync(configPath, "utf-8")) as IConfig;
|
||||
|
||||
// Set all values to undefined now so if the new config.json omits some fields that were previously present, it's correct in-memory.
|
||||
for (const key of Object.keys(config)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
||||
(config as any)[key] = undefined;
|
||||
}
|
||||
|
||||
Object.assign(config, JSON.parse(fs.readFileSync(configPath, "utf-8")));
|
||||
Object.assign(config, newConfig);
|
||||
};
|
||||
|
@ -1,27 +1,35 @@
|
||||
import fs from "fs";
|
||||
import chokidar from "chokidar";
|
||||
import fsPromises from "fs/promises";
|
||||
import { logger } from "../utils/logger";
|
||||
import { config, configPath, loadConfig } from "./configService";
|
||||
import { getWebPorts, startWebServer, stopWebServer } from "./webService";
|
||||
import { getWebPorts, sendWsBroadcast, startWebServer, stopWebServer } from "./webService";
|
||||
import { Inbox } from "../models/inboxModel";
|
||||
|
||||
let amnesia = false;
|
||||
fs.watchFile(configPath, () => {
|
||||
chokidar.watch(configPath).on("change", () => {
|
||||
if (amnesia) {
|
||||
amnesia = false;
|
||||
} else {
|
||||
logger.info("Detected a change to config.json, reloading its contents.");
|
||||
logger.info("Detected a change to config file, reloading its contents.");
|
||||
try {
|
||||
loadConfig();
|
||||
} catch (e) {
|
||||
logger.error("Failed to reload config.json. Did you delete it?! Execution cannot continue.");
|
||||
process.exit(1);
|
||||
logger.error("Config changes were not applied: " + (e as Error).message);
|
||||
return;
|
||||
}
|
||||
validateConfig();
|
||||
syncConfigWithDatabase();
|
||||
|
||||
const webPorts = getWebPorts();
|
||||
if (config.httpPort != webPorts.http || config.httpsPort != webPorts.https) {
|
||||
logger.info(`Restarting web server to apply port changes.`);
|
||||
|
||||
// Tell webui clients to reload with new port
|
||||
sendWsBroadcast({ ports: { http: config.httpPort, https: config.httpsPort } });
|
||||
|
||||
void stopWebServer().then(startWebServer);
|
||||
} else {
|
||||
sendWsBroadcast({ config_reloaded: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -40,19 +48,29 @@ export const validateConfig = (): void => {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
config.worldState?.galleonOfGhouls &&
|
||||
config.worldState.galleonOfGhouls != 1 &&
|
||||
config.worldState.galleonOfGhouls != 2 &&
|
||||
config.worldState.galleonOfGhouls != 3
|
||||
) {
|
||||
config.worldState.galleonOfGhouls = 0;
|
||||
modified = true;
|
||||
}
|
||||
if (modified) {
|
||||
logger.info(`Updating config.json to fix some issues with it.`);
|
||||
logger.info(`Updating config file to fix some issues with it.`);
|
||||
void saveConfig();
|
||||
}
|
||||
};
|
||||
|
||||
export const updateConfig = async (data: string): Promise<void> => {
|
||||
amnesia = true;
|
||||
await fsPromises.writeFile(configPath, data);
|
||||
Object.assign(config, JSON.parse(data));
|
||||
};
|
||||
|
||||
export const saveConfig = async (): Promise<void> => {
|
||||
amnesia = true;
|
||||
await fsPromises.writeFile(configPath, JSON.stringify(config, null, 2));
|
||||
};
|
||||
|
||||
export const syncConfigWithDatabase = (): void => {
|
||||
// Event messages are deleted after endDate. Since we don't use beginDate/endDate and instead have config toggles, we need to delete the messages once those bools are false.
|
||||
if (!config.worldState?.galleonOfGhouls) {
|
||||
void Inbox.deleteMany({ goalTag: "GalleonRobbery" }).then(() => {}); // For some reason, I can't just do `Inbox.deleteMany(...)`; it needs this whole circus.
|
||||
}
|
||||
};
|
||||
|
@ -349,7 +349,8 @@ export const removeDojoDeco = (
|
||||
component.DecoCapacity! += meta.capacityCost;
|
||||
}
|
||||
} else {
|
||||
const itemType = Object.entries(ExportResources).find(arr => arr[1].deco == deco.Type)![0];
|
||||
const [itemType, meta] = Object.entries(ExportResources).find(arr => arr[1].deco == deco.Type)!;
|
||||
component.DecoCapacity! += meta.dojoCapacityCost!;
|
||||
if (deco.Sockets !== undefined) {
|
||||
addVaultFusionTreasures(guild, [
|
||||
{
|
||||
@ -549,6 +550,19 @@ export const processFundedGuildTechProject = (
|
||||
guild.XP += recipe.guildXpValue;
|
||||
}
|
||||
setGuildTechLogState(guild, techProject.ItemType, config.noDojoResearchTime ? 4 : 3, techProject.CompletionDate);
|
||||
if (config.noDojoResearchTime) {
|
||||
processCompletedGuildTechProject(guild, techProject.ItemType);
|
||||
}
|
||||
};
|
||||
|
||||
export const processCompletedGuildTechProject = (guild: TGuildDatabaseDocument, type: string): void => {
|
||||
if (type.startsWith("/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/")) {
|
||||
guild.VaultDecoRecipes ??= [];
|
||||
guild.VaultDecoRecipes.push({
|
||||
ItemType: type,
|
||||
ItemCount: 1
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const setGuildTechLogState = (
|
||||
|
@ -296,6 +296,12 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
|
||||
db[key] = client[key];
|
||||
}
|
||||
}
|
||||
// IRewardAtten[]
|
||||
for (const key of ["SortieRewardAttenuation", "SpecialItemRewardAttenuation"] as const) {
|
||||
if (client[key] !== undefined) {
|
||||
db[key] = client[key];
|
||||
}
|
||||
}
|
||||
if (client.XPInfo !== undefined) {
|
||||
db.XPInfo = client.XPInfo;
|
||||
}
|
||||
|
@ -2,8 +2,8 @@ import { IMessageDatabase, Inbox } from "@/src/models/inboxModel";
|
||||
import { getAccountForRequest } from "@/src/services/loginService";
|
||||
import { HydratedDocument, Types } from "mongoose";
|
||||
import { Request } from "express";
|
||||
import eventMessages from "@/static/fixed_responses/eventMessages.json";
|
||||
import { logger } from "@/src/utils/logger";
|
||||
import { unixTimesInMs } from "../constants/timeConstants";
|
||||
import { config } from "./configService";
|
||||
|
||||
export const getAllMessagesSorted = async (accountId: string): Promise<HydratedDocument<IMessageDatabase>[]> => {
|
||||
const inbox = await Inbox.find({ ownerId: accountId }).sort({ date: -1 });
|
||||
@ -29,37 +29,72 @@ export const deleteAllMessagesRead = async (accountId: string): Promise<void> =>
|
||||
|
||||
export const createNewEventMessages = async (req: Request): Promise<void> => {
|
||||
const account = await getAccountForRequest(req);
|
||||
const latestEventMessageDate = account.LatestEventMessageDate;
|
||||
const newEventMessages: IMessageCreationTemplate[] = [];
|
||||
|
||||
//TODO: is baroo there? create these kind of messages too (periodical messages)
|
||||
const newEventMessages = eventMessages.Messages.filter(m => new Date(m.eventMessageDate) > latestEventMessageDate);
|
||||
// Baro
|
||||
const baroIndex = Math.trunc((Date.now() - 910800000) / (unixTimesInMs.day * 14));
|
||||
const baroStart = baroIndex * (unixTimesInMs.day * 14) + 910800000;
|
||||
const baroActualStart = baroStart + unixTimesInMs.day * (config.baroAlwaysAvailable ? 0 : 12);
|
||||
if (account.LatestEventMessageDate.getTime() < baroActualStart) {
|
||||
newEventMessages.push({
|
||||
sndr: "/Lotus/Language/G1Quests/VoidTraderName",
|
||||
sub: "/Lotus/Language/CommunityMessages/VoidTraderAppearanceTitle",
|
||||
msg: "/Lotus/Language/CommunityMessages/VoidTraderAppearanceMessage",
|
||||
icon: "/Lotus/Interface/Icons/Npcs/BaroKiTeerPortrait.png",
|
||||
startDate: new Date(baroActualStart),
|
||||
endDate: new Date(baroStart + unixTimesInMs.day * 14),
|
||||
CrossPlatform: true,
|
||||
arg: [
|
||||
{
|
||||
Key: "NODE_NAME",
|
||||
Tag: ["EarthHUB", "MercuryHUB", "SaturnHUB", "PlutoHUB"][baroIndex % 4]
|
||||
}
|
||||
],
|
||||
date: new Date(baroActualStart)
|
||||
});
|
||||
}
|
||||
|
||||
// BUG: Deleting the inbox message manually means it'll just be automatically re-created. This is because we don't use startDate/endDate for these config-toggled events.
|
||||
if (config.worldState?.galleonOfGhouls) {
|
||||
if (!(await Inbox.exists({ ownerId: account._id, goalTag: "GalleonRobbery" }))) {
|
||||
newEventMessages.push({
|
||||
sndr: "/Lotus/Language/Bosses/BossCouncilorVayHek",
|
||||
sub: "/Lotus/Language/Events/GalleonRobberyIntroMsgTitle",
|
||||
msg: "/Lotus/Language/Events/GalleonRobberyIntroMsgDesc",
|
||||
icon: "/Lotus/Interface/Icons/Npcs/VayHekPortrait.png",
|
||||
transmission: "/Lotus/Sounds/Dialog/GalleonOfGhouls/DGhoulsWeekOneInbox0010VayHek",
|
||||
att: ["/Lotus/Upgrades/Skins/Events/OgrisOldSchool"],
|
||||
startDate: new Date(),
|
||||
goalTag: "GalleonRobbery"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (newEventMessages.length === 0) {
|
||||
logger.debug(`No new event messages. Latest event message date: ${latestEventMessageDate.toISOString()}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const savedEventMessages = await createMessage(account._id, newEventMessages);
|
||||
logger.debug("created event messages", savedEventMessages);
|
||||
await createMessage(account._id, newEventMessages);
|
||||
|
||||
const latestEventMessage = newEventMessages.reduce((prev, current) =>
|
||||
prev.eventMessageDate > current.eventMessageDate ? prev : current
|
||||
prev.startDate! > current.startDate! ? prev : current
|
||||
);
|
||||
|
||||
account.LatestEventMessageDate = new Date(latestEventMessage.eventMessageDate);
|
||||
account.LatestEventMessageDate = new Date(latestEventMessage.startDate!);
|
||||
await account.save();
|
||||
};
|
||||
|
||||
export const createMessage = async (accountId: string | Types.ObjectId, messages: IMessageCreationTemplate[]) => {
|
||||
export const createMessage = async (
|
||||
accountId: string | Types.ObjectId,
|
||||
messages: IMessageCreationTemplate[]
|
||||
): Promise<void> => {
|
||||
const ownerIdMessages = messages.map(m => ({
|
||||
...m,
|
||||
date: m.date ?? new Date(),
|
||||
ownerId: accountId
|
||||
}));
|
||||
|
||||
const savedMessages = await Inbox.insertMany(ownerIdMessages);
|
||||
return savedMessages;
|
||||
await Inbox.insertMany(ownerIdMessages);
|
||||
};
|
||||
|
||||
export interface IMessageCreationTemplate extends Omit<IMessageDatabase, "_id" | "date" | "ownerId"> {
|
||||
ownerId?: string;
|
||||
date?: Date;
|
||||
}
|
||||
|
@ -29,7 +29,8 @@ import {
|
||||
ICalendarProgress,
|
||||
INemesisWeaponTargetFingerprint,
|
||||
INemesisPetTargetFingerprint,
|
||||
IDialogueDatabase
|
||||
IDialogueDatabase,
|
||||
IKubrowPetPrintClient
|
||||
} from "@/src/types/inventoryTypes/inventoryTypes";
|
||||
import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate";
|
||||
import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes";
|
||||
@ -43,6 +44,7 @@ import {
|
||||
} from "../types/inventoryTypes/commonInventoryTypes";
|
||||
import {
|
||||
ExportArcanes,
|
||||
ExportBoosters,
|
||||
ExportBundles,
|
||||
ExportChallenges,
|
||||
ExportCustoms,
|
||||
@ -81,12 +83,14 @@ import { addQuestKey, completeQuest } from "@/src/services/questService";
|
||||
import { handleBundleAcqusition } from "./purchaseService";
|
||||
import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json";
|
||||
import { getRandomElement, getRandomInt, getRandomWeightedReward, SRng } from "./rngService";
|
||||
import { createMessage } from "./inboxService";
|
||||
import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
|
||||
import { createMessage, IMessageCreationTemplate } from "./inboxService";
|
||||
import { getMaxStanding, getMinStanding } from "@/src/helpers/syndicateStandingHelper";
|
||||
import { getNightwaveSyndicateTag, getWorldState } from "./worldStateService";
|
||||
import { ICalendarSeason } from "@/src/types/worldStateTypes";
|
||||
import { generateNemesisProfile, INemesisProfile } from "../helpers/nemesisHelpers";
|
||||
import { TAccountDocument } from "./loginService";
|
||||
import { unixTimesInMs } from "../constants/timeConstants";
|
||||
import { addString } from "../helpers/stringHelpers";
|
||||
|
||||
export const createInventory = async (
|
||||
accountOwnerId: Types.ObjectId,
|
||||
@ -422,7 +426,6 @@ export const addItem = async (
|
||||
ItemType: "/Lotus/Types/Game/KubrowPet/Eggs/KubrowEgg",
|
||||
_id: new Types.ObjectId()
|
||||
};
|
||||
inventory.KubrowPetEggs ??= [];
|
||||
inventory.KubrowPetEggs.push(egg);
|
||||
changes.push({
|
||||
ItemType: egg.ItemType,
|
||||
@ -497,6 +500,7 @@ export const addItem = async (
|
||||
// - Blueprints for Ancient Protector Specter, Shield Osprey Specter, etc. have num=1 despite giving their purchaseQuantity.
|
||||
if (!exactQuantity) {
|
||||
quantity *= ExportGear[typeName].purchaseQuantity ?? 1;
|
||||
logger.debug(`non-exact acquisition of ${typeName}; factored quantity is ${quantity}`);
|
||||
}
|
||||
const consumablesChanges = [
|
||||
{
|
||||
@ -668,6 +672,17 @@ export const addItem = async (
|
||||
return await addEmailItem(inventory, typeName);
|
||||
}
|
||||
|
||||
// Boosters are an odd case. They're only added like this via Baro's Void Surplus afaik.
|
||||
{
|
||||
const boosterEntry = Object.entries(ExportBoosters).find(arr => arr[1].typeName == typeName);
|
||||
if (boosterEntry) {
|
||||
addBooster(typeName, quantity, inventory);
|
||||
return {
|
||||
Boosters: [{ ItemType: typeName, ExpiryDate: quantity }]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Path-based duck typing
|
||||
switch (typeName.substr(1).split("/")[1]) {
|
||||
case "Powersuits":
|
||||
@ -781,7 +796,11 @@ export const addItem = async (
|
||||
typeName.substr(1).split("/")[3] == "CatbrowPet" ||
|
||||
typeName.substr(1).split("/")[3] == "KubrowPet"
|
||||
) {
|
||||
if (typeName != "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem") {
|
||||
if (
|
||||
typeName != "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem" &&
|
||||
typeName != "/Lotus/Types/Game/KubrowPet/BlankTraitPrint" &&
|
||||
typeName != "/Lotus/Types/Game/KubrowPet/ImprintedTraitPrint"
|
||||
) {
|
||||
return addKubrowPet(inventory, typeName, undefined, premiumPurchase);
|
||||
}
|
||||
} else if (typeName.startsWith("/Lotus/Types/Game/CrewShip/CrewMember/")) {
|
||||
@ -1045,8 +1064,13 @@ export const addKubrowPet = (
|
||||
const configs: IItemConfig[] = applyDefaultUpgrades(inventory, kubrowPet?.defaultUpgrades);
|
||||
|
||||
if (!details) {
|
||||
let traits: ITraits;
|
||||
const isCatbrow = [
|
||||
"/Lotus/Types/Game/CatbrowPet/CheshireCatbrowPetPowerSuit",
|
||||
"/Lotus/Types/Game/CatbrowPet/MirrorCatbrowPetPowerSuit",
|
||||
"/Lotus/Types/Game/CatbrowPet/VampireCatbrowPetPowerSuit"
|
||||
].includes(kubrowPetName);
|
||||
|
||||
let traits: ITraits;
|
||||
if (kubrowPetName == "/Lotus/Types/Game/CatbrowPet/VampireCatbrowPetPowerSuit") {
|
||||
traits = {
|
||||
BaseColor: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseVampire",
|
||||
@ -1061,12 +1085,7 @@ export const addKubrowPet = (
|
||||
Tail: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailVampire"
|
||||
};
|
||||
} else {
|
||||
const isCatbrow = [
|
||||
"/Lotus/Types/Game/CatbrowPet/MirrorCatbrowPetPowerSuit",
|
||||
"/Lotus/Types/Game/CatbrowPet/CheshireCatbrowPetPowerSuit"
|
||||
].includes(kubrowPetName);
|
||||
const traitsPool = isCatbrow ? catbrowDetails : kubrowDetails;
|
||||
|
||||
traits = {
|
||||
BaseColor: getRandomWeightedReward(traitsPool.Colors, kubrowWeights)!.type,
|
||||
SecondaryColor: getRandomWeightedReward(traitsPool.Colors, kubrowWeights)!.type,
|
||||
@ -1085,7 +1104,7 @@ export const addKubrowPet = (
|
||||
Name: "",
|
||||
IsPuppy: !premiumPurchase,
|
||||
HasCollar: true,
|
||||
PrintsRemaining: 3,
|
||||
PrintsRemaining: isCatbrow ? 3 : 2,
|
||||
Status: premiumPurchase ? Status.StatusStasis : Status.StatusIncubating,
|
||||
HatchDate: premiumPurchase ? new Date() : new Date(Date.now() + 10 * unixTimesInMs.hour), // On live, this seems to be somewhat randomised so that the pet hatches 9~11 hours after start.
|
||||
IsMale: !!getRandomInt(0, 1),
|
||||
@ -1109,6 +1128,26 @@ export const addKubrowPet = (
|
||||
return inventoryChanges;
|
||||
};
|
||||
|
||||
export const addKubrowPetPrint = (
|
||||
inventory: TInventoryDatabaseDocument,
|
||||
pet: IEquipmentDatabase,
|
||||
inventoryChanges: IInventoryChanges
|
||||
): void => {
|
||||
inventoryChanges.KubrowPetPrints ??= [];
|
||||
inventoryChanges.KubrowPetPrints.push(
|
||||
inventory.KubrowPetPrints[
|
||||
inventory.KubrowPetPrints.push({
|
||||
ItemType: "/Lotus/Types/Game/KubrowPet/ImprintedTraitPrint",
|
||||
Name: pet.Details!.Name,
|
||||
IsMale: pet.Details!.IsMale,
|
||||
Size: pet.Details!.Size,
|
||||
DominantTraits: pet.Details!.DominantTraits,
|
||||
RecessiveTraits: pet.Details!.RecessiveTraits
|
||||
}) - 1
|
||||
].toJSON<IKubrowPetPrintClient>()
|
||||
);
|
||||
};
|
||||
|
||||
export const updateSlots = (
|
||||
inventory: TInventoryDatabaseDocument,
|
||||
slotName: SlotNames,
|
||||
@ -1202,8 +1241,10 @@ export const addStanding = (
|
||||
inventory: TInventoryDatabaseDocument,
|
||||
syndicateTag: string,
|
||||
gainedStanding: number,
|
||||
isMedallion: boolean = false
|
||||
): IAffiliationMods => {
|
||||
affiliationMods: IAffiliationMods[] = [],
|
||||
isMedallion: boolean = false,
|
||||
propagateAlignments: boolean = true
|
||||
): void => {
|
||||
let syndicate = inventory.Affiliations.find(x => x.Tag == syndicateTag);
|
||||
const syndicateMeta = ExportSyndicates[syndicateTag];
|
||||
|
||||
@ -1215,6 +1256,10 @@ export const addStanding = (
|
||||
const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0);
|
||||
if (syndicate.Standing + gainedStanding > max) gainedStanding = max - syndicate.Standing;
|
||||
|
||||
if (syndicate.Title == -2 && syndicate.Standing + gainedStanding < -71000) {
|
||||
gainedStanding = -71000 + syndicate.Standing;
|
||||
}
|
||||
|
||||
if (!isMedallion || syndicateMeta.medallionsCappedByDailyLimit) {
|
||||
if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) {
|
||||
gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin);
|
||||
@ -1223,10 +1268,27 @@ export const addStanding = (
|
||||
}
|
||||
|
||||
syndicate.Standing += gainedStanding;
|
||||
return {
|
||||
const affiliationMod: IAffiliationMods = {
|
||||
Tag: syndicateTag,
|
||||
Standing: gainedStanding
|
||||
};
|
||||
affiliationMods.push(affiliationMod);
|
||||
|
||||
if (syndicateMeta.alignments) {
|
||||
if (propagateAlignments) {
|
||||
for (const [tag, factor] of Object.entries(syndicateMeta.alignments)) {
|
||||
addStanding(inventory, tag, gainedStanding * factor, affiliationMods, isMedallion, false);
|
||||
}
|
||||
} else {
|
||||
while (syndicate.Standing < getMinStanding(syndicateMeta, syndicate.Title ?? 0)) {
|
||||
syndicate.Title ??= 0;
|
||||
syndicate.Title -= 1;
|
||||
affiliationMod.Title ??= 0;
|
||||
affiliationMod.Title -= 1;
|
||||
logger.debug(`${syndicateTag} is decreasing to title ${syndicate.Title} after applying alignment`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: AffiliationMods support (Nightwave).
|
||||
@ -1304,7 +1366,7 @@ export const addCustomization = (
|
||||
customizationName: string,
|
||||
inventoryChanges: IInventoryChanges = {}
|
||||
): IInventoryChanges => {
|
||||
if (!inventory.FlavourItems.find(x => x.ItemType == customizationName)) {
|
||||
if (!inventory.FlavourItems.some(x => x.ItemType == customizationName)) {
|
||||
const flavourItemIndex = inventory.FlavourItems.push({ ItemType: customizationName }) - 1;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
inventoryChanges.FlavourItems ??= [];
|
||||
@ -1320,7 +1382,7 @@ export const addSkin = (
|
||||
typeName: string,
|
||||
inventoryChanges: IInventoryChanges = {}
|
||||
): IInventoryChanges => {
|
||||
if (inventory.WeaponSkins.find(x => x.ItemType == typeName)) {
|
||||
if (inventory.WeaponSkins.some(x => x.ItemType == typeName)) {
|
||||
logger.debug(`refusing to add WeaponSkin ${typeName} because account already owns it`);
|
||||
} else {
|
||||
const index = inventory.WeaponSkins.push({ ItemType: typeName, IsNew: true }) - 1;
|
||||
@ -1501,7 +1563,22 @@ export const addEmailItem = async (
|
||||
const meta = ExportEmailItems[typeName];
|
||||
const emailItem = inventory.EmailItems.find(x => x.ItemType == typeName);
|
||||
if (!emailItem || !meta.sendOnlyOnce) {
|
||||
await createMessage(inventory.accountOwnerId, [convertInboxMessage(meta.message)]);
|
||||
const msg: IMessageCreationTemplate = convertInboxMessage(meta.message);
|
||||
if (msg.cinematic == "/Lotus/Levels/1999/PlayerHomeBalconyCinematics.level") {
|
||||
msg.customData = JSON.stringify({
|
||||
Tag: msg.customData + "KissCin",
|
||||
CinLoadout: {
|
||||
Skins: inventory.AdultOperatorLoadOuts[0].Skins,
|
||||
Upgrades: inventory.AdultOperatorLoadOuts[0].Upgrades,
|
||||
attcol: inventory.AdultOperatorLoadOuts[0].attcol,
|
||||
cloth: inventory.AdultOperatorLoadOuts[0].cloth,
|
||||
eyecol: inventory.AdultOperatorLoadOuts[0].eyecol,
|
||||
pricol: inventory.AdultOperatorLoadOuts[0].pricol,
|
||||
syancol: inventory.AdultOperatorLoadOuts[0].syancol
|
||||
}
|
||||
});
|
||||
}
|
||||
await createMessage(inventory.accountOwnerId, [msg]);
|
||||
|
||||
if (emailItem) {
|
||||
emailItem.ItemCount += 1;
|
||||
@ -1557,7 +1634,7 @@ export const addMiscItem = (
|
||||
inventory: TInventoryDatabaseDocument,
|
||||
type: string,
|
||||
count: number,
|
||||
inventoryChanges: IInventoryChanges
|
||||
inventoryChanges: IInventoryChanges = {}
|
||||
): void => {
|
||||
const miscItemChanges: IMiscItem[] = [
|
||||
{
|
||||
@ -1708,12 +1785,27 @@ export const addFocusXpIncreases = (inventory: TInventoryDatabaseDocument, focus
|
||||
AP_ANY
|
||||
}
|
||||
|
||||
inventory.FocusXP ??= { AP_ATTACK: 0, AP_DEFENSE: 0, AP_TACTIC: 0, AP_POWER: 0, AP_WARD: 0 };
|
||||
inventory.FocusXP ??= {};
|
||||
if (focusXpPlus[FocusType.AP_ATTACK]) {
|
||||
inventory.FocusXP.AP_ATTACK ??= 0;
|
||||
inventory.FocusXP.AP_ATTACK += focusXpPlus[FocusType.AP_ATTACK];
|
||||
}
|
||||
if (focusXpPlus[FocusType.AP_DEFENSE]) {
|
||||
inventory.FocusXP.AP_DEFENSE ??= 0;
|
||||
inventory.FocusXP.AP_DEFENSE += focusXpPlus[FocusType.AP_DEFENSE];
|
||||
}
|
||||
if (focusXpPlus[FocusType.AP_TACTIC]) {
|
||||
inventory.FocusXP.AP_TACTIC ??= 0;
|
||||
inventory.FocusXP.AP_TACTIC += focusXpPlus[FocusType.AP_TACTIC];
|
||||
}
|
||||
if (focusXpPlus[FocusType.AP_POWER]) {
|
||||
inventory.FocusXP.AP_POWER ??= 0;
|
||||
inventory.FocusXP.AP_POWER += focusXpPlus[FocusType.AP_POWER];
|
||||
}
|
||||
if (focusXpPlus[FocusType.AP_WARD]) {
|
||||
inventory.FocusXP.AP_WARD ??= 0;
|
||||
inventory.FocusXP.AP_WARD += focusXpPlus[FocusType.AP_WARD];
|
||||
}
|
||||
|
||||
if (!config.noDailyFocusLimit) {
|
||||
inventory.DailyFocus -= focusXpPlus.reduce((a, b) => a + b, 0);
|
||||
@ -1745,6 +1837,10 @@ export const addChallenges = (
|
||||
} else {
|
||||
inventory.ChallengeProgress.push({ Name, Progress });
|
||||
}
|
||||
|
||||
if (Name.startsWith("Calendar")) {
|
||||
addString(getCalendarProgress(inventory).SeasonProgress.ActivatedChallenges, Name);
|
||||
}
|
||||
});
|
||||
|
||||
const affiliationMods: IAffiliationMods[] = [];
|
||||
@ -1787,12 +1883,24 @@ export const addChallenges = (
|
||||
return affiliationMods;
|
||||
};
|
||||
|
||||
export const addMissionComplete = (inventory: TInventoryDatabaseDocument, { Tag, Completes }: IMission): void => {
|
||||
export const addCalendarProgress = (inventory: TInventoryDatabaseDocument, value: { challenge: string }[]): void => {
|
||||
const calendarProgress = getCalendarProgress(inventory);
|
||||
const currentSeason = getWorldState().KnownCalendarSeasons[0];
|
||||
calendarProgress.SeasonProgress.LastCompletedChallengeDayIdx = currentSeason.Days.findIndex(
|
||||
day => day.events.length != 0 && day.events[0].challenge == value[value.length - 1].challenge
|
||||
);
|
||||
checkCalendarChallengeCompletion(calendarProgress, currentSeason);
|
||||
};
|
||||
|
||||
export const addMissionComplete = (inventory: TInventoryDatabaseDocument, { Tag, Completes, Tier }: IMission): void => {
|
||||
const { Missions } = inventory;
|
||||
const itemIndex = Missions.findIndex(item => item.Tag === Tag);
|
||||
|
||||
if (itemIndex !== -1) {
|
||||
Missions[itemIndex].Completes += Completes;
|
||||
if (Tier) {
|
||||
Missions[itemIndex].Tier = Tier;
|
||||
}
|
||||
} else {
|
||||
Missions.push({ Tag, Completes });
|
||||
}
|
||||
@ -1988,6 +2096,20 @@ export const getCalendarProgress = (inventory: TInventoryDatabaseDocument): ICal
|
||||
return inventory.CalendarProgress;
|
||||
};
|
||||
|
||||
export const checkCalendarChallengeCompletion = (
|
||||
calendarProgress: ICalendarProgress,
|
||||
currentSeason: ICalendarSeason
|
||||
): void => {
|
||||
const dayIndex = calendarProgress.SeasonProgress.LastCompletedDayIdx + 1;
|
||||
if (calendarProgress.SeasonProgress.LastCompletedChallengeDayIdx >= dayIndex) {
|
||||
const day = currentSeason.Days[dayIndex];
|
||||
if (day.events.length != 0 && day.events[0].type == "CET_CHALLENGE") {
|
||||
//logger.debug(`already completed the challenge, skipping ahead`);
|
||||
calendarProgress.SeasonProgress.LastCompletedDayIdx++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const giveNemesisWeaponRecipe = (
|
||||
inventory: TInventoryDatabaseDocument,
|
||||
weaponType: string,
|
||||
|
@ -45,6 +45,39 @@ export type WeaponTypeInternal =
|
||||
| "SpecialItems";
|
||||
|
||||
export const getRecipe = (uniqueName: string): IRecipe | undefined => {
|
||||
// Handle crafting of archwing summon for versions prior to 39.0.0 as this blueprint was removed then.
|
||||
if (uniqueName == "/Lotus/Types/Recipes/EidolonRecipes/OpenArchwingSummonBlueprint") {
|
||||
return {
|
||||
resultType: "/Lotus/Types/Restoratives/OpenArchwingSummon",
|
||||
buildPrice: 7500,
|
||||
buildTime: 1800,
|
||||
skipBuildTimePrice: 10,
|
||||
consumeOnUse: false,
|
||||
num: 1,
|
||||
codexSecret: false,
|
||||
alwaysAvailable: true,
|
||||
ingredients: [
|
||||
{
|
||||
ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/IraditeItem",
|
||||
ItemCount: 50
|
||||
},
|
||||
{
|
||||
ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/GrokdrulItem",
|
||||
ItemCount: 50
|
||||
},
|
||||
{
|
||||
ItemType: "/Lotus/Types/Items/Fish/Eidolon/FishParts/EidolonFishOilItem",
|
||||
ItemCount: 30
|
||||
},
|
||||
{
|
||||
ItemType: "/Lotus/Types/Items/MiscItems/Circuits",
|
||||
ItemCount: 600
|
||||
}
|
||||
],
|
||||
excludeFromMarket: true
|
||||
};
|
||||
}
|
||||
|
||||
return ExportRecipes[uniqueName];
|
||||
};
|
||||
|
||||
@ -218,7 +251,9 @@ export const convertInboxMessage = (message: IInboxMessage): IMessage => {
|
||||
return {
|
||||
sndr: message.sender,
|
||||
msg: message.body,
|
||||
cinematic: message.cinematic,
|
||||
sub: message.title,
|
||||
customData: message.customData,
|
||||
att: message.attachments.length > 0 ? message.attachments : undefined,
|
||||
countedAtt: message.countedAttachments.length > 0 ? message.countedAttachments : undefined,
|
||||
icon: message.icon ?? "",
|
||||
|
@ -144,7 +144,8 @@ export const claimLoginReward = async (
|
||||
case "RT_STORE_ITEM":
|
||||
case "RT_RECIPE":
|
||||
case "RT_RANDOM_RECIPE":
|
||||
return (await handleStoreItemAcquisition(reward.StoreItemType, inventory, reward.Amount)).InventoryChanges;
|
||||
return (await handleStoreItemAcquisition(reward.StoreItemType, inventory, reward.Amount, undefined, true))
|
||||
.InventoryChanges;
|
||||
|
||||
case "RT_CREDITS":
|
||||
return updateCurrency(inventory, -reward.Amount, false);
|
||||
|
@ -18,6 +18,23 @@ export const isNameTaken = async (name: string): Promise<boolean> => {
|
||||
return !!(await Account.findOne({ DisplayName: name }));
|
||||
};
|
||||
|
||||
export const createNonce = (): number => {
|
||||
return Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
|
||||
};
|
||||
|
||||
export const getUsernameFromEmail = async (email: string): Promise<string> => {
|
||||
const nameFromEmail = email.substring(0, email.indexOf("@"));
|
||||
let name = nameFromEmail || email.substring(1) || "SpaceNinja";
|
||||
if (await isNameTaken(name)) {
|
||||
let suffix = 0;
|
||||
do {
|
||||
++suffix;
|
||||
name = nameFromEmail + suffix;
|
||||
} while (await isNameTaken(name));
|
||||
}
|
||||
return nameFromEmail;
|
||||
};
|
||||
|
||||
export const createAccount = async (accountData: IDatabaseAccountRequiredFields): Promise<IDatabaseAccountJson> => {
|
||||
const account = new Account(accountData);
|
||||
try {
|
||||
|
@ -2,6 +2,7 @@ import {
|
||||
ExportEnemies,
|
||||
ExportFusionBundles,
|
||||
ExportRegions,
|
||||
ExportRelics,
|
||||
ExportRewards,
|
||||
IMissionReward as IMissionRewardExternal,
|
||||
IRegion,
|
||||
@ -13,6 +14,7 @@ import { IRngResult, SRng, getRandomElement, getRandomReward } from "@/src/servi
|
||||
import { equipmentKeys, IMission, ITypeCount, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
|
||||
import {
|
||||
addBooster,
|
||||
addCalendarProgress,
|
||||
addChallenges,
|
||||
addConsumables,
|
||||
addCrewShipAmmo,
|
||||
@ -34,7 +36,6 @@ import {
|
||||
applyClientEquipmentUpdates,
|
||||
combineInventoryChanges,
|
||||
generateRewardSeed,
|
||||
getCalendarProgress,
|
||||
getDialogue,
|
||||
giveNemesisPetRecipe,
|
||||
giveNemesisWeaponRecipe,
|
||||
@ -44,7 +45,7 @@ import {
|
||||
import { updateQuestKey } from "@/src/services/questService";
|
||||
import { Types } from "mongoose";
|
||||
import { IAffiliationMods, IInventoryChanges } from "@/src/types/purchaseTypes";
|
||||
import { fromStoreItem, getLevelKeyRewards, toStoreItem } from "@/src/services/itemDataService";
|
||||
import { fromStoreItem, getLevelKeyRewards, isStoreItem, toStoreItem } from "@/src/services/itemDataService";
|
||||
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
|
||||
import { getEntriesUnsafe } from "@/src/utils/ts-utils";
|
||||
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
|
||||
@ -86,7 +87,7 @@ const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[]
|
||||
if (rewardInfo.VaultsCracked) {
|
||||
const rotations: number[] = [];
|
||||
for (let i = 0; i != rewardInfo.VaultsCracked; ++i) {
|
||||
rotations.push(i);
|
||||
rotations.push(Math.min(i, 2));
|
||||
}
|
||||
return rotations;
|
||||
}
|
||||
@ -266,7 +267,9 @@ export const addMissionInventoryUpdates = async (
|
||||
addMissionComplete(inventory, value);
|
||||
break;
|
||||
case "LastRegionPlayed":
|
||||
if (!(config.unfaithfulBugFixes?.ignore1999LastRegionPlayed && value === "1999MapName")) {
|
||||
inventory.LastRegionPlayed = value;
|
||||
}
|
||||
break;
|
||||
case "RawUpgrades":
|
||||
addMods(inventory, value);
|
||||
@ -478,7 +481,7 @@ export const addMissionInventoryUpdates = async (
|
||||
msg: "/Lotus/Language/G1Quests/DeathMarkMessage",
|
||||
icon: "/Lotus/Interface/Icons/Npcs/Stalker_d.png",
|
||||
highPriority: true,
|
||||
expiry: new Date(Date.now() + 86400_000) // TOVERIFY: This type of inbox message seems to automatically delete itself. We'll just delete it after 24 hours, but it's clear if this is correct.
|
||||
endDate: new Date(Date.now() + 86400_000) // TOVERIFY: This type of inbox message seems to automatically delete itself. We'll just delete it after 24 hours, but it's not clear if this is correct.
|
||||
}
|
||||
]);
|
||||
}
|
||||
@ -522,7 +525,6 @@ export const addMissionInventoryUpdates = async (
|
||||
}
|
||||
case "KubrowPetEggs": {
|
||||
for (const egg of value) {
|
||||
inventory.KubrowPetEggs ??= [];
|
||||
inventory.KubrowPetEggs.push({
|
||||
ItemType: egg.ItemType,
|
||||
_id: new Types.ObjectId()
|
||||
@ -605,6 +607,47 @@ export const addMissionInventoryUpdates = async (
|
||||
inventoryChanges.RegularCredits -= value;
|
||||
break;
|
||||
}
|
||||
case "GoalProgress": {
|
||||
for (const uploadProgress of value) {
|
||||
const goal = getWorldState().Goals.find(x => x._id.$oid == uploadProgress._id.$oid);
|
||||
if (goal && goal.Personal) {
|
||||
inventory.PersonalGoalProgress ??= [];
|
||||
const goalProgress = inventory.PersonalGoalProgress.find(x => x.goalId.equals(goal._id.$oid));
|
||||
if (goalProgress) {
|
||||
goalProgress.Best = Math.max(goalProgress.Best, uploadProgress.Best);
|
||||
goalProgress.Count += uploadProgress.Count;
|
||||
} else {
|
||||
inventory.PersonalGoalProgress.push({
|
||||
Best: uploadProgress.Best,
|
||||
Count: uploadProgress.Count,
|
||||
Tag: goal.Tag,
|
||||
goalId: new Types.ObjectId(goal._id.$oid)
|
||||
});
|
||||
|
||||
if (
|
||||
goal.Reward &&
|
||||
goal.Reward.items &&
|
||||
goal.MissionKeyName &&
|
||||
goal.MissionKeyName in goalMessagesByKey
|
||||
) {
|
||||
// Send reward via inbox
|
||||
const info = goalMessagesByKey[goal.MissionKeyName];
|
||||
await createMessage(inventory.accountOwnerId, [
|
||||
{
|
||||
sndr: info.sndr,
|
||||
msg: info.msg,
|
||||
att: goal.Reward.items.map(x => (isStoreItem(x) ? fromStoreItem(x) : x)),
|
||||
sub: info.sub,
|
||||
icon: info.icon,
|
||||
highPriority: true
|
||||
}
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "InvasionProgress": {
|
||||
for (const clientProgress of value) {
|
||||
const dbProgress = inventory.QualifyingInvasions.find(x =>
|
||||
@ -626,12 +669,7 @@ export const addMissionInventoryUpdates = async (
|
||||
break;
|
||||
}
|
||||
case "CalendarProgress": {
|
||||
const calendarProgress = getCalendarProgress(inventory);
|
||||
for (const progress of value) {
|
||||
const challengeName = progress.challenge.substring(progress.challenge.lastIndexOf("/") + 1);
|
||||
calendarProgress.SeasonProgress.LastCompletedChallengeDayIdx++;
|
||||
calendarProgress.SeasonProgress.ActivatedChallenges.push(challengeName);
|
||||
}
|
||||
addCalendarProgress(inventory, value);
|
||||
break;
|
||||
}
|
||||
case "duviriCaveOffers": {
|
||||
@ -923,6 +961,7 @@ const droptableAliases: Record<string, string> = {
|
||||
|
||||
//TODO: return type of partial missioninventoryupdate response
|
||||
export const addMissionRewards = async (
|
||||
account: TAccountDocument,
|
||||
inventory: TInventoryDatabaseDocument,
|
||||
{
|
||||
wagerTier: wagerTier,
|
||||
@ -958,17 +997,29 @@ export const addMissionRewards = async (
|
||||
|
||||
let missionCompletionCredits = 0;
|
||||
//inventory change is what the client has not rewarded itself, also the client needs to know the credit changes for display
|
||||
|
||||
if (rewardInfo.goalId) {
|
||||
const goal = getWorldState().Goals.find(x => x._id.$oid == rewardInfo.goalId);
|
||||
if (goal?.MissionKeyName) {
|
||||
levelKeyName = goal.MissionKeyName;
|
||||
}
|
||||
}
|
||||
|
||||
if (levelKeyName) {
|
||||
const fixedLevelRewards = getLevelKeyRewards(levelKeyName);
|
||||
//logger.debug(`fixedLevelRewards ${fixedLevelRewards}`);
|
||||
if (fixedLevelRewards.levelKeyRewards) {
|
||||
addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, inventory, MissionRewards, rewardInfo);
|
||||
missionCompletionCredits += addFixedLevelRewards(
|
||||
fixedLevelRewards.levelKeyRewards,
|
||||
MissionRewards,
|
||||
rewardInfo
|
||||
);
|
||||
}
|
||||
if (fixedLevelRewards.levelKeyRewards2) {
|
||||
for (const reward of fixedLevelRewards.levelKeyRewards2) {
|
||||
//quest stage completion credit rewards
|
||||
if (reward.rewardType == "RT_CREDITS") {
|
||||
missionCompletionCredits += reward.amount; // will be added to inventory in addCredits
|
||||
missionCompletionCredits += reward.amount;
|
||||
continue;
|
||||
}
|
||||
MissionRewards.push({
|
||||
@ -997,12 +1048,11 @@ export const addMissionRewards = async (
|
||||
) {
|
||||
const levelCreditReward = getLevelCreditRewards(node);
|
||||
missionCompletionCredits += levelCreditReward;
|
||||
inventory.RegularCredits += levelCreditReward;
|
||||
logger.debug(`levelCreditReward ${levelCreditReward}`);
|
||||
}
|
||||
|
||||
if (node.missionReward) {
|
||||
missionCompletionCredits += addFixedLevelRewards(node.missionReward, inventory, MissionRewards, rewardInfo);
|
||||
missionCompletionCredits += addFixedLevelRewards(node.missionReward, MissionRewards, rewardInfo);
|
||||
}
|
||||
|
||||
if (rewardInfo.sortieTag == "Mission1") {
|
||||
@ -1112,7 +1162,9 @@ export const addMissionRewards = async (
|
||||
combineInventoryChanges(inventoryChanges, inventoryChange.InventoryChanges);
|
||||
}
|
||||
|
||||
const credits = addCredits(inventory, {
|
||||
inventory.RegularCredits += missionCompletionCredits;
|
||||
|
||||
const credits = await addCredits(account, inventory, {
|
||||
missionCompletionCredits,
|
||||
missionDropCredits: creditDrops ?? 0,
|
||||
rngRewardCredits: inventoryChanges.RegularCredits ?? 0
|
||||
@ -1203,7 +1255,7 @@ export const addMissionRewards = async (
|
||||
|
||||
if (rewardInfo.JobStage != undefined && rewardInfo.jobId) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [jobType, unkIndex, hubNode, syndicateMissionId, locationTag] = rewardInfo.jobId.split("_");
|
||||
const [jobType, unkIndex, hubNode, syndicateMissionId] = rewardInfo.jobId.split("_");
|
||||
const syndicateMissions: ISyndicateMissionInfo[] = [];
|
||||
if (syndicateMissionId) {
|
||||
pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId));
|
||||
@ -1212,10 +1264,27 @@ export const addMissionRewards = async (
|
||||
if (syndicateEntry && syndicateEntry.Jobs) {
|
||||
let currentJob = syndicateEntry.Jobs[rewardInfo.JobTier!];
|
||||
if (syndicateEntry.Tag === "EntratiSyndicate") {
|
||||
const vault = syndicateEntry.Jobs.find(j => j.locationTag === locationTag);
|
||||
if (vault) currentJob = vault;
|
||||
let medallionAmount = Math.floor(currentJob.xpAmounts[rewardInfo.JobStage] / (rewardInfo.Q ? 0.8 : 1));
|
||||
|
||||
if (
|
||||
[
|
||||
"DeimosRuinsExterminateBounty",
|
||||
"DeimosRuinsEscortBounty",
|
||||
"DeimosRuinsMistBounty",
|
||||
"DeimosRuinsPurifyBounty",
|
||||
"DeimosRuinsSacBounty",
|
||||
"VaultBounty"
|
||||
].some(ending => jobType.endsWith(ending))
|
||||
) {
|
||||
const vault = syndicateEntry.Jobs.find(j => j.locationTag == rewardInfo.jobId!.split("_").at(-1));
|
||||
if (vault) {
|
||||
currentJob = vault;
|
||||
if (jobType.endsWith("VaultBounty")) {
|
||||
currentJob.xpAmounts = [currentJob.xpAmounts.reduce((partialSum, a) => partialSum + a, 0)];
|
||||
}
|
||||
}
|
||||
}
|
||||
let medallionAmount = Math.floor(
|
||||
Math.min(rewardInfo.JobStage, currentJob.xpAmounts.length - 1) / (rewardInfo.Q ? 0.8 : 1)
|
||||
);
|
||||
if (
|
||||
["DeimosEndlessAreaDefenseBounty", "DeimosEndlessExcavateBounty", "DeimosEndlessPurifyBounty"].some(
|
||||
ending => jobType.endsWith(ending)
|
||||
@ -1236,19 +1305,18 @@ export const addMissionRewards = async (
|
||||
SyndicateXPItemReward = medallionAmount;
|
||||
} else {
|
||||
if (rewardInfo.JobTier! >= 0) {
|
||||
AffiliationMods.push(
|
||||
addStanding(
|
||||
inventory,
|
||||
syndicateEntry.Tag,
|
||||
Math.floor(currentJob.xpAmounts[rewardInfo.JobStage] / (rewardInfo.Q ? 0.8 : 1))
|
||||
)
|
||||
Math.floor(currentJob.xpAmounts[rewardInfo.JobStage] / (rewardInfo.Q ? 0.8 : 1)),
|
||||
AffiliationMods
|
||||
);
|
||||
} else {
|
||||
if (jobType.endsWith("Heists/HeistProfitTakerBountyOne") && rewardInfo.JobStage === 2) {
|
||||
AffiliationMods.push(addStanding(inventory, syndicateEntry.Tag, 1000));
|
||||
addStanding(inventory, syndicateEntry.Tag, 1000, AffiliationMods);
|
||||
}
|
||||
if (jobType.endsWith("Hunts/AllTeralystsHunt") && rewardInfo.JobStage === 2) {
|
||||
AffiliationMods.push(addStanding(inventory, syndicateEntry.Tag, 5000));
|
||||
addStanding(inventory, syndicateEntry.Tag, 5000, AffiliationMods);
|
||||
}
|
||||
if (
|
||||
[
|
||||
@ -1259,7 +1327,7 @@ export const addMissionRewards = async (
|
||||
"Heists/HeistExploiterBountyOne"
|
||||
].some(ending => jobType.endsWith(ending))
|
||||
) {
|
||||
AffiliationMods.push(addStanding(inventory, syndicateEntry.Tag, 1000));
|
||||
addStanding(inventory, syndicateEntry.Tag, 1000, AffiliationMods);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1267,9 +1335,9 @@ export const addMissionRewards = async (
|
||||
}
|
||||
|
||||
if (rewardInfo.challengeMissionId) {
|
||||
const [syndicateTag, tierStr, chemistryStr] = rewardInfo.challengeMissionId.split("_");
|
||||
const [syndicateTag, tierStr, chemistryBuddyStr] = rewardInfo.challengeMissionId.split("_");
|
||||
const tier = Number(tierStr);
|
||||
const chemistry = Number(chemistryStr);
|
||||
const chemistryBuddy = Number(chemistryBuddyStr);
|
||||
const isSteelPath = missions?.Tier;
|
||||
if (syndicateTag === "ZarimanSyndicate") {
|
||||
let medallionAmount = tier + 1;
|
||||
@ -1284,24 +1352,21 @@ export const addMissionRewards = async (
|
||||
let standingAmount = (tier + 1) * 1000;
|
||||
if (tier > 5) standingAmount = 7500; // InfestedLichBounty
|
||||
if (isSteelPath) standingAmount *= 1.5;
|
||||
AffiliationMods.push(addStanding(inventory, syndicateTag, standingAmount));
|
||||
}
|
||||
if (syndicateTag == "HexSyndicate" && chemistry && tier < 6) {
|
||||
const seed = getWorldState().SyndicateMissions.find(x => x.Tag == "HexSyndicate")!.Seed;
|
||||
const { nodes, buddies } = getHexBounties(seed);
|
||||
const buddy = buddies[tier];
|
||||
logger.debug(`Hex seed is ${seed}, giving chemistry for ${buddy}`);
|
||||
if (missions?.Tag != nodes[tier]) {
|
||||
logger.warn(
|
||||
`Uh-oh, tier ${tier} bounty should've been on ${nodes[tier]} but you were just on ${missions?.Tag}`
|
||||
);
|
||||
addStanding(inventory, syndicateTag, standingAmount, AffiliationMods);
|
||||
}
|
||||
if (syndicateTag == "HexSyndicate" && tier < 6) {
|
||||
const buddy = chemistryBuddies[chemistryBuddy];
|
||||
const dialogue = getDialogue(inventory, buddy);
|
||||
if (Date.now() >= dialogue.BountyChemExpiry.getTime()) {
|
||||
logger.debug(`Giving 20 chemistry for ${buddy}`);
|
||||
const tomorrowAt0Utc = config.noKimCooldowns
|
||||
? Date.now()
|
||||
: (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000;
|
||||
const dialogue = getDialogue(inventory, buddy);
|
||||
dialogue.Chemistry += chemistry;
|
||||
dialogue.Chemistry += 20;
|
||||
dialogue.BountyChemExpiry = new Date(tomorrowAt0Utc);
|
||||
} else {
|
||||
logger.debug(`Already got today's chemistry for ${buddy}`);
|
||||
}
|
||||
}
|
||||
if (isSteelPath) {
|
||||
await addItem(inventory, "/Lotus/Types/Items/MiscItems/SteelEssence", 1);
|
||||
@ -1322,48 +1387,61 @@ export const addMissionRewards = async (
|
||||
};
|
||||
};
|
||||
|
||||
//creditBonus is not entirely accurate.
|
||||
//TODO: consider ActiveBoosters
|
||||
export const addCredits = (
|
||||
export const addCredits = async (
|
||||
account: TAccountDocument,
|
||||
inventory: TInventoryDatabaseDocument,
|
||||
{
|
||||
missionDropCredits,
|
||||
missionCompletionCredits,
|
||||
rngRewardCredits
|
||||
}: { missionDropCredits: number; missionCompletionCredits: number; rngRewardCredits: number }
|
||||
): IMissionCredits => {
|
||||
const hasDailyCreditBonus = true;
|
||||
const totalCredits = missionDropCredits + missionCompletionCredits + rngRewardCredits;
|
||||
|
||||
): Promise<IMissionCredits> => {
|
||||
const finalCredits: IMissionCredits = {
|
||||
MissionCredits: [missionDropCredits, missionDropCredits],
|
||||
CreditBonus: [missionCompletionCredits, missionCompletionCredits],
|
||||
TotalCredits: [totalCredits, totalCredits]
|
||||
CreditsBonus: [missionCompletionCredits, missionCompletionCredits],
|
||||
TotalCredits: [0, 0]
|
||||
};
|
||||
|
||||
if (hasDailyCreditBonus) {
|
||||
const today = Math.trunc(Date.now() / 86400000) * 86400;
|
||||
if (account.DailyFirstWinDate != today) {
|
||||
account.DailyFirstWinDate = today;
|
||||
await account.save();
|
||||
|
||||
logger.debug(`daily first win, doubling missionCompletionCredits (${missionCompletionCredits})`);
|
||||
|
||||
finalCredits.DailyMissionBonus = true;
|
||||
inventory.RegularCredits += missionCompletionCredits;
|
||||
finalCredits.CreditBonus[1] *= 2;
|
||||
finalCredits.MissionCredits[1] *= 2;
|
||||
finalCredits.TotalCredits[1] *= 2;
|
||||
finalCredits.CreditsBonus[1] *= 2;
|
||||
}
|
||||
|
||||
if (!hasDailyCreditBonus) {
|
||||
return finalCredits;
|
||||
const totalCredits = finalCredits.MissionCredits[1] + finalCredits.CreditsBonus[1] + rngRewardCredits;
|
||||
finalCredits.TotalCredits = [totalCredits, totalCredits];
|
||||
|
||||
if (config.worldState?.creditBoost) {
|
||||
inventory.RegularCredits += finalCredits.TotalCredits[1];
|
||||
finalCredits.TotalCredits[1] += finalCredits.TotalCredits[1];
|
||||
}
|
||||
return { ...finalCredits, DailyMissionBonus: true };
|
||||
const now = Math.trunc(Date.now() / 1000); // TOVERIFY: Should we maybe subtract mission time as to apply credit boosters that expired during mission?
|
||||
if ((inventory.Boosters.find(x => x.ItemType == "/Lotus/Types/Boosters/CreditBooster")?.ExpiryDate ?? 0) > now) {
|
||||
inventory.RegularCredits += finalCredits.TotalCredits[1];
|
||||
finalCredits.TotalCredits[1] += finalCredits.TotalCredits[1];
|
||||
}
|
||||
if ((inventory.Boosters.find(x => x.ItemType == "/Lotus/Types/Boosters/CreditBlessing")?.ExpiryDate ?? 0) > now) {
|
||||
inventory.RegularCredits += finalCredits.TotalCredits[1];
|
||||
finalCredits.TotalCredits[1] += finalCredits.TotalCredits[1];
|
||||
}
|
||||
|
||||
return finalCredits;
|
||||
};
|
||||
|
||||
export const addFixedLevelRewards = (
|
||||
rewards: IMissionRewardExternal,
|
||||
inventory: TInventoryDatabaseDocument,
|
||||
MissionRewards: IMissionReward[],
|
||||
rewardInfo?: IRewardInfo
|
||||
): number => {
|
||||
let missionBonusCredits = 0;
|
||||
if (rewards.credits) {
|
||||
missionBonusCredits += rewards.credits;
|
||||
inventory.RegularCredits += rewards.credits;
|
||||
}
|
||||
if (rewards.items) {
|
||||
for (const item of rewards.items) {
|
||||
@ -1376,7 +1454,7 @@ export const addFixedLevelRewards = (
|
||||
if (rewards.countedItems) {
|
||||
for (const item of rewards.countedItems) {
|
||||
MissionRewards.push({
|
||||
StoreItem: `/Lotus/StoreItems${item.ItemType.substring("Lotus/".length)}`,
|
||||
StoreItem: toStoreItem(item.ItemType),
|
||||
ItemCount: item.ItemCount
|
||||
});
|
||||
}
|
||||
@ -1537,6 +1615,27 @@ function getRandomMissionDrops(
|
||||
? "/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriKullervoSteelPathRNGRewards"
|
||||
: "/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriKullervoNormalRNGRewards"
|
||||
];
|
||||
} else if (RewardInfo.T == 17) {
|
||||
if (mission?.Tier == 1) {
|
||||
logger.warn(`non-steel path duviri murmur tier used on steel path?!`);
|
||||
}
|
||||
/*if (operation eight claw is active) {
|
||||
drops.push({
|
||||
StoreItem: "/Lotus/StoreItems/Types/Gameplay/DuviriMITW/Resources/DuviriMurmurItemEvent",
|
||||
ItemCount: 10
|
||||
});
|
||||
}*/
|
||||
rewardManifests = ["/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriMurmurFinalChestRewards"];
|
||||
} else if (RewardInfo.T == 19) {
|
||||
/*if (operation eight claw is active) {
|
||||
drops.push({
|
||||
StoreItem: "/Lotus/StoreItems/Types/Gameplay/DuviriMITW/Resources/DuviriMurmurItemEvent",
|
||||
ItemCount: 15
|
||||
});
|
||||
}*/
|
||||
rewardManifests = [
|
||||
"/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriMurmurFinalSteelChestRewards"
|
||||
];
|
||||
} else if (RewardInfo.T == 70) {
|
||||
// Orowyrm chest, gives 10 Pathos Clamps, or 15 on Steel Path.
|
||||
drops.push({
|
||||
@ -1552,7 +1651,7 @@ function getRandomMissionDrops(
|
||||
if (RewardInfo.jobId) {
|
||||
if (RewardInfo.JobStage! >= 0) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [jobType, unkIndex, hubNode, syndicateMissionId, locationTag] = RewardInfo.jobId.split("_");
|
||||
const [jobType, unkIndex, hubNode, syndicateMissionId] = RewardInfo.jobId.split("_");
|
||||
let isEndlessJob = false;
|
||||
if (syndicateMissionId) {
|
||||
const syndicateMissions: ISyndicateMissionInfo[] = [];
|
||||
@ -1564,19 +1663,30 @@ function getRandomMissionDrops(
|
||||
let job = syndicateEntry.Jobs[RewardInfo.JobTier!];
|
||||
|
||||
if (syndicateEntry.Tag === "EntratiSyndicate") {
|
||||
const vault = syndicateEntry.Jobs.find(j => j.locationTag === locationTag);
|
||||
if (vault && locationTag) job = vault;
|
||||
// if (
|
||||
// [
|
||||
// "DeimosRuinsExterminateBounty",
|
||||
// "DeimosRuinsEscortBounty",
|
||||
// "DeimosRuinsMistBounty",
|
||||
// "DeimosRuinsPurifyBounty",
|
||||
// "DeimosRuinsSacBounty"
|
||||
// ].some(ending => jobType.endsWith(ending))
|
||||
// ) {
|
||||
// job.rewards = "TODO"; // Droptable for Arcana Isolation Vault
|
||||
// }
|
||||
if (
|
||||
[
|
||||
"DeimosRuinsExterminateBounty",
|
||||
"DeimosRuinsEscortBounty",
|
||||
"DeimosRuinsMistBounty",
|
||||
"DeimosRuinsPurifyBounty",
|
||||
"DeimosRuinsSacBounty",
|
||||
"VaultBounty"
|
||||
].some(ending => jobType.endsWith(ending))
|
||||
) {
|
||||
const vault = syndicateEntry.Jobs.find(
|
||||
j => j.locationTag === RewardInfo.jobId!.split("_").at(-1)
|
||||
);
|
||||
if (vault) {
|
||||
job = vault;
|
||||
if (jobType.endsWith("VaultBounty")) {
|
||||
job.rewards = job.rewards.replace(
|
||||
"/Lotus/Types/Game/MissionDecks/",
|
||||
"/Supplementals/"
|
||||
);
|
||||
job.xpAmounts = [job.xpAmounts.reduce((partialSum, a) => partialSum + a, 0)];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
[
|
||||
"DeimosEndlessAreaDefenseBounty",
|
||||
@ -1635,24 +1745,32 @@ function getRandomMissionDrops(
|
||||
}
|
||||
rewardManifests = [job.rewards];
|
||||
if (job.xpAmounts.length > 1) {
|
||||
rotations = [RewardInfo.JobStage! % (job.xpAmounts.length - 1)];
|
||||
const curentStage = RewardInfo.JobStage! + 1;
|
||||
const totalStage = job.xpAmounts.length;
|
||||
let tableIndex = 1; // Stage 2, Stage 3 of 4, and Stage 3 of 5
|
||||
|
||||
if (curentStage == 1) {
|
||||
tableIndex = 0;
|
||||
} else if (curentStage == totalStage) {
|
||||
tableIndex = 3;
|
||||
} else if (totalStage == 5 && curentStage == 4) {
|
||||
tableIndex = 2;
|
||||
}
|
||||
|
||||
rotations = [tableIndex];
|
||||
} else {
|
||||
rotations = [0];
|
||||
}
|
||||
if (
|
||||
RewardInfo.Q &&
|
||||
(RewardInfo.JobStage === job.xpAmounts.length - 1 || job.isVault) &&
|
||||
(RewardInfo.JobStage === job.xpAmounts.length - 1 || jobType.endsWith("VaultBounty")) &&
|
||||
!isEndlessJob
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (ExportRewards[job.rewards]) {
|
||||
rewardManifests.push(job.rewards);
|
||||
rotations.push(ExportRewards[job.rewards].length - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (RewardInfo.challengeMissionId) {
|
||||
const rewardTables: Record<string, string[]> = {
|
||||
EntratiLabSyndicate: [
|
||||
@ -1813,6 +1931,23 @@ function getRandomMissionDrops(
|
||||
drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount });
|
||||
}
|
||||
}
|
||||
|
||||
if (config.missionsCanGiveAllRelics) {
|
||||
for (const drop of drops) {
|
||||
const itemType = fromStoreItem(drop.StoreItem);
|
||||
if (itemType in ExportRelics) {
|
||||
const relic = ExportRelics[itemType];
|
||||
const replacement = getRandomElement(
|
||||
Object.entries(ExportRelics).filter(
|
||||
arr => arr[1].era == relic.era && arr[1].quality == relic.quality
|
||||
)
|
||||
)!;
|
||||
logger.debug(`replacing ${relic.era} ${relic.category} with ${replacement[1].category}`);
|
||||
drop.StoreItem = toStoreItem(replacement[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return drops;
|
||||
}
|
||||
|
||||
@ -1865,7 +2000,16 @@ const libraryPersonalTargetToAvatar: Record<string, string> = {
|
||||
"/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/NullifySpacemanAvatar"
|
||||
};
|
||||
|
||||
const node_excluded_buddies: Record<string, string> = {
|
||||
const chemistryBuddies: readonly string[] = [
|
||||
"/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue",
|
||||
"/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue",
|
||||
"/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue",
|
||||
"/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue",
|
||||
"/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue",
|
||||
"/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue"
|
||||
];
|
||||
|
||||
/*const node_excluded_buddies: Record<string, string> = {
|
||||
SolNode856: "/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue",
|
||||
SolNode852: "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue",
|
||||
SolNode851: "/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue",
|
||||
@ -1915,4 +2059,25 @@ const getHexBounties = (seed: number): { nodes: string[]; buddies: string[] } =>
|
||||
}
|
||||
}
|
||||
return { nodes, buddies };
|
||||
};*/
|
||||
|
||||
const goalMessagesByKey: Record<string, { sndr: string; msg: string; sub: string; icon: string }> = {
|
||||
"/Lotus/Types/Keys/GalleonRobberyAlert": {
|
||||
sndr: "/Lotus/Language/Bosses/BossCouncilorVayHek",
|
||||
msg: "/Lotus/Language/Messages/GalleonRobbery2025RewardMsgA",
|
||||
sub: "/Lotus/Language/Messages/GalleonRobbery2025MissionTitleA",
|
||||
icon: "/Lotus/Interface/Icons/Npcs/VayHekPortrait.png"
|
||||
},
|
||||
"/Lotus/Types/Keys/GalleonRobberyAlertB": {
|
||||
sndr: "/Lotus/Language/Bosses/BossCouncilorVayHek",
|
||||
msg: "/Lotus/Language/Messages/GalleonRobbery2025RewardMsgB",
|
||||
sub: "/Lotus/Language/Messages/GalleonRobbery2025MissionTitleB",
|
||||
icon: "/Lotus/Interface/Icons/Npcs/VayHekPortrait.png"
|
||||
},
|
||||
"/Lotus/Types/Keys/GalleonRobberyAlertC": {
|
||||
sndr: "/Lotus/Language/Bosses/BossCouncilorVayHek",
|
||||
msg: "/Lotus/Language/Messages/GalleonRobbery2025RewardMsgC",
|
||||
sub: "/Lotus/Language/Messages/GalleonRobbery2025MissionTitleC",
|
||||
icon: "/Lotus/Interface/Icons/Npcs/VayHekPortrait.png"
|
||||
}
|
||||
};
|
||||
|
@ -8,12 +8,20 @@ import {
|
||||
updateCurrency,
|
||||
updateSlots
|
||||
} from "@/src/services/inventoryService";
|
||||
import { getRandomWeightedRewardUc } from "@/src/services/rngService";
|
||||
import { getRandomReward, getRandomWeightedRewardUc } from "@/src/services/rngService";
|
||||
import { applyStandingToVendorManifest, getVendorManifestByOid } from "@/src/services/serversideVendorsService";
|
||||
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
|
||||
import { IPurchaseRequest, IPurchaseResponse, SlotPurchase, IInventoryChanges } from "@/src/types/purchaseTypes";
|
||||
import {
|
||||
IPurchaseRequest,
|
||||
IPurchaseResponse,
|
||||
SlotPurchase,
|
||||
IInventoryChanges,
|
||||
PurchaseSource,
|
||||
IPurchaseParams
|
||||
} from "@/src/types/purchaseTypes";
|
||||
import { logger } from "@/src/utils/logger";
|
||||
import worldState from "@/static/fixed_responses/worldState/worldState.json";
|
||||
import { getWorldState } from "./worldStateService";
|
||||
import staticWorldState from "@/static/fixed_responses/worldState/worldState.json";
|
||||
import {
|
||||
ExportBoosterPacks,
|
||||
ExportBoosters,
|
||||
@ -28,6 +36,8 @@ import {
|
||||
import { config } from "./configService";
|
||||
import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel";
|
||||
import { fromStoreItem, toStoreItem } from "./itemDataService";
|
||||
import { DailyDeal } from "../models/worldStateModel";
|
||||
import { fromMongoDate, toMongoDate } from "../helpers/inventoryHelpers";
|
||||
|
||||
export const getStoreItemCategory = (storeItem: string): string => {
|
||||
const storeItemString = getSubstringFromKeyword(storeItem, "StoreItems/");
|
||||
@ -44,6 +54,58 @@ export const getStoreItemTypesCategory = (typesItem: string): string => {
|
||||
return typeElements[1];
|
||||
};
|
||||
|
||||
const tallyVendorPurchase = (
|
||||
inventory: TInventoryDatabaseDocument,
|
||||
inventoryChanges: IInventoryChanges,
|
||||
VendorType: string,
|
||||
ItemId: string,
|
||||
numPurchased: number,
|
||||
Expiry: Date
|
||||
): void => {
|
||||
if (!config.noVendorPurchaseLimits) {
|
||||
inventory.RecentVendorPurchases ??= [];
|
||||
let vendorPurchases = inventory.RecentVendorPurchases.find(x => x.VendorType == VendorType);
|
||||
if (!vendorPurchases) {
|
||||
vendorPurchases =
|
||||
inventory.RecentVendorPurchases[
|
||||
inventory.RecentVendorPurchases.push({
|
||||
VendorType: VendorType,
|
||||
PurchaseHistory: []
|
||||
}) - 1
|
||||
];
|
||||
}
|
||||
let historyEntry = vendorPurchases.PurchaseHistory.find(x => x.ItemId == ItemId);
|
||||
if (historyEntry) {
|
||||
if (Date.now() >= historyEntry.Expiry.getTime()) {
|
||||
historyEntry.NumPurchased = numPurchased;
|
||||
historyEntry.Expiry = Expiry;
|
||||
} else {
|
||||
historyEntry.NumPurchased += numPurchased;
|
||||
}
|
||||
} else {
|
||||
historyEntry =
|
||||
vendorPurchases.PurchaseHistory[
|
||||
vendorPurchases.PurchaseHistory.push({
|
||||
ItemId: ItemId,
|
||||
NumPurchased: numPurchased,
|
||||
Expiry: Expiry
|
||||
}) - 1
|
||||
];
|
||||
}
|
||||
inventoryChanges.NewVendorPurchase = {
|
||||
VendorType: VendorType,
|
||||
PurchaseHistory: [
|
||||
{
|
||||
ItemId: ItemId,
|
||||
NumPurchased: historyEntry.NumPurchased,
|
||||
Expiry: toMongoDate(Expiry)
|
||||
}
|
||||
]
|
||||
};
|
||||
inventoryChanges.RecentVendorPurchases = inventoryChanges.NewVendorPurchase;
|
||||
}
|
||||
};
|
||||
|
||||
export const handlePurchase = async (
|
||||
purchaseRequest: IPurchaseRequest,
|
||||
inventory: TInventoryDatabaseDocument
|
||||
@ -52,7 +114,7 @@ export const handlePurchase = async (
|
||||
|
||||
const prePurchaseInventoryChanges: IInventoryChanges = {};
|
||||
let seed: bigint | undefined;
|
||||
if (purchaseRequest.PurchaseParams.Source == 7) {
|
||||
if (purchaseRequest.PurchaseParams.Source == PurchaseSource.Vendor) {
|
||||
let manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!);
|
||||
if (manifest) {
|
||||
manifest = applyStandingToVendorManifest(inventory, manifest);
|
||||
@ -67,18 +129,17 @@ export const handlePurchase = async (
|
||||
if (!offer) {
|
||||
throw new Error(`unknown vendor offer: ${ItemId ? ItemId : purchaseRequest.PurchaseParams.StoreItem}`);
|
||||
}
|
||||
if (!config.dontSubtractPurchaseCreditCost) {
|
||||
if (offer.RegularPrice) {
|
||||
combineInventoryChanges(
|
||||
prePurchaseInventoryChanges,
|
||||
updateCurrency(inventory, offer.RegularPrice[0], false)
|
||||
);
|
||||
updateCurrency(inventory, offer.RegularPrice[0], false, prePurchaseInventoryChanges);
|
||||
}
|
||||
}
|
||||
if (!config.dontSubtractPurchasePlatinumCost) {
|
||||
if (offer.PremiumPrice) {
|
||||
combineInventoryChanges(
|
||||
prePurchaseInventoryChanges,
|
||||
updateCurrency(inventory, offer.PremiumPrice[0], true)
|
||||
);
|
||||
updateCurrency(inventory, offer.PremiumPrice[0], true, prePurchaseInventoryChanges);
|
||||
}
|
||||
}
|
||||
if (!config.dontSubtractPurchaseItemCost) {
|
||||
if (offer.ItemPrices) {
|
||||
handleItemPrices(
|
||||
inventory,
|
||||
@ -87,23 +148,11 @@ export const handlePurchase = async (
|
||||
prePurchaseInventoryChanges
|
||||
);
|
||||
}
|
||||
}
|
||||
if (offer.LocTagRandSeed !== undefined) {
|
||||
seed = BigInt(offer.LocTagRandSeed);
|
||||
}
|
||||
if (!config.noVendorPurchaseLimits && ItemId) {
|
||||
inventory.RecentVendorPurchases ??= [];
|
||||
let vendorPurchases = inventory.RecentVendorPurchases.find(
|
||||
x => x.VendorType == manifest!.VendorInfo.TypeName
|
||||
);
|
||||
if (!vendorPurchases) {
|
||||
vendorPurchases =
|
||||
inventory.RecentVendorPurchases[
|
||||
inventory.RecentVendorPurchases.push({
|
||||
VendorType: manifest.VendorInfo.TypeName,
|
||||
PurchaseHistory: []
|
||||
}) - 1
|
||||
];
|
||||
}
|
||||
if (ItemId) {
|
||||
let expiry = parseInt(offer.Expiry.$date.$numberLong);
|
||||
if (purchaseRequest.PurchaseParams.IsWeekly) {
|
||||
const EPOCH = 1734307200 * 1000; // Monday
|
||||
@ -111,34 +160,14 @@ export const handlePurchase = async (
|
||||
const weekStart = EPOCH + week * 604800000;
|
||||
expiry = weekStart + 604800000;
|
||||
}
|
||||
const historyEntry = vendorPurchases.PurchaseHistory.find(x => x.ItemId == ItemId);
|
||||
let numPurchased = purchaseRequest.PurchaseParams.Quantity;
|
||||
if (historyEntry) {
|
||||
if (Date.now() >= historyEntry.Expiry.getTime()) {
|
||||
historyEntry.NumPurchased = numPurchased;
|
||||
historyEntry.Expiry = new Date(expiry);
|
||||
} else {
|
||||
numPurchased += historyEntry.NumPurchased;
|
||||
historyEntry.NumPurchased += purchaseRequest.PurchaseParams.Quantity;
|
||||
}
|
||||
} else {
|
||||
vendorPurchases.PurchaseHistory.push({
|
||||
ItemId: ItemId,
|
||||
NumPurchased: purchaseRequest.PurchaseParams.Quantity,
|
||||
Expiry: new Date(expiry)
|
||||
});
|
||||
}
|
||||
prePurchaseInventoryChanges.NewVendorPurchase = {
|
||||
VendorType: manifest.VendorInfo.TypeName,
|
||||
PurchaseHistory: [
|
||||
{
|
||||
ItemId: ItemId,
|
||||
NumPurchased: numPurchased,
|
||||
Expiry: { $date: { $numberLong: expiry.toString() } }
|
||||
}
|
||||
]
|
||||
};
|
||||
prePurchaseInventoryChanges.RecentVendorPurchases = prePurchaseInventoryChanges.NewVendorPurchase;
|
||||
tallyVendorPurchase(
|
||||
inventory,
|
||||
prePurchaseInventoryChanges,
|
||||
manifest.VendorInfo.TypeName,
|
||||
ItemId,
|
||||
purchaseRequest.PurchaseParams.Quantity,
|
||||
new Date(expiry)
|
||||
);
|
||||
}
|
||||
purchaseRequest.PurchaseParams.Quantity *= offer.QuantityMultiplier;
|
||||
} else {
|
||||
@ -160,18 +189,16 @@ export const handlePurchase = async (
|
||||
);
|
||||
combineInventoryChanges(purchaseResponse.InventoryChanges, prePurchaseInventoryChanges);
|
||||
|
||||
const currencyChanges = updateCurrency(
|
||||
updateCurrency(
|
||||
inventory,
|
||||
purchaseRequest.PurchaseParams.ExpectedPrice,
|
||||
purchaseRequest.PurchaseParams.UsePremium
|
||||
purchaseRequest.PurchaseParams.UsePremium,
|
||||
prePurchaseInventoryChanges
|
||||
);
|
||||
purchaseResponse.InventoryChanges = {
|
||||
...currencyChanges,
|
||||
...purchaseResponse.InventoryChanges
|
||||
};
|
||||
|
||||
switch (purchaseRequest.PurchaseParams.Source) {
|
||||
case 1: {
|
||||
case PurchaseSource.VoidTrader: {
|
||||
const worldState = getWorldState();
|
||||
if (purchaseRequest.PurchaseParams.SourceId! != worldState.VoidTraders[0]._id.$oid) {
|
||||
throw new Error("invalid request source");
|
||||
}
|
||||
@ -179,14 +206,14 @@ export const handlePurchase = async (
|
||||
x => x.ItemType == purchaseRequest.PurchaseParams.StoreItem
|
||||
);
|
||||
if (offer) {
|
||||
combineInventoryChanges(
|
||||
purchaseResponse.InventoryChanges,
|
||||
updateCurrency(inventory, offer.RegularPrice, false)
|
||||
);
|
||||
if (!config.dontSubtractPurchaseCreditCost) {
|
||||
updateCurrency(inventory, offer.RegularPrice, false, purchaseResponse.InventoryChanges);
|
||||
}
|
||||
if (purchaseRequest.PurchaseParams.ExpectedPrice) {
|
||||
throw new Error(`vendor purchase should not have an expected price`);
|
||||
}
|
||||
|
||||
if (offer.PrimePrice && !config.dontSubtractPurchaseItemCost) {
|
||||
const invItem: IMiscItem = {
|
||||
ItemType: "/Lotus/Types/Items/MiscItems/PrimeBucks",
|
||||
ItemCount: offer.PrimePrice * purchaseRequest.PurchaseParams.Quantity * -1
|
||||
@ -195,9 +222,21 @@ export const handlePurchase = async (
|
||||
purchaseResponse.InventoryChanges.MiscItems ??= [];
|
||||
purchaseResponse.InventoryChanges.MiscItems.push(invItem);
|
||||
}
|
||||
|
||||
if (offer.Limit) {
|
||||
tallyVendorPurchase(
|
||||
inventory,
|
||||
purchaseResponse.InventoryChanges,
|
||||
"VoidTrader",
|
||||
offer.ItemType,
|
||||
purchaseRequest.PurchaseParams.Quantity,
|
||||
fromMongoDate(worldState.VoidTraders[0].Expiry)
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
case PurchaseSource.SyndicateFavor:
|
||||
{
|
||||
const syndicateTag = purchaseRequest.PurchaseParams.SyndicateTag!;
|
||||
if (purchaseRequest.PurchaseParams.UseFreeFavor!) {
|
||||
@ -211,7 +250,7 @@ export const handlePurchase = async (
|
||||
Title: lastTitle
|
||||
}
|
||||
];
|
||||
} else {
|
||||
} else if (!config.dontSubtractPurchaseStandingCost) {
|
||||
const syndicate = ExportSyndicates[syndicateTag];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (syndicate) {
|
||||
@ -234,24 +273,24 @@ export const handlePurchase = async (
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 7:
|
||||
case PurchaseSource.DailyDeal:
|
||||
if (purchaseRequest.PurchaseParams.ExpectedPrice) {
|
||||
throw new Error(`daily deal purchase should not have an expected price`);
|
||||
}
|
||||
await handleDailyDealPurchase(inventory, purchaseRequest.PurchaseParams, purchaseResponse);
|
||||
break;
|
||||
case PurchaseSource.Vendor:
|
||||
if (purchaseRequest.PurchaseParams.SourceId! in ExportVendors) {
|
||||
const vendor = ExportVendors[purchaseRequest.PurchaseParams.SourceId!];
|
||||
const offer = vendor.items.find(x => x.storeItem == purchaseRequest.PurchaseParams.StoreItem);
|
||||
if (offer) {
|
||||
if (typeof offer.credits == "number") {
|
||||
combineInventoryChanges(
|
||||
purchaseResponse.InventoryChanges,
|
||||
updateCurrency(inventory, offer.credits, false)
|
||||
);
|
||||
if (typeof offer.credits == "number" && !config.dontSubtractPurchaseCreditCost) {
|
||||
updateCurrency(inventory, offer.credits, false, purchaseResponse.InventoryChanges);
|
||||
}
|
||||
if (typeof offer.platinum == "number") {
|
||||
combineInventoryChanges(
|
||||
purchaseResponse.InventoryChanges,
|
||||
updateCurrency(inventory, offer.platinum, true)
|
||||
);
|
||||
if (typeof offer.platinum == "number" && !config.dontSubtractPurchasePlatinumCost) {
|
||||
updateCurrency(inventory, offer.platinum, true, purchaseResponse.InventoryChanges);
|
||||
}
|
||||
if (offer.itemPrices) {
|
||||
if (offer.itemPrices && !config.dontSubtractPurchaseItemCost) {
|
||||
handleItemPrices(
|
||||
inventory,
|
||||
offer.itemPrices,
|
||||
@ -265,19 +304,20 @@ export const handlePurchase = async (
|
||||
throw new Error(`vendor purchase should not have an expected price`);
|
||||
}
|
||||
break;
|
||||
case 18: {
|
||||
if (purchaseRequest.PurchaseParams.SourceId! != worldState.PrimeVaultTraders[0]._id.$oid) {
|
||||
case PurchaseSource.PrimeVaultTrader: {
|
||||
if (purchaseRequest.PurchaseParams.SourceId! != staticWorldState.PrimeVaultTraders[0]._id.$oid) {
|
||||
throw new Error("invalid request source");
|
||||
}
|
||||
const offer =
|
||||
worldState.PrimeVaultTraders[0].Manifest.find(
|
||||
staticWorldState.PrimeVaultTraders[0].Manifest.find(
|
||||
x => x.ItemType == purchaseRequest.PurchaseParams.StoreItem
|
||||
) ??
|
||||
worldState.PrimeVaultTraders[0].EvergreenManifest.find(
|
||||
staticWorldState.PrimeVaultTraders[0].EvergreenManifest.find(
|
||||
x => x.ItemType == purchaseRequest.PurchaseParams.StoreItem
|
||||
);
|
||||
if (offer) {
|
||||
if (offer.RegularPrice) {
|
||||
if (!config.dontSubtractPurchaseItemCost) {
|
||||
const invItem: IMiscItem = {
|
||||
ItemType: "/Lotus/Types/Items/MiscItems/SchismKey",
|
||||
ItemCount: offer.RegularPrice * purchaseRequest.PurchaseParams.Quantity * -1
|
||||
@ -287,6 +327,7 @@ export const handlePurchase = async (
|
||||
|
||||
purchaseResponse.InventoryChanges.MiscItems ??= [];
|
||||
purchaseResponse.InventoryChanges.MiscItems.push(invItem);
|
||||
}
|
||||
} else if (!config.infiniteRegalAya) {
|
||||
inventory.PrimeTokens -= offer.PrimePrice! * purchaseRequest.PurchaseParams.Quantity;
|
||||
|
||||
@ -326,6 +367,25 @@ const handleItemPrices = (
|
||||
}
|
||||
};
|
||||
|
||||
export const handleDailyDealPurchase = async (
|
||||
inventory: TInventoryDatabaseDocument,
|
||||
purchaseParams: IPurchaseParams,
|
||||
purchaseResponse: IPurchaseResponse
|
||||
): Promise<void> => {
|
||||
const dailyDeal = (await DailyDeal.findOne({ StoreItem: purchaseParams.StoreItem }))!;
|
||||
dailyDeal.AmountSold += 1;
|
||||
await dailyDeal.save();
|
||||
|
||||
if (!config.dontSubtractPurchasePlatinumCost) {
|
||||
updateCurrency(inventory, dailyDeal.SalePrice, true, purchaseResponse.InventoryChanges);
|
||||
}
|
||||
|
||||
if (!config.noVendorPurchaseLimits) {
|
||||
inventory.UsedDailyDeals.push(purchaseParams.StoreItem);
|
||||
purchaseResponse.DailyDealUsed = purchaseParams.StoreItem;
|
||||
}
|
||||
};
|
||||
|
||||
export const handleBundleAcqusition = async (
|
||||
storeItemName: string,
|
||||
inventory: TInventoryDatabaseDocument,
|
||||
@ -369,18 +429,28 @@ export const handleStoreItemAcquisition = async (
|
||||
} else {
|
||||
const storeCategory = getStoreItemCategory(storeItemName);
|
||||
const internalName = fromStoreItem(storeItemName);
|
||||
logger.debug(`store category ${storeCategory}`);
|
||||
if (!ignorePurchaseQuantity) {
|
||||
if (internalName in ExportGear) {
|
||||
quantity *= ExportGear[internalName].purchaseQuantity || 1;
|
||||
logger.debug(`factored quantity is ${quantity}`);
|
||||
} else if (internalName in ExportResources) {
|
||||
quantity *= ExportResources[internalName].purchaseQuantity || 1;
|
||||
logger.debug(`factored quantity is ${quantity}`);
|
||||
}
|
||||
}
|
||||
logger.debug(`store category ${storeCategory}`);
|
||||
switch (storeCategory) {
|
||||
default: {
|
||||
purchaseResponse = {
|
||||
InventoryChanges: await addItem(inventory, internalName, quantity, premiumPurchase, seed)
|
||||
InventoryChanges: await addItem(
|
||||
inventory,
|
||||
internalName,
|
||||
quantity,
|
||||
premiumPurchase,
|
||||
seed,
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
};
|
||||
break;
|
||||
}
|
||||
@ -470,12 +540,57 @@ const handleBoosterPackPurchase = async (
|
||||
"attempt to roll over 100 booster packs in a single go. possible but unlikely to be desirable for the user or the server."
|
||||
);
|
||||
}
|
||||
const specialItemReward = pack.components.find(x => x.PityIncreaseRate);
|
||||
for (let i = 0; i != quantity; ++i) {
|
||||
if (specialItemReward) {
|
||||
{
|
||||
const normalComponents = [];
|
||||
for (const comp of pack.components) {
|
||||
if (!comp.PityIncreaseRate) {
|
||||
const { Probability, ...rest } = comp;
|
||||
normalComponents.push({
|
||||
...rest,
|
||||
probability: Probability!
|
||||
});
|
||||
}
|
||||
}
|
||||
const result = getRandomReward(normalComponents)!;
|
||||
logger.debug(`booster pack rolled`, result);
|
||||
purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + ',{"lvl":0};';
|
||||
combineInventoryChanges(
|
||||
purchaseResponse.InventoryChanges,
|
||||
await addItem(inventory, result.Item, result.Amount)
|
||||
);
|
||||
}
|
||||
|
||||
if (!inventory.WeaponSkins.some(x => x.ItemType == specialItemReward.Item)) {
|
||||
inventory.SpecialItemRewardAttenuation ??= [];
|
||||
let atten = inventory.SpecialItemRewardAttenuation.find(x => x.Tag == specialItemReward.Item);
|
||||
if (!atten) {
|
||||
atten =
|
||||
inventory.SpecialItemRewardAttenuation[
|
||||
inventory.SpecialItemRewardAttenuation.push({
|
||||
Tag: specialItemReward.Item,
|
||||
Atten: specialItemReward.Probability!
|
||||
}) - 1
|
||||
];
|
||||
}
|
||||
if (Math.random() < atten.Atten) {
|
||||
purchaseResponse.BoosterPackItems += toStoreItem(specialItemReward.Item) + ',{"lvl":0};';
|
||||
combineInventoryChanges(
|
||||
purchaseResponse.InventoryChanges,
|
||||
await addItem(inventory, specialItemReward.Item)
|
||||
);
|
||||
// TOVERIFY: Is the SpecialItemRewardAttenuation entry removed now?
|
||||
} else {
|
||||
atten.Atten += specialItemReward.PityIncreaseRate!;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const disallowedItems = new Set();
|
||||
for (let roll = 0; roll != pack.rarityWeightsPerRoll.length; ) {
|
||||
const weights = pack.rarityWeightsPerRoll[roll];
|
||||
const result = getRandomWeightedRewardUc(pack.components, weights);
|
||||
if (result) {
|
||||
const result = getRandomWeightedRewardUc(pack.components, weights)!;
|
||||
logger.debug(`booster pack rolled`, result);
|
||||
if (disallowedItems.has(result.Item)) {
|
||||
logger.debug(`oops, can't use that one; trying again`);
|
||||
@ -485,11 +600,14 @@ const handleBoosterPackPurchase = async (
|
||||
disallowedItems.add(result.Item);
|
||||
}
|
||||
purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + ',{"lvl":0};';
|
||||
combineInventoryChanges(purchaseResponse.InventoryChanges, await addItem(inventory, result.Item, 1));
|
||||
}
|
||||
combineInventoryChanges(
|
||||
purchaseResponse.InventoryChanges,
|
||||
await addItem(inventory, result.Item, result.Amount)
|
||||
);
|
||||
++roll;
|
||||
}
|
||||
}
|
||||
}
|
||||
return purchaseResponse;
|
||||
};
|
||||
|
||||
@ -522,7 +640,9 @@ const handleTypesPurchase = async (
|
||||
logger.debug(`type category ${typeCategory}`);
|
||||
switch (typeCategory) {
|
||||
default:
|
||||
return { InventoryChanges: await addItem(inventory, typesName, quantity, premiumPurchase, seed) };
|
||||
return {
|
||||
InventoryChanges: await addItem(inventory, typesName, quantity, premiumPurchase, seed, undefined, true)
|
||||
};
|
||||
case "BoosterPacks":
|
||||
return handleBoosterPackPurchase(typesName, inventory, quantity);
|
||||
case "SlotItems":
|
||||
|
@ -331,7 +331,7 @@ export const giveKeyChainMissionReward = async (
|
||||
const fixedLevelRewards = getLevelKeyRewards(missionName);
|
||||
if (fixedLevelRewards.levelKeyRewards) {
|
||||
const missionRewards: { StoreItem: string; ItemCount: number }[] = [];
|
||||
addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, inventory, missionRewards);
|
||||
inventory.RegularCredits += addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, missionRewards);
|
||||
|
||||
for (const reward of missionRewards) {
|
||||
await addItem(inventory, fromStoreItem(reward.StoreItem), reward.ItemCount);
|
||||
|
@ -107,6 +107,16 @@ export class SRng {
|
||||
return arr[this.randomInt(0, arr.length - 1)];
|
||||
}
|
||||
|
||||
randomElementPop<T>(arr: T[]): T | undefined {
|
||||
if (arr.length != 0) {
|
||||
const index = this.randomInt(0, arr.length - 1);
|
||||
const elm = arr[index];
|
||||
arr.splice(index, 1);
|
||||
return elm;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
randomFloat(): number {
|
||||
this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn;
|
||||
return (Number(this.state >> 38n) & 0xffffff) * 0.000000059604645;
|
||||
|
@ -149,7 +149,8 @@ export const handleInventoryItemConfigChange = async (
|
||||
} else {
|
||||
const inventoryItem = inventory.WeaponSkins.id(itemId);
|
||||
if (!inventoryItem) {
|
||||
throw new Error(`inventory item WeaponSkins not found with id ${itemId}`);
|
||||
logger.warn(`inventory item WeaponSkins not found with id ${itemId}`);
|
||||
continue;
|
||||
}
|
||||
if ("Favorite" in itemConfigEntries) {
|
||||
inventoryItem.Favorite = itemConfigEntries.Favorite;
|
||||
@ -166,8 +167,13 @@ export const handleInventoryItemConfigChange = async (
|
||||
inventory.LotusCustomization = equipmentChanges.LotusCustomization;
|
||||
break;
|
||||
}
|
||||
case "ValidNewLoadoutId": {
|
||||
logger.debug(`ignoring ValidNewLoadoutId (${equipmentChanges.ValidNewLoadoutId})`);
|
||||
// seems always equal to the id of loadout config NORMAL[0], likely has no purpose and we're free to ignore it
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (equipmentKeys.includes(equipmentName as TEquipmentKey) && equipmentName != "ValidNewLoadoutId") {
|
||||
if (equipmentKeys.includes(equipmentName as TEquipmentKey)) {
|
||||
logger.debug(`general Item config saved of type ${equipmentName}`, {
|
||||
config: equipment
|
||||
});
|
||||
@ -177,7 +183,8 @@ export const handleInventoryItemConfigChange = async (
|
||||
const inventoryItem = inventory[equipmentName].id(itemId);
|
||||
|
||||
if (!inventoryItem) {
|
||||
throw new Error(`inventory item ${equipmentName} not found with id ${itemId}`);
|
||||
logger.warn(`inventory item ${equipmentName} not found with id ${itemId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [configId, config] of Object.entries(itemConfigEntries)) {
|
||||
@ -214,7 +221,7 @@ export const handleInventoryItemConfigChange = async (
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
logger.warn(`loadout category not implemented, changes may be lost: ${equipmentName}`, {
|
||||
logger.error(`loadout category not implemented, changes will be lost: ${equipmentName}`, {
|
||||
config: equipment
|
||||
});
|
||||
}
|
||||
|
@ -1,64 +1,12 @@
|
||||
import { unixTimesInMs } from "@/src/constants/timeConstants";
|
||||
import { isDev } from "@/src/helpers/pathHelper";
|
||||
import { args } from "@/src/helpers/commandLineArguments";
|
||||
import { catBreadHash } from "@/src/helpers/stringHelpers";
|
||||
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
|
||||
import { mixSeeds, SRng } from "@/src/services/rngService";
|
||||
import { IMongoDate } from "@/src/types/commonTypes";
|
||||
import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes";
|
||||
import { logger } from "@/src/utils/logger";
|
||||
import { ExportVendors, IRange, IVendor } from "warframe-public-export-plus";
|
||||
|
||||
import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json";
|
||||
import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json";
|
||||
import DeimosHivemindCommisionsManifestFishmonger from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestFishmonger.json";
|
||||
import DeimosHivemindCommisionsManifestPetVendor from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestPetVendor.json";
|
||||
import DeimosHivemindCommisionsManifestProspector from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestProspector.json";
|
||||
import DeimosHivemindCommisionsManifestTokenVendor from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestTokenVendor.json";
|
||||
import DeimosHivemindCommisionsManifestWeaponsmith from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestWeaponsmith.json";
|
||||
import DeimosHivemindTokenVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosHivemindTokenVendorManifest.json";
|
||||
import DeimosPetVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosPetVendorManifest.json";
|
||||
import DeimosProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosProspectorVendorManifest.json";
|
||||
import DuviriAcrithisVendorManifest from "@/static/fixed_responses/getVendorInfo/DuviriAcrithisVendorManifest.json";
|
||||
import EntratiLabsEntratiLabsCommisionsManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabsCommisionsManifest.json";
|
||||
import EntratiLabsEntratiLabVendorManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabVendorManifest.json";
|
||||
import HubsIronwakeDondaVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json";
|
||||
import HubsRailjackCrewMemberVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json";
|
||||
import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSalesmanManifest.json";
|
||||
import Nova1999ConquestShopManifest from "@/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json";
|
||||
import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json";
|
||||
import OstronProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronProspectorVendorManifest.json";
|
||||
import SolarisDebtTokenVendorRepossessionsManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorRepossessionsManifest.json";
|
||||
import SolarisProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisProspectorVendorManifest.json";
|
||||
import Temple1999VendorManifest from "@/static/fixed_responses/getVendorInfo/Temple1999VendorManifest.json";
|
||||
import TeshinHardModeVendorManifest from "@/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json";
|
||||
import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVendorInfo/ZarimanCommisionsManifestArchimedean.json";
|
||||
|
||||
const rawVendorManifests: IVendorManifest[] = [
|
||||
ArchimedeanVendorManifest,
|
||||
DeimosEntratiFragmentVendorProductsManifest,
|
||||
DeimosHivemindCommisionsManifestFishmonger,
|
||||
DeimosHivemindCommisionsManifestPetVendor,
|
||||
DeimosHivemindCommisionsManifestProspector,
|
||||
DeimosHivemindCommisionsManifestTokenVendor,
|
||||
DeimosHivemindCommisionsManifestWeaponsmith,
|
||||
DeimosHivemindTokenVendorManifest,
|
||||
DeimosPetVendorManifest,
|
||||
DeimosProspectorVendorManifest,
|
||||
DuviriAcrithisVendorManifest,
|
||||
EntratiLabsEntratiLabsCommisionsManifest,
|
||||
EntratiLabsEntratiLabVendorManifest,
|
||||
HubsIronwakeDondaVendorManifest, // uses preprocessing
|
||||
HubsRailjackCrewMemberVendorManifest,
|
||||
MaskSalesmanManifest,
|
||||
Nova1999ConquestShopManifest,
|
||||
OstronPetVendorManifest,
|
||||
OstronProspectorVendorManifest,
|
||||
SolarisDebtTokenVendorRepossessionsManifest,
|
||||
SolarisProspectorVendorManifest,
|
||||
Temple1999VendorManifest,
|
||||
TeshinHardModeVendorManifest, // uses preprocessing
|
||||
ZarimanCommisionsManifestArchimedean
|
||||
];
|
||||
import { ExportVendors, IRange, IVendor, IVendorOffer } from "warframe-public-export-plus";
|
||||
import { config } from "./configService";
|
||||
|
||||
interface IGeneratableVendorInfo extends Omit<IVendorInfo, "ItemManifest" | "Expiry"> {
|
||||
cycleOffset?: number;
|
||||
@ -83,10 +31,6 @@ const generatableVendors: IGeneratableVendorInfo[] = [
|
||||
cycleOffset: 1744934400_000,
|
||||
cycleDuration: 4 * unixTimesInMs.day
|
||||
}
|
||||
// {
|
||||
// _id: { $oid: "5dbb4c41e966f7886c3ce939" },
|
||||
// TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest"
|
||||
// }
|
||||
];
|
||||
|
||||
const getVendorOid = (typeName: string): string => {
|
||||
@ -101,60 +45,60 @@ const gcd = (a: number, b: number): number => {
|
||||
const getCycleDuration = (manifest: IVendor): number => {
|
||||
let dur = 0;
|
||||
for (const item of manifest.items) {
|
||||
if (typeof item.durationHours != "number") {
|
||||
if (item.alwaysOffered) {
|
||||
continue;
|
||||
}
|
||||
const durationHours = item.rotatedWeekly ? 168 : item.durationHours;
|
||||
if (typeof durationHours != "number") {
|
||||
dur = 1;
|
||||
break;
|
||||
}
|
||||
if (dur != item.durationHours) {
|
||||
dur = gcd(dur, item.durationHours);
|
||||
if (dur != durationHours) {
|
||||
dur = gcd(dur, durationHours);
|
||||
}
|
||||
}
|
||||
return dur * unixTimesInMs.hour;
|
||||
};
|
||||
|
||||
export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => {
|
||||
for (const vendorManifest of rawVendorManifests) {
|
||||
if (vendorManifest.VendorInfo.TypeName == typeName) {
|
||||
return preprocessVendorManifest(vendorManifest);
|
||||
}
|
||||
}
|
||||
export const getVendorManifestByTypeName = (typeName: string, fullStock?: boolean): IVendorManifest | undefined => {
|
||||
for (const vendorInfo of generatableVendors) {
|
||||
if (vendorInfo.TypeName == typeName) {
|
||||
return generateVendorManifest(vendorInfo);
|
||||
return generateVendorManifest(vendorInfo, fullStock ?? config.fullyStockedVendors);
|
||||
}
|
||||
}
|
||||
if (typeName in ExportVendors) {
|
||||
const manifest = ExportVendors[typeName];
|
||||
return generateVendorManifest({
|
||||
return generateVendorManifest(
|
||||
{
|
||||
_id: { $oid: getVendorOid(typeName) },
|
||||
TypeName: typeName,
|
||||
RandomSeedType: manifest.randomSeedType,
|
||||
cycleDuration: getCycleDuration(manifest)
|
||||
});
|
||||
},
|
||||
fullStock ?? config.fullyStockedVendors
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined => {
|
||||
for (const vendorManifest of rawVendorManifests) {
|
||||
if (vendorManifest.VendorInfo._id.$oid == oid) {
|
||||
return preprocessVendorManifest(vendorManifest);
|
||||
}
|
||||
}
|
||||
for (const vendorInfo of generatableVendors) {
|
||||
if (vendorInfo._id.$oid == oid) {
|
||||
return generateVendorManifest(vendorInfo);
|
||||
return generateVendorManifest(vendorInfo, config.fullyStockedVendors);
|
||||
}
|
||||
}
|
||||
for (const [typeName, manifest] of Object.entries(ExportVendors)) {
|
||||
const typeNameOid = getVendorOid(typeName);
|
||||
if (typeNameOid == oid) {
|
||||
return generateVendorManifest({
|
||||
return generateVendorManifest(
|
||||
{
|
||||
_id: { $oid: typeNameOid },
|
||||
TypeName: typeName,
|
||||
RandomSeedType: manifest.randomSeedType,
|
||||
cycleDuration: getCycleDuration(manifest)
|
||||
});
|
||||
},
|
||||
config.fullyStockedVendors
|
||||
);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
@ -197,30 +141,6 @@ export const applyStandingToVendorManifest = (
|
||||
};
|
||||
};
|
||||
|
||||
const preprocessVendorManifest = (originalManifest: IVendorManifest): IVendorManifest => {
|
||||
if (Date.now() >= parseInt(originalManifest.VendorInfo.Expiry.$date.$numberLong)) {
|
||||
const manifest = structuredClone(originalManifest);
|
||||
const info = manifest.VendorInfo;
|
||||
refreshExpiry(info.Expiry);
|
||||
for (const offer of info.ItemManifest) {
|
||||
refreshExpiry(offer.Expiry);
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
return originalManifest;
|
||||
};
|
||||
|
||||
const refreshExpiry = (expiry: IMongoDate): void => {
|
||||
const period = parseInt(expiry.$date.$numberLong);
|
||||
if (Date.now() >= period) {
|
||||
const epoch = 1734307200_000; // Monday (for weekly schedules)
|
||||
const iteration = Math.trunc((Date.now() - epoch) / period);
|
||||
const start = epoch + iteration * period;
|
||||
const end = start + period;
|
||||
expiry.$date.$numberLong = end.toString();
|
||||
}
|
||||
};
|
||||
|
||||
const toRange = (value: IRange | number): IRange => {
|
||||
if (typeof value == "number") {
|
||||
return { minValue: value, maxValue: value };
|
||||
@ -228,9 +148,54 @@ const toRange = (value: IRange | number): IRange => {
|
||||
return value;
|
||||
};
|
||||
|
||||
const getCycleDurationRange = (manifest: IVendor): IRange | undefined => {
|
||||
const res: IRange = { minValue: Number.MAX_SAFE_INTEGER, maxValue: 0 };
|
||||
for (const offer of manifest.items) {
|
||||
if (offer.durationHours) {
|
||||
const range = toRange(offer.durationHours);
|
||||
if (res.minValue > range.minValue) {
|
||||
res.minValue = range.minValue;
|
||||
}
|
||||
if (res.maxValue < range.maxValue) {
|
||||
res.maxValue = range.maxValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
return res.maxValue != 0 ? res : undefined;
|
||||
};
|
||||
|
||||
type TOfferId = string;
|
||||
|
||||
const getOfferId = (offer: IVendorOffer | IItemManifest): TOfferId => {
|
||||
if ("storeItem" in offer) {
|
||||
// IVendorOffer
|
||||
return offer.storeItem + "x" + offer.quantity;
|
||||
} else {
|
||||
// IItemManifest
|
||||
return offer.StoreItem + "x" + offer.QuantityMultiplier;
|
||||
}
|
||||
};
|
||||
|
||||
let vendorManifestsUsingFullStock = false;
|
||||
const vendorManifestCache: Record<string, IVendorManifest> = {};
|
||||
|
||||
const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifest => {
|
||||
const clearVendorCache = (): void => {
|
||||
for (const k of Object.keys(vendorManifestCache)) {
|
||||
delete vendorManifestCache[k];
|
||||
}
|
||||
};
|
||||
|
||||
const generateVendorManifest = (
|
||||
vendorInfo: IGeneratableVendorInfo,
|
||||
fullStock: boolean | undefined
|
||||
): IVendorManifest => {
|
||||
fullStock ??= config.fullyStockedVendors;
|
||||
fullStock ??= false;
|
||||
if (vendorManifestsUsingFullStock != fullStock) {
|
||||
vendorManifestsUsingFullStock = fullStock;
|
||||
clearVendorCache();
|
||||
}
|
||||
|
||||
if (!(vendorInfo.TypeName in vendorManifestCache)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { cycleOffset, cycleDuration, ...clientVendorInfo } = vendorInfo;
|
||||
@ -244,10 +209,16 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
|
||||
}
|
||||
const cacheEntry = vendorManifestCache[vendorInfo.TypeName];
|
||||
const info = cacheEntry.VendorInfo;
|
||||
if (Date.now() >= parseInt(info.Expiry.$date.$numberLong)) {
|
||||
const manifest = ExportVendors[vendorInfo.TypeName];
|
||||
const cycleDurationRange = getCycleDurationRange(manifest);
|
||||
let now = Date.now();
|
||||
if (cycleDurationRange && cycleDurationRange.minValue != cycleDurationRange.maxValue) {
|
||||
now -= (cycleDurationRange.maxValue - 1) * unixTimesInMs.hour;
|
||||
}
|
||||
while (Date.now() >= parseInt(info.Expiry.$date.$numberLong)) {
|
||||
// Remove expired offers
|
||||
for (let i = 0; i != info.ItemManifest.length; ) {
|
||||
if (Date.now() >= parseInt(info.ItemManifest[i].Expiry.$date.$numberLong)) {
|
||||
if (now >= parseInt(info.ItemManifest[i].Expiry.$date.$numberLong)) {
|
||||
info.ItemManifest.splice(i, 1);
|
||||
} else {
|
||||
++i;
|
||||
@ -258,62 +229,121 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
|
||||
const vendorSeed = parseInt(vendorInfo._id.$oid.substring(16), 16);
|
||||
const cycleOffset = vendorInfo.cycleOffset ?? 1734307200_000;
|
||||
const cycleDuration = vendorInfo.cycleDuration;
|
||||
const cycleIndex = Math.trunc((Date.now() - cycleOffset) / cycleDuration);
|
||||
const cycleIndex = Math.trunc((now - cycleOffset) / cycleDuration);
|
||||
const rng = new SRng(mixSeeds(vendorSeed, cycleIndex));
|
||||
const manifest = ExportVendors[vendorInfo.TypeName];
|
||||
const offersToAdd = [];
|
||||
if (
|
||||
manifest.numItems &&
|
||||
(manifest.numItems.minValue != manifest.numItems.maxValue ||
|
||||
manifest.items.length != manifest.numItems.minValue) &&
|
||||
!manifest.isOneBinPerCycle
|
||||
) {
|
||||
const remainingItemCapacity: Record<string, number> = {};
|
||||
for (const item of manifest.items) {
|
||||
remainingItemCapacity[item.storeItem] = 1 + item.duplicates;
|
||||
}
|
||||
for (const offer of info.ItemManifest) {
|
||||
remainingItemCapacity[offer.StoreItem] -= 1;
|
||||
}
|
||||
const numItemsTarget = rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue);
|
||||
while (info.ItemManifest.length + offersToAdd.length < numItemsTarget) {
|
||||
// TODO: Consider per-bin item limits
|
||||
// TODO: Consider item probability weightings
|
||||
const item = rng.randomElement(manifest.items)!;
|
||||
if (remainingItemCapacity[item.storeItem] != 0) {
|
||||
remainingItemCapacity[item.storeItem] -= 1;
|
||||
offersToAdd.push(item);
|
||||
}
|
||||
const offersToAdd: IVendorOffer[] = [];
|
||||
if (manifest.isOneBinPerCycle) {
|
||||
if (fullStock) {
|
||||
for (const rawItem of manifest.items) {
|
||||
offersToAdd.push(rawItem);
|
||||
}
|
||||
} else {
|
||||
let binThisCycle;
|
||||
if (manifest.isOneBinPerCycle) {
|
||||
binThisCycle = cycleIndex % 2; // Note: May want to auto-compute the bin size, but this is only used for coda weapons right now.
|
||||
}
|
||||
const binThisCycle = cycleIndex % 2; // Note: May want to check the actual number of bins, but this is only used for coda weapons right now.
|
||||
for (const rawItem of manifest.items) {
|
||||
if (!manifest.isOneBinPerCycle || rawItem.bin == binThisCycle) {
|
||||
if (rawItem.bin == binThisCycle) {
|
||||
offersToAdd.push(rawItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Compute vendor requirements, subtracting existing offers
|
||||
const remainingItemCapacity: Record<TOfferId, number> = {};
|
||||
const missingItemsPerBin: Record<number, number> = {};
|
||||
let numOffersThatNeedToMatchABin = 0;
|
||||
if (manifest.numItemsPerBin) {
|
||||
for (let bin = 0; bin != manifest.numItemsPerBin.length; ++bin) {
|
||||
missingItemsPerBin[bin] = manifest.numItemsPerBin[bin];
|
||||
numOffersThatNeedToMatchABin += manifest.numItemsPerBin[bin];
|
||||
}
|
||||
}
|
||||
for (const item of manifest.items) {
|
||||
remainingItemCapacity[getOfferId(item)] = 1 + item.duplicates;
|
||||
}
|
||||
for (const offer of info.ItemManifest) {
|
||||
remainingItemCapacity[getOfferId(offer)] -= 1;
|
||||
const bin = parseInt(offer.Bin.substring(4));
|
||||
if (missingItemsPerBin[bin]) {
|
||||
missingItemsPerBin[bin] -= 1;
|
||||
numOffersThatNeedToMatchABin -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
// For most vendors, the offers seem to roughly be in reverse order from the manifest. Coda weapons are an odd exception.
|
||||
if (!manifest.isOneBinPerCycle) {
|
||||
offersToAdd.reverse();
|
||||
// Add permanent offers
|
||||
let numUncountedOffers = 0;
|
||||
let numCountedOffers = 0;
|
||||
let offset = 0;
|
||||
for (const item of manifest.items) {
|
||||
if (item.alwaysOffered || item.rotatedWeekly) {
|
||||
++numUncountedOffers;
|
||||
const id = getOfferId(item);
|
||||
if (remainingItemCapacity[id] != 0) {
|
||||
remainingItemCapacity[id] -= 1;
|
||||
offersToAdd.push(item);
|
||||
++offset;
|
||||
}
|
||||
if (missingItemsPerBin[item.bin]) {
|
||||
missingItemsPerBin[item.bin] -= 1;
|
||||
numOffersThatNeedToMatchABin -= 1;
|
||||
}
|
||||
} else {
|
||||
numCountedOffers += 1 + item.duplicates;
|
||||
}
|
||||
}
|
||||
|
||||
// Add counted offers
|
||||
const useRng =
|
||||
manifest.numItems &&
|
||||
(manifest.numItems.minValue != manifest.numItems.maxValue ||
|
||||
manifest.numItems.minValue != numCountedOffers);
|
||||
const numItemsTarget = fullStock
|
||||
? numUncountedOffers + numCountedOffers
|
||||
: manifest.numItems
|
||||
? numUncountedOffers +
|
||||
(useRng
|
||||
? rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue)
|
||||
: manifest.numItems.minValue)
|
||||
: manifest.items.length;
|
||||
let i = 0;
|
||||
const rollableOffers = manifest.items.filter(x => x.probability !== undefined) as (Omit<
|
||||
IVendorOffer,
|
||||
"probability"
|
||||
> & { probability: number })[];
|
||||
while (info.ItemManifest.length + offersToAdd.length < numItemsTarget) {
|
||||
const item = useRng ? rng.randomReward(rollableOffers)! : rollableOffers[i++];
|
||||
if (
|
||||
remainingItemCapacity[getOfferId(item)] != 0 &&
|
||||
(numOffersThatNeedToMatchABin == 0 || missingItemsPerBin[item.bin])
|
||||
) {
|
||||
remainingItemCapacity[getOfferId(item)] -= 1;
|
||||
if (missingItemsPerBin[item.bin]) {
|
||||
missingItemsPerBin[item.bin] -= 1;
|
||||
numOffersThatNeedToMatchABin -= 1;
|
||||
}
|
||||
offersToAdd.splice(offset, 0, item);
|
||||
}
|
||||
if (i == rollableOffers.length) {
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
const cycleStart = cycleOffset + cycleIndex * cycleDuration;
|
||||
for (const rawItem of offersToAdd) {
|
||||
const durationHoursRange = toRange(rawItem.durationHours ?? cycleDuration);
|
||||
const expiry =
|
||||
cycleStart +
|
||||
rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour;
|
||||
const expiry = rawItem.alwaysOffered
|
||||
? 2051240400_000
|
||||
: cycleStart +
|
||||
(rawItem.rotatedWeekly
|
||||
? unixTimesInMs.week
|
||||
: rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour);
|
||||
const item: IItemManifest = {
|
||||
StoreItem: rawItem.storeItem,
|
||||
ItemPrices: rawItem.itemPrices?.map(itemPrice => ({ ...itemPrice, ProductCategory: "MiscItems" })),
|
||||
Bin: "BIN_" + rawItem.bin,
|
||||
QuantityMultiplier: 1,
|
||||
QuantityMultiplier: rawItem.quantity,
|
||||
Expiry: { $date: { $numberLong: expiry.toString() } },
|
||||
AllowMultipurchase: false,
|
||||
PurchaseQuantityLimit: rawItem.purchaseLimit,
|
||||
RotatedWeekly: rawItem.rotatedWeekly,
|
||||
AllowMultipurchase: rawItem.purchaseLimit !== 1,
|
||||
Id: {
|
||||
$oid:
|
||||
((cycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
|
||||
@ -322,7 +352,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
|
||||
}
|
||||
};
|
||||
if (rawItem.numRandomItemPrices) {
|
||||
item.ItemPrices = [];
|
||||
item.ItemPrices ??= [];
|
||||
for (let i = 0; i != rawItem.numRandomItemPrices; ++i) {
|
||||
let itemPrice: { type: string; count: IRange };
|
||||
do {
|
||||
@ -362,6 +392,14 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
|
||||
info.ItemManifest.push(item);
|
||||
}
|
||||
|
||||
if (manifest.numItemsPerBin) {
|
||||
info.ItemManifest.sort((a, b) => {
|
||||
const aBin = parseInt(a.Bin.substring(4));
|
||||
const bBin = parseInt(b.Bin.substring(4));
|
||||
return aBin == bBin ? 0 : aBin < bBin ? +1 : -1;
|
||||
});
|
||||
}
|
||||
|
||||
// Update vendor expiry
|
||||
let soonestOfferExpiry: number = Number.MAX_SAFE_INTEGER;
|
||||
for (const offer of info.ItemManifest) {
|
||||
@ -371,13 +409,27 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
|
||||
}
|
||||
}
|
||||
info.Expiry.$date.$numberLong = soonestOfferExpiry.toString();
|
||||
|
||||
now += unixTimesInMs.hour;
|
||||
}
|
||||
return cacheEntry;
|
||||
};
|
||||
|
||||
if (isDev) {
|
||||
const ads = getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest")!
|
||||
.VendorInfo.ItemManifest;
|
||||
if (args.dev) {
|
||||
if (
|
||||
getCycleDuration(ExportVendors["/Lotus/Types/Game/VendorManifests/Hubs/TeshinHardModeVendorManifest"]) !=
|
||||
unixTimesInMs.week
|
||||
) {
|
||||
logger.warn(`getCycleDuration self test failed`);
|
||||
}
|
||||
|
||||
for (let i = 0; i != 2; ++i) {
|
||||
const fullStock = !!i;
|
||||
|
||||
const ads = getVendorManifestByTypeName(
|
||||
"/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest",
|
||||
fullStock
|
||||
)!.VendorInfo.ItemManifest;
|
||||
if (
|
||||
ads.length != 5 ||
|
||||
ads[0].Bin != "BIN_4" ||
|
||||
@ -388,4 +440,59 @@ if (isDev) {
|
||||
) {
|
||||
logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest`);
|
||||
}
|
||||
|
||||
const pall = getVendorManifestByTypeName(
|
||||
"/Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest",
|
||||
fullStock
|
||||
)!.VendorInfo.ItemManifest;
|
||||
if (
|
||||
pall.length != 5 ||
|
||||
pall[0].StoreItem != "/Lotus/StoreItems/Types/Items/ShipDecos/HarrowQuestKeyOrnament" ||
|
||||
pall[1].StoreItem != "/Lotus/StoreItems/Types/BoosterPacks/RivenModPack" ||
|
||||
pall[2].StoreItem != "/Lotus/StoreItems/Types/StoreItems/CreditBundles/150000Credits" ||
|
||||
pall[3].StoreItem != "/Lotus/StoreItems/Types/Items/MiscItems/Kuva" ||
|
||||
pall[4].StoreItem != "/Lotus/StoreItems/Types/BoosterPacks/RivenModPack"
|
||||
) {
|
||||
logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest`);
|
||||
}
|
||||
}
|
||||
|
||||
const cms = getVendorManifestByTypeName(
|
||||
"/Lotus/Types/Game/VendorManifests/Hubs/RailjackCrewMemberVendorManifest",
|
||||
false
|
||||
)!.VendorInfo.ItemManifest;
|
||||
if (
|
||||
cms.length != 9 ||
|
||||
cms[0].Bin != "BIN_2" ||
|
||||
cms[8].Bin != "BIN_0" ||
|
||||
cms.reduce((a, x) => a + (x.Bin == "BIN_2" ? 1 : 0), 0) < 2 ||
|
||||
cms.reduce((a, x) => a + (x.Bin == "BIN_1" ? 1 : 0), 0) < 2 ||
|
||||
cms.reduce((a, x) => a + (x.Bin == "BIN_0" ? 1 : 0), 0) < 4
|
||||
) {
|
||||
logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/RailjackCrewMemberVendorManifest`);
|
||||
}
|
||||
|
||||
const temple = getVendorManifestByTypeName(
|
||||
"/Lotus/Types/Game/VendorManifests/TheHex/Temple1999VendorManifest",
|
||||
false
|
||||
)!.VendorInfo.ItemManifest;
|
||||
if (!temple.find(x => x.StoreItem == "/Lotus/StoreItems/Types/Items/MiscItems/Kuva")) {
|
||||
logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/TheHex/Temple1999VendorManifest`);
|
||||
}
|
||||
|
||||
const nakak = getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Ostron/MaskSalesmanManifest", false)!
|
||||
.VendorInfo.ItemManifest;
|
||||
if (
|
||||
nakak.length != 10 ||
|
||||
nakak[0].StoreItem != "/Lotus/StoreItems/Upgrades/Skins/Ostron/RevenantMask" ||
|
||||
nakak[1].StoreItem != "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyThumper" ||
|
||||
nakak[1].ItemPrices?.length != 4 ||
|
||||
nakak[2].StoreItem != "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyThumperMedium" ||
|
||||
nakak[2].ItemPrices?.length != 4 ||
|
||||
nakak[3].StoreItem != "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyThumperLarge" ||
|
||||
nakak[3].ItemPrices?.length != 4
|
||||
// The remaining offers should be computed by weighted RNG.
|
||||
) {
|
||||
logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Ostron/MaskSalesmanManifest`);
|
||||
}
|
||||
}
|
||||
|
@ -4,15 +4,18 @@ import {
|
||||
ISetShipCustomizationsRequest,
|
||||
IShipDecorationsRequest,
|
||||
IShipDecorationsResponse,
|
||||
ISetPlacedDecoInfoRequest
|
||||
ISetPlacedDecoInfoRequest,
|
||||
TBootLocation
|
||||
} from "@/src/types/shipTypes";
|
||||
import { logger } from "@/src/utils/logger";
|
||||
import { Types } from "mongoose";
|
||||
import { addShipDecorations, getInventory } from "./inventoryService";
|
||||
import { addFusionTreasures, addShipDecorations, getInventory } from "./inventoryService";
|
||||
import { config } from "./configService";
|
||||
import { Guild } from "../models/guildModel";
|
||||
import { hasGuildPermission } from "./guildService";
|
||||
import { GuildPermission } from "../types/guildTypes";
|
||||
import { ExportResources } from "warframe-public-export-plus";
|
||||
import { RoomsType, TPersonalRoomsDatabaseDocument } from "../types/personalRoomsTypes";
|
||||
|
||||
export const setShipCustomizations = async (
|
||||
accountId: string,
|
||||
@ -58,7 +61,16 @@ export const handleSetShipDecorations = async (
|
||||
const roomToPlaceIn = rooms.find(room => room.Name === placedDecoration.Room);
|
||||
|
||||
if (!roomToPlaceIn) {
|
||||
throw new Error("room not found");
|
||||
throw new Error(`unknown room: ${placedDecoration.Room}`);
|
||||
}
|
||||
|
||||
const entry = Object.entries(ExportResources).find(arr => arr[1].deco == placedDecoration.Type);
|
||||
if (!entry) {
|
||||
throw new Error(`unknown deco type: ${placedDecoration.Type}`);
|
||||
}
|
||||
const [itemType, meta] = entry;
|
||||
if (meta.capacityCost === undefined) {
|
||||
throw new Error(`unknown deco type: ${placedDecoration.Type}`);
|
||||
}
|
||||
|
||||
if (placedDecoration.MoveId) {
|
||||
@ -82,7 +94,7 @@ export const handleSetShipDecorations = async (
|
||||
OldRoom: placedDecoration.OldRoom,
|
||||
NewRoom: placedDecoration.Room,
|
||||
IsApartment: placedDecoration.IsApartment,
|
||||
MaxCapacityIncrease: 0 // TODO: calculate capacity change upon removal
|
||||
MaxCapacityIncrease: 0
|
||||
};
|
||||
}
|
||||
|
||||
@ -95,33 +107,44 @@ export const handleSetShipDecorations = async (
|
||||
}
|
||||
|
||||
oldRoom.PlacedDecos.pull({ _id: placedDecoration.MoveId });
|
||||
oldRoom.MaxCapacity += meta.capacityCost;
|
||||
|
||||
const newDecoration = {
|
||||
Type: placedDecoration.Type,
|
||||
Pos: placedDecoration.Pos,
|
||||
Rot: placedDecoration.Rot,
|
||||
Scale: placedDecoration.Scale,
|
||||
Sockets: placedDecoration.Sockets,
|
||||
_id: placedDecoration.MoveId
|
||||
};
|
||||
|
||||
//the new room is still roomToPlaceIn
|
||||
roomToPlaceIn.PlacedDecos.push(newDecoration);
|
||||
roomToPlaceIn.MaxCapacity -= meta.capacityCost;
|
||||
|
||||
await personalRooms.save();
|
||||
return {
|
||||
OldRoom: placedDecoration.OldRoom,
|
||||
NewRoom: placedDecoration.Room,
|
||||
IsApartment: placedDecoration.IsApartment,
|
||||
MaxCapacityIncrease: 0 // TODO: calculate capacity change upon removal
|
||||
MaxCapacityIncrease: -meta.capacityCost
|
||||
};
|
||||
}
|
||||
|
||||
if (placedDecoration.RemoveId) {
|
||||
roomToPlaceIn.PlacedDecos.pull({ _id: placedDecoration.RemoveId });
|
||||
const decoIndex = roomToPlaceIn.PlacedDecos.findIndex(x => x._id.equals(placedDecoration.RemoveId));
|
||||
const deco = roomToPlaceIn.PlacedDecos[decoIndex];
|
||||
roomToPlaceIn.PlacedDecos.splice(decoIndex, 1);
|
||||
roomToPlaceIn.MaxCapacity += meta.capacityCost;
|
||||
await personalRooms.save();
|
||||
|
||||
if (!config.unlockAllShipDecorations) {
|
||||
const inventory = await getInventory(accountId);
|
||||
addShipDecorations(inventory, [{ ItemType: placedDecoration.Type, ItemCount: 1 }]);
|
||||
if (deco.Sockets !== undefined) {
|
||||
addFusionTreasures(inventory, [{ ItemType: itemType, Sockets: deco.Sockets, ItemCount: 1 }]);
|
||||
} else {
|
||||
addShipDecorations(inventory, [{ ItemType: itemType, ItemCount: 1 }]);
|
||||
}
|
||||
await inventory.save();
|
||||
}
|
||||
|
||||
@ -129,17 +152,20 @@ export const handleSetShipDecorations = async (
|
||||
DecoId: placedDecoration.RemoveId,
|
||||
Room: placedDecoration.Room,
|
||||
IsApartment: placedDecoration.IsApartment,
|
||||
MaxCapacityIncrease: 0
|
||||
MaxCapacityIncrease: 0 // Client already implies the capacity being refunded.
|
||||
};
|
||||
} else {
|
||||
if (!config.unlockAllShipDecorations) {
|
||||
const inventory = await getInventory(accountId);
|
||||
addShipDecorations(inventory, [{ ItemType: placedDecoration.Type, ItemCount: -1 }]);
|
||||
await inventory.save();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: handle capacity
|
||||
if (!config.unlockAllShipDecorations) {
|
||||
const inventory = await getInventory(accountId);
|
||||
const itemType = Object.entries(ExportResources).find(arr => arr[1].deco == placedDecoration.Type)![0];
|
||||
if (placedDecoration.Sockets !== undefined) {
|
||||
addFusionTreasures(inventory, [{ ItemType: itemType, Sockets: placedDecoration.Sockets, ItemCount: -1 }]);
|
||||
} else {
|
||||
addShipDecorations(inventory, [{ ItemType: itemType, ItemCount: -1 }]);
|
||||
}
|
||||
await inventory.save();
|
||||
}
|
||||
|
||||
//place decoration
|
||||
const decoId = new Types.ObjectId();
|
||||
@ -148,12 +174,32 @@ export const handleSetShipDecorations = async (
|
||||
Pos: placedDecoration.Pos,
|
||||
Rot: placedDecoration.Rot,
|
||||
Scale: placedDecoration.Scale,
|
||||
Sockets: placedDecoration.Sockets,
|
||||
_id: decoId
|
||||
});
|
||||
roomToPlaceIn.MaxCapacity -= meta.capacityCost;
|
||||
|
||||
await personalRooms.save();
|
||||
|
||||
return { DecoId: decoId.toString(), Room: placedDecoration.Room, IsApartment: placedDecoration.IsApartment };
|
||||
return {
|
||||
DecoId: decoId.toString(),
|
||||
Room: placedDecoration.Room,
|
||||
IsApartment: placedDecoration.IsApartment,
|
||||
MaxCapacityIncrease: -meta.capacityCost
|
||||
};
|
||||
};
|
||||
|
||||
const getRoomsForBootLocation = (
|
||||
personalRooms: TPersonalRoomsDatabaseDocument,
|
||||
bootLocation: TBootLocation | undefined
|
||||
): RoomsType[] => {
|
||||
if (bootLocation == "SHOP") {
|
||||
return personalRooms.TailorShop.Rooms;
|
||||
}
|
||||
if (bootLocation == "APARTMENT") {
|
||||
return personalRooms.Apartment.Rooms;
|
||||
}
|
||||
return personalRooms.Ship.Rooms;
|
||||
};
|
||||
|
||||
export const handleSetPlacedDecoInfo = async (accountId: string, req: ISetPlacedDecoInfoRequest): Promise<void> => {
|
||||
@ -170,14 +216,14 @@ export const handleSetPlacedDecoInfo = async (accountId: string, req: ISetPlaced
|
||||
|
||||
const personalRooms = await getPersonalRooms(accountId);
|
||||
|
||||
const room = personalRooms.Ship.Rooms.find(room => room.Name === req.Room);
|
||||
const room = getRoomsForBootLocation(personalRooms, req.BootLocation).find(room => room.Name === req.Room);
|
||||
if (!room) {
|
||||
throw new Error("room not found");
|
||||
throw new Error(`unknown room: ${req.Room}`);
|
||||
}
|
||||
|
||||
const placedDeco = room.PlacedDecos.id(req.DecoId);
|
||||
if (!placedDeco) {
|
||||
throw new Error("deco not found");
|
||||
throw new Error(`unknown deco id: ${req.DecoId}`);
|
||||
}
|
||||
|
||||
placedDeco.PictureFrameInfo = req.PictureFrameInfo;
|
||||
|
@ -5,9 +5,17 @@ import { config } from "./configService";
|
||||
import { logger } from "../utils/logger";
|
||||
import { app } from "../app";
|
||||
import { AddressInfo } from "node:net";
|
||||
import ws from "ws";
|
||||
import { Account } from "../models/loginModel";
|
||||
import { createAccount, createNonce, getUsernameFromEmail, isCorrectPassword } from "./loginService";
|
||||
import { IDatabaseAccountJson } from "../types/loginTypes";
|
||||
import { HydratedDocument } from "mongoose";
|
||||
import { Agent, WebSocket as UnidiciWebSocket } from "undici";
|
||||
|
||||
let httpServer: http.Server | undefined;
|
||||
let httpsServer: https.Server | undefined;
|
||||
let wsServer: ws.Server | undefined;
|
||||
let wssServer: ws.Server | undefined;
|
||||
|
||||
const tlsOptions = {
|
||||
key: fs.readFileSync("static/certs/key.pem"),
|
||||
@ -21,17 +29,63 @@ export const startWebServer = (): void => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
httpServer = http.createServer(app);
|
||||
httpServer.listen(httpPort, () => {
|
||||
wsServer = new ws.Server({ server: httpServer });
|
||||
wsServer.on("connection", wsOnConnect);
|
||||
|
||||
logger.info("HTTP server started on port " + httpPort);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
httpsServer = https.createServer(tlsOptions, app);
|
||||
httpsServer.listen(httpsPort, () => {
|
||||
wssServer = new ws.Server({ server: httpsServer });
|
||||
wssServer.on("connection", wsOnConnect);
|
||||
|
||||
logger.info("HTTPS server started on port " + httpsPort);
|
||||
|
||||
logger.info(
|
||||
"Access the WebUI in your browser at http://localhost" + (httpPort == 80 ? "" : ":" + httpPort)
|
||||
);
|
||||
|
||||
void runWsSelfTest("wss", httpsPort).then(ok => {
|
||||
if (!ok) {
|
||||
logger.warn(`WSS self-test failed. The server may not actually be reachable at port ${httpsPort}.`);
|
||||
if (process.platform == "win32") {
|
||||
logger.warn(
|
||||
`You can check who actually has that port via powershell: Get-Process -Id (Get-NetTCPConnection -LocalPort ${httpsPort}).OwningProcess`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const runWsSelfTest = (protocol: "ws" | "wss", port: number): Promise<boolean> => {
|
||||
return new Promise(resolve => {
|
||||
// https://github.com/oven-sh/bun/issues/20547
|
||||
if (process.versions.bun) {
|
||||
const client = new WebSocket(`${protocol}://localhost:${port}/custom/selftest`, {
|
||||
tls: { rejectUnauthorized: false }
|
||||
} as unknown as string);
|
||||
client.onmessage = (e): void => {
|
||||
resolve(e.data == "SpaceNinjaServer");
|
||||
};
|
||||
client.onerror = client.onclose = (): void => {
|
||||
resolve(false);
|
||||
};
|
||||
} else {
|
||||
const agent = new Agent({ connect: { rejectUnauthorized: false } });
|
||||
const client = new UnidiciWebSocket(`${protocol}://localhost:${port}/custom/selftest`, {
|
||||
dispatcher: agent
|
||||
});
|
||||
client.onmessage = (e): void => {
|
||||
resolve(e.data == "SpaceNinjaServer");
|
||||
};
|
||||
client.onerror = client.onclose = (): void => {
|
||||
resolve(false);
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const getWebPorts = (): Record<"http" | "https", number | undefined> => {
|
||||
@ -61,5 +115,182 @@ export const stopWebServer = async (): Promise<void> => {
|
||||
})
|
||||
);
|
||||
}
|
||||
if (wsServer) {
|
||||
promises.push(
|
||||
new Promise(resolve => {
|
||||
wsServer!.close(() => {
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
if (wssServer) {
|
||||
promises.push(
|
||||
new Promise(resolve => {
|
||||
wssServer!.close(() => {
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
let lastWsid: number = 0;
|
||||
|
||||
interface IWsCustomData extends ws {
|
||||
id?: number;
|
||||
accountId?: string;
|
||||
}
|
||||
|
||||
interface IWsMsgFromClient {
|
||||
auth?: {
|
||||
email: string;
|
||||
password: string;
|
||||
isRegister: boolean;
|
||||
};
|
||||
logout?: boolean;
|
||||
}
|
||||
|
||||
interface IWsMsgToClient {
|
||||
//wsid?: number;
|
||||
reload?: boolean;
|
||||
ports?: {
|
||||
http: number | undefined;
|
||||
https: number | undefined;
|
||||
};
|
||||
config_reloaded?: boolean;
|
||||
auth_succ?: {
|
||||
id: string;
|
||||
DisplayName: string;
|
||||
Nonce: number;
|
||||
};
|
||||
auth_fail?: {
|
||||
isRegister: boolean;
|
||||
};
|
||||
logged_out?: boolean;
|
||||
update_inventory?: boolean;
|
||||
}
|
||||
|
||||
const wsOnConnect = (ws: ws, req: http.IncomingMessage): void => {
|
||||
if (req.url == "/custom/selftest") {
|
||||
ws.send("SpaceNinjaServer");
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
(ws as IWsCustomData).id = ++lastWsid;
|
||||
ws.send(JSON.stringify({ wsid: lastWsid }));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
ws.on("message", async msg => {
|
||||
const data = JSON.parse(String(msg)) as IWsMsgFromClient;
|
||||
if (data.auth) {
|
||||
let account: IDatabaseAccountJson | null = await Account.findOne({ email: data.auth.email });
|
||||
if (account) {
|
||||
if (isCorrectPassword(data.auth.password, account.password)) {
|
||||
if (!account.Nonce) {
|
||||
account.ClientType = "webui";
|
||||
account.Nonce = createNonce();
|
||||
await (account as HydratedDocument<IDatabaseAccountJson>).save();
|
||||
}
|
||||
} else {
|
||||
account = null;
|
||||
}
|
||||
} else if (data.auth.isRegister) {
|
||||
const name = await getUsernameFromEmail(data.auth.email);
|
||||
account = await createAccount({
|
||||
email: data.auth.email,
|
||||
password: data.auth.password,
|
||||
ClientType: "webui",
|
||||
LastLogin: new Date(),
|
||||
DisplayName: name,
|
||||
Nonce: createNonce()
|
||||
});
|
||||
}
|
||||
if (account) {
|
||||
(ws as IWsCustomData).accountId = account.id;
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
auth_succ: {
|
||||
id: account.id,
|
||||
DisplayName: account.DisplayName,
|
||||
Nonce: account.Nonce
|
||||
}
|
||||
} satisfies IWsMsgToClient)
|
||||
);
|
||||
} else {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
auth_fail: {
|
||||
isRegister: data.auth.isRegister
|
||||
}
|
||||
} satisfies IWsMsgToClient)
|
||||
);
|
||||
}
|
||||
}
|
||||
if (data.logout) {
|
||||
const accountId = (ws as IWsCustomData).accountId;
|
||||
(ws as IWsCustomData).accountId = undefined;
|
||||
await Account.updateOne(
|
||||
{
|
||||
_id: accountId,
|
||||
ClientType: "webui"
|
||||
},
|
||||
{
|
||||
Nonce: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const sendWsBroadcast = (data: IWsMsgToClient): void => {
|
||||
const msg = JSON.stringify(data);
|
||||
if (wsServer) {
|
||||
for (const client of wsServer.clients) {
|
||||
client.send(msg);
|
||||
}
|
||||
}
|
||||
if (wssServer) {
|
||||
for (const client of wssServer.clients) {
|
||||
client.send(msg);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const sendWsBroadcastTo = (accountId: string, data: IWsMsgToClient): void => {
|
||||
const msg = JSON.stringify(data);
|
||||
if (wsServer) {
|
||||
for (const client of wsServer.clients) {
|
||||
if ((client as IWsCustomData).accountId == accountId) {
|
||||
client.send(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wssServer) {
|
||||
for (const client of wssServer.clients) {
|
||||
if ((client as IWsCustomData).accountId == accountId) {
|
||||
client.send(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const sendWsBroadcastExcept = (wsid: number | undefined, data: IWsMsgToClient): void => {
|
||||
const msg = JSON.stringify(data);
|
||||
if (wsServer) {
|
||||
for (const client of wsServer.clients) {
|
||||
if ((client as IWsCustomData).id != wsid) {
|
||||
client.send(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wssServer) {
|
||||
for (const client of wssServer.clients) {
|
||||
if ((client as IWsCustomData).id != wsid) {
|
||||
client.send(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,12 +1,15 @@
|
||||
import staticWorldState from "@/static/fixed_responses/worldState/worldState.json";
|
||||
import baro from "@/static/fixed_responses/worldState/baro.json";
|
||||
import fissureMissions from "@/static/fixed_responses/worldState/fissureMissions.json";
|
||||
import sortieTilesets from "@/static/fixed_responses/worldState/sortieTilesets.json";
|
||||
import sortieTilesetMissions from "@/static/fixed_responses/worldState/sortieTilesetMissions.json";
|
||||
import syndicateMissions from "@/static/fixed_responses/worldState/syndicateMissions.json";
|
||||
import darvoDeals from "@/static/fixed_responses/worldState/darvoDeals.json";
|
||||
import { buildConfig } from "@/src/services/buildConfigService";
|
||||
import { unixTimesInMs } from "@/src/constants/timeConstants";
|
||||
import { config } from "@/src/services/configService";
|
||||
import { SRng } from "@/src/services/rngService";
|
||||
import { ExportRegions, ExportSyndicates, IRegion } from "warframe-public-export-plus";
|
||||
import { getRandomElement, getRandomInt, SRng } from "@/src/services/rngService";
|
||||
import { eMissionType, ExportRegions, ExportSyndicates, IRegion } from "warframe-public-export-plus";
|
||||
import {
|
||||
ICalendarDay,
|
||||
ICalendarEvent,
|
||||
@ -16,10 +19,16 @@ import {
|
||||
ISortie,
|
||||
ISortieMission,
|
||||
ISyndicateMissionInfo,
|
||||
IWorldState
|
||||
ITmp,
|
||||
IVoidStorm,
|
||||
IVoidTrader,
|
||||
IVoidTraderOffer,
|
||||
IWorldState,
|
||||
TCircuitGameMode
|
||||
} from "../types/worldStateTypes";
|
||||
import { version_compare } from "../helpers/inventoryHelpers";
|
||||
import { toMongoDate, toOid, version_compare } from "../helpers/inventoryHelpers";
|
||||
import { logger } from "../utils/logger";
|
||||
import { DailyDeal, Fissure } from "../models/worldStateModel";
|
||||
|
||||
const sortieBosses = [
|
||||
"SORTIE_BOSS_HYENA",
|
||||
@ -93,7 +102,7 @@ const sortieBossNode: Record<Exclude<TSortieBoss, "SORTIE_BOSS_CORRUPTED_VOR">,
|
||||
SORTIE_BOSS_VOR: "SolNode108"
|
||||
};
|
||||
|
||||
const eidolonJobs = [
|
||||
const eidolonJobs: readonly string[] = [
|
||||
"/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyAss",
|
||||
"/Lotus/Types/Gameplay/Eidolon/Jobs/AssassinateBountyCap",
|
||||
"/Lotus/Types/Gameplay/Eidolon/Jobs/AttritionBountySab",
|
||||
@ -109,14 +118,14 @@ const eidolonJobs = [
|
||||
"/Lotus/Types/Gameplay/Eidolon/Jobs/RescueBountyResc"
|
||||
];
|
||||
|
||||
const eidolonNarmerJobs = [
|
||||
const eidolonNarmerJobs: readonly string[] = [
|
||||
"/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AssassinateBountyAss",
|
||||
"/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AttritionBountyExt",
|
||||
"/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/ReclamationBountyTheft",
|
||||
"/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AttritionBountyLib"
|
||||
];
|
||||
|
||||
const venusJobs = [
|
||||
const venusJobs: readonly string[] = [
|
||||
"/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobAmbush",
|
||||
"/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobExcavation",
|
||||
"/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobRecovery",
|
||||
@ -142,14 +151,14 @@ const venusJobs = [
|
||||
"/Lotus/Types/Gameplay/Venus/Jobs/VenusWetworkJobSpy"
|
||||
];
|
||||
|
||||
const venusNarmerJobs = [
|
||||
const venusNarmerJobs: readonly string[] = [
|
||||
"/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobAssassinate",
|
||||
"/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusCullJobExterminate",
|
||||
"/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusPreservationJobDefense",
|
||||
"/Lotus/Types/Gameplay/Venus/Jobs/Narmer/NarmerVenusTheftJobExcavation"
|
||||
];
|
||||
|
||||
const microplanetJobs = [
|
||||
const microplanetJobs: readonly string[] = [
|
||||
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAreaDefenseBounty",
|
||||
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosAssassinateBounty",
|
||||
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosCrpSurvivorBounty",
|
||||
@ -159,7 +168,7 @@ const microplanetJobs = [
|
||||
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosPurifyBounty"
|
||||
];
|
||||
|
||||
const microplanetEndlessJobs = [
|
||||
const microplanetEndlessJobs: readonly string[] = [
|
||||
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessAreaDefenseBounty",
|
||||
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessExcavateBounty",
|
||||
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessPurifyBounty"
|
||||
@ -364,7 +373,7 @@ const getSeasonChallengePools = (syndicateTag: string): IRotatingSeasonChallenge
|
||||
hardWeekly: syndicate.weeklyChallenges!.filter(x =>
|
||||
x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/")
|
||||
),
|
||||
hasWeeklyPermanent: !!syndicate.weeklyChallenges!.find(x =>
|
||||
hasWeeklyPermanent: syndicate.weeklyChallenges!.some(x =>
|
||||
x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")
|
||||
)
|
||||
};
|
||||
@ -453,19 +462,44 @@ const pushWeeklyActs = (
|
||||
}
|
||||
};
|
||||
|
||||
const generateXpAmounts = (rng: SRng, stageCount: number, minXp: number, maxXp: number): number[] => {
|
||||
const step = minXp < 1000 ? 1 : 10;
|
||||
const totalDeciXp = rng.randomInt(minXp / step, maxXp / step);
|
||||
const xpAmounts: number[] = [];
|
||||
if (stageCount < 4) {
|
||||
const perStage = Math.ceil(totalDeciXp / stageCount) * step;
|
||||
for (let i = 0; i != stageCount; ++i) {
|
||||
xpAmounts.push(perStage);
|
||||
}
|
||||
} else {
|
||||
const perStage = Math.ceil(Math.round(totalDeciXp * 0.667) / (stageCount - 1)) * step;
|
||||
for (let i = 0; i != stageCount - 1; ++i) {
|
||||
xpAmounts.push(perStage);
|
||||
}
|
||||
xpAmounts.push(Math.ceil(totalDeciXp * 0.332) * step);
|
||||
}
|
||||
return xpAmounts;
|
||||
};
|
||||
// Test vectors:
|
||||
//console.log(generateXpAmounts(new SRng(1337n), 5, 5000, 5000)); // [840, 840, 840, 840, 1660]
|
||||
//console.log(generateXpAmounts(new SRng(1337n), 3, 40, 40)); // [14, 14, 14]
|
||||
//console.log(generateXpAmounts(new SRng(1337n), 5, 150, 150)); // [25, 25, 25, 25, 50]
|
||||
//console.log(generateXpAmounts(new SRng(1337n), 4, 10, 10)); // [2, 2, 2, 4]
|
||||
//console.log(generateXpAmounts(new SRng(1337n), 4, 15, 15)); // [4, 4, 4, 5]
|
||||
//console.log(generateXpAmounts(new SRng(1337n), 4, 20, 20)); // [5, 5, 5, 7]
|
||||
|
||||
export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[], bountyCycle: number): void => {
|
||||
const table = String.fromCharCode(65 + (bountyCycle % 3));
|
||||
const vaultTable = String.fromCharCode(65 + ((bountyCycle + 1) % 3));
|
||||
const deimosDTable = String.fromCharCode(65 + (bountyCycle % 2));
|
||||
|
||||
// TODO: xpAmounts need to be calculated based on the jobType somehow?
|
||||
|
||||
const seed = new SRng(bountyCycle).randomInt(0, 100_000);
|
||||
const bountyCycleStart = bountyCycle * 9000000;
|
||||
const bountyCycleEnd = bountyCycleStart + 9000000;
|
||||
|
||||
{
|
||||
const rng = new SRng(seed);
|
||||
const pool = [...eidolonJobs];
|
||||
syndicateMissions.push({
|
||||
_id: {
|
||||
$oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000008"
|
||||
@ -477,47 +511,47 @@ export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[],
|
||||
Nodes: [],
|
||||
Jobs: [
|
||||
{
|
||||
jobType: rng.randomElement(eidolonJobs),
|
||||
jobType: rng.randomElementPop(pool),
|
||||
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierATable${table}Rewards`,
|
||||
masteryReq: 0,
|
||||
minEnemyLevel: 5,
|
||||
maxEnemyLevel: 15,
|
||||
xpAmounts: [430, 430, 430]
|
||||
xpAmounts: generateXpAmounts(rng, 3, 1000, 1500)
|
||||
},
|
||||
{
|
||||
jobType: rng.randomElement(eidolonJobs),
|
||||
jobType: rng.randomElementPop(pool),
|
||||
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierBTable${table}Rewards`,
|
||||
masteryReq: 1,
|
||||
minEnemyLevel: 10,
|
||||
maxEnemyLevel: 30,
|
||||
xpAmounts: [620, 620, 620]
|
||||
xpAmounts: generateXpAmounts(rng, 3, 1750, 2250)
|
||||
},
|
||||
{
|
||||
jobType: rng.randomElement(eidolonJobs),
|
||||
jobType: rng.randomElementPop(pool),
|
||||
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierCTable${table}Rewards`,
|
||||
masteryReq: 2,
|
||||
minEnemyLevel: 20,
|
||||
maxEnemyLevel: 40,
|
||||
xpAmounts: [670, 670, 670, 990]
|
||||
xpAmounts: generateXpAmounts(rng, 4, 2500, 3000)
|
||||
},
|
||||
{
|
||||
jobType: rng.randomElement(eidolonJobs),
|
||||
jobType: rng.randomElementPop(pool),
|
||||
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierDTable${table}Rewards`,
|
||||
masteryReq: 3,
|
||||
minEnemyLevel: 30,
|
||||
maxEnemyLevel: 50,
|
||||
xpAmounts: [570, 570, 570, 570, 1110]
|
||||
xpAmounts: generateXpAmounts(rng, 5, 3250, 3750)
|
||||
},
|
||||
{
|
||||
jobType: rng.randomElement(eidolonJobs),
|
||||
jobType: rng.randomElementPop(pool),
|
||||
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETable${table}Rewards`,
|
||||
masteryReq: 5,
|
||||
minEnemyLevel: 40,
|
||||
maxEnemyLevel: 60,
|
||||
xpAmounts: [740, 740, 740, 740, 1450]
|
||||
xpAmounts: generateXpAmounts(rng, 5, 4000, 4500)
|
||||
},
|
||||
{
|
||||
jobType: rng.randomElement(eidolonJobs),
|
||||
jobType: rng.randomElementPop(pool),
|
||||
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETable${table}Rewards`,
|
||||
masteryReq: 10,
|
||||
minEnemyLevel: 100,
|
||||
@ -530,7 +564,7 @@ export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[],
|
||||
masteryReq: 0,
|
||||
minEnemyLevel: 50,
|
||||
maxEnemyLevel: 70,
|
||||
xpAmounts: [840, 840, 840, 840, 1650]
|
||||
xpAmounts: generateXpAmounts(rng, 5, 4500, 5000)
|
||||
}
|
||||
]
|
||||
});
|
||||
@ -538,6 +572,7 @@ export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[],
|
||||
|
||||
{
|
||||
const rng = new SRng(seed);
|
||||
const pool = [...venusJobs];
|
||||
syndicateMissions.push({
|
||||
_id: {
|
||||
$oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000025"
|
||||
@ -549,47 +584,47 @@ export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[],
|
||||
Nodes: [],
|
||||
Jobs: [
|
||||
{
|
||||
jobType: rng.randomElement(venusJobs),
|
||||
jobType: rng.randomElementPop(pool),
|
||||
rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierATable${table}Rewards`,
|
||||
masteryReq: 0,
|
||||
minEnemyLevel: 5,
|
||||
maxEnemyLevel: 15,
|
||||
xpAmounts: [340, 340, 340]
|
||||
xpAmounts: generateXpAmounts(rng, 3, 1000, 1500)
|
||||
},
|
||||
{
|
||||
jobType: rng.randomElement(venusJobs),
|
||||
jobType: rng.randomElementPop(pool),
|
||||
rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierBTable${table}Rewards`,
|
||||
masteryReq: 1,
|
||||
minEnemyLevel: 10,
|
||||
maxEnemyLevel: 30,
|
||||
xpAmounts: [660, 660, 660]
|
||||
xpAmounts: generateXpAmounts(rng, 3, 1750, 2250)
|
||||
},
|
||||
{
|
||||
jobType: rng.randomElement(venusJobs),
|
||||
jobType: rng.randomElementPop(pool),
|
||||
rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierCTable${table}Rewards`,
|
||||
masteryReq: 2,
|
||||
minEnemyLevel: 20,
|
||||
maxEnemyLevel: 40,
|
||||
xpAmounts: [610, 610, 610, 900]
|
||||
xpAmounts: generateXpAmounts(rng, 4, 2500, 3000)
|
||||
},
|
||||
{
|
||||
jobType: rng.randomElement(venusJobs),
|
||||
jobType: rng.randomElementPop(pool),
|
||||
rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierDTable${table}Rewards`,
|
||||
masteryReq: 3,
|
||||
minEnemyLevel: 30,
|
||||
maxEnemyLevel: 50,
|
||||
xpAmounts: [600, 600, 600, 600, 1170]
|
||||
xpAmounts: generateXpAmounts(rng, 5, 3250, 3750)
|
||||
},
|
||||
{
|
||||
jobType: rng.randomElement(venusJobs),
|
||||
jobType: rng.randomElementPop(pool),
|
||||
rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable${table}Rewards`,
|
||||
masteryReq: 5,
|
||||
minEnemyLevel: 40,
|
||||
maxEnemyLevel: 60,
|
||||
xpAmounts: [690, 690, 690, 690, 1350]
|
||||
xpAmounts: generateXpAmounts(rng, 5, 4000, 4500)
|
||||
},
|
||||
{
|
||||
jobType: rng.randomElement(venusJobs),
|
||||
jobType: rng.randomElementPop(pool),
|
||||
rewards: `/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETable${table}Rewards`,
|
||||
masteryReq: 10,
|
||||
minEnemyLevel: 100,
|
||||
@ -602,7 +637,7 @@ export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[],
|
||||
masteryReq: 0,
|
||||
minEnemyLevel: 50,
|
||||
maxEnemyLevel: 70,
|
||||
xpAmounts: [780, 780, 780, 780, 1540]
|
||||
xpAmounts: generateXpAmounts(rng, 5, 4500, 5000)
|
||||
}
|
||||
]
|
||||
});
|
||||
@ -610,6 +645,7 @@ export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[],
|
||||
|
||||
{
|
||||
const rng = new SRng(seed);
|
||||
const pool = [...microplanetJobs];
|
||||
syndicateMissions.push({
|
||||
_id: {
|
||||
$oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000002"
|
||||
@ -621,20 +657,20 @@ export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[],
|
||||
Nodes: [],
|
||||
Jobs: [
|
||||
{
|
||||
jobType: rng.randomElement(microplanetJobs),
|
||||
jobType: rng.randomElementPop(pool),
|
||||
rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierATable${table}Rewards`,
|
||||
masteryReq: 0,
|
||||
minEnemyLevel: 5,
|
||||
maxEnemyLevel: 15,
|
||||
xpAmounts: [5, 5, 5]
|
||||
xpAmounts: generateXpAmounts(rng, 3, 12, 18)
|
||||
},
|
||||
{
|
||||
jobType: rng.randomElement(microplanetJobs),
|
||||
jobType: rng.randomElementPop(pool),
|
||||
rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierCTable${table}Rewards`,
|
||||
masteryReq: 1,
|
||||
minEnemyLevel: 15,
|
||||
maxEnemyLevel: 25,
|
||||
xpAmounts: [12, 12, 12]
|
||||
xpAmounts: generateXpAmounts(rng, 3, 24, 36)
|
||||
},
|
||||
{
|
||||
jobType: rng.randomElement(microplanetEndlessJobs),
|
||||
@ -646,23 +682,23 @@ export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[],
|
||||
xpAmounts: [14, 14, 14]
|
||||
},
|
||||
{
|
||||
jobType: rng.randomElement(microplanetJobs),
|
||||
jobType: rng.randomElementPop(pool),
|
||||
rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierDTable${deimosDTable}Rewards`,
|
||||
masteryReq: 2,
|
||||
minEnemyLevel: 30,
|
||||
maxEnemyLevel: 40,
|
||||
xpAmounts: [17, 17, 17, 25]
|
||||
xpAmounts: generateXpAmounts(rng, 4, 72, 88)
|
||||
},
|
||||
{
|
||||
jobType: rng.randomElement(microplanetJobs),
|
||||
jobType: rng.randomElementPop(pool),
|
||||
rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards`,
|
||||
masteryReq: 3,
|
||||
minEnemyLevel: 40,
|
||||
maxEnemyLevel: 60,
|
||||
xpAmounts: [22, 22, 22, 22, 43]
|
||||
xpAmounts: generateXpAmounts(rng, 5, 115, 135)
|
||||
},
|
||||
{
|
||||
jobType: rng.randomElement(microplanetJobs),
|
||||
jobType: rng.randomElementPop(pool),
|
||||
rewards: `/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards`,
|
||||
masteryReq: 10,
|
||||
minEnemyLevel: 100,
|
||||
@ -939,6 +975,61 @@ const getCalendarSeason = (week: number): ICalendarSeason => {
|
||||
};
|
||||
};
|
||||
|
||||
// Not very faithful, but to avoid the same node coming up back-to-back (which is not valid), I've split these into 2 arrays which we're alternating between.
|
||||
|
||||
const voidStormMissionsA = {
|
||||
VoidT1: ["CrewBattleNode519", "CrewBattleNode518", "CrewBattleNode515", "CrewBattleNode503"],
|
||||
VoidT2: ["CrewBattleNode501", "CrewBattleNode534", "CrewBattleNode530"],
|
||||
VoidT3: ["CrewBattleNode521", "CrewBattleNode516"],
|
||||
VoidT4: [
|
||||
"CrewBattleNode555",
|
||||
"CrewBattleNode553",
|
||||
"CrewBattleNode554",
|
||||
"CrewBattleNode539",
|
||||
"CrewBattleNode531",
|
||||
"CrewBattleNode527"
|
||||
]
|
||||
};
|
||||
|
||||
const voidStormMissionsB = {
|
||||
VoidT1: ["CrewBattleNode509", "CrewBattleNode522", "CrewBattleNode511", "CrewBattleNode512"],
|
||||
VoidT2: ["CrewBattleNode535", "CrewBattleNode533"],
|
||||
VoidT3: ["CrewBattleNode524", "CrewBattleNode525"],
|
||||
VoidT4: [
|
||||
"CrewBattleNode542",
|
||||
"CrewBattleNode538",
|
||||
"CrewBattleNode543",
|
||||
"CrewBattleNode536",
|
||||
"CrewBattleNode550",
|
||||
"CrewBattleNode529"
|
||||
]
|
||||
};
|
||||
|
||||
const pushVoidStorms = (arr: IVoidStorm[], hour: number): void => {
|
||||
const activation = hour * unixTimesInMs.hour + 40 * unixTimesInMs.minute;
|
||||
const expiry = activation + 90 * unixTimesInMs.minute;
|
||||
let accum = 0;
|
||||
const rng = new SRng(new SRng(hour).randomInt(0, 100_000));
|
||||
const voidStormMissions = structuredClone(hour & 1 ? voidStormMissionsA : voidStormMissionsB);
|
||||
for (const tier of ["VoidT1", "VoidT1", "VoidT2", "VoidT3", "VoidT4", "VoidT4"] as const) {
|
||||
const idx = rng.randomInt(0, voidStormMissions[tier].length - 1);
|
||||
const node = voidStormMissions[tier][idx];
|
||||
voidStormMissions[tier].splice(idx, 1);
|
||||
arr.push({
|
||||
_id: {
|
||||
$oid:
|
||||
((activation / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
|
||||
"0321e89b" +
|
||||
(accum++).toString().padStart(8, "0")
|
||||
},
|
||||
Node: node,
|
||||
Activation: { $date: { $numberLong: activation.toString() } },
|
||||
Expiry: { $date: { $numberLong: expiry.toString() } },
|
||||
ActiveMissionTier: tier
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const doesTimeSatsifyConstraints = (timeSecs: number): boolean => {
|
||||
if (config.worldState?.eidolonOverride) {
|
||||
const eidolonEpoch = 1391992660;
|
||||
@ -986,6 +1077,27 @@ const doesTimeSatsifyConstraints = (timeSecs: number): boolean => {
|
||||
}
|
||||
}
|
||||
|
||||
if (config.worldState?.duviriOverride) {
|
||||
const duviriMoods = ["sorrow", "fear", "joy", "anger", "envy"];
|
||||
const desiredMood = duviriMoods.indexOf(config.worldState.duviriOverride);
|
||||
if (desiredMood == -1) {
|
||||
logger.warn(`ignoring invalid config value for worldState.duviriOverride`, {
|
||||
value: config.worldState.duviriOverride,
|
||||
valid_values: duviriMoods
|
||||
});
|
||||
} else {
|
||||
const moodIndex = Math.trunc(timeSecs / 7200);
|
||||
const moodStart = moodIndex * 7200;
|
||||
const moodEnd = moodStart + 7200;
|
||||
if (
|
||||
moodIndex % 5 != desiredMood ||
|
||||
isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, moodEnd * 1000)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@ -1007,21 +1119,21 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
|
||||
Alerts: [],
|
||||
Sorties: [],
|
||||
LiteSorties: [],
|
||||
ActiveMissions: [],
|
||||
GlobalUpgrades: [],
|
||||
VoidTraders: [],
|
||||
VoidStorms: [],
|
||||
DailyDeals: [],
|
||||
EndlessXpChoices: [],
|
||||
KnownCalendarSeasons: [],
|
||||
...staticWorldState,
|
||||
SyndicateMissions: [...staticWorldState.SyndicateMissions]
|
||||
};
|
||||
|
||||
// Omit void fissures for versions prior to Dante Unbound to avoid script errors.
|
||||
if (buildLabel && version_compare(buildLabel, "2024.03.24.20.00") < 0) {
|
||||
worldState.ActiveMissions = [];
|
||||
if (version_compare(buildLabel, "2017.10.12.17.04") < 0) {
|
||||
// Old versions seem to really get hung up on not being able to load these.
|
||||
if (buildLabel && version_compare(buildLabel, "2017.10.12.17.04") < 0) {
|
||||
worldState.PVPChallengeInstances = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (config.worldState?.starDays) {
|
||||
worldState.Goals.push({
|
||||
@ -1039,6 +1151,77 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
|
||||
Node: "SolarisUnitedHub1"
|
||||
});
|
||||
}
|
||||
// The client gets kinda confused when multiple goals have the same tag, so considering these mutually exclusive.
|
||||
if (config.worldState?.galleonOfGhouls == 1) {
|
||||
worldState.Goals.push({
|
||||
_id: { $oid: "6814ddf00000000000000000" },
|
||||
Activation: { $date: { $numberLong: "1746198000000" } },
|
||||
Expiry: { $date: { $numberLong: "2000000000000" } },
|
||||
Count: 0,
|
||||
Goal: 1,
|
||||
Success: 0,
|
||||
Personal: true,
|
||||
Bounty: true,
|
||||
ClampNodeScores: true,
|
||||
Node: "EventNode19",
|
||||
MissionKeyName: "/Lotus/Types/Keys/GalleonRobberyAlert",
|
||||
Desc: "/Lotus/Language/Events/GalleonRobberyEventMissionTitle",
|
||||
Icon: "/Lotus/Interface/Icons/Player/GalleonRobberiesEvent.png",
|
||||
Tag: "GalleonRobbery",
|
||||
Reward: {
|
||||
items: [
|
||||
"/Lotus/StoreItems/Types/Recipes/Weapons/GrnChainSawTonfaBlueprint",
|
||||
"/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
|
||||
]
|
||||
}
|
||||
});
|
||||
} else if (config.worldState?.galleonOfGhouls == 2) {
|
||||
worldState.Goals.push({
|
||||
_id: { $oid: "681e18700000000000000000" },
|
||||
Activation: { $date: { $numberLong: "1746802800000" } },
|
||||
Expiry: { $date: { $numberLong: "2000000000000" } },
|
||||
Count: 0,
|
||||
Goal: 1,
|
||||
Success: 0,
|
||||
Personal: true,
|
||||
Bounty: true,
|
||||
ClampNodeScores: true,
|
||||
Node: "EventNode28",
|
||||
MissionKeyName: "/Lotus/Types/Keys/GalleonRobberyAlertB",
|
||||
Desc: "/Lotus/Language/Events/GalleonRobberyEventMissionTitle",
|
||||
Icon: "/Lotus/Interface/Icons/Player/GalleonRobberiesEvent.png",
|
||||
Tag: "GalleonRobbery",
|
||||
Reward: {
|
||||
items: [
|
||||
"/Lotus/StoreItems/Types/Recipes/Weapons/MortiforShieldAndSwordBlueprint",
|
||||
"/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
|
||||
]
|
||||
}
|
||||
});
|
||||
} else if (config.worldState?.galleonOfGhouls == 3) {
|
||||
worldState.Goals.push({
|
||||
_id: { $oid: "682752f00000000000000000" },
|
||||
Activation: { $date: { $numberLong: "1747407600000" } },
|
||||
Expiry: { $date: { $numberLong: "2000000000000" } },
|
||||
Count: 0,
|
||||
Goal: 1,
|
||||
Success: 0,
|
||||
Personal: true,
|
||||
Bounty: true,
|
||||
ClampNodeScores: true,
|
||||
Node: "EventNode19",
|
||||
MissionKeyName: "/Lotus/Types/Keys/GalleonRobberyAlertC",
|
||||
Desc: "/Lotus/Language/Events/GalleonRobberyEventMissionTitle",
|
||||
Icon: "/Lotus/Interface/Icons/Player/GalleonRobberiesEvent.png",
|
||||
Tag: "GalleonRobbery",
|
||||
Reward: {
|
||||
items: [
|
||||
"/Lotus/Types/StoreItems/Packages/EventCatalystReactorBundle",
|
||||
"/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Nightwave Challenges
|
||||
const nightwaveSyndicateTag = getNightwaveSyndicateTag(buildLabel);
|
||||
@ -1139,6 +1322,77 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
|
||||
});
|
||||
}
|
||||
|
||||
// Baro
|
||||
{
|
||||
const baroIndex = Math.trunc((Date.now() - 910800000) / (unixTimesInMs.day * 14));
|
||||
const baroStart = baroIndex * (unixTimesInMs.day * 14) + 910800000;
|
||||
const baroActualStart = baroStart + unixTimesInMs.day * (config.baroAlwaysAvailable ? 0 : 12);
|
||||
const baroEnd = baroStart + unixTimesInMs.day * 14;
|
||||
const baroNode = ["EarthHUB", "MercuryHUB", "SaturnHUB", "PlutoHUB"][baroIndex % 4];
|
||||
const vt: IVoidTrader = {
|
||||
_id: { $oid: ((baroStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "493c96d6067610bc" },
|
||||
Activation: { $date: { $numberLong: baroActualStart.toString() } },
|
||||
Expiry: { $date: { $numberLong: baroEnd.toString() } },
|
||||
Character: "Baro'Ki Teel",
|
||||
Node: baroNode,
|
||||
Manifest: []
|
||||
};
|
||||
worldState.VoidTraders.push(vt);
|
||||
if (isBeforeNextExpectedWorldStateRefresh(timeMs, baroActualStart)) {
|
||||
vt.Manifest = [];
|
||||
if (config.baroFullyStocked) {
|
||||
for (const armorSet of baro.armorSets) {
|
||||
if (Array.isArray(armorSet[0])) {
|
||||
for (const set of armorSet as IVoidTraderOffer[][]) {
|
||||
for (const item of set) {
|
||||
vt.Manifest.push(item);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const item of armorSet as IVoidTraderOffer[]) {
|
||||
vt.Manifest.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const item of baro.rest) {
|
||||
vt.Manifest.push(item);
|
||||
}
|
||||
} else {
|
||||
const rng = new SRng(new SRng(baroIndex).randomInt(0, 100_000));
|
||||
// TOVERIFY: Constraint for upgrades amount?
|
||||
// TOVERIFY: Constraint for weapon amount?
|
||||
// TOVERIFY: Constraint for relics amount?
|
||||
let armorSet = rng.randomElement(baro.armorSets)!;
|
||||
if (Array.isArray(armorSet[0])) {
|
||||
armorSet = rng.randomElement(baro.armorSets)!;
|
||||
}
|
||||
while (vt.Manifest.length + armorSet.length < 31) {
|
||||
const item = rng.randomElement(baro.rest)!;
|
||||
if (vt.Manifest.indexOf(item) == -1) {
|
||||
const set = baro.allIfAny.find(set => set.indexOf(item.ItemType) != -1);
|
||||
if (set) {
|
||||
for (const itemType of set) {
|
||||
vt.Manifest.push(baro.rest.find(x => x.ItemType == itemType)!);
|
||||
}
|
||||
} else {
|
||||
vt.Manifest.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
const overflow = 31 - (vt.Manifest.length + armorSet.length);
|
||||
if (overflow > 0) {
|
||||
vt.Manifest.splice(0, overflow);
|
||||
}
|
||||
for (const armor of armorSet) {
|
||||
vt.Manifest.push(armor as IVoidTraderOffer);
|
||||
}
|
||||
}
|
||||
for (const item of baro.evergreen) {
|
||||
vt.Manifest.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sortie & syndicate missions cycling every day (at 16:00 or 17:00 UTC depending on if London, OT is observing DST)
|
||||
{
|
||||
const rollover = getSortieTime(day);
|
||||
@ -1204,12 +1458,38 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
|
||||
worldState.KnownCalendarSeasons.push(getCalendarSeason(week + 1));
|
||||
}
|
||||
|
||||
// Sentient Anomaly cycling every 30 minutes
|
||||
// Void Storms
|
||||
const hour = Math.trunc(timeMs / unixTimesInMs.hour);
|
||||
const overLastHourStormExpiry = hour * unixTimesInMs.hour + 10 * unixTimesInMs.minute;
|
||||
const thisHourStormActivation = hour * unixTimesInMs.hour + 40 * unixTimesInMs.minute;
|
||||
if (overLastHourStormExpiry > timeMs) {
|
||||
pushVoidStorms(worldState.VoidStorms, hour - 2);
|
||||
}
|
||||
pushVoidStorms(worldState.VoidStorms, hour - 1);
|
||||
if (isBeforeNextExpectedWorldStateRefresh(timeMs, thisHourStormActivation)) {
|
||||
pushVoidStorms(worldState.VoidStorms, hour);
|
||||
}
|
||||
|
||||
// Sentient Anomaly + Xtra Cheese cycles
|
||||
const halfHour = Math.trunc(timeMs / (unixTimesInMs.hour / 2));
|
||||
const tmp = {
|
||||
const hourInSeconds = 3600;
|
||||
const cheeseInterval = hourInSeconds * 8;
|
||||
const cheeseDuration = hourInSeconds * 2;
|
||||
const cheeseIndex = Math.trunc(timeSecs / cheeseInterval);
|
||||
let cheeseStart = cheeseIndex * cheeseInterval;
|
||||
let cheeseEnd = cheeseStart + cheeseDuration;
|
||||
let cheeseNext = (cheeseIndex + 1) * cheeseInterval;
|
||||
// Live servers only update the start time once it happens, which makes the
|
||||
// client show a negative countdown during off-hours. Optionally adjust the
|
||||
// times so the next activation is always in the future.
|
||||
if (config.unfaithfulBugFixes?.fixXtraCheeseTimer && timeSecs >= cheeseEnd) {
|
||||
cheeseStart = cheeseNext;
|
||||
cheeseEnd = cheeseStart + cheeseDuration;
|
||||
cheeseNext += cheeseInterval;
|
||||
}
|
||||
const tmp: ITmp = {
|
||||
cavabegin: "1690761600",
|
||||
PurchasePlatformLockEnabled: true,
|
||||
tcsn: true,
|
||||
pgr: {
|
||||
ts: "1732572900",
|
||||
en: "CUSTOM DECALS @ ZEVILA",
|
||||
@ -1230,13 +1510,77 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
|
||||
},
|
||||
ennnd: true,
|
||||
mbrt: true,
|
||||
fbst: {
|
||||
a: cheeseStart,
|
||||
e: cheeseEnd,
|
||||
n: cheeseNext
|
||||
},
|
||||
sfn: [550, 553, 554, 555][halfHour % 4]
|
||||
};
|
||||
if (Array.isArray(config.worldState?.circuitGameModes)) {
|
||||
tmp.edg = config.worldState.circuitGameModes as TCircuitGameMode[];
|
||||
}
|
||||
worldState.Tmp = JSON.stringify(tmp);
|
||||
|
||||
return worldState;
|
||||
};
|
||||
|
||||
export const populateFissures = async (worldState: IWorldState): Promise<void> => {
|
||||
if (config.worldState?.allTheFissures) {
|
||||
let i = 0;
|
||||
for (const [tier, nodes] of Object.entries(fissureMissions)) {
|
||||
for (const node of nodes) {
|
||||
const meta = ExportRegions[node];
|
||||
worldState.ActiveMissions.push({
|
||||
_id: { $oid: (i++).toString().padStart(8, "0") + "8e0c70ba050f1eb7" },
|
||||
Region: meta.systemIndex + 1,
|
||||
Seed: 1337,
|
||||
Activation: { $date: { $numberLong: "1000000000000" } },
|
||||
Expiry: { $date: { $numberLong: "2000000000000" } },
|
||||
Node: node,
|
||||
MissionType: eMissionType[meta.missionIndex].tag,
|
||||
Modifier: tier,
|
||||
Hard: config.worldState.allTheFissures == "hard"
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const fissures = await Fissure.find({});
|
||||
for (const fissure of fissures) {
|
||||
const meta = ExportRegions[fissure.Node];
|
||||
worldState.ActiveMissions.push({
|
||||
_id: toOid(fissure._id),
|
||||
Region: meta.systemIndex + 1,
|
||||
Seed: 1337,
|
||||
Activation: toMongoDate(fissure.Activation),
|
||||
Expiry: toMongoDate(fissure.Expiry),
|
||||
Node: fissure.Node,
|
||||
MissionType: eMissionType[meta.missionIndex].tag,
|
||||
Modifier: fissure.Modifier,
|
||||
Hard: fissure.Hard
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const populateDailyDeal = async (worldState: IWorldState): Promise<void> => {
|
||||
const dailyDeals = await DailyDeal.find({});
|
||||
for (const dailyDeal of dailyDeals) {
|
||||
if (dailyDeal.Expiry.getTime() > Date.now()) {
|
||||
worldState.DailyDeals.push({
|
||||
StoreItem: dailyDeal.StoreItem,
|
||||
Activation: toMongoDate(dailyDeal.Activation),
|
||||
Expiry: toMongoDate(dailyDeal.Expiry),
|
||||
Discount: dailyDeal.Discount,
|
||||
OriginalPrice: dailyDeal.OriginalPrice,
|
||||
SalePrice: dailyDeal.SalePrice,
|
||||
AmountTotal: Math.round(dailyDeal.AmountTotal * (config.worldState?.darvoStockMultiplier ?? 1)),
|
||||
AmountSold: dailyDeal.AmountSold
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const idToBountyCycle = (id: string): number => {
|
||||
return Math.trunc((parseInt(id.substring(0, 8), 16) * 1000) / 9000_000);
|
||||
};
|
||||
@ -1364,3 +1708,92 @@ const nightwaveTagToSeason: Record<string, number> = {
|
||||
RadioLegionIntermissionSyndicate: 1, // Intermission I
|
||||
RadioLegionSyndicate: 0 // The Wolf of Saturn Six
|
||||
};
|
||||
|
||||
const updateFissures = async (): Promise<void> => {
|
||||
const fissures = await Fissure.find();
|
||||
|
||||
const activeNodes = new Set<string>();
|
||||
const tierToFurthestExpiry: Record<string, number> = {
|
||||
VoidT1: 0,
|
||||
VoidT2: 0,
|
||||
VoidT3: 0,
|
||||
VoidT4: 0,
|
||||
VoidT5: 0,
|
||||
VoidT6: 0,
|
||||
VoidT1Hard: 0,
|
||||
VoidT2Hard: 0,
|
||||
VoidT3Hard: 0,
|
||||
VoidT4Hard: 0,
|
||||
VoidT5Hard: 0,
|
||||
VoidT6Hard: 0
|
||||
};
|
||||
for (const fissure of fissures) {
|
||||
activeNodes.add(fissure.Node);
|
||||
|
||||
const key = fissure.Modifier + (fissure.Hard ? "Hard" : "");
|
||||
tierToFurthestExpiry[key] = Math.max(tierToFurthestExpiry[key], fissure.Expiry.getTime());
|
||||
}
|
||||
|
||||
const deadline = Date.now() - 6 * unixTimesInMs.minute;
|
||||
for (const [tier, expiry] of Object.entries(tierToFurthestExpiry)) {
|
||||
if (expiry < deadline) {
|
||||
const numFissures = getRandomInt(1, 3);
|
||||
for (let i = 0; i != numFissures; ++i) {
|
||||
const modifier = tier.replace("Hard", "") as
|
||||
| "VoidT1"
|
||||
| "VoidT2"
|
||||
| "VoidT3"
|
||||
| "VoidT4"
|
||||
| "VoidT5"
|
||||
| "VoidT6";
|
||||
let node: string;
|
||||
do {
|
||||
node = getRandomElement(fissureMissions[modifier])!;
|
||||
} while (activeNodes.has(node));
|
||||
activeNodes.add(node);
|
||||
await Fissure.insertOne({
|
||||
Activation: new Date(),
|
||||
Expiry: new Date(Date.now() + getRandomInt(60, 120) * unixTimesInMs.minute),
|
||||
Node: node,
|
||||
Modifier: modifier,
|
||||
Hard: tier.indexOf("Hard") != -1 ? true : undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateDailyDeal = async (): Promise<void> => {
|
||||
let darvoIndex = Math.trunc((Date.now() - 25200000) / (26 * unixTimesInMs.hour));
|
||||
let darvoEnd;
|
||||
do {
|
||||
const darvoStart = darvoIndex * (26 * unixTimesInMs.hour) + 25200000;
|
||||
darvoEnd = darvoStart + 26 * unixTimesInMs.hour;
|
||||
const darvoOid = ((darvoStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "adc51a72f7324d95";
|
||||
if (!(await DailyDeal.findById(darvoOid))) {
|
||||
const seed = new SRng(darvoIndex).randomInt(0, 100_000);
|
||||
const rng = new SRng(seed);
|
||||
let deal;
|
||||
do {
|
||||
deal = rng.randomReward(darvoDeals)!; // Using an actual sampling collected over roughly a year because I can't extrapolate an algorithm from it with enough certainty.
|
||||
//const [storeItem, meta] = rng.randomElement(Object.entries(darvoDeals))!;
|
||||
//const discount = Math.min(rng.randomInt(1, 9) * 10, (meta as { MaxDiscount?: number }).MaxDiscount ?? 1);
|
||||
} while (await DailyDeal.exists({ StoreItem: deal.StoreItem }));
|
||||
await DailyDeal.insertOne({
|
||||
_id: darvoOid,
|
||||
StoreItem: deal.StoreItem,
|
||||
Activation: new Date(darvoStart),
|
||||
Expiry: new Date(darvoEnd),
|
||||
Discount: deal.Discount,
|
||||
OriginalPrice: deal.OriginalPrice,
|
||||
SalePrice: deal.SalePrice, //Math.trunc(deal.OriginalPrice * (1 - discount))
|
||||
AmountTotal: deal.AmountTotal,
|
||||
AmountSold: 0
|
||||
});
|
||||
}
|
||||
} while (darvoEnd < Date.now() + 6 * unixTimesInMs.minute && ++darvoIndex);
|
||||
};
|
||||
|
||||
export const updateWorldStateCollections = async (): Promise<void> => {
|
||||
await Promise.all([updateFissures(), updateDailyDeal()]);
|
||||
};
|
||||
|
@ -40,6 +40,7 @@ export interface IInventoryDatabase
|
||||
| "InfestedFoundry"
|
||||
| "DialogueHistory"
|
||||
| "KubrowPetEggs"
|
||||
| "KubrowPetPrints"
|
||||
| "PendingCoupon"
|
||||
| "Drones"
|
||||
| "RecentVendorPurchases"
|
||||
@ -56,6 +57,7 @@ export interface IInventoryDatabase
|
||||
| "QualifyingInvasions"
|
||||
| "LastInventorySync"
|
||||
| "EndlessXP"
|
||||
| "PersonalGoalProgress"
|
||||
| TEquipmentKey
|
||||
>,
|
||||
InventoryDatabaseEquipment {
|
||||
@ -63,7 +65,7 @@ export interface IInventoryDatabase
|
||||
Created: Date;
|
||||
TrainingDate: Date;
|
||||
LoadOutPresets: Types.ObjectId; // LoadOutPresets changed from ILoadOutPresets to Types.ObjectId for population
|
||||
Mailbox?: IMailboxDatabase;
|
||||
//Mailbox?: IMailboxDatabase;
|
||||
GuildId?: Types.ObjectId;
|
||||
PendingRecipes: IPendingRecipeDatabase[];
|
||||
QuestKeys: IQuestKeyDatabase[];
|
||||
@ -78,7 +80,8 @@ export interface IInventoryDatabase
|
||||
KahlLoadOuts: IOperatorConfigDatabase[];
|
||||
InfestedFoundry?: IInfestedFoundryDatabase;
|
||||
DialogueHistory?: IDialogueHistoryDatabase;
|
||||
KubrowPetEggs?: IKubrowPetEggDatabase[];
|
||||
KubrowPetEggs: IKubrowPetEggDatabase[];
|
||||
KubrowPetPrints: IKubrowPetPrintDatabase[];
|
||||
PendingCoupon?: IPendingCouponDatabase;
|
||||
Drones: IDroneDatabase[];
|
||||
RecentVendorPurchases?: IRecentVendorPurchaseDatabase[];
|
||||
@ -95,6 +98,7 @@ export interface IInventoryDatabase
|
||||
QualifyingInvasions: IInvasionProgressDatabase[];
|
||||
LastInventorySync?: Types.ObjectId;
|
||||
EndlessXP?: IEndlessXpProgressDatabase[];
|
||||
PersonalGoalProgress?: IPersonalGoalProgressDatabase[];
|
||||
}
|
||||
|
||||
export interface IQuestKeyDatabase {
|
||||
@ -150,9 +154,9 @@ export interface IMailboxClient {
|
||||
LastInboxId: IOid;
|
||||
}
|
||||
|
||||
export interface IMailboxDatabase {
|
||||
/*export interface IMailboxDatabase {
|
||||
LastInboxId: Types.ObjectId;
|
||||
}
|
||||
}*/
|
||||
|
||||
export type TSolarMapRegion =
|
||||
| "Earth"
|
||||
@ -234,7 +238,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
|
||||
HandlerPoints: number;
|
||||
MiscItems: IMiscItem[];
|
||||
HasOwnedVoidProjectionsPreviously?: boolean;
|
||||
ChallengesFixVersion: number;
|
||||
ChallengesFixVersion?: number;
|
||||
ChallengeProgress: IChallengeProgress[];
|
||||
RawUpgrades: IRawUpgrade[];
|
||||
ReceivedStartingGear: boolean;
|
||||
@ -285,6 +289,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
|
||||
ArchwingEnabled?: boolean;
|
||||
PendingSpectreLoadouts?: ISpectreLoadout[];
|
||||
SpectreLoadouts?: ISpectreLoadout[];
|
||||
UsedDailyDeals: string[];
|
||||
EmailItems: ITypeCount[];
|
||||
CompletedSyndicates: string[];
|
||||
FocusXP?: IFocusXP;
|
||||
@ -293,7 +298,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
|
||||
CompletedSorties: string[];
|
||||
LastSortieReward?: ILastSortieRewardClient[];
|
||||
LastLiteSortieReward?: ILastSortieRewardClient[];
|
||||
SortieRewardAttenuation?: ISortieRewardAttenuation[];
|
||||
SortieRewardAttenuation?: IRewardAttenuation[];
|
||||
Drones: IDroneClient[];
|
||||
StepSequencers: IStepSequencer[];
|
||||
ActiveAvatarImageType?: string;
|
||||
@ -304,9 +309,9 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
|
||||
FocusUpgrades: IFocusUpgrade[];
|
||||
HasContributedToDojo?: boolean;
|
||||
HWIDProtectEnabled?: boolean;
|
||||
//KubrowPetPrints: IKubrowPetPrint[];
|
||||
KubrowPetPrints: IKubrowPetPrintClient[];
|
||||
AlignmentReplay?: IAlignment;
|
||||
//PersonalGoalProgress: IPersonalGoalProgress[];
|
||||
PersonalGoalProgress?: IPersonalGoalProgressClient[];
|
||||
ThemeStyle: string;
|
||||
ThemeBackground: string;
|
||||
ThemeSounds: string;
|
||||
@ -349,7 +354,6 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
|
||||
//LeagueTickets: any[];
|
||||
//Quests: any[];
|
||||
//Robotics: any[];
|
||||
//UsedDailyDeals: any[];
|
||||
LibraryPersonalTarget?: string;
|
||||
LibraryPersonalProgress: ILibraryPersonalProgress[];
|
||||
CollectibleSeries?: ICollectibleEntry[];
|
||||
@ -378,6 +382,8 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
|
||||
LockedWeaponGroup?: ILockedWeaponGroupClient;
|
||||
HubNpcCustomizations?: IHubNpcCustomization[];
|
||||
Ship?: IOrbiter; // U22 and below, response only
|
||||
ClaimedJunctionChallengeRewards?: string[]; // U39
|
||||
SpecialItemRewardAttenuation?: IRewardAttenuation[]; // Baro's Void Surplus
|
||||
}
|
||||
|
||||
export interface IAffiliation {
|
||||
@ -446,8 +452,9 @@ export interface IVendorPurchaseHistoryEntryDatabase {
|
||||
|
||||
export interface IChallengeProgress {
|
||||
Progress: number;
|
||||
Name: string;
|
||||
Completed?: string[];
|
||||
ReceivedJunctionReward?: boolean; // U39
|
||||
Name: string;
|
||||
}
|
||||
|
||||
export interface ICollectibleEntry {
|
||||
@ -641,11 +648,11 @@ export interface IFocusUpgrade {
|
||||
}
|
||||
|
||||
export interface IFocusXP {
|
||||
AP_POWER: number;
|
||||
AP_TACTIC: number;
|
||||
AP_DEFENSE: number;
|
||||
AP_ATTACK: number;
|
||||
AP_WARD: number;
|
||||
AP_POWER?: number;
|
||||
AP_TACTIC?: number;
|
||||
AP_DEFENSE?: number;
|
||||
AP_ATTACK?: number;
|
||||
AP_WARD?: number;
|
||||
}
|
||||
|
||||
export type TFocusPolarity = keyof IFocusXP;
|
||||
@ -718,8 +725,8 @@ export interface IKubrowPetEggDatabase {
|
||||
_id: Types.ObjectId;
|
||||
}
|
||||
|
||||
export interface IKubrowPetPrint {
|
||||
ItemType: KubrowPetPrintItemType;
|
||||
export interface IKubrowPetPrintClient {
|
||||
ItemType: "/Lotus/Types/Game/KubrowPet/ImprintedTraitPrint";
|
||||
Name: string;
|
||||
IsMale: boolean;
|
||||
Size: number; // seems to be 0.7 to 1.0
|
||||
@ -729,6 +736,10 @@ export interface IKubrowPetPrint {
|
||||
InheritedModularParts?: any[];
|
||||
}
|
||||
|
||||
export interface IKubrowPetPrintDatabase extends Omit<IKubrowPetPrintClient, "ItemId" | "InheritedModularParts"> {
|
||||
_id: Types.ObjectId;
|
||||
}
|
||||
|
||||
export interface ITraits {
|
||||
BaseColor: string;
|
||||
SecondaryColor: string;
|
||||
@ -742,15 +753,11 @@ export interface ITraits {
|
||||
Tail?: string;
|
||||
}
|
||||
|
||||
export enum KubrowPetPrintItemType {
|
||||
LotusTypesGameKubrowPetImprintedTraitPrint = "/Lotus/Types/Game/KubrowPet/ImprintedTraitPrint"
|
||||
}
|
||||
|
||||
export interface IKubrowPetDetailsDatabase {
|
||||
Name?: string;
|
||||
IsPuppy?: boolean;
|
||||
HasCollar: boolean;
|
||||
PrintsRemaining?: number;
|
||||
PrintsRemaining: number;
|
||||
Status: Status;
|
||||
HatchDate?: Date;
|
||||
DominantTraits: ITraits;
|
||||
@ -779,7 +786,7 @@ export interface ILastSortieRewardDatabase extends Omit<ILastSortieRewardClient,
|
||||
SortieId: Types.ObjectId;
|
||||
}
|
||||
|
||||
export interface ISortieRewardAttenuation {
|
||||
export interface IRewardAttenuation {
|
||||
Tag: string;
|
||||
Atten: number;
|
||||
}
|
||||
@ -1015,13 +1022,17 @@ export interface IPeriodicMissionCompletionResponse extends Omit<IPeriodicMissio
|
||||
date: IMongoDate;
|
||||
}
|
||||
|
||||
export interface IPersonalGoalProgress {
|
||||
export interface IPersonalGoalProgressClient {
|
||||
Best: number;
|
||||
Count: number;
|
||||
Tag: string;
|
||||
Best?: number;
|
||||
_id: IOid;
|
||||
ReceivedClanReward0?: boolean;
|
||||
ReceivedClanReward1?: boolean;
|
||||
//ReceivedClanReward0?: boolean;
|
||||
//ReceivedClanReward1?: boolean;
|
||||
}
|
||||
|
||||
export interface IPersonalGoalProgressDatabase extends Omit<IPersonalGoalProgressClient, "_id"> {
|
||||
goalId: Types.ObjectId;
|
||||
}
|
||||
|
||||
export interface IPersonalTechProjectDatabase {
|
||||
|
@ -2,7 +2,7 @@ import { Types } from "mongoose";
|
||||
|
||||
export interface IAccountAndLoginResponseCommons {
|
||||
DisplayName: string;
|
||||
CountryCode: string;
|
||||
CountryCode?: string;
|
||||
ClientType?: string;
|
||||
CrossPlatformAllowed?: boolean;
|
||||
ForceLogoutVersion?: number;
|
||||
@ -25,6 +25,7 @@ export interface IDatabaseAccount extends IDatabaseAccountRequiredFields {
|
||||
LatestEventMessageDate: Date;
|
||||
LastLoginRewardDate: number;
|
||||
LoginDays: number;
|
||||
DailyFirstWinDate: number;
|
||||
}
|
||||
|
||||
// Includes virtual ID
|
||||
|
@ -17,9 +17,9 @@ export interface IMissionReward {
|
||||
}
|
||||
|
||||
export interface IMissionCredits {
|
||||
MissionCredits: number[];
|
||||
CreditBonus: number[];
|
||||
TotalCredits: number[];
|
||||
MissionCredits: [number, number];
|
||||
CreditsBonus: [number, number]; // "Credit Reward"; `CreditsBonus[1]` is `CreditsBonus[0] * 2` if DailyMissionBonus
|
||||
TotalCredits: [number, number];
|
||||
DailyMissionBonus?: boolean;
|
||||
}
|
||||
|
||||
|
@ -7,17 +7,46 @@ import {
|
||||
ITypeCount,
|
||||
IRecentVendorPurchaseClient,
|
||||
TEquipmentKey,
|
||||
ICrewMemberClient
|
||||
ICrewMemberClient,
|
||||
IKubrowPetPrintClient
|
||||
} from "./inventoryTypes/inventoryTypes";
|
||||
|
||||
export enum PurchaseSource {
|
||||
Market = 0,
|
||||
VoidTrader = 1,
|
||||
SyndicateFavor = 2,
|
||||
DailyDeal = 3,
|
||||
Arsenal = 4,
|
||||
Profile = 5,
|
||||
Hub = 6,
|
||||
Vendor = 7,
|
||||
AppearancePreview = 8,
|
||||
Museum = 9,
|
||||
Operator = 10,
|
||||
PlayerShip = 11,
|
||||
Crewship = 12,
|
||||
MenuStyle = 13,
|
||||
MenuHud = 14,
|
||||
Chat = 15,
|
||||
Inventory = 16,
|
||||
StarChart = 17,
|
||||
PrimeVaultTrader = 18,
|
||||
Incubator = 19,
|
||||
Prompt = 20,
|
||||
Kaithe = 21,
|
||||
DuviriWeapon = 22,
|
||||
UpdateScreen = 23,
|
||||
Motorcycle = 24
|
||||
}
|
||||
|
||||
export interface IPurchaseRequest {
|
||||
PurchaseParams: IPurchaseParams;
|
||||
buildLabel: string;
|
||||
}
|
||||
|
||||
export interface IPurchaseParams {
|
||||
Source: number;
|
||||
SourceId?: string; // for Source 1, 7 & 18
|
||||
Source: PurchaseSource;
|
||||
SourceId?: string; // VoidTrader, Vendor, PrimeVaultTrader
|
||||
StoreItem: string;
|
||||
StorePage: string;
|
||||
SearchTerm: string;
|
||||
@ -25,10 +54,10 @@ export interface IPurchaseParams {
|
||||
Quantity: number;
|
||||
UsePremium: boolean;
|
||||
ExpectedPrice: number;
|
||||
SyndicateTag?: string; // for Source 2
|
||||
UseFreeFavor?: boolean; // for Source 2
|
||||
ExtraPurchaseInfoJson?: string; // for Source 7
|
||||
IsWeekly?: boolean; // for Source 7
|
||||
SyndicateTag?: string; // SyndicateFavor
|
||||
UseFreeFavor?: boolean; // SyndicateFavor
|
||||
ExtraPurchaseInfoJson?: string; // Vendor
|
||||
IsWeekly?: boolean; // Vendor
|
||||
}
|
||||
|
||||
export type IInventoryChanges = {
|
||||
@ -50,6 +79,7 @@ export type IInventoryChanges = {
|
||||
NewVendorPurchase?: IRecentVendorPurchaseClient; // >= 38.5.0
|
||||
RecentVendorPurchases?: IRecentVendorPurchaseClient; // < 38.5.0
|
||||
CrewMembers?: ICrewMemberClient[];
|
||||
KubrowPetPrints?: IKubrowPetPrintClient[];
|
||||
} & Record<
|
||||
Exclude<
|
||||
string,
|
||||
@ -77,6 +107,7 @@ export interface IPurchaseResponse {
|
||||
Standing?: IAffiliationMods[];
|
||||
FreeFavorsUsed?: IAffiliationMods[];
|
||||
BoosterPackItems?: string;
|
||||
DailyDealUsed?: string;
|
||||
}
|
||||
|
||||
export type IBinChanges = {
|
||||
|
@ -139,6 +139,14 @@ export type IMissionInventoryUpdateRequest = {
|
||||
};
|
||||
wagerTier?: number; // the index
|
||||
creditsFee?: number; // the index
|
||||
GoalProgress?: {
|
||||
_id: IOid;
|
||||
Count: number;
|
||||
Best: number;
|
||||
Tag: string;
|
||||
IsMultiProgress: boolean;
|
||||
MultiProgress: unknown[];
|
||||
}[];
|
||||
InvasionProgress?: IInvasionProgressClient[];
|
||||
ConquestMissionsCompleted?: number;
|
||||
duviriSuitSelection?: string;
|
||||
@ -156,6 +164,8 @@ export type IMissionInventoryUpdateRequest = {
|
||||
|
||||
export interface IRewardInfo {
|
||||
node: string;
|
||||
goalId?: string;
|
||||
goalManifest?: string;
|
||||
invasionId?: string;
|
||||
invasionAllyFaction?: "FC_GRINEER" | "FC_CORPUS";
|
||||
sortieId?: string;
|
||||
|
@ -96,6 +96,7 @@ export interface IPlacedDecosDatabase {
|
||||
Pos: [number, number, number];
|
||||
Rot: [number, number, number];
|
||||
Scale?: number;
|
||||
Sockets?: number;
|
||||
PictureFrameInfo?: IPictureFrameInfo;
|
||||
_id: Types.ObjectId;
|
||||
}
|
||||
@ -136,6 +137,7 @@ export interface IShipDecorationsRequest {
|
||||
MoveId?: string;
|
||||
OldRoom?: string;
|
||||
Scale?: number;
|
||||
Sockets?: number;
|
||||
}
|
||||
|
||||
export interface IShipDecorationsResponse {
|
||||
@ -152,7 +154,7 @@ export interface ISetPlacedDecoInfoRequest {
|
||||
DecoId: string;
|
||||
Room: string;
|
||||
PictureFrameInfo: IPictureFrameInfo;
|
||||
BootLocation?: string;
|
||||
BootLocation?: TBootLocation;
|
||||
ComponentId?: string;
|
||||
GuildId?: string;
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { IMissionReward } from "warframe-public-export-plus";
|
||||
import { IMongoDate, IOid } from "./commonTypes";
|
||||
|
||||
export interface IWorldState {
|
||||
@ -9,9 +10,12 @@ export interface IWorldState {
|
||||
Sorties: ISortie[];
|
||||
LiteSorties: ILiteSortie[];
|
||||
SyndicateMissions: ISyndicateMissionInfo[];
|
||||
GlobalUpgrades: IGlobalUpgrade[];
|
||||
ActiveMissions: IFissure[];
|
||||
GlobalUpgrades: IGlobalUpgrade[];
|
||||
NodeOverrides: INodeOverride[];
|
||||
VoidTraders: IVoidTrader[];
|
||||
VoidStorms: IVoidStorm[];
|
||||
DailyDeals: IDailyDeal[];
|
||||
PVPChallengeInstances: IPVPChallengeInstance[];
|
||||
EndlessXpChoices: IEndlessXpChoice[];
|
||||
SeasonInfo?: {
|
||||
@ -35,11 +39,15 @@ export interface IGoal {
|
||||
Goal: number;
|
||||
Success: number;
|
||||
Personal: boolean;
|
||||
Bounty?: boolean;
|
||||
ClampNodeScores?: boolean;
|
||||
Desc: string;
|
||||
ToolTip: string;
|
||||
ToolTip?: string;
|
||||
Icon: string;
|
||||
Tag: string;
|
||||
Node: string;
|
||||
MissionKeyName?: string;
|
||||
Reward?: IMissionReward;
|
||||
}
|
||||
|
||||
export interface ISyndicateMissionInfo {
|
||||
@ -85,6 +93,14 @@ export interface IFissure {
|
||||
Hard?: boolean;
|
||||
}
|
||||
|
||||
export interface IFissureDatabase {
|
||||
Activation: Date;
|
||||
Expiry: Date;
|
||||
Node: string;
|
||||
Modifier: "VoidT1" | "VoidT2" | "VoidT3" | "VoidT4" | "VoidT5" | "VoidT6";
|
||||
Hard?: boolean;
|
||||
}
|
||||
|
||||
export interface INodeOverride {
|
||||
_id: IOid;
|
||||
Activation?: IMongoDate;
|
||||
@ -131,6 +147,52 @@ export interface ILiteSortie {
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface IVoidTrader {
|
||||
_id: IOid;
|
||||
Activation: IMongoDate;
|
||||
Expiry: IMongoDate;
|
||||
Character: string;
|
||||
Node: string;
|
||||
Manifest: IVoidTraderOffer[];
|
||||
}
|
||||
|
||||
export interface IVoidTraderOffer {
|
||||
ItemType: string;
|
||||
PrimePrice: number;
|
||||
RegularPrice: number;
|
||||
Limit?: number;
|
||||
}
|
||||
|
||||
export interface IVoidStorm {
|
||||
_id: IOid;
|
||||
Node: string;
|
||||
Activation: IMongoDate;
|
||||
Expiry: IMongoDate;
|
||||
ActiveMissionTier: string;
|
||||
}
|
||||
|
||||
export interface IDailyDeal {
|
||||
StoreItem: string;
|
||||
Activation: IMongoDate;
|
||||
Expiry: IMongoDate;
|
||||
Discount: number;
|
||||
OriginalPrice: number;
|
||||
SalePrice: number;
|
||||
AmountTotal: number;
|
||||
AmountSold: number;
|
||||
}
|
||||
|
||||
export interface IDailyDealDatabase {
|
||||
StoreItem: string;
|
||||
Activation: Date;
|
||||
Expiry: Date;
|
||||
Discount: number;
|
||||
OriginalPrice: number;
|
||||
SalePrice: number;
|
||||
AmountTotal: number;
|
||||
AmountSold: number;
|
||||
}
|
||||
|
||||
export interface IPVPChallengeInstance {
|
||||
_id: IOid;
|
||||
challengeTypeRefID: string;
|
||||
@ -182,3 +244,48 @@ export interface ICalendarEvent {
|
||||
dialogueName?: string;
|
||||
dialogueConvo?: string;
|
||||
}
|
||||
|
||||
export type TCircuitGameMode =
|
||||
| "Survival"
|
||||
| "VoidFlood"
|
||||
| "Excavation"
|
||||
| "Defense"
|
||||
| "Exterminate"
|
||||
| "Assassination"
|
||||
| "Alchemy";
|
||||
|
||||
export interface ITmp {
|
||||
cavabegin: string;
|
||||
PurchasePlatformLockEnabled: boolean; // Seems unused
|
||||
pgr: IPgr;
|
||||
ennnd?: boolean; // True if 1999 demo is available (no effect for >=38.6.0)
|
||||
mbrt?: boolean; // Related to mobile app rating request
|
||||
fbst: IFbst;
|
||||
sfn: number;
|
||||
edg?: TCircuitGameMode[]; // The Circuit game modes overwrite
|
||||
}
|
||||
|
||||
interface IPgr {
|
||||
ts: string;
|
||||
en: string;
|
||||
fr: string;
|
||||
it: string;
|
||||
de: string;
|
||||
es: string;
|
||||
pt: string;
|
||||
ru: string;
|
||||
pl: string;
|
||||
uk: string;
|
||||
tr: string;
|
||||
ja: string;
|
||||
zh: string;
|
||||
ko: string;
|
||||
tc: string;
|
||||
th: string;
|
||||
}
|
||||
|
||||
interface IFbst {
|
||||
a: number;
|
||||
e: number;
|
||||
n: number;
|
||||
}
|
||||
|
@ -20,6 +20,10 @@
|
||||
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/DojoRemasterTrophyGoldARecipe",
|
||||
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/DojoRemasterTrophyPlatinumARecipe",
|
||||
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/DojoRemasterTrophySilverARecipe",
|
||||
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/DuviriMurmurEventBronzeTrophyRecipe",
|
||||
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/DuviriMurmurEventClayTrophyRecipe",
|
||||
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/DuviriMurmurEventGoldTrophyRecipe",
|
||||
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/DuviriMurmurEventSilverTrophyRecipe",
|
||||
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/EntratiEventBaseTrophyRecipe",
|
||||
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/EntratiEventBronzeTrophyRecipe",
|
||||
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/EntratiEventGoldTrophyRecipe",
|
||||
@ -88,5 +92,13 @@
|
||||
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/ThumperTrophySilverRecipe",
|
||||
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/CorpusPlaceables/GasTurbineConeRecipe",
|
||||
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/NaturalPlaceables/CoralChunkARecipe",
|
||||
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/TennoPlaceables/TnoBeaconEmitterRecipe"
|
||||
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/TennoPlaceables/TnoBeaconEmitterRecipe",
|
||||
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/OstronFemaleSitting",
|
||||
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/OstronFemaleStanding",
|
||||
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/OstronMaleStanding",
|
||||
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/OstronMaleStandingTwo",
|
||||
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/SolarisForeman",
|
||||
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/SolarisHazard",
|
||||
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/SolarisStrikerOne",
|
||||
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/NpcPlaceables/SolarisStrikerThree"
|
||||
]
|
||||
|
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