forked from OpenWF/SpaceNinjaServer
Compare commits
42 Commits
a47eccdec8
...
54a73ad5d7
Author | SHA1 | Date | |
---|---|---|---|
54a73ad5d7 | |||
62eeb313ec | |||
62d4b9f6cb | |||
1d813a1b1b | |||
4823406229 | |||
bdc41de8bb | |||
60236a1154 | |||
1979b20f8c | |||
c736310ff3 | |||
2b555a6456 | |||
870c964854 | |||
4535b193e0 | |||
943574bf3a | |||
f9a4d48b4d | |||
135b1e54fe | |||
b7c47b91ff | |||
9def5c265e | |||
65387ccdea | |||
4118528603 | |||
8ffbb308c5 | |||
2e649cabf6 | |||
5c5296d565 | |||
8f5f2fc206 | |||
0997f9567f | |||
be02435661 | |||
20c4092dfe | |||
01492f4f16 | |||
d739945a1d | |||
d43e39d7b5 | |||
b0499a62aa | |||
8f02bd1509 | |||
32f4c5105a | |||
28da982c80 | |||
ae1850d6cd | |||
b90bdd2783 | |||
90f2b90398 | |||
84916bf64e | |||
d41e4f7f56 | |||
082ae536f7 | |||
ba6cd47432 | |||
92d34fd69e | |||
09b9683fa1 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -10,6 +10,8 @@ jobs:
|
||||
uses: actions/checkout@v4.1.2
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
node-version: ">=20.6.0"
|
||||
- run: npm ci
|
||||
- run: cp config.json.example config.json
|
||||
- run: npm run verify
|
||||
|
20
README.md
20
README.md
@ -14,4 +14,22 @@ SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [confi
|
||||
|
||||
- `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`.
|
||||
- `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`.
|
||||
- `worldState.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE.
|
||||
- `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.nightwaveOverride` will lock the nightwave season, assuming the client is new enough for it. Valid values:
|
||||
- `RadioLegionIntermission13Syndicate` for Nora's Mix Vol. 9
|
||||
- `RadioLegionIntermission12Syndicate` for Nora's Mix Vol. 8
|
||||
- `RadioLegionIntermission11Syndicate` for Nora's Mix Vol. 7
|
||||
- `RadioLegionIntermission10Syndicate` for Nora's Mix Vol. 6
|
||||
- `RadioLegionIntermission9Syndicate` for Nora's Mix Vol. 5
|
||||
- `RadioLegionIntermission8Syndicate` for Nora's Mix Vol. 4
|
||||
- `RadioLegionIntermission7Syndicate` for Nora's Mix Vol. 3
|
||||
- `RadioLegionIntermission6Syndicate` for Nora's Mix Vol. 2
|
||||
- `RadioLegionIntermission5Syndicate` for Nora's Mix Vol. 1
|
||||
- `RadioLegionIntermission4Syndicate` for Nora's Choice
|
||||
- `RadioLegionIntermission3Syndicate` for Intermission III
|
||||
- `RadioLegion3Syndicate` for Glassmaker
|
||||
- `RadioLegionIntermission2Syndicate` for Intermission II
|
||||
- `RadioLegion2Syndicate` for The Emissary
|
||||
- `RadioLegionIntermissionSyndicate` for Intermission I
|
||||
- `RadioLegionSyndicate` for The Wolf of Saturn Six
|
||||
|
@ -3,7 +3,7 @@
|
||||
echo Updating SpaceNinjaServer...
|
||||
git fetch --prune
|
||||
git stash
|
||||
git reset --hard origin/main
|
||||
git checkout -f origin/main
|
||||
|
||||
if exist static\data\0\ (
|
||||
echo Updating stripped assets...
|
||||
|
23
UPDATE AND START SERVER.sh
Executable file
23
UPDATE AND START SERVER.sh
Executable file
@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Updating SpaceNinjaServer..."
|
||||
git fetch --prune
|
||||
git stash
|
||||
git checkout -f origin/main
|
||||
|
||||
if [ -d "static/data/0/" ]; then
|
||||
echo "Updating stripped assets..."
|
||||
cd static/data/0/
|
||||
git pull
|
||||
cd ../../../
|
||||
fi
|
||||
|
||||
echo "Updating dependencies..."
|
||||
npm i --omit=dev
|
||||
|
||||
npm run build
|
||||
if [ $? -eq 0 ]; then
|
||||
npm run start
|
||||
echo "SpaceNinjaServer seems to have crashed."
|
||||
fi
|
||||
|
@ -55,6 +55,8 @@
|
||||
"affinityBoost": false,
|
||||
"resourceBoost": false,
|
||||
"starDays": true,
|
||||
"lockTime": 0
|
||||
"eidolonOverride": "",
|
||||
"vallisOverride": "",
|
||||
"nightwaveOverride": ""
|
||||
}
|
||||
}
|
||||
|
181
package-lock.json
generated
181
package-lock.json
generated
@ -18,24 +18,20 @@
|
||||
"morgan": "^1.10.0",
|
||||
"ncp": "^2.0.0",
|
||||
"typescript": "^5.5",
|
||||
"warframe-public-export-plus": "^0.5.62",
|
||||
"warframe-public-export-plus": "^0.5.66",
|
||||
"warframe-riven-info": "^0.1.2",
|
||||
"winston": "^3.17.0",
|
||||
"winston-daily-rotate-file": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rxliuli/tsgo": "^2025.3.31",
|
||||
"@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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.15.0",
|
||||
"npm": ">=9.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@colors/colors": {
|
||||
@ -308,32 +304,6 @@
|
||||
"url": "https://opencollective.com/pkgr"
|
||||
}
|
||||
},
|
||||
"node_modules/@rxliuli/tsgo": {
|
||||
"version": "2025.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@rxliuli/tsgo/-/tsgo-2025.5.8.tgz",
|
||||
"integrity": "sha512-P3/qxcUgiWz6nSJslJ5mMeAEqacK8LQSoOhdvHxI1/d0Xqxt2Qp6/nmhWuOlyqnCyAaIoXgoiUshiXWBGr2jaw==",
|
||||
"cpu": [
|
||||
"x64",
|
||||
"ia32",
|
||||
"arm",
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"os": [
|
||||
"darwin",
|
||||
"linux",
|
||||
"win32",
|
||||
"freebsd"
|
||||
],
|
||||
"bin": {
|
||||
"tsgo": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||
@ -695,6 +665,147 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/native-preview": {
|
||||
"version": "7.0.0-dev.20250523.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20250523.1.tgz",
|
||||
"integrity": "sha512-CgdgP/gmyaMThY7Fho19nDaTVryn9QV/zD/6w1KfDCn3M4Rq4WvkSc7Ob1ohc4V1XjCSIzg6Ul+HbLEc7xvV4Q==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsgo": "bin/tsgo.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.6.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@typescript/native-preview-darwin-arm64": "7.0.0-dev.20250523.1",
|
||||
"@typescript/native-preview-darwin-x64": "7.0.0-dev.20250523.1",
|
||||
"@typescript/native-preview-linux-arm": "7.0.0-dev.20250523.1",
|
||||
"@typescript/native-preview-linux-arm64": "7.0.0-dev.20250523.1",
|
||||
"@typescript/native-preview-linux-x64": "7.0.0-dev.20250523.1",
|
||||
"@typescript/native-preview-win32-arm64": "7.0.0-dev.20250523.1",
|
||||
"@typescript/native-preview-win32-x64": "7.0.0-dev.20250523.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/native-preview-darwin-arm64": {
|
||||
"version": "7.0.0-dev.20250523.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20250523.1.tgz",
|
||||
"integrity": "sha512-oWJMPD+lfH9/dvHhPSZdTv43lfyZGrn7crytefhkiQPSwP0MIUCpnDkofGP/ML1nv0xx0pwWhH+Ein88NW3LuA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/native-preview-darwin-x64": {
|
||||
"version": "7.0.0-dev.20250523.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20250523.1.tgz",
|
||||
"integrity": "sha512-Yk8bJEsYsRKgRqYlwPvh7DPdgBMC/oPN60X0LWeuMLci65+4kyqF8Cv6K/W3ABc005cB4tYn4iR+9T6zipvrKw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/native-preview-linux-arm": {
|
||||
"version": "7.0.0-dev.20250523.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20250523.1.tgz",
|
||||
"integrity": "sha512-B+8CRIv6ebL8gzAagnJP8wml3baFV2FtFWuXYl6jlAcLGoQOh/yGdcAueZoJjJKNod4gAOl8OJoTicuC0BVIxw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/native-preview-linux-arm64": {
|
||||
"version": "7.0.0-dev.20250523.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20250523.1.tgz",
|
||||
"integrity": "sha512-IErNI08z9qE6mHaJaT6tM7il8j21ryH3DNVyFP4yz5FTKnkXFj1Kb4NcI41Q8w226LTQgBR8kNErVlbUWr7ywA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/native-preview-linux-x64": {
|
||||
"version": "7.0.0-dev.20250523.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20250523.1.tgz",
|
||||
"integrity": "sha512-TCZtknsLUgPRaEfX9CvBZNgrHhMRZPYYZgF1Aasdv0PONv9mB8w0Xforgxoo4UFjdF5ZzOu2icgc7sKJJeu5vw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/native-preview-win32-arm64": {
|
||||
"version": "7.0.0-dev.20250523.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20250523.1.tgz",
|
||||
"integrity": "sha512-bulwrkLEkoY4Jqeuvfz24RiVOiZZ7Rr9TblFqZAgZFZOnyXuhjM1jE8F1hnJFC5AghJe2HdLD3EKfabqlffrIw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/native-preview-win32-x64": {
|
||||
"version": "7.0.0-dev.20250523.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20250523.1.tgz",
|
||||
"integrity": "sha512-ztzfO0oF/rj8xO5y3SyAcigmgvgczrqobCugEWFqiYumteWZPN2MYWcNYk2k8Y5LAgg1fN1xHIg8RRSPoo6XUg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ungap/structured-clone": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
||||
@ -3703,9 +3814,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/warframe-public-export-plus": {
|
||||
"version": "0.5.62",
|
||||
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.62.tgz",
|
||||
"integrity": "sha512-D8ZzjkU9rrK/59VqCfpMoV31HVmwHZV1dNZxPO85AOlcjg/G81Fu3kgITQTaw9sdNagLPLQnFaiXY58pxxRwgA=="
|
||||
"version": "0.5.66",
|
||||
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.66.tgz",
|
||||
"integrity": "sha512-AU7XQA96OfYrLm2RioCwDjjdI3IrsmUiqebXyE+bpM0iST+4x/NHu8LTRT4Oygfo/2OBtDYhib7G6re0EeAe5g=="
|
||||
},
|
||||
"node_modules/warframe-riven-info": {
|
||||
"version": "0.1.2",
|
||||
|
@ -25,23 +25,19 @@
|
||||
"morgan": "^1.10.0",
|
||||
"ncp": "^2.0.0",
|
||||
"typescript": "^5.5",
|
||||
"warframe-public-export-plus": "^0.5.62",
|
||||
"warframe-public-export-plus": "^0.5.66",
|
||||
"warframe-riven-info": "^0.1.2",
|
||||
"winston": "^3.17.0",
|
||||
"winston-daily-rotate-file": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rxliuli/tsgo": "^2025.3.31",
|
||||
"@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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.15.0",
|
||||
"npm": ">=9.5.0"
|
||||
}
|
||||
}
|
||||
|
27
src/controllers/api/adoptPetController.ts
Normal file
27
src/controllers/api/adoptPetController.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
|
||||
import { getInventory } from "@/src/services/inventoryService";
|
||||
import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import { RequestHandler } from "express";
|
||||
|
||||
export const adoptPetController: RequestHandler = async (req, res) => {
|
||||
const accountId = await getAccountIdForRequest(req);
|
||||
const inventory = await getInventory(accountId, "KubrowPets");
|
||||
const data = getJSONfromString<IAdoptPetRequest>(String(req.body));
|
||||
const details = inventory.KubrowPets.id(data.petId)!.Details!;
|
||||
details.Name = data.name;
|
||||
await inventory.save();
|
||||
res.json({
|
||||
petId: data.petId,
|
||||
newName: data.name
|
||||
} satisfies IAdoptPetResponse);
|
||||
};
|
||||
|
||||
interface IAdoptPetRequest {
|
||||
petId: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface IAdoptPetResponse {
|
||||
petId: string;
|
||||
newName: string;
|
||||
}
|
@ -17,7 +17,7 @@ import {
|
||||
} from "@/src/services/inventoryService";
|
||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
|
||||
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
|
||||
import { InventorySlot, IPendingRecipeDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
|
||||
import { InventorySlot, IPendingRecipeDatabase, Status } from "@/src/types/inventoryTypes/inventoryTypes";
|
||||
import { toOid2 } from "@/src/helpers/inventoryHelpers";
|
||||
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
|
||||
import { IRecipe } from "warframe-public-export-plus";
|
||||
@ -105,7 +105,21 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
|
||||
...updateCurrency(inventory, cost, true)
|
||||
};
|
||||
}
|
||||
if (recipe.secretIngredientAction != "SIA_UNBRAND") {
|
||||
|
||||
if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") {
|
||||
const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!;
|
||||
if (pet.Details!.HatchDate!.getTime() > Date.now()) {
|
||||
pet.Details!.HatchDate = new Date();
|
||||
}
|
||||
let canSetActive = true;
|
||||
for (const pet of inventory.KubrowPets) {
|
||||
if (pet.Details!.Status == Status.StatusAvailable) {
|
||||
canSetActive = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
pet.Details!.Status = canSetActive ? Status.StatusAvailable : Status.StatusStasis;
|
||||
} else if (recipe.secretIngredientAction != "SIA_UNBRAND") {
|
||||
InventoryChanges = {
|
||||
...InventoryChanges,
|
||||
...(await addItem(
|
||||
@ -118,7 +132,10 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
|
||||
))
|
||||
};
|
||||
}
|
||||
if (config.claimingBlueprintRefundsIngredients) {
|
||||
if (
|
||||
config.claimingBlueprintRefundsIngredients &&
|
||||
recipe.secretIngredientAction != "SIA_CREATE_KUBROW" // Can't refund the egg
|
||||
) {
|
||||
await refundRecipeIngredients(inventory, InventoryChanges, recipe, pendingRecipe);
|
||||
}
|
||||
await inventory.save();
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { RequestHandler } from "express";
|
||||
import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import { getAccountForRequest } from "@/src/services/loginService";
|
||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
|
||||
import { getInventory, addMiscItems, updateCurrency, addRecipes, freeUpSlot } from "@/src/services/inventoryService";
|
||||
import { IOid } from "@/src/types/commonTypes";
|
||||
@ -12,7 +12,7 @@ import {
|
||||
} from "@/src/types/inventoryTypes/inventoryTypes";
|
||||
import { ExportMisc } from "warframe-public-export-plus";
|
||||
import { getRecipe } from "@/src/services/itemDataService";
|
||||
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
|
||||
import { toMongoDate, version_compare } from "@/src/helpers/inventoryHelpers";
|
||||
import { logger } from "@/src/utils/logger";
|
||||
import { colorToShard } from "@/src/helpers/shardHelper";
|
||||
import { config } from "@/src/services/configService";
|
||||
@ -23,12 +23,12 @@ import {
|
||||
} from "@/src/services/infestedFoundryService";
|
||||
|
||||
export const infestedFoundryController: RequestHandler = async (req, res) => {
|
||||
const accountId = await getAccountIdForRequest(req);
|
||||
const account = await getAccountForRequest(req);
|
||||
switch (req.query.mode) {
|
||||
case "s": {
|
||||
// shard installation
|
||||
const request = getJSONfromString<IShardInstallRequest>(String(req.body));
|
||||
const inventory = await getInventory(accountId);
|
||||
const inventory = await getInventory(account._id.toString());
|
||||
const suit = inventory.Suits.id(request.SuitId.$oid)!;
|
||||
if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) {
|
||||
suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}];
|
||||
@ -56,7 +56,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
|
||||
case "x": {
|
||||
// shard removal
|
||||
const request = getJSONfromString<IShardUninstallRequest>(String(req.body));
|
||||
const inventory = await getInventory(accountId);
|
||||
const inventory = await getInventory(account._id.toString());
|
||||
const suit = inventory.Suits.id(request.SuitId.$oid)!;
|
||||
|
||||
const miscItemChanges: IMiscItem[] = [];
|
||||
@ -70,19 +70,30 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
|
||||
ItemCount: 1
|
||||
});
|
||||
addMiscItems(inventory, miscItemChanges);
|
||||
|
||||
// consume resources
|
||||
if (!config.infiniteHelminthMaterials) {
|
||||
let type: string;
|
||||
let count: number;
|
||||
if (account.BuildLabel && version_compare(account.BuildLabel, "2025.05.20.10.18") < 0) {
|
||||
// < 38.6.0
|
||||
type = "/Lotus/Types/Items/InfestedFoundry/HelminthBile";
|
||||
count = 300;
|
||||
} else {
|
||||
// >= 38.6.0
|
||||
type =
|
||||
archonCrystalRemovalResource[
|
||||
suit.ArchonCrystalUpgrades![request.Slot].Color!.replace("_MYTHIC", "")
|
||||
];
|
||||
count = suit.ArchonCrystalUpgrades![request.Slot].Color!.indexOf("_MYTHIC") != -1 ? 300 : 150;
|
||||
}
|
||||
inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == type)!.Count -= count;
|
||||
}
|
||||
}
|
||||
|
||||
// remove from suit
|
||||
suit.ArchonCrystalUpgrades![request.Slot] = {};
|
||||
|
||||
if (!config.infiniteHelminthMaterials) {
|
||||
// remove bile
|
||||
const bile = inventory.InfestedFoundry!.Resources!.find(
|
||||
x => x.ItemType == "/Lotus/Types/Items/InfestedFoundry/HelminthBile"
|
||||
)!;
|
||||
bile.Count -= 300;
|
||||
}
|
||||
|
||||
await inventory.save();
|
||||
|
||||
const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
|
||||
@ -99,7 +110,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
|
||||
case "n": {
|
||||
// name the beast
|
||||
const request = getJSONfromString<IHelminthNameRequest>(String(req.body));
|
||||
const inventory = await getInventory(accountId);
|
||||
const inventory = await getInventory(account._id.toString());
|
||||
inventory.InfestedFoundry ??= {};
|
||||
inventory.InfestedFoundry.Name = request.newName;
|
||||
await inventory.save();
|
||||
@ -122,7 +133,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
|
||||
}
|
||||
|
||||
const request = getJSONfromString<IHelminthFeedRequest>(String(req.body));
|
||||
const inventory = await getInventory(accountId);
|
||||
const inventory = await getInventory(account._id.toString());
|
||||
inventory.InfestedFoundry ??= {};
|
||||
inventory.InfestedFoundry.Resources ??= [];
|
||||
|
||||
@ -218,7 +229,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
|
||||
case "o": {
|
||||
// offerings update
|
||||
const request = getJSONfromString<IHelminthOfferingsUpdate>(String(req.body));
|
||||
const inventory = await getInventory(accountId);
|
||||
const inventory = await getInventory(account._id.toString());
|
||||
inventory.InfestedFoundry ??= {};
|
||||
inventory.InfestedFoundry.InvigorationIndex = request.OfferingsIndex;
|
||||
inventory.InfestedFoundry.InvigorationSuitOfferings = request.SuitTypes;
|
||||
@ -239,7 +250,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
|
||||
case "a": {
|
||||
// subsume warframe
|
||||
const request = getJSONfromString<IHelminthSubsumeRequest>(String(req.body));
|
||||
const inventory = await getInventory(accountId);
|
||||
const inventory = await getInventory(account._id.toString());
|
||||
const recipe = getRecipe(request.Recipe)!;
|
||||
if (!config.infiniteHelminthMaterials) {
|
||||
for (const ingredient of recipe.secretIngredients!) {
|
||||
@ -289,7 +300,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
|
||||
|
||||
case "r": {
|
||||
// rush subsume
|
||||
const inventory = await getInventory(accountId);
|
||||
const inventory = await getInventory(account._id.toString());
|
||||
const currencyChanges = updateCurrency(inventory, 50, true);
|
||||
const recipeChanges = handleSubsumeCompletion(inventory);
|
||||
await inventory.save();
|
||||
@ -307,7 +318,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
|
||||
|
||||
case "u": {
|
||||
const request = getJSONfromString<IHelminthInvigorationRequest>(String(req.body));
|
||||
const inventory = await getInventory(accountId);
|
||||
const inventory = await getInventory(account._id.toString());
|
||||
const suit = inventory.Suits.id(request.SuitId.$oid)!;
|
||||
const upgradesExpiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
||||
suit.OffensiveUpgrade = request.OffensiveUpgradeType;
|
||||
@ -340,7 +351,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
|
||||
}
|
||||
|
||||
case "custom_unlockall": {
|
||||
const inventory = await getInventory(accountId);
|
||||
const inventory = await getInventory(account._id.toString());
|
||||
inventory.InfestedFoundry ??= {};
|
||||
inventory.InfestedFoundry.XP ??= 0;
|
||||
if (151875_00 > inventory.InfestedFoundry.XP) {
|
||||
@ -439,3 +450,12 @@ const apetiteModel = (x: number): number => {
|
||||
}
|
||||
return 3;
|
||||
};
|
||||
|
||||
const archonCrystalRemovalResource: Record<string, string> = {
|
||||
ACC_RED: "/Lotus/Types/Items/InfestedFoundry/HelminthOxides",
|
||||
ACC_YELLOW: "/Lotus/Types/Items/InfestedFoundry/HelminthBile",
|
||||
ACC_BLUE: "/Lotus/Types/Items/InfestedFoundry/HelminthSynthetics",
|
||||
ACC_GREEN: "/Lotus/Types/Items/InfestedFoundry/HelminthBiotics",
|
||||
ACC_ORANGE: "/Lotus/Types/Items/InfestedFoundry/HelminthPheromones",
|
||||
ACC_PURPLE: "/Lotus/Types/Items/InfestedFoundry/HelminthCalx"
|
||||
};
|
||||
|
@ -310,7 +310,7 @@ export const getInventoryResponse = async (
|
||||
// Fix nemesis for older versions
|
||||
if (
|
||||
inventoryResponse.Nemesis &&
|
||||
version_compare(getNemesisManifest(inventoryResponse.Nemesis.manifest).minBuild, buildLabel) < 0
|
||||
version_compare(buildLabel, getNemesisManifest(inventoryResponse.Nemesis.manifest).minBuild) < 0
|
||||
) {
|
||||
inventoryResponse.Nemesis = undefined;
|
||||
}
|
||||
|
@ -20,7 +20,21 @@ export const loginController: RequestHandler = async (request, response) => {
|
||||
? request.query.buildLabel.split(" ").join("+")
|
||||
: buildConfig.buildLabel;
|
||||
|
||||
const myAddress = request.host.indexOf("warframe.com") == -1 ? request.host : config.myAddress;
|
||||
let myAddress: string;
|
||||
let myUrlBase: string = request.protocol + "://";
|
||||
if (request.host.indexOf("warframe.com") == -1) {
|
||||
// Client request was redirected cleanly, so we know it can reach us how it's reaching us now.
|
||||
myAddress = request.hostname;
|
||||
myUrlBase += request.host;
|
||||
} else {
|
||||
// Don't know how the client reached us, hoping the config does.
|
||||
myAddress = config.myAddress;
|
||||
myUrlBase += myAddress;
|
||||
const port: number = request.protocol == "http" ? config.httpPort || 80 : config.httpsPort || 443;
|
||||
if (port != (request.protocol == "http" ? 80 : 443)) {
|
||||
myUrlBase += ":" + port;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!account &&
|
||||
@ -52,7 +66,7 @@ export const loginController: RequestHandler = async (request, response) => {
|
||||
LastLogin: new Date()
|
||||
});
|
||||
logger.debug("created new account");
|
||||
response.json(createLoginResponse(myAddress, newAccount, buildLabel));
|
||||
response.json(createLoginResponse(myAddress, myUrlBase, newAccount, buildLabel));
|
||||
return;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
@ -98,10 +112,15 @@ export const loginController: RequestHandler = async (request, response) => {
|
||||
}
|
||||
await account.save();
|
||||
|
||||
response.json(createLoginResponse(myAddress, account.toJSON(), buildLabel));
|
||||
response.json(createLoginResponse(myAddress, myUrlBase, account.toJSON(), buildLabel));
|
||||
};
|
||||
|
||||
const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, buildLabel: string): ILoginResponse => {
|
||||
const createLoginResponse = (
|
||||
myAddress: string,
|
||||
myUrlBase: string,
|
||||
account: IDatabaseAccountJson,
|
||||
buildLabel: string
|
||||
): ILoginResponse => {
|
||||
const resp: ILoginResponse = {
|
||||
id: account.id,
|
||||
DisplayName: account.DisplayName,
|
||||
@ -139,11 +158,11 @@ const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, b
|
||||
}
|
||||
if (version_compare(buildLabel, "2022.09.06.19.24") >= 0) {
|
||||
resp.CrossPlatformAllowed = account.CrossPlatformAllowed;
|
||||
resp.HUB = `https://${myAddress}/api/`;
|
||||
resp.HUB = `${myUrlBase}/api/`;
|
||||
resp.MatchmakingBuildId = buildConfig.matchmakingBuildId;
|
||||
}
|
||||
if (version_compare(buildLabel, "2023.04.25.23.40") >= 0) {
|
||||
resp.platformCDNs = [`https://${myAddress}/`];
|
||||
resp.platformCDNs = [`${myUrlBase}/`];
|
||||
}
|
||||
return resp;
|
||||
};
|
||||
|
@ -57,7 +57,7 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
|
||||
const firstCompletion = missionReport.SortieId
|
||||
? inventory.CompletedSorties.indexOf(missionReport.SortieId) == -1
|
||||
: false;
|
||||
const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport);
|
||||
const inventoryUpdates = await addMissionInventoryUpdates(account, inventory, missionReport);
|
||||
|
||||
if (
|
||||
missionReport.MissionStatus !== "GS_SUCCESS" &&
|
||||
|
@ -1,12 +1,18 @@
|
||||
import { version_compare } from "@/src/helpers/inventoryHelpers";
|
||||
import {
|
||||
consumeModCharge,
|
||||
decodeNemesisGuess,
|
||||
encodeNemesisGuess,
|
||||
getInfNodes,
|
||||
getKnifeUpgrade,
|
||||
getNemesisManifest,
|
||||
getNemesisPasscode,
|
||||
getNemesisPasscodeModTypes,
|
||||
GUESS_CORRECT,
|
||||
GUESS_INCORRECT,
|
||||
GUESS_NEUTRAL,
|
||||
GUESS_NONE,
|
||||
GUESS_WILDCARD,
|
||||
IKnifeResponse
|
||||
} from "@/src/helpers/nemesisHelpers";
|
||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
|
||||
@ -82,7 +88,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i != 3; ++i) {
|
||||
if (body.guess[i] == passcode[i]) {
|
||||
if (body.guess[i] == passcode[i] || body.guess[i] == GUESS_WILDCARD) {
|
||||
++guessResult;
|
||||
}
|
||||
}
|
||||
@ -97,18 +103,29 @@ export const nemesisController: RequestHandler = async (req, res) => {
|
||||
if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
|
||||
const guess: number[] = [body.guess & 0xf, (body.guess >> 4) & 0xf, (body.guess >> 8) & 0xf];
|
||||
const passcode = getNemesisPasscode(inventory.Nemesis!)[0];
|
||||
|
||||
// Add to GuessHistory
|
||||
const result1 = passcode == guess[0] ? 0 : 1;
|
||||
const result2 = passcode == guess[1] ? 0 : 1;
|
||||
const result3 = passcode == guess[2] ? 0 : 1;
|
||||
const result1 = passcode == guess[0] ? GUESS_CORRECT : GUESS_INCORRECT;
|
||||
const result2 = passcode == guess[1] ? GUESS_CORRECT : GUESS_INCORRECT;
|
||||
const result3 = passcode == guess[2] ? GUESS_CORRECT : GUESS_INCORRECT;
|
||||
inventory.Nemesis!.GuessHistory.push(
|
||||
encodeNemesisGuess(guess[0], result1, guess[1], result2, guess[2], result3)
|
||||
encodeNemesisGuess([
|
||||
{
|
||||
symbol: guess[0],
|
||||
result: result1
|
||||
},
|
||||
{
|
||||
symbol: guess[1],
|
||||
result: result2
|
||||
},
|
||||
{
|
||||
symbol: guess[2],
|
||||
result: result3
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
// Increase antivirus if correct antivirus mod is installed
|
||||
const response: IKnifeResponse = {};
|
||||
if (result1 == 0 || result2 == 0 || result3 == 0) {
|
||||
if (result1 == GUESS_CORRECT || result2 == GUESS_CORRECT || result3 == GUESS_CORRECT) {
|
||||
let antivirusGain = 5;
|
||||
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
|
||||
const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid);
|
||||
@ -149,18 +166,48 @@ export const nemesisController: RequestHandler = async (req, res) => {
|
||||
await inventory.save();
|
||||
res.json(response);
|
||||
} else {
|
||||
const passcode = getNemesisPasscode(inventory.Nemesis!);
|
||||
if (passcode[body.position] != body.guess) {
|
||||
res.end();
|
||||
} else {
|
||||
inventory.Nemesis!.Rank += 1;
|
||||
inventory.Nemesis!.InfNodes = getInfNodes(
|
||||
getNemesisManifest(inventory.Nemesis!.manifest),
|
||||
inventory.Nemesis!.Rank
|
||||
// For first guess, create a new entry.
|
||||
if (body.position == 0) {
|
||||
inventory.Nemesis!.GuessHistory.push(
|
||||
encodeNemesisGuess([
|
||||
{
|
||||
symbol: GUESS_NONE,
|
||||
result: GUESS_NEUTRAL
|
||||
},
|
||||
{
|
||||
symbol: GUESS_NONE,
|
||||
result: GUESS_NEUTRAL
|
||||
},
|
||||
{
|
||||
symbol: GUESS_NONE,
|
||||
result: GUESS_NEUTRAL
|
||||
}
|
||||
])
|
||||
);
|
||||
await inventory.save();
|
||||
res.json({ RankIncrease: 1 });
|
||||
}
|
||||
|
||||
// Evaluate guess
|
||||
const correct =
|
||||
body.guess == GUESS_WILDCARD || getNemesisPasscode(inventory.Nemesis!)[body.position] == body.guess;
|
||||
|
||||
// Update entry
|
||||
const guess = decodeNemesisGuess(
|
||||
inventory.Nemesis!.GuessHistory[inventory.Nemesis!.GuessHistory.length - 1]
|
||||
);
|
||||
guess[body.position].symbol = body.guess;
|
||||
guess[body.position].result = correct ? GUESS_CORRECT : GUESS_INCORRECT;
|
||||
inventory.Nemesis!.GuessHistory[inventory.Nemesis!.GuessHistory.length - 1] = encodeNemesisGuess(guess);
|
||||
|
||||
// Increase rank if incorrect
|
||||
let RankIncrease: number | undefined;
|
||||
if (!correct) {
|
||||
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 });
|
||||
}
|
||||
} else if ((req.query.mode as string) == "rs") {
|
||||
// report spawn; POST but no application data in body
|
||||
@ -170,11 +217,14 @@ export const nemesisController: RequestHandler = async (req, res) => {
|
||||
res.json({ LastEnc: inventory.Nemesis!.LastEnc });
|
||||
} else if ((req.query.mode as string) == "s") {
|
||||
const inventory = await getInventory(account._id.toString(), "Nemesis");
|
||||
if (inventory.Nemesis) {
|
||||
logger.warn(`overwriting an existing nemesis as a new one is being requested`);
|
||||
}
|
||||
const body = getJSONfromString<INemesisStartRequest>(String(req.body));
|
||||
body.target.fp = BigInt(body.target.fp);
|
||||
|
||||
const manifest = getNemesisManifest(body.target.manifest);
|
||||
if (account.BuildLabel && version_compare(manifest.minBuild, account.BuildLabel) < 0) {
|
||||
if (account.BuildLabel && version_compare(account.BuildLabel, manifest.minBuild) < 0) {
|
||||
logger.warn(
|
||||
`client on version ${account.BuildLabel} provided nemesis manifest ${body.target.manifest} which was expected to require ${manifest.minBuild} or above. please file a bug report.`
|
||||
);
|
||||
@ -185,13 +235,15 @@ export const nemesisController: RequestHandler = async (req, res) => {
|
||||
const weapons: readonly string[] = manifest.weapons;
|
||||
const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1);
|
||||
weaponIdx = initialWeaponIdx;
|
||||
do {
|
||||
const weapon = weapons[weaponIdx];
|
||||
if (body.target.DisallowedWeapons.indexOf(weapon) == -1) {
|
||||
break;
|
||||
}
|
||||
weaponIdx = (weaponIdx + 1) % weapons.length;
|
||||
} while (weaponIdx != initialWeaponIdx);
|
||||
if (body.target.DisallowedWeapons) {
|
||||
do {
|
||||
const weapon = weapons[weaponIdx];
|
||||
if (body.target.DisallowedWeapons.indexOf(weapon) == -1) {
|
||||
break;
|
||||
}
|
||||
weaponIdx = (weaponIdx + 1) % weapons.length;
|
||||
} while (weaponIdx != initialWeaponIdx);
|
||||
}
|
||||
}
|
||||
|
||||
inventory.Nemesis = {
|
||||
@ -212,10 +264,10 @@ export const nemesisController: RequestHandler = async (req, res) => {
|
||||
GuessHistory: [],
|
||||
Hints: [],
|
||||
HintProgress: 0,
|
||||
Weakened: body.target.Weakened,
|
||||
Weakened: false,
|
||||
PrevOwners: 0,
|
||||
HenchmenKilled: 0,
|
||||
SecondInCommand: body.target.SecondInCommand,
|
||||
SecondInCommand: false,
|
||||
MissionCount: 0,
|
||||
LastEnc: 0
|
||||
};
|
||||
@ -276,7 +328,7 @@ interface INemesisStartRequest {
|
||||
KillingSuit: string;
|
||||
killingDamageType: number;
|
||||
ShoulderHelmet: string;
|
||||
DisallowedWeapons: string[];
|
||||
DisallowedWeapons?: string[];
|
||||
WeaponIdx: number;
|
||||
AgentIdx: number;
|
||||
BirthNode: string;
|
||||
|
23
src/controllers/api/renamePetController.ts
Normal file
23
src/controllers/api/renamePetController.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
|
||||
import { getInventory, updateCurrency } from "@/src/services/inventoryService";
|
||||
import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import { RequestHandler } from "express";
|
||||
|
||||
export const renamePetController: RequestHandler = async (req, res) => {
|
||||
const accountId = await getAccountIdForRequest(req);
|
||||
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);
|
||||
await inventory.save();
|
||||
res.json({
|
||||
...data,
|
||||
inventoryChanges: currencyChanges
|
||||
});
|
||||
};
|
||||
|
||||
interface IRenamePetRequest {
|
||||
petId: string;
|
||||
name: string;
|
||||
}
|
@ -3,12 +3,14 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers";
|
||||
import { logger } from "@/src/utils/logger";
|
||||
import { RequestHandler } from "express";
|
||||
import { getRecipe } from "@/src/services/itemDataService";
|
||||
import { addItem, freeUpSlot, getInventory, updateCurrency } from "@/src/services/inventoryService";
|
||||
import { addItem, addKubrowPet, freeUpSlot, getInventory, updateCurrency } from "@/src/services/inventoryService";
|
||||
import { unixTimesInMs } from "@/src/constants/timeConstants";
|
||||
import { Types } from "mongoose";
|
||||
import { InventorySlot, ISpectreLoadout } from "@/src/types/inventoryTypes/inventoryTypes";
|
||||
import { toOid } from "@/src/helpers/inventoryHelpers";
|
||||
import { fromOid, toOid } from "@/src/helpers/inventoryHelpers";
|
||||
import { ExportWeapons } from "warframe-public-export-plus";
|
||||
import { getRandomElement } from "@/src/services/rngService";
|
||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
|
||||
|
||||
interface IStartRecipeRequest {
|
||||
RecipeName: string;
|
||||
@ -42,24 +44,35 @@ export const startRecipeController: RequestHandler = async (req, res) => {
|
||||
|
||||
for (let i = 0; i != recipe.ingredients.length; ++i) {
|
||||
if (startRecipeRequest.Ids[i] && startRecipeRequest.Ids[i][0] != "/") {
|
||||
const category = ExportWeapons[recipe.ingredients[i].ItemType].productCategory;
|
||||
if (category != "LongGuns" && category != "Pistols" && category != "Melee") {
|
||||
throw new Error(`unexpected equipment ingredient type: ${category}`);
|
||||
if (recipe.ingredients[i].ItemType == "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem") {
|
||||
const index = inventory.KubrowPetEggs!.findIndex(x => x._id.equals(startRecipeRequest.Ids[i]));
|
||||
if (index != -1) {
|
||||
inventory.KubrowPetEggs!.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
const category = ExportWeapons[recipe.ingredients[i].ItemType].productCategory;
|
||||
if (category != "LongGuns" && category != "Pistols" && category != "Melee") {
|
||||
throw new Error(`unexpected equipment ingredient type: ${category}`);
|
||||
}
|
||||
const equipmentIndex = inventory[category].findIndex(x => x._id.equals(startRecipeRequest.Ids[i]));
|
||||
if (equipmentIndex == -1) {
|
||||
throw new Error(`could not find equipment item to use for recipe`);
|
||||
}
|
||||
pr[category] ??= [];
|
||||
pr[category].push(inventory[category][equipmentIndex]);
|
||||
inventory[category].splice(equipmentIndex, 1);
|
||||
freeUpSlot(inventory, InventorySlot.WEAPONS);
|
||||
}
|
||||
const equipmentIndex = inventory[category].findIndex(x => x._id.equals(startRecipeRequest.Ids[i]));
|
||||
if (equipmentIndex == -1) {
|
||||
throw new Error(`could not find equipment item to use for recipe`);
|
||||
}
|
||||
pr[category] ??= [];
|
||||
pr[category].push(inventory[category][equipmentIndex]);
|
||||
inventory[category].splice(equipmentIndex, 1);
|
||||
freeUpSlot(inventory, InventorySlot.WEAPONS);
|
||||
} else {
|
||||
await addItem(inventory, recipe.ingredients[i].ItemType, recipe.ingredients[i].ItemCount * -1);
|
||||
}
|
||||
}
|
||||
|
||||
if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
|
||||
let inventoryChanges: IInventoryChanges | undefined;
|
||||
if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") {
|
||||
inventoryChanges = addKubrowPet(inventory, getRandomElement(recipe.secretIngredients!)!.ItemType);
|
||||
pr.KubrowPet = new Types.ObjectId(fromOid(inventoryChanges.KubrowPets![0].ItemId));
|
||||
} else if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
|
||||
const spectreLoadout: ISpectreLoadout = {
|
||||
ItemType: recipe.resultType,
|
||||
Suits: "",
|
||||
@ -116,5 +129,5 @@ export const startRecipeController: RequestHandler = async (req, res) => {
|
||||
|
||||
await inventory.save();
|
||||
|
||||
res.json({ RecipeId: toOid(pr._id) });
|
||||
res.json({ RecipeId: toOid(pr._id), InventoryChanges: inventoryChanges });
|
||||
};
|
||||
|
@ -1,15 +1,13 @@
|
||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
|
||||
import { RequestHandler } from "express";
|
||||
import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import { ExportNightwave, ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus";
|
||||
import { ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus";
|
||||
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
|
||||
import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService";
|
||||
import { addMiscItem, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService";
|
||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
|
||||
import { isStoreItem, toStoreItem } from "@/src/services/itemDataService";
|
||||
import { toStoreItem } from "@/src/services/itemDataService";
|
||||
import { logger } from "@/src/utils/logger";
|
||||
|
||||
const nightwaveCredsItemType = ExportNightwave.rewards[ExportNightwave.rewards.length - 1].uniqueName;
|
||||
|
||||
export const syndicateSacrificeController: RequestHandler = async (request, response) => {
|
||||
const accountId = await getAccountIdForRequest(request);
|
||||
const inventory = await getInventory(accountId);
|
||||
@ -20,74 +18,83 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
|
||||
syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: data.AffiliationTag, Standing: 0 }) - 1];
|
||||
}
|
||||
|
||||
const level = data.SacrificeLevel - (syndicate.Title ?? 0);
|
||||
const oldLevel = syndicate.Title ?? 0;
|
||||
const levelIncrease = data.SacrificeLevel - oldLevel;
|
||||
if (levelIncrease < 0) {
|
||||
throw new Error(`syndicate sacrifice can not decrease level`);
|
||||
}
|
||||
if (levelIncrease > 1 && !data.AllowMultiple) {
|
||||
throw new Error(`desired syndicate level is an increase of ${levelIncrease}, max. allowed increase is 1`);
|
||||
}
|
||||
|
||||
const res: ISyndicateSacrificeResponse = {
|
||||
AffiliationTag: data.AffiliationTag,
|
||||
InventoryChanges: {},
|
||||
Level: data.SacrificeLevel,
|
||||
LevelIncrease: level <= 0 ? 1 : level,
|
||||
LevelIncrease: levelIncrease,
|
||||
NewEpisodeReward: false
|
||||
};
|
||||
|
||||
// Process sacrifices and rewards for every level we're reaching
|
||||
const manifest = ExportSyndicates[data.AffiliationTag];
|
||||
let sacrifice: ISyndicateSacrifice | undefined;
|
||||
let reward: string | undefined;
|
||||
if (data.SacrificeLevel == 0) {
|
||||
sacrifice = manifest.initiationSacrifice;
|
||||
reward = manifest.initiationReward;
|
||||
syndicate.Initiated = true;
|
||||
} else {
|
||||
sacrifice = manifest.titles?.find(x => x.level == data.SacrificeLevel)?.sacrifice;
|
||||
}
|
||||
for (let level = oldLevel + levelIncrease; level <= data.SacrificeLevel; ++level) {
|
||||
let sacrifice: ISyndicateSacrifice | undefined;
|
||||
if (level == 0) {
|
||||
sacrifice = manifest.initiationSacrifice;
|
||||
if (manifest.initiationReward) {
|
||||
combineInventoryChanges(
|
||||
res.InventoryChanges,
|
||||
(await handleStoreItemAcquisition(manifest.initiationReward, inventory)).InventoryChanges
|
||||
);
|
||||
}
|
||||
syndicate.Initiated = true;
|
||||
} else {
|
||||
sacrifice = manifest.titles?.find(x => x.level == level)?.sacrifice;
|
||||
}
|
||||
|
||||
if (sacrifice) {
|
||||
res.InventoryChanges = { ...updateCurrency(inventory, sacrifice.credits, false) };
|
||||
if (sacrifice) {
|
||||
updateCurrency(inventory, sacrifice.credits, false, res.InventoryChanges);
|
||||
|
||||
const miscItemChanges = sacrifice.items.map(x => ({
|
||||
ItemType: x.ItemType,
|
||||
ItemCount: x.ItemCount * -1
|
||||
}));
|
||||
addMiscItems(inventory, miscItemChanges);
|
||||
res.InventoryChanges.MiscItems = miscItemChanges;
|
||||
}
|
||||
for (const item of sacrifice.items) {
|
||||
addMiscItem(inventory, item.ItemType, item.ItemCount * -1, res.InventoryChanges);
|
||||
}
|
||||
}
|
||||
|
||||
syndicate.Title ??= 0;
|
||||
syndicate.Title += 1;
|
||||
|
||||
if (syndicate.Title > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == syndicate.Title)) {
|
||||
syndicate.FreeFavorsEarned ??= [];
|
||||
if (!syndicate.FreeFavorsEarned.includes(syndicate.Title)) {
|
||||
syndicate.FreeFavorsEarned.push(syndicate.Title);
|
||||
}
|
||||
}
|
||||
|
||||
if (reward) {
|
||||
combineInventoryChanges(
|
||||
res.InventoryChanges,
|
||||
(await handleStoreItemAcquisition(reward, inventory)).InventoryChanges
|
||||
);
|
||||
}
|
||||
|
||||
if (data.AffiliationTag == ExportNightwave.affiliationTag) {
|
||||
const index = syndicate.Title - 1;
|
||||
if (index < ExportNightwave.rewards.length) {
|
||||
res.NewEpisodeReward = true;
|
||||
const reward = ExportNightwave.rewards[index];
|
||||
let rewardType = reward.uniqueName;
|
||||
if (!isStoreItem(rewardType)) {
|
||||
rewardType = toStoreItem(rewardType);
|
||||
}
|
||||
const rewardInventoryChanges = (await handleStoreItemAcquisition(rewardType, inventory, reward.itemCount))
|
||||
.InventoryChanges;
|
||||
if (Object.keys(rewardInventoryChanges).length == 0) {
|
||||
logger.debug(`nightwave rank up reward did not seem to get added, giving 50 creds instead`);
|
||||
rewardInventoryChanges.MiscItems = [{ ItemType: nightwaveCredsItemType, ItemCount: 50 }];
|
||||
addMiscItems(inventory, rewardInventoryChanges.MiscItems);
|
||||
}
|
||||
combineInventoryChanges(res.InventoryChanges, rewardInventoryChanges);
|
||||
// Quacks like a nightwave syndicate?
|
||||
if (manifest.dailyChallenges) {
|
||||
const title = manifest.titles!.find(x => x.level == level);
|
||||
if (title) {
|
||||
res.NewEpisodeReward = true;
|
||||
let rewardType: string;
|
||||
let rewardCount: number;
|
||||
if (title.storeItemReward) {
|
||||
rewardType = title.storeItemReward;
|
||||
rewardCount = 1;
|
||||
} else {
|
||||
rewardType = toStoreItem(title.reward!.ItemType);
|
||||
rewardCount = title.reward!.ItemCount;
|
||||
}
|
||||
const rewardInventoryChanges = (await handleStoreItemAcquisition(rewardType, inventory, rewardCount))
|
||||
.InventoryChanges;
|
||||
if (Object.keys(rewardInventoryChanges).length == 0) {
|
||||
logger.debug(`nightwave rank up reward did not seem to get added, giving 50 creds instead`);
|
||||
const nightwaveCredsItemType = manifest.titles![0].reward!.ItemType;
|
||||
addMiscItem(inventory, nightwaveCredsItemType, 50, rewardInventoryChanges);
|
||||
}
|
||||
combineInventoryChanges(res.InventoryChanges, rewardInventoryChanges);
|
||||
}
|
||||
} else {
|
||||
if (level > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == level)) {
|
||||
syndicate.FreeFavorsEarned ??= [];
|
||||
if (!syndicate.FreeFavorsEarned.includes(level)) {
|
||||
syndicate.FreeFavorsEarned.push(level);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Commit
|
||||
syndicate.Title = data.SacrificeLevel;
|
||||
await inventory.save();
|
||||
|
||||
response.json(res);
|
||||
|
@ -35,6 +35,17 @@ const trainingResultController: RequestHandler = async (req, res): Promise<void>
|
||||
inventory.PlayerLevel += 1;
|
||||
inventory.TradesRemaining += 1;
|
||||
|
||||
if (inventory.PlayerLevel == 2) {
|
||||
await createMessage(accountId, [
|
||||
{
|
||||
sndr: "/Lotus/Language/Game/Maroo",
|
||||
msg: "/Lotus/Language/Clan/MarooClanSearchDesc",
|
||||
sub: "/Lotus/Language/Clan/MarooClanSearchTitle",
|
||||
icon: "/Lotus/Interface/Icons/Npcs/Maroo.png"
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
await createMessage(accountId, [
|
||||
{
|
||||
sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
|
||||
|
@ -1,18 +1,26 @@
|
||||
import { RequestHandler } from "express";
|
||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
|
||||
import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import { getAccountForRequest } from "@/src/services/loginService";
|
||||
import { addChallenges, getInventory } from "@/src/services/inventoryService";
|
||||
import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes";
|
||||
import { IAffiliationMods } from "@/src/types/purchaseTypes";
|
||||
|
||||
export const updateChallengeProgressController: RequestHandler = async (req, res) => {
|
||||
const challenges = getJSONfromString<IUpdateChallengeProgressRequest>(String(req.body));
|
||||
const accountId = await getAccountIdForRequest(req);
|
||||
const account = await getAccountForRequest(req);
|
||||
|
||||
const inventory = await getInventory(accountId, "ChallengeProgress SeasonChallengeHistory Affiliations");
|
||||
const inventory = await getInventory(
|
||||
account._id.toString(),
|
||||
"ChallengeProgress SeasonChallengeHistory Affiliations"
|
||||
);
|
||||
let affiliationMods: IAffiliationMods[] = [];
|
||||
if (challenges.ChallengeProgress) {
|
||||
affiliationMods = addChallenges(inventory, challenges.ChallengeProgress, challenges.SeasonChallengeCompletions);
|
||||
affiliationMods = addChallenges(
|
||||
account,
|
||||
inventory,
|
||||
challenges.ChallengeProgress,
|
||||
challenges.SeasonChallengeCompletions
|
||||
);
|
||||
}
|
||||
if (challenges.SeasonChallengeHistory) {
|
||||
challenges.SeasonChallengeHistory.forEach(({ challenge, id }) => {
|
||||
|
@ -3,6 +3,7 @@ import { getDict, getItemName, getString } from "@/src/services/itemDataService"
|
||||
import {
|
||||
ExportArcanes,
|
||||
ExportAvionics,
|
||||
ExportBoosters,
|
||||
ExportCustoms,
|
||||
ExportDrones,
|
||||
ExportGear,
|
||||
@ -25,6 +26,7 @@ import allIncarnons from "@/static/fixed_responses/allIncarnonList.json";
|
||||
interface ListedItem {
|
||||
uniqueName: string;
|
||||
name: string;
|
||||
subtype?: string;
|
||||
fusionLimit?: number;
|
||||
exalted?: string[];
|
||||
badReason?: "starter" | "frivolous" | "notraw";
|
||||
@ -54,6 +56,7 @@ interface ItemLists {
|
||||
KubrowPets: ListedItem[];
|
||||
EvolutionProgress: ListedItem[];
|
||||
mods: ListedItem[];
|
||||
Boosters: ListedItem[];
|
||||
}
|
||||
|
||||
const relicQualitySuffixes: Record<TRelicQuality, string> = {
|
||||
@ -85,7 +88,8 @@ const getItemListsController: RequestHandler = (req, response) => {
|
||||
QuestKeys: [],
|
||||
KubrowPets: [],
|
||||
EvolutionProgress: [],
|
||||
mods: []
|
||||
mods: [],
|
||||
Boosters: []
|
||||
};
|
||||
for (const [uniqueName, item] of Object.entries(ExportWarframes)) {
|
||||
res[item.productCategory].push({
|
||||
@ -175,7 +179,8 @@ const getItemListsController: RequestHandler = (req, response) => {
|
||||
) {
|
||||
res.miscitems.push({
|
||||
uniqueName: uniqueName,
|
||||
name: name
|
||||
name: name,
|
||||
subtype: "Resource"
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -193,7 +198,8 @@ const getItemListsController: RequestHandler = (req, response) => {
|
||||
for (const [uniqueName, item] of Object.entries(ExportGear)) {
|
||||
res.miscitems.push({
|
||||
uniqueName: uniqueName,
|
||||
name: getString(item.name, lang)
|
||||
name: getString(item.name, lang),
|
||||
subtype: "Gear"
|
||||
});
|
||||
}
|
||||
const recipeNameTemplate = getString("/Lotus/Language/Items/BlueprintAndItem", lang);
|
||||
@ -293,6 +299,13 @@ const getItemListsController: RequestHandler = (req, response) => {
|
||||
});
|
||||
}
|
||||
|
||||
for (const item of Object.values(ExportBoosters)) {
|
||||
res.Boosters.push({
|
||||
uniqueName: item.typeName,
|
||||
name: getString(item.name, lang)
|
||||
});
|
||||
}
|
||||
|
||||
response.json(res);
|
||||
};
|
||||
|
||||
|
45
src/controllers/custom/setBoosterController.ts
Normal file
45
src/controllers/custom/setBoosterController.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import { getInventory } from "@/src/services/inventoryService";
|
||||
import { RequestHandler } from "express";
|
||||
import { ExportBoosters } from "warframe-public-export-plus";
|
||||
|
||||
const I32_MAX = 0x7fffffff;
|
||||
|
||||
export const setBoosterController: RequestHandler = async (req, res) => {
|
||||
const accountId = await getAccountIdForRequest(req);
|
||||
const requests = req.body as { ItemType: string; ExpiryDate: number }[];
|
||||
const inventory = await getInventory(accountId, "Boosters");
|
||||
const boosters = inventory.Boosters;
|
||||
if (
|
||||
requests.some(request => {
|
||||
if (typeof request.ItemType !== "string") return true;
|
||||
if (Object.entries(ExportBoosters).find(([_, item]) => item.typeName === request.ItemType) === undefined)
|
||||
return true;
|
||||
if (typeof request.ExpiryDate !== "number") return true;
|
||||
if (request.ExpiryDate < 0 || request.ExpiryDate > I32_MAX) return true;
|
||||
return false;
|
||||
})
|
||||
) {
|
||||
res.status(400).send("Invalid ItemType provided.");
|
||||
return;
|
||||
}
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
for (const { ItemType, ExpiryDate } of requests) {
|
||||
if (ExpiryDate < now) {
|
||||
// remove expired boosters
|
||||
const index = boosters.findIndex(item => item.ItemType === ItemType);
|
||||
if (index !== -1) {
|
||||
boosters.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
const boosterItem = boosters.find(item => item.ItemType === ItemType);
|
||||
if (boosterItem) {
|
||||
boosterItem.ExpiryDate = ExpiryDate;
|
||||
} else {
|
||||
boosters.push({ ItemType, ExpiryDate });
|
||||
}
|
||||
}
|
||||
}
|
||||
await inventory.save();
|
||||
res.end();
|
||||
};
|
@ -237,7 +237,7 @@ export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: TNemesisFacti
|
||||
return passcode;
|
||||
};
|
||||
|
||||
const reqiuemMods: readonly string[] = [
|
||||
const requiemMods: readonly string[] = [
|
||||
"/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod",
|
||||
"/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod",
|
||||
"/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod",
|
||||
@ -263,29 +263,51 @@ export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: TNeme
|
||||
const passcode = getNemesisPasscode(nemesis);
|
||||
return nemesis.Faction == "FC_INFESTATION"
|
||||
? passcode.map(i => antivirusMods[i])
|
||||
: passcode.map(i => reqiuemMods[i]);
|
||||
: passcode.map(i => requiemMods[i]);
|
||||
};
|
||||
|
||||
export const encodeNemesisGuess = (
|
||||
symbol1: number,
|
||||
result1: number,
|
||||
symbol2: number,
|
||||
result2: number,
|
||||
symbol3: number,
|
||||
result3: number
|
||||
): number => {
|
||||
// Symbols; 0-7 are the normal requiem mods.
|
||||
export const GUESS_NONE = 8;
|
||||
export const GUESS_WILDCARD = 9;
|
||||
|
||||
// Results; there are 3, 4, 5 as well which are more muted versions but unused afaik.
|
||||
export const GUESS_NEUTRAL = 0;
|
||||
export const GUESS_INCORRECT = 1;
|
||||
export const GUESS_CORRECT = 2;
|
||||
|
||||
interface NemesisPositionGuess {
|
||||
symbol: number;
|
||||
result: number;
|
||||
}
|
||||
|
||||
export type NemesisGuess = [NemesisPositionGuess, NemesisPositionGuess, NemesisPositionGuess];
|
||||
|
||||
export const encodeNemesisGuess = (guess: NemesisGuess): number => {
|
||||
return (
|
||||
(symbol1 & 0xf) |
|
||||
((result1 & 3) << 12) |
|
||||
((symbol2 << 4) & 0xff) |
|
||||
((result2 << 14) & 0xffff) |
|
||||
((symbol3 & 0xf) << 8) |
|
||||
((result3 & 3) << 16)
|
||||
(guess[0].symbol & 0xf) |
|
||||
((guess[0].result & 3) << 12) |
|
||||
((guess[1].symbol << 4) & 0xff) |
|
||||
((guess[1].result << 14) & 0xffff) |
|
||||
((guess[2].symbol & 0xf) << 8) |
|
||||
((guess[2].result & 3) << 16)
|
||||
);
|
||||
};
|
||||
|
||||
export const decodeNemesisGuess = (val: number): number[] => {
|
||||
return [val & 0xf, (val >> 12) & 3, (val & 0xff) >> 4, (val & 0xffff) >> 14, (val >> 8) & 0xf, (val >> 16) & 3];
|
||||
export const decodeNemesisGuess = (val: number): NemesisGuess => {
|
||||
return [
|
||||
{
|
||||
symbol: val & 0xf,
|
||||
result: (val >> 12) & 3
|
||||
},
|
||||
{
|
||||
symbol: (val & 0xff) >> 4,
|
||||
result: (val & 0xffff) >> 14
|
||||
},
|
||||
{
|
||||
symbol: (val >> 8) & 0xf,
|
||||
result: (val >> 16) & 3
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
export interface IKnifeResponse {
|
||||
|
27
src/index.ts
27
src/index.ts
@ -12,12 +12,10 @@ import { logger } from "@/src/utils/logger";
|
||||
logger.info("Starting up...");
|
||||
|
||||
// Proceed with normal startup: bring up config watcher service, validate config, connect to MongoDB, and finally start listening for HTTP.
|
||||
import http from "http";
|
||||
import https from "https";
|
||||
import fs from "node:fs";
|
||||
import { app } from "./app";
|
||||
import mongoose from "mongoose";
|
||||
import { JSONStringify } from "json-with-bigint";
|
||||
import { startWebServer } from "./services/webService";
|
||||
|
||||
import { validateConfig } from "@/src/services/configWatcherService";
|
||||
|
||||
// Patch JSON.stringify to work flawlessly with Bigints.
|
||||
@ -29,26 +27,7 @@ mongoose
|
||||
.connect(config.mongodbUrl)
|
||||
.then(() => {
|
||||
logger.info("Connected to MongoDB");
|
||||
|
||||
const httpPort = config.httpPort || 80;
|
||||
const httpsPort = config.httpsPort || 443;
|
||||
const options = {
|
||||
key: fs.readFileSync("static/certs/key.pem"),
|
||||
cert: fs.readFileSync("static/certs/cert.pem")
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
http.createServer(app).listen(httpPort, () => {
|
||||
logger.info("HTTP server started on port " + httpPort);
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
https.createServer(options, app).listen(httpsPort, () => {
|
||||
logger.info("HTTPS server started on port " + httpsPort);
|
||||
|
||||
logger.info(
|
||||
"Access the WebUI in your browser at http://localhost" + (httpPort == 80 ? "" : ":" + httpPort)
|
||||
);
|
||||
});
|
||||
});
|
||||
startWebServer();
|
||||
})
|
||||
.catch(error => {
|
||||
if (error instanceof Error) {
|
||||
|
@ -1097,7 +1097,8 @@ const pendingRecipeSchema = new Schema<IPendingRecipeDatabase>(
|
||||
LongGuns: { type: [EquipmentSchema], default: undefined },
|
||||
Pistols: { type: [EquipmentSchema], default: undefined },
|
||||
Melee: { type: [EquipmentSchema], default: undefined },
|
||||
SuitToUnbrand: { type: Schema.Types.ObjectId, default: undefined }
|
||||
SuitToUnbrand: { type: Schema.Types.ObjectId, default: undefined },
|
||||
KubrowPet: { type: Schema.Types.ObjectId, default: undefined }
|
||||
},
|
||||
{ id: false }
|
||||
);
|
||||
@ -1115,6 +1116,7 @@ pendingRecipeSchema.set("toJSON", {
|
||||
delete returnedObject.Pistols;
|
||||
delete returnedObject.Melees;
|
||||
delete returnedObject.SuitToUnbrand;
|
||||
delete returnedObject.KubrowPet;
|
||||
(returnedObject as IPendingRecipeClient).CompletionDate = {
|
||||
$date: { $numberLong: (returnedObject as IPendingRecipeDatabase).CompletionDate.getTime().toString() }
|
||||
};
|
||||
|
@ -9,6 +9,7 @@ import { addIgnoredUserController } from "@/src/controllers/api/addIgnoredUserCo
|
||||
import { addPendingFriendController } from "@/src/controllers/api/addPendingFriendController";
|
||||
import { addToAllianceController } from "@/src/controllers/api/addToAllianceController";
|
||||
import { addToGuildController } from "@/src/controllers/api/addToGuildController";
|
||||
import { adoptPetController } from "@/src/controllers/api/adoptPetController";
|
||||
import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController";
|
||||
import { archonFusionController } from "@/src/controllers/api/archonFusionController";
|
||||
import { artifactsController } from "@/src/controllers/api/artifactsController";
|
||||
@ -107,6 +108,7 @@ import { removeFriendGetController, removeFriendPostController } from "@/src/con
|
||||
import { removeFromAllianceController } from "@/src/controllers/api/removeFromAllianceController";
|
||||
import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController";
|
||||
import { removeIgnoredUserController } from "@/src/controllers/api/removeIgnoredUserController";
|
||||
import { renamePetController } from "@/src/controllers/api/renamePetController";
|
||||
import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController";
|
||||
import { retrievePetFromStasisController } from "@/src/controllers/api/retrievePetFromStasisController";
|
||||
import { saveDialogueController } from "@/src/controllers/api/saveDialogueController";
|
||||
@ -225,6 +227,7 @@ apiRouter.post("/addIgnoredUser.php", addIgnoredUserController);
|
||||
apiRouter.post("/addPendingFriend.php", addPendingFriendController);
|
||||
apiRouter.post("/addToAlliance.php", addToAllianceController);
|
||||
apiRouter.post("/addToGuild.php", addToGuildController);
|
||||
apiRouter.post("/adoptPet.php", adoptPetController);
|
||||
apiRouter.post("/arcaneCommon.php", arcaneCommonController);
|
||||
apiRouter.post("/archonFusion.php", archonFusionController);
|
||||
apiRouter.post("/artifacts.php", artifactsController);
|
||||
@ -294,6 +297,7 @@ apiRouter.post("/releasePet.php", releasePetController);
|
||||
apiRouter.post("/removeFriend.php", removeFriendPostController);
|
||||
apiRouter.post("/removeFromGuild.php", removeFromGuildController);
|
||||
apiRouter.post("/removeIgnoredUser.php", removeIgnoredUserController);
|
||||
apiRouter.post("/renamePet.php", renamePetController);
|
||||
apiRouter.post("/rerollRandomMod.php", rerollRandomModController);
|
||||
apiRouter.post("/retrievePetFromStasis.php", retrievePetFromStasisController);
|
||||
apiRouter.post("/saveDialogue.php", saveDialogueController);
|
||||
|
@ -23,6 +23,7 @@ import { setEvolutionProgressController } from "@/src/controllers/custom/setEvol
|
||||
|
||||
import { getConfigDataController } from "@/src/controllers/custom/getConfigDataController";
|
||||
import { updateConfigDataController } from "@/src/controllers/custom/updateConfigDataController";
|
||||
import { setBoosterController } from "../controllers/custom/setBoosterController";
|
||||
|
||||
const customRouter = express.Router();
|
||||
|
||||
@ -46,6 +47,7 @@ customRouter.post("/addXp", addXpController);
|
||||
customRouter.post("/import", importController);
|
||||
customRouter.post("/manageQuests", manageQuestsController);
|
||||
customRouter.post("/setEvolutionProgress", setEvolutionProgressController);
|
||||
customRouter.post("/setBooster", setBoosterController);
|
||||
|
||||
customRouter.get("/config", getConfigDataController);
|
||||
customRouter.post("/config", updateConfigDataController);
|
||||
|
@ -61,8 +61,11 @@ interface IConfig {
|
||||
affinityBoost?: boolean;
|
||||
resourceBoost?: boolean;
|
||||
starDays?: boolean;
|
||||
lockTime?: number;
|
||||
eidolonOverride?: string;
|
||||
vallisOverride?: string;
|
||||
nightwaveOverride?: string;
|
||||
};
|
||||
nightwaveStandingMultiplier?: number;
|
||||
}
|
||||
|
||||
export const configPath = path.join(repoDir, "config.json");
|
||||
|
@ -2,6 +2,7 @@ import fs from "fs";
|
||||
import fsPromises from "fs/promises";
|
||||
import { logger } from "../utils/logger";
|
||||
import { config, configPath, loadConfig } from "./configService";
|
||||
import { getWebPorts, startWebServer, stopWebServer } from "./webService";
|
||||
|
||||
let amnesia = false;
|
||||
fs.watchFile(configPath, () => {
|
||||
@ -16,13 +17,31 @@ fs.watchFile(configPath, () => {
|
||||
process.exit(1);
|
||||
}
|
||||
validateConfig();
|
||||
|
||||
const webPorts = getWebPorts();
|
||||
if (config.httpPort != webPorts.http || config.httpsPort != webPorts.https) {
|
||||
logger.info(`Restarting web server to apply port changes.`);
|
||||
void stopWebServer().then(startWebServer);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const validateConfig = (): void => {
|
||||
if (typeof config.administratorNames == "string") {
|
||||
logger.info(`Updating config.json to make administratorNames an array.`);
|
||||
config.administratorNames = [config.administratorNames];
|
||||
let modified = false;
|
||||
if (config.administratorNames) {
|
||||
if (!Array.isArray(config.administratorNames)) {
|
||||
config.administratorNames = [config.administratorNames];
|
||||
modified = true;
|
||||
}
|
||||
for (let i = 0; i != config.administratorNames.length; ++i) {
|
||||
if (typeof config.administratorNames[i] != "string") {
|
||||
config.administratorNames[i] = String(config.administratorNames[i]);
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (modified) {
|
||||
logger.info(`Updating config.json to fix some issues with it.`);
|
||||
void saveConfig();
|
||||
}
|
||||
};
|
||||
|
@ -44,6 +44,7 @@ import {
|
||||
import {
|
||||
ExportArcanes,
|
||||
ExportBundles,
|
||||
ExportChallenges,
|
||||
ExportCustoms,
|
||||
ExportDrones,
|
||||
ExportEmailItems,
|
||||
@ -53,7 +54,6 @@ import {
|
||||
ExportGear,
|
||||
ExportKeys,
|
||||
ExportMisc,
|
||||
ExportNightwave,
|
||||
ExportRailjackWeapons,
|
||||
ExportRecipes,
|
||||
ExportResources,
|
||||
@ -83,8 +83,10 @@ 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 { getWorldState } from "./worldStateService";
|
||||
import { getNightwaveSyndicateTag, getWorldState } from "./worldStateService";
|
||||
import { generateNemesisProfile, INemesisProfile } from "../helpers/nemesisHelpers";
|
||||
import { TAccountDocument } from "./loginService";
|
||||
import { unixTimesInMs } from "../constants/timeConstants";
|
||||
|
||||
export const createInventory = async (
|
||||
accountOwnerId: Types.ObjectId,
|
||||
@ -721,6 +723,10 @@ export const addItem = async (
|
||||
}
|
||||
break;
|
||||
|
||||
case "Boons":
|
||||
// Can purchase /Lotus/Upgrades/Boons/DuviriVendorBoonItem from Acrithis, doesn't need to be added to inventory.
|
||||
return {};
|
||||
|
||||
case "Stickers":
|
||||
{
|
||||
const entry = inventory.RawUpgrades.find(x => x.ItemType == typeName);
|
||||
@ -775,7 +781,9 @@ export const addItem = async (
|
||||
typeName.substr(1).split("/")[3] == "CatbrowPet" ||
|
||||
typeName.substr(1).split("/")[3] == "KubrowPet"
|
||||
) {
|
||||
return addKubrowPet(inventory, typeName, undefined, premiumPurchase);
|
||||
if (typeName != "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem") {
|
||||
return addKubrowPet(inventory, typeName, undefined, premiumPurchase);
|
||||
}
|
||||
} else if (typeName.startsWith("/Lotus/Types/Game/CrewShip/CrewMember/")) {
|
||||
if (!seed) {
|
||||
throw new Error(`Expected crew member to have a seed`);
|
||||
@ -790,6 +798,12 @@ export const addItem = async (
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Items": {
|
||||
if (typeName.substr(1).split("/")[3] == "Emotes") {
|
||||
return addCustomization(inventory, typeName);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "NeutralCreatures": {
|
||||
if (inventory.Horses.length != 0) {
|
||||
logger.warn("refusing to add Horse because account already has one");
|
||||
@ -1014,12 +1028,13 @@ export const addSpaceSuit = (
|
||||
export const addKubrowPet = (
|
||||
inventory: TInventoryDatabaseDocument,
|
||||
kubrowPetName: string,
|
||||
details: IKubrowPetDetailsDatabase | undefined,
|
||||
premiumPurchase: boolean,
|
||||
details?: IKubrowPetDetailsDatabase,
|
||||
premiumPurchase: boolean = false,
|
||||
inventoryChanges: IInventoryChanges = {}
|
||||
): IInventoryChanges => {
|
||||
combineInventoryChanges(inventoryChanges, occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase));
|
||||
|
||||
// TODO: When incubating, this should only be given when claiming the recipe.
|
||||
const kubrowPet = ExportSentinels[kubrowPetName] as ISentinel | undefined;
|
||||
const exalted = kubrowPet?.exalted ?? [];
|
||||
for (const specialItem of exalted) {
|
||||
@ -1068,11 +1083,11 @@ export const addKubrowPet = (
|
||||
|
||||
details = {
|
||||
Name: "",
|
||||
IsPuppy: false,
|
||||
IsPuppy: !premiumPurchase,
|
||||
HasCollar: true,
|
||||
PrintsRemaining: 2,
|
||||
Status: Status.StatusStasis,
|
||||
HatchDate: new Date(Math.trunc(Date.now() / 86400000) * 86400000),
|
||||
PrintsRemaining: 3,
|
||||
Status: premiumPurchase ? Status.StatusStasis : Status.StatusIncubating,
|
||||
HatchDate: premiumPurchase ? new Date() : new Date(Date.now() + 10 * unixTimesInMs.hour), // On live, this seems to be somewhat randomised so that the pet hatches 9~11 hours after start.
|
||||
IsMale: !!getRandomInt(0, 1),
|
||||
Size: getRandomInt(70, 100) / 100,
|
||||
DominantTraits: traits,
|
||||
@ -1510,7 +1525,8 @@ export const applyClientEquipmentUpdates = (
|
||||
gearArray.forEach(({ ItemId, XP, InfestationDate }) => {
|
||||
const item = category.id(fromOid(ItemId));
|
||||
if (!item) {
|
||||
throw new Error(`No item with id ${fromOid(ItemId)} in ${categoryName}`);
|
||||
logger.warn(`Skipping unknown ${categoryName} item: id ${fromOid(ItemId)} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (XP) {
|
||||
@ -1716,6 +1732,7 @@ export const addLoreFragmentScans = (inventory: TInventoryDatabaseDocument, arr:
|
||||
};
|
||||
|
||||
export const addChallenges = (
|
||||
account: TAccountDocument,
|
||||
inventory: TInventoryDatabaseDocument,
|
||||
ChallengeProgress: IChallengeProgress[],
|
||||
SeasonChallengeCompletions: ISeasonChallenge[] | undefined
|
||||
@ -1738,26 +1755,33 @@ export const addChallenges = (
|
||||
continue;
|
||||
}
|
||||
|
||||
const meta = ExportNightwave.challenges[challenge.challenge];
|
||||
logger.debug("Completed challenge", meta);
|
||||
const meta = ExportChallenges[challenge.challenge];
|
||||
const nightwaveSyndicateTag = getNightwaveSyndicateTag(account.BuildLabel);
|
||||
logger.debug("Completed season challenge", {
|
||||
uniqueName: challenge.challenge,
|
||||
syndicateTag: nightwaveSyndicateTag,
|
||||
...meta
|
||||
});
|
||||
if (nightwaveSyndicateTag) {
|
||||
let affiliation = inventory.Affiliations.find(x => x.Tag == nightwaveSyndicateTag);
|
||||
if (!affiliation) {
|
||||
affiliation =
|
||||
inventory.Affiliations[
|
||||
inventory.Affiliations.push({
|
||||
Tag: nightwaveSyndicateTag,
|
||||
Standing: 0
|
||||
}) - 1
|
||||
];
|
||||
}
|
||||
|
||||
let affiliation = inventory.Affiliations.find(x => x.Tag == ExportNightwave.affiliationTag);
|
||||
if (!affiliation) {
|
||||
affiliation =
|
||||
inventory.Affiliations[
|
||||
inventory.Affiliations.push({
|
||||
Tag: ExportNightwave.affiliationTag,
|
||||
Standing: 0
|
||||
}) - 1
|
||||
];
|
||||
const standingToAdd = meta.standing! * (config.nightwaveStandingMultiplier ?? 1);
|
||||
affiliation.Standing += standingToAdd;
|
||||
if (affiliationMods.length == 0) {
|
||||
affiliationMods.push({ Tag: nightwaveSyndicateTag });
|
||||
}
|
||||
affiliationMods[0].Standing ??= 0;
|
||||
affiliationMods[0].Standing += standingToAdd;
|
||||
}
|
||||
affiliation.Standing += meta.standing;
|
||||
|
||||
if (affiliationMods.length == 0) {
|
||||
affiliationMods.push({ Tag: ExportNightwave.affiliationTag });
|
||||
}
|
||||
affiliationMods[0].Standing ??= 0;
|
||||
affiliationMods[0].Standing += meta.standing;
|
||||
}
|
||||
}
|
||||
return affiliationMods;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { IKeyChainRequest } from "@/src/types/requestTypes";
|
||||
import { getIndexAfter } from "@/src/helpers/stringHelpers";
|
||||
import {
|
||||
dict_de,
|
||||
dict_en,
|
||||
@ -53,20 +52,32 @@ export const getRecipeByResult = (resultType: string): IRecipe | undefined => {
|
||||
return Object.values(ExportRecipes).find(x => x.resultType == resultType);
|
||||
};
|
||||
|
||||
export const getItemCategoryByUniqueName = (uniqueName: string): string => {
|
||||
//Lotus/Types/Items/MiscItems/PolymerBundle
|
||||
|
||||
let splitWord = "Items/";
|
||||
if (!uniqueName.includes("/Items/")) {
|
||||
splitWord = "/Types/";
|
||||
export const getItemCategoryByUniqueName = (uniqueName: string): string | undefined => {
|
||||
if (uniqueName in ExportCustoms) {
|
||||
return ExportCustoms[uniqueName].productCategory;
|
||||
}
|
||||
|
||||
const index = getIndexAfter(uniqueName, splitWord);
|
||||
if (index === -1) {
|
||||
throw new Error(`error parsing item category ${uniqueName}`);
|
||||
if (uniqueName in ExportDrones) {
|
||||
return "Drones";
|
||||
}
|
||||
const category = uniqueName.substring(index).split("/")[0];
|
||||
return category;
|
||||
if (uniqueName in ExportKeys) {
|
||||
return "LevelKeys";
|
||||
}
|
||||
if (uniqueName in ExportGear) {
|
||||
return "Consumables";
|
||||
}
|
||||
if (uniqueName in ExportResources) {
|
||||
return ExportResources[uniqueName].productCategory;
|
||||
}
|
||||
if (uniqueName in ExportSentinels) {
|
||||
return ExportSentinels[uniqueName].productCategory;
|
||||
}
|
||||
if (uniqueName in ExportWarframes) {
|
||||
return ExportWarframes[uniqueName].productCategory;
|
||||
}
|
||||
if (uniqueName in ExportWeapons) {
|
||||
return ExportWeapons[uniqueName].productCategory;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getItemName = (uniqueName: string): string | undefined => {
|
||||
@ -222,7 +233,7 @@ export const isStoreItem = (type: string): boolean => {
|
||||
};
|
||||
|
||||
export const toStoreItem = (type: string): string => {
|
||||
if (type.startsWith("/Lotus/Types/StoreItems/Boosters/")) {
|
||||
if (type.startsWith("/Lotus/Types/Boosters/")) {
|
||||
const boosterEntry = Object.entries(ExportBoosters).find(arr => arr[1].typeName == type);
|
||||
if (boosterEntry) {
|
||||
return boosterEntry[0];
|
||||
|
@ -5,7 +5,13 @@ import { mixSeeds, SRng } from "./rngService";
|
||||
import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel";
|
||||
import { addBooster, updateCurrency } from "./inventoryService";
|
||||
import { handleStoreItemAcquisition } from "./purchaseService";
|
||||
import { ExportBoosters, ExportRecipes, ExportWarframes, ExportWeapons } from "warframe-public-export-plus";
|
||||
import {
|
||||
ExportBoosterPacks,
|
||||
ExportBoosters,
|
||||
ExportRecipes,
|
||||
ExportWarframes,
|
||||
ExportWeapons
|
||||
} from "warframe-public-export-plus";
|
||||
import { toStoreItem } from "./itemDataService";
|
||||
|
||||
export interface ILoginRewardsReponse {
|
||||
@ -76,6 +82,7 @@ export const getRandomLoginRewards = (
|
||||
const getRandomLoginReward = (rng: SRng, day: number, inventory: TInventoryDatabaseDocument): ILoginReward => {
|
||||
const reward = rng.randomReward(randomRewards)!;
|
||||
//const reward = randomRewards.find(x => x.RewardType == "RT_BOOSTER")!;
|
||||
let storeItemType: string = reward.StoreItemType;
|
||||
if (reward.RewardType == "RT_RANDOM_RECIPE") {
|
||||
const masteredItems = new Set();
|
||||
for (const entry of inventory.XPInfo) {
|
||||
@ -102,7 +109,12 @@ const getRandomLoginReward = (rng: SRng, day: number, inventory: TInventoryDatab
|
||||
// This account has all applicable warframes and weapons already mastered (filthy cheater), need a different reward.
|
||||
return getRandomLoginReward(rng, day, inventory);
|
||||
}
|
||||
reward.StoreItemType = toStoreItem(rng.randomElement(eligibleRecipes)!);
|
||||
storeItemType = toStoreItem(rng.randomElement(eligibleRecipes)!);
|
||||
} else if (reward.StoreItemType == "/Lotus/StoreItems/Types/BoosterPacks/LoginRewardRandomProjection") {
|
||||
storeItemType = toStoreItem(
|
||||
rng.randomElement(ExportBoosterPacks["/Lotus/Types/BoosterPacks/LoginRewardRandomProjection"].components)!
|
||||
.Item
|
||||
);
|
||||
}
|
||||
return {
|
||||
//_id: toOid(new Types.ObjectId()),
|
||||
@ -110,7 +122,7 @@ const getRandomLoginReward = (rng: SRng, day: number, inventory: TInventoryDatab
|
||||
//CouponType: "CPT_PLATINUM",
|
||||
Icon: reward.Icon ?? "",
|
||||
//ItemType: "",
|
||||
StoreItemType: reward.StoreItemType,
|
||||
StoreItemType: storeItemType,
|
||||
//ProductCategory: "Pistols",
|
||||
Amount: reward.Duration ? 1 : Math.round(scaleAmount(day, reward.Amount, reward.ScalingMultiplier)),
|
||||
ScalingMultiplier: reward.ScalingMultiplier,
|
||||
|
@ -79,6 +79,7 @@ import { config } from "./configService";
|
||||
import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json";
|
||||
import { ISyndicateMissionInfo } from "../types/worldStateTypes";
|
||||
import { fromOid } from "../helpers/inventoryHelpers";
|
||||
import { TAccountDocument } from "./loginService";
|
||||
|
||||
const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] => {
|
||||
// For Spy missions, e.g. 3 vaults cracked = A, B, C
|
||||
@ -135,6 +136,7 @@ const getRandomRewardByChance = (pool: IReward[], rng?: SRng): IRngResult | unde
|
||||
//const knownUnhandledKeys: readonly string[] = ["test"] as const; // for unimplemented but important keys
|
||||
|
||||
export const addMissionInventoryUpdates = async (
|
||||
account: TAccountDocument,
|
||||
inventory: TInventoryDatabaseDocument,
|
||||
inventoryUpdates: IMissionInventoryUpdateRequest
|
||||
): Promise<IInventoryChanges> => {
|
||||
@ -287,7 +289,7 @@ export const addMissionInventoryUpdates = async (
|
||||
addRecipes(inventory, value);
|
||||
break;
|
||||
case "ChallengeProgress":
|
||||
addChallenges(inventory, value, inventoryUpdates.SeasonChallengeCompletions);
|
||||
addChallenges(account, inventory, value, inventoryUpdates.SeasonChallengeCompletions);
|
||||
break;
|
||||
case "FusionTreasures":
|
||||
addFusionTreasures(inventory, value);
|
||||
@ -324,8 +326,8 @@ export const addMissionInventoryUpdates = async (
|
||||
break;
|
||||
}
|
||||
case "PlayerSkillGains": {
|
||||
inventory.PlayerSkills.LPP_SPACE += value.LPP_SPACE;
|
||||
inventory.PlayerSkills.LPP_DRIFTER += value.LPP_DRIFTER;
|
||||
inventory.PlayerSkills.LPP_SPACE += value.LPP_SPACE ?? 0;
|
||||
inventory.PlayerSkills.LPP_DRIFTER += value.LPP_DRIFTER ?? 0;
|
||||
break;
|
||||
}
|
||||
case "CustomMarkers": {
|
||||
@ -1180,14 +1182,12 @@ export const addMissionRewards = async (
|
||||
if (nodeIndex !== -1) inventory.Nemesis.InfNodes.splice(nodeIndex, 1);
|
||||
|
||||
if (inventory.Nemesis.InfNodes.length <= 0) {
|
||||
const manifest = getNemesisManifest(inventory.Nemesis.manifest);
|
||||
if (inventory.Nemesis.Faction != "FC_INFESTATION") {
|
||||
inventory.Nemesis.Rank = Math.min(inventory.Nemesis.Rank + 1, 4);
|
||||
inventory.Nemesis.Rank = Math.min(inventory.Nemesis.Rank + 1, manifest.systemIndexes.length - 1);
|
||||
inventoryChanges.Nemesis.Rank = inventory.Nemesis.Rank;
|
||||
}
|
||||
inventory.Nemesis.InfNodes = getInfNodes(
|
||||
getNemesisManifest(inventory.Nemesis.manifest),
|
||||
inventory.Nemesis.Rank
|
||||
);
|
||||
inventory.Nemesis.InfNodes = getInfNodes(manifest, inventory.Nemesis.Rank);
|
||||
}
|
||||
|
||||
if (inventory.Nemesis.Faction == "FC_INFESTATION") {
|
||||
@ -1205,7 +1205,9 @@ export const addMissionRewards = async (
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [jobType, unkIndex, hubNode, syndicateMissionId, locationTag] = rewardInfo.jobId.split("_");
|
||||
const syndicateMissions: ISyndicateMissionInfo[] = [];
|
||||
pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId));
|
||||
if (syndicateMissionId) {
|
||||
pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId));
|
||||
}
|
||||
const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId);
|
||||
if (syndicateEntry && syndicateEntry.Jobs) {
|
||||
let currentJob = syndicateEntry.Jobs[rewardInfo.JobTier!];
|
||||
@ -1554,7 +1556,9 @@ function getRandomMissionDrops(
|
||||
let isEndlessJob = false;
|
||||
if (syndicateMissionId) {
|
||||
const syndicateMissions: ISyndicateMissionInfo[] = [];
|
||||
pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId));
|
||||
if (syndicateMissionId) {
|
||||
pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId));
|
||||
}
|
||||
const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId);
|
||||
if (syndicateEntry && syndicateEntry.Jobs) {
|
||||
let job = syndicateEntry.Jobs[RewardInfo.JobTier!];
|
||||
|
@ -27,7 +27,6 @@ import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSal
|
||||
import Nova1999ConquestShopManifest from "@/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json";
|
||||
import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json";
|
||||
import OstronProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronProspectorVendorManifest.json";
|
||||
import RadioLegionIntermission12VendorManifest from "@/static/fixed_responses/getVendorInfo/RadioLegionIntermission12VendorManifest.json";
|
||||
import 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";
|
||||
@ -54,7 +53,6 @@ const rawVendorManifests: IVendorManifest[] = [
|
||||
Nova1999ConquestShopManifest,
|
||||
OstronPetVendorManifest,
|
||||
OstronProspectorVendorManifest,
|
||||
RadioLegionIntermission12VendorManifest,
|
||||
SolarisDebtTokenVendorRepossessionsManifest,
|
||||
SolarisProspectorVendorManifest,
|
||||
Temple1999VendorManifest,
|
||||
@ -305,7 +303,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
|
||||
}
|
||||
const cycleStart = cycleOffset + cycleIndex * cycleDuration;
|
||||
for (const rawItem of offersToAdd) {
|
||||
const durationHoursRange = toRange(rawItem.durationHours);
|
||||
const durationHoursRange = toRange(rawItem.durationHours ?? cycleDuration);
|
||||
const expiry =
|
||||
cycleStart +
|
||||
rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour;
|
||||
|
65
src/services/webService.ts
Normal file
65
src/services/webService.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import http from "http";
|
||||
import https from "https";
|
||||
import fs from "node:fs";
|
||||
import { config } from "./configService";
|
||||
import { logger } from "../utils/logger";
|
||||
import { app } from "../app";
|
||||
import { AddressInfo } from "node:net";
|
||||
|
||||
let httpServer: http.Server | undefined;
|
||||
let httpsServer: https.Server | undefined;
|
||||
|
||||
const tlsOptions = {
|
||||
key: fs.readFileSync("static/certs/key.pem"),
|
||||
cert: fs.readFileSync("static/certs/cert.pem")
|
||||
};
|
||||
|
||||
export const startWebServer = (): void => {
|
||||
const httpPort = config.httpPort || 80;
|
||||
const httpsPort = config.httpsPort || 443;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
httpServer = http.createServer(app);
|
||||
httpServer.listen(httpPort, () => {
|
||||
logger.info("HTTP server started on port " + httpPort);
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
httpsServer = https.createServer(tlsOptions, app);
|
||||
httpsServer.listen(httpsPort, () => {
|
||||
logger.info("HTTPS server started on port " + httpsPort);
|
||||
|
||||
logger.info(
|
||||
"Access the WebUI in your browser at http://localhost" + (httpPort == 80 ? "" : ":" + httpPort)
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const getWebPorts = (): Record<"http" | "https", number | undefined> => {
|
||||
return {
|
||||
http: (httpServer?.address() as AddressInfo | undefined)?.port,
|
||||
https: (httpsServer?.address() as AddressInfo | undefined)?.port
|
||||
};
|
||||
};
|
||||
|
||||
export const stopWebServer = async (): Promise<void> => {
|
||||
const promises: Promise<void>[] = [];
|
||||
if (httpServer) {
|
||||
promises.push(
|
||||
new Promise(resolve => {
|
||||
httpServer!.close(() => {
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
if (httpsServer) {
|
||||
promises.push(
|
||||
new Promise(resolve => {
|
||||
httpsServer!.close(() => {
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
};
|
@ -6,7 +6,7 @@ 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 { ExportNightwave, ExportRegions, IRegion } from "warframe-public-export-plus";
|
||||
import { ExportRegions, ExportSyndicates, IRegion } from "warframe-public-export-plus";
|
||||
import {
|
||||
ICalendarDay,
|
||||
ICalendarEvent,
|
||||
@ -19,6 +19,7 @@ import {
|
||||
IWorldState
|
||||
} from "../types/worldStateTypes";
|
||||
import { version_compare } from "../helpers/inventoryHelpers";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
const sortieBosses = [
|
||||
"SORTIE_BOSS_HYENA",
|
||||
@ -166,8 +167,8 @@ const microplanetEndlessJobs = [
|
||||
|
||||
const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0
|
||||
|
||||
const isBeforeNextExpectedWorldStateRefresh = (date: number): boolean => {
|
||||
return Date.now() + 300_000 > date;
|
||||
const isBeforeNextExpectedWorldStateRefresh = (nowMs: number, thenMs: number): boolean => {
|
||||
return nowMs + 300_000 > thenMs;
|
||||
};
|
||||
|
||||
const getSortieTime = (day: number): number => {
|
||||
@ -344,11 +345,32 @@ export const getSortie = (day: number): ISortie => {
|
||||
};
|
||||
};
|
||||
|
||||
const dailyChallenges = Object.keys(ExportNightwave.challenges).filter(x =>
|
||||
x.startsWith("/Lotus/Types/Challenges/Seasons/Daily/")
|
||||
);
|
||||
interface IRotatingSeasonChallengePools {
|
||||
daily: string[];
|
||||
weekly: string[];
|
||||
hardWeekly: string[];
|
||||
hasWeeklyPermanent: boolean;
|
||||
}
|
||||
|
||||
const getSeasonDailyChallenge = (day: number): ISeasonChallenge => {
|
||||
const getSeasonChallengePools = (syndicateTag: string): IRotatingSeasonChallengePools => {
|
||||
const syndicate = ExportSyndicates[syndicateTag];
|
||||
return {
|
||||
daily: syndicate.dailyChallenges!,
|
||||
weekly: syndicate.weeklyChallenges!.filter(
|
||||
x =>
|
||||
x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/") &&
|
||||
!x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")
|
||||
),
|
||||
hardWeekly: syndicate.weeklyChallenges!.filter(x =>
|
||||
x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/")
|
||||
),
|
||||
hasWeeklyPermanent: !!syndicate.weeklyChallenges!.find(x =>
|
||||
x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")
|
||||
)
|
||||
};
|
||||
};
|
||||
|
||||
const getSeasonDailyChallenge = (pools: IRotatingSeasonChallengePools, day: number): ISeasonChallenge => {
|
||||
const dayStart = EPOCH + day * 86400000;
|
||||
const dayEnd = EPOCH + (day + 3) * 86400000;
|
||||
const rng = new SRng(new SRng(day).randomInt(0, 100_000));
|
||||
@ -357,17 +379,11 @@ const getSeasonDailyChallenge = (day: number): ISeasonChallenge => {
|
||||
Daily: true,
|
||||
Activation: { $date: { $numberLong: dayStart.toString() } },
|
||||
Expiry: { $date: { $numberLong: dayEnd.toString() } },
|
||||
Challenge: rng.randomElement(dailyChallenges)!
|
||||
Challenge: rng.randomElement(pools.daily)!
|
||||
};
|
||||
};
|
||||
|
||||
const weeklyChallenges = Object.keys(ExportNightwave.challenges).filter(
|
||||
x =>
|
||||
x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/") &&
|
||||
!x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")
|
||||
);
|
||||
|
||||
const getSeasonWeeklyChallenge = (week: number, id: number): ISeasonChallenge => {
|
||||
const getSeasonWeeklyChallenge = (pools: IRotatingSeasonChallengePools, week: number, id: number): ISeasonChallenge => {
|
||||
const weekStart = EPOCH + week * 604800000;
|
||||
const weekEnd = weekStart + 604800000;
|
||||
const challengeId = week * 7 + id;
|
||||
@ -376,15 +392,15 @@ const getSeasonWeeklyChallenge = (week: number, id: number): ISeasonChallenge =>
|
||||
_id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") },
|
||||
Activation: { $date: { $numberLong: weekStart.toString() } },
|
||||
Expiry: { $date: { $numberLong: weekEnd.toString() } },
|
||||
Challenge: rng.randomElement(weeklyChallenges)!
|
||||
Challenge: rng.randomElement(pools.weekly)!
|
||||
};
|
||||
};
|
||||
|
||||
const weeklyHardChallenges = Object.keys(ExportNightwave.challenges).filter(x =>
|
||||
x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/")
|
||||
);
|
||||
|
||||
const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChallenge => {
|
||||
const getSeasonWeeklyHardChallenge = (
|
||||
pools: IRotatingSeasonChallengePools,
|
||||
week: number,
|
||||
id: number
|
||||
): ISeasonChallenge => {
|
||||
const weekStart = EPOCH + week * 604800000;
|
||||
const weekEnd = weekStart + 604800000;
|
||||
const challengeId = week * 7 + id;
|
||||
@ -393,36 +409,48 @@ const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChalleng
|
||||
_id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") },
|
||||
Activation: { $date: { $numberLong: weekStart.toString() } },
|
||||
Expiry: { $date: { $numberLong: weekEnd.toString() } },
|
||||
Challenge: rng.randomElement(weeklyHardChallenges)!
|
||||
Challenge: rng.randomElement(pools.hardWeekly)!
|
||||
};
|
||||
};
|
||||
|
||||
const pushWeeklyActs = (worldState: IWorldState, week: number): void => {
|
||||
const pushWeeklyActs = (
|
||||
activeChallenges: ISeasonChallenge[],
|
||||
pools: IRotatingSeasonChallengePools,
|
||||
week: number
|
||||
): void => {
|
||||
const weekStart = EPOCH + week * 604800000;
|
||||
const weekEnd = weekStart + 604800000;
|
||||
|
||||
worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyChallenge(week, 0));
|
||||
worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyChallenge(week, 1));
|
||||
worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyHardChallenge(week, 2));
|
||||
worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyHardChallenge(week, 3));
|
||||
worldState.SeasonInfo.ActiveChallenges.push({
|
||||
_id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") },
|
||||
Activation: { $date: { $numberLong: weekStart.toString() } },
|
||||
Expiry: { $date: { $numberLong: weekEnd.toString() } },
|
||||
Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" + (week - 12)
|
||||
});
|
||||
worldState.SeasonInfo.ActiveChallenges.push({
|
||||
_id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") },
|
||||
Activation: { $date: { $numberLong: weekStart.toString() } },
|
||||
Expiry: { $date: { $numberLong: weekEnd.toString() } },
|
||||
Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" + (week - 12)
|
||||
});
|
||||
worldState.SeasonInfo.ActiveChallenges.push({
|
||||
_id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") },
|
||||
Activation: { $date: { $numberLong: weekStart.toString() } },
|
||||
Expiry: { $date: { $numberLong: weekEnd.toString() } },
|
||||
Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" + (week - 12)
|
||||
});
|
||||
activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 0));
|
||||
activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 1));
|
||||
if (pools.hasWeeklyPermanent) {
|
||||
activeChallenges.push({
|
||||
_id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") },
|
||||
Activation: { $date: { $numberLong: weekStart.toString() } },
|
||||
Expiry: { $date: { $numberLong: weekEnd.toString() } },
|
||||
Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions"
|
||||
});
|
||||
activeChallenges.push({
|
||||
_id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") },
|
||||
Activation: { $date: { $numberLong: weekStart.toString() } },
|
||||
Expiry: { $date: { $numberLong: weekEnd.toString() } },
|
||||
Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus"
|
||||
});
|
||||
activeChallenges.push({
|
||||
_id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") },
|
||||
Activation: { $date: { $numberLong: weekStart.toString() } },
|
||||
Expiry: { $date: { $numberLong: weekEnd.toString() } },
|
||||
Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies"
|
||||
});
|
||||
activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 2));
|
||||
activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 3));
|
||||
} else {
|
||||
activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 2));
|
||||
activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 3));
|
||||
activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 4));
|
||||
activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 5));
|
||||
activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 6));
|
||||
}
|
||||
};
|
||||
|
||||
export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[], bountyCycle: number): void => {
|
||||
@ -911,30 +939,76 @@ const getCalendarSeason = (week: number): ICalendarSeason => {
|
||||
};
|
||||
};
|
||||
|
||||
const doesTimeSatsifyConstraints = (timeSecs: number): boolean => {
|
||||
if (config.worldState?.eidolonOverride) {
|
||||
const eidolonEpoch = 1391992660;
|
||||
const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / 9000);
|
||||
const eidolonCycleStart = eidolonEpoch + eidolonCycle * 9000;
|
||||
const eidolonCycleEnd = eidolonCycleStart + 9000;
|
||||
const eidolonCycleNightStart = eidolonCycleEnd - 3000;
|
||||
if (config.worldState.eidolonOverride == "day") {
|
||||
if (
|
||||
//timeSecs < eidolonCycleStart ||
|
||||
isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, eidolonCycleNightStart * 1000)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
timeSecs < eidolonCycleNightStart ||
|
||||
isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, eidolonCycleEnd * 1000)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.worldState?.vallisOverride) {
|
||||
const vallisEpoch = 1541837628;
|
||||
const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600);
|
||||
const vallisCycleStart = vallisEpoch + vallisCycle * 1600;
|
||||
const vallisCycleEnd = vallisCycleStart + 1600;
|
||||
const vallisCycleColdStart = vallisCycleStart + 400;
|
||||
if (config.worldState.vallisOverride == "cold") {
|
||||
if (
|
||||
timeSecs < vallisCycleColdStart ||
|
||||
isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, vallisCycleEnd * 1000)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
//timeSecs < vallisCycleStart ||
|
||||
isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, vallisCycleColdStart * 1000)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getWorldState = (buildLabel?: string): IWorldState => {
|
||||
const day = Math.trunc((Date.now() - EPOCH) / 86400000);
|
||||
let timeSecs = Math.round(Date.now() / 1000);
|
||||
while (!doesTimeSatsifyConstraints(timeSecs)) {
|
||||
timeSecs -= 60;
|
||||
}
|
||||
const timeMs = timeSecs * 1000;
|
||||
const day = Math.trunc((timeMs - EPOCH) / 86400000);
|
||||
const week = Math.trunc(day / 7);
|
||||
const weekStart = EPOCH + week * 604800000;
|
||||
const weekEnd = weekStart + 604800000;
|
||||
|
||||
const worldState: IWorldState = {
|
||||
BuildLabel: typeof buildLabel == "string" ? buildLabel.split(" ").join("+") : buildConfig.buildLabel,
|
||||
Time: config.worldState?.lockTime || Math.round(Date.now() / 1000),
|
||||
Time: timeSecs,
|
||||
Goals: [],
|
||||
Alerts: [],
|
||||
Sorties: [],
|
||||
LiteSorties: [],
|
||||
GlobalUpgrades: [],
|
||||
EndlessXpChoices: [],
|
||||
SeasonInfo: {
|
||||
Activation: { $date: { $numberLong: "1715796000000" } },
|
||||
Expiry: { $date: { $numberLong: "2000000000000" } },
|
||||
AffiliationTag: "RadioLegionIntermission12Syndicate",
|
||||
Season: 14,
|
||||
Phase: 0,
|
||||
Params: "",
|
||||
ActiveChallenges: []
|
||||
},
|
||||
KnownCalendarSeasons: [],
|
||||
...staticWorldState,
|
||||
SyndicateMissions: [...staticWorldState.SyndicateMissions]
|
||||
@ -967,17 +1041,27 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
|
||||
}
|
||||
|
||||
// Nightwave Challenges
|
||||
// Current nightwave season was introduced in 38.0.8 so omitting challenges before that to avoid UI bugs and even crashes on really old versions.
|
||||
if (!buildLabel || version_compare(buildLabel, "2025.02.05.11.19") >= 0) {
|
||||
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 2));
|
||||
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 1));
|
||||
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day - 0));
|
||||
if (isBeforeNextExpectedWorldStateRefresh(EPOCH + (day + 1) * 86400000)) {
|
||||
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day + 1));
|
||||
const nightwaveSyndicateTag = getNightwaveSyndicateTag(buildLabel);
|
||||
if (nightwaveSyndicateTag) {
|
||||
worldState.SeasonInfo = {
|
||||
Activation: { $date: { $numberLong: "1715796000000" } },
|
||||
Expiry: { $date: { $numberLong: "2000000000000" } },
|
||||
AffiliationTag: nightwaveSyndicateTag,
|
||||
Season: nightwaveTagToSeason[nightwaveSyndicateTag],
|
||||
Phase: 0,
|
||||
Params: "",
|
||||
ActiveChallenges: []
|
||||
};
|
||||
const pools = getSeasonChallengePools(nightwaveSyndicateTag);
|
||||
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 2));
|
||||
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 1));
|
||||
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 0));
|
||||
if (isBeforeNextExpectedWorldStateRefresh(timeMs, EPOCH + (day + 1) * 86400000)) {
|
||||
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day + 1));
|
||||
}
|
||||
pushWeeklyActs(worldState, week);
|
||||
if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) {
|
||||
pushWeeklyActs(worldState, week + 1);
|
||||
pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week);
|
||||
if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
|
||||
pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@ -985,7 +1069,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
|
||||
worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = new SRng(week).randomInt(0, 0xff_ffff);
|
||||
|
||||
// Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation
|
||||
let bountyCycle = Math.trunc(Date.now() / 9000000);
|
||||
let bountyCycle = Math.trunc(timeSecs / 9000);
|
||||
let bountyCycleEnd: number | undefined;
|
||||
do {
|
||||
const bountyCycleStart = bountyCycle * 9000000;
|
||||
@ -1016,7 +1100,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
|
||||
});
|
||||
|
||||
pushClassicBounties(worldState.SyndicateMissions, bountyCycle);
|
||||
} while (isBeforeNextExpectedWorldStateRefresh(bountyCycleEnd) && ++bountyCycle);
|
||||
} while (isBeforeNextExpectedWorldStateRefresh(timeMs, bountyCycleEnd) && ++bountyCycle);
|
||||
|
||||
if (config.worldState?.creditBoost) {
|
||||
worldState.GlobalUpgrades.push({
|
||||
@ -1059,15 +1143,15 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
|
||||
{
|
||||
const rollover = getSortieTime(day);
|
||||
|
||||
if (Date.now() < rollover) {
|
||||
if (timeMs < rollover) {
|
||||
worldState.Sorties.push(getSortie(day - 1));
|
||||
}
|
||||
if (isBeforeNextExpectedWorldStateRefresh(rollover)) {
|
||||
if (isBeforeNextExpectedWorldStateRefresh(timeMs, rollover)) {
|
||||
worldState.Sorties.push(getSortie(day));
|
||||
}
|
||||
|
||||
// The client does not seem to respect activation for classic syndicate missions, so only pushing current ones.
|
||||
const sdy = Date.now() >= rollover ? day : day - 1;
|
||||
const sdy = timeMs >= rollover ? day : day - 1;
|
||||
const rng = new SRng(sdy);
|
||||
pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48049", "ArbitersSyndicate");
|
||||
pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4804a", "CephalonSudaSyndicate");
|
||||
@ -1079,7 +1163,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
|
||||
|
||||
// Archon Hunt cycling every week
|
||||
worldState.LiteSorties.push(getLiteSortie(week));
|
||||
if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) {
|
||||
if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
|
||||
worldState.LiteSorties.push(getLiteSortie(week + 1));
|
||||
}
|
||||
|
||||
@ -1116,12 +1200,12 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
|
||||
|
||||
// 1999 Calendar Season cycling every week + YearIteration every 4 weeks
|
||||
worldState.KnownCalendarSeasons.push(getCalendarSeason(week));
|
||||
if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) {
|
||||
if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
|
||||
worldState.KnownCalendarSeasons.push(getCalendarSeason(week + 1));
|
||||
}
|
||||
|
||||
// Sentient Anomaly cycling every 30 minutes
|
||||
const halfHour = Math.trunc(Date.now() / (unixTimesInMs.hour / 2));
|
||||
const halfHour = Math.trunc(timeMs / (unixTimesInMs.hour / 2));
|
||||
const tmp = {
|
||||
cavabegin: "1690761600",
|
||||
PurchasePlatformLockEnabled: true,
|
||||
@ -1242,3 +1326,41 @@ export const isArchwingMission = (node: IRegion): boolean => {
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getNightwaveSyndicateTag = (buildLabel: string | undefined): string | undefined => {
|
||||
if (config.worldState?.nightwaveOverride) {
|
||||
if (config.worldState.nightwaveOverride in nightwaveTagToSeason) {
|
||||
return config.worldState.nightwaveOverride;
|
||||
}
|
||||
logger.warn(`ignoring invalid config value for worldState.nightwaveOverride`, {
|
||||
value: config.worldState.nightwaveOverride,
|
||||
valid_values: Object.keys(nightwaveTagToSeason)
|
||||
});
|
||||
}
|
||||
if (!buildLabel || version_compare(buildLabel, "2025.05.20.10.18") >= 0) {
|
||||
return "RadioLegionIntermission13Syndicate";
|
||||
}
|
||||
if (version_compare(buildLabel, "2025.02.05.11.19") >= 0) {
|
||||
return "RadioLegionIntermission12Syndicate";
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const nightwaveTagToSeason: Record<string, number> = {
|
||||
RadioLegionIntermission13Syndicate: 15, // Nora's Mix Vol. 9
|
||||
RadioLegionIntermission12Syndicate: 14, // Nora's Mix Vol. 8
|
||||
RadioLegionIntermission11Syndicate: 13, // Nora's Mix Vol. 7
|
||||
RadioLegionIntermission10Syndicate: 12, // Nora's Mix Vol. 6
|
||||
RadioLegionIntermission9Syndicate: 11, // Nora's Mix Vol. 5
|
||||
RadioLegionIntermission8Syndicate: 10, // Nora's Mix Vol. 4
|
||||
RadioLegionIntermission7Syndicate: 9, // Nora's Mix Vol. 3
|
||||
RadioLegionIntermission6Syndicate: 8, // Nora's Mix Vol. 2
|
||||
RadioLegionIntermission5Syndicate: 7, // Nora's Mix Vol. 1
|
||||
RadioLegionIntermission4Syndicate: 6, // Nora's Choice
|
||||
RadioLegionIntermission3Syndicate: 5, // Intermission III
|
||||
RadioLegion3Syndicate: 4, // Glassmaker
|
||||
RadioLegionIntermission2Syndicate: 3, // Intermission II
|
||||
RadioLegion2Syndicate: 2, // The Emissary
|
||||
RadioLegionIntermissionSyndicate: 1, // Intermission I
|
||||
RadioLegionSyndicate: 0 // The Wolf of Saturn Six
|
||||
};
|
||||
|
@ -765,7 +765,8 @@ export interface IKubrowPetDetailsClient extends Omit<IKubrowPetDetailsDatabase,
|
||||
|
||||
export enum Status {
|
||||
StatusAvailable = "STATUS_AVAILABLE",
|
||||
StatusStasis = "STATUS_STASIS"
|
||||
StatusStasis = "STATUS_STASIS",
|
||||
StatusIncubating = "STATUS_INCUBATING"
|
||||
}
|
||||
|
||||
export interface ILastSortieRewardClient {
|
||||
@ -929,10 +930,14 @@ export interface IPendingRecipeDatabase {
|
||||
Pistols?: IEquipmentDatabase[];
|
||||
Melee?: IEquipmentDatabase[];
|
||||
SuitToUnbrand?: Types.ObjectId;
|
||||
KubrowPet?: Types.ObjectId;
|
||||
}
|
||||
|
||||
export interface IPendingRecipeClient
|
||||
extends Omit<IPendingRecipeDatabase, "CompletionDate" | "LongGuns" | "Pistols" | "Melee" | "SuitToUnbrand"> {
|
||||
extends Omit<
|
||||
IPendingRecipeDatabase,
|
||||
"CompletionDate" | "LongGuns" | "Pistols" | "Melee" | "SuitToUnbrand" | "KubrowPet"
|
||||
> {
|
||||
CompletionDate: IMongoDate;
|
||||
}
|
||||
|
||||
|
@ -96,7 +96,7 @@ export type IMissionInventoryUpdateRequest = {
|
||||
FpsSamples: number;
|
||||
EvolutionProgress?: IEvolutionProgress[];
|
||||
FocusXpIncreases?: number[];
|
||||
PlayerSkillGains: IPlayerSkills;
|
||||
PlayerSkillGains: Partial<IPlayerSkills>;
|
||||
CustomMarkers?: ICustomMarkers[];
|
||||
LoreFragmentScans?: ILoreFragmentScan[];
|
||||
VoidTearParticipantsCurrWave?: {
|
||||
|
@ -14,7 +14,7 @@ export interface IWorldState {
|
||||
NodeOverrides: INodeOverride[];
|
||||
PVPChallengeInstances: IPVPChallengeInstance[];
|
||||
EndlessXpChoices: IEndlessXpChoice[];
|
||||
SeasonInfo: {
|
||||
SeasonInfo?: {
|
||||
Activation: IMongoDate;
|
||||
Expiry: IMongoDate;
|
||||
AffiliationTag: string;
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -416,6 +416,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="card mb-3" style="height: 400px;">
|
||||
<h5 class="card-header" data-loc="inventory_Boosters"></h5>
|
||||
<div class="card-body overflow-auto">
|
||||
<form class="input-group mb-3" onsubmit="doAcquireBoosters();return false;">
|
||||
<input class="form-control" id="acquire-type-Boosters" list="datalist-Boosters" />
|
||||
<button class="btn btn-primary" type="submit" data-loc="general_addButton"></button>
|
||||
</form>
|
||||
<table class="table table-hover w-100">
|
||||
<tbody id="Boosters-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-3">
|
||||
<h5 class="card-header" data-loc="general_bulkActions"></h5>
|
||||
@ -722,6 +736,10 @@
|
||||
<label class="form-label" for="spoofMasteryRank" data-loc="cheats_spoofMasteryRank"></label>
|
||||
<input class="form-control" id="spoofMasteryRank" type="number" min="-1" max="65535" />
|
||||
</div>
|
||||
<div class="form-group mt-2">
|
||||
<label class="form-label" for="nightwaveStandingMultiplier" data-loc="cheats_nightwaveStandingMultiplier"></label>
|
||||
<input class="form-control" id="nightwaveStandingMultiplier" type="number" min="1" max="1000000" value="1" />
|
||||
</div>
|
||||
<button class="btn btn-primary mt-3" type="submit" data-loc="cheats_saveSettings"></button>
|
||||
</form>
|
||||
</div>
|
||||
@ -804,6 +822,7 @@
|
||||
<datalist id="datalist-ModularParts-CATBROW_MUTAGEN"></datalist>
|
||||
<datalist id="datalist-ModularParts-KUBROW_ANTIGEN"></datalist>
|
||||
<datalist id="datalist-ModularParts-KUBROW_MUTAGEN"></datalist>
|
||||
<datalist id="datalist-Boosters"></datalist>
|
||||
<script src="/webui/libs/jquery-3.6.0.min.js"></script>
|
||||
<script src="/webui/libs/whirlpool-js.min.js"></script>
|
||||
<script src="/webui/libs/single.js"></script>
|
||||
|
@ -312,7 +312,7 @@ function fetchItemList() {
|
||||
document.getElementById("changeSyndicate").appendChild(option);
|
||||
});
|
||||
} else {
|
||||
const nameSet = new Set();
|
||||
const nameToItems = {};
|
||||
items.forEach(item => {
|
||||
item.name = item.name.replace(/<.+>/g, "").trim();
|
||||
if ("badReason" in item) {
|
||||
@ -322,6 +322,11 @@ function fetchItemList() {
|
||||
item.name += " " + loc("code_badItem");
|
||||
}
|
||||
}
|
||||
nameToItems[item.name] ??= [];
|
||||
nameToItems[item.name].push(item);
|
||||
});
|
||||
|
||||
items.forEach(item => {
|
||||
if (type == "ModularParts") {
|
||||
const supportedModularParts = [
|
||||
"LWPT_HB_DECK",
|
||||
@ -360,15 +365,26 @@ function fetchItemList() {
|
||||
.appendChild(option);
|
||||
}
|
||||
} else if (item.badReason != "notraw") {
|
||||
if (nameSet.has(item.name)) {
|
||||
//console.log(`Not adding ${item.uniqueName} to datalist for ${type} due to duplicate display name: ${item.name}`);
|
||||
} else {
|
||||
nameSet.add(item.name);
|
||||
|
||||
const ambiguous = nameToItems[item.name].length > 1;
|
||||
let canDisambiguate = true;
|
||||
if (ambiguous) {
|
||||
for (const i2 of nameToItems[item.name]) {
|
||||
if (!i2.subtype) {
|
||||
canDisambiguate = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!ambiguous || canDisambiguate || nameToItems[item.name][0] == item) {
|
||||
const option = document.createElement("option");
|
||||
option.setAttribute("data-key", item.uniqueName);
|
||||
option.value = item.name;
|
||||
if (ambiguous && canDisambiguate) {
|
||||
option.value += " (" + item.subtype + ")";
|
||||
}
|
||||
document.getElementById("datalist-" + type).appendChild(option);
|
||||
} else {
|
||||
//console.log(`Not adding ${item.uniqueName} to datalist for ${type} due to duplicate display name: ${item.name}`);
|
||||
}
|
||||
}
|
||||
itemMap[item.uniqueName] = { ...item, type };
|
||||
@ -476,7 +492,7 @@ function updateInventory() {
|
||||
}
|
||||
|
||||
let anyExaltedMissingXP = false;
|
||||
if (item.XP >= maxXP && "exalted" in itemMap[item.ItemType]) {
|
||||
if (item.XP >= maxXP && item.ItemType in itemMap && "exalted" in itemMap[item.ItemType]) {
|
||||
for (const exaltedType of itemMap[item.ItemType].exalted) {
|
||||
const exaltedItem = data.SpecialItems.find(x => x.ItemType == exaltedType);
|
||||
if (exaltedItem) {
|
||||
@ -995,6 +1011,63 @@ function updateInventory() {
|
||||
}
|
||||
}
|
||||
document.getElementById("changeSyndicate").value = data.SupportedSyndicate ?? "";
|
||||
|
||||
document.getElementById("Boosters-list").innerHTML = "";
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
data.Boosters.forEach(({ ItemType, ExpiryDate }) => {
|
||||
if (ExpiryDate < now) {
|
||||
// Booster has expired, skip it
|
||||
return;
|
||||
}
|
||||
const tr = document.createElement("tr");
|
||||
{
|
||||
const td = document.createElement("td");
|
||||
td.textContent = itemMap[ItemType]?.name ?? ItemType;
|
||||
tr.appendChild(td);
|
||||
}
|
||||
{
|
||||
const td = document.createElement("td");
|
||||
td.classList = "text-end text-nowrap";
|
||||
const timeString = formatDatetime("%Y-%m-%d %H:%M:%s", ExpiryDate * 1000);
|
||||
const inlineForm = document.createElement("form");
|
||||
const input = document.createElement("input");
|
||||
|
||||
inlineForm.style.display = "inline-block";
|
||||
inlineForm.onsubmit = function (event) {
|
||||
event.preventDefault();
|
||||
doChangeBoosterExpiry(ItemType, input);
|
||||
};
|
||||
input.type = "datetime-local";
|
||||
input.classList.add("form-control");
|
||||
input.classList.add("form-control-sm");
|
||||
input.value = timeString;
|
||||
let changed = false;
|
||||
input.onchange = function () {
|
||||
changed = true;
|
||||
};
|
||||
input.onblur = function () {
|
||||
if (changed) {
|
||||
doChangeBoosterExpiry(ItemType, input);
|
||||
}
|
||||
};
|
||||
inlineForm.appendChild(input);
|
||||
|
||||
td.appendChild(inlineForm);
|
||||
|
||||
const removeButton = document.createElement("a");
|
||||
removeButton.title = loc("code_remove");
|
||||
removeButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"/></svg>`;
|
||||
removeButton.href = "#";
|
||||
removeButton.onclick = function (event) {
|
||||
event.preventDefault();
|
||||
setBooster(ItemType, 0);
|
||||
};
|
||||
td.appendChild(removeButton);
|
||||
|
||||
tr.appendChild(td);
|
||||
}
|
||||
document.getElementById("Boosters-list").appendChild(tr);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -2011,3 +2084,77 @@ function handleModularSelection(category) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setBooster(ItemType, ExpiryDate, callback) {
|
||||
revalidateAuthz(() => {
|
||||
$.post({
|
||||
url: "/custom/setBooster?" + window.authz,
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify([
|
||||
{
|
||||
ItemType,
|
||||
ExpiryDate
|
||||
}
|
||||
])
|
||||
}).done(function () {
|
||||
updateInventory();
|
||||
if (callback) callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function doAcquireBoosters() {
|
||||
const uniqueName = getKey(document.getElementById("acquire-type-Boosters"));
|
||||
if (!uniqueName) {
|
||||
$("#acquire-type-Boosters").addClass("is-invalid").focus();
|
||||
return;
|
||||
}
|
||||
const ExpiryDate = Date.now() / 1000 + 3 * 24 * 60 * 60; // default 3 days
|
||||
setBooster(uniqueName, ExpiryDate, () => {
|
||||
$("#acquire-type-Boosters").val("");
|
||||
updateInventory();
|
||||
});
|
||||
}
|
||||
|
||||
function doChangeBoosterExpiry(ItemType, ExpiryDateInput) {
|
||||
console.log("Changing booster expiry for", ItemType, "to", ExpiryDateInput.value);
|
||||
// cast local datetime string to unix timestamp
|
||||
const ExpiryDate = new Date(ExpiryDateInput.value).getTime() / 1000;
|
||||
if (isNaN(ExpiryDate)) {
|
||||
ExpiryDateInput.addClass("is-invalid").focus();
|
||||
return false;
|
||||
}
|
||||
setBooster(ItemType, ExpiryDate);
|
||||
return true;
|
||||
}
|
||||
|
||||
function formatDatetime(fmt, date) {
|
||||
if (typeof date === "number") date = new Date(date);
|
||||
return fmt.replace(/(%[yY]|%m|%[Dd]|%H|%h|%M|%[Ss]|%[Pp])/g, match => {
|
||||
switch (match) {
|
||||
case "%Y":
|
||||
return date.getFullYear().toString();
|
||||
case "%y":
|
||||
return date.getFullYear().toString().slice(-2);
|
||||
case "%m":
|
||||
return (date.getMonth() + 1).toString().padStart(2, "0");
|
||||
case "%D":
|
||||
case "%d":
|
||||
return date.getDate().toString().padStart(2, "0");
|
||||
case "%H":
|
||||
return date.getHours().toString().padStart(2, "0");
|
||||
case "%h":
|
||||
return (date.getHours() % 12).toString().padStart(2, "0");
|
||||
case "%M":
|
||||
return date.getMinutes().toString().padStart(2, "0");
|
||||
case "%S":
|
||||
case "%s":
|
||||
return date.getSeconds().toString().padStart(2, "0");
|
||||
case "%P":
|
||||
case "%p":
|
||||
return date.getHours() < 12 ? "am" : "pm";
|
||||
default:
|
||||
return match;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -98,6 +98,7 @@ dict = {
|
||||
inventory_bulkRankUpSentinels: `Alle Wächter auf Max. Rang`,
|
||||
inventory_bulkRankUpSentinelWeapons: `Alle Wächter-Waffen auf Max. Rang`,
|
||||
inventory_bulkRankUpEvolutionProgress: `Alle Incarnon-Entwicklungsfortschritte auf Max. Rang`,
|
||||
inventory_Boosters: `[UNTRANSLATED] Boosters`,
|
||||
|
||||
quests_list: `Quests`,
|
||||
quests_completeAll: `Alle Quests abschließen`,
|
||||
@ -163,6 +164,7 @@ dict = {
|
||||
cheats_noDojoResearchTime: `Keine Dojo-Forschungszeit`,
|
||||
cheats_fastClanAscension: `Schneller Clan-Aufstieg`,
|
||||
cheats_spoofMasteryRank: `Gefälschter Meisterschaftsrang (-1 zum deaktivieren)`,
|
||||
cheats_nightwaveStandingMultiplier: `[UNTRANSLATED] Nightwave Standing Multiplier`,
|
||||
cheats_saveSettings: `Einstellungen speichern`,
|
||||
cheats_account: `Account`,
|
||||
cheats_unlockAllFocusSchools: `Alle Fokus-Schulen freischalten`,
|
||||
|
@ -97,6 +97,7 @@ dict = {
|
||||
inventory_bulkRankUpSentinels: `Max Rank All Sentinels`,
|
||||
inventory_bulkRankUpSentinelWeapons: `Max Rank All Sentinel Weapons`,
|
||||
inventory_bulkRankUpEvolutionProgress: `Max Rank All Incarnon Evolution Progress`,
|
||||
inventory_Boosters: `Boosters`,
|
||||
|
||||
quests_list: `Quests`,
|
||||
quests_completeAll: `Complete All Quests`,
|
||||
@ -162,6 +163,7 @@ dict = {
|
||||
cheats_noDojoResearchTime: `No Dojo Research Time`,
|
||||
cheats_fastClanAscension: `Fast Clan Ascension`,
|
||||
cheats_spoofMasteryRank: `Spoofed Mastery Rank (-1 to disable)`,
|
||||
cheats_nightwaveStandingMultiplier: `Nightwave Standing Multiplier`,
|
||||
cheats_saveSettings: `Save Settings`,
|
||||
cheats_account: `Account`,
|
||||
cheats_unlockAllFocusSchools: `Unlock All Focus Schools`,
|
||||
|
@ -98,6 +98,7 @@ dict = {
|
||||
inventory_bulkRankUpSentinels: `Maximizar rango de todos los centinelas`,
|
||||
inventory_bulkRankUpSentinelWeapons: `Maximizar rango de todas las armas de centinela`,
|
||||
inventory_bulkRankUpEvolutionProgress: `Maximizar todo el progreso de evolución Incarnon`,
|
||||
inventory_Boosters: `[UNTRANSLATED] Boosters`,
|
||||
|
||||
quests_list: `Misiones`,
|
||||
quests_completeAll: `Completar todas las misiones`,
|
||||
@ -151,7 +152,7 @@ dict = {
|
||||
cheats_noVendorPurchaseLimits: `Sin límite de compras de vendedores`,
|
||||
cheats_noDeathMarks: `Sin marcas de muerte`,
|
||||
cheats_noKimCooldowns: `Sin tiempo de espera para conversaciones KIM`,
|
||||
cheats_syndicateMissionsRepeatable: `[UNTRANSLATED] Syndicate Missions Repeatable`,
|
||||
cheats_syndicateMissionsRepeatable: `Misiones de sindicato rejugables`,
|
||||
cheats_instantFinishRivenChallenge: `Terminar desafío de agrietado inmediatamente`,
|
||||
cheats_instantResourceExtractorDrones: `Drones de extracción de recursos instantáneos`,
|
||||
cheats_noResourceExtractorDronesDamage: `Sin daño a los drones extractores de recursos`,
|
||||
@ -163,6 +164,7 @@ dict = {
|
||||
cheats_noDojoResearchTime: `Sin tiempo de investigación del dojo`,
|
||||
cheats_fastClanAscension: `Ascenso rápido del clan`,
|
||||
cheats_spoofMasteryRank: `Rango de maestría simulado (-1 para desactivar)`,
|
||||
cheats_nightwaveStandingMultiplier: `[UNTRANSLATED] Nightwave Standing Multiplier`,
|
||||
cheats_saveSettings: `Guardar configuración`,
|
||||
cheats_account: `Cuenta`,
|
||||
cheats_unlockAllFocusSchools: `Desbloquear todas las escuelas de enfoque`,
|
||||
|
@ -34,8 +34,8 @@ dict = {
|
||||
code_rerollsNumber: `Nombre de rerolls`,
|
||||
code_viewStats: `Voir les stats`,
|
||||
code_rank: `Rang`,
|
||||
code_rankUp: `[UNTRANSLATED] Rank up`,
|
||||
code_rankDown: `[UNTRANSLATED] Rank down`,
|
||||
code_rankUp: `Monter de rang`,
|
||||
code_rankDown: `Baisser de rang`,
|
||||
code_count: `Quantité`,
|
||||
code_focusAllUnlocked: `Les écoles de Focus sont déjà déverrouillées.`,
|
||||
code_focusUnlocked: `|COUNT| écoles de Focus déverrouillées ! Synchronisation de l'inventaire nécessaire.`,
|
||||
@ -59,7 +59,7 @@ dict = {
|
||||
login_emailLabel: `Email`,
|
||||
login_passwordLabel: `Mot de passe`,
|
||||
login_loginButton: `Connexion`,
|
||||
login_registerButton: `[UNTRANSLATED] Register`,
|
||||
login_registerButton: `S'enregistrer`,
|
||||
navbar_logout: `Déconnexion`,
|
||||
navbar_renameAccount: `Renommer le compte`,
|
||||
navbar_deleteAccount: `Supprimer le compte`,
|
||||
@ -83,21 +83,22 @@ dict = {
|
||||
inventory_hoverboards: `K-Drives`,
|
||||
inventory_moaPets: `Moas`,
|
||||
inventory_kubrowPets: `Bêtes`,
|
||||
inventory_evolutionProgress: `[UNTRANSLATED] Incarnon Evolution Progress`,
|
||||
inventory_evolutionProgress: `Progrès de l'évolution Incarnon`,
|
||||
inventory_bulkAddSuits: `Ajouter les Warframes manquantes`,
|
||||
inventory_bulkAddWeapons: `Ajouter les armes manquantes`,
|
||||
inventory_bulkAddSpaceSuits: `Ajouter les Archwings manquants`,
|
||||
inventory_bulkAddSpaceWeapons: `Ajouter les armes d'Archwing manquantes`,
|
||||
inventory_bulkAddSentinels: `Ajouter les Sentinelles manquantes`,
|
||||
inventory_bulkAddSentinelWeapons: `Ajouter les armes de Sentinelles manquantes`,
|
||||
inventory_bulkAddEvolutionProgress: `[UNTRANSLATED] Add Missing Incarnon Evolution Progress`,
|
||||
inventory_bulkRankUpSuits: `Toutes les Warframes rang max`,
|
||||
inventory_bulkRankUpWeapons: `Toutes les armes rang max`,
|
||||
inventory_bulkRankUpSpaceSuits: `Tous les Archwings rang max`,
|
||||
inventory_bulkRankUpSpaceWeapons: `Toutes les armes d'Archwing rang max`,
|
||||
inventory_bulkRankUpSentinels: `Toutes les Sentinelles rang max`,
|
||||
inventory_bulkRankUpSentinelWeapons: `Toutes les armes de Sentinelles rang max`,
|
||||
inventory_bulkRankUpEvolutionProgress: `[UNTRANSLATED] Max Rank All Incarnon Evolution Progress`,
|
||||
inventory_bulkAddEvolutionProgress: `Ajouter les évolutions Incarnon manquantes`,
|
||||
inventory_bulkRankUpSuits: `Toutes les Warframes au rang max`,
|
||||
inventory_bulkRankUpWeapons: `Toutes les armes au rang max`,
|
||||
inventory_bulkRankUpSpaceSuits: `Tous les Archwings au rang max`,
|
||||
inventory_bulkRankUpSpaceWeapons: `Toutes les armes d'Archwing au rang max`,
|
||||
inventory_bulkRankUpSentinels: `Toutes les Sentinelles au rang max`,
|
||||
inventory_bulkRankUpSentinelWeapons: `Toutes les armes de Sentinelles au rang max`,
|
||||
inventory_bulkRankUpEvolutionProgress: `Toutes les évolutions Incarnon au rang max`,
|
||||
inventory_Boosters: `[UNTRANSLATED] Boosters`,
|
||||
|
||||
quests_list: `Quêtes`,
|
||||
quests_completeAll: `Compléter toutes les quêtes`,
|
||||
@ -117,9 +118,9 @@ dict = {
|
||||
mods_fingerprintHelp: `Besoin d'aide pour l'empreinte ?`,
|
||||
mods_rivens: `Rivens`,
|
||||
mods_mods: `Mods`,
|
||||
mods_addMissingUnrankedMods: `[UNTRANSLATED] Add Missing Unranked Mods`,
|
||||
mods_removeUnranked: `[UNTRANSLATED] Remove Unranked Mods`,
|
||||
mods_addMissingMaxRankMods: `[UNTRANSLATED] Add Missing Max Rank Mods`,
|
||||
mods_addMissingUnrankedMods: `Ajouter les mods sans rang manquants`,
|
||||
mods_removeUnranked: `Retirer les mods sans rang`,
|
||||
mods_addMissingMaxRankMods: `Ajouter les mods niveau max manquants`,
|
||||
cheats_administratorRequirement: `Rôle d'administrateur requis pour cette fonctionnalité. Ajoutez <code>|DISPLAYNAME|</code> à la ligne <code>administratorNames</code> dans le fichier config.json.`,
|
||||
cheats_server: `Serveur`,
|
||||
cheats_skipTutorial: `Passer le tutoriel`,
|
||||
@ -131,9 +132,9 @@ dict = {
|
||||
cheats_infiniteEndo: `Endo infini`,
|
||||
cheats_infiniteRegalAya: `Aya Raffiné infini`,
|
||||
cheats_infiniteHelminthMaterials: `Ressources d'Helminth infinies`,
|
||||
cheats_claimingBlueprintRefundsIngredients: `[UNTRANSLATED] Claiming Blueprint Refunds Ingredients`,
|
||||
cheats_dontSubtractVoidTraces: `[UNTRANSLATED] Don't Subtract Void Traces`,
|
||||
cheats_dontSubtractConsumables: `[UNTRANSLATED] Don't Subtract Consumables`,
|
||||
cheats_claimingBlueprintRefundsIngredients: `Récupérer les items rend les ressources`,
|
||||
cheats_dontSubtractVoidTraces: `Ne pas consommer de Void Traces`,
|
||||
cheats_dontSubtractConsumables: `Ne pas retirer de consommables`,
|
||||
cheats_unlockAllShipFeatures: `Débloquer tous les segments du vaisseau`,
|
||||
cheats_unlockAllShipDecorations: `Débloquer toutes les décorations du vaisseau`,
|
||||
cheats_unlockAllFlavourItems: `Débloquer tous les <abbr title=\"Animations, Glyphes, Palettes, etc.\">Flavor Items</abbr>`,
|
||||
@ -151,11 +152,11 @@ dict = {
|
||||
cheats_noVendorPurchaseLimits: `Aucune limite d'achat chez les PNJ`,
|
||||
cheats_noDeathMarks: `Aucune marque d'assassin`,
|
||||
cheats_noKimCooldowns: `Aucun cooldown sur le KIM`,
|
||||
cheats_syndicateMissionsRepeatable: `[UNTRANSLATED] Syndicate Missions Repeatable`,
|
||||
cheats_instantFinishRivenChallenge: `[UNTRANSLATED] Instant Finish Riven Challenge`,
|
||||
cheats_syndicateMissionsRepeatable: `Mission syndicat répétables`,
|
||||
cheats_instantFinishRivenChallenge: `Débloquer le challenge Riven instantanément`,
|
||||
cheats_instantResourceExtractorDrones: `Ressources de drones d'extraction instantannées`,
|
||||
cheats_noResourceExtractorDronesDamage: `Aucun dégâts aux drones d'extraction de resources`,
|
||||
cheats_skipClanKeyCrafting: `[UNTRANSLATED] Skip Clan Key Crafting`,
|
||||
cheats_skipClanKeyCrafting: `Passer le craft de la clé de clan`,
|
||||
cheats_noDojoRoomBuildStage: `Aucune attente (construction des salles)`,
|
||||
cheats_noDojoDecoBuildStage: `Aucune attente (construction des décorations)`,
|
||||
cheats_fastDojoRoomDestruction: `Destruction de salle instantanée (Dojo)`,
|
||||
@ -163,6 +164,7 @@ dict = {
|
||||
cheats_noDojoResearchTime: `Aucun temps de recherche (Dojo)`,
|
||||
cheats_fastClanAscension: `Ascension de clan rapide`,
|
||||
cheats_spoofMasteryRank: `Rang de maîtrise personnalisé (-1 pour désactiver)`,
|
||||
cheats_nightwaveStandingMultiplier: `[UNTRANSLATED] Nightwave Standing Multiplier`,
|
||||
cheats_saveSettings: `Sauvegarder les paramètres`,
|
||||
cheats_account: `Compte`,
|
||||
cheats_unlockAllFocusSchools: `Débloquer toutes les écoles de focus`,
|
||||
|
@ -98,6 +98,7 @@ dict = {
|
||||
inventory_bulkRankUpSentinels: `Максимальный ранг всех стражей`,
|
||||
inventory_bulkRankUpSentinelWeapons: `Максимальный ранг всего оружия стражей`,
|
||||
inventory_bulkRankUpEvolutionProgress: `Максимальный ранг всех эволюций Инкарнонов`,
|
||||
inventory_Boosters: `[UNTRANSLATED] Boosters`,
|
||||
|
||||
quests_list: `Квесты`,
|
||||
quests_completeAll: `Завершить все квесты`,
|
||||
@ -163,6 +164,7 @@ dict = {
|
||||
cheats_noDojoResearchTime: `Мгновенные Исследование Додзё`,
|
||||
cheats_fastClanAscension: `Мгновенное Вознесение Клана`,
|
||||
cheats_spoofMasteryRank: `Подделанный ранг мастерства (-1 для отключения)`,
|
||||
cheats_nightwaveStandingMultiplier: `[UNTRANSLATED] Nightwave Standing Multiplier`,
|
||||
cheats_saveSettings: `Сохранить настройки`,
|
||||
cheats_account: `Аккаунт`,
|
||||
cheats_unlockAllFocusSchools: `Разблокировать все школы фокуса`,
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Chinese translation by meb154
|
||||
// Chinese translation by meb154 & bishan178
|
||||
dict = {
|
||||
general_inventoryUpdateNote: `注意:此处所做的更改只有在游戏同步仓库后才会生效。您可以通过访问星图来触发仓库更新。`,
|
||||
general_addButton: `添加`,
|
||||
@ -18,7 +18,7 @@ dict = {
|
||||
code_kDrive: `K式悬浮板`,
|
||||
code_legendaryCore: `传奇核心`,
|
||||
code_traumaticPeculiar: `创伤怪奇`,
|
||||
code_starter: `|MOD| (有瑕疵的)`,
|
||||
code_starter: `|MOD|(有瑕疵的)`,
|
||||
code_badItem: `(Imposter)`,
|
||||
code_maxRank: `满级`,
|
||||
code_rename: `重命名`,
|
||||
@ -28,7 +28,7 @@ dict = {
|
||||
code_succRankUp: `[UNTRANSLATED] Successfully ranked up.`,
|
||||
code_noEquipmentToRankUp: `没有可升级的装备。`,
|
||||
code_succAdded: `已成功添加。`,
|
||||
code_succRemoved: `[UNTRANSLATED] Successfully removed.`,
|
||||
code_succRemoved: `已成功移除。`,
|
||||
code_buffsNumber: `增益数量`,
|
||||
code_cursesNumber: `负面数量`,
|
||||
code_rerollsNumber: `洗卡次数`,
|
||||
@ -39,27 +39,27 @@ dict = {
|
||||
code_count: `数量`,
|
||||
code_focusAllUnlocked: `所有专精学派均已解锁。`,
|
||||
code_focusUnlocked: `已解锁 |COUNT| 个新专精学派!需要游戏内仓库更新才能生效,您可以通过访问星图来触发仓库更新。`,
|
||||
code_addModsConfirm: `确定要向账户添加 |COUNT| 张MOD吗?`,
|
||||
code_addModsConfirm: `确定要向账户添加 |COUNT| 张MOD吗?`,
|
||||
code_succImport: `导入成功。`,
|
||||
code_gild: `镀金`,
|
||||
code_moa: `恐鸟`,
|
||||
code_zanuka: `猎犬`,
|
||||
code_stage: `[UNTRANSLATED] Stage`,
|
||||
code_complete: `[UNTRANSLATED] Complete`,
|
||||
code_nextStage: `[UNTRANSLATED] Next stage`,
|
||||
code_prevStage: `[UNTRANSLATED] Previous stage`,
|
||||
code_reset: `[UNTRANSLATED] Reset`,
|
||||
code_setInactive: `[UNTRANSLATED] Make the quest inactive`,
|
||||
code_completed: `[UNTRANSLATED] Completed`,
|
||||
code_active: `[UNTRANSLATED] Active`,
|
||||
code_stage: `阶段`,
|
||||
code_complete: `完成`,
|
||||
code_nextStage: `下一阶段`,
|
||||
code_prevStage: `上一阶段`,
|
||||
code_reset: `重置`,
|
||||
code_setInactive: `使任务处于未激活状态`,
|
||||
code_completed: `已完成`,
|
||||
code_active: `正在执行`,
|
||||
code_pigment: `颜料`,
|
||||
code_mature: `[UNTRANSLATED] Mature for combat`,
|
||||
code_unmature: `[UNTRANSLATED] Regress genetic aging`,
|
||||
code_mature: `成长并战备`,
|
||||
code_unmature: `逆转衰老基因`,
|
||||
login_description: `使用您的 OpenWF 账户凭证登录(与游戏内连接本服务器时使用的昵称相同)。`,
|
||||
login_emailLabel: `电子邮箱`,
|
||||
login_passwordLabel: `密码`,
|
||||
login_loginButton: `登录`,
|
||||
login_registerButton: `[UNTRANSLATED] Register`,
|
||||
login_registerButton: `注册账号`,
|
||||
navbar_logout: `退出登录`,
|
||||
navbar_renameAccount: `重命名账户`,
|
||||
navbar_deleteAccount: `删除账户`,
|
||||
@ -82,22 +82,23 @@ dict = {
|
||||
inventory_operatorAmps: `增幅器`,
|
||||
inventory_hoverboards: `K式悬浮板`,
|
||||
inventory_moaPets: `恐鸟`,
|
||||
inventory_kubrowPets: `[UNTRANSLATED] Beasts`,
|
||||
inventory_evolutionProgress: `[UNTRANSLATED] Incarnon Evolution Progress`,
|
||||
inventory_kubrowPets: `动物同伴`,
|
||||
inventory_evolutionProgress: `灵化之源进度`,
|
||||
inventory_bulkAddSuits: `添加缺失战甲`,
|
||||
inventory_bulkAddWeapons: `添加缺失武器`,
|
||||
inventory_bulkAddSpaceSuits: `添加缺失Archwing`,
|
||||
inventory_bulkAddSpaceWeapons: `添加缺失Archwing武器`,
|
||||
inventory_bulkAddSentinels: `添加缺失守护`,
|
||||
inventory_bulkAddSentinelWeapons: `添加缺失守护武器`,
|
||||
inventory_bulkAddEvolutionProgress: `[UNTRANSLATED] Add Missing Incarnon Evolution Progress`,
|
||||
inventory_bulkAddEvolutionProgress: `添加缺失的灵化之源`,
|
||||
inventory_bulkRankUpSuits: `所有战甲升满级`,
|
||||
inventory_bulkRankUpWeapons: `所有武器升满级`,
|
||||
inventory_bulkRankUpSpaceSuits: `所有Archwing升满级`,
|
||||
inventory_bulkRankUpSpaceWeapons: `所有Archwing武器升满级`,
|
||||
inventory_bulkRankUpSentinels: `所有守护升满级`,
|
||||
inventory_bulkRankUpSentinelWeapons: `所有守护武器升满级`,
|
||||
inventory_bulkRankUpEvolutionProgress: `[UNTRANSLATED] Max Rank All Incarnon Evolution Progress`,
|
||||
inventory_bulkRankUpEvolutionProgress: `所有灵化之源最大等级`,
|
||||
inventory_Boosters: `加成器`,
|
||||
|
||||
quests_list: `任务`,
|
||||
quests_completeAll: `完成所有任务`,
|
||||
@ -111,15 +112,15 @@ dict = {
|
||||
currency_owned: `当前拥有 |COUNT|。`,
|
||||
powersuit_archonShardsLabel: `执刑官源力石槽位`,
|
||||
powersuit_archonShardsDescription: `您可以使用这些无限插槽应用各种强化效果`,
|
||||
powersuit_archonShardsDescription2: `[UNTRANSLATED] Note that each archon shard takes some time to be applied when loading in.`,
|
||||
powersuit_archonShardsDescription2: `请注意, 在加载时, 每个执政官源力石都需要一定的时间来生效。`,
|
||||
mods_addRiven: `添加裂罅MOD`,
|
||||
mods_fingerprint: `印记`,
|
||||
mods_fingerprintHelp: `需要印记相关的帮助?`,
|
||||
mods_rivens: `裂罅MOD`,
|
||||
mods_mods: `Mods`,
|
||||
mods_addMissingUnrankedMods: `[UNTRANSLATED] Add Missing Unranked Mods`,
|
||||
mods_removeUnranked: `[UNTRANSLATED] Remove Unranked Mods`,
|
||||
mods_addMissingMaxRankMods: `[UNTRANSLATED] Add Missing Max Rank Mods`,
|
||||
mods_addMissingUnrankedMods: `添加所有缺失的Mods`,
|
||||
mods_removeUnranked: `删除所有未升级的Mods`,
|
||||
mods_addMissingMaxRankMods: `添加所有缺失的满级Mods`,
|
||||
cheats_administratorRequirement: `您必须是管理员才能使用此功能。要成为管理员,请将 <code>|DISPLAYNAME|</code> 添加到 config.json 的 <code>administratorNames</code> 中。`,
|
||||
cheats_server: `服务器`,
|
||||
cheats_skipTutorial: `跳过教程`,
|
||||
@ -131,43 +132,44 @@ dict = {
|
||||
cheats_infiniteEndo: `无限内融核心`,
|
||||
cheats_infiniteRegalAya: `无限御品阿耶`,
|
||||
cheats_infiniteHelminthMaterials: `无限Helminth材料`,
|
||||
cheats_claimingBlueprintRefundsIngredients: `[UNTRANSLATED] Claiming Blueprint Refunds Ingredients`,
|
||||
cheats_dontSubtractVoidTraces: `[UNTRANSLATED] Don't Subtract Void Traces`,
|
||||
cheats_dontSubtractConsumables: `[UNTRANSLATED] Don't Subtract Consumables`,
|
||||
cheats_claimingBlueprintRefundsIngredients: `取消蓝图制造时返还材料`,
|
||||
cheats_dontSubtractVoidTraces: `虚空光体无消耗`,
|
||||
cheats_dontSubtractConsumables: `消耗物品使用时无损耗`,
|
||||
cheats_unlockAllShipFeatures: `解锁所有飞船功能`,
|
||||
cheats_unlockAllShipDecorations: `解锁所有飞船装饰`,
|
||||
cheats_unlockAllFlavourItems: `解锁所有<abbr title=\"动画组合、图标、调色板等\">装饰物品</abbr>`,
|
||||
cheats_unlockAllSkins: `解锁所有外观`,
|
||||
cheats_unlockAllCapturaScenes: `解锁所有Captura场景`,
|
||||
cheats_unlockAllDecoRecipes: `[UNTRANSLATED] Unlock All Dojo Deco Recipes`,
|
||||
cheats_unlockAllDecoRecipes: `解锁所有道场配方`,
|
||||
cheats_universalPolarityEverywhere: `全局万用极性`,
|
||||
cheats_unlockDoubleCapacityPotatoesEverywhere: `全物品自带Orokin反应堆`,
|
||||
cheats_unlockExilusEverywhere: `全物品自带适配器`,
|
||||
cheats_unlockArcanesEverywhere: `全物品自带赋能适配器`,
|
||||
cheats_noDailyStandingLimits: `无每日声望限制`,
|
||||
cheats_noDailyFocusLimit: `[UNTRANSLATED] No Daily Focus Limits`,
|
||||
cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`,
|
||||
cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`,
|
||||
cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`,
|
||||
cheats_noDeathMarks: `[UNTRANSLATED] No Death Marks`,
|
||||
cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`,
|
||||
cheats_syndicateMissionsRepeatable: `[UNTRANSLATED] Syndicate Missions Repeatable`,
|
||||
cheats_instantFinishRivenChallenge: `[UNTRANSLATED] Instant Finish Riven Challenge`,
|
||||
cheats_noDailyFocusLimit: `指挥官专精无每日获取上限`,
|
||||
cheats_noArgonCrystalDecay: `氩结晶无衰变`,
|
||||
cheats_noMasteryRankUpCooldown: `段位考核无冷却时间`,
|
||||
cheats_noVendorPurchaseLimits: `商城或商人无购买限制`,
|
||||
cheats_noDeathMarks: `无死亡标记(不会被 Stalker/Grustrag 三霸/Zanuka 猎人等标记)`,
|
||||
cheats_noKimCooldowns: `无 KIM 冷却时间`,
|
||||
cheats_syndicateMissionsRepeatable: `集团任务可重复`,
|
||||
cheats_instantFinishRivenChallenge: `立即完成裂罅挑战`,
|
||||
cheats_instantResourceExtractorDrones: `即时资源采集无人机`,
|
||||
cheats_noResourceExtractorDronesDamage: `[UNTRANSLATED] No Resource Extractor Drones Damage`,
|
||||
cheats_skipClanKeyCrafting: `[UNTRANSLATED] Skip Clan Key Crafting`,
|
||||
cheats_noResourceExtractorDronesDamage: `资源提取器不会损毁`,
|
||||
cheats_skipClanKeyCrafting: `跳过氏族钥匙制作, 进入道场无需氏族钥匙`,
|
||||
cheats_noDojoRoomBuildStage: `无视道场房间建造阶段`,
|
||||
cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`,
|
||||
cheats_noDojoDecoBuildStage: `道场装饰建造立即完成`,
|
||||
cheats_fastDojoRoomDestruction: `快速拆除道场房间`,
|
||||
cheats_noDojoResearchCosts: `无视道场研究消耗`,
|
||||
cheats_noDojoResearchTime: `无视道场研究时间`,
|
||||
cheats_fastClanAscension: `快速升级氏族`,
|
||||
cheats_spoofMasteryRank: `伪造精通段位(-1为禁用)`,
|
||||
cheats_spoofMasteryRank: `伪造精通段位(-1为禁用)`,
|
||||
cheats_nightwaveStandingMultiplier: `午夜电波声望倍率`,
|
||||
cheats_saveSettings: `保存设置`,
|
||||
cheats_account: `账户`,
|
||||
cheats_unlockAllFocusSchools: `解锁所有专精学派`,
|
||||
cheats_helminthUnlockAll: `完全升级Helminth`,
|
||||
cheats_intrinsicsUnlockAll: `[UNTRANSLATED] Max Rank All Intrinsics`,
|
||||
cheats_intrinsicsUnlockAll: `所有内源之力最大等级`,
|
||||
cheats_changeSupportedSyndicate: `支持的集团`,
|
||||
cheats_changeButton: `更改`,
|
||||
cheats_none: `无`,
|
||||
|
Loading…
x
Reference in New Issue
Block a user