feat: recipes that consume weapons (#1032)

Closes #720

Reviewed-on: OpenWF/SpaceNinjaServer#1032
This commit is contained in:
Sainan 2025-02-28 06:47:34 -08:00
parent 79147786f6
commit caec5a6cbf
4 changed files with 106 additions and 56 deletions

View File

@ -8,6 +8,9 @@ import { IOid } from "@/src/types/commonTypes";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory, updateCurrency, addItem, addMiscItems, addRecipes } from "@/src/services/inventoryService"; import { getInventory, updateCurrency, addItem, addMiscItems, addRecipes } from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
export interface IClaimCompletedRecipeRequest { export interface IClaimCompletedRecipeRequest {
RecipeIds: IOid[]; RecipeIds: IOid[];
@ -37,15 +40,35 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
} }
if (req.query.cancel) { if (req.query.cancel) {
const currencyChanges = updateCurrency(inventory, recipe.buildPrice * -1, false); const inventoryChanges: IInventoryChanges = {
addMiscItems(inventory, recipe.ingredients); ...updateCurrency(inventory, recipe.buildPrice * -1, false)
};
const nonMiscItemIngredients = 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>());
nonMiscItemIngredients.add(item.ItemType);
inventoryChanges.WeaponBin ??= { Slots: 0 };
inventoryChanges.WeaponBin.Slots -= 1;
});
}
}
const miscItemChanges: IMiscItem[] = [];
recipe.ingredients.forEach(ingredient => {
if (!nonMiscItemIngredients.has(ingredient.ItemType)) {
miscItemChanges.push(ingredient);
}
});
addMiscItems(inventory, miscItemChanges);
inventoryChanges.MiscItems = miscItemChanges;
await inventory.save(); await inventory.save();
// 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.
res.json({
...currencyChanges,
MiscItems: recipe.ingredients
});
} else { } else {
logger.debug("Claiming Recipe", { recipe, pendingRecipe }); logger.debug("Claiming Recipe", { recipe, pendingRecipe });

View File

@ -7,6 +7,8 @@ import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inven
import { unixTimesInMs } from "@/src/constants/timeConstants"; import { unixTimesInMs } from "@/src/constants/timeConstants";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { ISpectreLoadout } from "@/src/types/inventoryTypes/inventoryTypes"; import { ISpectreLoadout } from "@/src/types/inventoryTypes/inventoryTypes";
import { toOid } from "@/src/helpers/inventoryHelpers";
import { ExportWeapons } from "warframe-public-export-plus";
interface IStartRecipeRequest { interface IStartRecipeRequest {
RecipeName: string; RecipeName: string;
@ -26,23 +28,40 @@ export const startRecipeController: RequestHandler = async (req, res) => {
throw new Error(`unknown recipe ${recipeName}`); throw new Error(`unknown recipe ${recipeName}`);
} }
const ingredientsInverse = recipe.ingredients.map(component => ({
ItemType: component.ItemType,
ItemCount: component.ItemCount * -1
}));
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
updateCurrency(inventory, recipe.buildPrice, false); updateCurrency(inventory, recipe.buildPrice, false);
addMiscItems(inventory, ingredientsInverse);
//buildtime is in seconds const pr =
const completionDate = new Date(Date.now() + recipe.buildTime * unixTimesInMs.second); inventory.PendingRecipes[
inventory.PendingRecipes.push({
ItemType: recipeName,
CompletionDate: new Date(Date.now() + recipe.buildTime * unixTimesInMs.second),
_id: new Types.ObjectId()
}) - 1
];
inventory.PendingRecipes.push({ for (let i = 0; i != recipe.ingredients.length; ++i) {
ItemType: recipeName, if (startRecipeRequest.Ids[i]) {
CompletionDate: completionDate, const category = ExportWeapons[recipe.ingredients[i].ItemType].productCategory;
_id: new Types.ObjectId() if (category != "LongGuns" && category != "Pistols" && category != "Melee") {
}); throw new Error(`unexpected equipment ingredient type: ${category}`);
}
const equipmentIndex = inventory[category].findIndex(x => x._id.equals(startRecipeRequest.Ids[i]));
if (equipmentIndex == -1) {
throw new Error(`could not find equipment item to use for recipe`);
}
pr[category] ??= [];
pr[category].push(inventory[category][equipmentIndex]);
inventory[category].splice(equipmentIndex, 1);
} else {
addMiscItems(inventory, [
{
ItemType: recipe.ingredients[i].ItemType,
ItemCount: recipe.ingredients[i].ItemCount * -1
}
]);
}
}
if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") { if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
const spectreLoadout: ISpectreLoadout = { const spectreLoadout: ISpectreLoadout = {
@ -98,9 +117,7 @@ export const startRecipeController: RequestHandler = async (req, res) => {
} }
} }
const newInventory = await inventory.save(); await inventory.save();
res.json({ res.json({ RecipeId: toOid(pr._id) });
RecipeId: { $oid: newInventory.PendingRecipes[newInventory.PendingRecipes.length - 1]._id.toString() }
});
}; };

View File

@ -9,8 +9,8 @@ import {
ISlots, ISlots,
IMailboxDatabase, IMailboxDatabase,
IDuviriInfo, IDuviriInfo,
IPendingRecipe as IPendingRecipeDatabase, IPendingRecipeDatabase,
IPendingRecipeResponse, IPendingRecipeClient,
ITypeCount, ITypeCount,
IFocusXP, IFocusXP,
IFocusUpgrade, IFocusUpgrade,
@ -108,29 +108,6 @@ const focusUpgradeSchema = new Schema<IFocusUpgrade>(
{ _id: false } { _id: false }
); );
const pendingRecipeSchema = new Schema<IPendingRecipeDatabase>(
{
ItemType: String,
CompletionDate: Date
},
{ id: false }
);
pendingRecipeSchema.virtual("ItemId").get(function () {
return { $oid: this._id.toString() };
});
pendingRecipeSchema.set("toJSON", {
virtuals: true,
transform(_document, returnedObject) {
delete returnedObject._id;
delete returnedObject.__v;
(returnedObject as IPendingRecipeResponse).CompletionDate = {
$date: { $numberLong: (returnedObject as IPendingRecipeDatabase).CompletionDate.getTime().toString() }
};
}
});
const polaritySchema = new Schema<IPolarity>( const polaritySchema = new Schema<IPolarity>(
{ {
Slot: Number, Slot: Number,
@ -865,6 +842,35 @@ equipmentKeys.forEach(key => {
equipmentFields[key] = { type: [EquipmentSchema] }; equipmentFields[key] = { type: [EquipmentSchema] };
}); });
const pendingRecipeSchema = new Schema<IPendingRecipeDatabase>(
{
ItemType: String,
CompletionDate: Date,
LongGuns: { type: [EquipmentSchema], default: undefined },
Pistols: { type: [EquipmentSchema], default: undefined },
Melee: { type: [EquipmentSchema], default: undefined }
},
{ id: false }
);
pendingRecipeSchema.virtual("ItemId").get(function () {
return { $oid: this._id.toString() };
});
pendingRecipeSchema.set("toJSON", {
virtuals: true,
transform(_document, returnedObject) {
delete returnedObject._id;
delete returnedObject.__v;
delete returnedObject.LongGuns;
delete returnedObject.Pistols;
delete returnedObject.Melees;
(returnedObject as IPendingRecipeClient).CompletionDate = {
$date: { $numberLong: (returnedObject as IPendingRecipeDatabase).CompletionDate.getTime().toString() }
};
}
});
const infestedFoundrySchema = new Schema<IInfestedFoundryDatabase>( const infestedFoundrySchema = new Schema<IInfestedFoundryDatabase>(
{ {
Name: String, Name: String,

View File

@ -49,7 +49,7 @@ export interface IInventoryDatabase
LoadOutPresets: Types.ObjectId; // LoadOutPresets changed from ILoadOutPresets to Types.ObjectId for population LoadOutPresets: Types.ObjectId; // LoadOutPresets changed from ILoadOutPresets to Types.ObjectId for population
Mailbox?: IMailboxDatabase; Mailbox?: IMailboxDatabase;
GuildId?: Types.ObjectId; GuildId?: Types.ObjectId;
PendingRecipes: IPendingRecipe[]; PendingRecipes: IPendingRecipeDatabase[];
QuestKeys: IQuestKeyDatabase[]; QuestKeys: IQuestKeyDatabase[];
BlessingCooldown?: Date; BlessingCooldown?: Date;
Ships: Types.ObjectId[]; Ships: Types.ObjectId[];
@ -143,10 +143,6 @@ export type TSolarMapRegion =
//TODO: perhaps split response and database into their own files //TODO: perhaps split response and database into their own files
export interface IPendingRecipeResponse extends Omit<IPendingRecipe, "CompletionDate"> {
CompletionDate: IMongoDate;
}
export interface IDailyAffiliations { export interface IDailyAffiliations {
DailyAffiliation: number; DailyAffiliation: number;
DailyAffiliationPvp: number; DailyAffiliationPvp: number;
@ -217,7 +213,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
XPInfo: ITypeXPItem[]; XPInfo: ITypeXPItem[];
Recipes: ITypeCount[]; Recipes: ITypeCount[];
WeaponSkins: IWeaponSkinClient[]; WeaponSkins: IWeaponSkinClient[];
PendingRecipes: IPendingRecipeResponse[]; PendingRecipes: IPendingRecipeClient[];
TrainingDate: IMongoDate; TrainingDate: IMongoDate;
PlayerLevel: number; PlayerLevel: number;
Staff?: boolean; Staff?: boolean;
@ -783,12 +779,20 @@ export interface IPendingCouponClient {
Discount: number; Discount: number;
} }
export interface IPendingRecipe { export interface IPendingRecipeDatabase {
ItemType: string; ItemType: string;
CompletionDate: Date; CompletionDate: Date;
ItemId: IOid; ItemId: IOid;
TargetItemId?: string; // likely related to liches TargetItemId?: string; // likely related to liches
TargetFingerprint?: string; // likely related to liches TargetFingerprint?: string; // likely related to liches
LongGuns?: IEquipmentDatabase[];
Pistols?: IEquipmentDatabase[];
Melee?: IEquipmentDatabase[];
}
export interface IPendingRecipeClient
extends Omit<IPendingRecipeDatabase, "CompletionDate" | "LongGuns" | "Pistols" | "Melee"> {
CompletionDate: IMongoDate;
} }
export interface IPendingTrade { export interface IPendingTrade {