XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

SpaceNinjaServer

A simple server for a small space ninja game

公开
关注 0 Fork 1 Star 0
返回提交历史

XFEstudio/SpaceNinjaServer

feat: claim all recipes (#2700)

Closes #2699 tried to make it a cleaner diff, but this is the best I could do Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2700 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>

30398021
Sainan <63328889+Sainan@users.noreply.github.com>
提交于

代码差异

1 个文件 +220 -205
Modified src/controllers/api/claimCompletedRecipeController.ts +220 -205
@@ -4,8 +4,9 @@
4 4 import type { RequestHandler } from "express";
5 5 import { logger } from "../../utils/logger.ts";
6 6 import { getRecipe } from "../../services/itemDataService.ts";
7 import type { IOid, IOidWithLegacySupport } from "../../types/commonTypes.ts";
7 import type { IOidWithLegacySupport } from "../../types/commonTypes.ts";
8 8 import { getJSONfromString } from "../../helpers/stringHelpers.ts";
9 import type { TAccountDocument } from "../../services/loginService.ts";
9 10 import { getAccountForRequest } from "../../services/loginService.ts";
10 11 import {
11 12 getInventory,
@@ -21,240 +22,254 @@ import {
21 22 import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
22 23 import type { IPendingRecipeDatabase } from "../../types/inventoryTypes/inventoryTypes.ts";
23 24 import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts";
24 import { toOid2 } from "../../helpers/inventoryHelpers.ts";
25 import { fromOid, toOid2 } from "../../helpers/inventoryHelpers.ts";
25 26 import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts";
26 27 import type { IRecipe } from "warframe-public-export-plus";
27 28 import type { IEquipmentClient } from "../../types/equipmentTypes.ts";
28 29 import { EquipmentFeatures, Status } from "../../types/equipmentTypes.ts";
29 30
30 31 interface IClaimCompletedRecipeRequest {
31 RecipeIds: IOid[];
32 RecipeIds: IOidWithLegacySupport[];
33 }
34
35 interface IClaimCompletedRecipeResponse {
36 InventoryChanges: IInventoryChanges;
37 BrandedSuits?: IOidWithLegacySupport[];
32 38 }
33 39
34 40 export const claimCompletedRecipeController: RequestHandler = async (req, res) => {
35 41 const claimCompletedRecipeRequest = getJSONfromString<IClaimCompletedRecipeRequest>(String(req.body));
36 42 const account = await getAccountForRequest(req);
37 43 const inventory = await getInventory(account._id.toString());
38 const pendingRecipe = inventory.PendingRecipes.id(claimCompletedRecipeRequest.RecipeIds[0].$oid);
39 if (!pendingRecipe) {
40 throw new Error(`no pending recipe found with id ${claimCompletedRecipeRequest.RecipeIds[0].$oid}`);
41 }
44 const resp: IClaimCompletedRecipeResponse = {
45 InventoryChanges: {}
46 };
47 for (const recipeId of claimCompletedRecipeRequest.RecipeIds) {
48 const pendingRecipe = inventory.PendingRecipes.id(fromOid(recipeId));
49 if (!pendingRecipe) {
50 throw new Error(`no pending recipe found with id ${fromOid(recipeId)}`);
51 }
52
53 //check recipe is indeed ready to be completed
54 // if (pendingRecipe.CompletionDate > new Date()) {
55 // throw new Error(`recipe ${pendingRecipe._id} is not ready to be completed`);
56 // }
42 57
43 //check recipe is indeed ready to be completed
44 // if (pendingRecipe.CompletionDate > new Date()) {
45 // throw new Error(`recipe ${pendingRecipe._id} is not ready to be completed`);
46 // }
58 inventory.PendingRecipes.pull(pendingRecipe._id);
47 59
48 inventory.PendingRecipes.pull(pendingRecipe._id);
60 const recipe = getRecipe(pendingRecipe.ItemType);
61 if (!recipe) {
62 throw new Error(`no completed item found for recipe ${pendingRecipe._id.toString()}`);
63 }
49 64
50 const recipe = getRecipe(pendingRecipe.ItemType);
51 if (!recipe) {
52 throw new Error(`no completed item found for recipe ${pendingRecipe._id.toString()}`);
65 if (req.query.cancel) {
66 const inventoryChanges: IInventoryChanges = {};
67 await refundRecipeIngredients(inventory, inventoryChanges, recipe, pendingRecipe);
68 await inventory.save();
69 res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root.
70 return;
71 }
72
73 await claimCompletedRecipe(account, inventory, recipe, pendingRecipe, resp, req.query.rush);
53 74 }
75 await inventory.save();
76 res.json(resp);
77 };
54 78
55 if (req.query.cancel) {
56 const inventoryChanges: IInventoryChanges = {};
57 await refundRecipeIngredients(inventory, inventoryChanges, recipe, pendingRecipe);
58 await inventory.save();
59 res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root.
60 } else {
61 logger.debug("Claiming Recipe", { recipe, pendingRecipe });
79 const claimCompletedRecipe = async (
80 account: TAccountDocument,
81 inventory: TInventoryDatabaseDocument,
82 recipe: IRecipe,
83 pendingRecipe: IPendingRecipeDatabase,
84 resp: IClaimCompletedRecipeResponse,
85 rush: any
86 ): Promise<void> => {
87 logger.debug("Claiming Recipe", { recipe, pendingRecipe });
62 88
63 let BrandedSuits: undefined | IOidWithLegacySupport[];
64 if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
65 inventory.PendingSpectreLoadouts ??= [];
66 inventory.SpectreLoadouts ??= [];
89 if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
90 inventory.PendingSpectreLoadouts ??= [];
91 inventory.SpectreLoadouts ??= [];
67 92
68 const pendingLoadoutIndex = inventory.PendingSpectreLoadouts.findIndex(
69 x => x.ItemType == recipe.resultType
70 );
71 if (pendingLoadoutIndex != -1) {
72 const loadoutIndex = inventory.SpectreLoadouts.findIndex(x => x.ItemType == recipe.resultType);
73 if (loadoutIndex != -1) {
74 inventory.SpectreLoadouts.splice(loadoutIndex, 1);
75 }
76 logger.debug(
77 "moving spectre loadout from pending to active",
78 inventory.toJSON().PendingSpectreLoadouts![pendingLoadoutIndex]
79 );
80 inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]);
81 inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1);
93 const pendingLoadoutIndex = inventory.PendingSpectreLoadouts.findIndex(x => x.ItemType == recipe.resultType);
94 if (pendingLoadoutIndex != -1) {
95 const loadoutIndex = inventory.SpectreLoadouts.findIndex(x => x.ItemType == recipe.resultType);
96 if (loadoutIndex != -1) {
97 inventory.SpectreLoadouts.splice(loadoutIndex, 1);
82 98 }
83 } else if (recipe.secretIngredientAction == "SIA_UNBRAND") {
84 inventory.BrandedSuits!.splice(
85 inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)),
86 1
99 logger.debug(
100 "moving spectre loadout from pending to active",
101 inventory.toJSON().PendingSpectreLoadouts![pendingLoadoutIndex]
87 102 );
88 BrandedSuits = [toOid2(pendingRecipe.SuitToUnbrand!, account.BuildLabel)];
89 }
90
91 let InventoryChanges: IInventoryChanges = {};
92 if (recipe.consumeOnUse) {
93 addRecipes(inventory, [
94 {
95 ItemType: pendingRecipe.ItemType,
96 ItemCount: -1
97 }
98 ]);
99 }
100 if (req.query.rush) {
101 const end = Math.trunc(pendingRecipe.CompletionDate.getTime() / 1000);
102 const start = end - recipe.buildTime;
103 const secondsElapsed = Math.trunc(Date.now() / 1000) - start;
104 const progress = secondsElapsed / recipe.buildTime;
105 logger.debug(`rushing recipe at ${Math.trunc(progress * 100)}% completion`);
106 const cost =
107 progress > 0.5
108 ? Math.round(recipe.skipBuildTimePrice * (1 - (progress - 0.5)))
109 : recipe.skipBuildTimePrice;
110 InventoryChanges = {
111 ...InventoryChanges,
112 ...updateCurrency(inventory, cost, true)
113 };
103 inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]);
104 inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1);
114 105 }
106 } else if (recipe.secretIngredientAction == "SIA_UNBRAND") {
107 inventory.BrandedSuits!.splice(
108 inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)),
109 1
110 );
111 resp.BrandedSuits = [toOid2(pendingRecipe.SuitToUnbrand!, account.BuildLabel)];
112 }
115 113
116 if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") {
117 const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!;
118 if (pet.Details!.HatchDate!.getTime() > Date.now()) {
119 pet.Details!.HatchDate = new Date();
120 }
121 let canSetActive = true;
122 for (const pet of inventory.KubrowPets) {
123 if (pet.Details!.Status == Status.StatusAvailable) {
124 canSetActive = false;
125 break;
126 }
114 if (recipe.consumeOnUse) {
115 addRecipes(inventory, [
116 {
117 ItemType: pendingRecipe.ItemType,
118 ItemCount: -1
127 119 }
128 pet.Details!.Status = canSetActive ? Status.StatusAvailable : Status.StatusStasis;
129 } else if (recipe.secretIngredientAction == "SIA_DISTILL_PRINT") {
130 const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!;
131 addKubrowPetPrint(inventory, pet, InventoryChanges);
132 } else if (recipe.secretIngredientAction != "SIA_UNBRAND") {
133 if (recipe.resultType == "/Lotus/Powersuits/Excalibur/ExcaliburUmbra") {
134 // Quite the special case here...
135 // We don't just get Umbra, but also Skiajati and Umbra Mods. Both items are max rank, potatoed, and with the mods are pre-installed.
136 // Source: https://wiki.warframe.com/w/The_Sacrifice, https://wiki.warframe.com/w/Excalibur/Umbra, https://wiki.warframe.com/w/Skiajati
120 ]);
121 }
137 122
138 const umbraModA = (
139 await addItem(
140 inventory,
141 "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModA",
142 1,
143 false,
144 undefined,
145 `{"lvl":5}`
146 )
147 ).Upgrades![0];
148 const umbraModB = (
149 await addItem(
150 inventory,
151 "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModB",
152 1,
153 false,
154 undefined,
155 `{"lvl":5}`
156 )
157 ).Upgrades![0];
158 const umbraModC = (
159 await addItem(
160 inventory,
161 "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModC",
162 1,
163 false,
164 undefined,
165 `{"lvl":5}`
166 )
167 ).Upgrades![0];
168 const sacrificeModA = (
169 await addItem(
170 inventory,
171 "/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModA",
172 1,
173 false,
174 undefined,
175 `{"lvl":5}`
176 )
177 ).Upgrades![0];
178 const sacrificeModB = (
179 await addItem(
180 inventory,
181 "/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModB",
182 1,
183 false,
184 undefined,
185 `{"lvl":5}`
186 )
187 ).Upgrades![0];
188 InventoryChanges.Upgrades ??= [];
189 InventoryChanges.Upgrades.push(umbraModA, umbraModB, umbraModC, sacrificeModA, sacrificeModB);
123 if (rush) {
124 const end = Math.trunc(pendingRecipe.CompletionDate.getTime() / 1000);
125 const start = end - recipe.buildTime;
126 const secondsElapsed = Math.trunc(Date.now() / 1000) - start;
127 const progress = secondsElapsed / recipe.buildTime;
128 logger.debug(`rushing recipe at ${Math.trunc(progress * 100)}% completion`);
129 const cost =
130 progress > 0.5 ? Math.round(recipe.skipBuildTimePrice * (1 - (progress - 0.5))) : recipe.skipBuildTimePrice;
131 combineInventoryChanges(resp.InventoryChanges, updateCurrency(inventory, cost, true));
132 }
190 133
191 await addPowerSuit(
134 if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") {
135 const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!;
136 if (pet.Details!.HatchDate!.getTime() > Date.now()) {
137 pet.Details!.HatchDate = new Date();
138 }
139 let canSetActive = true;
140 for (const pet of inventory.KubrowPets) {
141 if (pet.Details!.Status == Status.StatusAvailable) {
142 canSetActive = false;
143 break;
144 }
145 }
146 pet.Details!.Status = canSetActive ? Status.StatusAvailable : Status.StatusStasis;
147 } else if (recipe.secretIngredientAction == "SIA_DISTILL_PRINT") {
148 const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!;
149 addKubrowPetPrint(inventory, pet, resp.InventoryChanges);
150 } else if (recipe.secretIngredientAction != "SIA_UNBRAND") {
151 if (recipe.resultType == "/Lotus/Powersuits/Excalibur/ExcaliburUmbra") {
152 // Quite the special case here...
153 // We don't just get Umbra, but also Skiajati and Umbra Mods. Both items are max rank, potatoed, and with the mods are pre-installed.
154 // Source: https://wiki.warframe.com/w/The_Sacrifice, https://wiki.warframe.com/w/Excalibur/Umbra, https://wiki.warframe.com/w/Skiajati
155
156 const umbraModA = (
157 await addItem(
158 inventory,
159 "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModA",
160 1,
161 false,
162 undefined,
163 `{"lvl":5}`
164 )
165 ).Upgrades![0];
166 const umbraModB = (
167 await addItem(
192 168 inventory,
193 "/Lotus/Powersuits/Excalibur/ExcaliburUmbra",
194 {
195 Configs: [
196 {
197 Upgrades: [
198 "",
199 "",
200 "",
201 "",
202 "",
203 umbraModA.ItemId.$oid,
204 umbraModB.ItemId.$oid,
205 umbraModC.ItemId.$oid
206 ]
207 }
208 ],
209 XP: 900_000,
210 Features: EquipmentFeatures.DOUBLE_CAPACITY
211 },
212 InventoryChanges
213 );
214 inventory.XPInfo.push({
215 ItemType: "/Lotus/Powersuits/Excalibur/ExcaliburUmbra",
216 XP: 900_000
217 });
169 "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModB",
170 1,
171 false,
172 undefined,
173 `{"lvl":5}`
174 )
175 ).Upgrades![0];
176 const umbraModC = (
177 await addItem(
178 inventory,
179 "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModC",
180 1,
181 false,
182 undefined,
183 `{"lvl":5}`
184 )
185 ).Upgrades![0];
186 const sacrificeModA = (
187 await addItem(
188 inventory,
189 "/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModA",
190 1,
191 false,
192 undefined,
193 `{"lvl":5}`
194 )
195 ).Upgrades![0];
196 const sacrificeModB = (
197 await addItem(
198 inventory,
199 "/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModB",
200 1,
201 false,
202 undefined,
203 `{"lvl":5}`
204 )
205 ).Upgrades![0];
206 resp.InventoryChanges.Upgrades ??= [];
207 resp.InventoryChanges.Upgrades.push(umbraModA, umbraModB, umbraModC, sacrificeModA, sacrificeModB);
208
209 await addPowerSuit(
210 inventory,
211 "/Lotus/Powersuits/Excalibur/ExcaliburUmbra",
212 {
213 Configs: [
214 {
215 Upgrades: [
216 "",
217 "",
218 "",
219 "",
220 "",
221 umbraModA.ItemId.$oid,
222 umbraModB.ItemId.$oid,
223 umbraModC.ItemId.$oid
224 ]
225 }
226 ],
227 XP: 900_000,
228 Features: EquipmentFeatures.DOUBLE_CAPACITY
229 },
230 resp.InventoryChanges
231 );
232 inventory.XPInfo.push({
233 ItemType: "/Lotus/Powersuits/Excalibur/ExcaliburUmbra",
234 XP: 900_000
235 });
218 236
219 addEquipment(
237 addEquipment(
238 inventory,
239 "Melee",
240 "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana",
241 {
242 Configs: [
243 { Upgrades: ["", "", "", "", "", "", sacrificeModA.ItemId.$oid, sacrificeModB.ItemId.$oid] }
244 ],
245 XP: 450_000,
246 Features: EquipmentFeatures.DOUBLE_CAPACITY
247 },
248 resp.InventoryChanges
249 );
250 inventory.XPInfo.push({
251 ItemType: "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana",
252 XP: 450_000
253 });
254 } else {
255 combineInventoryChanges(
256 resp.InventoryChanges,
257 await addItem(
220 258 inventory,
221 "Melee",
222 "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana",
223 {
224 Configs: [
225 { Upgrades: ["", "", "", "", "", "", sacrificeModA.ItemId.$oid, sacrificeModB.ItemId.$oid] }
226 ],
227 XP: 450_000,
228 Features: EquipmentFeatures.DOUBLE_CAPACITY
229 },
230 InventoryChanges
231 );
232 inventory.XPInfo.push({
233 ItemType: "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana",
234 XP: 450_000
235 });
236 } else {
237 InventoryChanges = {
238 ...InventoryChanges,
239 ...(await addItem(
240 inventory,
241 recipe.resultType,
242 recipe.num,
243 false,
244 undefined,
245 pendingRecipe.TargetFingerprint
246 ))
247 };
248 }
249 }
250 if (
251 inventory.claimingBlueprintRefundsIngredients &&
252 recipe.secretIngredientAction != "SIA_CREATE_KUBROW" // Can't refund the egg
253 ) {
254 await refundRecipeIngredients(inventory, InventoryChanges, recipe, pendingRecipe);
259 recipe.resultType,
260 recipe.num,
261 false,
262 undefined,
263 pendingRecipe.TargetFingerprint
264 )
265 );
255 266 }
256 await inventory.save();
257 res.json({ InventoryChanges, BrandedSuits });
267 }
268 if (
269 inventory.claimingBlueprintRefundsIngredients &&
270 recipe.secretIngredientAction != "SIA_CREATE_KUBROW" // Can't refund the egg
271 ) {
272 await refundRecipeIngredients(inventory, resp.InventoryChanges, recipe, pendingRecipe);
258 273 }
259 274 };
260 275