Compare commits

...

27 Commits

Author SHA1 Message Date
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
45 changed files with 1054 additions and 327 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

2
package-lock.json generated
View File

@ -13,7 +13,7 @@
"@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",

View File

@ -20,7 +20,7 @@
"@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",

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

@ -15,6 +15,14 @@ export const crewMembersController: RequestHandler = async (req, res) => {
dbCrewMember.WeaponConfigIdx = data.crewMember.WeaponConfigIdx;
dbCrewMember.WeaponId = new Types.ObjectId(data.crewMember.WeaponId.$oid);
dbCrewMember.Configs = data.crewMember.Configs;
if (data.crewMember.SecondInCommand) {
for (const cm of inventory.CrewMembers) {
if (cm.SecondInCommand) {
cm.SecondInCommand = false;
break;
}
}
}
dbCrewMember.SecondInCommand = data.crewMember.SecondInCommand;
await inventory.save();
res.json({

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

@ -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

@ -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,20 @@ 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"
};
// 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 +87,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 +248,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.

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,

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

@ -70,6 +70,7 @@ import { createShip } from "./shipService";
import {
catbrowDetails,
fromMongoDate,
fromOid,
kubrowDetails,
kubrowFurPatternsWeights,
kubrowWeights,
@ -423,7 +424,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 +870,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 +926,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 +963,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 +1003,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 +1085,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 +1113,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 +1270,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 +1507,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 +1585,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 +1607,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 +1654,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 +1669,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 });

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,
@ -62,6 +62,7 @@ import { getLiteSortie, getSortie, idToBountyCycle, idToDay, idToWeek, pushClass
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
@ -399,8 +400,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,7 +631,7 @@ 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
@ -818,6 +825,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 +854,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 +1040,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) {
@ -1289,6 +1307,7 @@ function getLevelCreditRewards(node: IRegion): number {
function getRandomMissionDrops(
inventory: TInventoryDatabaseDocument,
RewardInfo: IRewardInfo,
mission: IMission | undefined,
tierOverride: number | undefined,
firstCompletion: boolean
): IMissionReward[] {
@ -1371,20 +1390,16 @@ 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 = [];
}
@ -1507,7 +1522,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 +1549,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) {

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 {

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)!;
@ -350,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,
@ -370,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() } },
@ -387,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() } },
@ -431,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"
@ -508,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"
@ -580,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"
@ -700,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",
@ -981,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);
@ -1066,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
@ -1177,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);
@ -1239,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;

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: `解锁所有飞船装饰`,