From c7c9d901b10edcd77bdf881b8da57b4589984169 Mon Sep 17 00:00:00 2001 From: Sainan Date: Wed, 3 Jul 2024 12:30:32 +0200 Subject: [PATCH] feat: handle acquisition of booster packs (#452) --- package-lock.json | 8 +-- package.json | 2 +- src/services/inventoryService.ts | 70 ++++++++++++++++++- src/services/missionInventoryUpdateService.ts | 22 ++---- src/services/purchaseService.ts | 37 +++------- src/services/rngService.ts | 42 +++++++++++ 6 files changed, 129 insertions(+), 52 deletions(-) create mode 100644 src/services/rngService.ts diff --git a/package-lock.json b/package-lock.json index 8bceb7d80..10a9d206d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "copyfiles": "^2.4.1", "express": "^5.0.0-beta.3", "mongoose": "^8.1.1", - "warframe-public-export-plus": "^0.4.0", + "warframe-public-export-plus": "^0.4.1", "warframe-riven-info": "^0.1.0", "winston": "^3.11.0", "winston-daily-rotate-file": "^4.7.1" @@ -3669,9 +3669,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.4.0.tgz", - "integrity": "sha512-8wOkh9dET4IHmHDSZ8g8RW0GlfEevHnBwEETAqy3jRhwssyF0TgQsOOpJVuhcPKedCYeudR92HJ3JoXoiTCr6A==" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.4.1.tgz", + "integrity": "sha512-5SwnT/K/rMI0zJpdodzeEPlO/UnMlHiKv8NZGH647/5u52LZf8xfOpJHP4/yr/anjVVzDQJwY5K3CmbX0uMQdw==" }, "node_modules/warframe-riven-info": { "version": "0.1.0", diff --git a/package.json b/package.json index 522d03c4f..a9ec34788 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "copyfiles": "^2.4.1", "express": "^5.0.0-beta.3", "mongoose": "^8.1.1", - "warframe-public-export-plus": "^0.4.0", + "warframe-public-export-plus": "^0.4.1", "warframe-riven-info": "^0.1.0", "winston": "^3.11.0", "winston-daily-rotate-file": "^4.7.1" diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 93c0aa68f..74e1bba04 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -2,7 +2,7 @@ import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; import new_inventory from "@/static/fixed_responses/postTutorialInventory.json"; import { config } from "@/src/services/configService"; import { Types } from "mongoose"; -import { SlotNames, IInventoryChanges } from "@/src/types/purchaseTypes"; +import { SlotNames, IInventoryChanges, IBinChanges } from "@/src/types/purchaseTypes"; import { IChallengeProgress, IConsumable, @@ -27,9 +27,16 @@ import { } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; import { getWeaponType, getExalted } from "@/src/services/itemDataService"; +import { getRandomWeightedReward } from "@/src/services/rngService"; import { ISyndicateSacrifice, ISyndicateSacrificeResponse } from "../types/syndicateTypes"; import { IEquipmentClient } from "../types/inventoryTypes/commonInventoryTypes"; -import { ExportCustoms, ExportFlavour, ExportRecipes, ExportResources } from "warframe-public-export-plus"; +import { + ExportBoosterPacks, + ExportCustoms, + ExportFlavour, + ExportRecipes, + ExportResources +} from "warframe-public-export-plus"; export const createInventory = async ( accountOwnerId: Types.ObjectId, @@ -59,6 +66,31 @@ export const createInventory = async ( } }; +export const combineInventoryChanges = (InventoryChanges: IInventoryChanges, delta: IInventoryChanges): void => { + for (const key in delta) { + if (!(key in InventoryChanges)) { + InventoryChanges[key] = delta[key]; + } else if (Array.isArray(delta[key])) { + const left = InventoryChanges[key] as object[]; + const right = delta[key] as object[]; + for (const item of right) { + left.push(item); + } + } else { + console.assert(key.substring(-3) == "Bin"); + const left = InventoryChanges[key] as IBinChanges; + const right = delta[key] as IBinChanges; + left.count += right.count; + left.platinum += right.platinum; + left.Slots += right.Slots; + if (right.Extra) { + left.Extra ??= 0; + left.Extra += right.Extra; + } + } + } +}; + export const getInventory = async (accountOwnerId: string) => { const inventory = await Inventory.findOne({ accountOwnerId: accountOwnerId }); @@ -121,6 +153,21 @@ export const addItem = async ( } }; } + if (typeName in ExportBoosterPacks) { + const pack = ExportBoosterPacks[typeName]; + const InventoryChanges = {}; + for (const weights of pack.rarityWeightsPerRoll) { + const result = getRandomWeightedReward(pack.components, weights); + if (result) { + logger.debug(`booster pack rolled`, result); + combineInventoryChanges( + InventoryChanges, + (await addItem(accountId, result.type, result.itemCount)).InventoryChanges + ); + } + } + return { InventoryChanges }; + } // Path-based duck typing switch (typeName.substr(1).split("/")[1]) { @@ -261,6 +308,25 @@ export const addItem = async ( } } } + case "Game": + if (typeName.substr(1).split("/")[3] == "Projections") { + // Void Relics, e.g. /Lotus/Types/Game/Projections/T2VoidProjectionGaussPrimeDBronze + const inventory = await getInventory(accountId); + const miscItemChanges = [ + { + ItemType: typeName, + ItemCount: quantity + } satisfies IMiscItem + ]; + addMiscItems(inventory, miscItemChanges); + await inventory.save(); + return { + InventoryChanges: { + MiscItems: miscItemChanges + } + }; + } + break; case "Restoratives": // Codex Scanner, Remote Observer, Starburst const inventory = await getInventory(accountId); const consumablesChanges = [ diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index b01fbe453..b6f1f542a 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -11,6 +11,7 @@ import { } from "warframe-public-export-plus"; import { IMissionInventoryUpdateRequest } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; +import { IRngResult, getRandomReward } from "@/src/services/rngService"; // need reverse engineer rewardSeed, otherwise ingame displayed rotation reward will be different than added to db or displayed on mission end const getRewards = ({ @@ -23,7 +24,7 @@ const getRewards = ({ return { InventoryChanges: {}, MissionRewards: [] }; } - const drops: IReward[] = []; + const drops: IRngResult[] = []; if (RewardInfo.node in ExportRegions) { const region = ExportRegions[RewardInfo.node]; const rewardManifests = region.rewardManifests ?? []; @@ -117,21 +118,8 @@ const getRotations = (rotationCount: number): number[] => { return rotatedValues; }; -const getRandomRewardByChance = (data: IReward[]): IReward | undefined => { - if (data.length == 0) return; - - const totalChance = data.reduce((sum, item) => sum + item.probability!, 0); - const randomValue = Math.random() * totalChance; - - let cumulativeChance = 0; - for (const item of data) { - cumulativeChance += item.probability!; - if (randomValue <= cumulativeChance) { - return item; - } - } - - return; +const getRandomRewardByChance = (pool: IReward[]): IRngResult | undefined => { + return getRandomReward(pool as IRngResult[]); }; const creditBundles: Record = { @@ -157,7 +145,7 @@ const fusionBundles: Record = { }; const formatRewardsToInventoryType = ( - rewards: IReward[] + rewards: IRngResult[] ): { InventoryChanges: IMissionInventoryUpdateRequest; MissionRewards: IMissionRewardResponse[] } => { const InventoryChanges: IMissionInventoryUpdateRequest = {}; const MissionRewards: IMissionRewardResponse[] = []; diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index add4032c8..29bdc2187 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -1,7 +1,13 @@ import { parseSlotPurchaseName } from "@/src/helpers/purchaseHelpers"; import { getSubstringFromKeyword } from "@/src/helpers/stringHelpers"; -import { addItem, addBooster, updateCurrency, updateSlots } from "@/src/services/inventoryService"; -import { IPurchaseRequest, SlotPurchase, IInventoryChanges, IBinChanges } from "@/src/types/purchaseTypes"; +import { + addItem, + addBooster, + combineInventoryChanges, + updateCurrency, + updateSlots +} from "@/src/services/inventoryService"; +import { IPurchaseRequest, SlotPurchase, IInventoryChanges } from "@/src/types/purchaseTypes"; import { logger } from "@/src/utils/logger"; import { ExportBundles, ExportGear, TRarity } from "warframe-public-export-plus"; @@ -46,31 +52,6 @@ export const handlePurchase = async (purchaseRequest: IPurchaseRequest, accountI return purchaseResponse; }; -const addInventoryChanges = (InventoryChanges: IInventoryChanges, delta: IInventoryChanges): void => { - for (const key in delta) { - if (!(key in InventoryChanges)) { - InventoryChanges[key] = delta[key]; - } else if (Array.isArray(delta[key])) { - const left = InventoryChanges[key] as object[]; - const right = delta[key] as object[]; - for (const item of right) { - left.push(item); - } - } else { - console.assert(key.substring(-3) == "Bin"); - const left = InventoryChanges[key] as IBinChanges; - const right = delta[key] as IBinChanges; - left.count += right.count; - left.platinum += right.platinum; - left.Slots += right.Slots; - if (right.Extra) { - left.Extra ??= 0; - left.Extra += right.Extra; - } - } - } -}; - const handleStoreItemAcquisition = async ( storeItemName: string, accountId: string, @@ -86,7 +67,7 @@ const handleStoreItemAcquisition = async ( const bundle = ExportBundles[storeItemName]; logger.debug("acquiring bundle", bundle); for (const component of bundle.components) { - addInventoryChanges( + combineInventoryChanges( purchaseResponse.InventoryChanges, ( await handleStoreItemAcquisition( diff --git a/src/services/rngService.ts b/src/services/rngService.ts new file mode 100644 index 000000000..fad3a4ffe --- /dev/null +++ b/src/services/rngService.ts @@ -0,0 +1,42 @@ +import { TRarity } from "warframe-public-export-plus"; + +export interface IRngResult { + type: string; + itemCount: number; + probability: number; +} + +export const getRandomReward = (pool: IRngResult[]): IRngResult | undefined => { + if (pool.length == 0) return; + + const totalChance = pool.reduce((accum, item) => accum + item.probability, 0); + const randomValue = Math.random() * totalChance; + + let cumulativeChance = 0; + for (const item of pool) { + cumulativeChance += item.probability; + if (randomValue <= cumulativeChance) { + return item; + } + } + throw new Error("What the fuck?"); +}; + +export const getRandomWeightedReward = ( + pool: { Item: string; Rarity: TRarity }[], + weights: Record +): IRngResult | undefined => { + const resultPool: IRngResult[] = []; + const rarityCounts: Record = { COMMON: 0, UNCOMMON: 0, RARE: 0, LEGENDARY: 0 }; + for (const entry of pool) { + ++rarityCounts[entry.Rarity]; + } + for (const entry of pool) { + resultPool.push({ + type: entry.Item, + itemCount: 1, + probability: weights[entry.Rarity] / rarityCounts[entry.Rarity] + }); + } + return getRandomReward(resultPool); +};