Compare commits

..

1 Commits

Author SHA1 Message Date
4247272f17 feat(webui): add level keys via "add items"
All checks were successful
Build / build (20) (push) Successful in 46s
Build / build (22) (push) Successful in 1m19s
Build / build (18) (push) Successful in 1m13s
Build / build (18) (pull_request) Successful in 47s
Build / build (20) (pull_request) Successful in 1m16s
Build / build (22) (pull_request) Successful in 1m13s
2025-04-06 16:20:44 +02:00
141 changed files with 3855 additions and 8980 deletions

2
.gitattributes vendored
View File

@ -1,4 +1,4 @@
# Auto detect text files and perform LF normalization
* text=auto eol=lf
* text=auto
static/webui/libs/ linguist-vendored

View File

@ -5,22 +5,17 @@ on:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
version: [18, 20, 22]
steps:
- name: Checkout
uses: actions/checkout@v4.1.2
- name: Setup Node.js environment
uses: actions/setup-node@v4.0.2
with:
node-version: ${{ matrix.version }}
- run: npm ci
- run: cp config.json.example config.json
- run: npm run verify
- run: npm run lint:ci
- run: npm run prettier
- run: npm run update-translations
- name: Fail if there are uncommitted changes
run: |
if [[ -n "$(git status --porcelain)" ]]; then
echo "Uncommitted changes detected:"
git status
git --no-pager diff
exit 1
fi
- run: npm run lint

View File

@ -1,3 +1,5 @@
{
"recommendations": ["dbaeumer.vscode-eslint"]
"recommendations": [
"dbaeumer.vscode-eslint"
]
}

View File

@ -10,8 +10,6 @@ To get an idea of what functionality you can expect to be missing [have a look t
## config.json
SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [config.json.example](config.json.example), which has most cheats disabled.
- `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`.
- `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`.
- `worldState.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE.

View File

@ -1,6 +1,7 @@
@echo off
echo Updating SpaceNinjaServer...
git config remote.origin.url https://openwf.io/SpaceNinjaServer.git
git fetch --prune
git stash
git reset --hard origin/main

View File

@ -19,7 +19,6 @@
"infiniteEndo": false,
"infiniteRegalAya": false,
"infiniteHelminthMaterials": false,
"dontSubtractConsumables": false,
"unlockAllShipFeatures": false,
"unlockAllShipDecorations": false,
"unlockAllFlavourItems": false,
@ -30,17 +29,11 @@
"unlockExilusEverywhere": false,
"unlockArcanesEverywhere": false,
"noDailyStandingLimits": false,
"noDailyFocusLimit": false,
"noArgonCrystalDecay": false,
"noMasteryRankUpCooldown": false,
"noVendorPurchaseLimits": true,
"noDeathMarks": false,
"noKimCooldowns": false,
"instantResourceExtractorDrones": false,
"noResourceExtractorDronesDamage": false,
"skipClanKeyCrafting": false,
"noDojoRoomBuildStage": false,
"noDecoBuildStage": false,
"fastDojoRoomDestruction": false,
"noDojoResearchCosts": false,
"noDojoResearchTime": false,

16
package-lock.json generated
View File

@ -17,8 +17,8 @@
"mongoose": "^8.11.0",
"morgan": "^1.10.0",
"ncp": "^2.0.0",
"typescript": "^5.5",
"warframe-public-export-plus": "^0.5.59",
"typescript": ">=5.5 <5.6.0",
"warframe-public-export-plus": "^0.5.50",
"warframe-riven-info": "^0.1.2",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
@ -3720,9 +3720,9 @@
}
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"version": "5.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@ -3789,9 +3789,9 @@
}
},
"node_modules/warframe-public-export-plus": {
"version": "0.5.59",
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.59.tgz",
"integrity": "sha512-/SUCVjngVDBz6gahz7CdVLywtHLODL6O5nmNtQcxFDUwrUGnF1lETcG8/UO+WLeGxBVAy4BDPbq+9ZWlYZM4uQ=="
"version": "0.5.50",
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.50.tgz",
"integrity": "sha512-KlhdY/Q5sRAIn/RhmdviKBoX3gk+Jtuen0cWnFB2zqK7eKYMDtd79bKOtTPtnK9zCNzh6gFug2wEeDVam3Bwlw=="
},
"node_modules/warframe-riven-info": {
"version": "0.1.2",

View File

@ -4,12 +4,11 @@
"description": "WF Emulator",
"main": "index.ts",
"scripts": {
"start": "node --enable-source-maps --import ./build/src/pathman.js build/src/index.js",
"start": "node --import ./build/src/pathman.js build/src/index.js",
"dev": "ts-node-dev --openssl-legacy-provider -r tsconfig-paths/register src/index.ts ",
"build": "tsc --incremental --sourceMap && ncp static/webui build/static/webui",
"build": "tsc --incremental && ncp static/webui build/static/webui",
"verify": "tsgo --noEmit",
"lint": "eslint --ext .ts .",
"lint:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .",
"lint:fix": "eslint --fix --ext .ts .",
"prettier": "prettier --write .",
"update-translations": "cd scripts && node update-translations.js"
@ -24,8 +23,8 @@
"mongoose": "^8.11.0",
"morgan": "^1.10.0",
"ncp": "^2.0.0",
"typescript": "^5.5",
"warframe-public-export-plus": "^0.5.59",
"typescript": ">=5.5 <5.6.0",
"warframe-public-export-plus": "^0.5.50",
"warframe-riven-info": "^0.1.2",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"

View File

@ -4,7 +4,7 @@
const fs = require("fs");
function extractStrings(content) {
const regex = /([a-zA-Z0-9_]+): `([^`]*)`,/g;
const regex = /([a-zA-Z_]+): `([^`]*)`,/g;
let matches;
const strings = {};
while ((matches = regex.exec(content)) !== null) {
@ -15,7 +15,7 @@ function extractStrings(content) {
const source = fs.readFileSync("../static/webui/translations/en.js", "utf8");
const sourceStrings = extractStrings(source);
const sourceLines = source.substring(0, source.length - 1).split("\n");
const sourceLines = source.split("\n");
fs.readdirSync("../static/webui/translations").forEach(file => {
if (fs.lstatSync(`../static/webui/translations/${file}`).isFile() && file !== "en.js") {
@ -36,7 +36,7 @@ fs.readdirSync("../static/webui/translations").forEach(file => {
fs.writeSync(fileHandle, ` ${key}: \`[UNTRANSLATED] ${value}\`,\n`);
}
});
} else {
} else if (line.length) {
fs.writeSync(fileHandle, line + "\n");
}
});

View File

@ -16,9 +16,9 @@ import { webuiRouter } from "@/src/routes/webui";
const app = express();
app.use((req, _res, next) => {
// 38.5.0 introduced "ezip" for encrypted body blobs and "e" for request verification only (encrypted body blobs with no application data).
// 38.5.0 introduced "ezip" for encrypted body blobs.
// The bootstrapper decrypts it for us but having an unsupported Content-Encoding here would still be an issue for Express, so removing it.
if (req.headers["content-encoding"] == "ezip" || req.headers["content-encoding"] == "e") {
if (req.headers["content-encoding"] == "ezip") {
req.headers["content-encoding"] = undefined;
}
next();
@ -26,7 +26,7 @@ app.use((req, _res, next) => {
app.use(bodyParser.raw());
app.use(express.json({ limit: "4mb" }));
app.use(bodyParser.text({ limit: "4mb" }));
app.use(bodyParser.text());
app.use(requestLogger);
app.use("/api", apiRouter);

View File

@ -2,18 +2,15 @@ const millisecondsPerSecond = 1000;
const secondsPerMinute = 60;
const minutesPerHour = 60;
const hoursPerDay = 24;
const daysPerWeek = 7;
const unixSecond = millisecondsPerSecond;
const unixMinute = secondsPerMinute * millisecondsPerSecond;
const unixHour = unixMinute * minutesPerHour;
const unixDay = hoursPerDay * unixHour;
const unixWeek = daysPerWeek * unixDay;
export const unixTimesInMs = {
second: unixSecond,
minute: unixMinute,
hour: unixHour,
day: unixDay,
week: unixWeek
day: unixDay
};

View File

@ -17,7 +17,7 @@ export const activateRandomModController: RequestHandler = async (req, res) => {
ItemCount: -1
}
]);
const rivenType = getRandomElement(rivenRawToRealWeighted[request.ItemType])!;
const rivenType = getRandomElement(rivenRawToRealWeighted[request.ItemType]);
const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]);
const upgradeIndex =
inventory.Upgrades.push({

View File

@ -1,30 +0,0 @@
import { toOid } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Account, Ignore } from "@/src/models/loginModel";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IFriendInfo } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const addIgnoredUserController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const data = getJSONfromString<IAddIgnoredUserRequest>(String(req.body));
const ignoreeAccount = await Account.findOne(
{ DisplayName: data.playerName.substring(0, data.playerName.length - 1) },
"_id"
);
if (ignoreeAccount) {
await Ignore.create({ ignorer: accountId, ignoree: ignoreeAccount._id });
res.json({
Ignored: {
_id: toOid(ignoreeAccount._id),
DisplayName: data.playerName
} satisfies IFriendInfo
});
} else {
res.status(400).end();
}
};
interface IAddIgnoredUserRequest {
playerName: string;
}

View File

@ -28,7 +28,7 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
});
const rawRivenType = getRandomRawRivenType();
const rivenType = getRandomElement(rivenRawToRealWeighted[rawRivenType])!;
const rivenType = getRandomElement(rivenRawToRealWeighted[rawRivenType]);
const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]);
const upgradeIndex =
@ -57,16 +57,12 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
payload.Consumed.forEach(upgrade => {
const meta = ExportUpgrades[upgrade.ItemType];
counts[meta.rarity] += upgrade.ItemCount;
if (upgrade.ItemId.$oid != "000000000000000000000000") {
inventory.Upgrades.pull({ _id: upgrade.ItemId.$oid });
} else {
addMods(inventory, [
{
ItemType: upgrade.ItemType,
ItemCount: upgrade.ItemCount * -1
}
]);
}
addMods(inventory, [
{
ItemType: upgrade.ItemType,
ItemCount: upgrade.ItemCount * -1
}
]);
if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/AttackTransmuteCore") {
forcedPolarity = "AP_ATTACK";
} else if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/DefenseTransmuteCore") {
@ -76,33 +72,22 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
}
});
let newModType: string | undefined;
for (const specialModSet of specialModSets) {
if (specialModSet.indexOf(payload.Consumed[0].ItemType) != -1) {
newModType = getRandomElement(specialModSet);
break;
// Based on the table on https://wiki.warframe.com/w/Transmutation
const weights: Record<TRarity, number> = {
COMMON: counts.COMMON * 95 + counts.UNCOMMON * 15 + counts.RARE * 4,
UNCOMMON: counts.COMMON * 4 + counts.UNCOMMON * 80 + counts.RARE * 10,
RARE: counts.COMMON * 1 + counts.UNCOMMON * 5 + counts.RARE * 50,
LEGENDARY: 0
};
const options: { uniqueName: string; rarity: TRarity }[] = [];
Object.entries(ExportUpgrades).forEach(([uniqueName, upgrade]) => {
if (upgrade.canBeTransmutation && (!forcedPolarity || upgrade.polarity == forcedPolarity)) {
options.push({ uniqueName, rarity: upgrade.rarity });
}
}
if (!newModType) {
// Based on the table on https://wiki.warframe.com/w/Transmutation
const weights: Record<TRarity, number> = {
COMMON: counts.COMMON * 95 + counts.UNCOMMON * 15 + counts.RARE * 4,
UNCOMMON: counts.COMMON * 4 + counts.UNCOMMON * 80 + counts.RARE * 10,
RARE: counts.COMMON * 1 + counts.UNCOMMON * 5 + counts.RARE * 50,
LEGENDARY: 0
};
const options: { uniqueName: string; rarity: TRarity }[] = [];
Object.entries(ExportUpgrades).forEach(([uniqueName, upgrade]) => {
if (upgrade.canBeTransmutation && (!forcedPolarity || upgrade.polarity == forcedPolarity)) {
options.push({ uniqueName, rarity: upgrade.rarity });
}
});
newModType = getRandomWeightedReward(options, weights)!.uniqueName;
}
});
const newModType = getRandomWeightedReward(options, weights)!.uniqueName;
addMods(inventory, [
{
ItemType: newModType,
@ -145,34 +130,3 @@ interface IAgnosticUpgradeClient {
ItemCount: number;
LastAdded: IOid;
}
const specialModSets: string[][] = [
[
"/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalFourMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalFiveMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalSixMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalSevenMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalEightMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalWildcardMod"
],
[
"/Lotus/Upgrades/Mods/Immortal/AntivirusOneMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusTwoMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusThreeMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusFourMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusFiveMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusSixMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusSevenMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusEightMod"
],
[
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod",
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod",
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod",
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod",
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod"
]
];

View File

@ -15,12 +15,6 @@ export const changeDojoRootController: RequestHandler = async (req, res) => {
return;
}
// Example POST body: {"pivot":[0, 0, -64],"components":"{\"670429301ca0a63848ccc467\":{\"R\":[0,0,0],\"P\":[0,3,32]},\"6704254a1ca0a63848ccb33c\":{\"R\":[0,0,0],\"P\":[0,9.25,-32]},\"670429461ca0a63848ccc731\":{\"R\":[-90,0,0],\"P\":[-47.999992370605,3,16]}}"}
if (req.body) {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error("dojo reparent operation should not need deco repositioning"); // because we always provide SortId
}
const idToNode: Record<string, INode> = {};
guild.DojoComponents.forEach(x => {
idToNode[x._id.toString()] = {
@ -49,13 +43,23 @@ export const changeDojoRootController: RequestHandler = async (req, res) => {
newRoot.component.pp = undefined;
newRoot.parent = undefined;
// Set/update SortId in top-to-bottom order
// Don't even ask me why this is needed because I don't know either
const stack: INode[] = [newRoot];
let i = 0;
const idMap: Record<string, Types.ObjectId> = {};
while (stack.length != 0) {
const top = stack.shift()!;
top.component.SortId = new Types.ObjectId();
idMap[top.component._id.toString()] = new Types.ObjectId(
(++i).toString(16).padStart(8, "0") + top.component._id.toString().substr(8)
);
top.children.forEach(x => stack.push(x));
}
guild.DojoComponents.forEach(x => {
x._id = idMap[x._id.toString()];
if (x.pi) {
x.pi = idMap[x.pi.toString()];
}
});
logger.debug("New tree:\n" + treeToString(newRoot));

View File

@ -18,7 +18,6 @@ import {
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { toOid } from "@/src/helpers/inventoryHelpers";
interface IClaimCompletedRecipeRequest {
RecipeIds: IOid[];
@ -81,7 +80,6 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
} else {
logger.debug("Claiming Recipe", { recipe, pendingRecipe });
let BrandedSuits: undefined | IOid[];
if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
inventory.PendingSpectreLoadouts ??= [];
inventory.SpectreLoadouts ??= [];
@ -106,10 +104,9 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)),
1
);
BrandedSuits = [toOid(pendingRecipe.SuitToUnbrand!)];
}
let InventoryChanges: IInventoryChanges = {};
let InventoryChanges = {};
if (recipe.consumeOnUse) {
addRecipes(inventory, [
{
@ -133,17 +130,10 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
if (recipe.secretIngredientAction != "SIA_UNBRAND") {
InventoryChanges = {
...InventoryChanges,
...(await addItem(
inventory,
recipe.resultType,
recipe.num,
false,
undefined,
pendingRecipe.TargetFingerprint
))
...(await addItem(inventory, recipe.resultType, recipe.num, false))
};
}
await inventory.save();
res.json({ InventoryChanges, BrandedSuits });
res.json({ InventoryChanges });
}
};

View File

@ -1,4 +1,4 @@
import { addFusionPoints, getInventory } from "@/src/services/inventoryService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
@ -17,7 +17,7 @@ export const claimLibraryDailyTaskRewardController: RequestHandler = async (req,
}
syndicate.Standing += rewardStanding;
addFusionPoints(inventory, 80 * rewardQuantity);
inventory.FusionPoints += 80 * rewardQuantity;
await inventory.save();
res.json({

View File

@ -7,8 +7,6 @@ export const clearDialogueHistoryController: RequestHandler = async (req, res) =
const inventory = await getInventory(accountId);
const request = JSON.parse(String(req.body)) as IClearDialogueRequest;
if (inventory.DialogueHistory && inventory.DialogueHistory.Dialogues) {
inventory.DialogueHistory.Resets ??= 0;
inventory.DialogueHistory.Resets += 1;
for (const dialogueName of request.Dialogues) {
const index = inventory.DialogueHistory.Dialogues.findIndex(x => x.DialogueName == dialogueName);
if (index != -1) {

View File

@ -1,41 +0,0 @@
import { getCalendarProgress, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { getWorldState } from "@/src/services/worldStateService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express";
// GET request; query parameters: CompletedEventIdx=0&Iteration=4&Version=19&Season=CST_SUMMER
export const completeCalendarEventController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const calendarProgress = getCalendarProgress(inventory);
const currentSeason = getWorldState().KnownCalendarSeasons[0];
let inventoryChanges: IInventoryChanges = {};
let dayIndex = 0;
for (const day of currentSeason.Days) {
if (day.events.length == 0 || day.events[0].type != "CET_CHALLENGE") {
if (dayIndex == calendarProgress.SeasonProgress.LastCompletedDayIdx) {
if (day.events.length != 0) {
const selection = day.events[parseInt(req.query.CompletedEventIdx as string)];
if (selection.type == "CET_REWARD") {
inventoryChanges = (await handleStoreItemAcquisition(selection.reward!, inventory))
.InventoryChanges;
} else if (selection.type == "CET_UPGRADE") {
calendarProgress.YearProgress.Upgrades.push(selection.upgrade!);
} else if (selection.type != "CET_PLOT") {
throw new Error(`unexpected selection type: ${selection.type}`);
}
}
break;
}
++dayIndex;
}
}
calendarProgress.SeasonProgress.LastCompletedDayIdx++;
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
CalendarProgress: inventory.CalendarProgress
});
};

View File

@ -1,14 +1,8 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild, GuildMember } from "@/src/models/guildModel";
import { Account } from "@/src/models/loginModel";
import {
deleteGuild,
getGuildClient,
giveClanKey,
hasGuildPermission,
removeDojoKeyItems
} from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { deleteGuild, getGuildClient, hasGuildPermission, removeDojoKeyItems } from "@/src/services/guildService";
import { addRecipes, combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
@ -47,7 +41,14 @@ export const confirmGuildInvitationGetController: RequestHandler = async (req, r
// Update inventory of new member
const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes");
inventory.GuildId = new Types.ObjectId(req.query.clanId as string);
giveClanKey(inventory, inventoryChanges);
const recipeChanges = [
{
ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
ItemCount: 1
}
];
addRecipes(inventory, recipeChanges);
combineInventoryChanges(inventoryChanges, { Recipes: recipeChanges });
await inventory.save();
const guild = (await Guild.findById(req.query.clanId as string))!;
@ -95,9 +96,14 @@ export const confirmGuildInvitationPostController: RequestHandler = async (req,
await GuildMember.deleteMany({ accountId: guildMember.accountId, status: 1 });
// Update inventory of new member
const inventory = await getInventory(guildMember.accountId.toString(), "GuildId LevelKeys Recipes");
const inventory = await getInventory(guildMember.accountId.toString(), "GuildId Recipes");
inventory.GuildId = new Types.ObjectId(req.query.clanId as string);
giveClanKey(inventory);
addRecipes(inventory, [
{
ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
ItemCount: 1
}
]);
await inventory.save();
// Add join to clan log

View File

@ -1,8 +1,9 @@
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild } from "@/src/models/guildModel";
import { checkClanAscensionHasRequiredContributors } from "@/src/services/guildService";
import { addFusionPoints, getInventory } from "@/src/services/inventoryService";
import { Guild, GuildMember } from "@/src/models/guildModel";
import { config } from "@/src/services/configService";
import { createMessage } from "@/src/services/inboxService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
import { Types } from "mongoose";
@ -30,13 +31,49 @@ export const contributeGuildClassController: RequestHandler = async (req, res) =
guild.CeremonyContributors.push(new Types.ObjectId(accountId));
await checkClanAscensionHasRequiredContributors(guild);
// Once required contributor count is hit, the class is committed and there's 72 hours to claim endo.
if (guild.CeremonyContributors.length == payload.RequiredContributors) {
guild.Class = guild.CeremonyClass!;
guild.CeremonyClass = undefined;
guild.CeremonyResetDate = new Date(Date.now() + (config.fastClanAscension ? 5_000 : 72 * 3600_000));
if (!config.fastClanAscension) {
// Send message to all active guild members
const members = await GuildMember.find({ guildId: payload.GuildId, status: 0 }, "accountId");
for (const member of members) {
// somewhat unfaithful as on live the "msg" is not a loctag, but since we don't have the string, we'll let the client fill it in with "arg".
await createMessage(member.accountId, [
{
sndr: guild.Name,
msg: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgressDetails",
arg: [
{
Key: "RESETDATE",
Tag:
guild.CeremonyResetDate.getUTCMonth() +
"/" +
guild.CeremonyResetDate.getUTCDate() +
"/" +
(guild.CeremonyResetDate.getUTCFullYear() % 100) +
" " +
guild.CeremonyResetDate.getUTCHours().toString().padStart(2, "0") +
":" +
guild.CeremonyResetDate.getUTCMinutes().toString().padStart(2, "0")
}
],
sub: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgress",
icon: "/Lotus/Interface/Graphics/ClanTileImages/ClanEnterDojo.png",
highPriority: true
}
]);
}
}
}
await guild.save();
// Either way, endo is given to the contributor.
const inventory = await getInventory(accountId, "FusionPoints");
addFusionPoints(inventory, guild.CeremonyEndo!);
inventory.FusionPoints += guild.CeremonyEndo!;
await inventory.save();
res.json({

View File

@ -2,9 +2,8 @@ import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild, GuildMember } from "@/src/models/guildModel";
import { createUniqueClanName, getGuildClient, giveClanKey } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { createUniqueClanName, getGuildClient } from "@/src/services/guildService";
import { addRecipes, getInventory } from "@/src/services/inventoryService";
export const createGuildController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -27,15 +26,26 @@ export const createGuildController: RequestHandler = async (req, res) => {
rank: 0
});
const inventory = await getInventory(accountId, "GuildId LevelKeys Recipes");
const inventory = await getInventory(accountId, "GuildId Recipes");
inventory.GuildId = guild._id;
const inventoryChanges: IInventoryChanges = {};
giveClanKey(inventory, inventoryChanges);
addRecipes(inventory, [
{
ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
ItemCount: 1
}
]);
await inventory.save();
res.json({
...(await getGuildClient(guild, accountId)),
InventoryChanges: inventoryChanges
InventoryChanges: {
Recipes: [
{
ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
ItemCount: 1
}
]
}
});
};

View File

@ -1,28 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { ICrewMemberClient } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
import { Types } from "mongoose";
export const crewMembersController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "CrewMembers");
const data = getJSONfromString<ICrewMembersRequest>(String(req.body));
const dbCrewMember = inventory.CrewMembers.id(data.crewMember.ItemId.$oid)!;
dbCrewMember.AssignedRole = data.crewMember.AssignedRole;
dbCrewMember.SkillEfficiency = data.crewMember.SkillEfficiency;
dbCrewMember.WeaponConfigIdx = data.crewMember.WeaponConfigIdx;
dbCrewMember.WeaponId = new Types.ObjectId(data.crewMember.WeaponId.$oid);
dbCrewMember.Configs = data.crewMember.Configs;
dbCrewMember.SecondInCommand = data.crewMember.SecondInCommand;
await inventory.save();
res.json({
crewMemberId: data.crewMember.ItemId.$oid,
NemesisFingerprint: data.crewMember.NemesisFingerprint
});
};
interface ICrewMembersRequest {
crewMember: ICrewMemberClient;
}

View File

@ -1,84 +0,0 @@
import {
addCrewShipSalvagedWeaponSkin,
addCrewShipRawSalvage,
getInventory,
addEquipment
} from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
import { ICrewShipComponentFingerprint, IInnateDamageFingerprint } from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportCustoms, ExportRailjackWeapons, ExportUpgrades } from "warframe-public-export-plus";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { getRandomInt } from "@/src/services/rngService";
import { IFingerprintStat } from "@/src/helpers/rivenHelper";
import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes";
export const crewShipIdentifySalvageController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(
accountId,
"CrewShipSalvagedWeaponSkins CrewShipSalvagedWeapons CrewShipRawSalvage"
);
const payload = getJSONfromString<ICrewShipIdentifySalvageRequest>(String(req.body));
const inventoryChanges: IInventoryChanges = {};
if (payload.ItemType in ExportCustoms) {
const meta = ExportCustoms[payload.ItemType];
let upgradeFingerprint: ICrewShipComponentFingerprint = { compat: payload.ItemType, buffs: [] };
if (meta.subroutines) {
upgradeFingerprint = {
SubroutineIndex: getRandomInt(0, meta.subroutines.length - 1),
...upgradeFingerprint
};
}
for (const upgrade of meta.randomisedUpgrades!) {
upgradeFingerprint.buffs.push({ Tag: upgrade.tag, Value: Math.trunc(Math.random() * 0x40000000) });
}
addCrewShipSalvagedWeaponSkin(
inventory,
payload.ItemType,
JSON.stringify(upgradeFingerprint),
inventoryChanges
);
} else {
const meta = ExportRailjackWeapons[payload.ItemType];
let defaultOverwrites: Partial<IEquipmentDatabase> | undefined;
if (meta.defaultUpgrades?.[0]) {
const upgradeType = meta.defaultUpgrades[0].ItemType;
const upgradeMeta = ExportUpgrades[upgradeType];
const buffs: IFingerprintStat[] = [];
for (const buff of upgradeMeta.upgradeEntries!) {
buffs.push({
Tag: buff.tag,
Value: Math.trunc(Math.random() * 0x40000000)
});
}
defaultOverwrites = {
UpgradeType: upgradeType,
UpgradeFingerprint: JSON.stringify({
compat: payload.ItemType,
buffs
} satisfies IInnateDamageFingerprint)
};
}
addEquipment(inventory, "CrewShipSalvagedWeapons", payload.ItemType, defaultOverwrites, inventoryChanges);
}
inventoryChanges.CrewShipRawSalvage = [
{
ItemType: payload.ItemType,
ItemCount: -1
}
];
addCrewShipRawSalvage(inventory, inventoryChanges.CrewShipRawSalvage);
await inventory.save();
res.json({
InventoryChanges: inventoryChanges
});
};
interface ICrewShipIdentifySalvageRequest {
ItemType: string;
}

View File

@ -1,11 +1,5 @@
import { RequestHandler } from "express";
// Arbiter Dojo endpoints, not really used by us as we don't provide a ContentURL.
export const dojoController: RequestHandler = (_req, res) => {
res.json("-1"); // Tell client to use authorised request.
};
export const setDojoURLController: RequestHandler = (_req, res) => {
res.end();
};

View File

@ -55,7 +55,7 @@ export const dronesController: RequestHandler = async (req, res) => {
? new Date()
: new Date(Date.now() + getRandomInt(3 * 3600 * 1000, 4 * 3600 * 1000));
drone.PendingDamage =
!config.noResourceExtractorDronesDamage && Math.random() < system.damageChance
Math.random() < system.damageChance
? getRandomInt(system.droneDamage.minValue, system.droneDamage.maxValue)
: 0;
const resource = getRandomWeightedRewardUc(system.resources, droneMeta.probabilities)!;

View File

@ -21,12 +21,10 @@ export const entratiLabConquestModeController: RequestHandler = async (req, res)
inventory.EntratiVaultCountResetDate = new Date(weekEnd);
if (inventory.EntratiLabConquestUnlocked) {
inventory.EntratiLabConquestUnlocked = 0;
inventory.EntratiLabConquestCacheScoreMission = 0;
inventory.EntratiLabConquestActiveFrameVariants = [];
}
if (inventory.EchoesHexConquestUnlocked) {
inventory.EchoesHexConquestUnlocked = 0;
inventory.EchoesHexConquestCacheScoreMission = 0;
inventory.EchoesHexConquestActiveFrameVariants = [];
inventory.EchoesHexConquestActiveStickers = [];
}

View File

@ -1,9 +1,10 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMiscItems, addStanding, getInventory } from "@/src/services/inventoryService";
import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
import { addMiscItems, getInventory, getStandingLimit, updateStandingLimit } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
import { ExportResources } from "warframe-public-export-plus";
import { ExportResources, ExportSyndicates } from "warframe-public-export-plus";
export const fishmongerController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -30,15 +31,32 @@ export const fishmongerController: RequestHandler = async (req, res) => {
miscItemChanges.push({ ItemType: fish.ItemType, ItemCount: fish.ItemCount * -1 });
}
addMiscItems(inventory, miscItemChanges);
let affiliationMod;
if (gainedStanding && syndicateTag) affiliationMod = addStanding(inventory, syndicateTag, gainedStanding);
if (gainedStanding && syndicateTag) {
let syndicate = inventory.Affiliations.find(x => x.Tag == syndicateTag);
if (!syndicate) {
syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: syndicateTag, Standing: 0 }) - 1];
}
const syndicateMeta = ExportSyndicates[syndicateTag];
const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0);
if (syndicate.Standing + gainedStanding > max) {
gainedStanding = max - syndicate.Standing;
}
if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) {
gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin);
}
syndicate.Standing += gainedStanding;
updateStandingLimit(inventory, syndicateMeta.dailyLimitBin, gainedStanding);
}
await inventory.save();
res.json({
InventoryChanges: {
MiscItems: miscItemChanges
},
SyndicateTag: syndicateTag,
StandingChange: affiliationMod?.Standing || 0
StandingChange: gainedStanding
});
};

View File

@ -104,14 +104,13 @@ export const focusController: RequestHandler = async (req, res) => {
}
case FocusOperation.SentTrainingAmplifier: {
const request = JSON.parse(String(req.body)) as ISentTrainingAmplifierRequest;
const parts: string[] = [
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingGrip",
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingChassis",
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingBarrel"
];
const inventory = await getInventory(accountId);
const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, {
ModularParts: [
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingGrip",
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingChassis",
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingBarrel"
]
});
const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, parts);
occupySlot(inventory, InventorySlot.AMPS, false);
await inventory.save();
res.json((inventoryChanges.OperatorAmps as IEquipmentClient[])[0]);

View File

@ -1,84 +0,0 @@
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMiscItem, getInventory } from "@/src/services/inventoryService";
import { toStoreItem } from "@/src/services/itemDataService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService";
import { IMongoDate } from "@/src/types/commonTypes";
import { IMissionReward } from "@/src/types/missionTypes";
import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IGardeningClient } from "@/src/types/shipTypes";
import { RequestHandler } from "express";
import { dict_en, ExportResources } from "warframe-public-export-plus";
export const gardeningController: RequestHandler = async (req, res) => {
const data = getJSONfromString<IGardeningRequest>(String(req.body));
if (data.Mode != "HarvestAll") {
throw new Error(`unexpected gardening mode: ${data.Mode}`);
}
const accountId = await getAccountIdForRequest(req);
const [inventory, personalRooms] = await Promise.all([
getInventory(accountId, "MiscItems"),
getPersonalRooms(accountId, "Apartment")
]);
// Harvest plants
const inventoryChanges: IInventoryChanges = {};
const rewards: Record<string, IMissionReward[][]> = {};
for (const planter of personalRooms.Apartment.Gardening.Planters) {
rewards[planter.Name] = [];
for (const plant of planter.Plants) {
const itemType =
"/Lotus/Types/Gameplay/Duviri/Resource/DuviriPlantItem" +
plant.PlantType.substring(plant.PlantType.length - 1);
const itemCount = Math.random() < 0.775 ? 2 : 4;
addMiscItem(inventory, itemType, itemCount, inventoryChanges);
rewards[planter.Name].push([
{
StoreItem: toStoreItem(itemType),
TypeName: itemType,
ItemCount: itemCount,
DailyCooldown: false,
Rarity: itemCount == 2 ? 0.7743589743589744 : 0.22564102564102564,
TweetText: `${itemCount}x ${dict_en[ExportResources[itemType].name]} (Resource)`,
ProductCategory: "MiscItems"
}
]);
}
}
// Refresh garden
personalRooms.Apartment.Gardening = createGarden();
await Promise.all([inventory.save(), personalRooms.save()]);
const planter = personalRooms.Apartment.Gardening.Planters[personalRooms.Apartment.Gardening.Planters.length - 1];
const plant = planter.Plants[planter.Plants.length - 1];
res.json({
GardenTagName: planter.Name,
PlantType: plant.PlantType,
PlotIndex: plant.PlotIndex,
EndTime: toMongoDate(plant.EndTime),
InventoryChanges: inventoryChanges,
Gardening: personalRooms.toJSON<IPersonalRoomsClient>().Apartment.Gardening,
Rewards: rewards
} satisfies IGardeningResponse);
};
interface IGardeningRequest {
Mode: string;
}
interface IGardeningResponse {
GardenTagName: string;
PlantType: string;
PlotIndex: number;
EndTime: IMongoDate;
InventoryChanges: IInventoryChanges;
Gardening: IGardeningClient;
Rewards: Record<string, IMissionReward[][]>;
}

View File

@ -1,20 +1,16 @@
import { toOid } from "@/src/helpers/inventoryHelpers";
import { Account, Ignore } from "@/src/models/loginModel";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IFriendInfo } from "@/src/types/guildTypes";
import { parallelForeach } from "@/src/utils/async-utils";
import { RequestHandler } from "express";
export const getIgnoredUsersController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const ignores = await Ignore.find({ ignorer: accountId });
const ignoredUsers: IFriendInfo[] = [];
await parallelForeach(ignores, async ignore => {
const ignoreeAccount = (await Account.findById(ignore.ignoree, "DisplayName"))!;
ignoredUsers.push({
_id: toOid(ignore.ignoree),
DisplayName: ignoreeAccount.DisplayName + ""
});
const getIgnoredUsersController: RequestHandler = (_req, res) => {
res.writeHead(200, {
"Content-Type": "text/html",
"Content-Length": "3"
});
res.json({ IgnoredUsers: ignoredUsers });
res.end(
Buffer.from([
0x7b, 0x22, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x22, 0x3a, 0x38, 0x33, 0x30, 0x34, 0x30, 0x37, 0x37, 0x32, 0x32,
0x34, 0x30, 0x32, 0x32, 0x32, 0x36, 0x31, 0x35, 0x30, 0x31, 0x7d
])
);
};
export { getIgnoredUsersController };

View File

@ -1,12 +1,14 @@
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { generateRewardSeed } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
export const getNewRewardSeedController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const rewardSeed = generateRewardSeed();
logger.debug(`generated new reward seed: ${rewardSeed}`);
await Inventory.updateOne(
{
accountOwnerId: accountId

View File

@ -2,24 +2,17 @@ import { RequestHandler } from "express";
import { config } from "@/src/services/configService";
import allShipFeatures from "@/static/fixed_responses/allShipFeatures.json";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService";
import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { getShip } from "@/src/services/shipService";
import { toOid } from "@/src/helpers/inventoryHelpers";
import { IGetShipResponse } from "@/src/types/shipTypes";
import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
import { IPersonalRooms } from "@/src/types/personalRoomsTypes";
import { getLoadout } from "@/src/services/loadoutService";
export const getShipController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const personalRoomsDb = await getPersonalRooms(accountId);
// Setup gardening if it's missing. Maybe should be done as part of some quest completion in the future.
if (personalRoomsDb.Apartment.Gardening.Planters.length == 0) {
personalRoomsDb.Apartment.Gardening = createGarden();
await personalRoomsDb.save();
}
const personalRooms = personalRoomsDb.toJSON<IPersonalRoomsClient>();
const personalRooms = personalRoomsDb.toJSON<IPersonalRooms>();
const loadout = await getLoadout(accountId);
const ship = await getShip(personalRoomsDb.activeShipId, "ShipAttachments SkinFlavourItem");
@ -33,10 +26,7 @@ export const getShipController: RequestHandler = async (req, res) => {
Colors: personalRooms.ShipInteriorColors,
ShipAttachments: ship.ShipAttachments,
SkinFlavourItem: ship.SkinFlavourItem
},
FavouriteLoadoutId: personalRooms.Ship.FavouriteLoadoutId
? toOid(personalRooms.Ship.FavouriteLoadoutId)
: undefined
}
},
Apartment: personalRooms.Apartment,
TailorShop: personalRooms.TailorShop

View File

@ -2,25 +2,36 @@ import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { WeaponTypeInternal } from "@/src/services/itemDataService";
import { ArtifactPolarity, EquipmentFeatures, IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { ExportRecipes } from "warframe-public-export-plus";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
const modularWeaponCategory: (WeaponTypeInternal | "Hoverboards")[] = [
"LongGuns",
"Pistols",
"Melee",
"OperatorAmps",
"Hoverboards"
];
interface IGildWeaponRequest {
ItemName: string;
Recipe: string; // e.g. /Lotus/Weapons/SolarisUnited/LotusGildKitgunBlueprint
PolarizeSlot?: number;
PolarizeValue?: ArtifactPolarity;
ItemId: string;
Category: TEquipmentKey;
Category: WeaponTypeInternal | "Hoverboards";
}
export const gildWeaponController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const data = getJSONfromString<IGildWeaponRequest>(String(req.body));
data.ItemId = String(req.query.ItemId);
data.Category = req.query.Category as TEquipmentKey;
if (!modularWeaponCategory.includes(req.query.Category as WeaponTypeInternal | "Hoverboards")) {
throw new Error(`Unknown modular weapon Category: ${String(req.query.Category)}`);
}
data.Category = req.query.Category as WeaponTypeInternal | "Hoverboards";
const inventory = await getInventory(accountId);
const weaponIndex = inventory[data.Category].findIndex(x => String(x._id) === data.ItemId);
@ -31,10 +42,8 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
const weapon = inventory[data.Category][weaponIndex];
weapon.Features ??= 0;
weapon.Features |= EquipmentFeatures.GILDED;
if (data.Recipe != "webui") {
weapon.ItemName = data.ItemName;
weapon.XP = 0;
}
weapon.ItemName = data.ItemName;
weapon.XP = 0;
if (data.Category != "OperatorAmps" && data.PolarizeSlot && data.PolarizeValue) {
weapon.Polarity = [
{
@ -47,24 +56,21 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
const inventoryChanges: IInventoryChanges = {};
inventoryChanges[data.Category] = [weapon.toJSON<IEquipmentClient>()];
const recipe = ExportRecipes[data.Recipe];
inventoryChanges.MiscItems = recipe.secretIngredients!.map(ingredient => ({
ItemType: ingredient.ItemType,
ItemCount: ingredient.ItemCount * -1
}));
addMiscItems(inventory, inventoryChanges.MiscItems);
const affiliationMods = [];
if (data.Recipe != "webui") {
const recipe = ExportRecipes[data.Recipe];
inventoryChanges.MiscItems = recipe.secretIngredients!.map(ingredient => ({
ItemType: ingredient.ItemType,
ItemCount: ingredient.ItemCount * -1
}));
addMiscItems(inventory, inventoryChanges.MiscItems);
if (recipe.syndicateStandingChange) {
const affiliation = inventory.Affiliations.find(x => x.Tag == recipe.syndicateStandingChange!.tag)!;
affiliation.Standing += recipe.syndicateStandingChange.value;
affiliationMods.push({
Tag: recipe.syndicateStandingChange.tag,
Standing: recipe.syndicateStandingChange.value
});
}
if (recipe.syndicateStandingChange) {
const affiliation = inventory.Affiliations.find(x => x.Tag == recipe.syndicateStandingChange!.tag)!;
affiliation.Standing += recipe.syndicateStandingChange.value;
affiliationMods.push({
Tag: recipe.syndicateStandingChange.tag,
Standing: recipe.syndicateStandingChange.value
});
}
await inventory.save();

View File

@ -16,7 +16,7 @@ export const giveQuestKeyRewardController: RequestHandler = async (req, res) =>
const inventory = await getInventory(accountId);
const inventoryChanges = await addItem(inventory, reward.ItemType, reward.Amount);
await inventory.save();
res.json(inventoryChanges);
res.json(inventoryChanges.InventoryChanges);
//TODO: consider whishlist changes
};

View File

@ -1,20 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addLoreFragmentScans, addShipDecorations, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { ILoreFragmentScan, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
export const giveShipDecoAndLoreFragmentController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "LoreFragmentScans ShipDecorations");
const data = getJSONfromString<IGiveShipDecoAndLoreFragmentRequest>(String(req.body));
addLoreFragmentScans(inventory, data.LoreFragmentScans);
addShipDecorations(inventory, data.ShipDecorations);
await inventory.save();
res.end();
};
interface IGiveShipDecoAndLoreFragmentRequest {
LoreFragmentScans: ILoreFragmentScan[];
ShipDecorations: ITypeCount[];
}

View File

@ -14,33 +14,29 @@ import {
import { ExportDojoRecipes } from "warframe-public-export-plus";
import { getAccountIdForRequest } from "@/src/services/loginService";
import {
addCrewShipWeaponSkin,
addEquipment,
addItem,
addMiscItems,
addRecipes,
combineInventoryChanges,
getInventory,
occupySlot,
updateCurrency
} from "@/src/services/inventoryService";
import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { config } from "@/src/services/configService";
import { GuildPermission, ITechProjectClient } from "@/src/types/guildTypes";
import { GuildMember } from "@/src/models/guildModel";
import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { logger } from "@/src/utils/logger";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
export const guildTechController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const guild = await getGuildForRequestEx(req, inventory);
const data = JSON.parse(String(req.body)) as TGuildTechRequest;
if (data.Action == "Sync") {
let needSave = false;
const techProjects: ITechProjectClient[] = [];
const guild = await getGuildForRequestEx(req, inventory);
if (guild.TechProjects) {
for (const project of guild.TechProjects) {
const techProject: ITechProjectClient = {
@ -63,224 +59,137 @@ export const guildTechController: RequestHandler = async (req, res) => {
}
res.json({ TechProjects: techProjects });
} else if (data.Action == "Start") {
if (data.Mode == "Guild") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end();
return;
}
const recipe = ExportDojoRecipes.research[data.RecipeType];
guild.TechProjects ??= [];
if (!guild.TechProjects.find(x => x.ItemType == data.RecipeType)) {
const techProject =
guild.TechProjects[
guild.TechProjects.push({
ItemType: data.RecipeType,
ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price),
ReqItems: recipe.ingredients.map(x => ({
ItemType: x.ItemType,
ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount)
})),
State: 0
}) - 1
];
setGuildTechLogState(guild, techProject.ItemType, 5);
if (config.noDojoResearchCosts) {
processFundedGuildTechProject(guild, techProject, recipe);
} else {
if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") {
guild.ActiveDojoColorResearch = data.RecipeType;
}
}
}
await guild.save();
res.end();
} else {
const recipe = ExportDojoRecipes.research[data.RecipeType];
if (data.TechProductCategory) {
if (
data.TechProductCategory != "CrewShipWeapons" &&
data.TechProductCategory != "CrewShipWeaponSkins"
) {
throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`);
}
if (!inventory[getSalvageCategory(data.TechProductCategory)].id(data.CategoryItemId)) {
throw new Error(
`no item with id ${data.CategoryItemId} in ${getSalvageCategory(data.TechProductCategory)} array`
);
}
}
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end();
return;
}
const recipe = ExportDojoRecipes.research[data.RecipeType];
guild.TechProjects ??= [];
if (!guild.TechProjects.find(x => x.ItemType == data.RecipeType)) {
const techProject =
inventory.PersonalTechProjects[
inventory.PersonalTechProjects.push({
State: 0,
ReqCredits: recipe.price,
guild.TechProjects[
guild.TechProjects.push({
ItemType: data.RecipeType,
ProductCategory: data.TechProductCategory,
CategoryItemId: data.CategoryItemId,
ReqItems: recipe.ingredients
ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price),
ReqItems: recipe.ingredients.map(x => ({
ItemType: x.ItemType,
ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount)
})),
State: 0
}) - 1
];
await inventory.save();
res.json({
isPersonal: true,
action: "Start",
personalTech: techProject.toJSON()
});
setGuildTechLogState(guild, techProject.ItemType, 5);
if (config.noDojoResearchCosts) {
processFundedGuildTechProject(guild, techProject, recipe);
} else {
if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") {
guild.ActiveDojoColorResearch = data.RecipeType;
}
}
}
await guild.save();
res.end();
} else if (data.Action == "Contribute") {
if ((req.query.guildId as string) == "000000000000000000000000") {
const techProject = inventory.PersonalTechProjects.id(data.ResearchId)!;
techProject.ReqCredits -= data.RegularCredits;
const inventoryChanges: IInventoryChanges = updateCurrency(inventory, data.RegularCredits, false);
const miscItemChanges = [];
for (const miscItem of data.MiscItems) {
const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
if (reqItem) {
if (miscItem.ItemCount > reqItem.ItemCount) {
miscItem.ItemCount = reqItem.ItemCount;
}
reqItem.ItemCount -= miscItem.ItemCount;
miscItemChanges.push({
ItemType: miscItem.ItemType,
ItemCount: miscItem.ItemCount * -1
});
}
}
addMiscItems(inventory, miscItemChanges);
inventoryChanges.MiscItems = miscItemChanges;
techProject.HasContributions = true;
if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) {
techProject.State = 1;
const recipe = ExportDojoRecipes.research[techProject.ItemType];
techProject.CompletionDate = new Date(Date.now() + recipe.time * 1000);
}
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
PersonalResearch: { $oid: data.ResearchId },
PersonalResearchDate: techProject.CompletionDate ? toMongoDate(techProject.CompletionDate) : undefined
});
} else {
if (!hasAccessToDojo(inventory)) {
res.status(400).send("-1").end();
return;
}
const guild = await getGuildForRequestEx(req, inventory);
const guildMember = (await GuildMember.findOne(
{ accountId, guildId: guild._id },
"RegularCreditsContributed MiscItemsContributed"
))!;
const techProject = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!;
if (data.VaultCredits) {
if (data.VaultCredits > techProject.ReqCredits) {
data.VaultCredits = techProject.ReqCredits;
}
techProject.ReqCredits -= data.VaultCredits;
guild.VaultRegularCredits! -= data.VaultCredits;
}
if (data.RegularCredits > techProject.ReqCredits) {
data.RegularCredits = techProject.ReqCredits;
}
techProject.ReqCredits -= data.RegularCredits;
guildMember.RegularCreditsContributed ??= 0;
guildMember.RegularCreditsContributed += data.RegularCredits;
if (data.VaultMiscItems.length) {
for (const miscItem of data.VaultMiscItems) {
const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
if (reqItem) {
if (miscItem.ItemCount > reqItem.ItemCount) {
miscItem.ItemCount = reqItem.ItemCount;
}
reqItem.ItemCount -= miscItem.ItemCount;
const vaultMiscItem = guild.VaultMiscItems!.find(x => x.ItemType == miscItem.ItemType)!;
vaultMiscItem.ItemCount -= miscItem.ItemCount;
}
}
}
const miscItemChanges = [];
for (const miscItem of data.MiscItems) {
const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
if (reqItem) {
if (miscItem.ItemCount > reqItem.ItemCount) {
miscItem.ItemCount = reqItem.ItemCount;
}
reqItem.ItemCount -= miscItem.ItemCount;
miscItemChanges.push({
ItemType: miscItem.ItemType,
ItemCount: miscItem.ItemCount * -1
});
addGuildMemberMiscItemContribution(guildMember, miscItem);
}
}
addMiscItems(inventory, miscItemChanges);
const inventoryChanges: IInventoryChanges = updateCurrency(inventory, data.RegularCredits, false);
inventoryChanges.MiscItems = miscItemChanges;
// Check if research is fully funded now.
await processGuildTechProjectContributionsUpdate(guild, techProject);
await Promise.all([guild.save(), inventory.save(), guildMember.save()]);
res.json({
InventoryChanges: inventoryChanges,
Vault: getGuildVault(guild)
});
if (!hasAccessToDojo(inventory)) {
res.status(400).send("-1").end();
return;
}
const guildMember = (await GuildMember.findOne(
{ accountId, guildId: guild._id },
"RegularCreditsContributed MiscItemsContributed"
))!;
const contributions = data;
const techProject = guild.TechProjects!.find(x => x.ItemType == contributions.RecipeType)!;
if (contributions.VaultCredits) {
if (contributions.VaultCredits > techProject.ReqCredits) {
contributions.VaultCredits = techProject.ReqCredits;
}
techProject.ReqCredits -= contributions.VaultCredits;
guild.VaultRegularCredits! -= contributions.VaultCredits;
}
if (contributions.RegularCredits > techProject.ReqCredits) {
contributions.RegularCredits = techProject.ReqCredits;
}
techProject.ReqCredits -= contributions.RegularCredits;
guildMember.RegularCreditsContributed ??= 0;
guildMember.RegularCreditsContributed += contributions.RegularCredits;
if (contributions.VaultMiscItems.length) {
for (const miscItem of contributions.VaultMiscItems) {
const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
if (reqItem) {
if (miscItem.ItemCount > reqItem.ItemCount) {
miscItem.ItemCount = reqItem.ItemCount;
}
reqItem.ItemCount -= miscItem.ItemCount;
const vaultMiscItem = guild.VaultMiscItems!.find(x => x.ItemType == miscItem.ItemType)!;
vaultMiscItem.ItemCount -= miscItem.ItemCount;
}
}
}
const miscItemChanges = [];
for (const miscItem of contributions.MiscItems) {
const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
if (reqItem) {
if (miscItem.ItemCount > reqItem.ItemCount) {
miscItem.ItemCount = reqItem.ItemCount;
}
reqItem.ItemCount -= miscItem.ItemCount;
miscItemChanges.push({
ItemType: miscItem.ItemType,
ItemCount: miscItem.ItemCount * -1
});
addGuildMemberMiscItemContribution(guildMember, miscItem);
}
}
addMiscItems(inventory, miscItemChanges);
const inventoryChanges: IInventoryChanges = updateCurrency(inventory, contributions.RegularCredits, false);
inventoryChanges.MiscItems = miscItemChanges;
// Check if research is fully funded now.
await processGuildTechProjectContributionsUpdate(guild, techProject);
await Promise.all([guild.save(), inventory.save(), guildMember.save()]);
res.json({
InventoryChanges: inventoryChanges,
Vault: getGuildVault(guild)
});
} else if (data.Action.split(",")[0] == "Buy") {
const purchase = data as IGuildTechBuyRequest;
if (purchase.Mode == "Guild") {
const guild = await getGuildForRequestEx(req, inventory);
if (
!hasAccessToDojo(inventory) ||
!(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))
) {
res.status(400).send("-1").end();
return;
}
const quantity = parseInt(data.Action.split(",")[1]);
const recipeChanges = [
{
ItemType: purchase.RecipeType,
ItemCount: quantity
}
];
addRecipes(inventory, recipeChanges);
const currencyChanges = updateCurrency(
inventory,
ExportDojoRecipes.research[purchase.RecipeType].replicatePrice,
false
);
await inventory.save();
// Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`.
res.json({
inventoryChanges: {
...currencyChanges,
Recipes: recipeChanges
}
});
} else {
const inventoryChanges = claimSalvagedComponent(inventory, purchase.CategoryItemId!);
await inventory.save();
res.json({
inventoryChanges: inventoryChanges
});
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) {
res.status(400).send("-1").end();
return;
}
const purchase = data as IGuildTechBuyRequest;
const quantity = parseInt(data.Action.split(",")[1]);
const recipeChanges = [
{
ItemType: purchase.RecipeType,
ItemCount: quantity
}
];
addRecipes(inventory, recipeChanges);
const currencyChanges = updateCurrency(
inventory,
ExportDojoRecipes.research[purchase.RecipeType].replicatePrice,
false
);
await inventory.save();
// Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`.
res.json({
inventoryChanges: {
...currencyChanges,
Recipes: recipeChanges
}
});
} else if (data.Action == "Fabricate") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) {
res.status(400).send("-1").end();
return;
@ -297,7 +206,6 @@ export const guildTechController: RequestHandler = async (req, res) => {
// Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`.
res.json({ inventoryChanges: inventoryChanges });
} else if (data.Action == "Pause") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end();
return;
@ -309,7 +217,6 @@ export const guildTechController: RequestHandler = async (req, res) => {
await removePigmentsFromGuildMembers(guild._id);
res.end();
} else if (data.Action == "Unpause") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end();
return;
@ -319,66 +226,9 @@ export const guildTechController: RequestHandler = async (req, res) => {
guild.ActiveDojoColorResearch = data.RecipeType;
await guild.save();
res.end();
} else if (data.Action == "Cancel" && data.CategoryItemId) {
const personalTechProjectIndex = inventory.PersonalTechProjects.findIndex(x =>
x.CategoryItemId?.equals(data.CategoryItemId)
);
const personalTechProject = inventory.PersonalTechProjects[personalTechProjectIndex];
inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1);
const meta = ExportDojoRecipes.research[personalTechProject.ItemType];
const contributedCredits = meta.price - personalTechProject.ReqCredits;
const inventoryChanges = updateCurrency(inventory, contributedCredits * -1, false);
inventoryChanges.MiscItems = [];
for (const ingredient of meta.ingredients) {
const reqItem = personalTechProject.ReqItems.find(x => x.ItemType == ingredient.ItemType);
if (reqItem) {
const contributedItems = ingredient.ItemCount - reqItem.ItemCount;
inventoryChanges.MiscItems.push({
ItemType: ingredient.ItemType,
ItemCount: contributedItems
});
}
}
addMiscItems(inventory, inventoryChanges.MiscItems);
await inventory.save();
res.json({
action: "Cancel",
isPersonal: true,
inventoryChanges: inventoryChanges,
personalTech: {
ItemId: toOid(personalTechProject._id)
}
});
} else if (data.Action == "Rush" && data.CategoryItemId) {
const inventoryChanges: IInventoryChanges = {
...updateCurrency(inventory, 20, true),
...claimSalvagedComponent(inventory, data.CategoryItemId)
};
await inventory.save();
res.json({
inventoryChanges: inventoryChanges
});
} else if (data.Action == "InstantFinish") {
if (data.TechProductCategory != "CrewShipWeapons" && data.TechProductCategory != "CrewShipWeaponSkins") {
throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`);
}
const inventoryChanges = finishComponentRepair(inventory, data.TechProductCategory, data.CategoryItemId!);
inventoryChanges.MiscItems = [
{
ItemType: "/Lotus/Types/Items/MiscItems/InstantSalvageRepairItem",
ItemCount: -1
}
];
addMiscItems(inventory, inventoryChanges.MiscItems);
await inventory.save();
res.json({
inventoryChanges: inventoryChanges
});
} else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unhandled guildTech request`);
throw new Error(`unknown guildTech action: ${data.Action}`);
}
};
@ -388,70 +238,23 @@ type TGuildTechRequest =
| IGuildTechContributeRequest;
interface IGuildTechBasicRequest {
Action: "Start" | "Fabricate" | "Pause" | "Unpause" | "Cancel" | "Rush" | "InstantFinish";
Mode: "Guild" | "Personal";
Action: "Start" | "Fabricate" | "Pause" | "Unpause";
Mode: "Guild";
RecipeType: string;
TechProductCategory?: string;
CategoryItemId?: string;
}
interface IGuildTechBuyRequest extends Omit<IGuildTechBasicRequest, "Action"> {
interface IGuildTechBuyRequest {
Action: string;
Mode: "Guild";
RecipeType: string;
}
interface IGuildTechContributeRequest {
Action: "Contribute";
ResearchId: string;
ResearchId: "";
RecipeType: string;
RegularCredits: number;
MiscItems: IMiscItem[];
VaultCredits: number;
VaultMiscItems: IMiscItem[];
}
const getSalvageCategory = (
category: "CrewShipWeapons" | "CrewShipWeaponSkins"
): "CrewShipSalvagedWeapons" | "CrewShipSalvagedWeaponSkins" => {
return category == "CrewShipWeapons" ? "CrewShipSalvagedWeapons" : "CrewShipSalvagedWeaponSkins";
};
const claimSalvagedComponent = (inventory: TInventoryDatabaseDocument, itemId: string): IInventoryChanges => {
// delete personal tech project
const personalTechProjectIndex = inventory.PersonalTechProjects.findIndex(x => x.CategoryItemId?.equals(itemId));
const personalTechProject = inventory.PersonalTechProjects[personalTechProjectIndex];
inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1);
const category = personalTechProject.ProductCategory! as "CrewShipWeapons" | "CrewShipWeaponSkins";
return finishComponentRepair(inventory, category, itemId);
};
const finishComponentRepair = (
inventory: TInventoryDatabaseDocument,
category: "CrewShipWeapons" | "CrewShipWeaponSkins",
itemId: string
): IInventoryChanges => {
const salvageCategory = getSalvageCategory(category);
// find salved part & delete it
const salvageIndex = inventory[salvageCategory].findIndex(x => x._id.equals(itemId));
const salvageItem = inventory[salvageCategory][salvageIndex];
inventory[salvageCategory].splice(salvageIndex, 1);
// add final item
const inventoryChanges = {
...(category == "CrewShipWeaponSkins"
? addCrewShipWeaponSkin(inventory, salvageItem.ItemType, salvageItem.UpgradeFingerprint)
: addEquipment(inventory, category, salvageItem.ItemType, {
UpgradeFingerprint: salvageItem.UpgradeFingerprint
})),
...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false)
};
inventoryChanges.RemovedIdItems = [
{
ItemId: { $oid: itemId }
}
];
return inventoryChanges;
};

View File

@ -11,10 +11,8 @@ import {
import { getAccountForRequest, getAccountFromSuffixedName, getSuffixedName } from "@/src/services/loginService";
import { addItems, combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
import { logger } from "@/src/utils/logger";
import { ExportFlavour } from "warframe-public-export-plus";
import { ExportFlavour, ExportGear } from "warframe-public-export-plus";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { fromStoreItem, isStoreItem } from "@/src/services/itemDataService";
import { IOid } from "@/src/types/commonTypes";
export const inboxController: RequestHandler = async (req, res) => {
const { deleteId, lastMessage: latestClientMessageId, messageId } = req.query;
@ -29,15 +27,15 @@ export const inboxController: RequestHandler = async (req, res) => {
return;
}
await deleteMessageRead(parseOid(deleteId as string));
await deleteMessageRead(deleteId as string);
res.status(200).end();
} else if (messageId) {
const message = await getMessage(parseOid(messageId as string));
const message = await getMessage(messageId as string);
message.r = true;
await message.save();
const attachmentItems = message.attVisualOnly ? undefined : message.att;
const attachmentCountedItems = message.attVisualOnly ? undefined : message.countedAtt;
const attachmentItems = message.att;
const attachmentCountedItems = message.countedAtt;
if (!attachmentItems && !attachmentCountedItems && !message.gifts) {
res.status(200).end();
@ -50,8 +48,8 @@ export const inboxController: RequestHandler = async (req, res) => {
await addItems(
inventory,
attachmentItems.map(attItem => ({
ItemType: isStoreItem(attItem) ? fromStoreItem(attItem) : attItem,
ItemCount: 1
ItemType: attItem,
ItemCount: attItem in ExportGear ? (ExportGear[attItem].purchaseQuantity ?? 1) : 1
})),
inventoryChanges
);
@ -101,7 +99,7 @@ export const inboxController: RequestHandler = async (req, res) => {
await createNewEventMessages(req);
const messages = await Inbox.find({ ownerId: accountId }).sort({ date: 1 });
const latestClientMessage = messages.find(m => m._id.toString() === parseOid(latestClientMessageId as string));
const latestClientMessage = messages.find(m => m._id.toString() === latestClientMessageId);
if (!latestClientMessage) {
logger.debug(`this should only happen after DeleteAllRead `);
@ -124,11 +122,3 @@ export const inboxController: RequestHandler = async (req, res) => {
res.json({ Inbox: inbox });
}
};
// 33.6.0 has query arguments like lastMessage={"$oid":"68112baebf192e786d1502bb"} instead of lastMessage=68112baebf192e786d1502bb
const parseOid = (oid: string): string => {
if (oid[0] == "{") {
return (JSON.parse(oid) as IOid).$oid;
}
return oid;
};

View File

@ -14,16 +14,9 @@ import {
ExportVirtuals
} from "warframe-public-export-plus";
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService";
import {
addMiscItems,
allDailyAffiliationKeys,
cleanupInventory,
createLibraryDailyTask,
generateRewardSeed
} from "@/src/services/inventoryService";
import { addMiscItems, allDailyAffiliationKeys, createLibraryDailyTask } from "@/src/services/inventoryService";
import { logger } from "@/src/utils/logger";
import { catBreadHash } from "@/src/helpers/stringHelpers";
import { Types } from "mongoose";
export const inventoryController: RequestHandler = async (request, response) => {
const accountId = await getAccountIdForRequest(request);
@ -86,10 +79,8 @@ export const inventoryController: RequestHandler = async (request, response) =>
}
}
cleanupInventory(inventory);
inventory.NextRefill = new Date((Math.trunc(Date.now() / 86400000) + 1) * 86400000);
//await inventory.save();
await inventory.save();
}
if (
@ -98,27 +89,9 @@ export const inventoryController: RequestHandler = async (request, response) =>
new Date() >= inventory.InfestedFoundry.AbilityOverrideUnlockCooldown
) {
handleSubsumeCompletion(inventory);
//await inventory.save();
await inventory.save();
}
if (inventory.LastInventorySync) {
const lastSyncDuviriMood = Math.trunc(inventory.LastInventorySync.getTimestamp().getTime() / 7200000);
const currentDuviriMood = Math.trunc(Date.now() / 7200000);
if (lastSyncDuviriMood != currentDuviriMood) {
logger.debug(`refreshing duviri seed`);
if (!inventory.DuviriInfo) {
inventory.DuviriInfo = {
Seed: generateRewardSeed(),
NumCompletions: 0
};
} else {
inventory.DuviriInfo.Seed = generateRewardSeed();
}
}
}
inventory.LastInventorySync = new Types.ObjectId();
await inventory.save();
response.json(await getInventoryResponse(inventory, "xpBasedLevelCapDisabled" in request.query));
};
@ -176,7 +149,7 @@ export const getInventoryResponse = async (
inventoryResponse.ShipDecorations = [];
for (const [uniqueName, item] of Object.entries(ExportResources)) {
if (item.productCategory == "ShipDecorations") {
inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 999_999 });
inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 1 });
}
}
}
@ -229,8 +202,7 @@ export const getInventoryResponse = async (
if (config.universalPolarityEverywhere) {
const Polarity: IPolarity[] = [];
// 12 is needed for necramechs. 15 is needed for plexus/crewshipharness.
for (let i = 0; i != 15; ++i) {
for (let i = 0; i != 12; ++i) {
Polarity.push({
Slot: i,
Value: ArtifactPolarity.Any
@ -285,16 +257,12 @@ export const getInventoryResponse = async (
}
}
if (config.noDailyFocusLimit) {
inventoryResponse.DailyFocus = Math.max(999_999, 250000 + inventoryResponse.PlayerLevel * 5000);
}
if (inventoryResponse.InfestedFoundry) {
applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry);
}
// Omitting this field so opening the navigation resyncs the inventory which is more desirable for typical usage.
inventoryResponse.LastInventorySync = undefined;
//inventoryResponse.LastInventorySync = toOid(new Types.ObjectId());
// Set 2FA enabled so trading post can be used
inventoryResponse.HWIDProtectEnabled = true;

View File

@ -7,7 +7,6 @@ import { Account } from "@/src/models/loginModel";
import { createAccount, isCorrectPassword, isNameTaken } from "@/src/services/loginService";
import { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "@/src/types/loginTypes";
import { logger } from "@/src/utils/logger";
import { version_compare } from "@/src/services/worldStateService";
export const loginController: RequestHandler = async (request, response) => {
const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object
@ -22,11 +21,7 @@ export const loginController: RequestHandler = async (request, response) => {
const myAddress = request.host.indexOf("warframe.com") == -1 ? request.host : config.myAddress;
if (
!account &&
((config.autoCreateAccount && loginRequest.ClientType != "webui") ||
loginRequest.ClientType == "webui-register")
) {
if (!account && config.autoCreateAccount && loginRequest.ClientType != "webui") {
try {
const nameFromEmail = loginRequest.email.substring(0, loginRequest.email.indexOf("@"));
let name = nameFromEmail || loginRequest.email.substring(1) || "SpaceNinja";
@ -42,7 +37,7 @@ export const loginController: RequestHandler = async (request, response) => {
password: loginRequest.password,
DisplayName: name,
CountryCode: loginRequest.lang.toUpperCase(),
ClientType: loginRequest.ClientType == "webui-register" ? "webui" : loginRequest.ClientType,
ClientType: loginRequest.ClientType,
CrossPlatformAllowed: true,
ForceLogoutVersion: 0,
ConsentNeeded: false,
@ -64,11 +59,6 @@ export const loginController: RequestHandler = async (request, response) => {
return;
}
if (loginRequest.ClientType == "webui-register") {
response.status(400).json({ error: "account already exists" });
return;
}
if (!isCorrectPassword(loginRequest.password, account.password)) {
response.status(400).json({ error: "incorrect login data" });
return;
@ -95,7 +85,7 @@ export const loginController: RequestHandler = async (request, response) => {
};
const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, buildLabel: string): ILoginResponse => {
const resp: ILoginResponse = {
return {
id: account.id,
DisplayName: account.DisplayName,
CountryCode: account.CountryCode,
@ -109,14 +99,11 @@ const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, b
Nonce: account.Nonce,
Groups: [],
IRC: config.myIrcAddresses ?? [myAddress],
platformCDNs: [`https://${myAddress}/`],
HUB: `https://${myAddress}/api/`,
NRS: config.NRS,
DTLS: 99,
BuildLabel: buildLabel,
MatchmakingBuildId: buildConfig.matchmakingBuildId
};
if (version_compare(buildLabel, "2023.04.25.23.40") >= 0) {
resp.platformCDNs = [`https://${myAddress}/`];
}
return resp;
};

View File

@ -3,10 +3,9 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IMissionInventoryUpdateRequest } from "@/src/types/requestTypes";
import { addMissionInventoryUpdates, addMissionRewards } from "@/src/services/missionInventoryUpdateService";
import { generateRewardSeed, getInventory } from "@/src/services/inventoryService";
import { getInventory } from "@/src/services/inventoryService";
import { getInventoryResponse } from "./inventoryController";
import { logger } from "@/src/utils/logger";
import { IMissionInventoryUpdateResponse } from "@/src/types/missionTypes";
/*
**** INPUT ****
@ -54,16 +53,9 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
logger.debug("mission report:", missionReport);
const inventory = await getInventory(accountId);
const firstCompletion = missionReport.SortieId
? inventory.CompletedSorties.indexOf(missionReport.SortieId) == -1
: false;
const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport);
if (
missionReport.MissionStatus !== "GS_SUCCESS" &&
!(missionReport.RewardInfo?.jobId || missionReport.RewardInfo?.challengeMissionId)
) {
inventory.RewardSeed = generateRewardSeed();
if (missionReport.MissionStatus !== "GS_SUCCESS") {
await inventory.save();
const inventoryResponse = await getInventoryResponse(inventory, true);
res.json({
@ -73,16 +65,8 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
return;
}
const {
MissionRewards,
inventoryChanges,
credits,
AffiliationMods,
SyndicateXPItemReward,
ConquestCompletedMissionsCount
} = await addMissionRewards(inventory, missionReport, firstCompletion);
const { MissionRewards, inventoryChanges, credits } = await addMissionRewards(inventory, missionReport);
inventory.RewardSeed = generateRewardSeed();
await inventory.save();
const inventoryResponse = await getInventoryResponse(inventory, true);
@ -93,11 +77,8 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
MissionRewards,
...credits,
...inventoryUpdates,
//FusionPoints: inventoryChanges?.FusionPoints, // This in combination with InventoryJson or InventoryChanges seems to just double the number of endo shown, so unsure when this is needed.
SyndicateXPItemReward,
AffiliationMods,
ConquestCompletedMissionsCount
} satisfies IMissionInventoryUpdateResponse);
FusionPoints: inventoryChanges?.FusionPoints
});
};
/*

View File

@ -17,13 +17,12 @@ import { getDefaultUpgrades } from "@/src/services/itemDataService";
import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper";
import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { getRandomInt } from "@/src/services/rngService";
import { ExportSentinels, ExportWeapons, IDefaultUpgrade } from "warframe-public-export-plus";
import { ExportSentinels } from "warframe-public-export-plus";
import { Status } from "@/src/types/inventoryTypes/inventoryTypes";
interface IModularCraftRequest {
WeaponType: string;
Parts: string[];
isWebUi?: boolean;
}
export const modularWeaponCraftingController: RequestHandler = async (req, res) => {
@ -35,9 +34,9 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res)
const category = modularWeaponTypes[data.WeaponType];
const inventory = await getInventory(accountId);
let defaultUpgrades: IDefaultUpgrade[] | undefined;
const defaultUpgrades = getDefaultUpgrades(data.Parts);
const defaultOverwrites: Partial<IEquipmentDatabase> = {
ModularParts: data.Parts
Configs: applyDefaultUpgrades(inventory, defaultUpgrades)
};
const inventoryChanges: IInventoryChanges = {};
if (category == "KubrowPets") {
@ -130,63 +129,38 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res)
// Only save mutagen & antigen in the ModularParts.
defaultOverwrites.ModularParts = [data.Parts[1], data.Parts[2]];
const meta = ExportSentinels[data.WeaponType];
for (const specialItem of meta.exalted!) {
for (const specialItem of ExportSentinels[data.WeaponType].exalted!) {
addSpecialItem(inventory, specialItem, inventoryChanges);
}
defaultUpgrades = meta.defaultUpgrades;
} else {
defaultUpgrades = getDefaultUpgrades(data.Parts);
}
if (category == "MoaPets") {
const weapon = ExportSentinels[data.WeaponType].defaultWeapon;
if (weapon) {
const category = ExportWeapons[weapon].productCategory;
addEquipment(inventory, category, weapon, undefined, inventoryChanges);
combineInventoryChanges(
inventoryChanges,
occupySlot(inventory, productCategoryToInventoryBin(category)!, !!data.isWebUi)
);
}
}
defaultOverwrites.Configs = applyDefaultUpgrades(inventory, defaultUpgrades);
addEquipment(inventory, category, data.WeaponType, defaultOverwrites, inventoryChanges);
combineInventoryChanges(
inventoryChanges,
occupySlot(inventory, productCategoryToInventoryBin(category)!, !!data.isWebUi)
);
addEquipment(inventory, category, data.WeaponType, data.Parts, inventoryChanges, defaultOverwrites);
combineInventoryChanges(inventoryChanges, occupySlot(inventory, productCategoryToInventoryBin(category)!, false));
if (defaultUpgrades) {
inventoryChanges.RawUpgrades = defaultUpgrades.map(x => ({ ItemType: x.ItemType, ItemCount: 1 }));
}
// Remove credits & parts
const miscItemChanges = [];
let currencyChanges = {};
if (!data.isWebUi) {
for (const part of data.Parts) {
miscItemChanges.push({
ItemType: part,
ItemCount: -1
});
}
currencyChanges = updateCurrency(
inventory,
category == "Hoverboards" ||
category == "MoaPets" ||
category == "LongGuns" ||
category == "Pistols" ||
category == "KubrowPets"
? 5000
: 4000, // Definitely correct for Melee & OperatorAmps
false
);
addMiscItems(inventory, miscItemChanges);
for (const part of data.Parts) {
miscItemChanges.push({
ItemType: part,
ItemCount: -1
});
}
const currencyChanges = updateCurrency(
inventory,
category == "Hoverboards" ||
category == "MoaPets" ||
category == "LongGuns" ||
category == "Pistols" ||
category == "KubrowPets"
? 5000
: 4000, // Definitely correct for Melee & OperatorAmps
false
);
addMiscItems(inventory, miscItemChanges);
await inventory.save();
// Tell client what we did
res.json({
InventoryChanges: {

View File

@ -21,11 +21,7 @@ import { IInventoryChanges } from "@/src/types/purchaseTypes";
export const modularWeaponSaleController: RequestHandler = async (req, res) => {
const partTypeToParts: Record<string, string[]> = {};
for (const [uniqueName, data] of Object.entries(ExportWeapons)) {
if (
data.partType &&
data.premiumPrice &&
!data.excludeFromCodex // exclude pvp variants
) {
if (data.partType && data.premiumPrice) {
partTypeToParts[data.partType] ??= [];
partTypeToParts[data.partType].push(uniqueName);
}
@ -45,18 +41,24 @@ export const modularWeaponSaleController: RequestHandler = async (req, res) => {
const defaultUpgrades = getDefaultUpgrades(weaponInfo.ModularParts);
const configs = applyDefaultUpgrades(inventory, defaultUpgrades);
const inventoryChanges: IInventoryChanges = {
...addEquipment(inventory, category, weaponInfo.ItemType, {
Features: EquipmentFeatures.DOUBLE_CAPACITY | EquipmentFeatures.GILDED,
ItemName: payload.ItemName,
Configs: configs,
ModularParts: weaponInfo.ModularParts,
Polarity: [
{
Slot: payload.PolarizeSlot,
Value: payload.PolarizeValue
}
]
}),
...addEquipment(
inventory,
category,
weaponInfo.ItemType,
weaponInfo.ModularParts,
{},
{
Features: EquipmentFeatures.DOUBLE_CAPACITY | EquipmentFeatures.GILDED,
ItemName: payload.ItemName,
Configs: configs,
Polarity: [
{
Slot: payload.PolarizeSlot,
Value: payload.PolarizeValue
}
]
}
),
...occupySlot(inventory, productCategoryToInventoryBin(category)!, true),
...updateCurrency(inventory, weaponInfo.PremiumPrice, true)
};
@ -141,7 +143,7 @@ const getModularWeaponSale = (
getItemType: (parts: string[]) => string
): IModularWeaponSaleInfo => {
const rng = new CRng(day);
const parts = partTypes.map(partType => rng.randomElement(partTypeToParts[partType])!);
const parts = partTypes.map(partType => rng.randomElement(partTypeToParts[partType]));
let partsCost = 0;
for (const part of parts) {
partsCost += ExportWeapons[part].premiumPrice!;

View File

@ -1,31 +1,10 @@
import {
consumeModCharge,
encodeNemesisGuess,
getInfNodes,
getKnifeUpgrade,
getNemesisPasscode,
getNemesisPasscodeModTypes,
getWeaponsForManifest,
IKnifeResponse,
showdownNodes
} from "@/src/helpers/nemesisHelpers";
import { getInfNodes, getNemesisPasscode } from "@/src/helpers/nemesisHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
import { freeUpSlot, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { SRng } from "@/src/services/rngService";
import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import {
IInnateDamageFingerprint,
IInventoryClient,
INemesisClient,
InventorySlot,
IUpgradeClient,
IWeaponSkinClient,
LoadoutIndex,
TEquipmentKey
} from "@/src/types/inventoryTypes/inventoryTypes";
import { IInnateDamageFingerprint, InventorySlot, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
@ -70,7 +49,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
} else if ((req.query.mode as string) == "p") {
const inventory = await getInventory(accountId, "Nemesis");
const body = getJSONfromString<INemesisPrespawnCheckRequest>(String(req.body));
const passcode = getNemesisPasscode(inventory.Nemesis!);
const passcode = getNemesisPasscode(inventory.Nemesis!.fp, inventory.Nemesis!.Faction);
let guessResult = 0;
if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
for (let i = 0; i != 3; ++i) {
@ -87,83 +66,6 @@ export const nemesisController: RequestHandler = async (req, res) => {
}
}
res.json({ GuessResult: guessResult });
} else if (req.query.mode == "r") {
const inventory = await getInventory(
accountId,
"Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades"
);
const body = getJSONfromString<INemesisRequiemRequest>(String(req.body));
if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
const guess: number[] = [body.guess & 0xf, (body.guess >> 4) & 0xf, (body.guess >> 8) & 0xf];
const passcode = getNemesisPasscode(inventory.Nemesis!)[0];
// Add to GuessHistory
const result1 = passcode == guess[0] ? 0 : 1;
const result2 = passcode == guess[1] ? 0 : 1;
const result3 = passcode == guess[2] ? 0 : 1;
inventory.Nemesis!.GuessHistory.push(
encodeNemesisGuess(guess[0], result1, guess[1], result2, guess[2], result3)
);
// Increase antivirus if correct antivirus mod is installed
const response: IKnifeResponse = {};
if (result1 == 0 || result2 == 0 || result3 == 0) {
let antivirusGain = 5;
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid);
const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0;
const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!;
for (const upgrade of body.knife!.AttachedUpgrades) {
switch (upgrade.ItemType) {
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod":
antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod":
antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod": // Instant Secure
antivirusGain += 15;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod": // Immuno Shield
antivirusGain += 15;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod":
antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
}
}
inventory.Nemesis!.HenchmenKilled += antivirusGain;
}
if (inventory.Nemesis!.HenchmenKilled >= 100) {
inventory.Nemesis!.HenchmenKilled = 100;
}
inventory.Nemesis!.InfNodes = getInfNodes("FC_INFESTATION", 0);
await inventory.save();
res.json(response);
} else {
const passcode = getNemesisPasscode(inventory.Nemesis!);
if (passcode[body.position] != body.guess) {
res.end();
} else {
inventory.Nemesis!.Rank += 1;
inventory.Nemesis!.InfNodes = getInfNodes(inventory.Nemesis!.Faction, inventory.Nemesis!.Rank);
await inventory.save();
res.json({ RankIncrease: 1 });
}
}
} else if ((req.query.mode as string) == "rs") {
// report spawn; POST but no application data in body
const inventory = await getInventory(accountId, "Nemesis");
inventory.Nemesis!.LastEnc = inventory.Nemesis!.MissionCount;
await inventory.save();
res.json({ LastEnc: inventory.Nemesis!.LastEnc });
} else if ((req.query.mode as string) == "s") {
const inventory = await getInventory(accountId, "Nemesis");
const body = getJSONfromString<INemesisStartRequest>(String(req.body));
@ -171,7 +73,18 @@ export const nemesisController: RequestHandler = async (req, res) => {
let weaponIdx = -1;
if (body.target.Faction != "FC_INFESTATION") {
const weapons = getWeaponsForManifest(body.target.manifest);
let weapons: readonly string[];
if (body.target.manifest == "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix") {
weapons = kuvaLichVersionSixWeapons;
} else if (
body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour" ||
body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree"
) {
weapons = corpusVersionThreeWeapons;
} else {
throw new Error(`unknown nemesis manifest: ${body.target.manifest}`);
}
const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1);
weaponIdx = initialWeaponIdx;
do {
@ -213,38 +126,6 @@ export const nemesisController: RequestHandler = async (req, res) => {
res.json({
target: inventory.toJSON().Nemesis
});
} else if ((req.query.mode as string) == "w") {
const inventory = await getInventory(
accountId,
"Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades"
);
//const body = getJSONfromString<INemesisWeakenRequest>(String(req.body));
inventory.Nemesis!.InfNodes = [
{
Node: showdownNodes[inventory.Nemesis!.Faction],
Influence: 1
}
];
inventory.Nemesis!.Weakened = true;
const response: IKnifeResponse & { target: INemesisClient } = {
target: inventory.toJSON<IInventoryClient>().Nemesis!
};
// Consume charge of the correct requiem mod(s)
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid);
const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0;
const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!;
const modTypes = getNemesisPasscodeModTypes(inventory.Nemesis!);
for (const modType of modTypes) {
const upgrade = getKnifeUpgrade(inventory, dataknifeUpgrades, modType);
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
}
await inventory.save();
res.json(response);
} else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unknown nemesis mode: ${String(req.query.mode)}`);
@ -292,23 +173,38 @@ interface INemesisPrespawnCheckRequest {
potency?: number[];
}
interface INemesisRequiemRequest {
guess: number; // grn/crp: 4 bits | coda: 3x 4 bits
position: number; // grn/crp: 0-2 | coda: 0
// knife field provided for coda only
knife?: IKnife;
}
const kuvaLichVersionSixWeapons = [
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak",
"/Lotus/Weapons/Grineer/Melee/GrnKuvaLichScythe/GrnKuvaLichScytheWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Kohm/KuvaKohm",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Ogris/KuvaOgris",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Quartakk/KuvaQuartakk",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Tonkor/KuvaTonkor",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Brakk/KuvaBrakk",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Kraken/KuvaKraken",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Seer/KuvaSeer",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Stubba/KuvaStubba",
"/Lotus/Weapons/Grineer/HeavyWeapons/GrnHeavyGrenadeLauncher",
"/Lotus/Weapons/Grineer/LongGuns/GrnKuvaLichRifle/GrnKuvaLichRifleWeapon",
"/Lotus/Weapons/Grineer/Bows/GrnBow/GrnBowWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hind/KuvaHind",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Nukor/KuvaNukor",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hek/KuvaHekWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Zarr/KuvaZarr",
"/Lotus/Weapons/Grineer/KuvaLich/HeavyWeapons/Grattler/KuvaGrattler",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Sobek/KuvaSobek"
];
// interface INemesisWeakenRequest {
// target: INemesisClient;
// knife: IKnife;
// }
interface IKnife {
Item: IEquipmentClient;
Skins: IWeaponSkinClient[];
ModSlot: number;
CustSlot: number;
AttachedUpgrades: IUpgradeClient[];
HiddenWhenHolstered: boolean;
}
const corpusVersionThreeWeapons = [
"/Lotus/Weapons/Corpus/LongGuns/CrpBriefcaseLauncher/CrpBriefcaseLauncher",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEArcaPlasmor/CrpBEArcaPlasmor",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEFluxRifle/CrpBEFluxRifle",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBETetra/CrpBETetra",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBECycron/CrpBECycron",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEDetron/CrpBEDetron",
"/Lotus/Weapons/Corpus/Pistols/CrpIgniterPistol/CrpIgniterPistol",
"/Lotus/Weapons/Corpus/Pistols/CrpBriefcaseAkimbo/CrpBriefcaseAkimboPistol",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEPlinx/CrpBEPlinxWeapon",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEGlaxion/CrpBEGlaxion"
];

View File

@ -13,7 +13,6 @@ import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
import { Types } from "mongoose";
import { ExportDojoRecipes, ExportResources } from "warframe-public-export-plus";
import { config } from "@/src/services/configService";
export const placeDecoInComponentController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -37,7 +36,6 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) =
const deco = component.Decos.find(x => x._id.equals(request.MoveId))!;
deco.Pos = request.Pos;
deco.Rot = request.Rot;
deco.Scale = request.Scale;
} else {
const deco =
component.Decos[
@ -46,7 +44,6 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) =
Type: request.Type,
Pos: request.Pos,
Rot: request.Rot,
Scale: request.Scale,
Name: request.Name,
Sockets: request.Sockets
}) - 1
@ -65,41 +62,36 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) =
guild.VaultShipDecorations!.find(x => x.ItemType == itemType)!.ItemCount -= 1;
}
}
if (deco.Type != "/Lotus/Objects/Tenno/Props/TnoPaintBotDojoDeco") {
if (!meta || (meta.price == 0 && meta.ingredients.length == 0) || config.noDojoDecoBuildStage) {
deco.CompletionTime = new Date();
if (meta) {
processDojoBuildMaterialsGathered(guild, meta);
if (!meta || (meta.price == 0 && meta.ingredients.length == 0)) {
deco.CompletionTime = new Date();
} else if (guild.AutoContributeFromVault && guild.VaultRegularCredits && guild.VaultMiscItems) {
if (guild.VaultRegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) {
let enoughMiscItems = true;
for (const ingredient of meta.ingredients) {
if (
getVaultMiscItemCount(guild, ingredient.ItemType) <
scaleRequiredCount(guild.Tier, ingredient.ItemCount)
) {
enoughMiscItems = false;
break;
}
}
} else if (guild.AutoContributeFromVault && guild.VaultRegularCredits && guild.VaultMiscItems) {
if (guild.VaultRegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) {
let enoughMiscItems = true;
if (enoughMiscItems) {
guild.VaultRegularCredits -= meta.price;
deco.RegularCredits = meta.price;
deco.MiscItems = [];
for (const ingredient of meta.ingredients) {
if (
getVaultMiscItemCount(guild, ingredient.ItemType) <
scaleRequiredCount(guild.Tier, ingredient.ItemCount)
) {
enoughMiscItems = false;
break;
}
guild.VaultMiscItems.find(x => x.ItemType == ingredient.ItemType)!.ItemCount -=
scaleRequiredCount(guild.Tier, ingredient.ItemCount);
deco.MiscItems.push({
ItemType: ingredient.ItemType,
ItemCount: scaleRequiredCount(guild.Tier, ingredient.ItemCount)
});
}
if (enoughMiscItems) {
guild.VaultRegularCredits -= scaleRequiredCount(guild.Tier, meta.price);
deco.RegularCredits = scaleRequiredCount(guild.Tier, meta.price);
deco.MiscItems = [];
for (const ingredient of meta.ingredients) {
guild.VaultMiscItems.find(x => x.ItemType == ingredient.ItemType)!.ItemCount -=
scaleRequiredCount(guild.Tier, ingredient.ItemCount);
deco.MiscItems.push({
ItemType: ingredient.ItemType,
ItemCount: scaleRequiredCount(guild.Tier, ingredient.ItemCount)
});
}
deco.CompletionTime = new Date(Date.now() + meta.time * 1000);
processDojoBuildMaterialsGathered(guild, meta);
}
deco.CompletionTime = new Date(Date.now() + meta.time * 1000);
processDojoBuildMaterialsGathered(guild, meta);
}
}
}
@ -115,9 +107,9 @@ interface IPlaceDecoInComponentRequest {
Type: string;
Pos: number[];
Rot: number[];
Scale?: number;
Name?: string;
Sockets?: number;
Scale?: number; // only provided alongside MoveId and seems to always be 1
MoveId?: string;
ShipDeco?: boolean;
VaultDeco?: boolean;

View File

@ -1,9 +0,0 @@
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const playedParkourTutorialController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
await Inventory.updateOne({ accountOwnerId: accountId }, { PlayedParkourTutorial: true });
res.end();
};

View File

@ -8,11 +8,7 @@ export const releasePetController: RequestHandler = async (req, res) => {
const inventory = await getInventory(accountId, "RegularCredits KubrowPets");
const payload = getJSONfromString<IReleasePetRequest>(String(req.body));
const inventoryChanges = updateCurrency(
inventory,
payload.recipeName == "/Lotus/Types/Game/KubrowPet/ReleasePetRecipe" ? 25000 : 0,
false
);
const inventoryChanges = updateCurrency(inventory, 25000, false);
inventoryChanges.RemovedIdItems = [{ ItemId: { $oid: payload.petId } }];
inventory.KubrowPets.pull({ _id: payload.petId });
@ -22,6 +18,6 @@ export const releasePetController: RequestHandler = async (req, res) => {
};
interface IReleasePetRequest {
recipeName: "/Lotus/Types/Game/KubrowPet/ReleasePetRecipe" | "webui";
recipeName: "/Lotus/Types/Game/KubrowPet/ReleasePetRecipe";
petId: string;
}

View File

@ -1,21 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Account, Ignore } from "@/src/models/loginModel";
import { getAccountForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const removeIgnoredUserController: RequestHandler = async (req, res) => {
const accountId = await getAccountForRequest(req);
const data = getJSONfromString<IRemoveIgnoredUserRequest>(String(req.body));
const ignoreeAccount = await Account.findOne(
{ DisplayName: data.playerName.substring(0, data.playerName.length - 1) },
"_id"
);
if (ignoreeAccount) {
await Ignore.deleteOne({ ignorer: accountId, ignoree: ignoreeAccount._id });
}
res.end();
};
interface IRemoveIgnoredUserRequest {
playerName: string;
}

View File

@ -1,41 +1,53 @@
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { config } from "@/src/services/configService";
import { addEmailItem, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { ICompletedDialogue, IDialogueDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { ICompletedDialogue } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
export const saveDialogueController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const request = JSON.parse(String(req.body)) as SaveDialogueRequest;
if ("YearIteration" in request) {
const inventory = await getInventory(accountId, "DialogueHistory");
inventory.DialogueHistory ??= {};
inventory.DialogueHistory.YearIteration = request.YearIteration;
const inventory = await getInventory(accountId);
if (inventory.DialogueHistory) {
inventory.DialogueHistory.YearIteration = request.YearIteration;
} else {
inventory.DialogueHistory = { YearIteration: request.YearIteration };
}
await inventory.save();
res.end();
} else {
const inventory = await getInventory(accountId);
const inventoryChanges: IInventoryChanges = {};
const tomorrowAt0Utc = config.noKimCooldowns
? Date.now()
: (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000;
inventory.DialogueHistory ??= {};
if (!inventory.DialogueHistory) {
throw new Error("bad inventory state");
}
if (request.QueuedDialogues.length != 0 || request.OtherDialogueInfos.length != 0) {
logger.error(`saveDialogue request not fully handled: ${String(req.body)}`);
}
inventory.DialogueHistory.Dialogues ??= [];
const dialogue = getDialogue(inventory, request.DialogueName);
let dialogue = inventory.DialogueHistory.Dialogues.find(x => x.DialogueName == request.DialogueName);
if (!dialogue) {
dialogue =
inventory.DialogueHistory.Dialogues[
inventory.DialogueHistory.Dialogues.push({
Rank: 0,
Chemistry: 0,
AvailableDate: new Date(0),
AvailableGiftDate: new Date(0),
RankUpExpiry: new Date(0),
BountyChemExpiry: new Date(0),
Gifts: [],
Booleans: [],
Completed: [],
DialogueName: request.DialogueName
}) - 1
];
}
dialogue.Rank = request.Rank;
dialogue.Chemistry = request.Chemistry;
dialogue.QueuedDialogues = request.QueuedDialogues;
//dialogue.QueuedDialogues = request.QueuedDialogues;
for (const bool of request.Booleans) {
dialogue.Booleans.push(bool);
if (bool == "LizzieShawzin") {
await addEmailItem(
inventory,
"/Lotus/Types/Items/EmailItems/LizzieShawzinSkinEmailItem",
inventoryChanges
);
}
}
for (const bool of request.ResetBooleans) {
const index = dialogue.Booleans.findIndex(x => x == bool);
@ -43,38 +55,14 @@ export const saveDialogueController: RequestHandler = async (req, res) => {
dialogue.Booleans.splice(index, 1);
}
}
for (const info of request.OtherDialogueInfos) {
const otherDialogue = getDialogue(inventory, info.Dialogue);
if (info.Tag != "") {
otherDialogue.QueuedDialogues.push(info.Tag);
}
otherDialogue.Chemistry += info.Value; // unsure
}
if (request.Data) {
dialogue.Completed.push(request.Data);
dialogue.AvailableDate = new Date(tomorrowAt0Utc);
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
AvailableDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } }
});
} else if (request.Gift) {
const inventoryChanges = updateCurrency(inventory, request.Gift.Cost, false);
const gift = dialogue.Gifts.find(x => x.Item == request.Gift!.Item);
if (gift) {
gift.GiftedQuantity += 1;
} else {
dialogue.Gifts.push({ Item: request.Gift.Item, GiftedQuantity: 1 });
}
dialogue.AvailableGiftDate = new Date(tomorrowAt0Utc);
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
AvailableGiftDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } }
});
} else {
res.end();
}
dialogue.Completed.push(request.Data);
const tomorrowAt0Utc = (Math.trunc(Date.now() / (86400 * 1000)) + 1) * 86400 * 1000;
dialogue.AvailableDate = new Date(tomorrowAt0Utc);
await inventory.save();
res.json({
InventoryChanges: [],
AvailableDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } }
});
}
};
@ -89,17 +77,11 @@ interface SaveCompletedDialogueRequest {
Rank: number;
Chemistry: number;
CompletionType: number;
QueuedDialogues: string[];
Gift?: {
Item: string;
GainedChemistry: number;
Cost: number;
GiftedQuantity: number;
};
QueuedDialogues: string[]; // unsure
Booleans: string[];
ResetBooleans: string[];
Data?: ICompletedDialogue;
OtherDialogueInfos: IOtherDialogueInfo[];
Data: ICompletedDialogue;
OtherDialogueInfos: IOtherDialogueInfo[]; // unsure
}
interface IOtherDialogueInfo {
@ -107,26 +89,3 @@ interface IOtherDialogueInfo {
Tag: string;
Value: number;
}
const getDialogue = (inventory: TInventoryDatabaseDocument, dialogueName: string): IDialogueDatabase => {
let dialogue = inventory.DialogueHistory!.Dialogues!.find(x => x.DialogueName == dialogueName);
if (!dialogue) {
dialogue =
inventory.DialogueHistory!.Dialogues![
inventory.DialogueHistory!.Dialogues!.push({
Rank: 0,
Chemistry: 0,
AvailableDate: new Date(0),
AvailableGiftDate: new Date(0),
RankUpExpiry: new Date(0),
BountyChemExpiry: new Date(0),
QueuedDialogues: [],
Gifts: [],
Booleans: [],
Completed: [],
DialogueName: dialogueName
}) - 1
];
}
return dialogue;
};

View File

@ -0,0 +1,32 @@
import { RequestHandler } from "express";
import { ISaveLoadoutRequest } from "@/src/types/saveLoadoutTypes";
import { handleInventoryItemConfigChange } from "@/src/services/saveLoadoutService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { logger } from "@/src/utils/logger";
export const saveLoadoutController: RequestHandler = async (req, res) => {
//validate here
const accountId = await getAccountIdForRequest(req);
try {
const body: ISaveLoadoutRequest = JSON.parse(req.body as string) as ISaveLoadoutRequest;
// console.log(util.inspect(body, { showHidden: false, depth: null, colors: true }));
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { UpgradeVer, ...equipmentChanges } = body;
const newLoadoutId = await handleInventoryItemConfigChange(equipmentChanges, accountId);
//send back new loadout id, if new loadout was added
if (newLoadoutId) {
res.send(newLoadoutId);
}
res.status(200).end();
} catch (error: unknown) {
if (error instanceof Error) {
logger.error(`error in saveLoadoutController: ${error.message}`);
res.status(400).json({ error: error.message });
} else {
res.status(400).json({ error: "unknown error" });
}
}
};

View File

@ -1,21 +0,0 @@
import { RequestHandler } from "express";
import { ISaveLoadoutRequest } from "@/src/types/saveLoadoutTypes";
import { handleInventoryItemConfigChange } from "@/src/services/saveLoadoutService";
import { getAccountIdForRequest } from "@/src/services/loginService";
export const saveLoadoutController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const body: ISaveLoadoutRequest = JSON.parse(req.body as string) as ISaveLoadoutRequest;
// console.log(util.inspect(body, { showHidden: false, depth: null, colors: true }));
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { UpgradeVer, ...equipmentChanges } = body;
const newLoadoutId = await handleInventoryItemConfigChange(equipmentChanges, accountId);
//send back new loadout id, if new loadout was added
if (newLoadoutId) {
res.send(newLoadoutId);
}
res.end();
};

View File

@ -6,20 +6,14 @@ import {
addRecipes,
addMiscItems,
addConsumables,
freeUpSlot,
combineInventoryChanges,
addCrewShipRawSalvage,
addFusionPoints
freeUpSlot
} from "@/src/services/inventoryService";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportDojoRecipes } from "warframe-public-export-plus";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
export const sellController: RequestHandler = async (req, res) => {
const payload = JSON.parse(String(req.body)) as ISellRequest;
const accountId = await getAccountIdForRequest(req);
const requiredFields = new Set<keyof TInventoryDatabaseDocument>();
const requiredFields = new Set();
if (payload.SellCurrency == "SC_RegularCredits") {
requiredFields.add("RegularCredits");
} else if (payload.SellCurrency == "SC_FusionPoints") {
@ -28,7 +22,7 @@ export const sellController: RequestHandler = async (req, res) => {
requiredFields.add("MiscItems");
}
for (const key of Object.keys(payload.Items)) {
requiredFields.add(key as keyof TInventoryDatabaseDocument);
requiredFields.add(key);
}
if (requiredFields.has("Upgrades")) {
requiredFields.add("RawUpgrades");
@ -45,7 +39,7 @@ export const sellController: RequestHandler = async (req, res) => {
if (payload.Items.SpaceGuns || payload.Items.SpaceMelee) {
requiredFields.add(InventorySlot.SPACEWEAPONS);
}
if (payload.Items.Sentinels || payload.Items.SentinelWeapons || payload.Items.MoaPets) {
if (payload.Items.Sentinels || payload.Items.SentinelWeapons) {
requiredFields.add(InventorySlot.SENTINELS);
}
if (payload.Items.OperatorAmps) {
@ -54,23 +48,13 @@ export const sellController: RequestHandler = async (req, res) => {
if (payload.Items.Hoverboards) {
requiredFields.add(InventorySlot.SPACESUITS);
}
if (payload.Items.CrewShipWeapons || payload.Items.CrewShipWeaponSkins) {
requiredFields.add(InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
requiredFields.add("CrewShipRawSalvage");
if (payload.Items.CrewShipWeapons) {
requiredFields.add("CrewShipSalvagedWeapons");
}
if (payload.Items.CrewShipWeaponSkins) {
requiredFields.add("CrewShipSalvagedWeaponSkins");
}
}
const inventory = await getInventory(accountId, Array.from(requiredFields).join(" "));
// Give currency
if (payload.SellCurrency == "SC_RegularCredits") {
inventory.RegularCredits += payload.SellPrice;
} else if (payload.SellCurrency == "SC_FusionPoints") {
addFusionPoints(inventory, payload.SellPrice);
inventory.FusionPoints += payload.SellPrice;
} else if (payload.SellCurrency == "SC_PrimeBucks") {
addMiscItems(inventory, [
{
@ -85,14 +69,10 @@ export const sellController: RequestHandler = async (req, res) => {
ItemCount: payload.SellPrice
}
]);
} else if (payload.SellCurrency == "SC_Resources") {
// Will add appropriate MiscItems from CrewShipWeapons or CrewShipWeaponSkins
} else {
throw new Error("Unknown SellCurrency: " + payload.SellCurrency);
}
const inventoryChanges: IInventoryChanges = {};
// Remove item(s)
if (payload.Items.Suits) {
payload.Items.Suits.forEach(sellItem => {
@ -148,12 +128,6 @@ export const sellController: RequestHandler = async (req, res) => {
freeUpSlot(inventory, InventorySlot.SENTINELS);
});
}
if (payload.Items.MoaPets) {
payload.Items.MoaPets.forEach(sellItem => {
inventory.MoaPets.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.SENTINELS);
});
}
if (payload.Items.OperatorAmps) {
payload.Items.OperatorAmps.forEach(sellItem => {
inventory.OperatorAmps.pull({ _id: sellItem.String });
@ -171,56 +145,6 @@ export const sellController: RequestHandler = async (req, res) => {
inventory.Drones.pull({ _id: sellItem.String });
});
}
if (payload.Items.CrewShipWeapons) {
payload.Items.CrewShipWeapons.forEach(sellItem => {
if (sellItem.String[0] == "/") {
addCrewShipRawSalvage(inventory, [
{
ItemType: sellItem.String,
ItemCount: sellItem.Count * -1
}
]);
} else {
const index = inventory.CrewShipWeapons.findIndex(x => x._id.equals(sellItem.String));
if (index != -1) {
if (payload.SellCurrency == "SC_Resources") {
refundPartialBuildCosts(inventory, inventory.CrewShipWeapons[index].ItemType, inventoryChanges);
}
inventory.CrewShipWeapons.splice(index, 1);
freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
} else {
inventory.CrewShipSalvagedWeapons.pull({ _id: sellItem.String });
}
}
});
}
if (payload.Items.CrewShipWeaponSkins) {
payload.Items.CrewShipWeaponSkins.forEach(sellItem => {
if (sellItem.String[0] == "/") {
addCrewShipRawSalvage(inventory, [
{
ItemType: sellItem.String,
ItemCount: sellItem.Count * -1
}
]);
} else {
const index = inventory.CrewShipWeaponSkins.findIndex(x => x._id.equals(sellItem.String));
if (index != -1) {
if (payload.SellCurrency == "SC_Resources") {
refundPartialBuildCosts(
inventory,
inventory.CrewShipWeaponSkins[index].ItemType,
inventoryChanges
);
}
inventory.CrewShipWeaponSkins.splice(index, 1);
freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
} else {
inventory.CrewShipSalvagedWeaponSkins.pull({ _id: sellItem.String });
}
}
});
}
if (payload.Items.Consumables) {
const consumablesChanges = [];
for (const sellItem of payload.Items.Consumables) {
@ -267,9 +191,7 @@ export const sellController: RequestHandler = async (req, res) => {
}
await inventory.save();
res.json({
inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges"
});
res.json({});
};
interface ISellRequest {
@ -287,12 +209,9 @@ interface ISellRequest {
SpaceMelee?: ISellItem[];
Sentinels?: ISellItem[];
SentinelWeapons?: ISellItem[];
MoaPets?: ISellItem[];
OperatorAmps?: ISellItem[];
Hoverboards?: ISellItem[];
Drones?: ISellItem[];
CrewShipWeapons?: ISellItem[];
CrewShipWeaponSkins?: ISellItem[];
};
SellPrice: number;
SellCurrency:
@ -309,33 +228,3 @@ interface ISellItem {
String: string; // oid or uniqueName
Count: number;
}
const refundPartialBuildCosts = (
inventory: TInventoryDatabaseDocument,
itemType: string,
inventoryChanges: IInventoryChanges
): void => {
// House versions
const research = Object.values(ExportDojoRecipes.research).find(x => x.resultType == itemType);
if (research) {
const miscItemChanges = research.ingredients.map(x => ({
ItemType: x.ItemType,
ItemCount: Math.trunc(x.ItemCount * 0.8)
}));
addMiscItems(inventory, miscItemChanges);
combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges });
return;
}
// Sigma versions
const recipe = Object.values(ExportDojoRecipes.fabrications).find(x => x.resultType == itemType);
if (recipe) {
const miscItemChanges = recipe.ingredients.map(x => ({
ItemType: x.ItemType,
ItemCount: Math.trunc(x.ItemCount * 0.8)
}));
addMiscItems(inventory, miscItemChanges);
combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges });
return;
}
};

View File

@ -1,31 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { createMessage } from "@/src/services/inboxService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const sendMsgToInBoxController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const data = getJSONfromString<ISendMsgToInBoxRequest>(String(req.body));
await createMessage(accountId, [
{
sub: data.title,
msg: data.message,
sndr: data.sender ?? "/Lotus/Language/Bosses/Ordis",
icon: data.senderIcon,
highPriority: data.highPriority,
transmission: data.transmission,
att: data.attachments
}
]);
res.end();
};
interface ISendMsgToInBoxRequest {
title: string;
message: string;
sender?: string;
senderIcon?: string;
highPriority?: boolean;
transmission?: string;
attachments?: string[];
}

View File

@ -1,34 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const setDojoComponentColorsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId LevelKeys");
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Decorator))) {
res.json({ DojoRequestStatus: -1 });
return;
}
const data = getJSONfromString<ISetDojoComponentColorsRequest>(String(req.body));
const component = guild.DojoComponents.id(data.ComponentId)!;
//const deco = component.Decos!.find(x => x._id.equals(data.DecoId))!;
//deco.Pending = true;
//component.PaintBot = new Types.ObjectId(data.DecoId);
if ("lights" in req.query) {
component.PendingLights = data.Colours;
} else {
component.PendingColors = data.Colours;
}
await guild.save();
res.json(await getDojoClient(guild, 0, component._id));
};
interface ISetDojoComponentColorsRequest {
ComponentId: string;
DecoId: string;
Colours: number[];
}

View File

@ -1,25 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const setDojoComponentSettingsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId LevelKeys");
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Decorator))) {
res.json({ DojoRequestStatus: -1 });
return;
}
const component = guild.DojoComponents.id(req.query.componentId)!;
const data = getJSONfromString<ISetDojoComponentSettingsRequest>(String(req.body));
component.Settings = data.Settings;
await guild.save();
res.json(await getDojoClient(guild, 0, component._id));
};
interface ISetDojoComponentSettingsRequest {
Settings: string;
}

View File

@ -1,21 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IHubNpcCustomization } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
export const setHubNpcCustomizationsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "HubNpcCustomizations");
const upload = getJSONfromString<IHubNpcCustomization>(String(req.body));
inventory.HubNpcCustomizations ??= [];
const cust = inventory.HubNpcCustomizations.find(x => x.Tag == upload.Tag);
if (cust) {
cust.Colors = upload.Colors;
cust.Pattern = upload.Pattern;
} else {
inventory.HubNpcCustomizations.push(upload);
}
await inventory.save();
res.end();
};

View File

@ -1,5 +1,5 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IPictureFrameInfo, ISetPlacedDecoInfoRequest } from "@/src/types/shipTypes";
import { ISetPlacedDecoInfoRequest } from "@/src/types/shipTypes";
import { RequestHandler } from "express";
import { handleSetPlacedDecoInfo } from "@/src/services/shipCustomizationsService";
@ -7,17 +7,5 @@ export const setPlacedDecoInfoController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const payload = JSON.parse(req.body as string) as ISetPlacedDecoInfoRequest;
await handleSetPlacedDecoInfo(accountId, payload);
res.json({
DecoId: payload.DecoId,
IsPicture: true,
PictureFrameInfo: payload.PictureFrameInfo,
BootLocation: payload.BootLocation
} satisfies ISetPlacedDecoInfoResponse);
res.end();
};
interface ISetPlacedDecoInfoResponse {
DecoId: string;
IsPicture: boolean;
PictureFrameInfo?: IPictureFrameInfo;
BootLocation?: string;
}

View File

@ -3,40 +3,29 @@ import { RequestHandler } from "express";
import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { IOid } from "@/src/types/commonTypes";
import { Types } from "mongoose";
import { IFavouriteLoadoutDatabase, TBootLocation } from "@/src/types/shipTypes";
export const setShipFavouriteLoadoutController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const personalRooms = await getPersonalRooms(accountId);
const body = JSON.parse(String(req.body)) as ISetShipFavouriteLoadoutRequest;
if (body.BootLocation == "LISET") {
personalRooms.Ship.FavouriteLoadoutId = new Types.ObjectId(body.FavouriteLoadoutId.$oid);
} else if (body.BootLocation == "APARTMENT") {
updateTaggedDisplay(personalRooms.Apartment.FavouriteLoadouts, body);
} else if (body.BootLocation == "SHOP") {
updateTaggedDisplay(personalRooms.TailorShop.FavouriteLoadouts, body);
} else {
console.log(body);
if (body.BootLocation != "SHOP") {
throw new Error(`unexpected BootLocation: ${body.BootLocation}`);
}
await personalRooms.save();
res.json(body);
};
interface ISetShipFavouriteLoadoutRequest {
BootLocation: TBootLocation;
FavouriteLoadoutId: IOid;
TagName?: string;
}
const updateTaggedDisplay = (arr: IFavouriteLoadoutDatabase[], body: ISetShipFavouriteLoadoutRequest): void => {
const display = arr.find(x => x.Tag == body.TagName!);
const display = personalRooms.TailorShop.FavouriteLoadouts.find(x => x.Tag == body.TagName);
if (display) {
display.LoadoutId = new Types.ObjectId(body.FavouriteLoadoutId.$oid);
} else {
arr.push({
Tag: body.TagName!,
personalRooms.TailorShop.FavouriteLoadouts.push({
Tag: body.TagName,
LoadoutId: new Types.ObjectId(body.FavouriteLoadoutId.$oid)
});
}
await personalRooms.save();
res.json({});
};
interface ISetShipFavouriteLoadoutRequest {
BootLocation: string;
FavouriteLoadoutId: IOid;
TagName: string;
}

View File

@ -1,48 +0,0 @@
import { addMiscItems, combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
export const setShipVignetteController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "MiscItems");
const personalRooms = await getPersonalRooms(accountId);
const body = JSON.parse(String(req.body)) as ISetShipVignetteRequest;
personalRooms.Ship.Wallpaper = body.Wallpaper;
personalRooms.Ship.Vignette = body.Vignette;
personalRooms.Ship.VignetteFish ??= [];
const inventoryChanges: IInventoryChanges = {};
for (let i = 0; i != body.Fish.length; ++i) {
if (body.Fish[i] && !personalRooms.Ship.VignetteFish[i]) {
logger.debug(`moving ${body.Fish[i]} from inventory to vignette slot ${i}`);
const miscItemsDelta = [{ ItemType: body.Fish[i], ItemCount: -1 }];
addMiscItems(inventory, miscItemsDelta);
combineInventoryChanges(inventoryChanges, { MiscItems: miscItemsDelta });
} else if (personalRooms.Ship.VignetteFish[i] && !body.Fish[i]) {
logger.debug(`moving ${personalRooms.Ship.VignetteFish[i]} from vignette slot ${i} to inventory`);
const miscItemsDelta = [{ ItemType: personalRooms.Ship.VignetteFish[i], ItemCount: +1 }];
addMiscItems(inventory, miscItemsDelta);
combineInventoryChanges(inventoryChanges, { MiscItems: miscItemsDelta });
}
}
personalRooms.Ship.VignetteFish = body.Fish;
if (body.VignetteDecos.length) {
logger.error(`setShipVignette request not fully handled:`, body);
}
await Promise.all([inventory.save(), personalRooms.save()]);
res.json({
Wallpaper: body.Wallpaper,
Vignette: body.Vignette,
VignetteFish: body.Fish,
InventoryChanges: inventoryChanges
});
};
interface ISetShipVignetteRequest {
Wallpaper: string;
Vignette: string;
Fish: string[];
VignetteDecos: unknown[];
}

View File

@ -3,12 +3,15 @@ import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { ExportNightwave, ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService";
import {
addItem,
addMiscItems,
combineInventoryChanges,
getInventory,
updateCurrency
} from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { isStoreItem, toStoreItem } from "@/src/services/itemDataService";
import { logger } from "@/src/utils/logger";
const nightwaveCredsItemType = ExportNightwave.rewards[ExportNightwave.rewards.length - 1].uniqueName;
import { fromStoreItem, isStoreItem } from "@/src/services/itemDataService";
export const syndicateSacrificeController: RequestHandler = async (request, response) => {
const accountId = await getAccountIdForRequest(request);
@ -54,7 +57,7 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
syndicate.Title ??= 0;
syndicate.Title += 1;
if (syndicate.Title > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == syndicate.Title)) {
if (syndicate.Title > 0 && manifest.favours.length != 0) {
syndicate.FreeFavorsEarned ??= [];
if (!syndicate.FreeFavorsEarned.includes(syndicate.Title)) {
syndicate.FreeFavorsEarned.push(syndicate.Title);
@ -74,17 +77,10 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
res.NewEpisodeReward = true;
const reward = ExportNightwave.rewards[index];
let rewardType = reward.uniqueName;
if (!isStoreItem(rewardType)) {
rewardType = toStoreItem(rewardType);
if (isStoreItem(rewardType)) {
rewardType = fromStoreItem(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);
combineInventoryChanges(res.InventoryChanges, await addItem(inventory, rewardType, reward.itemCount ?? 1));
}
}

View File

@ -1,9 +1,16 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addMiscItems, addStanding, freeUpSlot, getInventory } from "@/src/services/inventoryService";
import {
addMiscItems,
freeUpSlot,
getInventory,
getStandingLimit,
updateStandingLimit
} from "@/src/services/inventoryService";
import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { IOid } from "@/src/types/commonTypes";
import { ExportSyndicates, ExportWeapons } from "warframe-public-export-plus";
import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
import { logger } from "@/src/utils/logger";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
@ -54,13 +61,38 @@ export const syndicateStandingBonusController: RequestHandler = async (req, res)
inventoryChanges[slotBin] = { count: -1, platinum: 0, Slots: 1 };
}
const affiliationMod = addStanding(inventory, request.Operation.AffiliationTag, gainedStanding, true);
let syndicate = inventory.Affiliations.find(x => x.Tag == request.Operation.AffiliationTag);
if (!syndicate) {
syndicate =
inventory.Affiliations[
inventory.Affiliations.push({ Tag: request.Operation.AffiliationTag, Standing: 0 }) - 1
];
}
const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0);
if (syndicate.Standing + gainedStanding > max) {
gainedStanding = max - syndicate.Standing;
}
if (syndicateMeta.medallionsCappedByDailyLimit) {
if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) {
gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin);
}
updateStandingLimit(inventory, syndicateMeta.dailyLimitBin, gainedStanding);
}
syndicate.Standing += gainedStanding;
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
AffiliationMods: [affiliationMod]
AffiliationMods: [
{
Tag: request.Operation.AffiliationTag,
Standing: gainedStanding
}
]
});
};

View File

@ -23,7 +23,7 @@ const trainingResultController: RequestHandler = async (req, res): Promise<void>
const trainingResults = getJSONfromString<ITrainingResultsRequest>(String(req.body));
const inventory = await getInventory(accountId, "TrainingDate PlayerLevel TradesRemaining");
const inventory = await getInventory(accountId);
if (trainingResults.numLevelsGained == 1) {
let time = Date.now();
@ -33,7 +33,6 @@ const trainingResultController: RequestHandler = async (req, res): Promise<void>
inventory.TrainingDate = new Date(time);
inventory.PlayerLevel += 1;
inventory.TradesRemaining += 1;
await createMessage(accountId, [
{

View File

@ -1,8 +1,10 @@
import { RequestHandler } from "express";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addChallenges, getInventory } from "@/src/services/inventoryService";
import { addChallenges, addSeasonalChallengeHistory, getInventory } from "@/src/services/inventoryService";
import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportNightwave } from "warframe-public-export-plus";
import { logger } from "@/src/utils/logger";
import { IAffiliationMods } from "@/src/types/purchaseTypes";
export const updateChallengeProgressController: RequestHandler = async (req, res) => {
@ -10,19 +12,41 @@ export const updateChallengeProgressController: RequestHandler = async (req, res
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "ChallengeProgress SeasonChallengeHistory Affiliations");
let affiliationMods: IAffiliationMods[] = [];
if (challenges.ChallengeProgress) {
affiliationMods = addChallenges(inventory, challenges.ChallengeProgress, challenges.SeasonChallengeCompletions);
addChallenges(inventory, challenges.ChallengeProgress);
}
if (challenges.SeasonChallengeHistory) {
challenges.SeasonChallengeHistory.forEach(({ challenge, id }) => {
const itemIndex = inventory.SeasonChallengeHistory.findIndex(i => i.challenge === challenge);
if (itemIndex !== -1) {
inventory.SeasonChallengeHistory[itemIndex].id = id;
} else {
inventory.SeasonChallengeHistory.push({ challenge, id });
addSeasonalChallengeHistory(inventory, challenges.SeasonChallengeHistory);
}
const affiliationMods: IAffiliationMods[] = [];
if (challenges.ChallengeProgress && challenges.SeasonChallengeCompletions) {
for (const challenge of challenges.SeasonChallengeCompletions) {
// Ignore challenges that weren't completed just now
if (!challenges.ChallengeProgress.find(x => challenge.challenge.indexOf(x.Name) != -1)) {
continue;
}
});
const meta = ExportNightwave.challenges[challenge.challenge];
logger.debug("Completed challenge", meta);
let affiliation = inventory.Affiliations.find(x => x.Tag == ExportNightwave.affiliationTag);
if (!affiliation) {
affiliation =
inventory.Affiliations[
inventory.Affiliations.push({
Tag: ExportNightwave.affiliationTag,
Standing: 0
}) - 1
];
}
affiliation.Standing += meta.standing;
if (affiliationMods.length == 0) {
affiliationMods.push({ Tag: ExportNightwave.affiliationTag });
}
affiliationMods[0].Standing ??= 0;
affiliationMods[0].Standing += meta.standing;
}
}
await inventory.save();

View File

@ -25,13 +25,7 @@ export const upgradesController: RequestHandler = async (req, res) => {
operation.UpgradeRequirement == "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker"
) {
updateCurrency(inventory, 10, true);
} else if (
operation.OperationType != "UOT_SWAP_POLARITY" &&
operation.OperationType != "UOT_ABILITY_OVERRIDE"
) {
if (!operation.UpgradeRequirement) {
throw new Error(`${operation.OperationType} operation should be free?`);
}
} else if (operation.OperationType != "UOT_ABILITY_OVERRIDE") {
addMiscItems(inventory, [
{
ItemType: operation.UpgradeRequirement,

View File

@ -1,16 +1,12 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addFusionPoints, getInventory } from "@/src/services/inventoryService";
import { getInventory } from "@/src/services/inventoryService";
export const addCurrencyController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const request = req.body as IAddCurrencyRequest;
const inventory = await getInventory(accountId, request.currency);
if (request.currency == "FusionPoints") {
addFusionPoints(inventory, request.delta);
} else {
inventory[request.currency] += request.delta;
}
inventory[request.currency] += request.delta;
await inventory.save();
res.end();
};

View File

@ -1,44 +0,0 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
import { ExportArcanes, ExportUpgrades } from "warframe-public-export-plus";
export const addMissingMaxRankModsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "Upgrades");
const maxOwnedRanks: Record<string, number> = {};
for (const upgrade of inventory.Upgrades) {
const fingerprint = JSON.parse(upgrade.UpgradeFingerprint ?? "{}") as { lvl?: number };
if (fingerprint.lvl) {
maxOwnedRanks[upgrade.ItemType] ??= 0;
if (fingerprint.lvl > maxOwnedRanks[upgrade.ItemType]) {
maxOwnedRanks[upgrade.ItemType] = fingerprint.lvl;
}
}
}
for (const [uniqueName, data] of Object.entries(ExportUpgrades)) {
if (data.fusionLimit != 0 && data.type != "PARAZON" && maxOwnedRanks[uniqueName] != data.fusionLimit) {
inventory.Upgrades.push({
ItemType: uniqueName,
UpgradeFingerprint: JSON.stringify({ lvl: data.fusionLimit })
});
}
}
for (const [uniqueName, data] of Object.entries(ExportArcanes)) {
if (
data.name != "/Lotus/Language/Items/GenericCosmeticEnhancerName" &&
maxOwnedRanks[uniqueName] != data.fusionLimit
) {
inventory.Upgrades.push({
ItemType: uniqueName,
UpgradeFingerprint: JSON.stringify({ lvl: data.fusionLimit })
});
}
}
await inventory.save();
res.end();
};

View File

@ -0,0 +1,98 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import {
getInventory,
addEquipment,
occupySlot,
productCategoryToInventoryBin,
applyDefaultUpgrades
} from "@/src/services/inventoryService";
import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper";
import { getDefaultUpgrades } from "@/src/services/itemDataService";
import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { ExportWeapons } from "warframe-public-export-plus";
import { RequestHandler } from "express";
export const addModularEquipmentController: RequestHandler = async (req, res) => {
const requiredFields = new Set();
const accountId = await getAccountIdForRequest(req);
const request = req.body as IAddModularEquipmentRequest;
const category = modularWeaponTypes[request.ItemType];
const inventoryBin = productCategoryToInventoryBin(category)!;
requiredFields.add(category);
requiredFields.add(inventoryBin);
request.ModularParts.forEach(part => {
if (ExportWeapons[part].gunType) {
if (category == "LongGuns") {
request.ItemType = {
GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary",
GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun",
GT_BEAM: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam"
}[ExportWeapons[part].gunType];
} else {
request.ItemType = {
GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary",
GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun",
GT_BEAM: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam"
}[ExportWeapons[part].gunType];
}
} else if (request.ItemType == "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetPowerSuit") {
if (part.includes("ZanukaPetPartHead")) {
request.ItemType = {
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA":
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB":
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC":
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit"
}[part]!;
}
}
});
const defaultUpgrades = getDefaultUpgrades(request.ModularParts);
if (defaultUpgrades) {
requiredFields.add("RawUpgrades");
}
const defaultWeaponsMap: Record<string, string[]> = {
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit": [
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetMeleeWeaponIP"
],
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit": [
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetMeleeWeaponIS"
],
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit": [
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetMeleeWeaponPS"
]
};
const defaultWeapons = defaultWeaponsMap[request.ItemType] as string[] | undefined;
if (defaultWeapons) {
for (const defaultWeapon of defaultWeapons) {
const category = ExportWeapons[defaultWeapon].productCategory;
requiredFields.add(category);
requiredFields.add(productCategoryToInventoryBin(category));
}
}
const inventory = await getInventory(accountId, Array.from(requiredFields).join(" "));
if (defaultWeapons) {
for (const defaultWeapon of defaultWeapons) {
const category = ExportWeapons[defaultWeapon].productCategory;
addEquipment(inventory, category, defaultWeapon);
occupySlot(inventory, productCategoryToInventoryBin(category)!, true);
}
}
const defaultOverwrites: Partial<IEquipmentDatabase> = {
Configs: applyDefaultUpgrades(inventory, defaultUpgrades)
};
addEquipment(inventory, category, request.ItemType, request.ModularParts, undefined, defaultOverwrites);
occupySlot(inventory, inventoryBin, true);
await inventory.save();
res.end();
};
interface IAddModularEquipmentRequest {
ItemType: string;
ModularParts: string[];
}

View File

@ -1,4 +1,4 @@
import { applyClientEquipmentUpdates, getInventory } from "@/src/services/inventoryService";
import { addGearExpByCategory, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
@ -20,7 +20,7 @@ export const addXpController: RequestHandler = async (req, res) => {
}
}
}
applyClientEquipmentUpdates(inventory, gear, category as TEquipmentKey);
addGearExpByCategory(inventory, gear, category as TEquipmentKey);
}
await inventory.save();
res.end();

View File

@ -1,6 +1,6 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { Account, Ignore } from "@/src/models/loginModel";
import { Account } from "@/src/models/loginModel";
import { Inbox } from "@/src/models/inboxModel";
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
@ -23,8 +23,6 @@ export const deleteAccountController: RequestHandler = async (req, res) => {
await Promise.all([
Account.deleteOne({ _id: accountId }),
GuildMember.deleteMany({ accountId: accountId }),
Ignore.deleteMany({ ignorer: accountId }),
Ignore.deleteMany({ ignoree: accountId }),
Inbox.deleteMany({ ownerId: accountId }),
Inventory.deleteOne({ accountOwnerId: accountId }),
Leaderboard.deleteMany({ ownerId: accountId }),

View File

@ -3,7 +3,6 @@ import { getDict, getItemName, getString } from "@/src/services/itemDataService"
import {
ExportArcanes,
ExportAvionics,
ExportCustoms,
ExportDrones,
ExportGear,
ExportKeys,
@ -20,7 +19,6 @@ import {
TRelicQuality
} from "warframe-public-export-plus";
import archonCrystalUpgrades from "@/static/fixed_responses/webuiArchonCrystalUpgrades.json";
import allIncarnons from "@/static/fixed_responses/allIncarnonList.json";
interface ListedItem {
uniqueName: string;
@ -30,30 +28,6 @@ interface ListedItem {
badReason?: "starter" | "frivolous" | "notraw";
partType?: string;
chainLength?: number;
parazon?: boolean;
}
interface ItemLists {
archonCrystalUpgrades: Record<string, string>;
uniqueLevelCaps: Record<string, number>;
Suits: ListedItem[];
LongGuns: ListedItem[];
Melee: ListedItem[];
ModularParts: ListedItem[];
Pistols: ListedItem[];
Sentinels: ListedItem[];
SentinelWeapons: ListedItem[];
SpaceGuns: ListedItem[];
SpaceMelee: ListedItem[];
SpaceSuits: ListedItem[];
MechSuits: ListedItem[];
miscitems: ListedItem[];
Syndicates: ListedItem[];
OperatorAmps: ListedItem[];
QuestKeys: ListedItem[];
KubrowPets: ListedItem[];
EvolutionProgress: ListedItem[];
mods: ListedItem[];
}
const relicQualitySuffixes: Record<TRelicQuality, string> = {
@ -65,28 +39,22 @@ const relicQualitySuffixes: Record<TRelicQuality, string> = {
const getItemListsController: RequestHandler = (req, response) => {
const lang = getDict(typeof req.query.lang == "string" ? req.query.lang : "en");
const res: ItemLists = {
archonCrystalUpgrades,
uniqueLevelCaps: ExportMisc.uniqueLevelCaps,
Suits: [],
LongGuns: [],
Melee: [],
ModularParts: [],
Pistols: [],
Sentinels: [],
SentinelWeapons: [],
SpaceGuns: [],
SpaceMelee: [],
SpaceSuits: [],
MechSuits: [],
miscitems: [],
Syndicates: [],
OperatorAmps: [],
QuestKeys: [],
KubrowPets: [],
EvolutionProgress: [],
mods: []
};
const res: Record<string, ListedItem[]> = {};
res.Suits = [];
res.LongGuns = [];
res.Melee = [];
res.ModularParts = [];
res.Pistols = [];
res.Sentinels = [];
res.SentinelWeapons = [];
res.SpaceGuns = [];
res.SpaceMelee = [];
res.SpaceSuits = [];
res.MechSuits = [];
res.miscitems = [];
res.Syndicates = [];
res.OperatorAmps = [];
res.QuestKeys = [];
for (const [uniqueName, item] of Object.entries(ExportWarframes)) {
res[item.productCategory].push({
uniqueName,
@ -95,23 +63,20 @@ const getItemListsController: RequestHandler = (req, response) => {
});
}
for (const [uniqueName, item] of Object.entries(ExportSentinels)) {
if (item.productCategory == "Sentinels" || item.productCategory == "KubrowPets") {
if (item.productCategory == "Sentinels") {
res[item.productCategory].push({
uniqueName,
name: getString(item.name, lang),
exalted: item.exalted
name: getString(item.name, lang)
});
}
}
for (const [uniqueName, item] of Object.entries(ExportWeapons)) {
if (item.partType) {
if (!uniqueName.startsWith("/Lotus/Types/Items/Deimos/")) {
res.ModularParts.push({
uniqueName,
name: getString(item.name, lang),
partType: item.partType
});
}
res.ModularParts.push({
uniqueName,
name: getString(item.name, lang),
partType: item.partType
});
if (uniqueName.split("/")[5] != "SentTrainingAmplifier") {
res.miscitems.push({
uniqueName: uniqueName,
@ -144,28 +109,12 @@ const getItemListsController: RequestHandler = (req, response) => {
let name = getString(item.name, lang);
if ("dissectionParts" in item) {
name = getString("/Lotus/Language/Fish/FishDisplayName", lang).split("|FISH_NAME|").join(name);
if (item.syndicateTag == "CetusSyndicate") {
if (uniqueName.indexOf("Large") != -1) {
name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeLargeAbbrev", lang));
} else if (uniqueName.indexOf("Medium") != -1) {
name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeMediumAbbrev", lang));
} else {
name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeSmallAbbrev", lang));
}
if (uniqueName.indexOf("Large") != -1) {
name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeLargeAbbrev", lang));
} else if (uniqueName.indexOf("Medium") != -1) {
name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeMediumAbbrev", lang));
} else {
if (uniqueName.indexOf("Large") != -1) {
name = name
.split("|FISH_SIZE|")
.join(getString("/Lotus/Language/SolarisVenus/RobofishAgeCategoryElderAbbrev", lang));
} else if (uniqueName.indexOf("Medium") != -1) {
name = name
.split("|FISH_SIZE|")
.join(getString("/Lotus/Language/SolarisVenus/RobofishAgeCategoryMatureAbbrev", lang));
} else {
name = name
.split("|FISH_SIZE|")
.join(getString("/Lotus/Language/SolarisVenus/RobofishAgeCategoryYoungAbbrev", lang));
}
name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeSmallAbbrev", lang));
}
}
if (
@ -201,11 +150,9 @@ const getItemListsController: RequestHandler = (req, response) => {
if (!item.hidden) {
const resultName = getItemName(item.resultType);
if (resultName) {
let itemName = getString(resultName, lang);
if (item.num > 1) itemName = `${itemName} X ${item.num}`;
res.miscitems.push({
uniqueName: uniqueName,
name: recipeNameTemplate.replace("|ITEM|", itemName)
name: recipeNameTemplate.replace("|ITEM|", getString(resultName, lang))
});
}
}
@ -222,13 +169,8 @@ const getItemListsController: RequestHandler = (req, response) => {
name: getString(item.name, lang)
});
}
for (const [uniqueName, item] of Object.entries(ExportCustoms)) {
res.miscitems.push({
uniqueName: uniqueName,
name: getString(item.name, lang)
});
}
res.mods = [];
for (const [uniqueName, upgrade] of Object.entries(ExportUpgrades)) {
const mod: ListedItem = {
uniqueName,
@ -242,9 +184,6 @@ const getItemListsController: RequestHandler = (req, response) => {
} else if (upgrade.upgradeEntries) {
mod.badReason = "notraw";
}
if (upgrade.type == "PARAZON") {
mod.parazon = true;
}
res.mods.push(mod);
}
for (const [uniqueName, upgrade] of Object.entries(ExportAvionics)) {
@ -286,14 +225,12 @@ const getItemListsController: RequestHandler = (req, response) => {
});
}
}
for (const uniqueName of allIncarnons) {
res.EvolutionProgress.push({
uniqueName,
name: getString(getItemName(uniqueName) || "", lang)
});
}
response.json(res);
response.json({
archonCrystalUpgrades,
uniqueLevelCaps: ExportMisc.uniqueLevelCaps,
...res
});
};
export { getItemListsController };

View File

@ -0,0 +1,23 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
export const gildEquipmentController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const request = req.body as IGildEquipmentRequest;
const inventory = await getInventory(accountId, request.Category);
const weapon = inventory[request.Category].id(request.ItemId);
if (weapon) {
weapon.Features ??= 0;
weapon.Features |= EquipmentFeatures.GILDED;
await inventory.save();
}
res.end();
};
type IGildEquipmentRequest = {
ItemId: string;
Category: TEquipmentKey;
};

View File

@ -35,8 +35,10 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
switch (operation) {
case "completeAll": {
for (const questKey of inventory.QuestKeys) {
await completeQuest(inventory, questKey.ItemType);
if (allQuestKeys.includes(questItemType)) {
for (const questKey of inventory.QuestKeys) {
await completeQuest(inventory, questKey.ItemType);
}
}
break;
}
@ -54,12 +56,15 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
break;
}
case "deleteKey": {
const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType);
if (!questKey) {
logger.error(`Quest key not found in inventory: ${questItemType}`);
break;
if (allQuestKeys.includes(questItemType)) {
const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType);
if (!questKey) {
logger.error(`Quest key not found in inventory: ${questItemType}`);
break;
}
inventory.QuestKeys.pull({ ItemType: questItemType });
}
inventory.QuestKeys.pull({ ItemType: questItemType });
break;
}
case "completeKey": {

View File

@ -1,33 +0,0 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const setEvolutionProgressController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const payload = req.body as ISetEvolutionProgressRequest;
inventory.EvolutionProgress ??= [];
payload.forEach(element => {
const entry = inventory.EvolutionProgress!.find(entry => entry.ItemType === element.ItemType);
if (entry) {
entry.Progress = 0;
entry.Rank = element.Rank;
} else {
inventory.EvolutionProgress!.push({
Progress: 0,
Rank: element.Rank,
ItemType: element.ItemType
});
}
});
await inventory.save();
res.end();
};
type ISetEvolutionProgressRequest = {
ItemType: string;
Rank: number;
}[];

View File

@ -1,6 +1,451 @@
import { RequestHandler } from "express";
import { getWorldState } from "@/src/services/worldStateService";
import staticWorldState from "@/static/fixed_responses/worldState/worldState.json";
import static1999FallDays from "@/static/fixed_responses/worldState/1999_fall_days.json";
import static1999SpringDays from "@/static/fixed_responses/worldState/1999_spring_days.json";
import static1999SummerDays from "@/static/fixed_responses/worldState/1999_summer_days.json";
import static1999WinterDays from "@/static/fixed_responses/worldState/1999_winter_days.json";
import { buildConfig } from "@/src/services/buildConfigService";
import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { unixTimesInMs } from "@/src/constants/timeConstants";
import { config } from "@/src/services/configService";
import { CRng } from "@/src/services/rngService";
import { ExportNightwave, ExportRegions } from "warframe-public-export-plus";
const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0
export const worldStateController: RequestHandler = (req, res) => {
res.json(getWorldState(req.query.buildLabel as string | undefined));
const day = Math.trunc((Date.now() - EPOCH) / 86400000);
const week = Math.trunc(day / 7);
const weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000;
const worldState: IWorldState = {
BuildLabel:
typeof req.query.buildLabel == "string"
? req.query.buildLabel.split(" ").join("+")
: buildConfig.buildLabel,
Time: config.worldState?.lockTime || Math.round(Date.now() / 1000),
Goals: [],
GlobalUpgrades: [],
LiteSorties: [],
EndlessXpChoices: [],
SeasonInfo: {
Activation: { $date: { $numberLong: "1715796000000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
AffiliationTag: "RadioLegionIntermission12Syndicate",
Season: 14,
Phase: 0,
Params: "",
ActiveChallenges: [
getSeasonDailyChallenge(day - 2),
getSeasonDailyChallenge(day - 1),
getSeasonDailyChallenge(day - 0),
getSeasonWeeklyChallenge(week, 0),
getSeasonWeeklyChallenge(week, 1),
getSeasonWeeklyHardChallenge(week, 2),
getSeasonWeeklyHardChallenge(week, 3),
{
_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)
},
{
_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)
},
{
_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)
}
]
},
...staticWorldState
};
if (config.worldState?.starDays) {
worldState.Goals.push({
_id: { $oid: "67a4dcce2a198564d62e1647" },
Activation: { $date: { $numberLong: "1738868400000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
Count: 0,
Goal: 0,
Success: 0,
Personal: true,
Desc: "/Lotus/Language/Events/ValentinesFortunaName",
ToolTip: "/Lotus/Language/Events/ValentinesFortunaName",
Icon: "/Lotus/Interface/Icons/WorldStatePanel/ValentinesEventIcon.png",
Tag: "FortunaValentines",
Node: "SolarisUnitedHub1"
});
}
// Elite Sanctuary Onslaught cycling every week
worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = week; // unfaithful
// Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation
const bountyCycle = Math.trunc(Date.now() / 9000000);
const bountyCycleStart = bountyCycle * 9000000;
const bountyCycleEnd = bountyCycleStart + 9000000;
worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "ZarimanSyndicate")] = {
_id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000029" },
Activation: { $date: { $numberLong: bountyCycleStart.toString() } },
Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } },
Tag: "ZarimanSyndicate",
Seed: bountyCycle,
Nodes: []
};
worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "EntratiLabSyndicate")] = {
_id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000004" },
Activation: { $date: { $numberLong: bountyCycleStart.toString() } },
Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } },
Tag: "EntratiLabSyndicate",
Seed: bountyCycle,
Nodes: []
};
worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "HexSyndicate")] = {
_id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000006" },
Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } },
Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } },
Tag: "HexSyndicate",
Seed: bountyCycle,
Nodes: []
};
if (config.worldState?.creditBoost) {
worldState.GlobalUpgrades.push({
_id: { $oid: "5b23106f283a555109666672" },
Activation: { $date: { $numberLong: "1740164400000" } },
ExpiryDate: { $date: { $numberLong: "2000000000000" } },
UpgradeType: "GAMEPLAY_MONEY_REWARD_AMOUNT",
OperationType: "MULTIPLY",
Value: 2,
LocalizeTag: "",
LocalizeDescTag: ""
});
}
if (config.worldState?.affinityBoost) {
worldState.GlobalUpgrades.push({
_id: { $oid: "5b23106f283a555109666673" },
Activation: { $date: { $numberLong: "1740164400000" } },
ExpiryDate: { $date: { $numberLong: "2000000000000" } },
UpgradeType: "GAMEPLAY_KILL_XP_AMOUNT",
OperationType: "MULTIPLY",
Value: 2,
LocalizeTag: "",
LocalizeDescTag: ""
});
}
if (config.worldState?.resourceBoost) {
worldState.GlobalUpgrades.push({
_id: { $oid: "5b23106f283a555109666674" },
Activation: { $date: { $numberLong: "1740164400000" } },
ExpiryDate: { $date: { $numberLong: "2000000000000" } },
UpgradeType: "GAMEPLAY_PICKUP_AMOUNT",
OperationType: "MULTIPLY",
Value: 2,
LocalizeTag: "",
LocalizeDescTag: ""
});
}
// Archon Hunt cycling every week
{
const boss = ["SORTIE_BOSS_AMAR", "SORTIE_BOSS_NIRA", "SORTIE_BOSS_BOREAL"][week % 3];
const showdownNode = ["SolNode99", "SolNode53", "SolNode24"][week % 3];
const systemIndex = [3, 4, 2][week % 3]; // Mars, Jupiter, Earth
const nodes: string[] = [];
for (const [key, value] of Object.entries(ExportRegions)) {
if (
value.systemIndex === systemIndex &&
value.factionIndex !== undefined &&
value.factionIndex < 2 &&
value.name.indexOf("Archwing") == -1 &&
value.missionIndex != 0 // Exclude MT_ASSASSINATION
) {
nodes.push(key);
}
}
const rng = new CRng(week);
const firstNodeIndex = rng.randomInt(0, nodes.length - 1);
const firstNode = nodes[firstNodeIndex];
nodes.splice(firstNodeIndex, 1);
worldState.LiteSorties.push({
_id: {
$oid: Math.trunc(weekStart / 1000).toString(16) + "5e23a244740a190c"
},
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards",
Seed: week,
Boss: boss,
Missions: [
{
missionType: rng.randomElement([
"MT_INTEL",
"MT_MOBILE_DEFENSE",
"MT_EXTERMINATION",
"MT_SABOTAGE",
"MT_RESCUE"
]),
node: firstNode
},
{
missionType: rng.randomElement([
"MT_DEFENSE",
"MT_TERRITORY",
"MT_ARTIFACT",
"MT_EXCAVATE",
"MT_SURVIVAL"
]),
node: rng.randomElement(nodes)
},
{
missionType: "MT_ASSASSINATION",
node: showdownNode
}
]
});
}
// Circuit choices cycling every week
worldState.EndlessXpChoices.push({
Category: "EXC_NORMAL",
Choices: [
["Nidus", "Octavia", "Harrow"],
["Gara", "Khora", "Revenant"],
["Garuda", "Baruuk", "Hildryn"],
["Excalibur", "Trinity", "Ember"],
["Loki", "Mag", "Rhino"],
["Ash", "Frost", "Nyx"],
["Saryn", "Vauban", "Nova"],
["Nekros", "Valkyr", "Oberon"],
["Hydroid", "Mirage", "Limbo"],
["Mesa", "Chroma", "Atlas"],
["Ivara", "Inaros", "Titania"]
][week % 12]
});
worldState.EndlessXpChoices.push({
Category: "EXC_HARD",
Choices: [
["Boar", "Gammacor", "Angstrum", "Gorgon", "Anku"],
["Bo", "Latron", "Furis", "Furax", "Strun"],
["Lex", "Magistar", "Boltor", "Bronco", "CeramicDagger"],
["Torid", "DualToxocyst", "DualIchor", "Miter", "Atomos"],
["AckAndBrunt", "Soma", "Vasto", "NamiSolo", "Burston"],
["Zylok", "Sibear", "Dread", "Despair", "Hate"],
["Dera", "Sybaris", "Cestra", "Sicarus", "Okina"],
["Braton", "Lato", "Skana", "Paris", "Kunai"]
][week % 8]
});
// 1999 Calendar Season cycling every week + YearIteration every 4 weeks
worldState.KnownCalendarSeasons[0].Activation = { $date: { $numberLong: weekStart.toString() } };
worldState.KnownCalendarSeasons[0].Expiry = { $date: { $numberLong: weekEnd.toString() } };
worldState.KnownCalendarSeasons[0].Season = ["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"][week % 4];
worldState.KnownCalendarSeasons[0].Days = [
static1999WinterDays,
static1999SpringDays,
static1999SummerDays,
static1999FallDays
][week % 4];
worldState.KnownCalendarSeasons[0].YearIteration = Math.trunc(week / 4);
// Sentient Anomaly cycling every 30 minutes
const halfHour = Math.trunc(Date.now() / (unixTimesInMs.hour / 2));
const tmp = {
cavabegin: "1690761600",
PurchasePlatformLockEnabled: true,
tcsn: true,
pgr: {
ts: "1732572900",
en: "CUSTOM DECALS @ ZEVILA",
fr: "DECALS CUSTOM @ ZEVILA",
it: "DECALCOMANIE PERSONALIZZATE @ ZEVILA",
de: "AUFKLEBER NACH WUNSCH @ ZEVILA",
es: "CALCOMANÍAS PERSONALIZADAS @ ZEVILA",
pt: "DECALQUES PERSONALIZADOS NA ZEVILA",
ru: "ПОЛЬЗОВАТЕЛЬСКИЕ НАКЛЕЙКИ @ ЗеВиЛа",
pl: "NOWE NAKLEJKI @ ZEVILA",
uk: "КОРИСТУВАЦЬКІ ДЕКОЛІ @ ЗІВІЛА",
tr: "ÖZEL ÇIKARTMALAR @ ZEVILA",
ja: "カスタムデカール @ ゼビラ",
zh: "定制贴花认准泽威拉",
ko: "커스텀 데칼 @ ZEVILA",
tc: "自訂貼花 @ ZEVILA",
th: "รูปลอกสั่งทำที่ ZEVILA"
},
ennnd: true,
mbrt: true,
sfn: [550, 553, 554, 555][halfHour % 4]
};
worldState.Tmp = JSON.stringify(tmp);
res.json(worldState);
};
interface IWorldState {
Version: number; // for goals
BuildLabel: string;
Time: number;
Goals: IGoal[];
SyndicateMissions: ISyndicateMission[];
GlobalUpgrades: IGlobalUpgrade[];
LiteSorties: ILiteSortie[];
NodeOverrides: INodeOverride[];
EndlessXpChoices: IEndlessXpChoice[];
SeasonInfo: {
Activation: IMongoDate;
Expiry: IMongoDate;
AffiliationTag: string;
Season: number;
Phase: number;
Params: string;
ActiveChallenges: ISeasonChallenge[];
};
KnownCalendarSeasons: ICalendarSeason[];
Tmp?: string;
}
interface IGoal {
_id: IOid;
Activation: IMongoDate;
Expiry: IMongoDate;
Count: number;
Goal: number;
Success: number;
Personal: boolean;
Desc: string;
ToolTip: string;
Icon: string;
Tag: string;
Node: string;
}
interface ISyndicateMission {
_id: IOid;
Activation: IMongoDate;
Expiry: IMongoDate;
Tag: string;
Seed: number;
Nodes: string[];
}
interface IGlobalUpgrade {
_id: IOid;
Activation: IMongoDate;
ExpiryDate: IMongoDate;
UpgradeType: string;
OperationType: string;
Value: number;
LocalizeTag: string;
LocalizeDescTag: string;
}
interface INodeOverride {
_id: IOid;
Activation?: IMongoDate;
Expiry?: IMongoDate;
Node: string;
Hide?: boolean;
Seed?: number;
LevelOverride?: string;
Faction?: string;
CustomNpcEncounters?: string;
}
interface ILiteSortie {
_id: IOid;
Activation: IMongoDate;
Expiry: IMongoDate;
Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards";
Seed: number;
Boss: string; // "SORTIE_BOSS_AMAR" | "SORTIE_BOSS_NIRA" | "SORTIE_BOSS_BOREAL"
Missions: {
missionType: string;
node: string;
}[];
}
interface IEndlessXpChoice {
Category: string;
Choices: string[];
}
interface ISeasonChallenge {
_id: IOid;
Daily?: boolean;
Activation: IMongoDate;
Expiry: IMongoDate;
Challenge: string;
}
interface ICalendarSeason {
Activation: IMongoDate;
Expiry: IMongoDate;
Season: string; // "CST_UNDEFINED" | "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL"
Days: {
day: number;
}[];
YearIteration: number;
}
const dailyChallenges = Object.keys(ExportNightwave.challenges).filter(x =>
x.startsWith("/Lotus/Types/Challenges/Seasons/Daily/")
);
const getSeasonDailyChallenge = (day: number): ISeasonChallenge => {
const dayStart = EPOCH + day * 86400000;
const dayEnd = EPOCH + (day + 3) * 86400000;
const rng = new CRng(day);
return {
_id: { $oid: "67e1b5ca9d00cb47" + day.toString().padStart(8, "0") },
Daily: true,
Activation: { $date: { $numberLong: dayStart.toString() } },
Expiry: { $date: { $numberLong: dayEnd.toString() } },
Challenge: rng.randomElement(dailyChallenges)
};
};
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 weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000;
const challengeId = week * 7 + id;
const rng = new CRng(challengeId);
return {
_id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") },
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
Challenge: rng.randomElement(weeklyChallenges)
};
};
const weeklyHardChallenges = Object.keys(ExportNightwave.challenges).filter(x =>
x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/")
);
const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChallenge => {
const weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000;
const challengeId = week * 7 + id;
const rng = new CRng(challengeId);
return {
_id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") },
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
Challenge: rng.randomElement(weeklyHardChallenges)
};
};

View File

@ -27,15 +27,7 @@ const viewController: RequestHandler = async (req, res) => {
for (const type of Object.keys(ExportEnemies.avatars)) {
if (!scans.has(type)) scans.add(type);
}
// Take any existing scans and also set them to 9999
if (responseJson.Scans) {
for (const scan of responseJson.Scans) {
scans.add(scan.type);
}
}
responseJson.Scans = [];
responseJson.Scans ??= [];
for (const type of scans) {
responseJson.Scans.push({ type: type, scans: 9999 });
}

View File

@ -1,6 +1,5 @@
import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { Types } from "mongoose";
import { TRarity } from "warframe-public-export-plus";
export const toOid = (objectId: Types.ObjectId): IOid => {
return { $oid: objectId.toString() } satisfies IOid;
@ -9,148 +8,3 @@ export const toOid = (objectId: Types.ObjectId): IOid => {
export const toMongoDate = (date: Date): IMongoDate => {
return { $date: { $numberLong: date.getTime().toString() } };
};
export const fromMongoData = (date: IMongoDate): Date => {
return new Date(parseInt(date.$date.$numberLong));
};
export const kubrowWeights: Record<TRarity, number> = {
COMMON: 6,
UNCOMMON: 4,
RARE: 2,
LEGENDARY: 1
};
export const kubrowFurPatternsWeights: Record<TRarity, number> = {
COMMON: 6,
UNCOMMON: 5,
RARE: 2,
LEGENDARY: 1
};
export const catbrowDetails = {
Colors: [
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseA", rarity: "COMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseB", rarity: "COMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseC", rarity: "COMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseD", rarity: "COMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryA", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryB", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryC", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryD", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryA", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryB", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryC", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryD", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsA", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsB", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsC", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsD", rarity: "LEGENDARY" as TRarity }
],
EyeColors: [
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesA", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesB", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesC", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesD", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesE", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesF", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesG", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesH", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesI", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesJ", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesK", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesL", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesM", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesN", rarity: "LEGENDARY" as TRarity }
],
FurPatterns: [{ type: "/Lotus/Types/Game/CatbrowPet/Patterns/CatbrowPetPatternA", rarity: "COMMON" as TRarity }],
BodyTypes: [
{ type: "/Lotus/Types/Game/CatbrowPet/BodyTypes/CatbrowPetRegularBodyType", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/BodyTypes/CatbrowPetRegularBodyType", rarity: "LEGENDARY" as TRarity }
],
Heads: [
{ type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadA", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadB", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadC", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadD", rarity: "LEGENDARY" as TRarity }
],
Tails: [
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailA", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailB", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailC", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailD", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailE", rarity: "LEGENDARY" as TRarity }
]
};
export const kubrowDetails = {
Colors: [
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneA", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneB", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneC", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneD", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneE", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneF", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneG", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneH", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidA", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidB", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidC", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidD", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidE", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidF", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidG", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidH", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantA", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantB", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantC", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantD", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantE", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantF", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantG", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantH", rarity: "LEGENDARY" as TRarity }
],
EyeColors: [
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesA", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesB", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesC", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesD", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesE", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesF", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesG", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesH", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesI", rarity: "LEGENDARY" as TRarity }
],
FurPatterns: [
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternB", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternA", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternC", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternD", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternE", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternF", rarity: "LEGENDARY" as TRarity }
],
BodyTypes: [
{ type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetRegularBodyType", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetHeavyBodyType", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetThinBodyType", rarity: "LEGENDARY" as TRarity }
],
Heads: [],
Tails: []
};

View File

@ -1,14 +1,6 @@
import { ExportRegions, ExportWarframes } from "warframe-public-export-plus";
import { IInfNode, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
import { getRewardAtPercentage, SRng } from "@/src/services/rngService";
import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel";
import { logger } from "../utils/logger";
import { IOid } from "../types/commonTypes";
import { Types } from "mongoose";
import { addMods, generateRewardSeed } from "../services/inventoryService";
import { isArchwingMission } from "../services/worldStateService";
import { fromStoreItem, toStoreItem } from "../services/itemDataService";
import { createMessage } from "../services/inboxService";
import { ExportRegions } from "warframe-public-export-plus";
import { IInfNode } from "@/src/types/inventoryTypes/inventoryTypes";
import { SRng } from "@/src/services/rngService";
export const getInfNodes = (faction: string, rank: number): IInfNode[] => {
const infNodes = [];
@ -25,7 +17,7 @@ export const getInfNodes = (faction: string, rank: number): IInfNode[] => {
value.missionIndex != 42 && // not face off
value.name.indexOf("1999NodeI") == -1 && // not stage defence
value.name.indexOf("1999NodeJ") == -1 && // not lich bounty
!isArchwingMission(value)
value.name.indexOf("Archwing") == -1
) {
//console.log(dict_en[value.name]);
infNodes.push({ Node: key, Influence: 1 });
@ -40,339 +32,13 @@ const systemIndexes: Record<string, number[]> = {
FC_INFESTATION: [23]
};
export const showdownNodes: Record<string, string> = {
FC_GRINEER: "CrewBattleNode557",
FC_CORPUS: "CrewBattleNode558",
FC_INFESTATION: "CrewBattleNode559"
};
// Get a parazon 'passcode' based on the nemesis fingerprint so it's always the same for the same nemesis.
export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: string }): number[] => {
const rng = new SRng(nemesis.fp);
const choices = [0, 1, 2, 3, 5, 6, 7];
let choiceIndex = rng.randomInt(0, choices.length - 1);
const passcode = [choices[choiceIndex]];
if (nemesis.Faction != "FC_INFESTATION") {
choices.splice(choiceIndex, 1);
choiceIndex = rng.randomInt(0, choices.length - 1);
passcode.push(choices[choiceIndex]);
choices.splice(choiceIndex, 1);
choiceIndex = rng.randomInt(0, choices.length - 1);
passcode.push(choices[choiceIndex]);
export const getNemesisPasscode = (fp: bigint, faction: string): number[] => {
const rng = new SRng(fp);
const passcode = [rng.randomInt(0, 7)];
if (faction != "FC_INFESTATION") {
passcode.push(rng.randomInt(0, 7));
passcode.push(rng.randomInt(0, 7));
}
return passcode;
};
const reqiuemMods: readonly string[] = [
"/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalFourMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalFiveMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalSixMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalSevenMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalEightMod"
];
const antivirusMods: readonly string[] = [
"/Lotus/Upgrades/Mods/Immortal/AntivirusOneMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusTwoMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusThreeMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusFourMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusFiveMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusSixMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusSevenMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusEightMod"
];
export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: string }): string[] => {
const passcode = getNemesisPasscode(nemesis);
return nemesis.Faction == "FC_INFESTATION"
? passcode.map(i => antivirusMods[i])
: passcode.map(i => reqiuemMods[i]);
};
export const encodeNemesisGuess = (
symbol1: number,
result1: number,
symbol2: number,
result2: number,
symbol3: number,
result3: number
): number => {
return (
(symbol1 & 0xf) |
((result1 & 3) << 12) |
((symbol2 << 4) & 0xff) |
((result2 << 14) & 0xffff) |
((symbol3 & 0xf) << 8) |
((result3 & 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 interface IKnifeResponse {
UpgradeIds?: string[];
UpgradeTypes?: string[];
UpgradeFingerprints?: { lvl: number }[];
UpgradeNew?: boolean[];
HasKnife?: boolean;
}
export const getKnifeUpgrade = (
inventory: TInventoryDatabaseDocument,
dataknifeUpgrades: string[],
type: string
): { ItemId: IOid; ItemType: string } => {
if (dataknifeUpgrades.indexOf(type) != -1) {
return {
ItemId: { $oid: "000000000000000000000000" },
ItemType: type
};
}
for (const upgradeId of dataknifeUpgrades) {
if (upgradeId.length == 24) {
const upgrade = inventory.Upgrades.id(upgradeId);
if (upgrade && upgrade.ItemType == type) {
return {
ItemId: { $oid: upgradeId },
ItemType: type
};
}
}
}
throw new Error(`${type} does not seem to be installed on parazon?!`);
};
export const consumeModCharge = (
response: IKnifeResponse,
inventory: TInventoryDatabaseDocument,
upgrade: { ItemId: IOid; ItemType: string },
dataknifeUpgrades: string[]
): void => {
response.UpgradeIds ??= [];
response.UpgradeTypes ??= [];
response.UpgradeFingerprints ??= [];
response.UpgradeNew ??= [];
response.HasKnife = true;
if (upgrade.ItemId.$oid != "000000000000000000000000") {
const dbUpgrade = inventory.Upgrades.id(upgrade.ItemId.$oid)!;
const fingerprint = JSON.parse(dbUpgrade.UpgradeFingerprint!) as { lvl: number };
fingerprint.lvl += 1;
dbUpgrade.UpgradeFingerprint = JSON.stringify(fingerprint);
response.UpgradeIds.push(upgrade.ItemId.$oid);
response.UpgradeTypes.push(upgrade.ItemType);
response.UpgradeFingerprints.push(fingerprint);
response.UpgradeNew.push(false);
} else {
const id = new Types.ObjectId();
inventory.Upgrades.push({
_id: id,
ItemType: upgrade.ItemType,
UpgradeFingerprint: `{"lvl":1}`
});
addMods(inventory, [
{
ItemType: upgrade.ItemType,
ItemCount: -1
}
]);
const dataknifeRawUpgradeIndex = dataknifeUpgrades.indexOf(upgrade.ItemType);
if (dataknifeRawUpgradeIndex != -1) {
dataknifeUpgrades[dataknifeRawUpgradeIndex] = id.toString();
} else {
logger.warn(`${upgrade.ItemType} not found in dataknife config`);
}
response.UpgradeIds.push(id.toString());
response.UpgradeTypes.push(upgrade.ItemType);
response.UpgradeFingerprints.push({ lvl: 1 });
response.UpgradeNew.push(true);
}
};
const kuvaLichVersionSixWeapons = [
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak",
"/Lotus/Weapons/Grineer/Melee/GrnKuvaLichScythe/GrnKuvaLichScytheWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Kohm/KuvaKohm",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Ogris/KuvaOgris",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Quartakk/KuvaQuartakk",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Tonkor/KuvaTonkor",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Brakk/KuvaBrakk",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Kraken/KuvaKraken",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Seer/KuvaSeer",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Stubba/KuvaStubba",
"/Lotus/Weapons/Grineer/HeavyWeapons/GrnHeavyGrenadeLauncher",
"/Lotus/Weapons/Grineer/LongGuns/GrnKuvaLichRifle/GrnKuvaLichRifleWeapon",
"/Lotus/Weapons/Grineer/Bows/GrnBow/GrnBowWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hind/KuvaHind",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Nukor/KuvaNukor",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hek/KuvaHekWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Zarr/KuvaZarr",
"/Lotus/Weapons/Grineer/KuvaLich/HeavyWeapons/Grattler/KuvaGrattler",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Sobek/KuvaSobek"
];
const corpusVersionThreeWeapons = [
"/Lotus/Weapons/Corpus/LongGuns/CrpBriefcaseLauncher/CrpBriefcaseLauncher",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEArcaPlasmor/CrpBEArcaPlasmor",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEFluxRifle/CrpBEFluxRifle",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBETetra/CrpBETetra",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBECycron/CrpBECycron",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEDetron/CrpBEDetron",
"/Lotus/Weapons/Corpus/Pistols/CrpIgniterPistol/CrpIgniterPistol",
"/Lotus/Weapons/Corpus/Pistols/CrpBriefcaseAkimbo/CrpBriefcaseAkimboPistol",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEPlinx/CrpBEPlinxWeapon",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEGlaxion/CrpBEGlaxion"
];
export const getWeaponsForManifest = (manifest: string): readonly string[] => {
switch (manifest) {
case "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix":
return kuvaLichVersionSixWeapons;
case "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree":
case "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour":
return corpusVersionThreeWeapons;
}
throw new Error(`unknown nemesis manifest: ${manifest}`);
};
export const getInnateDamageTag = (
KillingSuit: string
):
| "InnateElectricityDamage"
| "InnateFreezeDamage"
| "InnateHeatDamage"
| "InnateImpactDamage"
| "InnateMagDamage"
| "InnateRadDamage"
| "InnateToxinDamage" => {
return ExportWarframes[KillingSuit].nemesisUpgradeTag!;
};
// TODO: For -1399275245665749231n, the value should be 75306944, but we're off by 59 with 75307003.
export const getInnateDamageValue = (fp: bigint): number => {
const rng = new SRng(fp);
rng.randomFloat(); // used for the weapon index
const WeaponUpgradeValueAttenuationExponent = 2.25;
let value = Math.pow(rng.randomFloat(), WeaponUpgradeValueAttenuationExponent);
if (value >= 0.941428) {
value = 1;
}
return Math.trunc(value * 0x40000000);
};
export const getKillTokenRewardCount = (fp: bigint): number => {
const rng = new SRng(fp);
return rng.randomInt(10, 15);
};
// /Lotus/Types/Enemies/InfestedLich/InfestedLichRewardManifest
const infestedLichRotA = [
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyDJRomHuman", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyDJRomInfested", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyDrillbitHuman", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyDrillbitInfested", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyHarddriveHuman", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyHarddriveInfested", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyPacketHuman", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyPacketInfested", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyZekeHuman", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyZekeInfested", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandBillboardPosterA", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandBillboardPosterB", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandDespairPoster", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandGridPoster", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandHuddlePoster", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandJumpPoster", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandLimoPoster", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandLookingDownPosterDay", probability: 0.046 },
{
type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandLookingDownPosterNight",
probability: 0.045
},
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandSillyPoster", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandWhiteBluePoster", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandWhitePinkPoster", probability: 0.045 }
];
const infestedLichRotB = [
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraA", probability: 0.072 },
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraB", probability: 0.071 },
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraC", probability: 0.072 },
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraD", probability: 0.071 },
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraE", probability: 0.072 },
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraF", probability: 0.071 },
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraG", probability: 0.071 },
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraH", probability: 0.072 },
{ type: "/Lotus/StoreItems/Types/Items/Emotes/DanceDJRomHype", probability: 0.071 },
{ type: "/Lotus/StoreItems/Types/Items/Emotes/DancePacketWindmillShuffle", probability: 0.072 },
{ type: "/Lotus/StoreItems/Types/Items/Emotes/DanceHarddrivePony", probability: 0.071 },
{ type: "/Lotus/StoreItems/Types/Items/Emotes/DanceDrillbitCrisscross", probability: 0.072 },
{ type: "/Lotus/StoreItems/Types/Items/Emotes/DanceZekeCanthavethis", probability: 0.071 },
{ type: "/Lotus/StoreItems/Types/Items/PhotoBooth/PhotoboothTileRJLasXStadiumBossArena", probability: 0.071 }
];
export const getInfestedLichItemRewards = (fp: bigint): string[] => {
const rng = new SRng(fp);
const rotAReward = getRewardAtPercentage(infestedLichRotA, rng.randomFloat())!.type;
rng.randomFloat(); // unused afaict
const rotBReward = getRewardAtPercentage(infestedLichRotB, rng.randomFloat())!.type;
return [rotAReward, rotBReward];
};
export const sendCodaFinishedMessage = async (
inventory: TInventoryDatabaseDocument,
fp: bigint = generateRewardSeed(),
name: string = "ZEKE_BEATWOMAN_TM.1999",
killed: boolean = true
): Promise<void> => {
const att: string[] = [];
// First vanquish/convert gives a sigil
const sigil = killed
? "/Lotus/Upgrades/Skins/Sigils/InfLichVanquishedSigil"
: "/Lotus/Upgrades/Skins/Sigils/InfLichConvertedSigil";
if (!inventory.WeaponSkins.find(x => x.ItemType == sigil)) {
att.push(toStoreItem(sigil));
}
const [rotAReward, rotBReward] = getInfestedLichItemRewards(fp);
att.push(fromStoreItem(rotAReward));
att.push(fromStoreItem(rotBReward));
let countedAtt: ITypeCount[] | undefined;
if (killed) {
countedAtt = [
{
ItemType: "/Lotus/Types/Items/MiscItems/CodaWeaponBucks",
ItemCount: getKillTokenRewardCount(fp)
}
];
}
await createMessage(inventory.accountOwnerId, [
{
sndr: "/Lotus/Language/Bosses/Ordis",
msg: "/Lotus/Language/Inbox/VanquishBandMsgBody",
arg: [
{
Key: "LICH_NAME",
Tag: name
}
],
att: att,
countedAtt: countedAtt,
sub: "/Lotus/Language/Inbox/VanquishBandMsgTitle",
icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
highPriority: true
}
]);
};

View File

@ -31,7 +31,7 @@ export interface IFingerprintStat {
}
export const createVeiledRivenFingerprint = (meta: IUpgrade): IVeiledRivenFingerprint => {
const challenge = getRandomElement(meta.availableChallenges!)!;
const challenge = getRandomElement(meta.availableChallenges!);
const fingerprintChallenge: IRivenChallenge = {
Type: challenge.fullName,
Progress: 0,
@ -54,11 +54,11 @@ export const createVeiledRivenFingerprint = (meta: IUpgrade): IVeiledRivenFinger
export const createUnveiledRivenFingerprint = (meta: IUpgrade): IUnveiledRivenFingerprint => {
const fingerprint: IUnveiledRivenFingerprint = {
compat: getRandomElement(meta.compatibleItems!)!,
compat: getRandomElement(meta.compatibleItems!),
lim: 0,
lvl: 0,
lvlReq: getRandomInt(8, 16),
pol: getRandomElement(["AP_ATTACK", "AP_DEFENSE", "AP_TACTIC"])!,
pol: getRandomElement(["AP_ATTACK", "AP_DEFENSE", "AP_TACTIC"]),
buffs: [],
curses: []
};
@ -81,7 +81,7 @@ export const randomiseRivenStats = (meta: IUpgrade, fingerprint: IUnveiledRivenF
if (Math.random() < 0.5) {
const entry = getRandomElement(
meta.upgradeEntries!.filter(x => x.canBeCurse && !fingerprint.buffs.find(y => y.Tag == x.tag))
)!;
);
fingerprint.curses.push({ Tag: entry.tag, Value: Math.trunc(Math.random() * 0x40000000) });
}
};

View File

@ -23,15 +23,13 @@ const dojoDecoSchema = new Schema<IDojoDecoDatabase>({
Type: String,
Pos: [Number],
Rot: [Number],
Scale: Number,
Name: String,
Sockets: Number,
RegularCredits: Number,
MiscItems: { type: [typeCountSchema], default: undefined },
CompletionTime: Date,
RushPlatinum: Number,
PictureFrameInfo: pictureFrameInfoSchema,
Pending: Boolean
PictureFrameInfo: pictureFrameInfoSchema
});
const dojoLeaderboardEntrySchema = new Schema<IDojoLeaderboardEntry>(
@ -44,7 +42,6 @@ const dojoLeaderboardEntrySchema = new Schema<IDojoLeaderboardEntry>(
);
const dojoComponentSchema = new Schema<IDojoComponentDatabase>({
SortId: Schema.Types.ObjectId,
pf: { type: String, required: true },
ppf: String,
pi: Schema.Types.ObjectId,
@ -60,12 +57,6 @@ const dojoComponentSchema = new Schema<IDojoComponentDatabase>({
DestructionTime: Date,
Decos: [dojoDecoSchema],
DecoCapacity: Number,
PaintBot: Schema.Types.ObjectId,
PendingColors: { type: [Number], default: undefined },
Colors: { type: [Number], default: undefined },
PendingLights: { type: [Number], default: undefined },
Lights: { type: [Number], default: undefined },
Settings: String,
Leaderboard: { type: [dojoLeaderboardEntrySchema], default: undefined }
});
@ -192,7 +183,6 @@ const guildSchema = new Schema<IGuildDatabase>(
VaultMiscItems: { type: [typeCountSchema], default: undefined },
VaultShipDecorations: { type: [typeCountSchema], default: undefined },
VaultFusionTreasures: { type: [fusionTreasuresSchema], default: undefined },
VaultDecoRecipes: { type: [typeCountSchema], default: undefined },
TechProjects: { type: [techProjectSchema], default: undefined },
ActiveDojoColorResearch: { type: String, default: "" },
Class: { type: Number, default: 0 },

View File

@ -4,8 +4,7 @@ import { typeCountSchema } from "@/src/models/inventoryModels/inventoryModel";
import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
export interface IMessageClient
extends Omit<IMessageDatabase, "_id" | "date" | "startDate" | "endDate" | "ownerId" | "attVisualOnly" | "expiry"> {
export interface IMessageClient extends Omit<IMessageDatabase, "_id" | "date" | "startDate" | "endDate" | "ownerId"> {
_id?: IOid;
date: IMongoDate;
startDate?: IMongoDate;
@ -16,8 +15,6 @@ export interface IMessageClient
export interface IMessageDatabase extends IMessage {
ownerId: Types.ObjectId;
date: Date; //created at
attVisualOnly?: boolean;
expiry?: Date;
_id: Types.ObjectId;
}
@ -111,7 +108,6 @@ const messageSchema = new Schema<IMessageDatabase>(
att: { type: [String], default: undefined },
gifts: { type: [giftSchema], default: undefined },
countedAtt: { type: [typeCountSchema], default: undefined },
attVisualOnly: Boolean,
transmission: String,
arg: {
type: [
@ -138,14 +134,13 @@ messageSchema.virtual("messageId").get(function (this: IMessageDatabase) {
messageSchema.set("toJSON", {
virtuals: true,
transform(_document, returnedObject) {
delete returnedObject.ownerId;
const messageDatabase = returnedObject as IMessageDatabase;
const messageClient = returnedObject as IMessageClient;
delete returnedObject._id;
delete returnedObject.__v;
delete returnedObject.ownerId;
delete returnedObject.attVisualOnly;
delete returnedObject.expiry;
messageClient.date = toMongoDate(messageDatabase.date);
@ -158,6 +153,5 @@ messageSchema.set("toJSON", {
});
messageSchema.index({ ownerId: 1 });
messageSchema.index({ expiry: 1 }, { expireAfterSeconds: 0 });
export const Inbox = model<IMessageDatabase>("Inbox", messageSchema, "inbox");

View File

@ -39,9 +39,10 @@ import {
ILoreFragmentScan,
IEvolutionProgress,
IEndlessXpProgress,
ICrewShipPortGuns,
ICrewShipCustomization,
ICrewShipWeapon,
ICrewShipWeaponEmplacements,
ICrewShipPilotWeapon,
IShipExterior,
IHelminthFoodRecord,
ICrewShipMembersDatabase,
@ -83,21 +84,7 @@ import {
IInfNode,
IDiscoveredMarker,
IWeeklyMission,
ILockedWeaponGroupDatabase,
IPersonalTechProjectDatabase,
IPersonalTechProjectClient,
ILastSortieRewardDatabase,
ILastSortieRewardClient,
ICrewMemberSkill,
ICrewMemberSkillEfficiency,
ICrewMemberDatabase,
ICrewMemberClient,
ISortieRewardAttenuation,
IInvasionProgressDatabase,
IInvasionProgressClient,
IAccolades,
IHubNpcCustomization,
ILotusCustomization
ILockedWeaponGroupDatabase
} from "../../types/inventoryTypes/inventoryTypes";
import { IOid } from "../../types/commonTypes";
import {
@ -111,7 +98,7 @@ import {
IEquipmentClient
} from "@/src/types/inventoryTypes/commonInventoryTypes";
import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
import { EquipmentSelectionSchema, oidSchema } from "./loadoutModel";
import { EquipmentSelectionSchema } from "./loadoutModel";
export const typeCountSchema = new Schema<ITypeCount>({ ItemType: String, ItemCount: Number }, { _id: false });
@ -303,55 +290,6 @@ upgradeSchema.set("toJSON", {
}
});
const crewMemberSkillSchema = new Schema<ICrewMemberSkill>(
{
Assigned: Number
},
{ _id: false }
);
const crewMemberSkillEfficiencySchema = new Schema<ICrewMemberSkillEfficiency>(
{
PILOTING: crewMemberSkillSchema,
GUNNERY: crewMemberSkillSchema,
ENGINEERING: crewMemberSkillSchema,
COMBAT: crewMemberSkillSchema,
SURVIVABILITY: crewMemberSkillSchema
},
{ _id: false }
);
const crewMemberSchema = new Schema<ICrewMemberDatabase>(
{
ItemType: { type: String, required: true },
NemesisFingerprint: { type: BigInt, default: 0n },
Seed: { type: BigInt, default: 0n },
AssignedRole: Number,
SkillEfficiency: crewMemberSkillEfficiencySchema,
WeaponConfigIdx: Number,
WeaponId: { type: Schema.Types.ObjectId, default: "000000000000000000000000" },
XP: { type: Number, default: 0 },
PowersuitType: { type: String, required: true },
Configs: [ItemConfigSchema],
SecondInCommand: { type: Boolean, default: false }
},
{ id: false }
);
crewMemberSchema.set("toJSON", {
virtuals: true,
transform(_doc, obj) {
const db = obj as ICrewMemberDatabase;
const client = obj as ICrewMemberClient;
client.WeaponId = toOid(db.WeaponId);
client.ItemId = toOid(db._id);
delete obj._id;
delete obj.__v;
}
});
const slotsBinSchema = new Schema<ISlots>(
{
Slots: Number,
@ -391,8 +329,8 @@ MailboxSchema.set("toJSON", {
const DuviriInfoSchema = new Schema<IDuviriInfo>(
{
Seed: { type: BigInt, required: true },
NumCompletions: { type: Number, required: true }
Seed: Number,
NumCompletions: { type: Number, default: 0 }
},
{
_id: false,
@ -560,39 +498,7 @@ const seasonChallengeHistorySchema = new Schema<ISeasonChallenge>(
{ _id: false }
);
const personalTechProjectSchema = new Schema<IPersonalTechProjectDatabase>({
State: Number,
ReqCredits: Number,
ItemType: String,
ProductCategory: String,
CategoryItemId: Schema.Types.ObjectId,
ReqItems: { type: [typeCountSchema], default: undefined },
HasContributions: Boolean,
CompletionDate: Date
});
personalTechProjectSchema.virtual("ItemId").get(function () {
return { $oid: this._id.toString() };
});
personalTechProjectSchema.set("toJSON", {
virtuals: true,
transform(_doc, ret, _options) {
delete ret._id;
delete ret.__v;
const db = ret as IPersonalTechProjectDatabase;
const client = ret as IPersonalTechProjectClient;
if (db.CategoryItemId) {
client.CategoryItemId = toOid(db.CategoryItemId);
}
if (db.CompletionDate) {
client.CompletionDate = toMongoDate(db.CompletionDate);
}
}
});
//TODO: check whether this is complete
const playerSkillsSchema = new Schema<IPlayerSkills>(
{
LPP_SPACE: { type: Number, default: 0 },
@ -689,27 +595,6 @@ questKeysSchema.set("toJSON", {
export const fusionTreasuresSchema = new Schema<IFusionTreasure>().add(typeCountSchema).add({ Sockets: Number });
const invasionProgressSchema = new Schema<IInvasionProgressDatabase>(
{
invasionId: Schema.Types.ObjectId,
Delta: Number,
AttackerScore: Number,
DefenderScore: Number
},
{ _id: false }
);
invasionProgressSchema.set("toJSON", {
transform(_doc, obj) {
const db = obj as IInvasionProgressDatabase;
const client = obj as IInvasionProgressClient;
client._id = toOid(db.invasionId);
delete obj.invasionId;
delete obj.__v;
}
});
const spectreLoadoutsSchema = new Schema<ISpectreLoadout>(
{
ItemType: String,
@ -727,7 +612,6 @@ const spectreLoadoutsSchema = new Schema<ISpectreLoadout>(
const weaponSkinsSchema = new Schema<IWeaponSkinDatabase>(
{
ItemType: String,
Favorite: Boolean,
IsNew: Boolean
},
{ id: false }
@ -781,26 +665,6 @@ const loreFragmentScansSchema = new Schema<ILoreFragmentScan>(
{ _id: false }
);
// const lotusCustomizationSchema = new Schema<ILotusCustomization>().add(ItemConfigSchema).add({
// Persona: String
// });
// Laxer schema for cleanupInventory
const lotusCustomizationSchema = new Schema<ILotusCustomization>(
{
Skins: [String],
pricol: colorSchema,
attcol: Schema.Types.Mixed,
sigcol: Schema.Types.Mixed,
eyecol: Schema.Types.Mixed,
facial: Schema.Types.Mixed,
cloth: Schema.Types.Mixed,
syancol: Schema.Types.Mixed,
Persona: String
},
{ _id: false }
);
const evolutionProgressSchema = new Schema<IEvolutionProgress>(
{
Progress: Number,
@ -818,23 +682,25 @@ const endlessXpProgressSchema = new Schema<IEndlessXpProgress>(
{ _id: false }
);
const crewShipWeaponEmplacementsSchema = new Schema<ICrewShipWeaponEmplacements>(
const crewShipPilotWeaponSchema = new Schema<ICrewShipPilotWeapon>(
{
PRIMARY_A: EquipmentSelectionSchema,
PRIMARY_B: EquipmentSelectionSchema,
SECONDARY_A: EquipmentSelectionSchema,
SECONDARY_B: EquipmentSelectionSchema
SECONDARY_A: EquipmentSelectionSchema
},
{ _id: false }
);
const crewShipPortGunsSchema = new Schema<ICrewShipPortGuns>(
{
PRIMARY_A: EquipmentSelectionSchema
},
{ _id: false }
);
const crewShipWeaponSchema = new Schema<ICrewShipWeapon>(
{
PILOT: crewShipWeaponEmplacementsSchema,
PORT_GUNS: crewShipWeaponEmplacementsSchema,
STARBOARD_GUNS: crewShipWeaponEmplacementsSchema,
ARTILLERY: crewShipWeaponEmplacementsSchema,
SCANNER: crewShipWeaponEmplacementsSchema
PILOT: crewShipPilotWeaponSchema,
PORT_GUNS: crewShipPortGunsSchema
},
{ _id: false }
);
@ -858,7 +724,7 @@ const crewShipCustomizationSchema = new Schema<ICrewShipCustomization>(
const crewShipMemberSchema = new Schema<ICrewShipMemberDatabase>(
{
ItemId: { type: Schema.Types.ObjectId, required: false },
NemesisFingerprint: { type: BigInt, required: false }
NemesisFingerprint: { type: Number, required: false }
},
{ _id: false }
);
@ -907,7 +773,7 @@ const dialogueSchema = new Schema<IDialogueDatabase>(
AvailableGiftDate: Date,
RankUpExpiry: Date,
BountyChemExpiry: Date,
QueuedDialogues: { type: [String], default: [] },
//QueuedDialogues: ???
Gifts: { type: [dialogueGiftSchema], default: [] },
Booleans: { type: [String], default: [] },
Completed: { type: [completedDialogueSchema], default: [] },
@ -930,8 +796,7 @@ dialogueSchema.set("toJSON", {
const dialogueHistorySchema = new Schema<IDialogueHistoryDatabase>(
{
YearIteration: Number,
Resets: Number,
YearIteration: { type: Number, required: true },
Dialogues: { type: [dialogueSchema], required: false }
},
{ _id: false }
@ -1007,14 +872,12 @@ const EquipmentSchema = new Schema<IEquipmentDatabase>(
OffensiveUpgrade: String,
DefensiveUpgrade: String,
UpgradesExpiry: Date,
UmbraDate: Date,
ArchonCrystalUpgrades: { type: [ArchonCrystalUpgradeSchema], default: undefined },
Weapon: crewShipWeaponSchema,
Customization: crewShipCustomizationSchema,
RailjackImage: FlavourItemSchema,
CrewMembers: crewShipMembersSchema,
Details: detailsSchema,
Favorite: Boolean,
IsNew: Boolean
},
{ id: false }
@ -1039,9 +902,6 @@ EquipmentSchema.set("toJSON", {
if (db.UpgradesExpiry) {
client.UpgradesExpiry = toMongoDate(db.UpgradesExpiry);
}
if (db.UmbraDate) {
client.UmbraDate = toMongoDate(db.UmbraDate);
}
}
});
@ -1055,8 +915,6 @@ const pendingRecipeSchema = new Schema<IPendingRecipeDatabase>(
{
ItemType: String,
CompletionDate: Date,
TargetItemId: String,
TargetFingerprint: String,
LongGuns: { type: [EquipmentSchema], default: undefined },
Pistols: { type: [EquipmentSchema], default: undefined },
Melee: { type: [EquipmentSchema], default: undefined },
@ -1084,13 +942,6 @@ pendingRecipeSchema.set("toJSON", {
}
});
const accoladesSchema = new Schema<IAccolades>(
{
Heirloom: Boolean
},
{ _id: false }
);
const infestedFoundrySchema = new Schema<IInfestedFoundryDatabase>(
{
Name: String,
@ -1148,15 +999,15 @@ const CustomMarkersSchema = new Schema<ICustomMarkers>(
const calenderProgressSchema = new Schema<ICalendarProgress>(
{
Version: { type: Number, default: 19 },
Iteration: { type: Number, required: true },
Iteration: { type: Number, default: 2 },
YearProgress: {
Upgrades: { type: [String], default: [] }
Upgrades: { type: [] }
},
SeasonProgress: {
SeasonType: { type: String, required: true },
LastCompletedDayIdx: { type: Number, default: 0 },
LastCompletedChallengeDayIdx: { type: Number, default: 0 },
ActivatedChallenges: { type: [String], default: [] }
SeasonType: String,
LastCompletedDayIdx: { type: Number, default: -1 },
LastCompletedChallengeDayIdx: { type: Number, default: -1 },
ActivatedChallenges: []
}
},
{ _id: false }
@ -1278,11 +1129,11 @@ const nemesisSchema = new Schema<INemesisDatabase>(
PrevOwners: Number,
SecondInCommand: Boolean,
Weakened: Boolean,
InfNodes: { type: [infNodeSchema], default: undefined },
InfNodes: [infNodeSchema],
HenchmenKilled: Number,
HintProgress: Number,
Hints: { type: [Number], default: undefined },
GuessHistory: { type: [Number], default: undefined },
Hints: [Number],
GuessHistory: [Number],
MissionCount: Number,
LastEnc: Number
},
@ -1310,36 +1161,6 @@ const alignmentSchema = new Schema<IAlignment>(
{ _id: false }
);
const lastSortieRewardSchema = new Schema<ILastSortieRewardDatabase>(
{
SortieId: Schema.Types.ObjectId,
StoreItem: String,
Manifest: String
},
{ _id: false }
);
lastSortieRewardSchema.set("toJSON", {
virtuals: true,
transform(_doc, obj) {
const db = obj as ILastSortieRewardDatabase;
const client = obj as ILastSortieRewardClient;
client.SortieId = toOid(db.SortieId);
delete obj._id;
delete obj.__v;
}
});
const sortieRewardAttenutationSchema = new Schema<ISortieRewardAttenuation>(
{
Tag: String,
Atten: Number
},
{ _id: false }
);
const lockedWeaponGroupSchema = new Schema<ILockedWeaponGroupDatabase>(
{
s: Schema.Types.ObjectId,
@ -1351,21 +1172,12 @@ const lockedWeaponGroupSchema = new Schema<ILockedWeaponGroupDatabase>(
{ _id: false }
);
const hubNpcCustomizationSchema = new Schema<IHubNpcCustomization>(
{
Colors: colorSchema,
Pattern: String,
Tag: String
},
{ _id: false }
);
const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
{
accountOwnerId: Schema.Types.ObjectId,
SubscribedToEmails: { type: Number, default: 0 },
SubscribedToEmailsPersonalized: { type: Number, default: 0 },
RewardSeed: BigInt,
RewardSeed: Number,
//Credit
RegularCredits: { type: Number, default: 0 },
@ -1399,7 +1211,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//How many Gift do you have left*(gift spends the trade)
GiftsRemaining: { type: Number, default: 8 },
//Curent trade info Giving or Getting items
//PendingTrades: [Schema.Types.Mixed],
PendingTrades: [Schema.Types.Mixed],
//Syndicate currently being pledged to.
SupportedSyndicate: String,
@ -1449,7 +1261,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
KubrowPetEggs: [kubrowPetEggSchema],
//Prints Cat(3 Prints)\Kubrow(2 Prints) Pets
//KubrowPetPrints: [Schema.Types.Mixed],
KubrowPetPrints: [Schema.Types.Mixed],
//Item for EquippedGear example:Scaner,LoadoutTechSummon etc
Consumables: [typeCountSchema],
@ -1486,7 +1298,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
CrewShipSalvagedWeaponSkins: [upgradeSchema],
//RailJack Crew
CrewMembers: [crewMemberSchema],
CrewMembers: [Schema.Types.Mixed],
//Complete Mission\Quests
Missions: [missionSchema],
@ -1495,7 +1307,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//item like DojoKey or Boss missions key
LevelKeys: [typeCountSchema],
//Active quests
//Quests: [Schema.Types.Mixed],
Quests: [Schema.Types.Mixed],
//Cosmetics like profile glyphs\Kavasa Prime Kubrow Collar\Game Theme etc
FlavourItems: [FlavourItemSchema],
@ -1507,16 +1319,6 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//Mastery Rank next availability
TrainingDate: { type: Date, default: new Date(0) },
//Accolades
Staff: Boolean,
Founder: Number,
Guide: Number,
Moderator: Boolean,
Partner: Boolean,
Accolades: accoladesSchema,
//Not an accolade but unlocks an extra chat
Counselor: Boolean,
//you saw last played Region when you opened the star map
LastRegionPlayed: String,
@ -1534,7 +1336,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
TauntHistory: { type: [tauntSchema], default: undefined },
//noShow2FA,VisitPrimeVault etc
//WebFlags: Schema.Types.Mixed,
WebFlags: Schema.Types.Mixed,
//Id CompletedAlerts
CompletedAlerts: [String],
@ -1554,9 +1356,9 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//the color your clan requests like Items/Research/DojoColors/DojoColorPlainsB
ActiveDojoColorResearch: String,
//SentientSpawnChanceBoosters: Schema.Types.Mixed,
SentientSpawnChanceBoosters: Schema.Types.Mixed,
QualifyingInvasions: [invasionProgressSchema],
QualifyingInvasions: [Schema.Types.Mixed],
FactionScores: [Number],
// https://warframe.fandom.com/wiki/Specter_(Tenno)
@ -1576,9 +1378,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//https://warframe.fandom.com/wiki/Sortie
CompletedSorties: [String],
LastSortieReward: { type: [lastSortieRewardSchema], default: undefined },
LastLiteSortieReward: { type: [lastSortieRewardSchema], default: undefined },
SortieRewardAttenuation: { type: [sortieRewardAttenutationSchema], default: undefined },
LastSortieReward: [Schema.Types.Mixed],
// Resource Extractor Drones
Drones: [droneSchema],
@ -1589,10 +1389,10 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
// open location store like EidolonPlainsDiscoverable or OrbVallisCaveDiscoverable
DiscoveredMarkers: [discoveredMarkerSchema],
//Open location mission like "JobId" + "StageCompletions"
//CompletedJobs: [Schema.Types.Mixed],
CompletedJobs: [Schema.Types.Mixed],
//Game mission\ivent score example "Tag": "WaterFight", "Best": 170, "Count": 1258,
//PersonalGoalProgress: [Schema.Types.Mixed],
PersonalGoalProgress: [Schema.Types.Mixed],
//Setting interface Style
ThemeStyle: String,
@ -1610,7 +1410,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//https://warframe.fandom.com/wiki/Heist
//ProfitTaker(1-4) Example:"LocationTag": "EudicoHeists", "Jobs":Mission name
CompletedJobChains: { type: [completedJobChainsSchema], default: undefined },
CompletedJobChains: [completedJobChainsSchema],
//Night Wave Challenge
SeasonChallengeHistory: [seasonChallengeHistorySchema],
@ -1622,27 +1422,27 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
LibraryActiveDailyTaskInfo: libraryDailyTaskInfoSchema,
//https://warframe.fandom.com/wiki/Invasion
//InvasionChainProgress: [Schema.Types.Mixed],
InvasionChainProgress: [Schema.Types.Mixed],
//CorpusLich or GrineerLich
NemesisAbandonedRewards: { type: [String], default: [] },
Nemesis: nemesisSchema,
NemesisHistory: { type: [nemesisSchema], default: undefined },
//LastNemesisAllySpawnTime: Schema.Types.Mixed,
NemesisHistory: [Schema.Types.Mixed],
LastNemesisAllySpawnTime: Schema.Types.Mixed,
//TradingRulesConfirmed,ShowFriendInvNotifications(Option->Social)
Settings: settingsSchema,
//Railjack craft
//https://warframe.fandom.com/wiki/Rising_Tide
PersonalTechProjects: { type: [personalTechProjectSchema], default: [] },
PersonalTechProjects: [Schema.Types.Mixed],
//Modulars lvl and exp(Railjack|Duviri)
//https://warframe.fandom.com/wiki/Intrinsics
PlayerSkills: { type: playerSkillsSchema, default: {} },
//TradeBannedUntil data
//TradeBannedUntil: Schema.Types.Mixed,
TradeBannedUntil: Schema.Types.Mixed,
//https://warframe.fandom.com/wiki/Helminth
InfestedFoundry: infestedFoundrySchema,
@ -1651,7 +1451,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//Purchase this new permanent skin from the Lotus customization options in Personal Quarters located in your Orbiter.
//https://warframe.fandom.com/wiki/Lotus#The_New_War
LotusCustomization: { type: lotusCustomizationSchema, default: undefined },
LotusCustomization: Schema.Types.Mixed,
//Progress+Rank+ItemType(ZarimanPumpShotgun)
//https://warframe.fandom.com/wiki/Incarnon
@ -1662,24 +1462,23 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//Unknown and system
DuviriInfo: DuviriInfoSchema,
LastInventorySync: Schema.Types.ObjectId,
Mailbox: MailboxSchema,
HandlerPoints: Number,
ChallengesFixVersion: { type: Number, default: 6 },
ChallengesFixVersion: Number,
PlayedParkourTutorial: Boolean,
//ActiveLandscapeTraps: [Schema.Types.Mixed],
//RepVotes: [Schema.Types.Mixed],
//LeagueTickets: [Schema.Types.Mixed],
ActiveLandscapeTraps: [Schema.Types.Mixed],
RepVotes: [Schema.Types.Mixed],
LeagueTickets: [Schema.Types.Mixed],
HasContributedToDojo: Boolean,
HWIDProtectEnabled: Boolean,
LoadOutPresets: { type: Schema.Types.ObjectId, ref: "Loadout" },
CurrentLoadOutIds: [oidSchema],
CurrentLoadOutIds: [Schema.Types.Mixed],
RandomUpgradesIdentified: Number,
BountyScore: Number,
//ChallengeInstanceStates: [Schema.Types.Mixed],
ChallengeInstanceStates: [Schema.Types.Mixed],
RecentVendorPurchases: { type: [recentVendorPurchaseSchema], default: undefined },
//Robotics: [Schema.Types.Mixed],
//UsedDailyDeals: [Schema.Types.Mixed],
Robotics: [Schema.Types.Mixed],
UsedDailyDeals: [Schema.Types.Mixed],
CollectibleSeries: { type: [collectibleEntrySchema], default: undefined },
HasResetAccount: { type: Boolean, default: false },
@ -1688,9 +1487,9 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//Like BossAladV,BossCaptainVor come for you on missions % chance
DeathMarks: { type: [String], default: [] },
//Zanuka
Harvestable: { type: Boolean, default: true },
Harvestable: Boolean,
//Grustag three
DeathSquadable: { type: Boolean, default: true },
DeathSquadable: Boolean,
EndlessXP: { type: [endlessXpProgressSchema], default: undefined },
@ -1714,9 +1513,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
// G3 + Zanuka
BrandedSuits: { type: [Schema.Types.ObjectId], default: undefined },
LockedWeaponGroup: { type: lockedWeaponGroupSchema, default: undefined },
HubNpcCustomizations: { type: [hubNpcCustomizationSchema], default: undefined }
LockedWeaponGroup: { type: lockedWeaponGroupSchema, default: undefined }
},
{ timestamps: { createdAt: "Created", updatedAt: false } }
);
@ -1760,9 +1557,6 @@ inventorySchema.set("toJSON", {
sn: inventoryDatabase.LockedWeaponGroup.sn ? toOid(inventoryDatabase.LockedWeaponGroup.sn) : undefined
};
}
if (inventoryDatabase.LastInventorySync) {
inventoryResponse.LastInventorySync = toOid(inventoryDatabase.LastInventorySync);
}
}
});
@ -1783,9 +1577,7 @@ export type InventoryDocumentProps = {
QuestKeys: Types.DocumentArray<IQuestKeyDatabase>;
Drones: Types.DocumentArray<IDroneDatabase>;
CrewShipWeaponSkins: Types.DocumentArray<IUpgradeDatabase>;
CrewShipSalvagedWeaponSkins: Types.DocumentArray<IUpgradeDatabase>;
PersonalTechProjects: Types.DocumentArray<IPersonalTechProjectDatabase>;
CrewMembers: Types.DocumentArray<ICrewMemberDatabase>;
CrewShipSalvagedWeaponsSkins: Types.DocumentArray<IUpgradeDatabase>;
} & { [K in TEquipmentKey]: Types.DocumentArray<IEquipmentDatabase> };
// eslint-disable-next-line @typescript-eslint/no-empty-object-type

View File

@ -3,7 +3,7 @@ import { IEquipmentSelection } from "@/src/types/inventoryTypes/commonInventoryT
import { ILoadoutConfigDatabase, ILoadoutDatabase } from "@/src/types/saveLoadoutTypes";
import { Document, Model, Schema, Types, model } from "mongoose";
export const oidSchema = new Schema<IOid>(
const oidSchema = new Schema<IOid>(
{
$oid: String
},

View File

@ -1,4 +1,4 @@
import { IDatabaseAccountJson, IIgnore } from "@/src/types/loginTypes";
import { IDatabaseAccountJson } from "@/src/types/loginTypes";
import { model, Schema, SchemaOptions } from "mongoose";
const opts = {
@ -37,13 +37,3 @@ databaseAccountSchema.set("toJSON", {
});
export const Account = model<IDatabaseAccountJson>("Account", databaseAccountSchema);
const ignoreSchema = new Schema<IIgnore>({
ignorer: Schema.Types.ObjectId,
ignoree: Schema.Types.ObjectId
});
ignoreSchema.index({ ignorer: 1 });
ignoreSchema.index({ ignorer: 1, ignoree: 1 }, { unique: true });
export const Ignore = model<IIgnore>("Ignore", ignoreSchema);

View File

@ -1,17 +1,14 @@
import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
import { toOid } from "@/src/helpers/inventoryHelpers";
import { colorSchema } from "@/src/models/inventoryModels/inventoryModel";
import { IOrbiter, IPersonalRoomsDatabase, PersonalRoomsModelType } from "@/src/types/personalRoomsTypes";
import {
IApartment,
IFavouriteLoadoutDatabase,
IGardeningDatabase,
IGardening,
IPlacedDecosDatabase,
IPictureFrameInfo,
IRoom,
ITailorShopDatabase,
IApartmentDatabase,
IPlanterDatabase,
IPlantDatabase,
IPlantClient
ITailorShopDatabase
} from "@/src/types/shipTypes";
import { Schema, model } from "mongoose";
@ -65,64 +62,19 @@ const roomSchema = new Schema<IRoom>(
{ _id: false }
);
const favouriteLoadoutSchema = new Schema<IFavouriteLoadoutDatabase>(
{
Tag: String,
LoadoutId: Schema.Types.ObjectId
},
{ _id: false }
);
favouriteLoadoutSchema.set("toJSON", {
virtuals: true,
transform(_document, returnedObject) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
returnedObject.LoadoutId = toOid(returnedObject.LoadoutId);
}
const gardeningSchema = new Schema<IGardening>({
Planters: [Schema.Types.Mixed] //TODO: add when implementing gardening
});
const plantSchema = new Schema<IPlantDatabase>(
{
PlantType: String,
EndTime: Date,
PlotIndex: Number
},
{ _id: false }
);
plantSchema.set("toJSON", {
virtuals: true,
transform(_doc, obj) {
const client = obj as IPlantClient;
const db = obj as IPlantDatabase;
client.EndTime = toMongoDate(db.EndTime);
}
});
const planterSchema = new Schema<IPlanterDatabase>(
{
Name: { type: String, required: true },
Plants: { type: [plantSchema], default: [] }
},
{ _id: false }
);
const gardeningSchema = new Schema<IGardeningDatabase>(
{
Planters: { type: [planterSchema], default: [] }
},
{ _id: false }
);
const apartmentSchema = new Schema<IApartmentDatabase>(
const apartmentSchema = new Schema<IApartment>(
{
Rooms: [roomSchema],
FavouriteLoadouts: [favouriteLoadoutSchema],
Gardening: gardeningSchema
FavouriteLoadouts: [Schema.Types.Mixed],
Gardening: gardeningSchema // TODO: ensure this is correct
},
{ _id: false }
);
const apartmentDefault: IApartmentDatabase = {
const apartmentDefault: IApartment = {
Rooms: [
{ Name: "ElevatorLanding", MaxCapacity: 1600 },
{ Name: "ApartmentRoomA", MaxCapacity: 1000 },
@ -131,19 +83,13 @@ const apartmentDefault: IApartmentDatabase = {
{ Name: "DuviriHallway", MaxCapacity: 1600 }
],
FavouriteLoadouts: [],
Gardening: {
Planters: []
}
Gardening: {}
};
const orbiterSchema = new Schema<IOrbiter>(
{
Features: [String],
Rooms: [roomSchema],
VignetteFish: { type: [String], default: undefined },
FavouriteLoadoutId: Schema.Types.ObjectId,
Wallpaper: String,
Vignette: String,
ContentUrlSignature: { type: String, required: false },
BootLocation: String
},
@ -161,6 +107,21 @@ const orbiterDefault: IOrbiter = {
]
};
const favouriteLoadoutSchema = new Schema<IFavouriteLoadoutDatabase>(
{
Tag: String,
LoadoutId: Schema.Types.ObjectId
},
{ _id: false }
);
favouriteLoadoutSchema.set("toJSON", {
virtuals: true,
transform(_document, returnedObject) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
returnedObject.LoadoutId = toOid(returnedObject.LoadoutId);
}
});
const tailorShopSchema = new Schema<ITailorShopDatabase>(
{
FavouriteLoadouts: [favouriteLoadoutSchema],

View File

@ -4,7 +4,6 @@ import { abortDojoComponentController } from "@/src/controllers/api/abortDojoCom
import { abortDojoComponentDestructionController } from "@/src/controllers/api/abortDojoComponentDestructionController";
import { activateRandomModController } from "@/src/controllers/api/activateRandomModController";
import { addFriendImageController } from "@/src/controllers/api/addFriendImageController";
import { addIgnoredUserController } from "@/src/controllers/api/addIgnoredUserController";
import { addToAllianceController } from "@/src/controllers/api/addToAllianceController";
import { addToGuildController } from "@/src/controllers/api/addToGuildController";
import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController";
@ -19,7 +18,6 @@ import { claimCompletedRecipeController } from "@/src/controllers/api/claimCompl
import { claimLibraryDailyTaskRewardController } from "@/src/controllers/api/claimLibraryDailyTaskRewardController";
import { clearDialogueHistoryController } from "@/src/controllers/api/clearDialogueHistoryController";
import { clearNewEpisodeRewardController } from "@/src/controllers/api/clearNewEpisodeRewardController";
import { completeCalendarEventController } from "@/src/controllers/api/completeCalendarEventController";
import { completeRandomModChallengeController } from "@/src/controllers/api/completeRandomModChallengeController";
import { confirmAllianceInvitationController } from "@/src/controllers/api/confirmAllianceInvitationController";
import { confirmGuildInvitationGetController, confirmGuildInvitationPostController } from "@/src/controllers/api/confirmGuildInvitationController";
@ -29,8 +27,6 @@ import { contributeToVaultController } from "@/src/controllers/api/contributeToV
import { createAllianceController } from "@/src/controllers/api/createAllianceController";
import { createGuildController } from "@/src/controllers/api/createGuildController";
import { creditsController } from "@/src/controllers/api/creditsController";
import { crewMembersController } from "@/src/controllers/api/crewMembersController";
import { crewShipIdentifySalvageController } from "@/src/controllers/api/crewShipIdentifySalvageController";
import { customizeGuildRanksController } from "@/src/controllers/api/customizeGuildRanksController";
import { customObstacleCourseLeaderboardController } from "@/src/controllers/api/customObstacleCourseLeaderboardController";
import { declineAllianceInviteController } from "@/src/controllers/api/declineAllianceInviteController";
@ -39,7 +35,7 @@ import { deleteSessionController } from "@/src/controllers/api/deleteSessionCont
import { destroyDojoDecoController } from "@/src/controllers/api/destroyDojoDecoController";
import { divvyAllianceVaultController } from "@/src/controllers/api/divvyAllianceVaultController";
import { dojoComponentRushController } from "@/src/controllers/api/dojoComponentRushController";
import { dojoController, setDojoURLController } from "@/src/controllers/api/dojoController";
import { dojoController } from "@/src/controllers/api/dojoController";
import { dronesController } from "@/src/controllers/api/dronesController";
import { endlessXpController } from "@/src/controllers/api/endlessXpController";
import { entratiLabConquestModeController } from "@/src/controllers/api/entratiLabConquestModeController";
@ -48,7 +44,6 @@ import { findSessionsController } from "@/src/controllers/api/findSessionsContro
import { fishmongerController } from "@/src/controllers/api/fishmongerController";
import { focusController } from "@/src/controllers/api/focusController";
import { fusionTreasuresController } from "@/src/controllers/api/fusionTreasuresController";
import { gardeningController } from "@/src/controllers/api/gardeningController";
import { genericUpdateController } from "@/src/controllers/api/genericUpdateController";
import { getAllianceController } from "@/src/controllers/api/getAllianceController";
import { getDailyDealStockLevelsController } from "@/src/controllers/api/getDailyDealStockLevelsController";
@ -66,8 +61,7 @@ import { giftingController } from "@/src/controllers/api/giftingController";
import { gildWeaponController } from "@/src/controllers/api/gildWeaponController";
import { giveKeyChainTriggeredItemsController } from "@/src/controllers/api/giveKeyChainTriggeredItemsController";
import { giveKeyChainTriggeredMessageController } from "@/src/controllers/api/giveKeyChainTriggeredMessageController";
import { giveQuestKeyRewardController } from "@/src/controllers/api/giveQuestKeyRewardController";
import { giveShipDecoAndLoreFragmentController } from "@/src/controllers/api/giveShipDecoAndLoreFragmentController";
import { giveQuestKeyRewardController } from "@/src/controllers/api/giveQuestKey";
import { giveStartingGearController } from "@/src/controllers/api/giveStartingGearController";
import { guildTechController } from "@/src/controllers/api/guildTechController";
import { hostSessionController } from "@/src/controllers/api/hostSessionController";
@ -91,7 +85,6 @@ import { modularWeaponSaleController } from "@/src/controllers/api/modularWeapon
import { nameWeaponController } from "@/src/controllers/api/nameWeaponController";
import { nemesisController } from "@/src/controllers/api/nemesisController";
import { placeDecoInComponentController } from "@/src/controllers/api/placeDecoInComponentController";
import { playedParkourTutorialController } from "@/src/controllers/api/playedParkourTutorialController";
import { playerSkillsController } from "@/src/controllers/api/playerSkillsController";
import { postGuildAdvertisementController } from "@/src/controllers/api/postGuildAdvertisementController";
import { projectionManagerController } from "@/src/controllers/api/projectionManagerController";
@ -101,29 +94,23 @@ import { redeemPromoCodeController } from "@/src/controllers/api/redeemPromoCode
import { releasePetController } from "@/src/controllers/api/releasePetController";
import { removeFromAllianceController } from "@/src/controllers/api/removeFromAllianceController";
import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController";
import { removeIgnoredUserController } from "@/src/controllers/api/removeIgnoredUserController";
import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController";
import { retrievePetFromStasisController } from "@/src/controllers/api/retrievePetFromStasisController";
import { saveDialogueController } from "@/src/controllers/api/saveDialogueController";
import { saveLoadoutController } from "@/src/controllers/api/saveLoadoutController";
import { saveLoadoutController } from "@/src/controllers/api/saveLoadout";
import { saveSettingsController } from "@/src/controllers/api/saveSettingsController";
import { saveVaultAutoContributeController } from "@/src/controllers/api/saveVaultAutoContributeController";
import { sellController } from "@/src/controllers/api/sellController";
import { sendMsgToInBoxController } from "@/src/controllers/api/sendMsgToInBoxController";
import { setActiveQuestController } from "@/src/controllers/api/setActiveQuestController";
import { setActiveShipController } from "@/src/controllers/api/setActiveShipController";
import { setAllianceGuildPermissionsController } from "@/src/controllers/api/setAllianceGuildPermissionsController";
import { setBootLocationController } from "@/src/controllers/api/setBootLocationController";
import { setDojoComponentColorsController } from "@/src/controllers/api/setDojoComponentColorsController";
import { setDojoComponentMessageController } from "@/src/controllers/api/setDojoComponentMessageController";
import { setDojoComponentSettingsController } from "@/src/controllers/api/setDojoComponentSettingsController";
import { setEquippedInstrumentController } from "@/src/controllers/api/setEquippedInstrumentController";
import { setGuildMotdController } from "@/src/controllers/api/setGuildMotdController";
import { setHubNpcCustomizationsController } from "@/src/controllers/api/setHubNpcCustomizationsController";
import { setPlacedDecoInfoController } from "@/src/controllers/api/setPlacedDecoInfoController";
import { setShipCustomizationsController } from "@/src/controllers/api/setShipCustomizationsController";
import { setShipFavouriteLoadoutController } from "@/src/controllers/api/setShipFavouriteLoadoutController";
import { setShipVignetteController } from "@/src/controllers/api/setShipVignetteController";
import { setSupportedSyndicateController } from "@/src/controllers/api/setSupportedSyndicateController";
import { setWeaponSkillTreeController } from "@/src/controllers/api/setWeaponSkillTreeController";
import { shipDecorationsController } from "@/src/controllers/api/shipDecorationsController";
@ -156,11 +143,9 @@ const apiRouter = express.Router();
apiRouter.get("/abandonLibraryDailyTask.php", abandonLibraryDailyTaskController);
apiRouter.get("/abortDojoComponentDestruction.php", abortDojoComponentDestructionController);
apiRouter.get("/cancelGuildAdvertisement.php", cancelGuildAdvertisementController);
apiRouter.get("/changeDojoRoot.php", changeDojoRootController);
apiRouter.get("/changeGuildRank.php", changeGuildRankController);
apiRouter.get("/checkDailyMissionBonus.php", checkDailyMissionBonusController);
apiRouter.get("/claimLibraryDailyTaskReward.php", claimLibraryDailyTaskRewardController);
apiRouter.get("/completeCalendarEvent.php", completeCalendarEventController);
apiRouter.get("/confirmAllianceInvitation.php", confirmAllianceInvitationController);
apiRouter.get("/confirmGuildInvitation.php", confirmGuildInvitationGetController);
apiRouter.get("/credits.php", creditsController);
@ -189,14 +174,12 @@ apiRouter.get("/logout.php", logoutController);
apiRouter.get("/marketRecommendations.php", marketRecommendationsController);
apiRouter.get("/marketSearchRecommendations.php", marketRecommendationsController);
apiRouter.get("/modularWeaponSale.php", modularWeaponSaleController);
apiRouter.get("/playedParkourTutorial.php", playedParkourTutorialController);
apiRouter.get("/queueDojoComponentDestruction.php", queueDojoComponentDestructionController);
apiRouter.get("/removeFromAlliance.php", removeFromAllianceController);
apiRouter.get("/setActiveQuest.php", setActiveQuestController);
apiRouter.get("/setActiveShip.php", setActiveShipController);
apiRouter.get("/setAllianceGuildPermissions.php", setAllianceGuildPermissionsController);
apiRouter.get("/setBootLocation.php", setBootLocationController);
apiRouter.get("/setDojoURL", setDojoURLController);
apiRouter.get("/setGuildMotd.php", setGuildMotdController);
apiRouter.get("/setSupportedSyndicate.php", setSupportedSyndicateController);
apiRouter.get("/startLibraryDailyTask.php", startLibraryDailyTaskController);
@ -209,7 +192,6 @@ apiRouter.get("/updateSession.php", updateSessionGetController);
apiRouter.post("/abortDojoComponent.php", abortDojoComponentController);
apiRouter.post("/activateRandomMod.php", activateRandomModController);
apiRouter.post("/addFriendImage.php", addFriendImageController);
apiRouter.post("/addIgnoredUser.php", addIgnoredUserController);
apiRouter.post("/addToAlliance.php", addToAllianceController);
apiRouter.post("/addToGuild.php", addToGuildController);
apiRouter.post("/arcaneCommon.php", arcaneCommonController);
@ -227,8 +209,6 @@ apiRouter.post("/contributeToDojoComponent.php", contributeToDojoComponentContro
apiRouter.post("/contributeToVault.php", contributeToVaultController);
apiRouter.post("/createAlliance.php", createAllianceController);
apiRouter.post("/createGuild.php", createGuildController);
apiRouter.post("/crewMembers.php", crewMembersController);
apiRouter.post("/crewShipIdentifySalvage.php", crewShipIdentifySalvageController);
apiRouter.post("/customizeGuildRanks.php", customizeGuildRanksController);
apiRouter.post("/customObstacleCourseLeaderboard.php", customObstacleCourseLeaderboardController);
apiRouter.post("/destroyDojoDeco.php", destroyDojoDecoController);
@ -241,7 +221,6 @@ apiRouter.post("/findSessions.php", findSessionsController);
apiRouter.post("/fishmonger.php", fishmongerController);
apiRouter.post("/focus.php", focusController);
apiRouter.post("/fusionTreasures.php", fusionTreasuresController);
apiRouter.post("/gardening.php", gardeningController);
apiRouter.post("/genericUpdate.php", genericUpdateController);
apiRouter.post("/getAlliance.php", getAllianceController);
apiRouter.post("/getFriends.php", getFriendsController);
@ -252,7 +231,6 @@ apiRouter.post("/gildWeapon.php", gildWeaponController);
apiRouter.post("/giveKeyChainTriggeredItems.php", giveKeyChainTriggeredItemsController);
apiRouter.post("/giveKeyChainTriggeredMessage.php", giveKeyChainTriggeredMessageController);
apiRouter.post("/giveQuestKeyReward.php", giveQuestKeyRewardController);
apiRouter.post("/giveShipDecoAndLoreFragment.php", giveShipDecoAndLoreFragmentController);
apiRouter.post("/giveStartingGear.php", giveStartingGearController);
apiRouter.post("/guildTech.php", guildTechController);
apiRouter.post("/hostSession.php", hostSessionController);
@ -276,7 +254,6 @@ apiRouter.post("/purchase.php", purchaseController);
apiRouter.post("/redeemPromoCode.php", redeemPromoCodeController);
apiRouter.post("/releasePet.php", releasePetController);
apiRouter.post("/removeFromGuild.php", removeFromGuildController);
apiRouter.post("/removeIgnoredUser.php", removeIgnoredUserController);
apiRouter.post("/rerollRandomMod.php", rerollRandomModController);
apiRouter.post("/retrievePetFromStasis.php", retrievePetFromStasisController);
apiRouter.post("/saveDialogue.php", saveDialogueController);
@ -284,17 +261,12 @@ apiRouter.post("/saveLoadout.php", saveLoadoutController);
apiRouter.post("/saveSettings.php", saveSettingsController);
apiRouter.post("/saveVaultAutoContribute.php", saveVaultAutoContributeController);
apiRouter.post("/sell.php", sellController);
apiRouter.post("/sendMsgToInBox.php", sendMsgToInBoxController);
apiRouter.post("/setDojoComponentColors.php", setDojoComponentColorsController);
apiRouter.post("/setDojoComponentMessage.php", setDojoComponentMessageController);
apiRouter.post("/setDojoComponentSettings.php", setDojoComponentSettingsController);
apiRouter.post("/setEquippedInstrument.php", setEquippedInstrumentController);
apiRouter.post("/setGuildMotd.php", setGuildMotdController);
apiRouter.post("/setHubNpcCustomizations.php", setHubNpcCustomizationsController);
apiRouter.post("/setPlacedDecoInfo.php", setPlacedDecoInfoController);
apiRouter.post("/setShipCustomizations.php", setShipCustomizationsController);
apiRouter.post("/setShipFavouriteLoadout.php", setShipFavouriteLoadoutController);
apiRouter.post("/setShipVignette.php", setShipVignetteController);
apiRouter.post("/setWeaponSkillTree.php", setWeaponSkillTreeController);
apiRouter.post("/shipDecorations.php", shipDecorationsController);
apiRouter.post("/startCollectibleEntry.php", startCollectibleEntryController);

View File

@ -10,19 +10,19 @@ import { getAccountInfoController } from "@/src/controllers/custom/getAccountInf
import { renameAccountController } from "@/src/controllers/custom/renameAccountController";
import { ircDroppedController } from "@/src/controllers/custom/ircDroppedController";
import { unlockAllIntrinsicsController } from "@/src/controllers/custom/unlockAllIntrinsicsController";
import { addMissingMaxRankModsController } from "@/src/controllers/custom/addMissingMaxRankModsController";
import { createAccountController } from "@/src/controllers/custom/createAccountController";
import { createMessageController } from "@/src/controllers/custom/createMessageController";
import { addCurrencyController } from "@/src/controllers/custom/addCurrencyController";
import { addItemsController } from "@/src/controllers/custom/addItemsController";
import { addModularEquipmentController } from "@/src/controllers/custom/addModularEquipmentController";
import { addXpController } from "@/src/controllers/custom/addXpController";
import { gildEquipmentController } from "@/src/controllers/custom/gildEquipmentController";
import { importController } from "@/src/controllers/custom/importController";
import { manageQuestsController } from "@/src/controllers/custom/manageQuestsController";
import { setEvolutionProgressController } from "@/src/controllers/custom/setEvolutionProgressController";
import { getConfigDataController } from "@/src/controllers/custom/getConfigDataController";
import { updateConfigDataController } from "@/src/controllers/custom/updateConfigDataController";
import { manageQuestsController } from "@/src/controllers/custom/manageQuestsController";
const customRouter = express.Router();
@ -36,16 +36,16 @@ customRouter.get("/getAccountInfo", getAccountInfoController);
customRouter.get("/renameAccount", renameAccountController);
customRouter.get("/ircDropped", ircDroppedController);
customRouter.get("/unlockAllIntrinsics", unlockAllIntrinsicsController);
customRouter.get("/addMissingMaxRankMods", addMissingMaxRankModsController);
customRouter.post("/createAccount", createAccountController);
customRouter.post("/createMessage", createMessageController);
customRouter.post("/addCurrency", addCurrencyController);
customRouter.post("/addItems", addItemsController);
customRouter.post("/addModularEquipment", addModularEquipmentController);
customRouter.post("/addXp", addXpController);
customRouter.post("/gildEquipment", gildEquipmentController);
customRouter.post("/import", importController);
customRouter.post("/manageQuests", manageQuestsController);
customRouter.post("/setEvolutionProgress", setEvolutionProgressController);
customRouter.get("/config", getConfigDataController);
customRouter.post("/config", updateConfigDataController);

View File

@ -24,29 +24,21 @@ interface IConfig {
infiniteEndo?: boolean;
infiniteRegalAya?: boolean;
infiniteHelminthMaterials?: boolean;
dontSubtractConsumables?: boolean;
unlockAllShipFeatures?: boolean;
unlockAllShipDecorations?: boolean;
unlockAllFlavourItems?: boolean;
unlockAllSkins?: boolean;
unlockAllCapturaScenes?: boolean;
unlockAllDecoRecipes?: boolean;
universalPolarityEverywhere?: boolean;
unlockDoubleCapacityPotatoesEverywhere?: boolean;
unlockExilusEverywhere?: boolean;
unlockArcanesEverywhere?: boolean;
noDailyStandingLimits?: boolean;
noDailyFocusLimit?: boolean;
noArgonCrystalDecay?: boolean;
noMasteryRankUpCooldown?: boolean;
noVendorPurchaseLimits?: boolean;
noDeathMarks?: boolean;
noKimCooldowns?: boolean;
instantResourceExtractorDrones?: boolean;
noResourceExtractorDronesDamage?: boolean;
skipClanKeyCrafting?: boolean;
noDojoRoomBuildStage?: boolean;
noDojoDecoBuildStage?: boolean;
fastDojoRoomDestruction?: boolean;
noDojoResearchCosts?: boolean;
noDojoResearchTime?: boolean;

View File

@ -1,6 +1,6 @@
import { Request } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addLevelKeys, addRecipes, combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
import { getInventory } from "@/src/services/inventoryService";
import { Alliance, AllianceMember, Guild, GuildAd, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import {
@ -30,8 +30,6 @@ import { Inbox } from "../models/inboxModel";
import { IFusionTreasure, ITypeCount } from "../types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "../types/purchaseTypes";
import { parallelForeach } from "../utils/async-utils";
import allDecoRecipes from "@/static/fixed_responses/allDecoRecipes.json";
import { createMessage } from "./inboxService";
export const getGuildForRequest = async (req: Request): Promise<TGuildDatabaseDocument> => {
const accountId = await getAccountIdForRequest(req);
@ -59,7 +57,6 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s
const members: IGuildMemberClient[] = [];
let missingEntry = true;
const dataFillInPromises: Promise<void>[] = [];
for (const guildMember of guildMembers) {
const member: IGuildMemberClient = {
_id: toOid(guildMember.accountId),
@ -71,12 +68,8 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s
if (guildMember.accountId.equals(accountId)) {
missingEntry = false;
} else {
dataFillInPromises.push(
(async (): Promise<void> => {
member.DisplayName = (await Account.findById(guildMember.accountId, "DisplayName"))!.DisplayName;
})()
);
dataFillInPromises.push(fillInInventoryDataForGuildMember(member));
member.DisplayName = (await Account.findById(guildMember.accountId, "DisplayName"))!.DisplayName;
await fillInInventoryDataForGuildMember(member);
}
members.push(member);
}
@ -95,8 +88,6 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s
});
}
await Promise.all(dataFillInPromises);
return {
_id: toOid(guild._id),
Name: guild.Name,
@ -104,8 +95,8 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s
LongMOTD: guild.LongMOTD,
Members: members,
Ranks: guild.Ranks,
TradeTax: guild.TradeTax,
Tier: guild.Tier,
Emblem: guild.Emblem,
Vault: getGuildVault(guild),
ActiveDojoColorResearch: guild.ActiveDojoColorResearch,
Class: guild.Class,
@ -113,7 +104,6 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s
IsContributor: !!guild.CeremonyContributors?.find(x => x.equals(accountId)),
NumContributors: guild.CeremonyContributors?.length ?? 0,
CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined,
AutoContributeFromVault: guild.AutoContributeFromVault,
AllianceId: guild.AllianceId ? toOid(guild.AllianceId) : undefined
};
};
@ -124,26 +114,19 @@ export const getGuildVault = (guild: TGuildDatabaseDocument): IGuildVault => {
DojoRefundMiscItems: guild.VaultMiscItems,
DojoRefundPremiumCredits: guild.VaultPremiumCredits,
ShipDecorations: guild.VaultShipDecorations,
FusionTreasures: guild.VaultFusionTreasures,
DecoRecipes: config.unlockAllDecoRecipes
? allDecoRecipes.map(recipe => ({ ItemType: recipe, ItemCount: 1 }))
: guild.VaultDecoRecipes
FusionTreasures: guild.VaultFusionTreasures
};
};
export const getDojoClient = async (
guild: TGuildDatabaseDocument,
status: number,
componentId?: Types.ObjectId | string
componentId: Types.ObjectId | string | undefined = undefined
): Promise<IDojoClient> => {
const dojo: IDojoClient = {
_id: { $oid: guild._id.toString() },
Name: guild.Name,
Tier: guild.Tier,
GuildEmblem: guild.Emblem,
TradeTax: guild.TradeTax,
NumContributors: guild.CeremonyContributors?.length ?? 0,
CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined,
Tier: 1,
FixedContributions: true,
DojoRevision: 1,
Vault: getGuildVault(guild),
@ -154,19 +137,16 @@ export const getDojoClient = async (
DojoComponents: []
};
const roomsToRemove: Types.ObjectId[] = [];
const decosToRemoveNoRefund: { componentId: Types.ObjectId; decoId: Types.ObjectId }[] = [];
let needSave = false;
for (const dojoComponent of guild.DojoComponents) {
if (!componentId || dojoComponent._id.equals(componentId)) {
const clientComponent: IDojoComponentClient = {
id: toOid(dojoComponent._id),
SortId: toOid(dojoComponent.SortId ?? dojoComponent._id), // always providing a SortId so decos don't need repositioning to reparent
pf: dojoComponent.pf,
ppf: dojoComponent.ppf,
Name: dojoComponent.Name,
Message: dojoComponent.Message,
DecoCapacity: dojoComponent.DecoCapacity ?? 600,
Settings: dojoComponent.Settings
DecoCapacity: dojoComponent.DecoCapacity ?? 600
};
if (dojoComponent.pi) {
clientComponent.pi = toOid(dojoComponent.pi);
@ -223,27 +203,11 @@ export const getDojoClient = async (
Type: deco.Type,
Pos: deco.Pos,
Rot: deco.Rot,
Scale: deco.Scale,
Name: deco.Name,
Sockets: deco.Sockets,
PictureFrameInfo: deco.PictureFrameInfo
};
if (deco.CompletionTime) {
if (
deco.Type == "/Lotus/Objects/Tenno/Props/TnoPaintBotDojoDeco" &&
Date.now() >= deco.CompletionTime.getTime()
) {
if (dojoComponent.PendingColors) {
dojoComponent.Colors = dojoComponent.PendingColors;
dojoComponent.PendingColors = undefined;
}
if (dojoComponent.PendingLights) {
dojoComponent.Lights = dojoComponent.PendingLights;
dojoComponent.PendingLights = undefined;
}
decosToRemoveNoRefund.push({ componentId: dojoComponent._id, decoId: deco._id });
continue;
}
clientDeco.CompletionTime = toMongoDate(deco.CompletionTime);
} else {
clientDeco.RegularCredits = deco.RegularCredits;
@ -252,10 +216,6 @@ export const getDojoClient = async (
clientComponent.Decos.push(clientDeco);
}
}
clientComponent.PendingColors = dojoComponent.PendingColors;
clientComponent.Colors = dojoComponent.Colors;
clientComponent.PendingLights = dojoComponent.PendingLights;
clientComponent.Lights = dojoComponent.Lights;
dojo.DojoComponents.push(clientComponent);
}
}
@ -266,15 +226,6 @@ export const getDojoClient = async (
}
needSave = true;
}
for (const deco of decosToRemoveNoRefund) {
logger.debug(`removing polychrome`, deco);
const component = guild.DojoComponents.id(deco.componentId)!;
component.Decos!.splice(
component.Decos!.findIndex(x => x._id.equals(deco.decoId)),
1
);
needSave = true;
}
if (needSave) {
await guild.save();
}
@ -505,7 +456,7 @@ export const hasGuildPermissionEx = (
export const removePigmentsFromGuildMembers = async (guildId: string | Types.ObjectId): Promise<void> => {
const members = await GuildMember.find({ guildId, status: 0 }, "accountId");
await parallelForeach(members, async member => {
for (const member of members) {
const inventory = await getInventory(member.accountId.toString(), "MiscItems");
const index = inventory.MiscItems.findIndex(
x => x.ItemType == "/Lotus/Types/Items/Research/DojoColors/GenericDojoColorPigment"
@ -514,7 +465,7 @@ export const removePigmentsFromGuildMembers = async (guildId: string | Types.Obj
inventory.MiscItems.splice(index, 1);
await inventory.save();
}
});
}
};
export const processGuildTechProjectContributionsUpdate = async (
@ -554,7 +505,7 @@ export const setGuildTechLogState = (
guild: TGuildDatabaseDocument,
type: string,
state: number,
dateTime?: Date
dateTime: Date | undefined = undefined
): boolean => {
guild.TechChanges ??= [];
const entry = guild.TechChanges.find(x => x.details == type);
@ -611,76 +562,6 @@ const setGuildTier = async (guild: TGuildDatabaseDocument, newTier: number): Pro
await processGuildTechProjectContributionsUpdate(guild, project);
}
}
if (guild.CeremonyContributors) {
await checkClanAscensionHasRequiredContributors(guild);
}
};
export const checkClanAscensionHasRequiredContributors = async (guild: TGuildDatabaseDocument): Promise<void> => {
const requiredContributors = [1, 5, 15, 30, 50][guild.Tier - 1];
// Once required contributor count is hit, the class is committed and there's 72 hours to claim endo.
if (guild.CeremonyContributors!.length >= requiredContributors) {
guild.Class = guild.CeremonyClass!;
guild.CeremonyClass = undefined;
guild.CeremonyResetDate = new Date(Date.now() + (config.fastClanAscension ? 5_000 : 72 * 3600_000));
if (!config.fastClanAscension) {
// Send message to all active guild members
const members = await GuildMember.find({ guildId: guild._id, status: 0 }, "accountId");
await parallelForeach(members, async member => {
// somewhat unfaithful as on live the "msg" is not a loctag, but since we don't have the string, we'll let the client fill it in with "arg".
await createMessage(member.accountId, [
{
sndr: guild.Name,
msg: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgressDetails",
arg: [
{
Key: "RESETDATE",
Tag:
guild.CeremonyResetDate!.getUTCMonth() +
"/" +
guild.CeremonyResetDate!.getUTCDate() +
"/" +
(guild.CeremonyResetDate!.getUTCFullYear() % 100) +
" " +
guild.CeremonyResetDate!.getUTCHours().toString().padStart(2, "0") +
":" +
guild.CeremonyResetDate!.getUTCMinutes().toString().padStart(2, "0")
}
],
sub: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgress",
icon: "/Lotus/Interface/Graphics/ClanTileImages/ClanEnterDojo.png",
highPriority: true
}
]);
});
}
}
};
export const giveClanKey = (inventory: TInventoryDatabaseDocument, inventoryChanges?: IInventoryChanges): void => {
if (config.skipClanKeyCrafting) {
const levelKeyChanges = [
{
ItemType: "/Lotus/Types/Keys/DojoKey",
ItemCount: 1
}
];
addLevelKeys(inventory, levelKeyChanges);
if (inventoryChanges) {
combineInventoryChanges(inventoryChanges, { LevelKeys: levelKeyChanges });
}
} else {
const recipeChanges = [
{
ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
ItemCount: 1
}
];
addRecipes(inventory, recipeChanges);
if (inventoryChanges) {
combineInventoryChanges(inventoryChanges, { Recipes: recipeChanges });
}
}
};
export const removeDojoKeyItems = (inventory: TInventoryDatabaseDocument): IInventoryChanges => {

View File

@ -2,7 +2,6 @@ import { Types } from "mongoose";
import {
IEquipmentClient,
IEquipmentDatabase,
IItemConfig,
IOperatorConfigClient,
IOperatorConfigDatabase
} from "../types/inventoryTypes/commonInventoryTypes";
@ -38,7 +37,6 @@ import {
} from "../types/inventoryTypes/inventoryTypes";
import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel";
import { ILoadoutConfigDatabase, ILoadoutDatabase } from "../types/saveLoadoutTypes";
import { slotNames } from "../types/purchaseTypes";
const convertDate = (value: IMongoDate): Date => {
return new Date(parseInt(value.$date.$numberLong));
@ -56,7 +54,6 @@ const convertEquipment = (client: IEquipmentClient): IEquipmentDatabase => {
InfestationDate: convertOptionalDate(client.InfestationDate),
Expiry: convertOptionalDate(client.Expiry),
UpgradesExpiry: convertOptionalDate(client.UpgradesExpiry),
UmbraDate: convertOptionalDate(client.UmbraDate),
CrewMembers: client.CrewMembers ? convertCrewShipMembers(client.CrewMembers) : undefined,
Details: client.Details ? convertKubrowDetails(client.Details) : undefined,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@ -106,18 +103,18 @@ const replaceSlots = (db: ISlots, client: ISlots): void => {
db.Slots = client.Slots;
};
export const importCrewMemberId = (crewMemberId: ICrewShipMemberClient): ICrewShipMemberDatabase => {
if (crewMemberId.ItemId) {
return { ItemId: new Types.ObjectId(crewMemberId.ItemId.$oid) };
}
return { NemesisFingerprint: BigInt(crewMemberId.NemesisFingerprint ?? 0) };
const convertCrewShipMember = (client: ICrewShipMemberClient): ICrewShipMemberDatabase => {
return {
...client,
ItemId: client.ItemId ? new Types.ObjectId(client.ItemId.$oid) : undefined
};
};
const convertCrewShipMembers = (client: ICrewShipMembersClient): ICrewShipMembersDatabase => {
return {
SLOT_A: client.SLOT_A ? importCrewMemberId(client.SLOT_A) : undefined,
SLOT_B: client.SLOT_B ? importCrewMemberId(client.SLOT_B) : undefined,
SLOT_C: client.SLOT_C ? importCrewMemberId(client.SLOT_C) : undefined
SLOT_A: client.SLOT_A ? convertCrewShipMember(client.SLOT_A) : undefined,
SLOT_B: client.SLOT_B ? convertCrewShipMember(client.SLOT_B) : undefined,
SLOT_C: client.SLOT_C ? convertCrewShipMember(client.SLOT_C) : undefined
};
};
@ -170,25 +167,10 @@ const convertPendingRecipe = (client: IPendingRecipeClient): IPendingRecipeDatab
const convertNemesis = (client: INemesisClient): INemesisDatabase => {
return {
...client,
fp: BigInt(client.fp),
d: convertDate(client.d)
};
};
// Empty objects from live may have been encoded as empty arrays because of PHP.
const convertItemConfig = <T extends IItemConfig>(client: T): T => {
return {
...client,
pricol: Array.isArray(client.pricol) ? {} : client.pricol,
attcol: Array.isArray(client.attcol) ? {} : client.attcol,
sigcol: Array.isArray(client.sigcol) ? {} : client.sigcol,
eyecol: Array.isArray(client.eyecol) ? {} : client.eyecol,
facial: Array.isArray(client.facial) ? {} : client.facial,
cloth: Array.isArray(client.cloth) ? {} : client.cloth,
syancol: Array.isArray(client.syancol) ? {} : client.syancol
};
};
export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<IInventoryClient>): void => {
for (const key of equipmentKeys) {
if (client[key] !== undefined) {
@ -229,28 +211,35 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
replaceArray<IOperatorConfigDatabase>(db[key], client[key].map(convertOperatorConfig));
}
}
for (const key of slotNames) {
for (const key of [
"SuitBin",
"WeaponBin",
"SentinelBin",
"SpaceSuitBin",
"SpaceWeaponBin",
"PvpBonusLoadoutBin",
"PveBonusLoadoutBin",
"RandomModBin",
"MechBin",
"CrewMemberBin",
"OperatorAmpBin",
"CrewShipSalvageBin"
] as const) {
if (client[key] !== undefined) {
replaceSlots(db[key], client[key]);
}
}
// boolean
for (const key of [
"UseAdultOperatorLoadout",
"HasOwnedVoidProjectionsPreviously",
"ReceivedStartingGear",
"ArchwingEnabled",
"PlayedParkourTutorial",
"Staff",
"Moderator",
"Partner",
"Counselor"
"PlayedParkourTutorial"
] as const) {
if (client[key] !== undefined) {
db[key] = client[key];
}
}
// number
for (const key of [
"PlayerLevel",
"RegularCredits",
@ -260,15 +249,12 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
"PrimeTokens",
"TradesRemaining",
"GiftsRemaining",
"ChallengesFixVersion",
"Founder",
"Guide"
"ChallengesFixVersion"
] as const) {
if (client[key] !== undefined) {
db[key] = client[key];
}
}
// string
for (const key of [
"ThemeStyle",
"ThemeBackground",
@ -283,7 +269,6 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
db[key] = client[key];
}
}
// string[]
for (const key of [
"EquippedGear",
"EquippedEmotes",
@ -367,7 +352,7 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
db.PlayerSkills = client.PlayerSkills;
}
if (client.LotusCustomization !== undefined) {
db.LotusCustomization = convertItemConfig(client.LotusCustomization);
db.LotusCustomization = client.LotusCustomization;
}
if (client.CollectibleSeries !== undefined) {
db.CollectibleSeries = client.CollectibleSeries;
@ -394,9 +379,6 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
});
});
}
if (client.Accolades !== undefined) {
db.Accolades = client.Accolades;
}
};
const convertLoadOutConfig = (client: ILoadoutConfigClient): ILoadoutConfigDatabase => {

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,7 @@
import { IKeyChainRequest } from "@/src/types/requestTypes";
import { getIndexAfter } from "@/src/helpers/stringHelpers";
import { ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import {
dict_de,
dict_en,
@ -17,7 +19,6 @@ import {
dict_uk,
dict_zh,
ExportArcanes,
ExportBoosters,
ExportCustoms,
ExportDrones,
ExportGear,
@ -33,6 +34,7 @@ import {
IRecipe,
TReward
} from "warframe-public-export-plus";
import questCompletionItems from "@/static/fixed_responses/questCompletionRewards.json";
import { IMessage } from "../models/inboxModel";
export type WeaponTypeInternal =
@ -179,6 +181,32 @@ export const getLevelKeyRewards = (
};
};
export const getQuestCompletionItems = (questKey: string): ITypeCount[] | undefined => {
if (questKey in questCompletionItems) {
return questCompletionItems[questKey as keyof typeof questCompletionItems];
}
logger.warn(`Quest ${questKey} not found in questCompletionItems`);
const items: ITypeCount[] = [];
const meta = ExportKeys[questKey];
if (meta.rewards) {
for (const reward of meta.rewards) {
if (reward.rewardType == "RT_STORE_ITEM") {
items.push({
ItemType: fromStoreItem(reward.itemType),
ItemCount: 1
});
} else if (reward.rewardType == "RT_RESOURCE" || reward.rewardType == "RT_RECIPE") {
items.push({
ItemType: reward.itemType,
ItemCount: reward.amount
});
}
}
}
return items;
};
export const getKeyChainMessage = ({ KeyChain, ChainStage }: IKeyChainRequest): IMessage => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const chainStages = ExportKeys[KeyChain]?.chainStages;
@ -186,15 +214,14 @@ export const getKeyChainMessage = ({ KeyChain, ChainStage }: IKeyChainRequest):
throw new Error(`KeyChain ${KeyChain} does not contain chain stages`);
}
let i = ChainStage;
let chainStageMessage = chainStages[i].messageToSendWhenTriggered;
while (!chainStageMessage) {
if (++i >= chainStages.length) {
break;
}
chainStageMessage = chainStages[i].messageToSendWhenTriggered;
const keyChainStage = chainStages[ChainStage];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!keyChainStage) {
throw new Error(`KeyChainStage ${ChainStage} not found`);
}
const chainStageMessage = keyChainStage.messageToSendWhenTriggered;
if (!chainStageMessage) {
throw new Error(
`client requested key chain message in keychain ${KeyChain} at stage ${ChainStage} but they did not exist`
@ -218,30 +245,15 @@ export const convertInboxMessage = (message: IInboxMessage): IMessage => {
};
export const isStoreItem = (type: string): boolean => {
return type.startsWith("/Lotus/StoreItems/") || type in ExportBoosters;
return type.startsWith("/Lotus/StoreItems/");
};
export const toStoreItem = (type: string): string => {
if (type.startsWith("/Lotus/Types/StoreItems/Boosters/")) {
const boosterEntry = Object.entries(ExportBoosters).find(arr => arr[1].typeName == type);
if (boosterEntry) {
return boosterEntry[0];
}
throw new Error(`could not convert ${type} to a store item`);
}
return "/Lotus/StoreItems/" + type.substring("/Lotus/".length);
};
export const fromStoreItem = (type: string): string => {
if (type.startsWith("/Lotus/StoreItems/")) {
return "/Lotus/" + type.substring("/Lotus/StoreItems/".length);
}
if (type in ExportBoosters) {
return ExportBoosters[type].typeName;
}
throw new Error(`${type} is not a store item`);
return "/Lotus/" + type.substring("/Lotus/StoreItems/".length);
};
export const getDefaultUpgrades = (parts: string[]): IDefaultUpgrade[] | undefined => {

View File

@ -8,7 +8,7 @@ export const submitLeaderboardScore = async (
ownerId: string,
displayName: string,
score: number,
guildId: string | undefined
guildId?: string
): Promise<void> => {
let expiry: Date;
if (schedule == "daily") {
@ -39,9 +39,9 @@ export const getLeaderboard = async (
leaderboard: string,
before: number,
after: number,
pivotId: string | undefined,
guildId: string | undefined,
guildTier: number | undefined
pivotId?: string,
guildId?: string,
guildTier?: number
): Promise<ILeaderboardEntryClient[]> => {
const filter: { leaderboard: string; guildId?: string; guildTier?: number } = { leaderboard };
if (guildId) {

View File

@ -77,6 +77,7 @@ const getRandomLoginReward = (rng: CRng, day: number, inventory: TInventoryDatab
const reward = rng.randomReward(randomRewards)!;
//const reward = randomRewards.find(x => x.RewardType == "RT_BOOSTER")!;
if (reward.RewardType == "RT_RANDOM_RECIPE") {
// Not very faithful implementation but roughly the same idea
const masteredItems = new Set();
for (const entry of inventory.XPInfo) {
masteredItems.add(entry.ItemType);
@ -94,15 +95,15 @@ const getRandomLoginReward = (rng: CRng, day: number, inventory: TInventoryDatab
}
const eligibleRecipes: string[] = [];
for (const [uniqueName, recipe] of Object.entries(ExportRecipes)) {
if (!recipe.excludeFromMarket && unmasteredItems.has(recipe.resultType)) {
if (unmasteredItems.has(recipe.resultType)) {
eligibleRecipes.push(uniqueName);
}
}
if (eligibleRecipes.length == 0) {
// This account has all applicable warframes and weapons already mastered (filthy cheater), need a different reward.
// This account has all warframes and weapons already mastered (filthy cheater), need a different reward.
return getRandomLoginReward(rng, day, inventory);
}
reward.StoreItemType = toStoreItem(rng.randomElement(eligibleRecipes)!);
reward.StoreItemType = toStoreItem(rng.randomElement(eligibleRecipes));
}
return {
//_id: toOid(new Types.ObjectId()),

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,9 @@
import { PersonalRooms } from "@/src/models/personalRoomsModel";
import { addItem, getInventory } from "@/src/services/inventoryService";
import { TPersonalRoomsDatabaseDocument } from "../types/personalRoomsTypes";
import { IGardeningDatabase } from "../types/shipTypes";
import { getRandomElement } from "./rngService";
export const getPersonalRooms = async (
accountId: string,
projection?: string
): Promise<TPersonalRoomsDatabaseDocument> => {
const personalRooms = await PersonalRooms.findOne({ personalRoomsOwnerId: accountId }, projection);
export const getPersonalRooms = async (accountId: string): Promise<TPersonalRoomsDatabaseDocument> => {
const personalRooms = await PersonalRooms.findOne({ personalRoomsOwnerId: accountId });
if (!personalRooms) {
throw new Error(`personal rooms not found for account ${accountId}`);
@ -30,64 +25,3 @@ export const updateShipFeature = async (accountId: string, shipFeature: string):
await addItem(inventory, shipFeature, -1);
await inventory.save();
};
export const createGarden = (): IGardeningDatabase => {
const plantTypes = [
"/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantA",
"/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantB",
"/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantC",
"/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantD",
"/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantE",
"/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantF"
];
const endTime = new Date((Math.trunc(Date.now() / 1000) + 79200) * 1000); // Plants will take 22 hours to grow
return {
Planters: [
{
Name: "Garden0",
Plants: [
{
PlantType: getRandomElement(plantTypes)!,
EndTime: endTime,
PlotIndex: 0
},
{
PlantType: getRandomElement(plantTypes)!,
EndTime: endTime,
PlotIndex: 1
}
]
},
{
Name: "Garden1",
Plants: [
{
PlantType: getRandomElement(plantTypes)!,
EndTime: endTime,
PlotIndex: 0
},
{
PlantType: getRandomElement(plantTypes)!,
EndTime: endTime,
PlotIndex: 1
}
]
},
{
Name: "Garden2",
Plants: [
{
PlantType: getRandomElement(plantTypes)!,
EndTime: endTime,
PlotIndex: 0
},
{
PlantType: getRandomElement(plantTypes)!,
EndTime: endTime,
PlotIndex: 1
}
]
}
]
};
};

View File

@ -66,18 +66,6 @@ export const handlePurchase = async (
if (!offer) {
throw new Error(`unknown vendor offer: ${ItemId ? ItemId : purchaseRequest.PurchaseParams.StoreItem}`);
}
if (offer.RegularPrice) {
combineInventoryChanges(
prePurchaseInventoryChanges,
updateCurrency(inventory, offer.RegularPrice[0], false)
);
}
if (offer.PremiumPrice) {
combineInventoryChanges(
prePurchaseInventoryChanges,
updateCurrency(inventory, offer.PremiumPrice[0], true)
);
}
if (offer.ItemPrices) {
handleItemPrices(
inventory,
@ -153,8 +141,7 @@ export const handlePurchase = async (
inventory,
purchaseRequest.PurchaseParams.Quantity,
undefined,
false,
purchaseRequest.PurchaseParams.UsePremium,
undefined,
seed
);
combineInventoryChanges(purchaseResponse.InventoryChanges, prePurchaseInventoryChanges);
@ -182,9 +169,6 @@ export const handlePurchase = async (
purchaseResponse.InventoryChanges,
updateCurrency(inventory, offer.RegularPrice, false)
);
if (purchaseRequest.PurchaseParams.ExpectedPrice) {
throw new Error(`vendor purchase should not have an expected price`);
}
const invItem: IMiscItem = {
ItemType: "/Lotus/Types/Items/MiscItems/PrimeBucks",
@ -238,18 +222,12 @@ export const handlePurchase = async (
const vendor = ExportVendors[purchaseRequest.PurchaseParams.SourceId!];
const offer = vendor.items.find(x => x.storeItem == purchaseRequest.PurchaseParams.StoreItem);
if (offer) {
if (typeof offer.credits == "number") {
if (offer.credits) {
combineInventoryChanges(
purchaseResponse.InventoryChanges,
updateCurrency(inventory, offer.credits, false)
);
}
if (typeof offer.platinum == "number") {
combineInventoryChanges(
purchaseResponse.InventoryChanges,
updateCurrency(inventory, offer.platinum, true)
);
}
if (offer.itemPrices) {
handleItemPrices(
inventory,
@ -260,9 +238,6 @@ export const handlePurchase = async (
}
}
}
if (purchaseRequest.PurchaseParams.ExpectedPrice) {
throw new Error(`vendor purchase should not have an expected price`);
}
break;
case 18: {
if (purchaseRequest.PurchaseParams.SourceId! != worldState.PrimeVaultTraders[0]._id.$oid) {
@ -356,7 +331,6 @@ export const handleStoreItemAcquisition = async (
quantity: number = 1,
durability: TRarity = "COMMON",
ignorePurchaseQuantity: boolean = false,
premiumPurchase: boolean = true,
seed?: bigint
): Promise<IPurchaseResponse> => {
let purchaseResponse = {
@ -378,20 +352,11 @@ export const handleStoreItemAcquisition = async (
}
switch (storeCategory) {
default: {
purchaseResponse = {
InventoryChanges: await addItem(inventory, internalName, quantity, premiumPurchase, seed)
};
purchaseResponse = { InventoryChanges: await addItem(inventory, internalName, quantity, true, seed) };
break;
}
case "Types":
purchaseResponse = await handleTypesPurchase(
internalName,
inventory,
quantity,
ignorePurchaseQuantity,
premiumPurchase,
seed
);
purchaseResponse = await handleTypesPurchase(internalName, inventory, quantity, ignorePurchaseQuantity);
break;
case "Boosters":
purchaseResponse = handleBoostersPurchase(storeItemName, inventory, durability);
@ -513,15 +478,13 @@ const handleTypesPurchase = async (
typesName: string,
inventory: TInventoryDatabaseDocument,
quantity: number,
ignorePurchaseQuantity: boolean,
premiumPurchase: boolean = true,
seed?: bigint
ignorePurchaseQuantity: boolean
): Promise<IPurchaseResponse> => {
const typeCategory = getStoreItemTypesCategory(typesName);
logger.debug(`type category ${typeCategory}`);
switch (typeCategory) {
default:
return { InventoryChanges: await addItem(inventory, typesName, quantity, premiumPurchase, seed) };
return { InventoryChanges: await addItem(inventory, typesName, quantity) };
case "BoosterPacks":
return handleBoosterPackPurchase(typesName, inventory, quantity);
case "SlotItems":

View File

@ -3,14 +3,18 @@ import { isEmptyObject } from "@/src/helpers/general";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { createMessage } from "@/src/services/inboxService";
import { addItem, addItems, addKeyChainItems, setupKahlSyndicate } from "@/src/services/inventoryService";
import { fromStoreItem, getKeyChainMessage, getLevelKeyRewards } from "@/src/services/itemDataService";
import { IQuestKeyClient, IQuestKeyDatabase, IQuestStage, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
import {
fromStoreItem,
getKeyChainMessage,
getLevelKeyRewards,
getQuestCompletionItems
} from "@/src/services/itemDataService";
import { IQuestKeyClient, IQuestKeyDatabase, IQuestStage } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { Types } from "mongoose";
import { ExportKeys } from "warframe-public-export-plus";
import { addFixedLevelRewards } from "./missionInventoryUpdateService";
import { IInventoryChanges } from "../types/purchaseTypes";
import questCompletionItems from "@/static/fixed_responses/questCompletionRewards.json";
export interface IUpdateQuestRequest {
QuestKeys: Omit<IQuestKeyDatabase, "CompletionDate">[];
@ -38,12 +42,23 @@ export const updateQuestKey = async (
inventory.QuestKeys[questKeyIndex].overwrite(questKeyUpdate[0]);
const inventoryChanges: IInventoryChanges = {};
let inventoryChanges: IInventoryChanges = {};
if (questKeyUpdate[0].Completed) {
inventory.QuestKeys[questKeyIndex].CompletionDate = new Date();
const questKey = questKeyUpdate[0].ItemType;
await handleQuestCompletion(inventory, questKey, inventoryChanges);
logger.debug(`completed quest ${questKeyUpdate[0].ItemType} `);
const questKeyName = questKeyUpdate[0].ItemType;
const questCompletionItems = getQuestCompletionItems(questKeyName);
logger.debug(`quest completion items`, questCompletionItems);
if (questCompletionItems) {
inventoryChanges = await addItems(inventory, questCompletionItems);
}
inventory.ActiveQuest = "";
if (questKeyUpdate[0].ItemType == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain") {
setupKahlSyndicate(inventory);
}
}
return inventoryChanges;
};
@ -162,124 +177,17 @@ export const completeQuest = async (inventory: TInventoryDatabaseDocument, quest
await giveKeyChainMissionReward(inventory, { KeyChain: questKey, ChainStage: i });
}
await handleQuestCompletion(inventory, questKey);
};
const getQuestCompletionItems = (questKey: string): ITypeCount[] | undefined => {
if (questKey in questCompletionItems) {
return questCompletionItems[questKey as keyof typeof questCompletionItems];
}
logger.warn(`Quest ${questKey} not found in questCompletionItems`);
const items: ITypeCount[] = [];
const meta = ExportKeys[questKey];
if (meta.rewards) {
for (const reward of meta.rewards) {
if (reward.rewardType == "RT_STORE_ITEM") {
items.push({
ItemType: fromStoreItem(reward.itemType),
ItemCount: 1
});
} else if (reward.rewardType == "RT_RESOURCE" || reward.rewardType == "RT_RECIPE") {
items.push({
ItemType: reward.itemType,
ItemCount: reward.amount
});
}
}
}
return items;
};
// Checks that `questKey` is in `requirements`, and if so, that all other quests in `requirements` are also already completed.
const doesQuestCompletionFinishSet = (
inventory: TInventoryDatabaseDocument,
questKey: string,
requirements: string[]
): boolean => {
let holds = false;
for (const requirement of requirements) {
if (questKey == requirement) {
holds = true;
} else {
if (!inventory.QuestKeys.find(x => x.ItemType == requirement)?.Completed) {
return false;
}
}
}
return holds;
};
const handleQuestCompletion = async (
inventory: TInventoryDatabaseDocument,
questKey: string,
inventoryChanges: IInventoryChanges = {}
): Promise<void> => {
logger.debug(`completed quest ${questKey}`);
if (questKey == "/Lotus/Types/Keys/OrokinMoonQuest/OrokinMoonQuestKeyChain") {
await createMessage(inventory.accountOwnerId, [
{
sndr: "/Lotus/Language/Bosses/Ordis",
msg: "/Lotus/Language/G1Quests/SecondDreamFinishInboxMessage",
att: [
"/Lotus/Weapons/Tenno/Melee/Swords/StalkerTwo/StalkerTwoSmallSword",
"/Lotus/Upgrades/Skins/Sigils/ScarSigil"
],
sub: "/Lotus/Language/G1Quests/SecondDreamFinishInboxTitle",
icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
highPriority: true
}
]);
} else if (questKey == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain") {
setupKahlSyndicate(inventory);
}
// Whispers in the Walls is unlocked once The New + Heart of Deimos are completed.
if (
doesQuestCompletionFinishSet(inventory, questKey, [
"/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain",
"/Lotus/Types/Keys/InfestedMicroplanetQuest/InfestedMicroplanetQuestKeyChain"
])
) {
await createMessage(inventory.accountOwnerId, [
{
sndr: "/Lotus/Language/Bosses/Loid",
msg: "/Lotus/Language/EntratiLab/EntratiQuest/WiTWQuestRecievedInboxBody",
att: ["/Lotus/Types/Keys/EntratiLab/EntratiQuestKeyChain"],
sub: "/Lotus/Language/EntratiLab/EntratiQuest/WiTWQuestRecievedInboxTitle",
icon: "/Lotus/Interface/Icons/Npcs/Entrati/Loid.png",
highPriority: true
}
]);
}
// The Hex (Quest) is unlocked once The Lotus Eaters + The Duviri Paradox are completed.
if (
doesQuestCompletionFinishSet(inventory, questKey, [
"/Lotus/Types/Keys/1999PrologueQuest/1999PrologueQuestKeyChain",
"/Lotus/Types/Keys/DuviriQuest/DuviriQuestKeyChain"
])
) {
await createMessage(inventory.accountOwnerId, [
{
sndr: "/Lotus/Language/NewWar/P3M1ChooseMara",
msg: "/Lotus/Language/1999Quest/1999QuestInboxBody",
att: ["/Lotus/Types/Keys/1999Quest/1999QuestKeyChain"],
sub: "/Lotus/Language/1999Quest/1999QuestInboxSubject",
icon: "/Lotus/Interface/Icons/Npcs/Operator.png",
highPriority: true
}
]);
}
const questCompletionItems = getQuestCompletionItems(questKey);
logger.debug(`quest completion items`, questCompletionItems);
if (questCompletionItems) {
await addItems(inventory, questCompletionItems, inventoryChanges);
await addItems(inventory, questCompletionItems);
}
if (inventory.ActiveQuest == questKey) inventory.ActiveQuest = "";
if (questKey == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain") {
setupKahlSyndicate(inventory);
}
};
export const giveKeyChainItem = async (
@ -289,7 +197,7 @@ export const giveKeyChainItem = async (
const inventoryChanges = await addKeyChainItems(inventory, keyChainInfo);
if (isEmptyObject(inventoryChanges)) {
logger.warn("inventory changes was empty after getting keychain items: should not happen");
throw new Error("inventory changes was empty after getting keychain items: should not happen");
}
// items were added: update quest stage's i (item was given)
updateQuestStage(inventory, keyChainInfo, { i: true });

View File

@ -6,7 +6,7 @@ export interface IRngResult {
probability: number;
}
export const getRandomElement = <T>(arr: readonly T[]): T | undefined => {
export const getRandomElement = <T>(arr: T[]): T => {
return arr[Math.floor(Math.random() * arr.length)];
};
@ -18,10 +18,7 @@ export const getRandomInt = (min: number, max: number): number => {
return Math.floor(Math.random() * (max - min + 1)) + min;
};
export const getRewardAtPercentage = <T extends { probability: number }>(
pool: T[],
percentage: number
): T | undefined => {
const getRewardAtPercentage = <T extends { probability: number }>(pool: T[], percentage: number): T | undefined => {
if (pool.length == 0) return;
const totalChance = pool.reduce((accum, item) => accum + item.probability, 0);
@ -34,7 +31,7 @@ export const getRewardAtPercentage = <T extends { probability: number }>(
return item;
}
}
return pool[pool.length - 1];
throw new Error("What the fuck?");
};
export const getRandomReward = <T extends { probability: number }>(pool: T[]): T | undefined => {
@ -100,32 +97,18 @@ export class CRng {
}
randomInt(min: number, max: number): number {
const diff = max - min;
if (diff != 0) {
if (diff < 0) {
throw new Error(`max must be greater than min`);
}
if (diff > 0x3fffffff) {
throw new Error(`insufficient entropy`);
}
min += Math.floor(this.random() * (diff + 1));
}
return min;
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(this.random() * (max - min + 1)) + min;
}
randomElement<T>(arr: readonly T[]): T | undefined {
randomElement<T>(arr: T[]): T {
return arr[Math.floor(this.random() * arr.length)];
}
randomReward<T extends { probability: number }>(pool: T[]): T | undefined {
return getRewardAtPercentage(pool, this.random());
}
churnSeed(its: number): void {
while (its--) {
this.state = (this.state * 1103515245 + 12345) & 0x7fffffff;
}
}
}
// Seeded RNG for cases where we need identical results to the game client. Based on work by Donald Knuth.
@ -145,7 +128,7 @@ export class SRng {
return min;
}
randomElement<T>(arr: readonly T[]): T | undefined {
randomElement<T>(arr: T[]): T {
return arr[this.randomInt(0, arr.length - 1)];
}
@ -153,8 +136,4 @@ export class SRng {
this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn;
return (Number(this.state >> 38n) & 0xffffff) * 0.000000059604645;
}
randomReward<T extends { probability: number }>(pool: T[]): T | undefined {
return getRewardAtPercentage(pool, this.randomFloat());
}
}

View File

@ -13,8 +13,6 @@ import { Types } from "mongoose";
import { isEmptyObject } from "@/src/helpers/general";
import { logger } from "@/src/utils/logger";
import { equipmentKeys, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { IItemConfig } from "../types/inventoryTypes/commonInventoryTypes";
import { importCrewMemberId } from "./importService";
//TODO: setup default items on account creation or like originally in giveStartingItems.php
@ -142,30 +140,10 @@ export const handleInventoryItemConfigChange = async (
case "WeaponSkins": {
const itemEntries = equipment as IItemEntry;
for (const [itemId, itemConfigEntries] of Object.entries(itemEntries)) {
if (itemId.startsWith("ca70ca70ca70ca70")) {
logger.warn(
`unlockAllSkins does not work with favoriting items because you don't actually own it`
);
} else {
const inventoryItem = inventory.WeaponSkins.id(itemId);
if (!inventoryItem) {
throw new Error(`inventory item WeaponSkins not found with id ${itemId}`);
}
if ("Favorite" in itemConfigEntries) {
inventoryItem.Favorite = itemConfigEntries.Favorite;
}
if ("IsNew" in itemConfigEntries) {
inventoryItem.IsNew = itemConfigEntries.IsNew;
}
}
inventory.WeaponSkins.id(itemId)!.IsNew = itemConfigEntries.IsNew;
}
break;
}
case "LotusCustomization": {
logger.debug(`saved LotusCustomization`, equipmentChanges.LotusCustomization);
inventory.LotusCustomization = equipmentChanges.LotusCustomization;
break;
}
default: {
if (equipmentKeys.includes(equipmentName as TEquipmentKey) && equipmentName != "ValidNewLoadoutId") {
logger.debug(`general Item config saved of type ${equipmentName}`, {
@ -181,36 +159,13 @@ export const handleInventoryItemConfigChange = async (
}
for (const [configId, config] of Object.entries(itemConfigEntries)) {
if (/^[0-9]+$/.test(configId)) {
inventoryItem.Configs[parseInt(configId)] = config as IItemConfig;
if (typeof config !== "boolean") {
inventoryItem.Configs[parseInt(configId)] = config;
}
}
if ("Favorite" in itemConfigEntries) {
inventoryItem.Favorite = itemConfigEntries.Favorite;
}
if ("IsNew" in itemConfigEntries) {
inventoryItem.IsNew = itemConfigEntries.IsNew;
}
if ("ItemName" in itemConfigEntries) {
inventoryItem.ItemName = itemConfigEntries.ItemName;
}
if ("RailjackImage" in itemConfigEntries) {
inventoryItem.RailjackImage = itemConfigEntries.RailjackImage;
}
if ("Customization" in itemConfigEntries) {
inventoryItem.Customization = itemConfigEntries.Customization;
}
if ("Weapon" in itemConfigEntries) {
inventoryItem.Weapon = itemConfigEntries.Weapon;
}
if (itemConfigEntries.CrewMembers) {
inventoryItem.CrewMembers = {
SLOT_A: importCrewMemberId(itemConfigEntries.CrewMembers.SLOT_A ?? {}),
SLOT_B: importCrewMemberId(itemConfigEntries.CrewMembers.SLOT_B ?? {}),
SLOT_C: importCrewMemberId(itemConfigEntries.CrewMembers.SLOT_C ?? {})
};
}
}
break;
} else {

View File

@ -1,308 +1,143 @@
import { unixTimesInMs } from "@/src/constants/timeConstants";
import { catBreadHash } from "@/src/helpers/stringHelpers";
import fs from "fs";
import path from "path";
import { repoDir } from "@/src/helpers/pathHelper";
import { CRng, mixSeeds } from "@/src/services/rngService";
import { IMongoDate } from "@/src/types/commonTypes";
import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes";
import { ExportVendors, IRange } from "warframe-public-export-plus";
import { IItemManifestPreprocessed, IRawVendorManifest, IVendorManifestPreprocessed } from "@/src/types/vendorTypes";
import { JSONParse } from "json-with-bigint";
import { ExportVendors } from "warframe-public-export-plus";
import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json";
import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json";
import DeimosHivemindCommisionsManifestFishmonger from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestFishmonger.json";
import DeimosHivemindCommisionsManifestPetVendor from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestPetVendor.json";
import DeimosHivemindCommisionsManifestProspector from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestProspector.json";
import DeimosHivemindCommisionsManifestTokenVendor from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestTokenVendor.json";
import DeimosHivemindCommisionsManifestWeaponsmith from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestWeaponsmith.json";
import DeimosHivemindTokenVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosHivemindTokenVendorManifest.json";
import DeimosPetVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosPetVendorManifest.json";
import DeimosProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosProspectorVendorManifest.json";
import DuviriAcrithisVendorManifest from "@/static/fixed_responses/getVendorInfo/DuviriAcrithisVendorManifest.json";
import EntratiLabsEntratiLabsCommisionsManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabsCommisionsManifest.json";
import EntratiLabsEntratiLabVendorManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabVendorManifest.json";
import HubsIronwakeDondaVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json";
import HubsRailjackCrewMemberVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json";
import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSalesmanManifest.json";
import Nova1999ConquestShopManifest from "@/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json";
import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json";
import OstronProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronProspectorVendorManifest.json";
import 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";
import TeshinHardModeVendorManifest from "@/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json";
import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVendorInfo/ZarimanCommisionsManifestArchimedean.json";
const rawVendorManifests: IVendorManifest[] = [
ArchimedeanVendorManifest,
DeimosEntratiFragmentVendorProductsManifest,
DeimosHivemindCommisionsManifestFishmonger,
DeimosHivemindCommisionsManifestPetVendor,
DeimosHivemindCommisionsManifestProspector,
DeimosHivemindCommisionsManifestTokenVendor,
DeimosHivemindCommisionsManifestWeaponsmith,
DeimosHivemindTokenVendorManifest,
DeimosPetVendorManifest,
DeimosProspectorVendorManifest,
DuviriAcrithisVendorManifest,
EntratiLabsEntratiLabsCommisionsManifest,
EntratiLabsEntratiLabVendorManifest,
HubsIronwakeDondaVendorManifest, // uses preprocessing
HubsRailjackCrewMemberVendorManifest,
MaskSalesmanManifest,
Nova1999ConquestShopManifest,
OstronPetVendorManifest,
OstronProspectorVendorManifest,
RadioLegionIntermission12VendorManifest,
SolarisDebtTokenVendorRepossessionsManifest,
SolarisProspectorVendorManifest,
Temple1999VendorManifest,
TeshinHardModeVendorManifest, // uses preprocessing
ZarimanCommisionsManifestArchimedean
];
interface IGeneratableVendorInfo extends Omit<IVendorInfo, "ItemManifest" | "Expiry"> {
cycleOffset?: number;
cycleDuration: number;
}
const generatableVendors: IGeneratableVendorInfo[] = [
{
_id: { $oid: "67dadc30e4b6e0e5979c8d84" },
TypeName: "/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest",
RandomSeedType: "VRST_WEAPON",
RequiredGoalTag: "",
WeaponUpgradeValueAttenuationExponent: 2.25,
cycleOffset: 1740960000_000,
cycleDuration: 4 * unixTimesInMs.day
},
{
_id: { $oid: "60ad3b6ec96976e97d227e19" },
TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/PerrinSequenceWeaponVendorManifest",
RandomSeedType: "VRST_WEAPON",
WeaponUpgradeValueAttenuationExponent: 2.25,
cycleOffset: 1744934400_000,
cycleDuration: 4 * unixTimesInMs.day
},
{
_id: { $oid: "61ba123467e5d37975aeeb03" },
TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest",
RandomSeedType: "VRST_FLAVOUR_TEXT",
cycleDuration: unixTimesInMs.week // TODO: Auto-detect this based on the items, so we don't need to specify it explicitly.
}
// {
// _id: { $oid: "5dbb4c41e966f7886c3ce939" },
// TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest"
// }
];
const getVendorOid = (typeName: string): string => {
return "5be4a159b144f3cd" + catBreadHash(typeName).toString(16).padStart(8, "0");
const getVendorManifestJson = (name: string): IRawVendorManifest => {
return JSONParse(fs.readFileSync(path.join(repoDir, `static/fixed_responses/getVendorInfo/${name}.json`), "utf-8"));
};
export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => {
const rawVendorManifests: IRawVendorManifest[] = [
getVendorManifestJson("ArchimedeanVendorManifest"),
getVendorManifestJson("DeimosEntratiFragmentVendorProductsManifest"),
getVendorManifestJson("DeimosFishmongerVendorManifest"),
getVendorManifestJson("DeimosHivemindCommisionsManifestFishmonger"),
getVendorManifestJson("DeimosHivemindCommisionsManifestPetVendor"),
getVendorManifestJson("DeimosHivemindCommisionsManifestProspector"),
getVendorManifestJson("DeimosHivemindCommisionsManifestTokenVendor"),
getVendorManifestJson("DeimosHivemindCommisionsManifestWeaponsmith"),
getVendorManifestJson("DeimosHivemindTokenVendorManifest"),
getVendorManifestJson("DeimosPetVendorManifest"),
getVendorManifestJson("DeimosProspectorVendorManifest"),
getVendorManifestJson("DuviriAcrithisVendorManifest"),
getVendorManifestJson("EntratiLabsEntratiLabsCommisionsManifest"),
getVendorManifestJson("EntratiLabsEntratiLabVendorManifest"),
getVendorManifestJson("GuildAdvertisementVendorManifest"), // uses preprocessing
getVendorManifestJson("HubsIronwakeDondaVendorManifest"), // uses preprocessing
getVendorManifestJson("HubsPerrinSequenceWeaponVendorManifest"),
getVendorManifestJson("HubsRailjackCrewMemberVendorManifest"),
getVendorManifestJson("MaskSalesmanManifest"),
getVendorManifestJson("Nova1999ConquestShopManifest"),
getVendorManifestJson("OstronFishmongerVendorManifest"),
getVendorManifestJson("OstronPetVendorManifest"),
getVendorManifestJson("OstronProspectorVendorManifest"),
getVendorManifestJson("RadioLegionIntermission12VendorManifest"),
getVendorManifestJson("SolarisDebtTokenVendorManifest"),
getVendorManifestJson("SolarisDebtTokenVendorRepossessionsManifest"),
getVendorManifestJson("SolarisFishmongerVendorManifest"),
getVendorManifestJson("SolarisProspectorVendorManifest"),
getVendorManifestJson("TeshinHardModeVendorManifest"), // uses preprocessing
getVendorManifestJson("ZarimanCommisionsManifestArchimedean")
];
export const getVendorManifestByTypeName = (typeName: string): IVendorManifestPreprocessed | undefined => {
for (const vendorManifest of rawVendorManifests) {
if (vendorManifest.VendorInfo.TypeName == typeName) {
return preprocessVendorManifest(vendorManifest);
}
}
for (const vendorInfo of generatableVendors) {
if (vendorInfo.TypeName == typeName) {
return generateVendorManifest(vendorInfo);
}
}
if (typeName in ExportVendors) {
return generateVendorManifest({
_id: { $oid: getVendorOid(typeName) },
TypeName: typeName,
RandomSeedType: ExportVendors[typeName].randomSeedType,
cycleDuration: unixTimesInMs.hour
});
if (typeName == "/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest") {
return generateCodaWeaponVendorManifest();
}
return undefined;
};
export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined => {
export const getVendorManifestByOid = (oid: string): IVendorManifestPreprocessed | undefined => {
for (const vendorManifest of rawVendorManifests) {
if (vendorManifest.VendorInfo._id.$oid == oid) {
return preprocessVendorManifest(vendorManifest);
}
}
for (const vendorInfo of generatableVendors) {
if (vendorInfo._id.$oid == oid) {
return generateVendorManifest(vendorInfo);
}
}
for (const [typeName, manifest] of Object.entries(ExportVendors)) {
const typeNameOid = getVendorOid(typeName);
if (typeNameOid == oid) {
return generateVendorManifest({
_id: { $oid: typeNameOid },
TypeName: typeName,
RandomSeedType: manifest.randomSeedType,
cycleDuration: unixTimesInMs.hour
});
}
if (oid == "67dadc30e4b6e0e5979c8d84") {
return generateCodaWeaponVendorManifest();
}
return undefined;
};
const preprocessVendorManifest = (originalManifest: IVendorManifest): IVendorManifest => {
const preprocessVendorManifest = (originalManifest: IRawVendorManifest): IVendorManifestPreprocessed => {
if (Date.now() >= parseInt(originalManifest.VendorInfo.Expiry.$date.$numberLong)) {
const manifest = structuredClone(originalManifest);
const info = manifest.VendorInfo;
refreshExpiry(info.Expiry);
for (const offer of info.ItemManifest) {
refreshExpiry(offer.Expiry);
const iteration = refreshExpiry(offer.Expiry);
if (offer.ItemPrices) {
for (const price of offer.ItemPrices) {
if (typeof price.ItemType != "string") {
const itemSeed = parseInt(offer.Id.$oid.substring(16), 16);
const rng = new CRng(mixSeeds(itemSeed, iteration));
price.ItemType = rng.randomElement(price.ItemType);
}
}
}
}
return manifest;
return manifest as IVendorManifestPreprocessed;
}
return originalManifest;
return originalManifest as IVendorManifestPreprocessed;
};
const refreshExpiry = (expiry: IMongoDate): void => {
const refreshExpiry = (expiry: IMongoDate): number => {
const period = parseInt(expiry.$date.$numberLong);
if (Date.now() >= period) {
const epoch = 1734307200_000; // Monday (for weekly schedules)
const epoch = 1734307200 * 1000; // Monday (for weekly schedules)
const iteration = Math.trunc((Date.now() - epoch) / period);
const start = epoch + iteration * period;
const end = start + period;
expiry.$date.$numberLong = end.toString();
return iteration;
}
return 0;
};
const toRange = (value: IRange | number): IRange => {
if (typeof value == "number") {
return { minValue: value, maxValue: value };
}
return value;
};
const vendorInfoCache: Record<string, IVendorInfo> = {};
const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifest => {
if (!(vendorInfo.TypeName in vendorInfoCache)) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { cycleOffset, cycleDuration, ...clientVendorInfo } = vendorInfo;
vendorInfoCache[vendorInfo.TypeName] = {
...clientVendorInfo,
ItemManifest: [],
Expiry: { $date: { $numberLong: "0" } }
};
}
const processed = vendorInfoCache[vendorInfo.TypeName];
if (Date.now() >= parseInt(processed.Expiry.$date.$numberLong)) {
// Remove expired offers
for (let i = 0; i != processed.ItemManifest.length; ) {
if (Date.now() >= parseInt(processed.ItemManifest[i].Expiry.$date.$numberLong)) {
processed.ItemManifest.splice(i, 1);
} else {
++i;
}
const generateCodaWeaponVendorManifest = (): IVendorManifestPreprocessed => {
const EPOCH = 1740960000 * 1000;
const DUR = 4 * 86400 * 1000;
const cycle = Math.trunc((Date.now() - EPOCH) / DUR);
const cycleStart = EPOCH + cycle * DUR;
const cycleEnd = cycleStart + DUR;
const binThisCycle = cycle % 2; // isOneBinPerCycle
const items: IItemManifestPreprocessed[] = [];
const manifest = ExportVendors["/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest"];
const rng = new CRng(cycle);
for (const rawItem of manifest.items) {
if (rawItem.bin != binThisCycle) {
continue;
}
// Add new offers
const vendorSeed = parseInt(vendorInfo._id.$oid.substring(16), 16);
const cycleOffset = vendorInfo.cycleOffset ?? 1734307200_000;
const cycleDuration = vendorInfo.cycleDuration;
const cycleIndex = Math.trunc((Date.now() - cycleOffset) / cycleDuration);
const rng = new CRng(mixSeeds(vendorSeed, cycleIndex));
const manifest = ExportVendors[vendorInfo.TypeName];
const offersToAdd = [];
if (manifest.numItems && !manifest.isOneBinPerCycle) {
const numItemsTarget = rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue);
while (processed.ItemManifest.length + offersToAdd.length < numItemsTarget) {
// TODO: Consider per-bin item limits
// TODO: Consider item probability weightings
offersToAdd.push(rng.randomElement(manifest.items)!);
}
} else {
let binThisCycle;
if (manifest.isOneBinPerCycle) {
binThisCycle = cycleIndex % 2; // Note: May want to auto-compute the bin size, but this is only used for coda weapons right now.
}
for (const rawItem of manifest.items) {
if (!manifest.isOneBinPerCycle || rawItem.bin == binThisCycle) {
offersToAdd.push(rawItem);
}
}
// For most vendors, the offers seem to roughly be in reverse order from the manifest. Coda weapons are an odd exception.
if (!manifest.isOneBinPerCycle) {
offersToAdd.reverse();
}
}
const cycleStart = cycleOffset + cycleIndex * cycleDuration;
for (const rawItem of offersToAdd) {
const durationHoursRange = toRange(rawItem.durationHours);
const expiry =
cycleStart +
rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour;
const item: IItemManifest = {
StoreItem: rawItem.storeItem,
ItemPrices: rawItem.itemPrices?.map(itemPrice => ({ ...itemPrice, ProductCategory: "MiscItems" })),
Bin: "BIN_" + rawItem.bin,
QuantityMultiplier: 1,
Expiry: { $date: { $numberLong: expiry.toString() } },
AllowMultipurchase: false,
Id: {
$oid:
((cycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
vendorInfo._id.$oid.substring(8, 16) +
rng.randomInt(0, 0xffff).toString(16).padStart(4, "0") +
rng.randomInt(0, 0xffff).toString(16).padStart(4, "0")
}
};
if (rawItem.numRandomItemPrices) {
item.ItemPrices = [];
for (let i = 0; i != rawItem.numRandomItemPrices; ++i) {
let itemPrice: { type: string; count: IRange };
do {
itemPrice = rng.randomElement(manifest.randomItemPricesPerBin![rawItem.bin])!;
} while (item.ItemPrices.find(x => x.ItemType == itemPrice.type));
item.ItemPrices.push({
ItemType: itemPrice.type,
ItemCount: rng.randomInt(itemPrice.count.minValue, itemPrice.count.maxValue),
ProductCategory: "MiscItems"
});
}
}
if (rawItem.credits) {
const value =
typeof rawItem.credits == "number"
? rawItem.credits
: rng.randomInt(
rawItem.credits.minValue / rawItem.credits.step,
rawItem.credits.maxValue / rawItem.credits.step
) * rawItem.credits.step;
item.RegularPrice = [value, value];
}
if (rawItem.platinum) {
const value =
typeof rawItem.platinum == "number"
? rawItem.platinum
: rng.randomInt(rawItem.platinum.minValue, rawItem.platinum.maxValue);
item.PremiumPrice = [value, value];
}
if (vendorInfo.RandomSeedType) {
item.LocTagRandSeed = (rng.randomInt(0, 0xffff) << 16) | rng.randomInt(0, 0xffff);
if (vendorInfo.RandomSeedType == "VRST_WEAPON") {
const highDword = (rng.randomInt(0, 0xffff) << 16) | rng.randomInt(0, 0xffff);
item.LocTagRandSeed = (BigInt(highDword) << 32n) | (BigInt(item.LocTagRandSeed) & 0xffffffffn);
}
}
processed.ItemManifest.push(item);
}
// Update vendor expiry
let soonestOfferExpiry: number = Number.MAX_SAFE_INTEGER;
for (const offer of processed.ItemManifest) {
const offerExpiry = parseInt(offer.Expiry.$date.$numberLong);
if (soonestOfferExpiry > offerExpiry) {
soonestOfferExpiry = offerExpiry;
}
}
processed.Expiry.$date.$numberLong = soonestOfferExpiry.toString();
items.push({
StoreItem: rawItem.storeItem,
ItemPrices: rawItem.itemPrices!.map(item => ({ ...item, ProductCategory: "MiscItems" })),
Bin: "BIN_" + rawItem.bin,
QuantityMultiplier: 1,
Expiry: { $date: { $numberLong: cycleEnd.toString() } },
AllowMultipurchase: false,
LocTagRandSeed: (BigInt(rng.randomInt(0, 0xffffffff)) << 32n) | BigInt(rng.randomInt(0, 0xffffffff)),
Id: { $oid: "67e9da12793a120d" + rng.randomInt(0, 0xffffffff).toString(16).padStart(8, "0") }
});
}
return {
VendorInfo: processed
VendorInfo: {
_id: { $oid: "67dadc30e4b6e0e5979c8d84" },
TypeName: "/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest",
ItemManifest: items,
PropertyTextHash: "77093DD05A8561A022DEC9A4B9BB4A56",
RandomSeedType: "VRST_WEAPON",
RequiredGoalTag: "",
WeaponUpgradeValueAttenuationExponent: 2.25,
Expiry: { $date: { $numberLong: cycleEnd.toString() } }
}
};
};

Some files were not shown because too many files have changed in this diff Show More