返回提交历史
Modified
src/constants/gameToBuildVersion.ts
+4
-1
Modified
src/controllers/api/giftingController.ts
+24
-2
Modified
src/controllers/api/purchaseController.ts
+31
-1
Modified
src/routes/api.ts
+3
-2
Modified
src/services/inventoryService.ts
+2
-2
Modified
src/services/itemDataService.ts
+281
-0
Modified
src/services/purchaseService.ts
+54
-17
Modified
src/services/questService.ts
+2
-2
Modified
src/types/purchaseTypes.ts
+6
-4
XFEstudio/XFESpaceNinjaServer
feat: purchase for old versions (#3249)
It should work fine. Tested on `2013.05.23.16.06` (U8) and `2015.10.15.12.24` (U17.7.1). Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/3249 Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com> Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
9335efd7
代码差异
9 个文件
+407
-31
@@ -49,8 +49,11 @@ const gameToBuildVersion = {
49
49
"16.5.5": "2015.05.14.16.29",
50
50
"16.0.2": "2015.03.21.08.17",
51
51
"15.14.1": "2015.02.13.10.41",
52
"15.0.6": "2014.10.27.17.07",
52
53
"15.0.0": "2014.10.24.08.24",
53
54
"14.0.0": "2014.07.21.18.38",
54
"13.0.0": "2014.04.10.17.47"
55
"13.0.0": "2014.04.10.17.47",
56
"10.3.3": "2013.10.11.17.01",
57
"9.1.2": "2013.07.15.20.46"
55
58
} as const;
56
59
export default gameToBuildVersion as Record<keyof typeof gameToBuildVersion, string>;
@@ -15,6 +15,8 @@ import type { IPurchaseParams, IPurchaseResponse } from "../../types/purchaseTyp
15
15
import { PurchaseSource } from "../../types/purchaseTypes.ts";
16
16
import type { RequestHandler } from "express";
17
17
import { ExportBundles, ExportFlavour } from "warframe-public-export-plus";
18
import { logger } from "../../utils/logger.ts";
19
import { getBundle, getPrice } from "../../services/itemDataService.ts";
18
20
19
21
const checkPurchaseParams = (params: IPurchaseParams): boolean => {
20
22
switch (params.Source) {
@@ -29,6 +31,16 @@ const checkPurchaseParams = (params: IPurchaseParams): boolean => {
29
31
30
32
export const giftingController: RequestHandler = async (req, res) => {
31
33
const data = getJSONfromString<IGiftingRequest>(String(req.body));
34
if (!data.PurchaseParams) {
35
const nameParts = String(req.query.productName).split(";");
36
data.PurchaseParams = {
37
Source: PurchaseSource.Market,
38
StoreItem: nameParts[0],
39
Quantity: Number(req.query.quantity),
40
UsePremium: true
41
};
42
if (nameParts[1]) data.PurchaseParams.Durability = Number(nameParts[1]);
43
}
32
44
if (!checkPurchaseParams(data.PurchaseParams)) {
33
45
throw new Error(`unexpected purchase params in gifting request: ${String(req.body)}`);
34
46
}
@@ -76,10 +88,20 @@ export const giftingController: RequestHandler = async (req, res) => {
76
88
if (data.PurchaseParams.Source == PurchaseSource.DailyDeal) {
77
89
await handleDailyDealPurchase(senderInventory, data.PurchaseParams, response);
78
90
} else {
91
if (!data.PurchaseParams.ExpectedPrice) {
92
logger.debug(`client didn't provide ExpectedPrice, attempt to get it from PE+`);
93
data.PurchaseParams.ExpectedPrice = getPrice(
94
data.PurchaseParams.StoreItem,
95
data.PurchaseParams.Quantity,
96
data.PurchaseParams.Durability,
97
data.PurchaseParams.UsePremium,
98
data.buildLabel
99
);
100
}
79
101
updateCurrency(senderInventory, data.PurchaseParams.ExpectedPrice, true, response.InventoryChanges);
80
102
}
81
103
if (data.PurchaseParams.StoreItem in ExportBundles) {
82
const bundle = ExportBundles[data.PurchaseParams.StoreItem];
104
const bundle = getBundle(data.PurchaseParams.StoreItem, data.buildLabel)!;
83
105
if (bundle.giftingBonus) {
84
106
combineInventoryChanges(
85
107
response.InventoryChanges,
@@ -118,7 +140,7 @@ export const giftingController: RequestHandler = async (req, res) => {
118
140
};
119
141
120
142
interface IGiftingRequest {
121
PurchaseParams: IPurchaseParams;
143
PurchaseParams?: IPurchaseParams;
122
144
Message?: string;
123
145
Recipient?: string;
124
146
RecipientId?: IOid;
@@ -1,13 +1,17 @@
1
1
import type { RequestHandler } from "express";
2
2
import { getAccountForRequest } from "../../services/loginService.ts";
3
import { PurchaseSource } from "../../types/purchaseTypes.ts";
3
4
import type { IPurchaseRequest } from "../../types/purchaseTypes.ts";
4
5
import { handlePurchase } from "../../services/purchaseService.ts";
5
6
import { getInventory } from "../../services/inventoryService.ts";
6
7
import { sendWsBroadcastTo } from "../../services/wsService.ts";
8
import { toStoreItem } from "../../services/itemDataService.ts";
9
import { ExportBundles } from "warframe-public-export-plus";
7
10
8
export const purchaseController: RequestHandler = async (req, res) => {
11
export const purchasePostController: RequestHandler = async (req, res) => {
9
12
const purchaseRequest = JSON.parse(String(req.body)) as IPurchaseRequest;
10
13
const account = await getAccountForRequest(req);
14
if (!purchaseRequest.buildLabel && account.BuildLabel) purchaseRequest.buildLabel = account.BuildLabel;
11
15
if (purchaseRequest.buildLabel && account.BuildLabel && purchaseRequest.buildLabel != account.BuildLabel) {
12
16
throw new Error(
13
17
`account logged into ${account.BuildLabel} but is now attempting a purchase in ${purchaseRequest.buildLabel} ?!`
@@ -21,3 +25,29 @@ export const purchaseController: RequestHandler = async (req, res) => {
21
25
res.json(response);
22
26
sendWsBroadcastTo(accountId, { update_inventory: true });
23
27
};
28
29
export const purchaseGetController: RequestHandler = async (req, res) => {
30
const account = await getAccountForRequest(req);
31
const accountId = account._id.toString();
32
let internalName = String(req.query.productName);
33
if (!(internalName in ExportBundles)) internalName = toStoreItem(internalName);
34
const purchaseRequest: IPurchaseRequest = {
35
PurchaseParams: {
36
Source: PurchaseSource.Market,
37
StoreItem: internalName,
38
Quantity: 1,
39
UsePremium: Boolean(Number(req.query.usePremium))
40
},
41
buildLabel: account.BuildLabel!
42
};
43
if (req.query.durability) purchaseRequest.PurchaseParams.Durability = Number(req.query.durability);
44
const inventory = await getInventory(accountId);
45
const response = await handlePurchase(purchaseRequest, inventory);
46
await inventory.save();
47
if (response.Body) {
48
res.send(response.Body.replace(/\/StoreItems/g, "").replace(/lvl=\d+;/g, ""));
49
} else {
50
res.json(1);
51
}
52
sendWsBroadcastTo(accountId, { update_inventory: true });
53
};
@@ -108,7 +108,7 @@ import { playedParkourTutorialController } from "../controllers/api/playedParkou
108
108
import { playerSkillsController } from "../controllers/api/playerSkillsController.ts";
109
109
import { postGuildAdvertisementController } from "../controllers/api/postGuildAdvertisementController.ts";
110
110
import { projectionManagerController } from "../controllers/api/projectionManagerController.ts";
111
import { purchaseController } from "../controllers/api/purchaseController.ts";
111
import { purchaseGetController, purchasePostController } from "../controllers/api/purchaseController.ts";
112
112
import { questControlController } from "../controllers/api/questControlController.ts";
113
113
import { queueDojoComponentDestructionController } from "../controllers/api/queueDojoComponentDestructionController.ts";
114
114
import { redeemPromoCodeController } from "../controllers/api/redeemPromoCodeController.ts";
@@ -224,6 +224,7 @@ apiRouter.get("/marketRecommendations.php", marketRecommendationsController);
224
224
apiRouter.get("/marketSearchRecommendations.php", marketRecommendationsController);
225
225
apiRouter.get("/modularWeaponSale.php", modularWeaponSaleController);
226
226
apiRouter.get("/playedParkourTutorial.php", playedParkourTutorialController);
227
apiRouter.get("/purchase.php", purchaseGetController); // U8
227
228
apiRouter.get("/questControl.php", questControlController);
228
229
apiRouter.get("/queueDojoComponentDestruction.php", queueDojoComponentDestructionController);
229
230
apiRouter.get("/removeFriend.php", removeFriendGetController);
@@ -329,7 +330,7 @@ apiRouter.post("/placeDecoInComponent.php", placeDecoInComponentController);
329
330
apiRouter.post("/playerSkills.php", playerSkillsController);
330
331
apiRouter.post("/postGuildAdvertisement.php", postGuildAdvertisementController);
331
332
apiRouter.post("/projectionManager.php", projectionManagerController);
332
apiRouter.post("/purchase.php", purchaseController);
333
apiRouter.post("/purchase.php", purchasePostController);
333
334
apiRouter.post("/questControl.php", questControlController); // U17
334
335
apiRouter.post("/redeemPromoCode.php", redeemPromoCodeController);
335
336
apiRouter.post("/releasePet.php", releasePetController);
@@ -72,7 +72,7 @@ import {
72
72
version_compare
73
73
} from "../helpers/inventoryHelpers.ts";
74
74
import { addQuestKey, completeQuest } from "./questService.ts";
75
import { handleBundleAcqusition } from "./purchaseService.ts";
75
import { handleBundleAcquisition } from "./purchaseService.ts";
76
76
import libraryDailyTasks from "../../static/fixed_responses/libraryDailyTasks.json" with { type: "json" };
77
77
import { generateRewardSeed, getRandomElement, getRandomInt, getRandomWeightedReward, SRng } from "./rngService.ts";
78
78
import type { IMessageCreationTemplate } from "./inboxService.ts";
@@ -476,7 +476,7 @@ export const addItem = async (
476
476
): Promise<IInventoryChanges> => {
477
477
// Bundles are technically StoreItems but a) they don't have a normal counterpart, and b) they are used in non-StoreItem contexts, e.g. email attachments.
478
478
if (typeName in ExportBundles) {
479
return await handleBundleAcqusition(typeName, inventory, quantity);
479
return await handleBundleAcquisition(typeName, inventory, quantity, {}, buildLabel);
480
480
}
481
481
482
482
// Strict typing
@@ -1,5 +1,7 @@
1
1
import type { IKeyChainRequest } from "../types/requestTypes.ts";
2
2
import type {
3
IBoosterPack,
4
IBundle,
3
5
IDefaultUpgrade,
4
6
IInboxMessage,
5
7
IKey,
@@ -25,11 +27,14 @@ import {
25
27
dict_uk,
26
28
dict_zh,
27
29
ExportArcanes,
30
ExportBoosterPacks,
28
31
ExportBoosters,
29
32
ExportBundles,
33
ExportCreditBundles,
30
34
ExportCustoms,
31
35
ExportDojoRecipes,
32
36
ExportDrones,
37
ExportFlavour,
33
38
ExportGear,
34
39
ExportKeys,
35
40
ExportRailjackWeapons,
@@ -374,3 +379,279 @@ export const getProductCategory = (uniqueName: string): string => {
374
379
}
375
380
throw new Error(`don't know product category of ${uniqueName}`);
376
381
};
382
383
export const getBundle = (uniqueName: string, buildLabel: string = ""): IBundle | undefined => {
384
if (buildLabel) {
385
if (
386
uniqueName == "/Lotus/Types/StoreItems/Packages/StalkerPack" &&
387
version_compare(buildLabel, "2024.06.12.18.42") < 0 // < 36.0.0
388
) {
389
return {
390
name: "/Lotus/Language/Items/StalkerPackName",
391
description: "/Lotus/Language/Items/StalkerPackDesc",
392
icon: "/Lotus/Interface/Icons/StoreIcons/MarketBundles/Weapons/StalkerPack.png",
393
components: [
394
{ typeName: "/Lotus/StoreItems/Weapons/Tenno/Bows/StalkerBow", purchaseQuantity: 1 },
395
{ typeName: "/Lotus/StoreItems/Weapons/Tenno/ThrowingWeapons/StalkerKunai", purchaseQuantity: 1 },
396
{
397
typeName: "/Lotus/StoreItems/Weapons/Tenno/Melee/Scythe/StalkerScytheWeapon",
398
purchaseQuantity: 1
399
},
400
{
401
typeName: "/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/NinjaColourPickerItem",
402
purchaseQuantity: 1
403
}
404
],
405
packageDiscount: 0.059
406
};
407
}
408
}
409
410
return ExportBundles[uniqueName];
411
};
412
413
export const getBoosterPack = (uniqueName: string, buildLabel: string = ""): IBoosterPack | undefined => {
414
if (
415
version_compare(buildLabel, gameToBuildVersion["18.16.0"]) < 0 &&
416
uniqueName == "/Lotus/Types/BoosterPacks/RandomKey"
417
) {
418
const boosterPack: IBoosterPack = {
419
name: "/Lotus/Language/Items/RandomKey",
420
description: "/Lotus/Language/Items/RandomKeyDesc",
421
icon: "/Lotus/Interface/Icons/Store/OrokinKey.png",
422
components: [
423
{ Item: "/Lotus/Types/Keys/OrokinKeyA", Rarity: "COMMON", Amount: 1 },
424
{ Item: "/Lotus/Types/Keys/OrokinKeyB", Rarity: "COMMON", Amount: 1 },
425
{ Item: "/Lotus/Types/Keys/OrokinKeyC", Rarity: "UNCOMMON", Amount: 1 },
426
{ Item: "/Lotus/Types/Keys/OrokinKeyD", Rarity: "UNCOMMON", Amount: 1 },
427
{ Item: "/Lotus/Types/Keys/OrokinKeyE", Rarity: "RARE", Amount: 1 }
428
],
429
rarityWeightsPerRoll: [
430
{ COMMON: 1, UNCOMMON: 0.050000001, RARE: 0.0099999998, LEGENDARY: 0 },
431
{ COMMON: 1, UNCOMMON: 0.25, RARE: 0.050000001, LEGENDARY: 0 },
432
{ COMMON: 1, UNCOMMON: 0.25, RARE: 0.050000001, LEGENDARY: 0 },
433
{ COMMON: 1, UNCOMMON: 0.25, RARE: 0.050000001, LEGENDARY: 0 },
434
{ COMMON: 1, UNCOMMON: 0.25, RARE: 0.1, LEGENDARY: 0 }
435
],
436
canGiveDuplicates: true,
437
platinumCost: 75
438
};
439
if (buildLabel) {
440
if (version_compare(buildLabel, "2013.06.07.23.44") >= 0) {
441
boosterPack.rarityWeightsPerRoll[4] = { COMMON: 0, UNCOMMON: 0, RARE: 1, LEGENDARY: 0 };
442
}
443
if (version_compare(buildLabel, gameToBuildVersion["9.1.2"]) >= 0) {
444
boosterPack.components.push(
445
{ Item: "/Lotus/Types/Keys/OrokinCaptureKeyA", Rarity: "COMMON", Amount: 1 },
446
{ Item: "/Lotus/Types/Keys/OrokinCaptureKeyB", Rarity: "COMMON", Amount: 1 },
447
{ Item: "/Lotus/Types/Keys/OrokinCaptureKeyC", Rarity: "RARE", Amount: 1 },
448
{ Item: "/Lotus/Types/Keys/OrokinMobileDefenseKeyA", Rarity: "COMMON", Amount: 1 },
449
{ Item: "/Lotus/Types/Keys/OrokinMobileDefenseKeyB", Rarity: "UNCOMMON", Amount: 1 },
450
{ Item: "/Lotus/Types/Keys/OrokinMobileDefenseKeyC", Rarity: "RARE", Amount: 1 },
451
{ Item: "/Lotus/Types/Keys/OrokinDefenseKeyA", Rarity: "COMMON", Amount: 1 },
452
{ Item: "/Lotus/Types/Keys/OrokinDefenseKeyB", Rarity: "UNCOMMON", Amount: 1 },
453
{ Item: "/Lotus/Types/Keys/OrokinDefenseKeyC", Rarity: "RARE", Amount: 1 }
454
);
455
}
456
if (version_compare(buildLabel, gameToBuildVersion["10.3.3"]) >= 0) {
457
boosterPack.components.push({
458
Item: "/Lotus/Types/Keys/OrokinTowerSurvivalT3Key",
459
Rarity: "UNCOMMON",
460
Amount: 1
461
});
462
}
463
if (version_compare(buildLabel, gameToBuildVersion["14.0.0"]) >= 0) {
464
boosterPack.components.find(c => c.Item === "/Lotus/Types/Keys/OrokinTowerSurvivalT3Key")!.Rarity =
465
"RARE";
466
boosterPack.components.push(
467
{ Item: "/Lotus/Types/Keys/OrokinTowerKeys/OrokinTowerCaptureTier4Key", Rarity: "RARE", Amount: 1 },
468
{ Item: "/Lotus/Types/Keys/OrokinTowerKeys/OrokinTowerDefenseTier4Key", Rarity: "RARE", Amount: 1 },
469
{
470
Item: "/Lotus/Types/Keys/OrokinTowerKeys/OrokinTowerExterminateTier4Key",
471
Rarity: "RARE",
472
Amount: 1
473
},
474
{
475
Item: "/Lotus/Types/Keys/OrokinTowerKeys/OrokinTowerInterceptionTier4Key",
476
Rarity: "RARE",
477
Amount: 1
478
},
479
{
480
Item: "/Lotus/Types/Keys/OrokinTowerKeys/OrokinTowerMobileDefenseTier4Key",
481
Rarity: "RARE",
482
Amount: 1
483
},
484
{ Item: "/Lotus/Types/Keys/OrokinTowerKeys/OrokinTowerSurvivalTier4Key", Rarity: "RARE", Amount: 1 }
485
);
486
}
487
if (version_compare(buildLabel, gameToBuildVersion["15.0.6"]) >= 0) {
488
boosterPack.components.push(
489
{
490
Item: "/Lotus/Types/Keys/OrokinTowerKeys/OrokinTowerSabotageTier1Key",
491
Rarity: "COMMON",
492
Amount: 1
493
},
494
{
495
Item: "/Lotus/Types/Keys/OrokinTowerKeys/OrokinTowerSabotageTier2Key",
496
Rarity: "UNCOMMON",
497
Amount: 1
498
},
499
{
500
Item: "/Lotus/Types/Keys/OrokinTowerKeys/OrokinTowerSabotageTier3Key",
501
Rarity: "RARE",
502
Amount: 1
503
},
504
{ Item: "/Lotus/Types/Keys/OrokinTowerKeys/OrokinTowerSabotageTier4Key", Rarity: "RARE", Amount: 1 }
505
);
506
}
507
}
508
return boosterPack;
509
}
510
if (version_compare(buildLabel, gameToBuildVersion["18.18.0"]) < 0) {
511
if (uniqueName == "/Lotus/Types/BoosterPacks/CommonFusionPack") {
512
return {
513
name: "/Lotus/Language/Items/CommonFusionPack",
514
description: "/Lotus/Language/Items/CommonFusionPackDesc",
515
icon: "/Lotus/Interface/Icons/Store/FusionCorePackBronze.png",
516
components: [
517
{ Item: "/Lotus/Upgrades/Mods/Fusers/RareModFuser", Rarity: "RARE", Amount: 1 },
518
{ Item: "/Lotus/Upgrades/Mods/Fusers/UncommonModFuser", Rarity: "UNCOMMON", Amount: 1 },
519
{ Item: "/Lotus/Upgrades/Mods/Fusers/CommonModFuser", Rarity: "COMMON", Amount: 1 }
520
],
521
rarityWeightsPerRoll: [
522
{ COMMON: 0.60000002, UNCOMMON: 0.40000001, RARE: 0, LEGENDARY: 0 },
523
{ COMMON: 0.60000002, UNCOMMON: 0.40000001, RARE: 0, LEGENDARY: 0 },
524
{ COMMON: 0.5, UNCOMMON: 0.30000001, RARE: 0.2, LEGENDARY: 0 }
525
],
526
canGiveDuplicates: true,
527
platinumCost: 55
528
};
529
}
530
if (uniqueName == "/Lotus/Types/BoosterPacks/PremiumUncommonFusionPack") {
531
return {
532
name: "/Lotus/Language/Items/PremiumUncommonFusionPack",
533
description: "/Lotus/Language/Items/PremiumUncommonFusionPackDesc",
534
icon: "/Lotus/Interface/Icons/Store/FusionCorePackSilver.png",
535
components: [
536
{ Item: "/Lotus/Upgrades/Mods/Fusers/RareModFuser", Rarity: "RARE", Amount: 1 },
537
{ Item: "/Lotus/Upgrades/Mods/Fusers/UncommonModFuser", Rarity: "UNCOMMON", Amount: 1 },
538
{ Item: "/Lotus/Upgrades/Mods/Fusers/CommonModFuser", Rarity: "COMMON", Amount: 1 }
539
],
540
rarityWeightsPerRoll: [
541
{ COMMON: 0, UNCOMMON: 1, RARE: 0, LEGENDARY: 0 },
542
{ COMMON: 0.5, UNCOMMON: 0.30000001, RARE: 0.2, LEGENDARY: 0 },
543
{ COMMON: 0.5, UNCOMMON: 0.30000001, RARE: 0.2, LEGENDARY: 0 }
544
],
545
canGiveDuplicates: true,
546
platinumCost: 70
547
};
548
}
549
if (uniqueName == "/Lotus/Types/BoosterPacks/PremiumRareFusionPack") {
550
return {
551
name: "/Lotus/Language/Items/PremiumRareFusionPack",
552
description: "/Lotus/Language/Items/PremiumRareFusionPackDesc",
553
icon: "/Lotus/Interface/Icons/Store/FusionCorePackGold.png",
554
components: [
555
{ Item: "/Lotus/Upgrades/Mods/Fusers/CommonModFuser", Rarity: "COMMON", Amount: 1 },
556
{ Item: "/Lotus/Upgrades/Mods/Fusers/UncommonModFuser", Rarity: "UNCOMMON", Amount: 1 },
557
{ Item: "/Lotus/Upgrades/Mods/Fusers/RareModFuser", Rarity: "RARE", Amount: 1 }
558
],
559
rarityWeightsPerRoll: [
560
{ COMMON: 0.5, UNCOMMON: 0.30000001, RARE: 0.2, LEGENDARY: 0 },
561
{ COMMON: 0.5, UNCOMMON: 0.30000001, RARE: 0.2, LEGENDARY: 0 },
562
{ COMMON: 0, UNCOMMON: 0, RARE: 1, LEGENDARY: 0 }
563
],
564
canGiveDuplicates: true,
565
platinumCost: 80
566
};
567
}
568
}
569
570
return ExportBoosterPacks[uniqueName];
571
};
572
573
export const getPrice = (
574
storeItemName: string,
575
quantity: number = 1,
576
durability: number = 0,
577
usePremium: boolean,
578
buildLabel: string
579
): number => {
580
let price: number | undefined;
581
const isBundle = storeItemName in ExportBundles;
582
const isBooster = storeItemName in ExportBoosters;
583
if (isBooster) {
584
price = 40 * (durability + 1);
585
} else if (isBundle) {
586
const bundle = getBundle(storeItemName, buildLabel)!;
587
if (usePremium && bundle.platinumCost) {
588
price = ExportBundles[storeItemName].platinumCost;
589
} else if (!usePremium && bundle.creditsCost) {
590
price = ExportBundles[storeItemName].creditsCost;
591
} else {
592
let sum = 0;
593
for (const component of bundle.components) {
594
sum += getPrice(
595
component.typeName,
596
component.purchaseQuantity,
597
[3, 7, 30, 90].indexOf(component.durabilityDays ?? 3),
598
usePremium,
599
buildLabel
600
);
601
}
602
const discount = typeof bundle.packageDiscount === "number" ? bundle.packageDiscount : 0.25;
603
price = Math.round(sum * (1 - discount));
604
}
605
} else {
606
const internalName = fromStoreItem(storeItemName);
607
const boosterPack = getBoosterPack(fromStoreItem(storeItemName), buildLabel);
608
const isBoosterPack = boosterPack !== undefined;
609
if (isBoosterPack) {
610
if (usePremium) price = boosterPack.platinumCost;
611
} else {
612
const categories = [
613
ExportBundles,
614
ExportCreditBundles,
615
ExportCustoms,
616
ExportFlavour,
617
ExportGear,
618
ExportRecipes,
619
ExportResources,
620
ExportSentinels,
621
ExportWarframes,
622
ExportWeapons
623
];
624
const category = categories.find(c => internalName in c);
625
if (category) {
626
const item = category[internalName];
627
if (usePremium && "platinumCost" in item) {
628
price = item.platinumCost;
629
} else if (!usePremium && "creditsCost" in item) {
630
price = item.creditsCost;
631
}
632
}
633
634
if (usePremium) {
635
if (version_compare(buildLabel, gameToBuildVersion["18.16.0"]) < 0) {
636
if (internalName == "/Lotus/Powersuits/Mag/Mag") {
637
price = ExportWarframes["/Lotus/Powersuits/Loki/Loki"].platinumCost;
638
} else if (internalName == "/Lotus/Powersuits/Loki/Loki") {
639
price = ExportWarframes["/Lotus/Powersuits/Mag/Mag"].platinumCost;
640
}
641
}
642
if (version_compare(buildLabel, gameToBuildVersion["18.0.2"]) < 0) {
643
if (internalName == "/Lotus/Upgrades/Skins/Dragon/DragonAltHelmet") price = 40;
644
}
645
} else {
646
// I'm not sure when they stopped selling it
647
if (storeItemName == "/Lotus/StoreItems/Types/Restoratives/Cipher") price = 250;
648
}
649
}
650
}
651
652
if (price == undefined) {
653
throw new Error(`no price found for ${storeItemName}`);
654
}
655
656
return price * quantity;
657
};
@@ -21,7 +21,6 @@ import { PurchaseSource } from "../types/purchaseTypes.ts";
21
21
import { logger } from "../utils/logger.ts";
22
22
import { getWorldState } from "./worldStateService.ts";
23
23
import {
24
ExportBoosterPacks,
25
24
ExportBoosters,
26
25
ExportBundles,
27
26
ExportCreditBundles,
@@ -31,7 +30,7 @@ import {
31
30
ExportVendors
32
31
} from "warframe-public-export-plus";
33
32
import type { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel.ts";
34
import { fromStoreItem, toStoreItem } from "./itemDataService.ts";
33
import { fromStoreItem, getBoosterPack, getBundle, getPrice, toStoreItem } from "./itemDataService.ts";
35
34
import { DailyDeal } from "../models/worldStateModel.ts";
36
35
import { fromMongoDate, toMongoDate } from "../helpers/inventoryHelpers.ts";
37
36
import { Guild } from "../models/guildModel.ts";
@@ -200,10 +199,22 @@ export const handlePurchase = async (
200
199
undefined,
201
200
false,
202
201
purchaseRequest.PurchaseParams.UsePremium,
203
seed
202
seed,
203
purchaseRequest.buildLabel
204
204
);
205
205
combineInventoryChanges(purchaseResponse.InventoryChanges, prePurchaseInventoryChanges);
206
206
207
if (!purchaseRequest.PurchaseParams.ExpectedPrice) {
208
logger.debug(`client didn't provide ExpectedPrice, attempt to get it from PE+`);
209
purchaseRequest.PurchaseParams.ExpectedPrice = getPrice(
210
purchaseRequest.PurchaseParams.StoreItem,
211
purchaseRequest.PurchaseParams.Quantity,
212
purchaseRequest.PurchaseParams.Durability,
213
purchaseRequest.PurchaseParams.UsePremium,
214
purchaseRequest.buildLabel
215
);
216
}
217
207
218
updateCurrency(
208
219
inventory,
209
220
purchaseRequest.PurchaseParams.ExpectedPrice,
@@ -402,13 +413,14 @@ export const handleDailyDealPurchase = async (
402
413
}
403
414
};
404
415
405
export const handleBundleAcqusition = async (
416
export const handleBundleAcquisition = async (
406
417
storeItemName: string,
407
418
inventory: TInventoryDatabaseDocument,
408
419
quantity: number = 1,
409
inventoryChanges: IInventoryChanges = {}
420
inventoryChanges: IInventoryChanges = {},
421
buildLabel?: string
410
422
): Promise<IInventoryChanges> => {
411
const bundle = ExportBundles[storeItemName];
423
const bundle = getBundle(storeItemName, buildLabel)!;
412
424
logger.debug("acquiring bundle", bundle);
413
425
for (const component of bundle.components) {
414
426
combineInventoryChanges(
@@ -419,7 +431,10 @@ export const handleBundleAcqusition = async (
419
431
inventory,
420
432
component.purchaseQuantity * quantity,
421
433
component.durabilityDays,
422
true
434
true,
435
true,
436
undefined,
437
buildLabel
423
438
)
424
439
).InventoryChanges
425
440
);
@@ -434,14 +449,21 @@ export const handleStoreItemAcquisition = async (
434
449
durabilityDays: number = 3,
435
450
ignorePurchaseQuantity: boolean = false,
436
451
premiumPurchase: boolean = true,
437
seed?: bigint
452
seed?: bigint,
453
buildLabel?: string
438
454
): Promise<IPurchaseResponse> => {
439
455
let purchaseResponse = {
440
456
InventoryChanges: {}
441
457
};
442
logger.debug(`handling acquision of ${storeItemName}`);
458
logger.debug(`handling acquisition of ${storeItemName}`);
443
459
if (storeItemName in ExportBundles) {
444
await handleBundleAcqusition(storeItemName, inventory, quantity, purchaseResponse.InventoryChanges);
460
await handleBundleAcquisition(
461
storeItemName,
462
inventory,
463
quantity,
464
purchaseResponse.InventoryChanges,
465
buildLabel
466
);
445
467
} else {
446
468
const storeCategory = getStoreItemCategory(storeItemName);
447
469
const internalName = fromStoreItem(storeItemName);
@@ -477,7 +499,8 @@ export const handleStoreItemAcquisition = async (
477
499
quantity,
478
500
ignorePurchaseQuantity,
479
501
premiumPurchase,
480
seed
502
seed,
503
buildLabel
481
504
);
482
505
break;
483
506
case "Boosters":
@@ -544,15 +567,17 @@ const handleSlotPurchase = (
544
567
const handleBoosterPackPurchase = async (
545
568
typeName: string,
546
569
inventory: TInventoryDatabaseDocument,
547
quantity: number
570
quantity: number,
571
buildLabel?: string
548
572
): Promise<IPurchaseResponse> => {
549
const pack = ExportBoosterPacks[typeName];
573
const pack = getBoosterPack(typeName, buildLabel);
550
574
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
551
575
if (!pack) {
552
576
throw new Error(`unknown booster pack: ${typeName}`);
553
577
}
554
578
const purchaseResponse: IPurchaseResponse = {
555
579
BoosterPackItems: "",
580
Body: "",
556
581
InventoryChanges: {}
557
582
};
558
583
if (quantity < 1) {
@@ -622,15 +647,26 @@ const handleBoosterPackPurchase = async (
622
647
if (!pack.canGiveDuplicates) {
623
648
disallowedItems.add(result.Item);
624
649
}
625
purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + ',{"lvl":0};';
650
const fingerprint = { lvl: 0 };
651
if (result.Item.startsWith("/Lotus/Upgrades/Mods/Fusers/")) {
652
if (result.Item.endsWith("RareModFuser")) {
653
fingerprint.lvl = Math.floor(Math.random() * 4) + 2;
654
} else if (result.Item.endsWith("CommonModFuser") || result.Item.endsWith("UncommonModFuser")) {
655
fingerprint.lvl = Math.floor(Math.random() * 3) + 1;
656
}
657
}
658
const stringifiedFingerprint = JSON.stringify(fingerprint);
659
purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + `,${stringifiedFingerprint};`;
626
660
combineInventoryChanges(
627
661
purchaseResponse.InventoryChanges,
628
await addItem(inventory, result.Item, result.Amount)
662
await addItem(inventory, result.Item, result.Amount, false, undefined, stringifiedFingerprint)
629
663
);
630
664
++roll;
631
665
}
632
666
}
633
667
}
668
if (purchaseResponse.BoosterPackItems)
669
purchaseResponse.Body = purchaseResponse.BoosterPackItems.replace(/[{}"]/g, "").replace(/:/g, "=");
634
670
return purchaseResponse;
635
671
};
636
672
@@ -657,7 +693,8 @@ const handleTypesPurchase = async (
657
693
quantity: number,
658
694
ignorePurchaseQuantity: boolean,
659
695
premiumPurchase: boolean = true,
660
seed?: bigint
696
seed?: bigint,
697
buildLabel?: string
661
698
): Promise<IPurchaseResponse> => {
662
699
const typeCategory = getStoreItemTypesCategory(typesName);
663
700
logger.debug(`type category ${typeCategory}`);
@@ -667,7 +704,7 @@ const handleTypesPurchase = async (
667
704
InventoryChanges: await addItem(inventory, typesName, quantity, premiumPurchase, seed, undefined, true)
668
705
};
669
706
case "BoosterPacks":
670
return handleBoosterPackPurchase(typesName, inventory, quantity);
707
return handleBoosterPackPurchase(typesName, inventory, quantity, buildLabel);
671
708
case "SlotItems":
672
709
return handleSlotPurchase(typesName, inventory, quantity, ignorePurchaseQuantity);
673
710
case "CreditBundles":
@@ -16,7 +16,7 @@ import { logger } from "../utils/logger.ts";
16
16
import { ExportKeys, ExportRecipes } from "warframe-public-export-plus";
17
17
import { addFixedLevelRewards } from "./missionInventoryUpdateService.ts";
18
18
import { fromOid } from "../helpers/inventoryHelpers.ts";
19
import { handleBundleAcqusition } from "./purchaseService.ts";
19
import { handleBundleAcquisition } from "./purchaseService.ts";
20
20
import type { IInventoryChanges } from "../types/purchaseTypes.ts";
21
21
import questCompletionItems from "../../static/fixed_responses/questCompletionRewards.json" with { type: "json" };
22
22
import type { ITypeCount } from "../types/commonTypes.ts";
@@ -304,7 +304,7 @@ const handleQuestCompletion = async (
304
304
}
305
305
if (!syndicate.Initiated) {
306
306
syndicate.Initiated = true;
307
await handleBundleAcqusition("/Lotus/Types/StoreItems/Packages/SanctuaryInitiationKit", inventory);
307
await handleBundleAcquisition("/Lotus/Types/StoreItems/Packages/SanctuaryInitiationKit", inventory);
308
308
}
309
309
} else if (questKey == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain" && !isRerun) {
310
310
setupKahlSyndicate(inventory);
@@ -53,12 +53,13 @@ export interface IPurchaseParams {
53
53
Source: PurchaseSource;
54
54
SourceId?: string; // VoidTrader, Vendor, PrimeVaultTrader
55
55
StoreItem: string;
56
StorePage: string;
57
SearchTerm: string;
58
CurrentLocation: string;
56
StorePage?: string;
57
SearchTerm?: string;
58
CurrentLocation?: string;
59
59
Quantity: number;
60
60
UsePremium: boolean;
61
ExpectedPrice: number;
61
ExpectedPrice?: number;
62
Durability?: number;
62
63
SyndicateTag?: string; // SyndicateFavor
63
64
UseFreeFavor?: boolean; // SyndicateFavor
64
65
ExtraPurchaseInfoJson?: string; // Vendor
@@ -120,6 +121,7 @@ export interface IPurchaseResponse {
120
121
Standing?: IAffiliationMods[];
121
122
FreeFavorsUsed?: IAffiliationMods[];
122
123
BoosterPackItems?: string;
124
Body?: string;
123
125
DailyDealUsed?: string;
124
126
}
125
127