Compare commits

..

33 Commits

Author SHA1 Message Date
f724073d93 feat: classic lich regalia rewards (#2073)
Closes #2068

Reviewed-on: OpenWF/SpaceNinjaServer#2073
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-13 22:28:59 -07:00
8fb676c906 feat: classic lich ephemera reward (#2067)
Reviewed-on: OpenWF/SpaceNinjaServer#2067
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-13 20:41:49 -07:00
2ce5cc4562 fix: handle converted lich as crew member (#2071)
saveLoadout was missing bigint support to properly store NemesisFingerprint, and crewMembers was missing handling for liches being set on-call (the only option available for them)

Reviewed-on: OpenWF/SpaceNinjaServer#2071
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-13 20:39:03 -07:00
099f12a197 feat: bounty chemistry bonus (#2070)
Re #388

Reviewed-on: OpenWF/SpaceNinjaServer#2070
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-13 20:38:52 -07:00
bfe2e93c76 feat: resource reward along with duviri decree (#2066)
Closes #561

Reviewed-on: OpenWF/SpaceNinjaServer#2066
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-13 20:38:37 -07:00
8b97bb4b0a feat: classic lich hints (#2064)
Closes #1923

Reviewed-on: OpenWF/SpaceNinjaServer#2064
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-13 20:37:30 -07:00
85a45a04ea fix: ensure that only one CrewMember is ever on call (#2069)
Reviewed-on: OpenWF/SpaceNinjaServer#2069
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-13 08:25:09 -07:00
2a40449604 chore: add TNemesisFaction 2025-05-13 12:21:20 +02:00
382f8c55ce chore: update Docker stuff (#2065)
Reviewed-on: OpenWF/SpaceNinjaServer#2065
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-05-13 01:47:09 -07:00
77513190e4 fix: incorrect droptable name for zariman tier c (#2062)
Fixes #2061

Reviewed-on: OpenWF/SpaceNinjaServer#2062
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-12 19:49:30 -07:00
5e8ce934c9 chore: clarify which category has a negative count 2025-05-12 06:59:20 +02:00
6de81c2b41 chore: handle LasrianTankSteelPathDropTable for DROP_MOD (#2057)
Closes #2056

Reviewed-on: OpenWF/SpaceNinjaServer#2057
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-11 21:53:16 -07:00
4c5ac4f03a chore(webui): update German translation (#2059)
Reviewed-on: OpenWF/SpaceNinjaServer#2059
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-05-11 02:16:23 -07:00
d6f4c1a035 chore(webui): update to Spanish translation (#2058)
Reviewed-on: OpenWF/SpaceNinjaServer#2058
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-05-11 00:02:57 -07:00
c58c70c4ce chore: update json-with-bigint minimum required version 2025-05-11 08:29:13 +02:00
3e1e19d6c5 feat: dontSubtractVoidTraces cheat (#2055)
Closes #2051

Reviewed-on: OpenWF/SpaceNinjaServer#2055
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-10 19:12:42 -07:00
2521733e55 fix: exclude open worlds from archon hunt (#2054)
Closes #2048

Reviewed-on: OpenWF/SpaceNinjaServer#2054
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-10 19:12:34 -07:00
d5297d3547 fix(webui): sidebar toggler not showing up on small screens (#2053)
Closes #2049

Reviewed-on: OpenWF/SpaceNinjaServer#2053
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-10 19:12:25 -07:00
5bc39aac8a fix: login failure on U22.8 (#2044)
2018.01.04.13.12

Reviewed-on: OpenWF/SpaceNinjaServer#2044
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-10 19:12:17 -07:00
b201508fa1 chore(webui): update German translation (#2046)
Reviewed-on: OpenWF/SpaceNinjaServer#2046
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-05-10 03:44:55 -07:00
5f9ae2aef6 chore(webui): update to Spanish translation (#2045)
Reviewed-on: OpenWF/SpaceNinjaServer#2045
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-05-10 03:44:42 -07:00
b451c73598 chore: handle mods picked up in mission on U19 (#2042)
Reviewed-on: OpenWF/SpaceNinjaServer#2042
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-09 21:38:42 -07:00
9d4bce852e feat: the circuit (#2039)
Closes #1965

Closes #2041

Reviewed-on: OpenWF/SpaceNinjaServer#2039
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-09 21:38:13 -07:00
c83e732b88 feat: gifting bonus (#2036)
Closes #2014

Reviewed-on: OpenWF/SpaceNinjaServer#2036
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-09 21:37:59 -07:00
3d13ec311e feat: claimingBlueprintRefundsIngredients cheat (#2034)
Closes #1922

Reviewed-on: OpenWF/SpaceNinjaServer#2034
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-09 21:37:28 -07:00
31043b55de feat: batch remove friends (#2032)
Closes #1947

Reviewed-on: OpenWF/SpaceNinjaServer#2032
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-09 21:37:09 -07:00
ab32728c47 fix: don't give assassination blueprint reward for archon hunt (#2031)
Closes #2025

Reviewed-on: OpenWF/SpaceNinjaServer#2031
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-09 21:36:49 -07:00
3fc2dccf81 chore: use 64-bit RNG everywhere (#2030)
Closes #2026

Reviewed-on: OpenWF/SpaceNinjaServer#2030
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-09 21:36:22 -07:00
1084932afb fix: only set IsNew flag if the ItemType is new (#2028)
Reviewed-on: OpenWF/SpaceNinjaServer#2028
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-09 21:35:58 -07:00
f9b3fecc10 chore: some initial handling of legacy oid format (#2033)
This at least allows mission inventory update to succeed on U19.5 and below.

Reviewed-on: OpenWF/SpaceNinjaServer#2033
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-09 00:20:54 -07:00
7a51fab5d3 chore: address some questionable calls to DocumentArray.id 2025-05-09 07:18:25 +02:00
0e255067a8 chore: increase seed ranges to be more accurate (#2029)
Reviewed-on: OpenWF/SpaceNinjaServer#2029
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-08 18:00:22 -07:00
8d788d38a5 chore: remove query for ship in getShipController (#2022)
as far as I can tell, the ShipAttachments and SkinFlavourItem are just here due to the fact that the type from ShipExterior is being reused, but they aren't actually needed because the interior can't have attachments or flavour items - and if it could, they would be different from the exterior anyway.

Reviewed-on: OpenWF/SpaceNinjaServer#2022
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-05-08 17:37:00 -07:00
48 changed files with 1402 additions and 433 deletions

View File

@ -14,6 +14,8 @@ ENV APP_INFINITE_PLATINUM=false
ENV APP_INFINITE_ENDO=false
ENV APP_INFINITE_REGAL_AYA=false
ENV APP_INFINITE_HELMINTH_MATERIALS=false
ENV APP_CLAIMING_BLUEPRINT_REFUNDS_INGREDIENTS=false
ENV APP_DONT_SUBTRACT_VOIDTRACES=false
ENV APP_DONT_SUBTRACT_CONSUMABLES=false
ENV APP_UNLOCK_ALL_SHIP_FEATURES=false
ENV APP_UNLOCK_ALL_SHIP_DECORATIONS=false

View File

@ -19,6 +19,8 @@
"infiniteEndo": false,
"infiniteRegalAya": false,
"infiniteHelminthMaterials": false,
"claimingBlueprintRefundsIngredients": false,
"dontSubtractVoidTraces": false,
"dontSubtractConsumables": false,
"unlockAllShipFeatures": false,
"unlockAllShipDecorations": false,

View File

@ -21,6 +21,8 @@ services:
# APP_INFINITE_ENDO: false
# APP_INFINITE_REGAL_AYA: false
# APP_INFINITE_HELMINTH_MATERIALS: false
# APP_CLAIMING_BLUEPRINT_REFUNDS_INGREDIENTS: false
# APP_DONT_SUBTRACT_VOIDTRACES: false
# APP_DONT_SUBTRACT_CONSUMABLES: false
# APP_UNLOCK_ALL_SHIP_FEATURES: false
# APP_UNLOCK_ALL_SHIP_DECORATIONS: false

10
package-lock.json generated
View File

@ -13,12 +13,12 @@
"@types/morgan": "^1.9.9",
"crc-32": "^1.2.2",
"express": "^5",
"json-with-bigint": "^3.2.2",
"json-with-bigint": "^3.4.4",
"mongoose": "^8.11.0",
"morgan": "^1.10.0",
"ncp": "^2.0.0",
"typescript": "^5.5",
"warframe-public-export-plus": "^0.5.60",
"warframe-public-export-plus": "^0.5.62",
"warframe-riven-info": "^0.1.2",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
@ -3703,9 +3703,9 @@
}
},
"node_modules/warframe-public-export-plus": {
"version": "0.5.60",
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.60.tgz",
"integrity": "sha512-vMfytUc4xRi+b7RTSq+TJEl91vwEegpQKxLtXwRPfs9ZHhntxc4rmDYSNWJTvgf/aWXsFUxQlqL/GV5OLPGM7g=="
"version": "0.5.62",
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.62.tgz",
"integrity": "sha512-D8ZzjkU9rrK/59VqCfpMoV31HVmwHZV1dNZxPO85AOlcjg/G81Fu3kgITQTaw9sdNagLPLQnFaiXY58pxxRwgA=="
},
"node_modules/warframe-riven-info": {
"version": "0.1.2",

View File

@ -20,12 +20,12 @@
"@types/morgan": "^1.9.9",
"crc-32": "^1.2.2",
"express": "^5",
"json-with-bigint": "^3.2.2",
"json-with-bigint": "^3.4.4",
"mongoose": "^8.11.0",
"morgan": "^1.10.0",
"ncp": "^2.0.0",
"typescript": "^5.5",
"warframe-public-export-plus": "^0.5.60",
"warframe-public-export-plus": "^0.5.62",
"warframe-riven-info": "^0.1.2",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"

View File

@ -1,9 +1,9 @@
import { toOid } from "@/src/helpers/inventoryHelpers";
import { fromOid, toOid } from "@/src/helpers/inventoryHelpers";
import { createVeiledRivenFingerprint, rivenRawToRealWeighted } from "@/src/helpers/rivenHelper";
import { addMiscItems, addMods, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getRandomElement, getRandomWeightedReward, getRandomWeightedRewardUc } from "@/src/services/rngService";
import { IOid } from "@/src/types/commonTypes";
import { IUpgradeFromClient } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
import { ExportBoosterPacks, ExportUpgrades, TRarity } from "warframe-public-export-plus";
@ -24,7 +24,7 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
]);
payload.Consumed.forEach(upgrade => {
inventory.Upgrades.pull({ _id: upgrade.ItemId.$oid });
inventory.Upgrades.pull({ _id: fromOid(upgrade.ItemId) });
});
const rawRivenType = getRandomRawRivenType();
@ -57,8 +57,8 @@ 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 });
if (fromOid(upgrade.ItemId) != "000000000000000000000000") {
inventory.Upgrades.pull({ _id: fromOid(upgrade.ItemId) });
} else {
addMods(inventory, [
{
@ -128,24 +128,14 @@ const getRandomRawRivenType = (): string => {
};
interface IArtifactTransmutationRequest {
Upgrade: IAgnosticUpgradeClient;
Upgrade: IUpgradeFromClient;
LevelDiff: number;
Consumed: IAgnosticUpgradeClient[];
Consumed: IUpgradeFromClient[];
Cost: number;
FusionPointCost: number;
RivenTransmute?: boolean;
}
interface IAgnosticUpgradeClient {
ItemType: string;
ItemId: IOid;
FromSKU: boolean;
UpgradeFingerprint: string;
PendingRerollFingerprint: string;
ItemCount: number;
LastAdded: IOid;
}
const specialModSets: string[][] = [
[
"/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod",

View File

@ -4,9 +4,9 @@
import { RequestHandler } from "express";
import { logger } from "@/src/utils/logger";
import { getRecipe } from "@/src/services/itemDataService";
import { IOid } from "@/src/types/commonTypes";
import { IOid, IOidWithLegacySupport } from "@/src/types/commonTypes";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getAccountForRequest } from "@/src/services/loginService";
import {
getInventory,
updateCurrency,
@ -17,8 +17,11 @@ import {
} from "@/src/services/inventoryService";
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";
import { InventorySlot, IPendingRecipeDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
import { toOid2 } from "@/src/helpers/inventoryHelpers";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { IRecipe } from "warframe-public-export-plus";
import { config } from "@/src/services/configService";
interface IClaimCompletedRecipeRequest {
RecipeIds: IOid[];
@ -26,10 +29,8 @@ interface IClaimCompletedRecipeRequest {
export const claimCompletedRecipeController: RequestHandler = async (req, res) => {
const claimCompletedRecipeRequest = getJSONfromString<IClaimCompletedRecipeRequest>(String(req.body));
const accountId = await getAccountIdForRequest(req);
if (!accountId) throw new Error("no account id");
const inventory = await getInventory(accountId);
const account = await getAccountForRequest(req);
const inventory = await getInventory(account._id.toString());
const pendingRecipe = inventory.PendingRecipes.id(claimCompletedRecipeRequest.RecipeIds[0].$oid);
if (!pendingRecipe) {
throw new Error(`no pending recipe found with id ${claimCompletedRecipeRequest.RecipeIds[0].$oid}`);
@ -48,40 +49,14 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
}
if (req.query.cancel) {
const inventoryChanges: IInventoryChanges = {
...updateCurrency(inventory, recipe.buildPrice * -1, false)
};
const equipmentIngredients = new Set();
for (const category of ["LongGuns", "Pistols", "Melee"] as const) {
if (pendingRecipe[category]) {
pendingRecipe[category].forEach(item => {
const index = inventory[category].push(item) - 1;
inventoryChanges[category] ??= [];
inventoryChanges[category].push(inventory[category][index].toJSON<IEquipmentClient>());
equipmentIngredients.add(item.ItemType);
occupySlot(inventory, InventorySlot.WEAPONS, false);
inventoryChanges.WeaponBin ??= { Slots: 0 };
inventoryChanges.WeaponBin.Slots -= 1;
});
}
}
for (const ingredient of recipe.ingredients) {
if (!equipmentIngredients.has(ingredient.ItemType)) {
combineInventoryChanges(
inventoryChanges,
await addItem(inventory, ingredient.ItemType, ingredient.ItemCount)
);
}
}
const inventoryChanges: IInventoryChanges = {};
await refundRecipeIngredients(inventory, inventoryChanges, recipe, pendingRecipe);
await inventory.save();
res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root.
} else {
logger.debug("Claiming Recipe", { recipe, pendingRecipe });
let BrandedSuits: undefined | IOid[];
let BrandedSuits: undefined | IOidWithLegacySupport[];
if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
inventory.PendingSpectreLoadouts ??= [];
inventory.SpectreLoadouts ??= [];
@ -106,7 +81,7 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)),
1
);
BrandedSuits = [toOid(pendingRecipe.SuitToUnbrand!)];
BrandedSuits = [toOid2(pendingRecipe.SuitToUnbrand!, account.BuildLabel)];
}
let InventoryChanges: IInventoryChanges = {};
@ -143,7 +118,43 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
))
};
}
if (config.claimingBlueprintRefundsIngredients) {
await refundRecipeIngredients(inventory, InventoryChanges, recipe, pendingRecipe);
}
await inventory.save();
res.json({ InventoryChanges, BrandedSuits });
}
};
const refundRecipeIngredients = async (
inventory: TInventoryDatabaseDocument,
inventoryChanges: IInventoryChanges,
recipe: IRecipe,
pendingRecipe: IPendingRecipeDatabase
): Promise<void> => {
updateCurrency(inventory, recipe.buildPrice * -1, false, inventoryChanges);
const equipmentIngredients = new Set();
for (const category of ["LongGuns", "Pistols", "Melee"] as const) {
if (pendingRecipe[category]) {
pendingRecipe[category].forEach(item => {
const index = inventory[category].push(item) - 1;
inventoryChanges[category] ??= [];
inventoryChanges[category].push(inventory[category][index].toJSON<IEquipmentClient>());
equipmentIngredients.add(item.ItemType);
occupySlot(inventory, InventorySlot.WEAPONS, false);
inventoryChanges.WeaponBin ??= { Slots: 0 };
inventoryChanges.WeaponBin.Slots -= 1;
});
}
}
for (const ingredient of recipe.ingredients) {
if (!equipmentIngredients.has(ingredient.ItemType)) {
combineInventoryChanges(
inventoryChanges,
await addItem(inventory, ingredient.ItemType, ingredient.ItemCount)
);
}
}
};

View File

@ -1,4 +1,5 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { ICrewMemberClient } from "@/src/types/inventoryTypes/inventoryTypes";
@ -7,15 +8,23 @@ import { Types } from "mongoose";
export const crewMembersController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "CrewMembers");
const inventory = await getInventory(accountId, "CrewMembers NemesisHistory");
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;
if (data.crewMember.SecondInCommand) {
clearOnCall(inventory);
}
if (data.crewMember.ItemId.$oid == "000000000000000000000000") {
const convertedNemesis = inventory.NemesisHistory!.find(x => x.fp == data.crewMember.NemesisFingerprint)!;
convertedNemesis.SecondInCommand = data.crewMember.SecondInCommand;
} else {
const dbCrewMember = inventory.CrewMembers.id(data.crewMember.ItemId.$oid)!;
dbCrewMember.AssignedRole = data.crewMember.AssignedRole;
dbCrewMember.SkillEfficiency = data.crewMember.SkillEfficiency;
dbCrewMember.WeaponConfigIdx = data.crewMember.WeaponConfigIdx;
dbCrewMember.WeaponId = new Types.ObjectId(data.crewMember.WeaponId.$oid);
dbCrewMember.Configs = data.crewMember.Configs;
dbCrewMember.SecondInCommand = data.crewMember.SecondInCommand;
}
await inventory.save();
res.json({
crewMemberId: data.crewMember.ItemId.$oid,
@ -26,3 +35,20 @@ export const crewMembersController: RequestHandler = async (req, res) => {
interface ICrewMembersRequest {
crewMember: ICrewMemberClient;
}
const clearOnCall = (inventory: TInventoryDatabaseDocument): void => {
for (const cm of inventory.CrewMembers) {
if (cm.SecondInCommand) {
cm.SecondInCommand = false;
return;
}
}
if (inventory.NemesisHistory) {
for (const cm of inventory.NemesisHistory) {
if (cm.SecondInCommand) {
cm.SecondInCommand = false;
return;
}
}
}
};

View File

@ -1,60 +1,529 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
import { combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { TEndlessXpCategory } from "@/src/types/inventoryTypes/inventoryTypes";
import { IEndlessXpReward, IInventoryClient, TEndlessXpCategory } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { ExportRewards, ICountedStoreItem } from "warframe-public-export-plus";
import { getRandomElement } from "@/src/services/rngService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
export const endlessXpController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const payload = getJSONfromString<IEndlessXpRequest>(String(req.body));
inventory.EndlessXP ??= [];
const entry = inventory.EndlessXP.find(x => x.Category == payload.Category);
if (entry) {
entry.Choices = payload.Choices;
} else {
inventory.EndlessXP.push({
Category: payload.Category,
Choices: payload.Choices
});
}
await inventory.save();
res.json({
NewProgress: {
Category: payload.Category,
Earn: 0,
Claim: 0,
BonusAvailable: {
$date: {
$numberLong: "9999999999999"
}
},
Expiry: {
$date: {
$numberLong: "9999999999999"
}
},
Choices: payload.Choices,
PendingRewards: [
{
RequiredTotalXp: 190,
Rewards: [
{
StoreItem: "/Lotus/StoreItems/Upgrades/Mods/Aura/PlayerHealthAuraMod",
ItemCount: 1
}
]
}
// ...
]
if (payload.Mode == "r") {
const inventory = await getInventory(accountId, "EndlessXP");
inventory.EndlessXP ??= [];
let entry = inventory.EndlessXP.find(x => x.Category == payload.Category);
if (!entry) {
entry = {
Category: payload.Category,
Earn: 0,
Claim: 0,
Choices: payload.Choices,
PendingRewards: []
};
inventory.EndlessXP.push(entry);
}
});
const weekStart = 1734307200_000 + Math.trunc((Date.now() - 1734307200_000) / 604800000) * 604800000;
const weekEnd = weekStart + 604800000;
entry.Earn = 0;
entry.Claim = 0;
entry.BonusAvailable = new Date(weekStart);
entry.Expiry = new Date(weekEnd);
entry.Choices = payload.Choices;
entry.PendingRewards =
payload.Category == "EXC_HARD"
? generateHardModeRewards(payload.Choices)
: generateNormalModeRewards(payload.Choices);
await inventory.save();
res.json({
NewProgress: inventory.toJSON<IInventoryClient>().EndlessXP!.find(x => x.Category == payload.Category)!
});
} else if (payload.Mode == "c") {
const inventory = await getInventory(accountId);
const entry = inventory.EndlessXP!.find(x => x.Category == payload.Category)!;
const inventoryChanges: IInventoryChanges = {};
for (const reward of entry.PendingRewards) {
if (entry.Claim < reward.RequiredTotalXp && reward.RequiredTotalXp <= entry.Earn) {
combineInventoryChanges(
inventoryChanges,
(
await handleStoreItemAcquisition(
reward.Rewards[0].StoreItem,
inventory,
reward.Rewards[0].ItemCount
)
).InventoryChanges
);
}
}
entry.Claim = entry.Earn;
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
ClaimedXp: entry.Claim
});
} else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unexpected endlessXp mode: ${payload.Mode}`);
}
};
interface IEndlessXpRequest {
Mode: string; // "r"
Category: TEndlessXpCategory;
Choices: string[];
}
type IEndlessXpRequest =
| {
Mode: "r";
Category: TEndlessXpCategory;
Choices: string[];
}
| {
Mode: "c" | "something else";
Category: TEndlessXpCategory;
};
const generateRandomRewards = (deckName: string): ICountedStoreItem[] => {
const reward = getRandomElement(ExportRewards[deckName][0])!;
return [
{
StoreItem: reward.type,
ItemCount: reward.itemCount
}
];
};
const normalModeChosenRewards: Record<string, string[]> = {
Excalibur: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Excalibur/RadialJavelinAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburBlueprint"
],
Trinity: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinityHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinityChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Trinity/EnergyVampireAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinitySystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinityBlueprint"
],
Ember: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Ember/WorldOnFireAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberBlueprint"
],
Loki: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKIHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKIChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Loki/InvisibilityAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKISystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKIBlueprint"
],
Mag: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Mag/CrushAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagBlueprint"
],
Rhino: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Rhino/RhinoChargeAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoBlueprint"
],
Ash: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Ninja/GlaiveAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshBlueprint"
],
Frost: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Frost/IceShieldAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostBlueprint"
],
Nyx: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Jade/SelfBulletAttractorAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxBlueprint"
],
Saryn: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Saryn/PoisonAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynBlueprint"
],
Vauban: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Trapper/LevTrapAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperBlueprint"
],
Nova: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaChassisBlueprint",
"/Lotus/StoreItems/Powersuits/AntiMatter/MolecularPrimeAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaBlueprint"
],
Nekros: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Necro/CloneTheDeadAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroBlueprint"
],
Valkyr: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Berserker/IntimidateAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerBlueprint"
],
Oberon: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Paladin/RegenerationAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinBlueprint"
],
Hydroid: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Pirate/CannonBarrageAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidBlueprint"
],
Mirage: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Harlequin/LightAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinBlueprint"
],
Limbo: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Magician/TearInSpaceAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianBlueprint"
],
Mesa: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Cowgirl/GunFuPvPAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerBlueprint"
],
Chroma: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Dragon/DragonLuckAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaBlueprint"
],
Atlas: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Brawler/BrawlerPassiveAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerBlueprint"
],
Ivara: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Ranger/RangerStealAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerBlueprint"
],
Inaros: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummyHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummyChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Sandman/SandmanSwarmAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummySystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummyBlueprint"
],
Titania: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairyHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairyChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Fairy/FairyFlightAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairySystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairyBlueprint"
],
Nidus: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Infestation/InfestPodsAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusBlueprint"
],
Octavia: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Bard/BardCharmAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaBlueprint"
],
Harrow: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Priest/PriestPactAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestBlueprint"
],
Gara: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Glass/GlassFragmentAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassBlueprint"
],
Khora: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Khora/KhoraCrackAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraBlueprint"
],
Revenant: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Revenant/RevenantMarkAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantBlueprint"
],
Garuda: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Garuda/GarudaUnstoppableAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaBlueprint"
],
Baruuk: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Pacifist/PacifistFistAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistBlueprint"
],
Hildryn: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeChassisBlueprint",
"/Lotus/StoreItems/Powersuits/IronFrame/IronFrameStripAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeBlueprint"
]
};
const generateNormalModeRewards = (choices: string[]): IEndlessXpReward[] => {
const choiceRewards = normalModeChosenRewards[choices[0]];
return [
{
RequiredTotalXp: 190,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalSilverRewards"
)
},
{
RequiredTotalXp: 400,
Rewards: [
{
StoreItem: choiceRewards[0],
ItemCount: 1
}
]
},
{
RequiredTotalXp: 630,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalSilverRewards"
)
},
{
RequiredTotalXp: 890,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalMODRewards"
)
},
{
RequiredTotalXp: 1190,
Rewards: [
{
StoreItem: choiceRewards[1],
ItemCount: 1
}
]
},
{
RequiredTotalXp: 1540,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalGoldRewards"
)
},
{
RequiredTotalXp: 1950,
Rewards: [
{
StoreItem: choiceRewards[2],
ItemCount: 1
}
]
},
{
RequiredTotalXp: 2430,
Rewards: [
{
StoreItem: choiceRewards[3],
ItemCount: 1
}
]
},
{
RequiredTotalXp: 2990,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalArcaneRewards"
)
},
{
RequiredTotalXp: 3640,
Rewards: [
{
StoreItem: choiceRewards[4],
ItemCount: 1
}
]
}
];
};
const hardModeChosenRewards: Record<string, string> = {
Braton: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BratonIncarnonUnlocker",
Lato: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/LatoIncarnonUnlocker",
Skana: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/SkanaIncarnonUnlocker",
Paris: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/ParisIncarnonUnlocker",
Kunai: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/KunaiIncarnonUnlocker",
Boar: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BoarIncarnonUnlocker",
Gammacor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/GammacorIncarnonUnlocker",
Anku: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/AnkuIncarnonUnlocker",
Gorgon: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/GorgonIncarnonUnlocker",
Angstrum: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/AngstrumIncarnonUnlocker",
Bo: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/BoIncarnonUnlocker",
Latron: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/LatronIncarnonUnlocker",
Furis: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/FurisIncarnonUnlocker",
Furax: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/FuraxIncarnonUnlocker",
Strun: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/StrunIncarnonUnlocker",
Lex: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/LexIncarnonUnlocker",
Magistar: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/MagistarIncarnonUnlocker",
Boltor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BoltorIncarnonUnlocker",
Bronco: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/BroncoIncarnonUnlocker",
CeramicDagger: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/CeramicDaggerIncarnonUnlocker",
Torid: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/ToridIncarnonUnlocker",
DualToxocyst: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/DualToxocystIncarnonUnlocker",
DualIchor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/DualIchorIncarnonUnlocker",
Miter: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/MiterIncarnonUnlocker",
Atomos: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/AtomosIncarnonUnlocker",
AckAndBrunt: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/AckAndBruntIncarnonUnlocker",
Soma: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/SomaIncarnonUnlocker",
Vasto: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/VastoIncarnonUnlocker",
NamiSolo: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/NamiSoloIncarnonUnlocker",
Burston: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BurstonIncarnonUnlocker",
Zylok: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/ZylokIncarnonUnlocker",
Sibear: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/SibearIncarnonUnlocker",
Dread: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/DreadIncarnonUnlocker",
Despair: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/DespairIncarnonUnlocker",
Hate: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/HateIncarnonUnlocker",
Dera: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/DeraIncarnonUnlocker",
Cestra: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/CestraIncarnonUnlocker",
Okina: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/OkinaIncarnonUnlocker",
Sybaris: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/SybarisIncarnonUnlocker",
Sicarus: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/SicarusIncarnonUnlocker",
RivenPrimary: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawRifleRandomMod",
RivenSecondary: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawPistolRandomMod",
RivenMelee: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawMeleeRandomMod",
Kuva: "/Lotus/Types/Game/DuviriEndless/CircuitSteelPathBIGKuvaReward"
};
const generateHardModeRewards = (choices: string[]): IEndlessXpReward[] => {
return [
{
RequiredTotalXp: 285,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards"
)
},
{
RequiredTotalXp: 600,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathArcaneRewards"
)
},
{
RequiredTotalXp: 945,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards"
)
},
{
RequiredTotalXp: 1335,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards"
)
},
{
RequiredTotalXp: 1785,
Rewards: [
{
StoreItem: hardModeChosenRewards[choices[0]],
ItemCount: 1
}
]
},
{
RequiredTotalXp: 2310,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathGoldRewards"
)
},
{
RequiredTotalXp: 2925,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathGoldRewards"
)
},
{
RequiredTotalXp: 3645,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathArcaneRewards"
)
},
{
RequiredTotalXp: 4485,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSteelEssenceRewards"
)
},
{
RequiredTotalXp: 5460,
Rewards: [
{
StoreItem: hardModeChosenRewards[choices[1]],
ItemCount: 1
}
]
}
];
};

View File

@ -3,7 +3,6 @@ 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 { getShip } from "@/src/services/shipService";
import { toOid } from "@/src/helpers/inventoryHelpers";
import { IGetShipResponse } from "@/src/types/shipTypes";
import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
@ -21,7 +20,6 @@ export const getShipController: RequestHandler = async (req, res) => {
const personalRooms = personalRoomsDb.toJSON<IPersonalRoomsClient>();
const loadout = await getLoadout(accountId);
const ship = await getShip(personalRoomsDb.activeShipId, "ShipAttachments SkinFlavourItem");
const getShipResponse: IGetShipResponse = {
ShipOwnerId: accountId,
@ -31,8 +29,8 @@ export const getShipController: RequestHandler = async (req, res) => {
ShipId: toOid(personalRoomsDb.activeShipId),
ShipInterior: {
Colors: personalRooms.ShipInteriorColors,
ShipAttachments: ship.ShipAttachments,
SkinFlavourItem: ship.SkinFlavourItem
ShipAttachments: { HOOD_ORNAMENT: "" },
SkinFlavourItem: ""
},
FavouriteLoadoutId: personalRooms.Ship.FavouriteLoadoutId
? toOid(personalRooms.Ship.FavouriteLoadoutId)

View File

@ -2,12 +2,13 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Account } from "@/src/models/loginModel";
import { areFriends } from "@/src/services/friendService";
import { createMessage } from "@/src/services/inboxService";
import { getInventory, updateCurrency } from "@/src/services/inventoryService";
import { combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { IOid } from "@/src/types/commonTypes";
import { IPurchaseParams } from "@/src/types/purchaseTypes";
import { IInventoryChanges, IPurchaseParams } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express";
import { ExportFlavour } from "warframe-public-export-plus";
import { ExportBundles, ExportFlavour } from "warframe-public-export-plus";
export const giftingController: RequestHandler = async (req, res) => {
const data = getJSONfromString<IGiftingRequest>(String(req.body));
@ -44,10 +45,7 @@ export const giftingController: RequestHandler = async (req, res) => {
// TODO: Cannot gift archwing items to players that have not completed the archwing quest. (Code 7)
// TODO: Cannot gift necramechs to players that have not completed heart of deimos. (Code 20)
const senderInventory = await getInventory(
senderAccount._id.toString(),
"PremiumCredits PremiumCreditsFree ActiveAvatarImageType GiftsRemaining"
);
const senderInventory = await getInventory(senderAccount._id.toString());
if (senderInventory.GiftsRemaining == 0) {
res.status(400).send("10").end();
@ -55,7 +53,20 @@ export const giftingController: RequestHandler = async (req, res) => {
}
senderInventory.GiftsRemaining -= 1;
updateCurrency(senderInventory, data.PurchaseParams.ExpectedPrice, true);
const inventoryChanges: IInventoryChanges = updateCurrency(
senderInventory,
data.PurchaseParams.ExpectedPrice,
true
);
if (data.PurchaseParams.StoreItem in ExportBundles) {
const bundle = ExportBundles[data.PurchaseParams.StoreItem];
if (bundle.giftingBonus) {
combineInventoryChanges(
inventoryChanges,
(await handleStoreItemAcquisition(bundle.giftingBonus, senderInventory)).InventoryChanges
);
}
}
await senderInventory.save();
const senderName = getSuffixedName(senderAccount);
@ -83,7 +94,9 @@ export const giftingController: RequestHandler = async (req, res) => {
}
]);
res.end();
res.json({
InventoryChanges: inventoryChanges
});
};
interface IGiftingRequest {

View File

@ -104,7 +104,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
) {
throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`);
}
if (!inventory[getSalvageCategory(data.TechProductCategory)].id(data.CategoryItemId)) {
if (!inventory[getSalvageCategory(data.TechProductCategory)].id(data.CategoryItemId!)) {
throw new Error(
`no item with id ${data.CategoryItemId} in ${getSalvageCategory(data.TechProductCategory)} array`
);

View File

@ -25,10 +25,10 @@ import { logger } from "@/src/utils/logger";
import { catBreadHash } from "@/src/helpers/stringHelpers";
import { Types } from "mongoose";
import { isNemesisCompatibleWithVersion } from "@/src/helpers/nemesisHelpers";
import { version_compare } from "@/src/services/worldStateService";
import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
import { Ship } from "@/src/models/shipModel";
import { toLegacyOid, version_compare } from "@/src/helpers/inventoryHelpers";
export const inventoryController: RequestHandler = async (request, response) => {
const account = await getAccountForRequest(request);
@ -306,19 +306,34 @@ export const getInventoryResponse = async (
// Set 2FA enabled so trading post can be used
inventoryResponse.HWIDProtectEnabled = true;
// Fix nemesis for older versions
if (
inventoryResponse.Nemesis &&
buildLabel &&
!isNemesisCompatibleWithVersion(inventoryResponse.Nemesis, buildLabel)
) {
inventoryResponse.Nemesis = undefined;
}
if (buildLabel) {
// Fix nemesis for older versions
if (inventoryResponse.Nemesis && !isNemesisCompatibleWithVersion(inventoryResponse.Nemesis, buildLabel)) {
inventoryResponse.Nemesis = undefined;
}
if (buildLabel && version_compare(buildLabel, "2018.02.22.14.34") < 0) {
const personalRoomsDb = await getPersonalRooms(inventory.accountOwnerId.toString());
const personalRooms = personalRoomsDb.toJSON<IPersonalRoomsClient>();
inventoryResponse.Ship = personalRooms.Ship;
if (version_compare(buildLabel, "2018.02.22.14.34") < 0) {
const personalRoomsDb = await getPersonalRooms(inventory.accountOwnerId.toString());
const personalRooms = personalRoomsDb.toJSON<IPersonalRoomsClient>();
inventoryResponse.Ship = personalRooms.Ship;
if (version_compare(buildLabel, "2016.12.21.19.13") <= 0) {
// U19.5 and below use $id instead of $oid
for (const category of equipmentKeys) {
for (const item of inventoryResponse[category]) {
toLegacyOid(item.ItemId);
}
}
for (const upgrade of inventoryResponse.Upgrades) {
toLegacyOid(upgrade.ItemId);
}
if (inventoryResponse.BrandedSuits) {
for (const id of inventoryResponse.BrandedSuits) {
toLegacyOid(id);
}
}
}
}
}
return inventoryResponse;

View File

@ -7,7 +7,7 @@ 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";
import { version_compare } from "@/src/helpers/inventoryHelpers";
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

View File

@ -61,7 +61,11 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
if (
missionReport.MissionStatus !== "GS_SUCCESS" &&
!(missionReport.RewardInfo?.jobId || missionReport.RewardInfo?.challengeMissionId)
!(
missionReport.RewardInfo?.jobId ||
missionReport.RewardInfo?.challengeMissionId ||
missionReport.RewardInfo?.T
)
) {
if (missionReport.EndOfMatchUpload) {
inventory.RewardSeed = generateRewardSeed();

View File

@ -2,7 +2,7 @@ import { RequestHandler } from "express";
import { ExportWeapons } from "warframe-public-export-plus";
import { IMongoDate } from "@/src/types/commonTypes";
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { CRng } from "@/src/services/rngService";
import { SRng } from "@/src/services/rngService";
import { ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import {
@ -140,7 +140,7 @@ const getModularWeaponSale = (
partTypes: string[],
getItemType: (parts: string[]) => string
): IModularWeaponSaleInfo => {
const rng = new CRng(day);
const rng = new SRng(day);
const parts = partTypes.map(partType => rng.randomElement(partTypeToParts[partType])!);
let partsCost = 0;
for (const part of parts) {

View File

@ -24,7 +24,8 @@ import {
IUpgradeClient,
IWeaponSkinClient,
LoadoutIndex,
TEquipmentKey
TEquipmentKey,
TNemesisFaction
} from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
@ -269,7 +270,7 @@ interface INemesisStartRequest {
WeaponIdx: number;
AgentIdx: number;
BirthNode: string;
Faction: string;
Faction: TNemesisFaction;
Rank: number;
k: boolean;
Traded: boolean;

View File

@ -2,13 +2,16 @@ import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { ExportRelics, IRelic } from "warframe-public-export-plus";
import { config } from "@/src/services/configService";
export const projectionManagerController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const request = JSON.parse(String(req.body)) as IProjectionUpgradeRequest;
const [era, category, currentQuality] = parseProjection(request.projectionType);
const upgradeCost = (request.qualityTag - qualityKeywordToNumber[currentQuality]) * 25;
const upgradeCost = config.dontSubtractVoidTraces
? 0
: (request.qualityTag - qualityKeywordToNumber[currentQuality]) * 25;
const newProjectionType = findProjection(era, category, qualityNumberToKeyword[request.qualityTag]);
addMiscItems(inventory, [
{

View File

@ -1,8 +1,13 @@
import { toOid } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Friendship } from "@/src/models/friendModel";
import { Account } from "@/src/models/loginModel";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IOid } from "@/src/types/commonTypes";
import { parallelForeach } from "@/src/utils/async-utils";
import { RequestHandler } from "express";
import { Types } from "mongoose";
export const removeFriendGetController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -22,7 +27,7 @@ export const removeFriendGetController: RequestHandler = async (req, res) => {
await Promise.all(promises);
res.json({
Friends: friends
});
} satisfies IRemoveFriendsResponse);
} else {
const friendId = req.query.friendId as string;
await Promise.all([
@ -30,7 +35,65 @@ export const removeFriendGetController: RequestHandler = async (req, res) => {
Friendship.deleteOne({ owner: friendId, friend: accountId })
]);
res.json({
Friends: [{ $oid: friendId } satisfies IOid]
});
Friends: [{ $oid: friendId }]
} satisfies IRemoveFriendsResponse);
}
};
export const removeFriendPostController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const data = getJSONfromString<IBatchRemoveFriendsRequest>(String(req.body));
const friends = new Set((await Friendship.find({ owner: accountId }, "friend")).map(x => x.friend));
// TOVERIFY: Should pending friendships also be kept?
// Keep friends that have been online within threshold
await parallelForeach([...friends], async friend => {
const account = (await Account.findById(friend, "LastLogin"))!;
const daysLoggedOut = (Date.now() - account.LastLogin.getTime()) / 86400_000;
if (daysLoggedOut < data.DaysLoggedOut) {
friends.delete(friend);
}
});
if (data.SkipClanmates) {
const inventory = await getInventory(accountId, "GuildId");
if (inventory.GuildId) {
await parallelForeach([...friends], async friend => {
const friendInventory = await getInventory(friend.toString(), "GuildId");
if (friendInventory.GuildId?.equals(inventory.GuildId)) {
friends.delete(friend);
}
});
}
}
// Remove all remaining friends that aren't in SkipFriendIds & give response.
const promises = [];
const response: IOid[] = [];
for (const friend of friends) {
if (!data.SkipFriendIds.find(skipFriendId => checkFriendId(skipFriendId, friend))) {
promises.push(Friendship.deleteOne({ owner: accountId, friend: friend }));
promises.push(Friendship.deleteOne({ owner: friend, friend: accountId }));
response.push(toOid(friend));
}
}
await Promise.all(promises);
res.json({
Friends: response
} satisfies IRemoveFriendsResponse);
};
// The friend ids format is a bit weird, e.g. when 6633b81e9dba0b714f28ff02 (A) is friends with 67cdac105ef1f4b49741c267 (B), A's friend id for B is 808000105ef1f40560ca079e and B's friend id for A is 8000b81e9dba0b06408a8075.
const checkFriendId = (friendId: string, b: Types.ObjectId): boolean => {
return friendId.substring(6, 6 + 8) == b.toString().substring(6, 6 + 8);
};
interface IBatchRemoveFriendsRequest {
DaysLoggedOut: number;
SkipClanmates: boolean;
SkipFriendIds: string[];
}
interface IRemoveFriendsResponse {
Friends: IOid[];
}

View File

@ -1,8 +1,7 @@
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { config } from "@/src/services/configService";
import { addEmailItem, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { addEmailItem, getDialogue, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { ICompletedDialogue, IDialogueDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
import { ICompletedDialogue } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express";
@ -107,26 +106,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

@ -2,11 +2,12 @@ import { RequestHandler } from "express";
import { ISaveLoadoutRequest } from "@/src/types/saveLoadoutTypes";
import { handleInventoryItemConfigChange } from "@/src/services/saveLoadoutService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
export const saveLoadoutController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const body: ISaveLoadoutRequest = JSON.parse(req.body as string) as ISaveLoadoutRequest;
const body: ISaveLoadoutRequest = getJSONfromString<ISaveLoadoutRequest>(String(req.body));
// console.log(util.inspect(body, { showHidden: false, depth: null, colors: true }));
// eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@ -13,7 +13,7 @@ export const setDojoComponentSettingsController: RequestHandler = async (req, re
res.json({ DojoRequestStatus: -1 });
return;
}
const component = guild.DojoComponents.id(req.query.componentId)!;
const component = guild.DojoComponents.id(req.query.componentId as string)!;
const data = getJSONfromString<ISetDojoComponentSettingsRequest>(String(req.body));
component.Settings = data.Settings;
await guild.save();

View File

@ -1,8 +1,8 @@
import { version_compare } from "@/src/helpers/inventoryHelpers";
import { Alliance, Guild, GuildMember } from "@/src/models/guildModel";
import { hasGuildPermissionEx } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { version_compare } from "@/src/services/worldStateService";
import { GuildPermission, ILongMOTD } from "@/src/types/guildTypes";
import { RequestHandler } from "express";

View File

@ -1,5 +1,6 @@
import { applyClientEquipmentUpdates, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IOid } from "@/src/types/commonTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
@ -11,7 +12,7 @@ export const addXpController: RequestHandler = async (req, res) => {
const request = req.body as IAddXpRequest;
for (const [category, gear] of Object.entries(request)) {
for (const clientItem of gear) {
const dbItem = inventory[category as TEquipmentKey].id(clientItem.ItemId.$oid);
const dbItem = inventory[category as TEquipmentKey].id((clientItem.ItemId as IOid).$oid);
if (dbItem) {
if (dbItem.ItemType in ExportMisc.uniqueLevelCaps) {
if ((dbItem.Polarized ?? 0) < 5) {

View File

@ -1,9 +1,46 @@
import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { IMongoDate, IOid, IOidWithLegacySupport } from "@/src/types/commonTypes";
import { Types } from "mongoose";
import { TRarity } from "warframe-public-export-plus";
export const version_compare = (a: string, b: string): number => {
const a_digits = a
.split("/")[0]
.split(".")
.map(x => parseInt(x));
const b_digits = b
.split("/")[0]
.split(".")
.map(x => parseInt(x));
for (let i = 0; i != a_digits.length; ++i) {
if (a_digits[i] != b_digits[i]) {
return a_digits[i] > b_digits[i] ? 1 : -1;
}
}
return 0;
};
export const toOid = (objectId: Types.ObjectId): IOid => {
return { $oid: objectId.toString() } satisfies IOid;
return { $oid: objectId.toString() };
};
export function toOid2(objectId: Types.ObjectId, buildLabel: undefined): IOid;
export function toOid2(objectId: Types.ObjectId, buildLabel: string | undefined): IOidWithLegacySupport;
export function toOid2(objectId: Types.ObjectId, buildLabel: string | undefined): IOidWithLegacySupport {
if (buildLabel && version_compare(buildLabel, "2016.12.21.19.13") <= 0) {
return { $id: objectId.toString() };
}
return { $oid: objectId.toString() };
}
export const toLegacyOid = (oid: IOidWithLegacySupport): void => {
if (!("$id" in oid)) {
oid.$id = oid.$oid;
delete oid.$oid;
}
};
export const fromOid = (oid: IOidWithLegacySupport): string => {
return (oid.$oid ?? oid.$id)!;
};
export const toMongoDate = (date: Date): IMongoDate => {

View File

@ -1,16 +1,17 @@
import { ExportRegions, ExportWarframes } from "warframe-public-export-plus";
import { IInfNode, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInfNode, ITypeCount, TNemesisFaction } 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, version_compare } from "../services/worldStateService";
import { isArchwingMission } from "../services/worldStateService";
import { fromStoreItem, toStoreItem } from "../services/itemDataService";
import { createMessage } from "../services/inboxService";
import { version_compare } from "./inventoryHelpers";
export const getInfNodes = (faction: string, rank: number): IInfNode[] => {
export const getInfNodes = (faction: TNemesisFaction, rank: number): IInfNode[] => {
const infNodes = [];
const systemIndex = systemIndexes[faction][rank];
for (const [key, value] of Object.entries(ExportRegions)) {
@ -34,20 +35,56 @@ export const getInfNodes = (faction: string, rank: number): IInfNode[] => {
return infNodes;
};
const systemIndexes: Record<string, number[]> = {
const systemIndexes: Record<TNemesisFaction, number[]> = {
FC_GRINEER: [2, 3, 9, 11, 18],
FC_CORPUS: [1, 15, 4, 7, 8],
FC_INFESTATION: [23]
};
export const showdownNodes: Record<string, string> = {
export const showdownNodes: Record<TNemesisFaction, string> = {
FC_GRINEER: "CrewBattleNode557",
FC_CORPUS: "CrewBattleNode558",
FC_INFESTATION: "CrewBattleNode559"
};
const ephemeraProbabilities: Record<TNemesisFaction, number> = {
FC_GRINEER: 0.05,
FC_CORPUS: 0.2,
FC_INFESTATION: 0
};
type TInnateDamageTag =
| "InnateElectricityDamage"
| "InnateHeatDamage"
| "InnateFreezeDamage"
| "InnateToxinDamage"
| "InnateMagDamage"
| "InnateRadDamage"
| "InnateImpactDamage";
const ephmeraTypes: Record<"FC_GRINEER" | "FC_CORPUS", Record<TInnateDamageTag, string>> = {
FC_GRINEER: {
InnateElectricityDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaLightningEphemera",
InnateHeatDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaFireEphemera",
InnateFreezeDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaIceEphemera",
InnateToxinDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaToxinEphemera",
InnateMagDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaMagneticEphemera",
InnateRadDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaTricksterEphemera",
InnateImpactDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaImpactEphemera"
},
FC_CORPUS: {
InnateElectricityDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraA",
InnateHeatDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraB",
InnateFreezeDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraC",
InnateToxinDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraD",
InnateMagDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraE",
InnateRadDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraF",
InnateImpactDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraG"
}
};
// 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[] => {
export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: TNemesisFaction }): number[] => {
const rng = new SRng(nemesis.fp);
const choices = [0, 1, 2, 3, 5, 6, 7];
let choiceIndex = rng.randomInt(0, choices.length - 1);
@ -86,7 +123,7 @@ const antivirusMods: readonly string[] = [
"/Lotus/Upgrades/Mods/Immortal/AntivirusEightMod"
];
export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: string }): string[] => {
export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: TNemesisFaction }): string[] => {
const passcode = getNemesisPasscode(nemesis);
return nemesis.Faction == "FC_INFESTATION"
? passcode.map(i => antivirusMods[i])
@ -247,7 +284,7 @@ export const getWeaponsForManifest = (manifest: string): readonly string[] => {
};
export const isNemesisCompatibleWithVersion = (
nemesis: { manifest: string; Faction: string },
nemesis: { manifest: string; Faction: TNemesisFaction },
buildLabel: string
): boolean => {
// Anything below 35.6.0 is not going to be okay given our set of supported manifests.
@ -270,21 +307,25 @@ export const isNemesisCompatibleWithVersion = (
return true;
};
export const getInnateDamageTag = (
KillingSuit: string
):
| "InnateElectricityDamage"
| "InnateFreezeDamage"
| "InnateHeatDamage"
| "InnateImpactDamage"
| "InnateMagDamage"
| "InnateRadDamage"
| "InnateToxinDamage" => {
export const getInnateDamageTag = (KillingSuit: string): TInnateDamageTag => {
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 => {
export interface INemesisProfile {
innateDamageTag: TInnateDamageTag;
innateDamageValue: number;
ephemera?: string;
petHead?: string;
petBody?: string;
petLegs?: string;
petTail?: string;
}
export const generateNemesisProfile = (
fp: bigint = generateRewardSeed(),
Faction: TNemesisFaction = "FC_CORPUS",
killingSuit: string = "/Lotus/Powersuits/Ember/Ember"
): INemesisProfile => {
const rng = new SRng(fp);
rng.randomFloat(); // used for the weapon index
const WeaponUpgradeValueAttenuationExponent = 2.25;
@ -292,7 +333,37 @@ export const getInnateDamageValue = (fp: bigint): number => {
if (value >= 0.941428) {
value = 1;
}
return Math.trunc(value * 0x40000000);
const profile: INemesisProfile = {
innateDamageTag: getInnateDamageTag(killingSuit),
innateDamageValue: Math.trunc(value * 0x40000000) // TODO: For -1399275245665749231n, the value should be 75306944, but we're off by 59 with 75307003.
};
if (rng.randomFloat() <= ephemeraProbabilities[Faction] && Faction != "FC_INFESTATION") {
profile.ephemera = ephmeraTypes[Faction][profile.innateDamageTag];
}
rng.randomFloat(); // something related to sentinel agent maybe
if (Faction == "FC_CORPUS") {
profile.petHead = rng.randomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC"
])!;
profile.petBody = rng.randomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyC"
])!;
profile.petLegs = rng.randomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsC"
])!;
profile.petTail = rng.randomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailC"
])!;
}
return profile;
};
export const getKillTokenRewardCount = (fp: bigint): number => {

View File

@ -38,7 +38,8 @@ import {
IPeriodicMissionCompletionResponse,
ILoreFragmentScan,
IEvolutionProgress,
IEndlessXpProgress,
IEndlessXpProgressDatabase,
IEndlessXpProgressClient,
ICrewShipCustomization,
ICrewShipWeapon,
ICrewShipWeaponEmplacements,
@ -97,7 +98,8 @@ import {
IInvasionProgressClient,
IAccolades,
IHubNpcCustomization,
ILotusCustomization
ILotusCustomization,
IEndlessXpReward
} from "../../types/inventoryTypes/inventoryTypes";
import { IOid } from "../../types/commonTypes";
import {
@ -112,6 +114,7 @@ import {
} from "@/src/types/inventoryTypes/commonInventoryTypes";
import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
import { EquipmentSelectionSchema, oidSchema } from "./loadoutModel";
import { ICountedStoreItem } from "warframe-public-export-plus";
export const typeCountSchema = new Schema<ITypeCount>({ ItemType: String, ItemCount: Number }, { _id: false });
@ -810,14 +813,48 @@ const evolutionProgressSchema = new Schema<IEvolutionProgress>(
{ _id: false }
);
const endlessXpProgressSchema = new Schema<IEndlessXpProgress>(
const countedStoreItemSchema = new Schema<ICountedStoreItem>(
{
Category: String,
Choices: [String]
StoreItem: String,
ItemCount: Number
},
{ _id: false }
);
const endlessXpRewardSchema = new Schema<IEndlessXpReward>(
{
RequiredTotalXp: Number,
Rewards: [countedStoreItemSchema]
},
{ _id: false }
);
const endlessXpProgressSchema = new Schema<IEndlessXpProgressDatabase>(
{
Category: { type: String, required: true },
Earn: { type: Number, default: 0 },
Claim: { type: Number, default: 0 },
BonusAvailable: Date,
Expiry: Date,
Choices: { type: [String], required: true },
PendingRewards: { type: [endlessXpRewardSchema], default: [] }
},
{ _id: false }
);
endlessXpProgressSchema.set("toJSON", {
transform(_doc, ret) {
const db = ret as IEndlessXpProgressDatabase;
const client = ret as IEndlessXpProgressClient;
if (db.BonusAvailable) {
client.BonusAvailable = toMongoDate(db.BonusAvailable);
}
if (db.Expiry) {
client.Expiry = toMongoDate(db.Expiry);
}
}
});
const crewShipWeaponEmplacementsSchema = new Schema<ICrewShipWeaponEmplacements>(
{
PRIMARY_A: EquipmentSelectionSchema,
@ -1281,7 +1318,7 @@ const nemesisSchema = new Schema<INemesisDatabase>(
InfNodes: { type: [infNodeSchema], default: undefined },
HenchmenKilled: Number,
HintProgress: Number,
Hints: { type: [Number], default: undefined },
Hints: { type: [Number], default: [] },
GuessHistory: { type: [Number], default: undefined },
MissionCount: Number,
LastEnc: Number

View File

@ -103,7 +103,7 @@ import { questControlController } from "@/src/controllers/api/questControlContro
import { queueDojoComponentDestructionController } from "@/src/controllers/api/queueDojoComponentDestructionController";
import { redeemPromoCodeController } from "@/src/controllers/api/redeemPromoCodeController";
import { releasePetController } from "@/src/controllers/api/releasePetController";
import { removeFriendGetController } from "@/src/controllers/api/removeFriendController";
import { removeFriendGetController, removeFriendPostController } from "@/src/controllers/api/removeFriendController";
import { removeFromAllianceController } from "@/src/controllers/api/removeFromAllianceController";
import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController";
import { removeIgnoredUserController } from "@/src/controllers/api/removeIgnoredUserController";
@ -187,6 +187,7 @@ apiRouter.get("/getIgnoredUsers.php", getIgnoredUsersController);
apiRouter.get("/getMessages.php", inboxController); // unsure if this is correct, but needed for U17
apiRouter.get("/getNewRewardSeed.php", getNewRewardSeedController);
apiRouter.get("/getShip.php", getShipController);
apiRouter.get("/getShipDecos.php", (_req, res) => { res.end(); }); // needed to log in on U22.8
apiRouter.get("/getVendorInfo.php", getVendorInfoController);
apiRouter.get("/hub", hubController);
apiRouter.get("/hubInstances", hubInstancesController);
@ -290,6 +291,7 @@ apiRouter.post("/purchase.php", purchaseController);
apiRouter.post("/questControl.php", questControlController); // U17
apiRouter.post("/redeemPromoCode.php", redeemPromoCodeController);
apiRouter.post("/releasePet.php", releasePetController);
apiRouter.post("/removeFriend.php", removeFriendPostController);
apiRouter.post("/removeFromGuild.php", removeFromGuildController);
apiRouter.post("/removeIgnoredUser.php", removeIgnoredUserController);
apiRouter.post("/rerollRandomMod.php", rerollRandomModController);

View File

@ -24,6 +24,8 @@ interface IConfig {
infiniteEndo?: boolean;
infiniteRegalAya?: boolean;
infiniteHelminthMaterials?: boolean;
claimingBlueprintRefundsIngredients?: boolean;
dontSubtractVoidTraces?: boolean;
dontSubtractConsumables?: boolean;
unlockAllShipFeatures?: boolean;
unlockAllShipDecorations?: boolean;

View File

@ -377,9 +377,6 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
db[key] = client[key];
}
}
if (client.EndlessXP !== undefined) {
db.EndlessXP = client.EndlessXP;
}
if (client.SongChallenges !== undefined) {
db.SongChallenges = client.SongChallenges;
}

View File

@ -28,7 +28,8 @@ import {
ITraits,
ICalendarProgress,
INemesisWeaponTargetFingerprint,
INemesisPetTargetFingerprint
INemesisPetTargetFingerprint,
IDialogueDatabase
} from "@/src/types/inventoryTypes/inventoryTypes";
import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate";
import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes";
@ -70,6 +71,7 @@ import { createShip } from "./shipService";
import {
catbrowDetails,
fromMongoDate,
fromOid,
kubrowDetails,
kubrowFurPatternsWeights,
kubrowWeights,
@ -82,7 +84,7 @@ import { getRandomElement, getRandomInt, getRandomWeightedReward, SRng } from ".
import { createMessage } from "./inboxService";
import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
import { getWorldState } from "./worldStateService";
import { getInnateDamageTag, getInnateDamageValue } from "../helpers/nemesisHelpers";
import { generateNemesisProfile, INemesisProfile } from "../helpers/nemesisHelpers";
export const createInventory = async (
accountOwnerId: Types.ObjectId,
@ -423,7 +425,7 @@ export const addItem = async (
changes.push({
ItemType: egg.ItemType,
ExpirationDate: { $date: { $numberLong: "2000000000000" } },
ItemId: toOid(egg._id)
ItemId: toOid(egg._id) // TODO: Pass on buildLabel from purchaseService
});
}
return {
@ -869,10 +871,14 @@ const addSentinel = (
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const configs: IItemConfig[] = applyDefaultUpgrades(inventory, ExportSentinels[sentinelName]?.defaultUpgrades);
const features = premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined;
const sentinelIndex =
inventory.Sentinels.push({ ItemType: sentinelName, Configs: configs, XP: 0, Features: features, IsNew: true }) -
1;
inventory.Sentinels.push({
ItemType: sentinelName,
Configs: configs,
XP: 0,
Features: premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined,
IsNew: inventory.Sentinels.find(x => x.ItemType == sentinelName) ? undefined : true
}) - 1;
inventoryChanges.Sentinels ??= [];
inventoryChanges.Sentinels.push(inventory.Sentinels[sentinelIndex].toJSON<IEquipmentClient>());
@ -921,6 +927,9 @@ export const addPowerSuit = async (
},
defaultOverwrites
);
if (suit.IsNew) {
suit.IsNew = !inventory.Suits.find(x => x.ItemType == powersuitName);
}
if (!suit.IsNew) {
suit.IsNew = undefined;
}
@ -955,7 +964,7 @@ export const addMechSuit = async (
UpgradeVer: 101,
XP: 0,
Features: features,
IsNew: true
IsNew: inventory.MechSuits.find(x => x.ItemType == mechsuitName) ? undefined : true
}) - 1;
inventoryChanges.MechSuits ??= [];
inventoryChanges.MechSuits.push(inventory.MechSuits[suitIndex].toJSON<IEquipmentClient>());
@ -995,7 +1004,7 @@ export const addSpaceSuit = (
UpgradeVer: 101,
XP: 0,
Features: features,
IsNew: true
IsNew: inventory.SpaceSuits.find(x => x.ItemType == spacesuitName) ? undefined : true
}) - 1;
inventoryChanges.SpaceSuits ??= [];
inventoryChanges.SpaceSuits.push(inventory.SpaceSuits[suitIndex].toJSON<IEquipmentClient>());
@ -1077,7 +1086,7 @@ export const addKubrowPet = (
Configs: configs,
XP: 0,
Details: details,
IsNew: true
IsNew: inventory.KubrowPets.find(x => x.ItemType == kubrowPetName) ? undefined : true
}) - 1;
inventoryChanges.KubrowPets ??= [];
inventoryChanges.KubrowPets.push(inventory.KubrowPets[kubrowPetIndex].toJSON<IEquipmentClient>());
@ -1105,24 +1114,29 @@ const isCurrencyTracked = (usePremium: boolean): boolean => {
export const updateCurrency = (
inventory: TInventoryDatabaseDocument,
price: number,
usePremium: boolean
usePremium: boolean,
inventoryChanges: IInventoryChanges = {}
): IInventoryChanges => {
const currencyChanges: IInventoryChanges = {};
if (price != 0 && isCurrencyTracked(usePremium)) {
if (usePremium) {
if (inventory.PremiumCreditsFree > 0) {
currencyChanges.PremiumCreditsFree = Math.min(price, inventory.PremiumCreditsFree) * -1;
inventory.PremiumCreditsFree += currencyChanges.PremiumCreditsFree;
const premiumCreditsFreeDelta = Math.min(price, inventory.PremiumCreditsFree) * -1;
inventoryChanges.PremiumCreditsFree ??= 0;
inventoryChanges.PremiumCreditsFree += premiumCreditsFreeDelta;
inventory.PremiumCreditsFree += premiumCreditsFreeDelta;
}
currencyChanges.PremiumCredits = -price;
inventory.PremiumCredits += currencyChanges.PremiumCredits;
inventoryChanges.PremiumCredits ??= 0;
inventoryChanges.PremiumCredits -= price;
inventory.PremiumCredits -= price;
logger.debug(`currency changes `, { PremiumCredits: -price });
} else {
currencyChanges.RegularCredits = -price;
inventory.RegularCredits += currencyChanges.RegularCredits;
inventoryChanges.RegularCredits ??= 0;
inventoryChanges.RegularCredits -= price;
inventory.RegularCredits -= price;
logger.debug(`currency changes `, { RegularCredits: -price });
}
logger.debug(`currency changes `, currencyChanges);
}
return currencyChanges;
return inventoryChanges;
};
export const addFusionPoints = (inventory: TInventoryDatabaseDocument, add: number): number => {
@ -1257,6 +1271,9 @@ export const addEquipment = (
},
defaultOverwrites
);
if (equipment.IsNew) {
equipment.IsNew = !inventory[category].find(x => x.ItemType == type);
}
if (!equipment.IsNew) {
equipment.IsNew = undefined;
}
@ -1491,9 +1508,9 @@ export const applyClientEquipmentUpdates = (
const category = inventory[categoryName];
gearArray.forEach(({ ItemId, XP, InfestationDate }) => {
const item = category.id(ItemId.$oid);
const item = category.id(fromOid(ItemId));
if (!item) {
throw new Error(`No item with id ${ItemId.$oid} in ${categoryName}`);
throw new Error(`No item with id ${fromOid(ItemId)} in ${categoryName}`);
}
if (XP) {
@ -1569,12 +1586,17 @@ export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray:
if (MiscItems[itemIndex].ItemCount == 0) {
MiscItems.splice(itemIndex, 1);
} else if (MiscItems[itemIndex].ItemCount <= 0) {
logger.warn(`account now owns a negative amount of ${ItemType}`);
logger.warn(`inventory.MiscItems has a negative count for ${ItemType}`);
}
});
};
const applyArrayChanges = (arr: ITypeCount[], changes: ITypeCount[]): void => {
const applyArrayChanges = (
inventory: TInventoryDatabaseDocument,
key: "ShipDecorations" | "Consumables" | "CrewShipRawSalvage" | "CrewShipAmmo" | "Recipes" | "LevelKeys",
changes: ITypeCount[]
): void => {
const arr: ITypeCount[] = inventory[key];
for (const change of changes) {
if (change.ItemCount != 0) {
let itemIndex = arr.findIndex(x => x.ItemType === change.ItemType);
@ -1586,34 +1608,34 @@ const applyArrayChanges = (arr: ITypeCount[], changes: ITypeCount[]): void => {
if (arr[itemIndex].ItemCount == 0) {
arr.splice(itemIndex, 1);
} else if (arr[itemIndex].ItemCount <= 0) {
logger.warn(`account now owns a negative amount of ${change.ItemType}`);
logger.warn(`inventory.${key} has a negative count for ${change.ItemType}`);
}
}
}
};
export const addShipDecorations = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => {
applyArrayChanges(inventory.ShipDecorations, itemsArray);
applyArrayChanges(inventory, "ShipDecorations", itemsArray);
};
export const addConsumables = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => {
applyArrayChanges(inventory.Consumables, itemsArray);
applyArrayChanges(inventory, "Consumables", itemsArray);
};
export const addCrewShipRawSalvage = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => {
applyArrayChanges(inventory.CrewShipRawSalvage, itemsArray);
applyArrayChanges(inventory, "CrewShipRawSalvage", itemsArray);
};
export const addCrewShipAmmo = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => {
applyArrayChanges(inventory.CrewShipAmmo, itemsArray);
applyArrayChanges(inventory, "CrewShipAmmo", itemsArray);
};
export const addRecipes = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => {
applyArrayChanges(inventory.Recipes, itemsArray);
applyArrayChanges(inventory, "Recipes", itemsArray);
};
export const addLevelKeys = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => {
applyArrayChanges(inventory.LevelKeys, itemsArray);
applyArrayChanges(inventory, "LevelKeys", itemsArray);
};
export const addMods = (inventory: TInventoryDatabaseDocument, itemsArray: IRawUpgrade[]): void => {
@ -1633,7 +1655,7 @@ export const addMods = (inventory: TInventoryDatabaseDocument, itemsArray: IRawU
if (RawUpgrades[itemIndex].ItemCount == 0) {
RawUpgrades.splice(itemIndex, 1);
} else if (RawUpgrades[itemIndex].ItemCount <= 0) {
logger.warn(`account now owns a negative amount of ${ItemType}`);
logger.warn(`inventory.RawUpgrades has a negative count for ${ItemType}`);
}
});
};
@ -1648,7 +1670,7 @@ export const addFusionTreasures = (inventory: TInventoryDatabaseDocument, itemsA
if (FusionTreasures[itemIndex].ItemCount == 0) {
FusionTreasures.splice(itemIndex, 1);
} else if (FusionTreasures[itemIndex].ItemCount <= 0) {
logger.warn(`account now owns a negative amount of ${ItemType}`);
logger.warn(`inventory.FusionTreasures has a negative count for ${ItemType}`);
}
} else {
FusionTreasures.push({ ItemCount, ItemType, Sockets });
@ -1885,6 +1907,29 @@ export const cleanupInventory = (inventory: TInventoryDatabaseDocument): void =>
}
};
export 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;
};
export const getCalendarProgress = (inventory: TInventoryDatabaseDocument): ICalendarProgress => {
const currentSeason = getWorldState().KnownCalendarSeasons[0];
@ -1924,8 +1969,7 @@ export const giveNemesisWeaponRecipe = (
weaponType: string,
nemesisName: string = "AGOR ROK",
weaponLoc?: string,
KillingSuit: string = "/Lotus/Powersuits/Ember/Ember",
fp: bigint = generateRewardSeed()
profile: INemesisProfile = generateNemesisProfile()
): void => {
if (!weaponLoc) {
weaponLoc = ExportWeapons[weaponType].name;
@ -1946,8 +1990,8 @@ export const giveNemesisWeaponRecipe = (
compat: weaponType,
buffs: [
{
Tag: getInnateDamageTag(KillingSuit),
Value: getInnateDamageValue(fp)
Tag: profile.innateDamageTag,
Value: profile.innateDamageValue
}
]
},
@ -1956,27 +2000,15 @@ export const giveNemesisWeaponRecipe = (
});
};
export const giveNemesisPetRecipe = (inventory: TInventoryDatabaseDocument, nemesisName: string = "AGOR ROK"): void => {
const head = getRandomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC"
])!;
const body = getRandomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyC"
])!;
const legs = getRandomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsC"
])!;
const tail = getRandomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailC"
])!;
export const giveNemesisPetRecipe = (
inventory: TInventoryDatabaseDocument,
nemesisName: string = "AGOR ROK",
profile: INemesisProfile = generateNemesisProfile()
): void => {
const head = profile.petHead!;
const body = profile.petBody!;
const legs = profile.petLegs!;
const tail = profile.petTail!;
const recipeType = Object.entries(ExportRecipes).find(arr => arr[1].resultType == head)![0];
addRecipes(inventory, [
{

View File

@ -1,7 +1,7 @@
import randomRewards from "@/static/fixed_responses/loginRewards/randomRewards.json";
import { IInventoryChanges } from "../types/purchaseTypes";
import { TAccountDocument } from "./loginService";
import { CRng, mixSeeds } from "./rngService";
import { mixSeeds, SRng } from "./rngService";
import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel";
import { addBooster, updateCurrency } from "./inventoryService";
import { handleStoreItemAcquisition } from "./purchaseService";
@ -49,8 +49,8 @@ const scaleAmount = (day: number, amount: number, scalingMultiplier: number): nu
// Always produces the same result for the same account _id & LoginDays pair.
export const isLoginRewardAChoice = (account: TAccountDocument): boolean => {
const accountSeed = parseInt(account._id.toString().substring(16), 16);
const rng = new CRng(mixSeeds(accountSeed, account.LoginDays));
return rng.random() < 0.25; // Using 25% as an approximate chance for pick-a-doors. More conclusive data analysis is needed.
const rng = new SRng(mixSeeds(accountSeed, account.LoginDays));
return rng.randomFloat() < 0.25;
};
// Always produces the same result for the same account _id & LoginDays pair.
@ -59,8 +59,8 @@ export const getRandomLoginRewards = (
inventory: TInventoryDatabaseDocument
): ILoginReward[] => {
const accountSeed = parseInt(account._id.toString().substring(16), 16);
const rng = new CRng(mixSeeds(accountSeed, account.LoginDays));
const pick_a_door = rng.random() < 0.25; // Using 25% as an approximate chance for pick-a-doors. More conclusive data analysis is needed.
const rng = new SRng(mixSeeds(accountSeed, account.LoginDays));
const pick_a_door = rng.randomFloat() < 0.25;
const rewards = [getRandomLoginReward(rng, account.LoginDays, inventory)];
if (pick_a_door) {
do {
@ -73,7 +73,7 @@ export const getRandomLoginRewards = (
return rewards;
};
const getRandomLoginReward = (rng: CRng, day: number, inventory: TInventoryDatabaseDocument): ILoginReward => {
const getRandomLoginReward = (rng: SRng, day: number, inventory: TInventoryDatabaseDocument): ILoginReward => {
const reward = rng.randomReward(randomRewards)!;
//const reward = randomRewards.find(x => x.RewardType == "RT_BOOSTER")!;
if (reward.RewardType == "RT_RANDOM_RECIPE") {

View File

@ -10,7 +10,7 @@ import {
import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes";
import { logger } from "@/src/utils/logger";
import { IRngResult, SRng, getRandomElement, getRandomReward } from "@/src/services/rngService";
import { equipmentKeys, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { equipmentKeys, IMission, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import {
addBooster,
addChallenges,
@ -35,6 +35,7 @@ import {
combineInventoryChanges,
generateRewardSeed,
getCalendarProgress,
getDialogue,
giveNemesisPetRecipe,
giveNemesisWeaponRecipe,
updateCurrency,
@ -55,13 +56,28 @@ import kuriaMessage50 from "@/static/fixed_responses/kuriaMessages/fiftyPercent.
import kuriaMessage75 from "@/static/fixed_responses/kuriaMessages/seventyFivePercent.json";
import kuriaMessage100 from "@/static/fixed_responses/kuriaMessages/oneHundredPercent.json";
import conservationAnimals from "@/static/fixed_responses/conservationAnimals.json";
import { getInfNodes, getWeaponsForManifest, sendCodaFinishedMessage } from "@/src/helpers/nemesisHelpers";
import {
generateNemesisProfile,
getInfNodes,
getNemesisPasscode,
getWeaponsForManifest,
sendCodaFinishedMessage
} from "@/src/helpers/nemesisHelpers";
import { Loadout } from "../models/inventoryModels/loadoutModel";
import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes";
import { getLiteSortie, getSortie, idToBountyCycle, idToDay, idToWeek, pushClassicBounties } from "./worldStateService";
import {
getLiteSortie,
getSortie,
getWorldState,
idToBountyCycle,
idToDay,
idToWeek,
pushClassicBounties
} from "./worldStateService";
import { config } from "./configService";
import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json";
import { ISyndicateMissionInfo } from "../types/worldStateTypes";
import { fromOid } from "../helpers/inventoryHelpers";
const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] => {
// For Spy missions, e.g. 3 vaults cracked = A, B, C
@ -167,6 +183,14 @@ export const addMissionInventoryUpdates = async (
}
if (inventoryUpdates.RewardInfo.NemesisHintProgress && inventory.Nemesis) {
inventory.Nemesis.HintProgress += inventoryUpdates.RewardInfo.NemesisHintProgress;
if (inventory.Nemesis.Faction != "FC_INFESTATION" && inventory.Nemesis.Hints.length != 3) {
const progressNeeded = [35, 60, 100][inventory.Nemesis.Hints.length];
if (inventory.Nemesis.HintProgress >= progressNeeded) {
inventory.Nemesis.HintProgress -= progressNeeded;
const passcode = getNemesisPasscode(inventory.Nemesis);
inventory.Nemesis.Hints.push(passcode[inventory.Nemesis.Hints.length]);
}
}
}
if (inventoryUpdates.MissionStatus == "GS_SUCCESS" && inventoryUpdates.RewardInfo.jobId) {
// e.g. for Profit-Taker Phase 1:
@ -399,8 +423,14 @@ export const addMissionInventoryUpdates = async (
break;
case "Upgrades":
value.forEach(clientUpgrade => {
const upgrade = inventory.Upgrades.id(clientUpgrade.ItemId.$oid)!;
upgrade.UpgradeFingerprint = clientUpgrade.UpgradeFingerprint; // primitive way to copy over the riven challenge progress
const id = fromOid(clientUpgrade.ItemId);
if (id == "") {
// U19 does not provide RawUpgrades and instead interleaves them with riven progress here
addMods(inventory, [clientUpgrade]);
} else {
const upgrade = inventory.Upgrades.id(id)!;
upgrade.UpgradeFingerprint = clientUpgrade.UpgradeFingerprint; // primitive way to copy over the riven challenge progress
}
});
break;
case "WeaponSkins":
@ -624,12 +654,18 @@ export const addMissionInventoryUpdates = async (
Rank: inventory.Nemesis.Rank,
Traded: inventory.Nemesis.Traded,
PrevOwners: inventory.Nemesis.PrevOwners,
SecondInCommand: inventory.Nemesis.SecondInCommand,
SecondInCommand: false,
Weakened: inventory.Nemesis.Weakened,
// And set killed flag
k: value.killed
});
const profile = generateNemesisProfile(
inventory.Nemesis.fp,
inventory.Nemesis.Faction,
inventory.Nemesis.KillingSuit
);
if (value.killed) {
if (
value.weaponLoc &&
@ -638,23 +674,46 @@ export const addMissionInventoryUpdates = async (
const weaponType = getWeaponsForManifest(inventory.Nemesis.manifest)[
inventory.Nemesis.WeaponIdx
];
giveNemesisWeaponRecipe(
inventory,
weaponType,
value.nemesisName,
value.weaponLoc,
inventory.Nemesis.KillingSuit,
inventory.Nemesis.fp
);
giveNemesisWeaponRecipe(inventory, weaponType, value.nemesisName, value.weaponLoc, profile);
}
if (value.petLoc) {
giveNemesisPetRecipe(inventory);
giveNemesisPetRecipe(inventory, value.nemesisName, profile);
}
}
// TOVERIFY: Is the inbox message also sent when converting a lich? If not, how are the rewards given?
if (inventory.Nemesis.Faction == "FC_INFESTATION") {
await sendCodaFinishedMessage(inventory, inventory.Nemesis.fp, value.nemesisName, value.killed);
// "Players will receive a Lich's Ephemera regardless of whether they Vanquish or Convert them."
if (profile.ephemera) {
addSkin(inventory, profile.ephemera);
}
switch (inventory.Nemesis.Faction) {
case "FC_GRINEER":
addSkin(
inventory,
value.killed
? "/Lotus/Upgrades/Skins/Clan/LichKillerBadgeItem"
: "/Lotus/Upgrades/Skins/Sigils/KuvaLichSigil"
);
break;
case "FC_CORPUS":
addSkin(
inventory,
value.killed
? "/Lotus/Upgrades/Skins/Clan/CorpusLichBadgeItem"
: "/Lotus/Upgrades/Skins/Sigils/CorpusLichSigil"
);
break;
case "FC_INFESTATION":
// TOVERIFY: Is the inbox message also sent when converting a lich? If not, how are the rewards given?
await sendCodaFinishedMessage(
inventory,
inventory.Nemesis.fp,
value.nemesisName,
value.killed
);
break;
}
inventory.Nemesis = undefined;
@ -818,6 +877,13 @@ const hexConquestRewards: IConquestReward[] = [
}
];
const droptableAliases: Record<string, string> = {
"/Lotus/Types/DropTables/ManInTheWall/MITWGruzzlingArcanesDropTable":
"/Lotus/Types/DropTables/EntratiLabDropTables/DoppelgangerDropTable",
"/Lotus/Types/DropTables/WF1999DropTables/LasrianTankSteelPathDropTable":
"/Lotus/Types/DropTables/WF1999DropTables/LasrianTankHardModeDropTable"
};
//TODO: return type of partial missioninventoryupdate response
export const addMissionRewards = async (
inventory: TInventoryDatabaseDocument,
@ -840,7 +906,13 @@ export const addMissionRewards = async (
}
//TODO: check double reward merging
const MissionRewards: IMissionReward[] = getRandomMissionDrops(inventory, rewardInfo, wagerTier, firstCompletion);
const MissionRewards: IMissionReward[] = getRandomMissionDrops(
inventory,
rewardInfo,
missions,
wagerTier,
firstCompletion
);
logger.debug("random mission drops:", MissionRewards);
const inventoryChanges: IInventoryChanges = {};
const AffiliationMods: IAffiliationMods[] = [];
@ -1020,11 +1092,9 @@ export const addMissionRewards = async (
if (strippedItems) {
for (const si of strippedItems) {
if (si.DropTable == "/Lotus/Types/DropTables/ManInTheWall/MITWGruzzlingArcanesDropTable") {
logger.debug(
`rewriting ${si.DropTable} to /Lotus/Types/DropTables/EntratiLabDropTables/DoppelgangerDropTable`
);
si.DropTable = "/Lotus/Types/DropTables/EntratiLabDropTables/DoppelgangerDropTable";
if (si.DropTable in droptableAliases) {
logger.debug(`rewriting ${si.DropTable} to ${droptableAliases[si.DropTable]}`);
si.DropTable = droptableAliases[si.DropTable];
}
const droptables = ExportEnemies.droptables[si.DropTable] ?? [];
if (si.DROP_MOD) {
@ -1157,8 +1227,9 @@ export const addMissionRewards = async (
}
if (rewardInfo.challengeMissionId) {
const [syndicateTag, tierStr] = rewardInfo.challengeMissionId.split("_"); // TODO: third part in HexSyndicate jobs - Chemistry points
const [syndicateTag, tierStr, chemistryStr] = rewardInfo.challengeMissionId.split("_");
const tier = Number(tierStr);
const chemistry = Number(chemistryStr);
const isSteelPath = missions?.Tier;
if (syndicateTag === "ZarimanSyndicate") {
let medallionAmount = tier + 1;
@ -1175,6 +1246,23 @@ export const addMissionRewards = async (
if (isSteelPath) standingAmount *= 1.5;
AffiliationMods.push(addStanding(inventory, syndicateTag, standingAmount));
}
if (syndicateTag == "HexSyndicate" && chemistry && tier < 6) {
const seed = getWorldState().SyndicateMissions.find(x => x.Tag == "HexSyndicate")!.Seed;
const { nodes, buddies } = getHexBounties(seed);
const buddy = buddies[tier];
logger.debug(`Hex seed is ${seed}, giving chemistry for ${buddy}`);
if (missions?.Tag != nodes[tier]) {
logger.warn(
`Uh-oh, tier ${tier} bounty should've been on ${nodes[tier]} but you were just on ${missions?.Tag}`
);
}
const tomorrowAt0Utc = config.noKimCooldowns
? Date.now()
: (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000;
const dialogue = getDialogue(inventory, buddy);
dialogue.Chemistry += chemistry;
dialogue.BountyChemExpiry = new Date(tomorrowAt0Utc);
}
if (isSteelPath) {
await addItem(inventory, "/Lotus/Types/Items/MiscItems/SteelEssence", 1);
MissionRewards.push({
@ -1289,6 +1377,7 @@ function getLevelCreditRewards(node: IRegion): number {
function getRandomMissionDrops(
inventory: TInventoryDatabaseDocument,
RewardInfo: IRewardInfo,
mission: IMission | undefined,
tierOverride: number | undefined,
firstCompletion: boolean
): IMissionReward[] {
@ -1371,23 +1460,50 @@ function getRandomMissionDrops(
// TODO: Check that the invasion faction is indeed FC_INFESTATION once the Invasions in worldState are more dynamic
rewardManifests = ["/Lotus/Types/Game/MissionDecks/BossMissionRewards/NyxRewards"];
} else if (RewardInfo.sortieId) {
// Sortie mission types differ from the underlying node and hence also don't give rewards from the underlying nodes. Assassinations are an exception to this.
// Sortie mission types differ from the underlying node and hence also don't give rewards from the underlying nodes.
// Assassinations in non-lite sorties are an exception to this.
if (region.missionIndex == 0) {
const arr = RewardInfo.sortieId.split("_");
let sortieId = arr[1];
if (sortieId == "Lite") {
sortieId = arr[2];
}
const sortie = getSortie(idToDay(sortieId));
const mission = sortie.Variants.find(x => x.node == arr[0])!;
if (mission.missionType == "MT_ASSASSINATION") {
rewardManifests = region.rewardManifests;
} else {
rewardManifests = [];
let giveNodeReward = false;
if (arr[1] != "Lite") {
const sortie = getSortie(idToDay(arr[1]));
giveNodeReward = sortie.Variants.find(x => x.node == arr[0])!.missionType == "MT_ASSASSINATION";
}
rewardManifests = giveNodeReward ? region.rewardManifests : [];
} else {
rewardManifests = [];
}
} else if (RewardInfo.T == 13) {
// Undercroft extra/side portal (normal mode), gives 1 Pathos Clamp + Duviri Arcane.
drops.push({
StoreItem: "/Lotus/StoreItems/Types/Gameplay/Duviri/Resource/DuviriDragonDropItem",
ItemCount: 1
});
rewardManifests = [
"/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriStaticUndercroftResourceRewards"
];
} else if (RewardInfo.T == 14) {
// Undercroft extra/side portal (steel path), gives 3 Pathos Clamps + Eidolon Arcane.
drops.push({
StoreItem: "/Lotus/StoreItems/Types/Gameplay/Duviri/Resource/DuviriDragonDropItem",
ItemCount: 3
});
rewardManifests = [
"/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriSteelPathStaticUndercroftResourceRewards"
];
} else if (RewardInfo.T == 15) {
rewardManifests = [
mission?.Tier == 1
? "/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriKullervoSteelPathRNGRewards"
: "/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriKullervoNormalRNGRewards"
];
} else if (RewardInfo.T == 70) {
// Orowyrm chest, gives 10 Pathos Clamps, or 15 on Steel Path.
drops.push({
StoreItem: "/Lotus/StoreItems/Types/Gameplay/Duviri/Resource/DuviriDragonDropItem",
ItemCount: mission?.Tier == 1 ? 15 : 10
});
rewardManifests = [];
} else {
rewardManifests = region.rewardManifests;
}
@ -1507,7 +1623,7 @@ function getRandomMissionDrops(
ZarimanSyndicate: [
"/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierATableRewards",
"/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierBTableRewards",
"/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierCTableRewards",
"/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierCTableARewards", // [sic]
"/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierDTableRewards",
"/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierETableRewards"
],
@ -1534,6 +1650,35 @@ function getRandomMissionDrops(
logger.error(`Unknown syndicate or tier: ${RewardInfo.challengeMissionId}`);
}
} else {
if (RewardInfo.node == "SolNode238") {
// The Circuit
const category = mission?.Tier == 1 ? "EXC_HARD" : "EXC_NORMAL";
const progress = inventory.EndlessXP?.find(x => x.Category == category);
if (progress) {
// https://wiki.warframe.com/w/The%20Circuit#Tiers_and_Weekly_Rewards
const roundsCompleted = RewardInfo.rewardQualifications?.length || 0;
if (roundsCompleted >= 1) {
progress.Earn += 100;
}
if (roundsCompleted >= 2) {
progress.Earn += 110;
}
if (roundsCompleted >= 3) {
progress.Earn += 125;
}
if (roundsCompleted >= 4) {
progress.Earn += 145;
if (progress.BonusAvailable && progress.BonusAvailable.getTime() <= Date.now()) {
progress.Earn += 50;
progress.BonusAvailable = new Date(Date.now() + 24 * 3600_000); // TOVERIFY
}
}
if (roundsCompleted >= 5) {
progress.Earn += (roundsCompleted - 4) * 170;
}
}
tierOverride = 0;
}
rotations = getRotations(RewardInfo, tierOverride);
}
if (rewardManifests.length != 0) {
@ -1677,3 +1822,55 @@ const libraryPersonalTargetToAvatar: Record<string, string> = {
"/Lotus/Types/Game/Library/Targets/Research10Target":
"/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/NullifySpacemanAvatar"
};
const node_excluded_buddies: Record<string, string> = {
SolNode856: "/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue",
SolNode852: "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue",
SolNode851: "/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue",
SolNode850: "/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue",
SolNode853: "/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue",
SolNode854: "/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue"
};
const getHexBounties = (seed: number): { nodes: string[]; buddies: string[] } => {
// We're gonna shuffle these arrays, so they're not truly 'const'.
const nodes: string[] = [
"SolNode850",
"SolNode851",
"SolNode852",
"SolNode853",
"SolNode854",
"SolNode856",
"SolNode858"
];
const excludable_nodes: string[] = ["SolNode851", "SolNode852", "SolNode853", "SolNode854"];
const buddies: string[] = [
"/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue",
"/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue",
"/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue",
"/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue",
"/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue",
"/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue"
];
const rng = new SRng(seed);
rng.shuffleArray(nodes);
rng.shuffleArray(excludable_nodes);
while (nodes.length > buddies.length) {
nodes.splice(
nodes.findIndex(x => x == excludable_nodes[0]),
1
);
excludable_nodes.splice(0, 1);
}
rng.shuffleArray(buddies);
for (let i = 0; i != 6; ++i) {
if (buddies[i] == node_excluded_buddies[nodes[i]]) {
const swapIdx = (i + 1) % buddies.length;
const tmp = buddies[swapIdx];
buddies[swapIdx] = buddies[i];
buddies[i] = tmp;
}
}
return { nodes, buddies };
};

View File

@ -86,54 +86,12 @@ export const mixSeeds = (seed1: number, seed2: number): number => {
return seed >>> 0;
};
// Seeded RNG for internal usage. Based on recommendations in the ISO C standards.
export class CRng {
state: number;
constructor(seed: number = 1) {
this.state = seed;
}
random(): number {
this.state = (this.state * 1103515245 + 12345) & 0x7fffffff;
return (this.state & 0x3fffffff) / 0x3fffffff;
}
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;
}
randomElement<T>(arr: readonly T[]): T | undefined {
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.
// Seeded RNG with identical results to the game client. Based on work by Donald Knuth.
export class SRng {
state: bigint;
constructor(seed: bigint) {
this.state = seed;
constructor(seed: bigint | number) {
this.state = BigInt(seed);
}
randomInt(min: number, max: number): number {
@ -157,4 +115,19 @@ export class SRng {
randomReward<T extends { probability: number }>(pool: T[]): T | undefined {
return getRewardAtPercentage(pool, this.randomFloat());
}
churnSeed(its: number): void {
while (its--) {
this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn;
}
}
shuffleArray<T>(arr: T[]): void {
for (let lastIdx = arr.length - 1; lastIdx >= 1; --lastIdx) {
const swapIdx = this.randomInt(0, lastIdx);
const tmp = arr[swapIdx];
arr[swapIdx] = arr[lastIdx];
arr[lastIdx] = tmp;
}
}
}

View File

@ -1,6 +1,6 @@
import { unixTimesInMs } from "@/src/constants/timeConstants";
import { catBreadHash } from "@/src/helpers/stringHelpers";
import { CRng, mixSeeds } from "@/src/services/rngService";
import { mixSeeds, SRng } 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";
@ -204,7 +204,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
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 rng = new SRng(mixSeeds(vendorSeed, cycleIndex));
const manifest = ExportVendors[vendorInfo.TypeName];
const offersToAdd = [];
if (manifest.numItems && !manifest.isOneBinPerCycle) {
@ -247,8 +247,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
$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")
rng.randomInt(0, 0xffff_ffff).toString(16).padStart(8, "0")
}
};
if (rawItem.numRandomItemPrices) {
@ -283,9 +282,9 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
item.PremiumPrice = [value, value];
}
if (vendorInfo.RandomSeedType) {
item.LocTagRandSeed = (rng.randomInt(0, 0xffff) << 16) | rng.randomInt(0, 0xffff);
item.LocTagRandSeed = rng.randomInt(0, 0xffff_ffff);
if (vendorInfo.RandomSeedType == "VRST_WEAPON") {
const highDword = (rng.randomInt(0, 0xffff) << 16) | rng.randomInt(0, 0xffff);
const highDword = rng.randomInt(0, 0xffff_ffff);
item.LocTagRandSeed = (BigInt(highDword) << 32n) | (BigInt(item.LocTagRandSeed) & 0xffffffffn);
}
}

View File

@ -5,7 +5,7 @@ import syndicateMissions from "@/static/fixed_responses/worldState/syndicateMiss
import { buildConfig } from "@/src/services/buildConfigService";
import { unixTimesInMs } from "@/src/constants/timeConstants";
import { config } from "@/src/services/configService";
import { CRng } from "@/src/services/rngService";
import { SRng } from "@/src/services/rngService";
import { ExportNightwave, ExportRegions, IRegion } from "warframe-public-export-plus";
import {
ICalendarDay,
@ -18,6 +18,7 @@ import {
ISyndicateMissionInfo,
IWorldState
} from "../types/worldStateTypes";
import { version_compare } from "../helpers/inventoryHelpers";
const sortieBosses = [
"SORTIE_BOSS_HYENA",
@ -192,7 +193,7 @@ const pushSyndicateMissions = (
): void => {
const nodeOptions: string[] = [...syndicateMissions];
const rng = new CRng(seed);
const rng = new SRng(seed);
const nodes: string[] = [];
for (let i = 0; i != 6; ++i) {
const index = rng.randomInt(0, nodeOptions.length - 1);
@ -234,8 +235,8 @@ const pushTilesetModifiers = (modifiers: string[], tileset: TSortieTileset): voi
};
export const getSortie = (day: number): ISortie => {
const seed = new CRng(day).randomInt(0, 0xffff);
const rng = new CRng(seed);
const seed = new SRng(day).randomInt(0, 100_000);
const rng = new SRng(seed);
const boss = rng.randomElement(sortieBosses)!;
@ -252,12 +253,10 @@ export const getSortie = (day: number): ISortie => {
const selectedNodes: ISortieMission[] = [];
const missionTypes = new Set();
const modifierTypes = new Set();
for (let i = 0; i < 3; i++) {
const randomIndex = rng.randomInt(0, nodes.length - 1);
const node = nodes[randomIndex];
let modifierType: string;
const modifiers = [
"SORTIE_MODIFIER_LOW_ENERGY",
@ -285,9 +284,7 @@ export const getSortie = (day: number): ISortie => {
const tileset = sortieTilesets[sortieBossNode[boss] as keyof typeof sortieTilesets] as TSortieTileset;
pushTilesetModifiers(modifiers, tileset);
do {
modifierType = rng.randomElement(modifiers)!;
} while (modifierTypes.has(modifierType));
const modifierType = rng.randomElement(modifiers)!;
selectedNodes.push({
missionType: "MT_ASSASSINATION",
@ -322,9 +319,7 @@ export const getSortie = (day: number): ISortie => {
modifiers.push("SORTIE_MODIFIER_SHIELDS");
}
do {
modifierType = rng.randomElement(modifiers)!;
} while (modifierTypes.has(modifierType));
const modifierType = rng.randomElement(modifiers)!;
selectedNodes.push({
missionType,
@ -334,7 +329,6 @@ export const getSortie = (day: number): ISortie => {
});
nodes.splice(randomIndex, 1);
missionTypes.add(missionType);
modifierTypes.add(modifierType);
}
const dayStart = getSortieTime(day);
@ -357,7 +351,7 @@ const dailyChallenges = Object.keys(ExportNightwave.challenges).filter(x =>
const getSeasonDailyChallenge = (day: number): ISeasonChallenge => {
const dayStart = EPOCH + day * 86400000;
const dayEnd = EPOCH + (day + 3) * 86400000;
const rng = new CRng(new CRng(day).randomInt(0, 0xffff));
const rng = new SRng(new SRng(day).randomInt(0, 100_000));
return {
_id: { $oid: "67e1b5ca9d00cb47" + day.toString().padStart(8, "0") },
Daily: true,
@ -377,7 +371,7 @@ 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(new CRng(challengeId).randomInt(0, 0xffff));
const rng = new SRng(new SRng(challengeId).randomInt(0, 100_000));
return {
_id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") },
Activation: { $date: { $numberLong: weekStart.toString() } },
@ -394,7 +388,7 @@ const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChalleng
const weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000;
const challengeId = week * 7 + id;
const rng = new CRng(new CRng(challengeId).randomInt(0, 0xffff));
const rng = new SRng(new SRng(challengeId).randomInt(0, 100_000));
return {
_id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") },
Activation: { $date: { $numberLong: weekStart.toString() } },
@ -438,12 +432,12 @@ export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[],
// TODO: xpAmounts need to be calculated based on the jobType somehow?
const seed = new CRng(bountyCycle).randomInt(0, 0xffff);
const seed = new SRng(bountyCycle).randomInt(0, 100_000);
const bountyCycleStart = bountyCycle * 9000000;
const bountyCycleEnd = bountyCycleStart + 9000000;
{
const rng = new CRng(seed);
const rng = new SRng(seed);
syndicateMissions.push({
_id: {
$oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000008"
@ -515,7 +509,7 @@ export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[],
}
{
const rng = new CRng(seed);
const rng = new SRng(seed);
syndicateMissions.push({
_id: {
$oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000025"
@ -587,7 +581,7 @@ export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[],
}
{
const rng = new CRng(seed);
const rng = new SRng(seed);
syndicateMissions.push({
_id: {
$oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000002"
@ -707,7 +701,7 @@ const getCalendarSeason = (week: number): ICalendarSeason => {
//logger.debug(`birthday on day ${day}`);
eventDays.push({ day, events: [] }); // This is how CET_PLOT looks in worldState as of around 38.5.0
}
const rng = new CRng(new CRng(week).randomInt(0, 0xffff));
const rng = new SRng(new SRng(week).randomInt(0, 100_000));
const challenges = [
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesMedium",
@ -988,7 +982,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
}
// Elite Sanctuary Onslaught cycling every week
worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = new CRng(week).randomInt(0, 0xffff);
worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = new SRng(week).randomInt(0, 0xff_ffff);
// Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation
let bountyCycle = Math.trunc(Date.now() / 9000000);
@ -1073,14 +1067,14 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
}
// The client does not seem to respect activation for classic syndicate missions, so only pushing current ones.
const sday = Date.now() >= rollover ? day : day - 1;
const rng = new CRng(sday);
pushSyndicateMissions(worldState, sday, rng.randomInt(0, 0xffff), "ba6f84724fa48049", "ArbitersSyndicate");
pushSyndicateMissions(worldState, sday, rng.randomInt(0, 0xffff), "ba6f84724fa4804a", "CephalonSudaSyndicate");
pushSyndicateMissions(worldState, sday, rng.randomInt(0, 0xffff), "ba6f84724fa4804e", "NewLokaSyndicate");
pushSyndicateMissions(worldState, sday, rng.randomInt(0, 0xffff), "ba6f84724fa48050", "PerrinSyndicate");
pushSyndicateMissions(worldState, sday, rng.randomInt(0, 0xffff), "ba6f84724fa4805e", "RedVeilSyndicate");
pushSyndicateMissions(worldState, sday, rng.randomInt(0, 0xffff), "ba6f84724fa48061", "SteelMeridianSyndicate");
const sdy = Date.now() >= rollover ? day : day - 1;
const rng = new SRng(sdy);
pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48049", "ArbitersSyndicate");
pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4804a", "CephalonSudaSyndicate");
pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4804e", "NewLokaSyndicate");
pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48050", "PerrinSyndicate");
pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4805e", "RedVeilSyndicate");
pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48061", "SteelMeridianSyndicate");
}
// Archon Hunt cycling every week
@ -1184,14 +1178,16 @@ export const getLiteSortie = (week: number): ILiteSortie => {
value.factionIndex < 2 &&
!isArchwingMission(value) &&
value.missionIndex != 0 && // Exclude MT_ASSASSINATION
value.missionIndex != 23 && // Exclude junctions
value.missionIndex != 28 && // Exclude open worlds
value.missionIndex != 32 // Exclude railjack
) {
nodes.push(key);
}
}
const seed = new CRng(week).randomInt(0, 0xffff);
const rng = new CRng(seed);
const seed = new SRng(week).randomInt(0, 100_000);
const rng = new SRng(seed);
const firstNodeIndex = rng.randomInt(0, nodes.length - 1);
const firstNode = nodes[firstNodeIndex];
nodes.splice(firstNodeIndex, 1);
@ -1246,20 +1242,3 @@ export const isArchwingMission = (node: IRegion): boolean => {
}
return false;
};
export const version_compare = (a: string, b: string): number => {
const a_digits = a
.split("/")[0]
.split(".")
.map(x => parseInt(x));
const b_digits = b
.split("/")[0]
.split(".")
.map(x => parseInt(x));
for (let i = 0; i != a_digits.length; ++i) {
if (a_digits[i] != b_digits[i]) {
return a_digits[i] > b_digits[i] ? 1 : -1;
}
}
return 0;
};

View File

@ -4,6 +4,11 @@ export interface IOid {
$oid: string;
}
export interface IOidWithLegacySupport {
$oid?: string;
$id?: string;
}
export interface IMongoDate {
$date: {
$numberLong: string;

View File

@ -1,4 +1,4 @@
import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { IMongoDate, IOid, IOidWithLegacySupport } from "@/src/types/commonTypes";
import { Types } from "mongoose";
import {
ICrewShipCustomization,
@ -92,7 +92,7 @@ export interface IEquipmentClient
IEquipmentDatabase,
"_id" | "InfestationDate" | "Expiry" | "UpgradesExpiry" | "UmbraDate" | "CrewMembers" | "Details"
> {
ItemId: IOid;
ItemId: IOidWithLegacySupport;
InfestationDate?: IMongoDate;
Expiry?: IMongoDate;
UpgradesExpiry?: IMongoDate;

View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Types } from "mongoose";
import { IOid, IMongoDate } from "../commonTypes";
import { IOid, IMongoDate, IOidWithLegacySupport } from "../commonTypes";
import {
IColor,
IItemConfig,
@ -12,6 +12,7 @@ import {
} from "@/src/types/inventoryTypes/commonInventoryTypes";
import { IFingerprintStat, RivenFingerprint } from "@/src/helpers/rivenHelper";
import { IOrbiter } from "../personalRoomsTypes";
import { ICountedStoreItem } from "warframe-public-export-plus";
export type InventoryDatabaseEquipment = {
[_ in TEquipmentKey]: IEquipmentDatabase[];
@ -54,6 +55,7 @@ export interface IInventoryDatabase
| "CrewMembers"
| "QualifyingInvasions"
| "LastInventorySync"
| "EndlessXP"
| TEquipmentKey
>,
InventoryDatabaseEquipment {
@ -92,6 +94,7 @@ export interface IInventoryDatabase
CrewMembers: ICrewMemberDatabase[];
QualifyingInvasions: IInvasionProgressDatabase[];
LastInventorySync?: Types.ObjectId;
EndlessXP?: IEndlessXpProgressDatabase[];
}
export interface IQuestKeyDatabase {
@ -356,7 +359,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
PendingCoupon?: IPendingCouponClient;
Harvestable: boolean;
DeathSquadable: boolean;
EndlessXP?: IEndlessXpProgress[];
EndlessXP?: IEndlessXpProgressClient[];
DialogueHistory?: IDialogueHistoryClient;
CalendarProgress?: ICalendarProgress;
SongChallenges?: ISongChallenge[];
@ -371,7 +374,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
EchoesHexConquestCacheScoreMission?: number;
EchoesHexConquestActiveFrameVariants?: string[];
EchoesHexConquestActiveStickers?: string[];
BrandedSuits?: IOid[];
BrandedSuits?: IOidWithLegacySupport[];
LockedWeaponGroup?: ILockedWeaponGroupClient;
HubNpcCustomizations?: IHubNpcCustomization[];
Ship?: IOrbiter; // U22 and below, response only
@ -532,6 +535,16 @@ export interface IUpgradeDatabase extends Omit<IUpgradeClient, "ItemId"> {
_id: Types.ObjectId;
}
export interface IUpgradeFromClient {
ItemType: string;
ItemId: IOidWithLegacySupport;
FromSKU?: boolean;
UpgradeFingerprint: string;
PendingRerollFingerprint: string;
ItemCount: number;
LastAdded: IOidWithLegacySupport;
}
export interface ICrewShipMembersClient {
SLOT_A?: ICrewShipMemberClient;
SLOT_B?: ICrewShipMemberClient;
@ -850,6 +863,8 @@ export interface IMission extends IMissionDatabase {
RewardsCooldownTime?: IMongoDate;
}
export type TNemesisFaction = "FC_GRINEER" | "FC_CORPUS" | "FC_INFESTATION";
export interface INemesisBaseClient {
fp: bigint | number;
manifest: string;
@ -859,7 +874,7 @@ export interface INemesisBaseClient {
WeaponIdx: number;
AgentIdx: number;
BirthNode: string;
Faction: string;
Faction: TNemesisFaction;
Rank: number;
k: boolean;
Traded: boolean;
@ -1050,7 +1065,7 @@ export interface IQuestStage {
export interface IRawUpgrade {
ItemType: string;
ItemCount: number;
LastAdded?: IOid;
LastAdded?: IOidWithLegacySupport;
}
export interface ISeasonChallenge {
@ -1143,9 +1158,24 @@ export interface IEvolutionProgress {
export type TEndlessXpCategory = "EXC_NORMAL" | "EXC_HARD";
export interface IEndlessXpProgress {
export interface IEndlessXpProgressDatabase {
Category: TEndlessXpCategory;
Earn: number;
Claim: number;
BonusAvailable?: Date;
Expiry?: Date;
Choices: string[];
PendingRewards: IEndlessXpReward[];
}
export interface IEndlessXpProgressClient extends Omit<IEndlessXpProgressDatabase, "BonusAvailable" | "Expiry"> {
BonusAvailable?: IMongoDate;
Expiry?: IMongoDate;
}
export interface IEndlessXpReward {
RequiredTotalXp: number;
Rewards: ICountedStoreItem[];
}
export interface IDialogueHistoryClient {

View File

@ -15,7 +15,7 @@ import {
IPlayerSkills,
IQuestKeyDatabase,
ILoreFragmentScan,
IUpgradeClient,
IUpgradeFromClient,
ICollectibleEntry,
IDiscoveredMarker,
ILockedWeaponGroupClient,
@ -111,7 +111,7 @@ export type IMissionInventoryUpdateRequest = {
Standing: number;
}[];
CollectibleScans?: ICollectibleEntry[];
Upgrades?: IUpgradeClient[]; // riven challenge progress
Upgrades?: IUpgradeFromClient[]; // riven challenge progress
WeaponSkins?: IWeaponSkinClient[];
StrippedItems?: {
DropTable: string;
@ -177,6 +177,7 @@ export interface IRewardInfo {
PurgatoryRewardQualifications?: string;
rewardSeed?: number | bigint;
periodicMissionTag?: string;
T?: number; // Duviri
ConquestType?: string;
ConquestCompleted?: number;
ConquestEquipmentSuggestionsFulfilled?: number;

View File

@ -7,9 +7,9 @@
<link rel="stylesheet" href="/webui/style.css" />
</head>
<body>
<nav class="navbar navbar-expand sticky-top bg-body-tertiary">
<nav class="navbar navbar-expand-lg sticky-top bg-body-tertiary">
<div class="container">
<button class="navbar-toggler d-lg-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidebar" aria-controls="sidebar" aria-label="Toggle sidebar">
<button class="navbar-toggler d-lg-none me-3" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidebar" aria-controls="sidebar" aria-label="Toggle sidebar">
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand">OpenWF WebUI</a>
@ -49,7 +49,7 @@
<div class="container pt-3 pb-3" id="main-view">
<div class="offcanvas-lg offcanvas-start" tabindex="-1" id="sidebar" aria-labelledby="sidebarLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="sidebarLabel">Sidebar</h5>
<h5 class="offcanvas-title" id="sidebarLabel">OpenWF WebUI</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#sidebar" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
@ -590,10 +590,18 @@
<input class="form-check-input" type="checkbox" id="infiniteRegalAya" />
<label class="form-check-label" for="infiniteRegalAya" data-loc="cheats_infiniteRegalAya"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="claimingBlueprintRefundsIngredients" />
<label class="form-check-label" for="claimingBlueprintRefundsIngredients" data-loc="cheats_claimingBlueprintRefundsIngredients"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="infiniteHelminthMaterials" />
<label class="form-check-label" for="infiniteHelminthMaterials" data-loc="cheats_infiniteHelminthMaterials"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="dontSubtractVoidTraces" />
<label class="form-check-label" for="dontSubtractVoidTraces" data-loc="cheats_dontSubtractVoidTraces"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="dontSubtractConsumables" />
<label class="form-check-label" for="dontSubtractConsumables" data-loc="cheats_dontSubtractConsumables"></label>

View File

@ -29,3 +29,13 @@ td.text-end > a > svg {
.card-body {
overflow: auto;
}
/* fixes for navbar on small resolutions due to not being navbar-expand */
.navbar.sticky-top .navbar-nav {
flex-direction: row;
}
.dropdown-toggle {
padding-right: var(--bs-navbar-nav-link-padding-x);
padding-left: var(--bs-navbar-nav-link-padding-x);
}

View File

@ -131,6 +131,8 @@ dict = {
cheats_infiniteEndo: `Unendlich Endo`,
cheats_infiniteRegalAya: `Unendlich Reines Aya`,
cheats_infiniteHelminthMaterials: `Unendlich Helminth-Materialien`,
cheats_claimingBlueprintRefundsIngredients: `Fertige Blaupausen erstatten Ressourcen zurück`,
cheats_dontSubtractVoidTraces: `Void-Spuren nicht verbrauchen`,
cheats_dontSubtractConsumables: `Verbrauchsgegenstände (Ausrüstung) nicht verbrauchen`,
cheats_unlockAllShipFeatures: `Alle Schiffs-Funktionen freischalten`,
cheats_unlockAllShipDecorations: `Alle Schiffsdekorationen freischalten`,

View File

@ -130,6 +130,8 @@ dict = {
cheats_infiniteEndo: `Infinite Endo`,
cheats_infiniteRegalAya: `Infinite Regal Aya`,
cheats_infiniteHelminthMaterials: `Infinite Helminth Materials`,
cheats_claimingBlueprintRefundsIngredients: `Claiming Blueprint Refunds Ingredients`,
cheats_dontSubtractVoidTraces: `Don't Subtract Void Traces`,
cheats_dontSubtractConsumables: `Don't Subtract Consumables`,
cheats_unlockAllShipFeatures: `Unlock All Ship Features`,
cheats_unlockAllShipDecorations: `Unlock All Ship Decorations`,

View File

@ -131,6 +131,8 @@ dict = {
cheats_infiniteEndo: `Endo infinito`,
cheats_infiniteRegalAya: `Aya Real infinita`,
cheats_infiniteHelminthMaterials: `Materiales Helminto infinitos`,
cheats_claimingBlueprintRefundsIngredients: `Reclamar ingredientes devueltos por planos`,
cheats_dontSubtractVoidTraces: `No descontar vestigios del Vacío`,
cheats_dontSubtractConsumables: `No restar consumibles`,
cheats_unlockAllShipFeatures: `Desbloquear todas las funciones de nave`,
cheats_unlockAllShipDecorations: `Desbloquear todas las decoraciones de nave`,

View File

@ -131,6 +131,8 @@ dict = {
cheats_infiniteEndo: `Endo infini`,
cheats_infiniteRegalAya: `Aya Raffiné infini`,
cheats_infiniteHelminthMaterials: `Ressources d'Helminth infinies`,
cheats_claimingBlueprintRefundsIngredients: `[UNTRANSLATED] Claiming Blueprint Refunds Ingredients`,
cheats_dontSubtractVoidTraces: `[UNTRANSLATED] Don't Subtract Void Traces`,
cheats_dontSubtractConsumables: `[UNTRANSLATED] Don't Subtract Consumables`,
cheats_unlockAllShipFeatures: `Débloquer tous les segments du vaisseau`,
cheats_unlockAllShipDecorations: `Débloquer toutes les décorations du vaisseau`,

View File

@ -131,6 +131,8 @@ dict = {
cheats_infiniteEndo: `Бесконечное эндо`,
cheats_infiniteRegalAya: `Бесконечная Королевская Айя`,
cheats_infiniteHelminthMaterials: `Бесконечные Выделения Гельминта`,
cheats_claimingBlueprintRefundsIngredients: `[UNTRANSLATED] Claiming Blueprint Refunds Ingredients`,
cheats_dontSubtractVoidTraces: `[UNTRANSLATED] Don't Subtract Void Traces`,
cheats_dontSubtractConsumables: `Не уменьшать количество расходников`,
cheats_unlockAllShipFeatures: `Разблокировать все функции корабля`,
cheats_unlockAllShipDecorations: `Разблокировать все украшения корабля`,

View File

@ -131,6 +131,8 @@ dict = {
cheats_infiniteEndo: `无限内融核心`,
cheats_infiniteRegalAya: `无限御品阿耶`,
cheats_infiniteHelminthMaterials: `无限Helminth材料`,
cheats_claimingBlueprintRefundsIngredients: `[UNTRANSLATED] Claiming Blueprint Refunds Ingredients`,
cheats_dontSubtractVoidTraces: `[UNTRANSLATED] Don't Subtract Void Traces`,
cheats_dontSubtractConsumables: `[UNTRANSLATED] Don't Subtract Consumables`,
cheats_unlockAllShipFeatures: `解锁所有飞船功能`,
cheats_unlockAllShipDecorations: `解锁所有飞船装饰`,