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

XFESpaceNinjaServer

A simple server for a small space ninja game

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

XFEstudio/XFESpaceNinjaServer

feat: handle acquisition of booster packs (#452)

c7c9d901
Sainan <sainan@calamity.gg>
提交于

代码差异

6 个文件 +129 -52
Modified package-lock.json +4 -4
@@ -12,7 +12,7 @@
12 12 "copyfiles": "^2.4.1",
13 13 "express": "^5.0.0-beta.3",
14 14 "mongoose": "^8.1.1",
15 "warframe-public-export-plus": "^0.4.0",
15 "warframe-public-export-plus": "^0.4.1",
16 16 "warframe-riven-info": "^0.1.0",
17 17 "winston": "^3.11.0",
18 18 "winston-daily-rotate-file": "^4.7.1"
@@ -3669,9 +3669,9 @@
3669 3669 }
3670 3670 },
3671 3671 "node_modules/warframe-public-export-plus": {
3672 "version": "0.4.0",
3673 "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.4.0.tgz",
3674 "integrity": "sha512-8wOkh9dET4IHmHDSZ8g8RW0GlfEevHnBwEETAqy3jRhwssyF0TgQsOOpJVuhcPKedCYeudR92HJ3JoXoiTCr6A=="
3672 "version": "0.4.1",
3673 "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.4.1.tgz",
3674 "integrity": "sha512-5SwnT/K/rMI0zJpdodzeEPlO/UnMlHiKv8NZGH647/5u52LZf8xfOpJHP4/yr/anjVVzDQJwY5K3CmbX0uMQdw=="
3675 3675 },
3676 3676 "node_modules/warframe-riven-info": {
3677 3677 "version": "0.1.0",
Modified package.json +1 -1
@@ -16,7 +16,7 @@
16 16 "copyfiles": "^2.4.1",
17 17 "express": "^5.0.0-beta.3",
18 18 "mongoose": "^8.1.1",
19 "warframe-public-export-plus": "^0.4.0",
19 "warframe-public-export-plus": "^0.4.1",
20 20 "warframe-riven-info": "^0.1.0",
21 21 "winston": "^3.11.0",
22 22 "winston-daily-rotate-file": "^4.7.1"
Modified src/services/inventoryService.ts +68 -2
@@ -2,7 +2,7 @@ import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
2 2 import new_inventory from "@/static/fixed_responses/postTutorialInventory.json";
3 3 import { config } from "@/src/services/configService";
4 4 import { Types } from "mongoose";
5 import { SlotNames, IInventoryChanges } from "@/src/types/purchaseTypes";
5 import { SlotNames, IInventoryChanges, IBinChanges } from "@/src/types/purchaseTypes";
6 6 import {
7 7 IChallengeProgress,
8 8 IConsumable,
@@ -27,9 +27,16 @@ import {
27 27 } from "../types/requestTypes";
28 28 import { logger } from "@/src/utils/logger";
29 29 import { getWeaponType, getExalted } from "@/src/services/itemDataService";
30 import { getRandomWeightedReward } from "@/src/services/rngService";
30 31 import { ISyndicateSacrifice, ISyndicateSacrificeResponse } from "../types/syndicateTypes";
31 32 import { IEquipmentClient } from "../types/inventoryTypes/commonInventoryTypes";
32 import { ExportCustoms, ExportFlavour, ExportRecipes, ExportResources } from "warframe-public-export-plus";
33 import {
34 ExportBoosterPacks,
35 ExportCustoms,
36 ExportFlavour,
37 ExportRecipes,
38 ExportResources
39 } from "warframe-public-export-plus";
33 40
34 41 export const createInventory = async (
35 42 accountOwnerId: Types.ObjectId,
@@ -59,6 +66,31 @@ export const createInventory = async (
59 66 }
60 67 };
61 68
69 export const combineInventoryChanges = (InventoryChanges: IInventoryChanges, delta: IInventoryChanges): void => {
70 for (const key in delta) {
71 if (!(key in InventoryChanges)) {
72 InventoryChanges[key] = delta[key];
73 } else if (Array.isArray(delta[key])) {
74 const left = InventoryChanges[key] as object[];
75 const right = delta[key] as object[];
76 for (const item of right) {
77 left.push(item);
78 }
79 } else {
80 console.assert(key.substring(-3) == "Bin");
81 const left = InventoryChanges[key] as IBinChanges;
82 const right = delta[key] as IBinChanges;
83 left.count += right.count;
84 left.platinum += right.platinum;
85 left.Slots += right.Slots;
86 if (right.Extra) {
87 left.Extra ??= 0;
88 left.Extra += right.Extra;
89 }
90 }
91 }
92 };
93
62 94 export const getInventory = async (accountOwnerId: string) => {
63 95 const inventory = await Inventory.findOne({ accountOwnerId: accountOwnerId });
64 96
@@ -121,6 +153,21 @@ export const addItem = async (
121 153 }
122 154 };
123 155 }
156 if (typeName in ExportBoosterPacks) {
157 const pack = ExportBoosterPacks[typeName];
158 const InventoryChanges = {};
159 for (const weights of pack.rarityWeightsPerRoll) {
160 const result = getRandomWeightedReward(pack.components, weights);
161 if (result) {
162 logger.debug(`booster pack rolled`, result);
163 combineInventoryChanges(
164 InventoryChanges,
165 (await addItem(accountId, result.type, result.itemCount)).InventoryChanges
166 );
167 }
168 }
169 return { InventoryChanges };
170 }
124 171
125 172 // Path-based duck typing
126 173 switch (typeName.substr(1).split("/")[1]) {
@@ -261,6 +308,25 @@ export const addItem = async (
261 308 }
262 309 }
263 310 }
311 case "Game":
312 if (typeName.substr(1).split("/")[3] == "Projections") {
313 // Void Relics, e.g. /Lotus/Types/Game/Projections/T2VoidProjectionGaussPrimeDBronze
314 const inventory = await getInventory(accountId);
315 const miscItemChanges = [
316 {
317 ItemType: typeName,
318 ItemCount: quantity
319 } satisfies IMiscItem
320 ];
321 addMiscItems(inventory, miscItemChanges);
322 await inventory.save();
323 return {
324 InventoryChanges: {
325 MiscItems: miscItemChanges
326 }
327 };
328 }
329 break;
264 330 case "Restoratives": // Codex Scanner, Remote Observer, Starburst
265 331 const inventory = await getInventory(accountId);
266 332 const consumablesChanges = [
Modified src/services/missionInventoryUpdateService.ts +5 -17
@@ -11,6 +11,7 @@ import {
11 11 } from "warframe-public-export-plus";
12 12 import { IMissionInventoryUpdateRequest } from "../types/requestTypes";
13 13 import { logger } from "@/src/utils/logger";
14 import { IRngResult, getRandomReward } from "@/src/services/rngService";
14 15
15 16 // need reverse engineer rewardSeed, otherwise ingame displayed rotation reward will be different than added to db or displayed on mission end
16 17 const getRewards = ({
@@ -23,7 +24,7 @@ const getRewards = ({
23 24 return { InventoryChanges: {}, MissionRewards: [] };
24 25 }
25 26
26 const drops: IReward[] = [];
27 const drops: IRngResult[] = [];
27 28 if (RewardInfo.node in ExportRegions) {
28 29 const region = ExportRegions[RewardInfo.node];
29 30 const rewardManifests = region.rewardManifests ?? [];
@@ -117,21 +118,8 @@ const getRotations = (rotationCount: number): number[] => {
117 118 return rotatedValues;
118 119 };
119 120
120 const getRandomRewardByChance = (data: IReward[]): IReward | undefined => {
121 if (data.length == 0) return;
122
123 const totalChance = data.reduce((sum, item) => sum + item.probability!, 0);
124 const randomValue = Math.random() * totalChance;
125
126 let cumulativeChance = 0;
127 for (const item of data) {
128 cumulativeChance += item.probability!;
129 if (randomValue <= cumulativeChance) {
130 return item;
131 }
132 }
133
134 return;
121 const getRandomRewardByChance = (pool: IReward[]): IRngResult | undefined => {
122 return getRandomReward(pool as IRngResult[]);
135 123 };
136 124
137 125 const creditBundles: Record<string, number> = {
@@ -157,7 +145,7 @@ const fusionBundles: Record<string, number> = {
157 145 };
158 146
159 147 const formatRewardsToInventoryType = (
160 rewards: IReward[]
148 rewards: IRngResult[]
161 149 ): { InventoryChanges: IMissionInventoryUpdateRequest; MissionRewards: IMissionRewardResponse[] } => {
162 150 const InventoryChanges: IMissionInventoryUpdateRequest = {};
163 151 const MissionRewards: IMissionRewardResponse[] = [];
Modified src/services/purchaseService.ts +9 -28
@@ -1,7 +1,13 @@
1 1 import { parseSlotPurchaseName } from "@/src/helpers/purchaseHelpers";
2 2 import { getSubstringFromKeyword } from "@/src/helpers/stringHelpers";
3 import { addItem, addBooster, updateCurrency, updateSlots } from "@/src/services/inventoryService";
4 import { IPurchaseRequest, SlotPurchase, IInventoryChanges, IBinChanges } from "@/src/types/purchaseTypes";
3 import {
4 addItem,
5 addBooster,
6 combineInventoryChanges,
7 updateCurrency,
8 updateSlots
9 } from "@/src/services/inventoryService";
10 import { IPurchaseRequest, SlotPurchase, IInventoryChanges } from "@/src/types/purchaseTypes";
5 11 import { logger } from "@/src/utils/logger";
6 12 import { ExportBundles, ExportGear, TRarity } from "warframe-public-export-plus";
7 13
@@ -46,31 +52,6 @@ export const handlePurchase = async (purchaseRequest: IPurchaseRequest, accountI
46 52 return purchaseResponse;
47 53 };
48 54
49 const addInventoryChanges = (InventoryChanges: IInventoryChanges, delta: IInventoryChanges): void => {
50 for (const key in delta) {
51 if (!(key in InventoryChanges)) {
52 InventoryChanges[key] = delta[key];
53 } else if (Array.isArray(delta[key])) {
54 const left = InventoryChanges[key] as object[];
55 const right = delta[key] as object[];
56 for (const item of right) {
57 left.push(item);
58 }
59 } else {
60 console.assert(key.substring(-3) == "Bin");
61 const left = InventoryChanges[key] as IBinChanges;
62 const right = delta[key] as IBinChanges;
63 left.count += right.count;
64 left.platinum += right.platinum;
65 left.Slots += right.Slots;
66 if (right.Extra) {
67 left.Extra ??= 0;
68 left.Extra += right.Extra;
69 }
70 }
71 }
72 };
73
74 55 const handleStoreItemAcquisition = async (
75 56 storeItemName: string,
76 57 accountId: string,
@@ -86,7 +67,7 @@ const handleStoreItemAcquisition = async (
86 67 const bundle = ExportBundles[storeItemName];
87 68 logger.debug("acquiring bundle", bundle);
88 69 for (const component of bundle.components) {
89 addInventoryChanges(
70 combineInventoryChanges(
90 71 purchaseResponse.InventoryChanges,
91 72 (
92 73 await handleStoreItemAcquisition(
Added src/services/rngService.ts +42 -0
@@ -0,0 +1,42 @@
1 import { TRarity } from "warframe-public-export-plus";
2
3 export interface IRngResult {
4 type: string;
5 itemCount: number;
6 probability: number;
7 }
8
9 export const getRandomReward = (pool: IRngResult[]): IRngResult | undefined => {
10 if (pool.length == 0) return;
11
12 const totalChance = pool.reduce((accum, item) => accum + item.probability, 0);
13 const randomValue = Math.random() * totalChance;
14
15 let cumulativeChance = 0;
16 for (const item of pool) {
17 cumulativeChance += item.probability;
18 if (randomValue <= cumulativeChance) {
19 return item;
20 }
21 }
22 throw new Error("What the fuck?");
23 };
24
25 export const getRandomWeightedReward = (
26 pool: { Item: string; Rarity: TRarity }[],
27 weights: Record<TRarity, number>
28 ): IRngResult | undefined => {
29 const resultPool: IRngResult[] = [];
30 const rarityCounts: Record<TRarity, number> = { COMMON: 0, UNCOMMON: 0, RARE: 0, LEGENDARY: 0 };
31 for (const entry of pool) {
32 ++rarityCounts[entry.Rarity];
33 }
34 for (const entry of pool) {
35 resultPool.push({
36 type: entry.Item,
37 itemCount: 1,
38 probability: weights[entry.Rarity] / rarityCounts[entry.Rarity]
39 });
40 }
41 return getRandomReward(resultPool);
42 };