forked from OpenWF/SpaceNinjaServer
feat: handle acquisition of booster packs (#452)
This commit is contained in:
parent
84720a7058
commit
c7c9d901b1
8
package-lock.json
generated
8
package-lock.json
generated
@ -12,7 +12,7 @@
|
|||||||
"copyfiles": "^2.4.1",
|
"copyfiles": "^2.4.1",
|
||||||
"express": "^5.0.0-beta.3",
|
"express": "^5.0.0-beta.3",
|
||||||
"mongoose": "^8.1.1",
|
"mongoose": "^8.1.1",
|
||||||
"warframe-public-export-plus": "^0.4.0",
|
"warframe-public-export-plus": "^0.4.1",
|
||||||
"warframe-riven-info": "^0.1.0",
|
"warframe-riven-info": "^0.1.0",
|
||||||
"winston": "^3.11.0",
|
"winston": "^3.11.0",
|
||||||
"winston-daily-rotate-file": "^4.7.1"
|
"winston-daily-rotate-file": "^4.7.1"
|
||||||
@ -3669,9 +3669,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/warframe-public-export-plus": {
|
"node_modules/warframe-public-export-plus": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.4.1.tgz",
|
||||||
"integrity": "sha512-8wOkh9dET4IHmHDSZ8g8RW0GlfEevHnBwEETAqy3jRhwssyF0TgQsOOpJVuhcPKedCYeudR92HJ3JoXoiTCr6A=="
|
"integrity": "sha512-5SwnT/K/rMI0zJpdodzeEPlO/UnMlHiKv8NZGH647/5u52LZf8xfOpJHP4/yr/anjVVzDQJwY5K3CmbX0uMQdw=="
|
||||||
},
|
},
|
||||||
"node_modules/warframe-riven-info": {
|
"node_modules/warframe-riven-info": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
"copyfiles": "^2.4.1",
|
"copyfiles": "^2.4.1",
|
||||||
"express": "^5.0.0-beta.3",
|
"express": "^5.0.0-beta.3",
|
||||||
"mongoose": "^8.1.1",
|
"mongoose": "^8.1.1",
|
||||||
"warframe-public-export-plus": "^0.4.0",
|
"warframe-public-export-plus": "^0.4.1",
|
||||||
"warframe-riven-info": "^0.1.0",
|
"warframe-riven-info": "^0.1.0",
|
||||||
"winston": "^3.11.0",
|
"winston": "^3.11.0",
|
||||||
"winston-daily-rotate-file": "^4.7.1"
|
"winston-daily-rotate-file": "^4.7.1"
|
||||||
|
@ -2,7 +2,7 @@ import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
|
|||||||
import new_inventory from "@/static/fixed_responses/postTutorialInventory.json";
|
import new_inventory from "@/static/fixed_responses/postTutorialInventory.json";
|
||||||
import { config } from "@/src/services/configService";
|
import { config } from "@/src/services/configService";
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import { SlotNames, IInventoryChanges } from "@/src/types/purchaseTypes";
|
import { SlotNames, IInventoryChanges, IBinChanges } from "@/src/types/purchaseTypes";
|
||||||
import {
|
import {
|
||||||
IChallengeProgress,
|
IChallengeProgress,
|
||||||
IConsumable,
|
IConsumable,
|
||||||
@ -27,9 +27,16 @@ import {
|
|||||||
} from "../types/requestTypes";
|
} from "../types/requestTypes";
|
||||||
import { logger } from "@/src/utils/logger";
|
import { logger } from "@/src/utils/logger";
|
||||||
import { getWeaponType, getExalted } from "@/src/services/itemDataService";
|
import { getWeaponType, getExalted } from "@/src/services/itemDataService";
|
||||||
|
import { getRandomWeightedReward } from "@/src/services/rngService";
|
||||||
import { ISyndicateSacrifice, ISyndicateSacrificeResponse } from "../types/syndicateTypes";
|
import { ISyndicateSacrifice, ISyndicateSacrificeResponse } from "../types/syndicateTypes";
|
||||||
import { IEquipmentClient } from "../types/inventoryTypes/commonInventoryTypes";
|
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 (
|
export const createInventory = async (
|
||||||
accountOwnerId: Types.ObjectId,
|
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) => {
|
export const getInventory = async (accountOwnerId: string) => {
|
||||||
const inventory = await Inventory.findOne({ accountOwnerId: accountOwnerId });
|
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
|
// Path-based duck typing
|
||||||
switch (typeName.substr(1).split("/")[1]) {
|
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
|
case "Restoratives": // Codex Scanner, Remote Observer, Starburst
|
||||||
const inventory = await getInventory(accountId);
|
const inventory = await getInventory(accountId);
|
||||||
const consumablesChanges = [
|
const consumablesChanges = [
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
} from "warframe-public-export-plus";
|
} from "warframe-public-export-plus";
|
||||||
import { IMissionInventoryUpdateRequest } from "../types/requestTypes";
|
import { IMissionInventoryUpdateRequest } from "../types/requestTypes";
|
||||||
import { logger } from "@/src/utils/logger";
|
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
|
// need reverse engineer rewardSeed, otherwise ingame displayed rotation reward will be different than added to db or displayed on mission end
|
||||||
const getRewards = ({
|
const getRewards = ({
|
||||||
@ -23,7 +24,7 @@ const getRewards = ({
|
|||||||
return { InventoryChanges: {}, MissionRewards: [] };
|
return { InventoryChanges: {}, MissionRewards: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const drops: IReward[] = [];
|
const drops: IRngResult[] = [];
|
||||||
if (RewardInfo.node in ExportRegions) {
|
if (RewardInfo.node in ExportRegions) {
|
||||||
const region = ExportRegions[RewardInfo.node];
|
const region = ExportRegions[RewardInfo.node];
|
||||||
const rewardManifests = region.rewardManifests ?? [];
|
const rewardManifests = region.rewardManifests ?? [];
|
||||||
@ -117,21 +118,8 @@ const getRotations = (rotationCount: number): number[] => {
|
|||||||
return rotatedValues;
|
return rotatedValues;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRandomRewardByChance = (data: IReward[]): IReward | undefined => {
|
const getRandomRewardByChance = (pool: IReward[]): IRngResult | undefined => {
|
||||||
if (data.length == 0) return;
|
return getRandomReward(pool as IRngResult[]);
|
||||||
|
|
||||||
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 creditBundles: Record<string, number> = {
|
const creditBundles: Record<string, number> = {
|
||||||
@ -157,7 +145,7 @@ const fusionBundles: Record<string, number> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formatRewardsToInventoryType = (
|
const formatRewardsToInventoryType = (
|
||||||
rewards: IReward[]
|
rewards: IRngResult[]
|
||||||
): { InventoryChanges: IMissionInventoryUpdateRequest; MissionRewards: IMissionRewardResponse[] } => {
|
): { InventoryChanges: IMissionInventoryUpdateRequest; MissionRewards: IMissionRewardResponse[] } => {
|
||||||
const InventoryChanges: IMissionInventoryUpdateRequest = {};
|
const InventoryChanges: IMissionInventoryUpdateRequest = {};
|
||||||
const MissionRewards: IMissionRewardResponse[] = [];
|
const MissionRewards: IMissionRewardResponse[] = [];
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
import { parseSlotPurchaseName } from "@/src/helpers/purchaseHelpers";
|
import { parseSlotPurchaseName } from "@/src/helpers/purchaseHelpers";
|
||||||
import { getSubstringFromKeyword } from "@/src/helpers/stringHelpers";
|
import { getSubstringFromKeyword } from "@/src/helpers/stringHelpers";
|
||||||
import { addItem, addBooster, updateCurrency, updateSlots } from "@/src/services/inventoryService";
|
import {
|
||||||
import { IPurchaseRequest, SlotPurchase, IInventoryChanges, IBinChanges } from "@/src/types/purchaseTypes";
|
addItem,
|
||||||
|
addBooster,
|
||||||
|
combineInventoryChanges,
|
||||||
|
updateCurrency,
|
||||||
|
updateSlots
|
||||||
|
} from "@/src/services/inventoryService";
|
||||||
|
import { IPurchaseRequest, SlotPurchase, IInventoryChanges } from "@/src/types/purchaseTypes";
|
||||||
import { logger } from "@/src/utils/logger";
|
import { logger } from "@/src/utils/logger";
|
||||||
import { ExportBundles, ExportGear, TRarity } from "warframe-public-export-plus";
|
import { ExportBundles, ExportGear, TRarity } from "warframe-public-export-plus";
|
||||||
|
|
||||||
@ -46,31 +52,6 @@ export const handlePurchase = async (purchaseRequest: IPurchaseRequest, accountI
|
|||||||
return purchaseResponse;
|
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 (
|
const handleStoreItemAcquisition = async (
|
||||||
storeItemName: string,
|
storeItemName: string,
|
||||||
accountId: string,
|
accountId: string,
|
||||||
@ -86,7 +67,7 @@ const handleStoreItemAcquisition = async (
|
|||||||
const bundle = ExportBundles[storeItemName];
|
const bundle = ExportBundles[storeItemName];
|
||||||
logger.debug("acquiring bundle", bundle);
|
logger.debug("acquiring bundle", bundle);
|
||||||
for (const component of bundle.components) {
|
for (const component of bundle.components) {
|
||||||
addInventoryChanges(
|
combineInventoryChanges(
|
||||||
purchaseResponse.InventoryChanges,
|
purchaseResponse.InventoryChanges,
|
||||||
(
|
(
|
||||||
await handleStoreItemAcquisition(
|
await handleStoreItemAcquisition(
|
||||||
|
42
src/services/rngService.ts
Normal file
42
src/services/rngService.ts
Normal file
@ -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<TRarity, number>
|
||||||
|
): IRngResult | undefined => {
|
||||||
|
const resultPool: IRngResult[] = [];
|
||||||
|
const rarityCounts: Record<TRarity, number> = { 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);
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user