返回提交历史
Modified
src/controllers/api/claimCompletedRecipeController.ts
+220
-205
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
代码差异
1 个文件
+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