feat: claim all recipes (#2700)

Closes #2699

tried to make it a cleaner diff, but this is the best I could do

Reviewed-on: OpenWF/SpaceNinjaServer#2700
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
This commit is contained in:
Sainan 2025-08-27 01:29:46 -07:00 committed by Sainan
parent 15578b04d2
commit 30398021b3

View File

@ -4,8 +4,9 @@
import type { RequestHandler } from "express"; import type { RequestHandler } from "express";
import { logger } from "../../utils/logger.ts"; import { logger } from "../../utils/logger.ts";
import { getRecipe } from "../../services/itemDataService.ts"; import { getRecipe } from "../../services/itemDataService.ts";
import type { IOid, IOidWithLegacySupport } from "../../types/commonTypes.ts"; import type { IOidWithLegacySupport } from "../../types/commonTypes.ts";
import { getJSONfromString } from "../../helpers/stringHelpers.ts"; import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import type { TAccountDocument } from "../../services/loginService.ts";
import { getAccountForRequest } from "../../services/loginService.ts"; import { getAccountForRequest } from "../../services/loginService.ts";
import { import {
getInventory, getInventory,
@ -21,23 +22,32 @@ import {
import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
import type { IPendingRecipeDatabase } from "../../types/inventoryTypes/inventoryTypes.ts"; import type { IPendingRecipeDatabase } from "../../types/inventoryTypes/inventoryTypes.ts";
import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts"; import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts";
import { toOid2 } from "../../helpers/inventoryHelpers.ts"; import { fromOid, toOid2 } from "../../helpers/inventoryHelpers.ts";
import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts"; import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts";
import type { IRecipe } from "warframe-public-export-plus"; import type { IRecipe } from "warframe-public-export-plus";
import type { IEquipmentClient } from "../../types/equipmentTypes.ts"; import type { IEquipmentClient } from "../../types/equipmentTypes.ts";
import { EquipmentFeatures, Status } from "../../types/equipmentTypes.ts"; import { EquipmentFeatures, Status } from "../../types/equipmentTypes.ts";
interface IClaimCompletedRecipeRequest { interface IClaimCompletedRecipeRequest {
RecipeIds: IOid[]; RecipeIds: IOidWithLegacySupport[];
}
interface IClaimCompletedRecipeResponse {
InventoryChanges: IInventoryChanges;
BrandedSuits?: IOidWithLegacySupport[];
} }
export const claimCompletedRecipeController: RequestHandler = async (req, res) => { export const claimCompletedRecipeController: RequestHandler = async (req, res) => {
const claimCompletedRecipeRequest = getJSONfromString<IClaimCompletedRecipeRequest>(String(req.body)); const claimCompletedRecipeRequest = getJSONfromString<IClaimCompletedRecipeRequest>(String(req.body));
const account = await getAccountForRequest(req); const account = await getAccountForRequest(req);
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(account._id.toString());
const pendingRecipe = inventory.PendingRecipes.id(claimCompletedRecipeRequest.RecipeIds[0].$oid); const resp: IClaimCompletedRecipeResponse = {
InventoryChanges: {}
};
for (const recipeId of claimCompletedRecipeRequest.RecipeIds) {
const pendingRecipe = inventory.PendingRecipes.id(fromOid(recipeId));
if (!pendingRecipe) { if (!pendingRecipe) {
throw new Error(`no pending recipe found with id ${claimCompletedRecipeRequest.RecipeIds[0].$oid}`); throw new Error(`no pending recipe found with id ${fromOid(recipeId)}`);
} }
//check recipe is indeed ready to be completed //check recipe is indeed ready to be completed
@ -57,17 +67,30 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
await refundRecipeIngredients(inventory, inventoryChanges, recipe, pendingRecipe); await refundRecipeIngredients(inventory, inventoryChanges, recipe, pendingRecipe);
await inventory.save(); await inventory.save();
res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root. res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root.
} else { return;
}
await claimCompletedRecipe(account, inventory, recipe, pendingRecipe, resp, req.query.rush);
}
await inventory.save();
res.json(resp);
};
const claimCompletedRecipe = async (
account: TAccountDocument,
inventory: TInventoryDatabaseDocument,
recipe: IRecipe,
pendingRecipe: IPendingRecipeDatabase,
resp: IClaimCompletedRecipeResponse,
rush: any
): Promise<void> => {
logger.debug("Claiming Recipe", { recipe, pendingRecipe }); logger.debug("Claiming Recipe", { recipe, pendingRecipe });
let BrandedSuits: undefined | IOidWithLegacySupport[];
if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") { if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
inventory.PendingSpectreLoadouts ??= []; inventory.PendingSpectreLoadouts ??= [];
inventory.SpectreLoadouts ??= []; inventory.SpectreLoadouts ??= [];
const pendingLoadoutIndex = inventory.PendingSpectreLoadouts.findIndex( const pendingLoadoutIndex = inventory.PendingSpectreLoadouts.findIndex(x => x.ItemType == recipe.resultType);
x => x.ItemType == recipe.resultType
);
if (pendingLoadoutIndex != -1) { if (pendingLoadoutIndex != -1) {
const loadoutIndex = inventory.SpectreLoadouts.findIndex(x => x.ItemType == recipe.resultType); const loadoutIndex = inventory.SpectreLoadouts.findIndex(x => x.ItemType == recipe.resultType);
if (loadoutIndex != -1) { if (loadoutIndex != -1) {
@ -85,10 +108,9 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)), inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)),
1 1
); );
BrandedSuits = [toOid2(pendingRecipe.SuitToUnbrand!, account.BuildLabel)]; resp.BrandedSuits = [toOid2(pendingRecipe.SuitToUnbrand!, account.BuildLabel)];
} }
let InventoryChanges: IInventoryChanges = {};
if (recipe.consumeOnUse) { if (recipe.consumeOnUse) {
addRecipes(inventory, [ addRecipes(inventory, [
{ {
@ -97,20 +119,16 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
} }
]); ]);
} }
if (req.query.rush) {
if (rush) {
const end = Math.trunc(pendingRecipe.CompletionDate.getTime() / 1000); const end = Math.trunc(pendingRecipe.CompletionDate.getTime() / 1000);
const start = end - recipe.buildTime; const start = end - recipe.buildTime;
const secondsElapsed = Math.trunc(Date.now() / 1000) - start; const secondsElapsed = Math.trunc(Date.now() / 1000) - start;
const progress = secondsElapsed / recipe.buildTime; const progress = secondsElapsed / recipe.buildTime;
logger.debug(`rushing recipe at ${Math.trunc(progress * 100)}% completion`); logger.debug(`rushing recipe at ${Math.trunc(progress * 100)}% completion`);
const cost = const cost =
progress > 0.5 progress > 0.5 ? Math.round(recipe.skipBuildTimePrice * (1 - (progress - 0.5))) : recipe.skipBuildTimePrice;
? Math.round(recipe.skipBuildTimePrice * (1 - (progress - 0.5))) combineInventoryChanges(resp.InventoryChanges, updateCurrency(inventory, cost, true));
: recipe.skipBuildTimePrice;
InventoryChanges = {
...InventoryChanges,
...updateCurrency(inventory, cost, true)
};
} }
if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") { if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") {
@ -128,7 +146,7 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
pet.Details!.Status = canSetActive ? Status.StatusAvailable : Status.StatusStasis; pet.Details!.Status = canSetActive ? Status.StatusAvailable : Status.StatusStasis;
} else if (recipe.secretIngredientAction == "SIA_DISTILL_PRINT") { } else if (recipe.secretIngredientAction == "SIA_DISTILL_PRINT") {
const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!; const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!;
addKubrowPetPrint(inventory, pet, InventoryChanges); addKubrowPetPrint(inventory, pet, resp.InventoryChanges);
} else if (recipe.secretIngredientAction != "SIA_UNBRAND") { } else if (recipe.secretIngredientAction != "SIA_UNBRAND") {
if (recipe.resultType == "/Lotus/Powersuits/Excalibur/ExcaliburUmbra") { if (recipe.resultType == "/Lotus/Powersuits/Excalibur/ExcaliburUmbra") {
// Quite the special case here... // Quite the special case here...
@ -185,8 +203,8 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
`{"lvl":5}` `{"lvl":5}`
) )
).Upgrades![0]; ).Upgrades![0];
InventoryChanges.Upgrades ??= []; resp.InventoryChanges.Upgrades ??= [];
InventoryChanges.Upgrades.push(umbraModA, umbraModB, umbraModC, sacrificeModA, sacrificeModB); resp.InventoryChanges.Upgrades.push(umbraModA, umbraModB, umbraModC, sacrificeModA, sacrificeModB);
await addPowerSuit( await addPowerSuit(
inventory, inventory,
@ -209,7 +227,7 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
XP: 900_000, XP: 900_000,
Features: EquipmentFeatures.DOUBLE_CAPACITY Features: EquipmentFeatures.DOUBLE_CAPACITY
}, },
InventoryChanges resp.InventoryChanges
); );
inventory.XPInfo.push({ inventory.XPInfo.push({
ItemType: "/Lotus/Powersuits/Excalibur/ExcaliburUmbra", ItemType: "/Lotus/Powersuits/Excalibur/ExcaliburUmbra",
@ -227,34 +245,31 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
XP: 450_000, XP: 450_000,
Features: EquipmentFeatures.DOUBLE_CAPACITY Features: EquipmentFeatures.DOUBLE_CAPACITY
}, },
InventoryChanges resp.InventoryChanges
); );
inventory.XPInfo.push({ inventory.XPInfo.push({
ItemType: "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana", ItemType: "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana",
XP: 450_000 XP: 450_000
}); });
} else { } else {
InventoryChanges = { combineInventoryChanges(
...InventoryChanges, resp.InventoryChanges,
...(await addItem( await addItem(
inventory, inventory,
recipe.resultType, recipe.resultType,
recipe.num, recipe.num,
false, false,
undefined, undefined,
pendingRecipe.TargetFingerprint pendingRecipe.TargetFingerprint
)) )
}; );
} }
} }
if ( if (
inventory.claimingBlueprintRefundsIngredients && inventory.claimingBlueprintRefundsIngredients &&
recipe.secretIngredientAction != "SIA_CREATE_KUBROW" // Can't refund the egg recipe.secretIngredientAction != "SIA_CREATE_KUBROW" // Can't refund the egg
) { ) {
await refundRecipeIngredients(inventory, InventoryChanges, recipe, pendingRecipe); await refundRecipeIngredients(inventory, resp.InventoryChanges, recipe, pendingRecipe);
}
await inventory.save();
res.json({ InventoryChanges, BrandedSuits });
} }
}; };