mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-09 15:11:29 -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
|
- **`QueryRedirectRarity` wins over `Rarity`** when present — sf2 carries both and they
|
||||||
agree; sf3's boxes carry only `Rarity`.
|
agree; sf3's boxes carry only `Rarity`.
|
||||||
- **An empty pool grants nothing** (logged `query gift-drop rolled nothing`) — an owner of
|
- **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
|
every 4-star item still gets the box, just nothing in it.
|
||||||
echoes the drop the player _bought_, i.e. the box; the rolled item shows up in the box
|
- **`buyItem` answers with the ROLLED item, not the box.** The client draws the purchase
|
||||||
itself, via `GET /api/avatar/v2/gifts`.
|
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
|
## Consume envelopes
|
||||||
|
|
||||||
|
|||||||
+23
-14
@@ -1777,8 +1777,16 @@ const app = new Hono<App>({ strict: false })
|
|||||||
if (!paid) return c.json({ error: 'Insufficient balance' }, 400)
|
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
|
// 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.
|
// `IsQuery` drop, e.g. sf2's "4-Star Unique Box") rolls its prize in here, and
|
||||||
const { id: giftId } = await grantGiftDrop(c, receiverId, item.GiftDrop, message)
|
// `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
|
// 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
|
// 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
|
// 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
|
// 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
|
// `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
|
// entry is the gift-drop the client RECEIVED — the rolled item for a query box, the
|
||||||
// consumable count (the count is a getUnlocked concept; each box is one instance).
|
// 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({
|
return c.json({
|
||||||
BalanceUpdates: [
|
BalanceUpdates: [
|
||||||
{
|
{
|
||||||
@@ -1799,22 +1808,22 @@ const app = new Hono<App>({ strict: false })
|
|||||||
{
|
{
|
||||||
Id: giftId,
|
Id: giftId,
|
||||||
FromPlayerId: fromPlayerId,
|
FromPlayerId: fromPlayerId,
|
||||||
ConsumableItemDesc: item.GiftDrop.ConsumableItemDesc,
|
ConsumableItemDesc: granted.ConsumableItemDesc,
|
||||||
AvatarItemDesc: item.GiftDrop.AvatarItemDesc,
|
AvatarItemDesc: granted.AvatarItemDesc,
|
||||||
AvatarItemType: item.GiftDrop.AvatarItemType ?? 0,
|
AvatarItemType: granted.AvatarItemType ?? 0,
|
||||||
EquipmentPrefabName: item.GiftDrop.EquipmentPrefabName,
|
EquipmentPrefabName: granted.EquipmentPrefabName,
|
||||||
EquipmentModificationGuid: item.GiftDrop.EquipmentModificationGuid,
|
EquipmentModificationGuid: granted.EquipmentModificationGuid,
|
||||||
CurrencyType: item.GiftDrop.CurrencyType,
|
CurrencyType: granted.CurrencyType,
|
||||||
Currency: item.GiftDrop.Currency,
|
Currency: granted.Currency,
|
||||||
Xp: 0,
|
Xp: granted.Xp ?? 0,
|
||||||
Level: 0,
|
Level: 0,
|
||||||
Platform: -1,
|
Platform: -1,
|
||||||
PlatformsToSpawnOn: -1,
|
PlatformsToSpawnOn: -1,
|
||||||
BalanceType: ALL_PLATFORMS,
|
BalanceType: ALL_PLATFORMS,
|
||||||
GiftContext: Number.isInteger(gift?.GiftContext)
|
GiftContext: Number.isInteger(gift?.GiftContext)
|
||||||
? (gift?.GiftContext as number)
|
? (gift?.GiftContext as number)
|
||||||
: item.GiftDrop.Context,
|
: granted.Context,
|
||||||
GiftRarity: item.GiftDrop.Rarity,
|
GiftRarity: granted.Rarity,
|
||||||
Message: message,
|
Message: message,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1611,10 +1611,26 @@ describe('econ endpoints', () => {
|
|||||||
})
|
})
|
||||||
expect(res.status).toBe(200)
|
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')
|
const boxes = await giftBoxes('76')
|
||||||
expect(boxes).toHaveLength(1)
|
expect(boxes).toHaveLength(1)
|
||||||
// The box shows what was rolled — a real 4-star item, not the empty box drop.
|
// The box shows what was rolled — a real 4-star item, not the empty box drop.
|
||||||
expect(boxes[0]?.GiftRarity).toBe(30)
|
expect(boxes[0]?.GiftRarity).toBe(30)
|
||||||
|
expect(entry?.AvatarItemDesc).toBe(boxes[0]?.AvatarItemDesc)
|
||||||
const key = (box?: { AvatarItemDesc: string; EquipmentModificationGuid: string }) =>
|
const key = (box?: { AvatarItemDesc: string; EquipmentModificationGuid: string }) =>
|
||||||
`${box?.AvatarItemDesc ?? ''}|${box?.EquipmentModificationGuid ?? ''}`
|
`${box?.AvatarItemDesc ?? ''}|${box?.EquipmentModificationGuid ?? ''}`
|
||||||
expect(key(boxes[0])).not.toBe('|')
|
expect(key(boxes[0])).not.toBe('|')
|
||||||
@@ -1647,6 +1663,38 @@ describe('econ endpoints', () => {
|
|||||||
expect(key(after[0])).not.toBe(key(after[1]))
|
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 () => {
|
test('POST /api/gamerewards/v1/request claims once an hour per reward type', async () => {
|
||||||
const headers = {
|
const headers = {
|
||||||
...(await bearer('80')),
|
...(await bearer('80')),
|
||||||
|
|||||||
Reference in New Issue
Block a user