mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
[econ] fix box roll
This commit is contained in:
+5
-3
@@ -125,9 +125,11 @@ weekly gift — hand over a real item rather than an unopenable box:
|
||||
- **`QueryRedirectRarity` wins over `Rarity`** when present — sf2 carries both and they
|
||||
agree; sf3's boxes carry only `Rarity`.
|
||||
- **An empty pool grants nothing** (logged `query gift-drop rolled nothing`) — an owner of
|
||||
every 4-star item still gets the box, just nothing in it. The `buyItem` response still
|
||||
echoes the drop the player _bought_, i.e. the box; the rolled item shows up in the box
|
||||
itself, via `GET /api/avatar/v2/gifts`.
|
||||
every 4-star item still gets the box, just nothing in it.
|
||||
- **`buyItem` answers with the ROLLED item, not the box.** The client draws the purchase
|
||||
from `BalanceUpdates[0].Data[0]`, and a query drop's own item fields are all empty — echo
|
||||
those and the player sees an empty box for a purchase that actually granted something. The
|
||||
stored box was always correct; only the response was wrong.
|
||||
|
||||
## Consume envelopes
|
||||
|
||||
|
||||
+23
-14
@@ -1777,8 +1777,16 @@ const app = new Hono<App>({ strict: false })
|
||||
if (!paid) return c.json({ error: 'Insufficient balance' }, 400)
|
||||
|
||||
// Grant the item to the recipient, with the gift box that renders it. A box (an
|
||||
// `IsQuery` drop, e.g. sf2's "4-Star Unique Box") rolls its prize in here.
|
||||
const { id: giftId } = await grantGiftDrop(c, receiverId, item.GiftDrop, message)
|
||||
// `IsQuery` drop, e.g. sf2's "4-Star Unique Box") rolls its prize in here, and
|
||||
// `granted.drop` is what the roll landed on — the response has to describe THAT, not
|
||||
// the box, or a query purchase answers with every item field empty and the client
|
||||
// draws an empty box.
|
||||
const { id: giftId, drop: granted } = await grantGiftDrop(
|
||||
c,
|
||||
receiverId,
|
||||
item.GiftDrop,
|
||||
message
|
||||
)
|
||||
|
||||
// Push the debit over the socket so the buyer's client updates the shown total
|
||||
// immediately — the buyer (`id`) is who was charged, in the currency they spent. The
|
||||
@@ -1789,8 +1797,9 @@ const app = new Hono<App>({ strict: false })
|
||||
// The response mirrors a captured real buyItem: `Balance` is the change applied (the
|
||||
// negated price), not the resulting balance (the client reads its new total from
|
||||
// `GET /balance/:type`); `BalanceType` is -2 (account-wide, all platforms). The Data
|
||||
// entry is the gift-drop the client received — it carries no FriendlyName or
|
||||
// consumable count (the count is a getUnlocked concept; each box is one instance).
|
||||
// entry is the gift-drop the client RECEIVED — the rolled item for a query box, the
|
||||
// bought drop otherwise — and it carries no FriendlyName or consumable count (the
|
||||
// count is a getUnlocked concept; each box is one instance).
|
||||
return c.json({
|
||||
BalanceUpdates: [
|
||||
{
|
||||
@@ -1799,22 +1808,22 @@ const app = new Hono<App>({ strict: false })
|
||||
{
|
||||
Id: giftId,
|
||||
FromPlayerId: fromPlayerId,
|
||||
ConsumableItemDesc: item.GiftDrop.ConsumableItemDesc,
|
||||
AvatarItemDesc: item.GiftDrop.AvatarItemDesc,
|
||||
AvatarItemType: item.GiftDrop.AvatarItemType ?? 0,
|
||||
EquipmentPrefabName: item.GiftDrop.EquipmentPrefabName,
|
||||
EquipmentModificationGuid: item.GiftDrop.EquipmentModificationGuid,
|
||||
CurrencyType: item.GiftDrop.CurrencyType,
|
||||
Currency: item.GiftDrop.Currency,
|
||||
Xp: 0,
|
||||
ConsumableItemDesc: granted.ConsumableItemDesc,
|
||||
AvatarItemDesc: granted.AvatarItemDesc,
|
||||
AvatarItemType: granted.AvatarItemType ?? 0,
|
||||
EquipmentPrefabName: granted.EquipmentPrefabName,
|
||||
EquipmentModificationGuid: granted.EquipmentModificationGuid,
|
||||
CurrencyType: granted.CurrencyType,
|
||||
Currency: granted.Currency,
|
||||
Xp: granted.Xp ?? 0,
|
||||
Level: 0,
|
||||
Platform: -1,
|
||||
PlatformsToSpawnOn: -1,
|
||||
BalanceType: ALL_PLATFORMS,
|
||||
GiftContext: Number.isInteger(gift?.GiftContext)
|
||||
? (gift?.GiftContext as number)
|
||||
: item.GiftDrop.Context,
|
||||
GiftRarity: item.GiftDrop.Rarity,
|
||||
: granted.Context,
|
||||
GiftRarity: granted.Rarity,
|
||||
Message: message,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1611,10 +1611,26 @@ describe('econ endpoints', () => {
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
|
||||
// The RESPONSE describes what the roll landed on, not the box that was bought: the
|
||||
// client draws the purchase from this entry, and the box's own fields are all empty.
|
||||
const bought = (await res.json()) as {
|
||||
BalanceUpdates: Array<{
|
||||
Data: Array<{
|
||||
AvatarItemDesc: string
|
||||
EquipmentModificationGuid: string
|
||||
GiftRarity: number
|
||||
}>
|
||||
}>
|
||||
}
|
||||
const entry = bought.BalanceUpdates[0]?.Data[0]
|
||||
expect(entry?.GiftRarity).toBe(30)
|
||||
expect(`${entry?.AvatarItemDesc ?? ''}${entry?.EquipmentModificationGuid ?? ''}`).not.toBe('')
|
||||
|
||||
const boxes = await giftBoxes('76')
|
||||
expect(boxes).toHaveLength(1)
|
||||
// The box shows what was rolled — a real 4-star item, not the empty box drop.
|
||||
expect(boxes[0]?.GiftRarity).toBe(30)
|
||||
expect(entry?.AvatarItemDesc).toBe(boxes[0]?.AvatarItemDesc)
|
||||
const key = (box?: { AvatarItemDesc: string; EquipmentModificationGuid: string }) =>
|
||||
`${box?.AvatarItemDesc ?? ''}|${box?.EquipmentModificationGuid ?? ''}`
|
||||
expect(key(boxes[0])).not.toBe('|')
|
||||
@@ -1647,6 +1663,38 @@ describe('econ endpoints', () => {
|
||||
expect(key(after[0])).not.toBe(key(after[1]))
|
||||
})
|
||||
|
||||
test('buying sf3’s Uncommon Random box answers with the rolled item', async () => {
|
||||
// The purchase that came back as an empty box: an sf3 query drop, rolled out of the very
|
||||
// catalog it sells in.
|
||||
const res = await exports.default.fetch(`${ORIGIN}/api/storefronts/v2/buyItem`, {
|
||||
method: 'POST',
|
||||
headers: { ...(await bearer('77')), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
StorefrontType: 3,
|
||||
PurchasableItemId: 2455,
|
||||
CurrencyType: CurrencyType.RecCenterTokens,
|
||||
RequestedPrice: 200,
|
||||
CouponConsumablePlayerMappingId: null,
|
||||
Gift: null,
|
||||
}),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
const body = (await res.json()) as {
|
||||
BalanceUpdates: Array<{
|
||||
Data: Array<{ Id: number; AvatarItemDesc: string; GiftRarity: number }>
|
||||
}>
|
||||
}
|
||||
const entry = body.BalanceUpdates[0]?.Data[0]
|
||||
// Uncommon: rarity 10, and a real item rather than the box's empty fields.
|
||||
expect(entry?.GiftRarity).toBe(10)
|
||||
expect(entry?.AvatarItemDesc).not.toBe('')
|
||||
|
||||
const boxes = await giftBoxes('77')
|
||||
expect(boxes).toHaveLength(1)
|
||||
expect(boxes[0]?.Id).toBe(entry?.Id)
|
||||
expect(boxes[0]?.AvatarItemDesc).toBe(entry?.AvatarItemDesc)
|
||||
})
|
||||
|
||||
test('POST /api/gamerewards/v1/request claims once an hour per reward type', async () => {
|
||||
const headers = {
|
||||
...(await bearer('80')),
|
||||
|
||||
Reference in New Issue
Block a user