@indiegems/gem-web-sdk 23.2.0 → 24.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/dist/conformance.d.ts +5 -2
- package/dist/function.d.ts +22 -1
- package/dist/game.d.ts +1590 -1466
- package/dist/gem-web-sdk.bridge.js +2 -2
- package/dist/gem-web-sdk.conformance.js +3 -3
- package/dist/gem-web-sdk.esm.js +3 -3
- package/dist/gem-web-sdk.function.js +3 -3
- package/dist/gem-web-sdk.game.js +3 -3
- package/dist/gem-web-sdk.min.js +3 -3
- package/dist/gem-web-sdk.sdk.js +3 -3
- package/dist/gem-web-sdk.server.js +2 -2
- package/dist/index.d.ts +8 -1
- package/dist/sdk.d.ts +1600 -1464
- package/dist/server.d.ts +44 -1
- package/docs/analytics.md +40 -0
- package/docs/api/api-reference-function.md +196 -45
- package/docs/api/api-reference-game.md +778 -33
- package/docs/api/api-reference-server.md +477 -3
- package/docs/api/reference.json +1833 -102
- package/docs/arcade-and-store.md +166 -61
- package/docs/arcade-economy.md +184 -184
- package/docs/changelog.md +48 -0
- package/docs/deprecation-policy.md +2 -0
- package/docs/getting-started.md +1 -1
- package/docs/inventory-and-catalogs.md +247 -76
- package/docs/manifest.json +24 -24
- package/docs/platform-rules.md +36 -27
- package/docs/player-sign-in.md +60 -3
- package/docs/rewarded-ads.md +32 -1
- package/docs/server-bundles.md +9 -7
- package/docs/what-a-game-must-do.md +7 -4
- package/package.json +1 -1
- package/schema/bridge.v1.json +97 -7
package/docs/arcade-and-store.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Arcade features and the Arcade Store
|
|
2
2
|
|
|
3
3
|
Your game can reach a few things the Arcade owns: its own app and yours in the app stores, another
|
|
4
|
-
game, games like yours, and the **Arcade Store**, which sells
|
|
5
|
-
bundles for real money.
|
|
4
|
+
game, games like yours, and the **Arcade Store**, which sells Coin packs, passes such as VIP,
|
|
5
|
+
Boosts and bundles for real money. **Only the Arcade sells Coins**: a game never sells currency
|
|
6
|
+
for money.
|
|
6
7
|
|
|
7
8
|
```js
|
|
8
9
|
// From the press on your own "More games like this" card.
|
|
@@ -12,17 +13,17 @@ async function onCardPressed(card) {
|
|
|
12
13
|
if (outcome !== 'ok') showCardAsUnavailable(card);
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
// From the press on your own "Get
|
|
16
|
+
// From the press on your own "Get Coins" button.
|
|
16
17
|
async function onBuyPressed(offer) {
|
|
17
18
|
const { outcome, reason } = await gem.store.purchase(offer.skuId);
|
|
18
19
|
// Read through gem.me; nothing here carries an amount. `error` is "outcome unknown" (§9).
|
|
19
20
|
if (outcome === 'ok' || reason === 'error') await refreshBalances();
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
// From the press on your own "Get VIP" button: the Arcade draws the
|
|
23
|
+
// From the press on your own "Get VIP" button: the Arcade draws the pass, found by its slug.
|
|
23
24
|
async function onVipPressed() {
|
|
24
|
-
const {
|
|
25
|
-
if (
|
|
25
|
+
const { held } = await gem.store.showPass('vip');
|
|
26
|
+
if (held) drawVipBadge(); // a fresh read already shows VIP held (§10)
|
|
26
27
|
}
|
|
27
28
|
```
|
|
28
29
|
|
|
@@ -51,10 +52,10 @@ itself. You get an outcome.
|
|
|
51
52
|
| Money | none | **real money**, through the Arcade Store's checkout |
|
|
52
53
|
| Who draws it | the Arcade (a new tab, a QR code, the next game) | your card or the Arcade's sheet, then the checkout |
|
|
53
54
|
|
|
54
|
-
`gem.store` is not `gem.purchases`. **`gem.purchases` spends
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
`gem.store` is not `gem.purchases`. **`gem.purchases` spends Coins on your game's own items**
|
|
56
|
+
([`inventory-and-catalogs.md`](inventory-and-catalogs.md)). **`gem.store` sells the Arcade Store's
|
|
57
|
+
own SKUs for the player's money.** A game that wants a "Get Coins" button uses `gem.store`; a game
|
|
58
|
+
selling a sword for 250 Coins uses `gem.purchases`.
|
|
58
59
|
|
|
59
60
|
## 2. Nothing rejects
|
|
60
61
|
|
|
@@ -78,7 +79,7 @@ An Arcade that does not offer a feature answers every call for it at once with `
|
|
|
78
79
|
```js
|
|
79
80
|
if (gem.arcade.available.openGame) showMoreGamesSection();
|
|
80
81
|
if (gem.arcade.available.appLinks) showGetTheAppButton();
|
|
81
|
-
if (gem.store.available)
|
|
82
|
+
if (gem.store.available) showGetCoinsButton();
|
|
82
83
|
if (gem.store.showOfferAvailable) showGetVipButton();
|
|
83
84
|
```
|
|
84
85
|
|
|
@@ -222,15 +223,26 @@ policy refused. Allow that host rather than a wildcard.
|
|
|
222
223
|
## 8. The Arcade Store's offers
|
|
223
224
|
|
|
224
225
|
```js
|
|
225
|
-
const { outcome,
|
|
226
|
-
|
|
227
|
-
for (const offer of offers) drawOffer(offer);
|
|
226
|
+
const { outcome, offers } = await gem.store.offers();
|
|
227
|
+
for (const offer of offers) if (offer.kind === 'coin_pack') drawCoinPack(offer);
|
|
228
228
|
```
|
|
229
229
|
|
|
230
|
-
`offers()` lists what the Arcade Store sells this player, here, right now:
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
230
|
+
`offers()` lists what the Arcade Store sells this player, here, right now: Coin packs, passes,
|
|
231
|
+
Boosts and bundles. Each offer has:
|
|
232
|
+
|
|
233
|
+
- a `skuId`, which `purchase` and `showOffer` take;
|
|
234
|
+
- a `kind`: `coin_pack`, `pass`, `boost` or `bundle`. `gem_pack` is deprecated: Gems are being
|
|
235
|
+
retired and a Gem pack is no longer sold. Keep a `default` arm;
|
|
236
|
+
- the store's own `name` and `description`, an optional `imageUrl`, and a `priceDisplay`;
|
|
237
|
+
- for a Coin pack, a `grant` naming the currency (`ARCADE_TICKETS`) and the amount;
|
|
238
|
+
- for a pass, its slug in `pass` (VIP is `vip`), which `showPass` takes;
|
|
239
|
+
- `earnMultipliers`: for an offer that makes the player earn a currency faster while they hold it,
|
|
240
|
+
such as VIP's +50% Coins, one `{ currencySlug, multiplierBp }` per currency. Empty otherwise.
|
|
241
|
+
**Never multiply a reward by it yourself**; the platform applies it when it pays.
|
|
242
|
+
|
|
243
|
+
**A guest is answered too.** Buying takes a guest through sign-up in the Arcade's own screens, then
|
|
244
|
+
back to what they chose, so a guest sees your "Get Coins" and "Get VIP" buttons. An older Arcade
|
|
245
|
+
answers a guest `unavailable` / `sign_in_required`: hide the buttons there.
|
|
234
246
|
|
|
235
247
|
- **Show `priceDisplay` exactly as given.** It is the payment rail's own price in the player's
|
|
236
248
|
currency, already formatted. Never parse it, compare two of them, or compute a discount from it.
|
|
@@ -294,16 +306,17 @@ arrives, so your own celebration is visible.
|
|
|
294
306
|
|
|
295
307
|
```js
|
|
296
308
|
// Decide on the screen you draw the button on.
|
|
297
|
-
|
|
298
|
-
const vip = offers.find((offer) => offer.skuId === VIP_SKU);
|
|
299
|
-
if (gem.store.showOfferAvailable && vip) showGetVipButton();
|
|
309
|
+
if (gem.store.showOfferAvailable && (await gem.store.passOnSale('vip'))) showGetVipButton();
|
|
300
310
|
else hideGetVipButton();
|
|
301
311
|
|
|
302
312
|
// From the press on that button.
|
|
303
|
-
const { outcome, reason } = await gem.store.
|
|
304
|
-
if (outcome === 'ok' || reason === 'error') await refreshBalances();
|
|
313
|
+
const { outcome, reason, held } = await gem.store.showPass('vip');
|
|
305
314
|
```
|
|
306
315
|
|
|
316
|
+
For a pass, use **`showPass(slug)`**: it finds the pass's offer by its slug and shows it, so your
|
|
317
|
+
game never names a SKU ([below](#a-pass-by-its-slug-showpass)). `showOffer(skuId)` is the same
|
|
318
|
+
sheet for any other SKU, such as a bundle.
|
|
319
|
+
|
|
307
320
|
`showOffer(skuId)` asks the Arcade to draw one SKU from `offers()` in its own sheet, over your paused
|
|
308
321
|
and hidden game. The Arcade draws the art, what the player gets, a sale if one is running, and, for a
|
|
309
322
|
pass the player already holds, how long it has left. The sheet's Buy opens the same checkout
|
|
@@ -311,13 +324,13 @@ pass the player already holds, how long it has left. The sheet's Buy opens the s
|
|
|
311
324
|
|
|
312
325
|
Pick by who can say what the offer is:
|
|
313
326
|
|
|
314
|
-
| | **You draw it: `offers()` + `purchase()`**
|
|
315
|
-
| --------------- |
|
|
316
|
-
| Use it for |
|
|
317
|
-
| Your button | says what it buys and shows `priceDisplay`
|
|
318
|
-
| The press opens | the checkout, straight away
|
|
319
|
-
| Perks and sales | not available to you
|
|
320
|
-
| A holder sees | the same Buy button
|
|
327
|
+
| | **You draw it: `offers()` + `purchase()`** | **The Arcade draws it: `showPass()` or `showOffer()`** |
|
|
328
|
+
| --------------- | ---------------------------------------------------- | --------------------------------------------------------------- |
|
|
329
|
+
| Use it for | Coin packs: a name, an amount and a price say it all | VIP and other passes (`showPass`), and bundles |
|
|
330
|
+
| Your button | says what it buys and shows `priceDisplay` | says what it opens, such as "Get VIP" |
|
|
331
|
+
| The press opens | the checkout, straight away | the Arcade's sheet, then the checkout if the player presses Buy |
|
|
332
|
+
| Perks and sales | not available to you | always current, in the player's language |
|
|
333
|
+
| A holder sees | the same Buy button | what they already have, and how to add time |
|
|
321
334
|
|
|
322
335
|
- **Never write an offer's perks into your game.** They change without a new build of your game, and
|
|
323
336
|
yours would go stale. Your button only needs the offer's name.
|
|
@@ -327,7 +340,9 @@ Pick by who can say what the offer is:
|
|
|
327
340
|
- **The sheet does not tell you what the player holds.** It tells the player, so a button that
|
|
328
341
|
stays up for a holder is fine: they see what they already have, and choose from there. To change
|
|
329
342
|
your own screen for a holder, read `gem.me.passes` ([§11](#11-what-the-player-holds-and-a-vip-door)).
|
|
330
|
-
- **Hide the button when the
|
|
343
|
+
- **Hide the button when the pass is not on sale.** `passOnSale(slug)` says so, and is `false`
|
|
344
|
+
where the store is off, for a player the platform won't sell it to, once the player has declined
|
|
345
|
+
its sheet twice today, and on an older Arcade. A pass that is not on sale answers
|
|
331
346
|
`unavailable` / `not_found`, and the player would have pressed a button that does nothing.
|
|
332
347
|
- **Read `showOfferAvailable`, not `available`.** An older Arcade can sell through `purchase` and
|
|
333
348
|
still not draw this sheet. It answers `unavailable` / `unsupported` and sends nothing. Hide the
|
|
@@ -336,16 +351,72 @@ Pick by who can say what the offer is:
|
|
|
336
351
|
Every rule of `purchase` holds ([§9](#9-buying-from-the-arcade-store)), and the answers are the
|
|
337
352
|
same, with one addition. `declined` / `cancelled` also covers the player leaving the sheet without
|
|
338
353
|
pressing Buy, and you are not told which of the two they closed. Nothing backs off: the player's
|
|
339
|
-
next press on either button opens it again.
|
|
354
|
+
next press on either button opens it again. A pass is the exception: after the player declines its
|
|
355
|
+
sheet **twice in a day**, the Arcade stops offering it to them until tomorrow, and `passOnSale`
|
|
356
|
+
answers `false`. Your game cannot reset that count.
|
|
357
|
+
|
|
358
|
+
### A pass, by its slug: `showPass`
|
|
359
|
+
|
|
360
|
+
`showPass(slug)` is `offers()` plus `showOffer()`, with one promise of its own: **`ok` means a fresh
|
|
361
|
+
read of `gem.me.passes` shows the pass held.** A recipe or claim your game runs next gets the pass's
|
|
362
|
+
bonus, with no reload. That makes it safe to call at the moment the player is about to claim
|
|
363
|
+
something:
|
|
364
|
+
|
|
365
|
+
| Answer | `held` | What happened | Then |
|
|
366
|
+
| ----------------------------- | ------- | ------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
367
|
+
| `ok` | `true` | the player bought it, and it is held now | run the recipe or claim: the bonus applies |
|
|
368
|
+
| `ok` / `pending` | `false` | the player paid, and the pass has not arrived yet (Ask to Buy, say) | run it now, without the bonus; nothing is held back for later |
|
|
369
|
+
| `declined` / `cancelled` | `false` | they closed the sheet | run it as it is, and don't ask again at this moment |
|
|
370
|
+
| `unavailable` / `not_found` | `false` | not on sale to this player here, or an older Arcade | run it as it is, and hide the button |
|
|
371
|
+
| `unavailable` / `unsupported` | `false` | this Arcade draws no sheet | run it as it is, and hide the button |
|
|
372
|
+
| `unavailable` / `error` | `false` | **the outcome is unknown, and the player may have paid** | run it; `gem.me.passes.on('changed')` hears the pass if it arrives |
|
|
373
|
+
|
|
374
|
+
- **Pass it the slug the platform gave you,** never a SKU. VIP is `vip`.
|
|
375
|
+
- **A guest** goes through sign-up first, then back to the sheet. A guest who signs up and buys
|
|
376
|
+
answers `ok` like anyone else.
|
|
377
|
+
- **After a paid sheet it waits a little.** It reads `gem.me.passes` for about ten seconds before it
|
|
378
|
+
settles for `pending`, because the payment provider's notice can trail the checkout.
|
|
379
|
+
- **A player who already holds the pass** sees how long they have left and can add time.
|
|
380
|
+
|
|
381
|
+
### At a reward: the VIP extra roll
|
|
382
|
+
|
|
383
|
+
The moment a pass sells best is the moment it would pay: a chest the player is about to open, where
|
|
384
|
+
VIP adds a roll. **Your game never names the pass and never works out the extra.** It asks
|
|
385
|
+
`gem.gameConfig.preview` what the claim gives, and the answer's `upsells` says what each pass on
|
|
386
|
+
sale here would add:
|
|
387
|
+
|
|
388
|
+
```js
|
|
389
|
+
// The recipe carries `bonuses: [{ id: 'vip_roll', requires: [{ kind: 'holds_pass', pass: 'vip' }],
|
|
390
|
+
// grants: [{ type: 'roll', loot_table_id: '<chest_common>', count: 1 }] }]` in your game config.
|
|
391
|
+
openButton.onclick = async () => {
|
|
392
|
+
const preview = await gem.gameConfig.preview({ recipe: 'open_chest' });
|
|
393
|
+
const upsell = preview.upsells[0];
|
|
394
|
+
if (upsell) {
|
|
395
|
+
// "VIP: +1 roll, 45 Coins" — upsell.name, and the extras locked, never what a roll would land on.
|
|
396
|
+
const choice = await vipCard.ask(upsell); // Get VIP and Continue, with equal weight
|
|
397
|
+
if (choice === 'get') await gem.store.showPass(upsell.pass);
|
|
398
|
+
openButton.focus(); // focus goes back where it was
|
|
399
|
+
}
|
|
400
|
+
const order = await gem.me.mutations.executeRecipe(CHESTS, 'open_chest');
|
|
401
|
+
// The base always runs; the VIP roll applies if the pass is held now.
|
|
402
|
+
};
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
`upsells` is already empty wherever no upsell may be shown: for a minor, a store that is off, a pass
|
|
406
|
+
the player declined twice today or already holds, and a claim no pass would change. So an empty
|
|
407
|
+
list means draw no card. **Upsell only before the grant**, once per reward moment: the platform
|
|
408
|
+
holds nothing back, so a pass bought afterwards cannot reach a chest already opened.
|
|
409
|
+
[inventory-and-catalogs.md](inventory-and-catalogs.md#offering-vip-before-a-reward-the-upsell) has
|
|
410
|
+
the rest of the rules, every one a must, and `bonuses` in full.
|
|
340
411
|
|
|
341
412
|
## 11. What the player holds, and a VIP door
|
|
342
413
|
|
|
343
414
|
```js
|
|
344
415
|
const passes = await gem.me.passes.list();
|
|
345
|
-
// [{ pass: 'vip', held: true, endsAt: '2026-11-29T12:34:56Z' }]
|
|
416
|
+
// [{ pass: 'vip', held: true, daysLeft: 7, endsAt: '2026-11-29T12:34:56Z' }]
|
|
346
417
|
|
|
347
418
|
const vip = await gem.me.passes.get('vip'); // one pass by its slug
|
|
348
|
-
if (vip.held) drawVipBadge();
|
|
419
|
+
if (vip.held) drawVipBadge(vip.daysLeft === 7 ? '7+ days left' : `${vip.daysLeft} days left`);
|
|
349
420
|
```
|
|
350
421
|
|
|
351
422
|
`gem.me.passes` says which store passes the player holds. **VIP is the pass `vip`**, its offering
|
|
@@ -354,12 +425,15 @@ slug. Key on the slug, which is stable and not localized, and never on a display
|
|
|
354
425
|
- **It lists every pass games may see, held or not.** An admin marks a pass as visible to games, and
|
|
355
426
|
the list has one entry for each, so its shape never says what the player bought. A pass that is not
|
|
356
427
|
marked is left out, and `get` reads a pass that is not listed as not held.
|
|
357
|
-
- **A pass is a slug, whether it is held, and
|
|
358
|
-
about how the player got it, and nothing about a pass that has ended.
|
|
359
|
-
- **`
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
-
|
|
428
|
+
- **A pass is a slug, whether it is held, and how many days it has left.** No price, no name, no
|
|
429
|
+
perks, nothing about how the player got it, and nothing about a pass that has ended.
|
|
430
|
+
- **`daysLeft` is whole days, rounded up and capped at 7**: `7` means "7 or more", and is also what
|
|
431
|
+
a pass held with no end reads (support can grant one). `0` when it is not held. Show `7` as
|
|
432
|
+
"7+ days left", never as a date.
|
|
433
|
+
- **`endsAt` is deprecated.** It is the exact end, to the second, and two games could match that
|
|
434
|
+
value to link one player's accounts, so the platform will stop sending it. Use `daysLeft`.
|
|
435
|
+
- **A guest holds nothing.** When the Arcade says the player is a guest, every pass reads
|
|
436
|
+
`held: false` without a call to the platform, and that is no error.
|
|
363
437
|
- **A read that fails rejects with a `GemApiError`, as every `gem.me` read does.** That is unknown,
|
|
364
438
|
not "not held", so keep what you drew. It is the one call on this page that rejects.
|
|
365
439
|
- **It is a read over your play token,** not an ask of the Arcade. It needs no press, and works
|
|
@@ -401,7 +475,7 @@ door.onPress = async () => {
|
|
|
401
475
|
};
|
|
402
476
|
|
|
403
477
|
// 3. BUYING. From the press on your own "How to get VIP" button.
|
|
404
|
-
howToGetVipButton.onclick = () => gem.store.
|
|
478
|
+
howToGetVipButton.onclick = () => gem.store.showPass('vip');
|
|
405
479
|
```
|
|
406
480
|
|
|
407
481
|
The function, in Rust, because only the platform's answer counts:
|
|
@@ -439,25 +513,32 @@ Ok(answer.to_string().into_bytes())
|
|
|
439
513
|
"Me" on that route is the player who invoked the function. A function your server invoked has no
|
|
440
514
|
player, and the route refuses it with `403`. The body is `{ pass, held, ends_at }` for each pass.
|
|
441
515
|
|
|
516
|
+
**Gate on the pass, never on the boost.** VIP also shows in `gem.player.boost` as +50% Coins, but a
|
|
517
|
+
boost is display-only and a player can hold one without VIP. What a pass unlocks is decided by
|
|
518
|
+
`held` on this read, or by a `requires: holds_pass` on your config, checked by the platform
|
|
519
|
+
([platform rules §8](platform-rules.md#8-a-players-boost-is-for-display-never-a-gate)).
|
|
520
|
+
|
|
442
521
|
- **If the door and the function disagree, the function is right.** The player sees "How to get VIP"
|
|
443
522
|
or the lounge, and either way nobody got in by editing the page. Redraw the door when the function
|
|
444
523
|
says no.
|
|
445
|
-
- **A guest has no VIP and
|
|
446
|
-
|
|
524
|
+
- **A guest has no VIP, and can buy it from here.** Their door is shut, and `showPass('vip')` takes
|
|
525
|
+
them through sign-up, then back to the sheet. On an older Arcade, which answers a guest
|
|
526
|
+
`unavailable` / `sign_in_required`, offer `gem.account.requestSignIn` instead, when
|
|
527
|
+
`gem.account.signInAvailable` says the Arcade can show them a sign-in.
|
|
447
528
|
|
|
448
529
|
### When the door moves by itself
|
|
449
530
|
|
|
450
531
|
While something listens to `on('changed')`, the SDK reads again at each of these, and calls you only
|
|
451
532
|
if the answer differs:
|
|
452
533
|
|
|
453
|
-
| Moment
|
|
454
|
-
|
|
|
455
|
-
| `store.purchase` or `
|
|
456
|
-
| Either answers `unavailable` / `error`
|
|
457
|
-
| `gem.store.onSettled` fires
|
|
458
|
-
| Your game resumes
|
|
459
|
-
| The player signs in
|
|
460
|
-
| A held pass reaches `endsAt`
|
|
534
|
+
| Moment | Why you cannot rely on something else |
|
|
535
|
+
| --------------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
536
|
+
| `store.purchase`, `showOffer` or `showPass` is `ok` | `onSettled` does not fire after a plain `ok` |
|
|
537
|
+
| Either answers `unavailable` / `error` | The outcome is unknown, and the player may have paid |
|
|
538
|
+
| `gem.store.onSettled` fires | `ok` / `pending` still reads as not held |
|
|
539
|
+
| Your game resumes | A purchase in the Arcade's own store, from the Gem menu, sends no event at all |
|
|
540
|
+
| The player signs in | A guest who signs in may hold VIP |
|
|
541
|
+
| A held pass reaches `endsAt` | VIP does not renew, so the door has to lock by itself |
|
|
461
542
|
|
|
462
543
|
A read that fails changes nothing, and is tried again later. A read that was already on its way when
|
|
463
544
|
a purchase ended is never reused, so a stale answer cannot shut the door on the buyer. If you read
|
|
@@ -467,14 +548,36 @@ without a listener, call `list()` yourself at those moments; it is always fresh.
|
|
|
467
548
|
|
|
468
549
|
### On a dedicated server
|
|
469
550
|
|
|
470
|
-
|
|
471
|
-
`
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
551
|
+
`gem.match.playerPasses(playerId)` reads which passes a player in your match holds, in the shape
|
|
552
|
+
`gem.me.passes.list()` gives: `{ pass, held, daysLeft }` for each pass marked visible to games. On a
|
|
553
|
+
server it is the decision itself, because the browser is not in the call:
|
|
554
|
+
|
|
555
|
+
```js
|
|
556
|
+
export default defineServer((gem) => ({
|
|
557
|
+
async join(playerId) {
|
|
558
|
+
let vip = false;
|
|
559
|
+
try {
|
|
560
|
+
vip = (await gem.match.playerPasses(playerId)).some((p) => p.pass === 'vip' && p.held);
|
|
561
|
+
} catch {
|
|
562
|
+
// unknown: keep the lounge shut, and let the player ask again
|
|
563
|
+
}
|
|
564
|
+
if (vip) admitToLounge(playerId);
|
|
565
|
+
},
|
|
566
|
+
}));
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
- **The platform half of the runtime makes the read**, with the match's own proof, which never
|
|
570
|
+
enters your bundle. So does `gem.match.end()`.
|
|
571
|
+
- **It throws on a runtime image that predates it, and on a listen host** (a player's browser
|
|
572
|
+
hosting a P2P match), which has no such proof. Decide in a function there.
|
|
573
|
+
- **A player outside your match** is refused `not_found` (`player_not_in_match`), and nothing about
|
|
574
|
+
them is read.
|
|
575
|
+
- **It may be refused `forbidden` (`client_permission_denied`)** until the platform grants game
|
|
576
|
+
servers the pass read. That is unknown, not "not held": keep the door shut and say so, rather
|
|
577
|
+
than telling a VIP they have no VIP.
|
|
476
578
|
|
|
477
|
-
|
|
579
|
+
`gem.match.playerBoost(playerId)` reads the player's Boost for **display** — "2x Coins" on a results
|
|
580
|
+
screen — never for a decision.
|
|
478
581
|
|
|
479
582
|
## 12. Outcomes and reasons
|
|
480
583
|
|
|
@@ -484,9 +587,10 @@ Until then, decide in a function, or leave a VIP door out of a dedicated-server
|
|
|
484
587
|
| `openApp` | opened, or `handed_off` | `busy` | `already_in_app`, `unsupported`, `error` |
|
|
485
588
|
| `openGame` | the next game is starting | `busy`, `same_game` | `not_found`, `unsupported`, `error` |
|
|
486
589
|
| `similarGames` | cards, possibly none | | `unsupported`, `error` |
|
|
487
|
-
| `store.offers` | offers, possibly none | | `
|
|
590
|
+
| `store.offers` | offers, possibly none | | `store_off`, `sign_in_required` (an older Arcade's guest), `unsupported`, `error` |
|
|
488
591
|
| `store.purchase` | paid, or `pending` | `cancelled`, `busy` | `sign_in_required`, `store_off`, `not_found`, the provider's reason, `unsupported`, `error` (outcome unknown) |
|
|
489
592
|
| `store.showOffer` | paid, or `pending` | `cancelled`, `busy` | `sign_in_required`, `store_off`, `not_found`, the provider's reason, `unsupported`, `error` (outcome unknown) |
|
|
593
|
+
| `store.showPass` | held, or `pending` | `cancelled`, `busy` | `not_found`, `store_off`, the provider's reason, `invalid_argument`, `unsupported`, `error` (outcome unknown) |
|
|
490
594
|
|
|
491
595
|
Every reason is a short code of lower-case letters and underscores. The Arcade turns a payment
|
|
492
596
|
provider's own code into that shape before it reaches you.
|
|
@@ -495,10 +599,11 @@ Four reasons come from the SDK itself rather than the Arcade:
|
|
|
495
599
|
|
|
496
600
|
- `unsupported`: this Arcade does not offer the feature. Nothing was sent.
|
|
497
601
|
- `invalid_argument`: the SDK refused the argument (an unknown `target`, a `ref` that is not an id
|
|
498
|
-
or a web address, a `limit` outside 1–24, an empty `skuId`). Nothing was sent.
|
|
602
|
+
or a web address, a `limit` outside 1–24, an empty `skuId` or pass slug). Nothing was sent.
|
|
499
603
|
- `busy`: another acting ask from your game is still open. Nothing was sent.
|
|
500
604
|
- `error`: no reliable answer arrived. The ask could not be sent, a read or an open waited out its
|
|
501
605
|
timeout (30 seconds for a read, two minutes for `openApp` and `openGame`), the bridge closed
|
|
502
606
|
before the answer came, or the Arcade said it had no reliable answer. For a read, try again
|
|
503
|
-
later. For `store.purchase` and `store.
|
|
607
|
+
later. For `store.purchase`, `store.showOffer` and `store.showPass` it means **the outcome is
|
|
608
|
+
unknown**, never
|
|
504
609
|
"nothing was bought" ([§9](#9-buying-from-the-arcade-store)).
|