diff --git a/src/controllers/api/purchaseController.ts b/src/controllers/api/purchaseController.ts index ba314845..390e8d72 100644 --- a/src/controllers/api/purchaseController.ts +++ b/src/controllers/api/purchaseController.ts @@ -11,6 +11,7 @@ export const purchaseController: RequestHandler = async (req, res) => { const inventory = await getInventory(accountId); const response = await handlePurchase(purchaseRequest, inventory); await inventory.save(); + //console.log(JSON.stringify(response, null, 2)); res.json(response); sendWsBroadcastTo(accountId, { update_inventory: true }); }; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index cd597bb4..44bdb241 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -91,7 +91,7 @@ import { ICrewMemberSkillEfficiency, ICrewMemberDatabase, ICrewMemberClient, - ISortieRewardAttenuation, + IRewardAttenuation, IInvasionProgressDatabase, IInvasionProgressClient, IAccolades, @@ -1394,10 +1394,10 @@ lastSortieRewardSchema.set("toJSON", { } }); -const sortieRewardAttenutationSchema = new Schema( +const rewardAttenutationSchema = new Schema( { - Tag: String, - Atten: Number + Tag: { type: String, required: true }, + Atten: { type: Number, required: true } }, { _id: false } ); @@ -1640,7 +1640,7 @@ const inventorySchema = new Schema( CompletedSorties: [String], LastSortieReward: { type: [lastSortieRewardSchema], default: undefined }, LastLiteSortieReward: { type: [lastSortieRewardSchema], default: undefined }, - SortieRewardAttenuation: { type: [sortieRewardAttenutationSchema], default: undefined }, + SortieRewardAttenuation: { type: [rewardAttenutationSchema], default: undefined }, // Resource Extractor Drones Drones: [droneSchema], @@ -1780,7 +1780,9 @@ const inventorySchema = new Schema( HubNpcCustomizations: { type: [hubNpcCustomizationSchema], default: undefined }, - ClaimedJunctionChallengeRewards: { type: [String], default: undefined } + ClaimedJunctionChallengeRewards: { type: [String], default: undefined }, + + SpecialItemRewardAttenuation: { type: [rewardAttenutationSchema], default: undefined } }, { timestamps: { createdAt: "Created", updatedAt: false } } ); diff --git a/src/services/importService.ts b/src/services/importService.ts index 3f7f0051..29eee138 100644 --- a/src/services/importService.ts +++ b/src/services/importService.ts @@ -296,6 +296,12 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< db[key] = client[key]; } } + // IRewardAtten[] + for (const key of ["SortieRewardAttenuation", "SpecialItemRewardAttenuation"] as const) { + if (client[key] !== undefined) { + db[key] = client[key]; + } + } if (client.XPInfo !== undefined) { db.XPInfo = client.XPInfo; } diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index fafbca6c..d4997c28 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -43,6 +43,7 @@ import { } from "../types/inventoryTypes/commonInventoryTypes"; import { ExportArcanes, + ExportBoosters, ExportBundles, ExportChallenges, ExportCustoms, @@ -671,6 +672,17 @@ export const addItem = async ( return await addEmailItem(inventory, typeName); } + // Boosters are an odd case. They're only added like this via Baro's Void Surplus afaik. + { + const boosterEntry = Object.entries(ExportBoosters).find(arr => arr[1].typeName == typeName); + if (boosterEntry) { + addBooster(typeName, quantity, inventory); + return { + Boosters: [{ ItemType: typeName, ExpiryDate: quantity }] + }; + } + } + // Path-based duck typing switch (typeName.substr(1).split("/")[1]) { case "Powersuits": @@ -1330,7 +1342,7 @@ export const addCustomization = ( customizationName: string, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { - if (!inventory.FlavourItems.find(x => x.ItemType == customizationName)) { + if (!inventory.FlavourItems.some(x => x.ItemType == customizationName)) { const flavourItemIndex = inventory.FlavourItems.push({ ItemType: customizationName }) - 1; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition inventoryChanges.FlavourItems ??= []; @@ -1346,7 +1358,7 @@ export const addSkin = ( typeName: string, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { - if (inventory.WeaponSkins.find(x => x.ItemType == typeName)) { + if (inventory.WeaponSkins.some(x => x.ItemType == typeName)) { logger.debug(`refusing to add WeaponSkin ${typeName} because account already owns it`); } else { const index = inventory.WeaponSkins.push({ ItemType: typeName, IsNew: true }) - 1; diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 0c4cad35..20b8a987 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -8,7 +8,7 @@ import { updateCurrency, updateSlots } from "@/src/services/inventoryService"; -import { getRandomWeightedRewardUc } from "@/src/services/rngService"; +import { getRandomReward, getRandomWeightedRewardUc } from "@/src/services/rngService"; import { applyStandingToVendorManifest, getVendorManifestByOid } from "@/src/services/serversideVendorsService"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { @@ -35,6 +35,7 @@ import { import { config } from "./configService"; import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel"; import { fromStoreItem, toStoreItem } from "./itemDataService"; +import { fromMongoDate, toMongoDate } from "../helpers/inventoryHelpers"; export const getStoreItemCategory = (storeItem: string): string => { const storeItemString = getSubstringFromKeyword(storeItem, "StoreItems/"); @@ -51,6 +52,58 @@ export const getStoreItemTypesCategory = (typesItem: string): string => { return typeElements[1]; }; +const tallyVendorPurchase = ( + inventory: TInventoryDatabaseDocument, + inventoryChanges: IInventoryChanges, + VendorType: string, + ItemId: string, + numPurchased: number, + Expiry: Date +): void => { + if (!config.noVendorPurchaseLimits) { + inventory.RecentVendorPurchases ??= []; + let vendorPurchases = inventory.RecentVendorPurchases.find(x => x.VendorType == VendorType); + if (!vendorPurchases) { + vendorPurchases = + inventory.RecentVendorPurchases[ + inventory.RecentVendorPurchases.push({ + VendorType: VendorType, + PurchaseHistory: [] + }) - 1 + ]; + } + let historyEntry = vendorPurchases.PurchaseHistory.find(x => x.ItemId == ItemId); + if (historyEntry) { + if (Date.now() >= historyEntry.Expiry.getTime()) { + historyEntry.NumPurchased = numPurchased; + historyEntry.Expiry = Expiry; + } else { + historyEntry.NumPurchased += numPurchased; + } + } else { + historyEntry = + vendorPurchases.PurchaseHistory[ + vendorPurchases.PurchaseHistory.push({ + ItemId: ItemId, + NumPurchased: numPurchased, + Expiry: Expiry + }) - 1 + ]; + } + inventoryChanges.NewVendorPurchase = { + VendorType: VendorType, + PurchaseHistory: [ + { + ItemId: ItemId, + NumPurchased: historyEntry.NumPurchased, + Expiry: toMongoDate(Expiry) + } + ] + }; + inventoryChanges.RecentVendorPurchases = inventoryChanges.NewVendorPurchase; + } +}; + export const handlePurchase = async ( purchaseRequest: IPurchaseRequest, inventory: TInventoryDatabaseDocument @@ -97,20 +150,7 @@ export const handlePurchase = async ( if (offer.LocTagRandSeed !== undefined) { seed = BigInt(offer.LocTagRandSeed); } - if (!config.noVendorPurchaseLimits && ItemId) { - inventory.RecentVendorPurchases ??= []; - let vendorPurchases = inventory.RecentVendorPurchases.find( - x => x.VendorType == manifest!.VendorInfo.TypeName - ); - if (!vendorPurchases) { - vendorPurchases = - inventory.RecentVendorPurchases[ - inventory.RecentVendorPurchases.push({ - VendorType: manifest.VendorInfo.TypeName, - PurchaseHistory: [] - }) - 1 - ]; - } + if (ItemId) { let expiry = parseInt(offer.Expiry.$date.$numberLong); if (purchaseRequest.PurchaseParams.IsWeekly) { const EPOCH = 1734307200 * 1000; // Monday @@ -118,34 +158,14 @@ export const handlePurchase = async ( const weekStart = EPOCH + week * 604800000; expiry = weekStart + 604800000; } - const historyEntry = vendorPurchases.PurchaseHistory.find(x => x.ItemId == ItemId); - let numPurchased = purchaseRequest.PurchaseParams.Quantity; - if (historyEntry) { - if (Date.now() >= historyEntry.Expiry.getTime()) { - historyEntry.NumPurchased = numPurchased; - historyEntry.Expiry = new Date(expiry); - } else { - numPurchased += historyEntry.NumPurchased; - historyEntry.NumPurchased += purchaseRequest.PurchaseParams.Quantity; - } - } else { - vendorPurchases.PurchaseHistory.push({ - ItemId: ItemId, - NumPurchased: purchaseRequest.PurchaseParams.Quantity, - Expiry: new Date(expiry) - }); - } - prePurchaseInventoryChanges.NewVendorPurchase = { - VendorType: manifest.VendorInfo.TypeName, - PurchaseHistory: [ - { - ItemId: ItemId, - NumPurchased: numPurchased, - Expiry: { $date: { $numberLong: expiry.toString() } } - } - ] - }; - prePurchaseInventoryChanges.RecentVendorPurchases = prePurchaseInventoryChanges.NewVendorPurchase; + tallyVendorPurchase( + inventory, + prePurchaseInventoryChanges, + manifest.VendorInfo.TypeName, + ItemId, + purchaseRequest.PurchaseParams.Quantity, + new Date(expiry) + ); } purchaseRequest.PurchaseParams.Quantity *= offer.QuantityMultiplier; } else { @@ -191,7 +211,7 @@ export const handlePurchase = async ( throw new Error(`vendor purchase should not have an expected price`); } - if (!config.dontSubtractPurchaseItemCost) { + if (offer.PrimePrice && !config.dontSubtractPurchaseItemCost) { const invItem: IMiscItem = { ItemType: "/Lotus/Types/Items/MiscItems/PrimeBucks", ItemCount: offer.PrimePrice * purchaseRequest.PurchaseParams.Quantity * -1 @@ -200,6 +220,17 @@ export const handlePurchase = async ( purchaseResponse.InventoryChanges.MiscItems ??= []; purchaseResponse.InventoryChanges.MiscItems.push(invItem); } + + if (offer.Limit) { + tallyVendorPurchase( + inventory, + purchaseResponse.InventoryChanges, + "VoidTrader", + offer.ItemType, + purchaseRequest.PurchaseParams.Quantity, + fromMongoDate(worldState.VoidTraders[0].Expiry) + ); + } } break; } @@ -482,12 +513,57 @@ const handleBoosterPackPurchase = async ( "attempt to roll over 100 booster packs in a single go. possible but unlikely to be desirable for the user or the server." ); } + const specialItemReward = pack.components.find(x => x.PityIncreaseRate); for (let i = 0; i != quantity; ++i) { - const disallowedItems = new Set(); - for (let roll = 0; roll != pack.rarityWeightsPerRoll.length; ) { - const weights = pack.rarityWeightsPerRoll[roll]; - const result = getRandomWeightedRewardUc(pack.components, weights); - if (result) { + if (specialItemReward) { + { + const normalComponents = []; + for (const comp of pack.components) { + if (!comp.PityIncreaseRate) { + const { Probability, ...rest } = comp; + normalComponents.push({ + ...rest, + probability: Probability! + }); + } + } + const result = getRandomReward(normalComponents)!; + logger.debug(`booster pack rolled`, result); + purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + ',{"lvl":0};'; + combineInventoryChanges( + purchaseResponse.InventoryChanges, + await addItem(inventory, result.Item, result.Amount) + ); + } + + if (!inventory.WeaponSkins.some(x => x.ItemType == specialItemReward.Item)) { + inventory.SpecialItemRewardAttenuation ??= []; + let atten = inventory.SpecialItemRewardAttenuation.find(x => x.Tag == specialItemReward.Item); + if (!atten) { + atten = + inventory.SpecialItemRewardAttenuation[ + inventory.SpecialItemRewardAttenuation.push({ + Tag: specialItemReward.Item, + Atten: specialItemReward.Probability! + }) - 1 + ]; + } + if (Math.random() < atten.Atten) { + purchaseResponse.BoosterPackItems += toStoreItem(specialItemReward.Item) + ',{"lvl":0};'; + combineInventoryChanges( + purchaseResponse.InventoryChanges, + await addItem(inventory, specialItemReward.Item) + ); + // TOVERIFY: Is the SpecialItemRewardAttenuation entry removed now? + } else { + atten.Atten += specialItemReward.PityIncreaseRate!; + } + } + } else { + const disallowedItems = new Set(); + for (let roll = 0; roll != pack.rarityWeightsPerRoll.length; ) { + const weights = pack.rarityWeightsPerRoll[roll]; + const result = getRandomWeightedRewardUc(pack.components, weights)!; logger.debug(`booster pack rolled`, result); if (disallowedItems.has(result.Item)) { logger.debug(`oops, can't use that one; trying again`); @@ -497,9 +573,12 @@ const handleBoosterPackPurchase = async ( disallowedItems.add(result.Item); } purchaseResponse.BoosterPackItems += toStoreItem(result.Item) + ',{"lvl":0};'; - combineInventoryChanges(purchaseResponse.InventoryChanges, await addItem(inventory, result.Item, 1)); + combineInventoryChanges( + purchaseResponse.InventoryChanges, + await addItem(inventory, result.Item, result.Amount) + ); + ++roll; } - ++roll; } } return purchaseResponse; diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 4c4a9821..2d5b6472 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -295,7 +295,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu CompletedSorties: string[]; LastSortieReward?: ILastSortieRewardClient[]; LastLiteSortieReward?: ILastSortieRewardClient[]; - SortieRewardAttenuation?: ISortieRewardAttenuation[]; + SortieRewardAttenuation?: IRewardAttenuation[]; Drones: IDroneClient[]; StepSequencers: IStepSequencer[]; ActiveAvatarImageType?: string; @@ -381,6 +381,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu HubNpcCustomizations?: IHubNpcCustomization[]; Ship?: IOrbiter; // U22 and below, response only ClaimedJunctionChallengeRewards?: string[]; // U39 + SpecialItemRewardAttenuation?: IRewardAttenuation[]; // Baro's Void Surplus } export interface IAffiliation { @@ -783,7 +784,7 @@ export interface ILastSortieRewardDatabase extends Omit