@indiegems/gem-web-sdk 21.1.1 → 23.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 +61 -0
- package/dist/conformance.d.ts +4 -1
- package/dist/function.d.ts +10 -1
- package/dist/game.d.ts +46 -2
- 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-api.js +1 -1
- package/dist/gem-web-sdk.server.js +2 -2
- package/dist/index.d.ts +66 -6
- package/dist/sdk.d.ts +47 -2
- package/dist/server-api.d.ts +1 -0
- package/dist/server.d.ts +3 -0
- package/docs/api/api-reference-function.md +129 -9
- package/docs/api/api-reference-game.md +292 -13
- package/docs/api/api-reference-server.md +34 -1
- package/docs/api/reference.json +276 -10
- package/docs/arcade-economy.md +3 -1
- package/docs/changelog.md +61 -0
- package/docs/daily-challenges.md +3 -1
- package/docs/friends.md +120 -15
- package/docs/getting-started.md +7 -1
- package/docs/inventory-and-catalogs.md +86 -13
- package/docs/manifest.json +22 -22
- package/docs/rooms-and-matches.md +6 -3
- package/docs/server-bundles.md +2 -2
- package/docs/serverless-functions.md +38 -0
- package/docs/trading-and-gifting.md +12 -9
- package/docs/what-a-game-must-do.md +22 -1
- package/package.json +1 -1
- package/schema/bridge.v1.json +293 -1
- package/schema/capabilities.public.json +20 -0
package/docs/friends.md
CHANGED
|
@@ -11,8 +11,9 @@ for (const friend of page.friends) {
|
|
|
11
11
|
}
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
That read,
|
|
15
|
-
confirms with the player and sends itself
|
|
14
|
+
That read, two asks — [sending a friend a challenge](#friend-challenges), which the Arcade
|
|
15
|
+
confirms with the player and sends itself, and [inviting friends to play](#inviting-a-friend-to-play),
|
|
16
|
+
which opens the Arcade's own invite sheet — and [reading the challenges the player
|
|
16
17
|
received](#reading-the-challenges-a-player-received) in your game are the whole surface. The
|
|
17
18
|
boundary around them is the thing to understand before you design against it.
|
|
18
19
|
|
|
@@ -36,8 +37,10 @@ Everything else about the friend graph stays on the Arcade, by design:
|
|
|
36
37
|
answered (`gem.friends.markAnswered`), which records that they played it and tells nobody.
|
|
37
38
|
- **No states.** Pending requests, blocks and relationship history do not appear. An accepted
|
|
38
39
|
friend appears; everyone else does not exist on this surface.
|
|
39
|
-
- **No presence, no invites.** Nothing here says who is online
|
|
40
|
-
is something happening
|
|
40
|
+
- **No presence, and no invites delivered to your game.** Nothing here says who is online, and an
|
|
41
|
+
invite is something happening _in_ the graph, so it is rendered where the graph lives. Your game
|
|
42
|
+
may [ask the Arcade to open its own invite sheet](#inviting-a-friend-to-play), and that is the
|
|
43
|
+
whole of it: the Arcade picks no one for you, and tells you nothing back. See the
|
|
41
44
|
deliberate-absence notes in [`rooms-and-matches.md`](rooms-and-matches.md). A friend challenge
|
|
42
45
|
is not an invite to play together: it is a card in the friend's Arcade inbox, written by the
|
|
43
46
|
platform from your template, that launches your game when they tap it — whenever they next open
|
|
@@ -46,7 +49,7 @@ Everything else about the friend graph stays on the Arcade, by design:
|
|
|
46
49
|
Two properties worth designing around rather than discovering:
|
|
47
50
|
|
|
48
51
|
- **Every accepted friend appears, whether or not they have played your game.** That is the
|
|
49
|
-
point, not an oversight — an invite-a-friend flow needs exactly the friends who have
|
|
52
|
+
point, not an oversight — an invite-a-friend flow needs exactly the friends who have _not_
|
|
50
53
|
played it. If you only want friends with progress in your game, join the list against your own
|
|
51
54
|
data (a friend's `playerId` is the same id leaderboard rows and transfers carry).
|
|
52
55
|
- **It is a player surface only.** On a dedicated server there is no `gem.friends`, and the route
|
|
@@ -132,12 +135,12 @@ the card already says who sent it, and a name in a param is one nobody checked.
|
|
|
132
135
|
`challenge()` resolves for every answer the player or the platform gives, and none of them
|
|
133
136
|
carries a reason:
|
|
134
137
|
|
|
135
|
-
| outcome
|
|
136
|
-
|
|
137
|
-
| `accepted`
|
|
138
|
-
| `cancelled`
|
|
139
|
-
| `refused`
|
|
140
|
-
| `unavailable` | the Arcade could not offer this, or no answer arrived
|
|
138
|
+
| outcome | what happened | what to show |
|
|
139
|
+
| ------------- | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
|
|
140
|
+
| `accepted` | the player pressed Send and the platform took it | "Challenge sent", or nothing — the Arcade already said so |
|
|
141
|
+
| `cancelled` | the player closed the sheet, or another prompt already held the screen | **nothing** |
|
|
142
|
+
| `refused` | the platform will not send this one — a guest or signed out, not friends with that player, or no access to this channel | **nothing** |
|
|
143
|
+
| `unavailable` | the Arcade could not offer this, or no answer arrived | **nothing**, and never retry it on your own |
|
|
141
144
|
|
|
142
145
|
⚠️ **`accepted` is not a delivery receipt.** The platform may decide not to deliver a challenge
|
|
143
146
|
— the friend muted your game, has had enough challenges today, or cannot receive them — and
|
|
@@ -171,6 +174,7 @@ existing `instanceof GemApiError` handling keeps working:
|
|
|
171
174
|
digits, `_` and `-`, a plain object of strings and integers, at most 8 keys — so most of these
|
|
172
175
|
reach you before anything is shown to the player. `detail` is the platform's sentence, for your
|
|
173
176
|
console, never for a player.
|
|
177
|
+
|
|
174
178
|
- **`rate_limited`** — the player has sent too many. `retryAfterSeconds` is the earliest a fresh
|
|
175
179
|
press could succeed, and it can be up to a day.
|
|
176
180
|
|
|
@@ -199,9 +203,10 @@ const challenge = await gem.friends.launchedChallenge();
|
|
|
199
203
|
if (challenge?.template === 'beat_code_jar') {
|
|
200
204
|
// Check every param before it does anything, as you would a launch key: the template
|
|
201
205
|
// constrained it when it was sent, but your config may have changed since.
|
|
202
|
-
const jar =
|
|
203
|
-
|
|
204
|
-
|
|
206
|
+
const jar =
|
|
207
|
+
typeof challenge.params.jar === 'string' && /^[A-Z0-9]{7}$/.test(challenge.params.jar)
|
|
208
|
+
? challenge.params.jar
|
|
209
|
+
: null;
|
|
205
210
|
if (jar) {
|
|
206
211
|
startJar(jar, {
|
|
207
212
|
from: challenge.from, // who sent it, from the platform's records
|
|
@@ -273,6 +278,106 @@ while (page.nextCursor) {
|
|
|
273
278
|
|
|
274
279
|
For the challenge this launch carried, use `launchedChallenge()` rather than searching the inbox.
|
|
275
280
|
|
|
281
|
+
## Inviting a friend to play
|
|
282
|
+
|
|
283
|
+
Your game may ask the Arcade to open **its own invite sheet**, where the player picks friends to
|
|
284
|
+
invite to come and play your game with them, now:
|
|
285
|
+
|
|
286
|
+
```js
|
|
287
|
+
inviteButton.hidden = !gem.frame.parentSupports('playInvites');
|
|
288
|
+
inviteButton.onclick = async () => {
|
|
289
|
+
const shown = await gem.friends.inviteToPlay();
|
|
290
|
+
if (!shown) showJoinCode(); // the sheet did not open: offer your own way in
|
|
291
|
+
};
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The Arcade lists the player's friends in its own chrome, pauses your game while the sheet is
|
|
295
|
+
open, and sends an invite each time the player presses Invite there. A friend who accepts is
|
|
296
|
+
opened into your game by the Arcade, and their own quick-join seats them in the player's room
|
|
297
|
+
when it has a seat — your game makes no call for it, is not told who is coming, and finds them in
|
|
298
|
+
the room as a member who shares the player's `groupId`. [Landing in a friend's
|
|
299
|
+
room](rooms-and-matches.md#5e-landing-in-a-friends-room-play-with) is how they arrive.
|
|
300
|
+
|
|
301
|
+
An invite is live, unlike a challenge: it reaches a friend who is on the Arcade now, and nothing
|
|
302
|
+
waits in an inbox. For "beat my score whenever you next play", send a
|
|
303
|
+
[friend challenge](#friend-challenges) instead.
|
|
304
|
+
|
|
305
|
+
### Invites and share links are one window
|
|
306
|
+
|
|
307
|
+
The sheet is the Arcade's **Friends window**. The player also opens it from the Arcade's own menu
|
|
308
|
+
over your game, without your game asking. It holds the friends to invite **and** a link to share
|
|
309
|
+
with anyone else, and both open your game the same way: with **your launch parameters**, on
|
|
310
|
+
exactly [`gem.share.request`'s rules](getting-started.md#linking-to-your-game). So a game whose
|
|
311
|
+
rooms are joined by a code says the code once, and a friend who accepts an invite and a stranger
|
|
312
|
+
who opens the shared link both arrive with it.
|
|
313
|
+
|
|
314
|
+
There are two ways to say it, for the window's two ways in:
|
|
315
|
+
|
|
316
|
+
- **`gem.friends.setInviteParams(params)`** — a standing declaration, for the window the PLAYER
|
|
317
|
+
opens from the Arcade's menu. Call it when the player enters a room joined by code, and
|
|
318
|
+
`setInviteParams(null)` when they leave it, so the window never invites anyone into a room that
|
|
319
|
+
is gone. The latest call stands; the SDK holds it and says it again after a reconnect.
|
|
320
|
+
- **`gem.friends.inviteToPlay({ params })`** — from your own Invite button, for that one opening.
|
|
321
|
+
It wins over the declaration. Omit `params` to use the declaration.
|
|
322
|
+
|
|
323
|
+
A room joined by code, end to end:
|
|
324
|
+
|
|
325
|
+
```js
|
|
326
|
+
// The host makes the room and says what the window should open.
|
|
327
|
+
const room = await gem.rooms.create({/* your template */});
|
|
328
|
+
gem.friends.setInviteParams({ room: room.joinCode });
|
|
329
|
+
|
|
330
|
+
// Your own Invite button, if you draw one, needs nothing more:
|
|
331
|
+
inviteButton.onclick = () => gem.friends.inviteToPlay();
|
|
332
|
+
|
|
333
|
+
// Leaving the room clears it.
|
|
334
|
+
gem.friends.setInviteParams(null);
|
|
335
|
+
|
|
336
|
+
// Arrival is unchanged: whoever opens the link or accepts the invite launches your game
|
|
337
|
+
// with ?room=<code>, and you join as you already do for a shared link.
|
|
338
|
+
const code = new URLSearchParams(location.search).get('room');
|
|
339
|
+
if (code) await gem.rooms.joinByCode(code);
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
The params name what a link opens and never a person, so they tell the Arcade nothing about the
|
|
343
|
+
player's friends, and nothing about who was invited comes back. A param the share rules refuse —
|
|
344
|
+
a reserved key, more than 16, a bad key or value — is said on the console and nothing is sent:
|
|
345
|
+
`inviteToPlay` resolves `false`, and `setInviteParams` returns `false` and leaves the last
|
|
346
|
+
declaration standing. A game with a quick-join hub needs no params at all: the friend's
|
|
347
|
+
quick-join already seats them in the player's room (above).
|
|
348
|
+
|
|
349
|
+
### One answer, and nothing else
|
|
350
|
+
|
|
351
|
+
`inviteToPlay()` resolves one boolean. It never rejects.
|
|
352
|
+
|
|
353
|
+
| resolves | what happened |
|
|
354
|
+
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
355
|
+
| `true` | the Arcade opened its invite sheet |
|
|
356
|
+
| `false` | it did not: outside the Arcade or without `playInvites` (answered at once, with nothing asked), no press behind the ask, another sheet or prompt already open, a player the Arcade offers no invites to, or no answer arrived |
|
|
357
|
+
|
|
358
|
+
**What your game never learns:** whom the player invited, how many, whether any invite was sent,
|
|
359
|
+
delivered or accepted, or why the sheet did not open. Who a player invites is a fact about the
|
|
360
|
+
people they know, and whether an invite landed is a fact about somebody else's settings; neither
|
|
361
|
+
is your game's. `true` means a sheet opened, not that anybody was invited.
|
|
362
|
+
|
|
363
|
+
**Your game names nobody and writes nothing.** No player, no text: the friends on the sheet are
|
|
364
|
+
the Arcade's own list, and the words on the sheet and on the invite are the Arcade's. Your launch
|
|
365
|
+
params say what the link opens, as a share's do, and nothing else.
|
|
366
|
+
|
|
367
|
+
### Rules that are not optional
|
|
368
|
+
|
|
369
|
+
- **Call it only from a player's press.** The Arcade answers an ask with no press behind it
|
|
370
|
+
`false`, and shows nothing. Never on a timer, as a screen opens, or after a long `await`.
|
|
371
|
+
- **Never gate progress or a reward on it.** You cannot tell whether anyone was invited, by
|
|
372
|
+
design, so a reward for it is a reward for pressing a button.
|
|
373
|
+
- **Never retry it on your own.** `false` is an answer: offer your own way in instead, such as a
|
|
374
|
+
join code or a [share link](getting-started.md#linking-to-your-game).
|
|
375
|
+
- **One sheet at a time.** Calls made while an ask is open share its answer, and an ask that
|
|
376
|
+
arrives while the Arcade already shows a sheet is answered `false`.
|
|
377
|
+
- **Hide the button where it cannot work.** `gem.frame.parentSupports('playInvites')` is false
|
|
378
|
+
outside the Arcade — a local dev host included — and `inviteToPlay()` answers `false` there at
|
|
379
|
+
once, asking nothing. Try the real flow on your game's dev channel in the Arcade.
|
|
380
|
+
|
|
276
381
|
## Inspecting a player: game profiles
|
|
277
382
|
|
|
278
383
|
The "inspect this player" screen — tap a roster row, a leaderboard entry or a friend, see who
|
|
@@ -296,7 +401,7 @@ profile. Entries arrive ordered by key, each naming its slot's declared `visibil
|
|
|
296
401
|
|
|
297
402
|
Three properties to design around:
|
|
298
403
|
|
|
299
|
-
- **A game shapes its profile in the game config, not here.** Profile content
|
|
404
|
+
- **A game shapes its profile in the game config, not here.** Profile content _is_ the game's
|
|
300
405
|
showcase slots — save schemas declared with a `visibility` of `friends` or `public` — and the
|
|
301
406
|
write path is `gem.showcase.set` (or your dedicated server, for values a modified client must
|
|
302
407
|
not forge). There is no profile write on this surface, and identity is the player's own,
|
package/docs/getting-started.md
CHANGED
|
@@ -50,7 +50,7 @@ either way.
|
|
|
50
50
|
```jsonc
|
|
51
51
|
// package.json
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@indiegems/gem-web-sdk": "
|
|
53
|
+
"@indiegems/gem-web-sdk": "23.0.0"
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
@@ -308,6 +308,12 @@ whether they shared, cancelled, or closed the panel. Where the arcade handles it
|
|
|
308
308
|
`gem.share.available` is true; outside the arcade, before the bridge is up, and anywhere the
|
|
309
309
|
arcade does not share your game, it is false.
|
|
310
310
|
|
|
311
|
+
**The Arcade's Friends window shares the same link.** The player can open it from the Arcade's
|
|
312
|
+
menu over your game to invite friends and share a link, and both carry the launch parameters you
|
|
313
|
+
give `gem.friends.setInviteParams` or `gem.friends.inviteToPlay`, on the rules below. A game whose
|
|
314
|
+
rooms are joined by code declares the code there; see
|
|
315
|
+
[Invites and share links are one window](friends.md#invites-and-share-links-are-one-window).
|
|
316
|
+
|
|
311
317
|
`request` checks every parameter against the rules below **before** anything is sent, and
|
|
312
318
|
throws on the first one the arcade would refuse, naming it: more than 16, a key that breaks the
|
|
313
319
|
key rule or is one the arcade will not carry, or a value that is not a string, is too long, or
|
|
@@ -117,7 +117,8 @@ snap.progressions.data; // progression state
|
|
|
117
117
|
The snapshot is **cached**. Repeat calls, and reads derived from it — `gem.me.currencies`,
|
|
118
118
|
`gem.me.progressions`, item-instance lookups — serve from the one cached read rather than issuing
|
|
119
119
|
a request each, so a HUD polling a balance does not re-download the whole bag. Any change the SDK
|
|
120
|
-
makes drops the cache
|
|
120
|
+
makes drops the cache, and so does the Arcade's hint that your inventory changed on the server;
|
|
121
|
+
[§6](#6-when-to-re-read) is about both.
|
|
121
122
|
|
|
122
123
|
Each of the three sections carries its **own cursor**, because any one of them can be large
|
|
123
124
|
independently, and the snapshot carries an **`etag`** — the inventory's version — which
|
|
@@ -126,18 +127,35 @@ conditional.
|
|
|
126
127
|
|
|
127
128
|
## 6. When to re-read
|
|
128
129
|
|
|
129
|
-
**
|
|
130
|
-
your dedicated server, a
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
**On an Arcade that relays it, the frame hears about changes to your game's own inventory.** A
|
|
131
|
+
grant from your function or your dedicated server, a purchase, a gift another player sent, a
|
|
132
|
+
support grant, a change made on the player's other device: each one drops the SDK's cached reads
|
|
133
|
+
and fires `gem.me.inventory.onChanged` (and a held character's `inventory.onChanged`). The
|
|
134
|
+
Arcade sends nothing but the fact that something changed, so your listener re-reads:
|
|
133
135
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
136
|
+
```js
|
|
137
|
+
gem.me.inventory.onChanged(async () => {
|
|
138
|
+
drawBag(await gem.me.inventory.snapshot()); // the cache is already dropped: this really asks
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
It is a cue to re-read, not a receipt, and it arrives a moment after the change commits. It is
|
|
143
|
+
only ever about **this** game, on the channel you are playing: the Arcade never relays another
|
|
144
|
+
game's changes or the platform wallet's (Gems and Coins have their own hint,
|
|
145
|
+
`gem.platform.earnAllowance.on('changed')`). A burst — a function granting in a loop — reaches
|
|
146
|
+
you as one call now and at most one more every two seconds, and the hint itself makes no request.
|
|
147
|
+
|
|
148
|
+
**After your own function's grant, don't wait for a read to show it.** When
|
|
149
|
+
`gem.functions.invoke` resolves, what the function says it granted has already been written: the
|
|
150
|
+
function's answer is the receipt. Show it, let the player go on, and re-read in the background
|
|
151
|
+
(see
|
|
152
|
+
[`serverless-functions.md`](serverless-functions.md#6-calling-it-from-your-game)).
|
|
153
|
+
`onChanged` redraws the bag when the fresh read lands.
|
|
137
154
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
155
|
+
**An older Arcade sends no hint.** There, only a change your own game made through the SDK drops
|
|
156
|
+
the cache and fires `onChanged`, so keep a fallback that does not depend on the hint. The remedy
|
|
157
|
+
is not a poll. **Do not poll**: a fixed-period re-read is a per-session load on the backend that
|
|
158
|
+
scales with your player count and buys staleness you could have designed around. Instead:
|
|
141
159
|
|
|
142
160
|
- Re-read on screen-open. When the tab becomes visible, drop the cache and read again:
|
|
143
161
|
|
|
@@ -629,8 +647,32 @@ no fallback celebration.
|
|
|
629
647
|
`false` means the arcade will not show it here — the bridge is not up, or the embedder has no wallet
|
|
630
648
|
— and your game does not draw one of its own in its place.
|
|
631
649
|
|
|
632
|
-
Call it when your
|
|
633
|
-
|
|
650
|
+
Call it when your game decides to award, right beside the call that pays.
|
|
651
|
+
|
|
652
|
+
**When your serverless function pays it, give the cue and the call one `cueId`.** The platform's
|
|
653
|
+
event can reach the arcade before your `gem.functions.invoke` call even returns, so a cue sent after
|
|
654
|
+
it would otherwise look like a second reward. Make one id per award with `crypto.randomUUID()`, pass
|
|
655
|
+
it to `cueReward` and as the `cueId` of the `functions.invoke` that pays, and the arcade flies that
|
|
656
|
+
reward once, whichever reaches it first, so you may cue before the call or after it returns:
|
|
657
|
+
|
|
658
|
+
```js
|
|
659
|
+
const cueId = crypto.randomUUID(); // one per award, never reused
|
|
660
|
+
const result = await gem.functions.invoke({ claim: 'daily' }, { cueId });
|
|
661
|
+
gem.cueReward({ currencySlug: 'GEMS', expected: 15, origin, cueId });
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
A payout that pays Gems and Coins in one call cues each currency with the same `cueId`. The id is
|
|
665
|
+
yours and opaque: 1 to 64 letters, digits, `_` or `-`, and the SDK leaves out anything else with a
|
|
666
|
+
console warning rather than fail the cue or the call. It is for the animation only. It moves no
|
|
667
|
+
balance, your function never sees it, and no receipt or order carries it.
|
|
668
|
+
|
|
669
|
+
**A payout with no id** (your own game server's write, or a build that passes none) is paired by
|
|
670
|
+
currency and timing: cue it just before the write, or within about half a second after it.
|
|
671
|
+
|
|
672
|
+
**Either way, the arcade decides when the payout is shown.** A payout no cue has met within about
|
|
673
|
+
half a second flies from the middle of your frame, and a cue that comes after it adds nothing. If
|
|
674
|
+
your reward moment comes after your server pays, present the payouts yourself (below). Three things
|
|
675
|
+
to know either way:
|
|
634
676
|
|
|
635
677
|
- **The arcade never takes your word for the amount.** What lands, and the player's new total, come
|
|
636
678
|
from the platform's own event, sent only after the grant committed. `expected` is drawn as
|
|
@@ -648,6 +690,37 @@ not matter; the arcade reconciles. Three things to know:
|
|
|
648
690
|
- **This is presentation, not permission.** It grants nothing, spends nothing and reveals nothing,
|
|
649
691
|
and a reduced-motion player sees the total change without the flight.
|
|
650
692
|
|
|
693
|
+
#### Showing the payout at a moment of your own: `gem.setRewardPresentation('game')`
|
|
694
|
+
|
|
695
|
+
Your server pays when the reward is decided (the board is won, the run is verified), because that
|
|
696
|
+
is when the result is checked and the progress saved. Your reward moment can come seconds or minutes
|
|
697
|
+
later: the player taps **Claim** on the win card, or the coins fly on the screen they return to. Say
|
|
698
|
+
so once, at boot, and each payout waits for your cue:
|
|
699
|
+
|
|
700
|
+
```js
|
|
701
|
+
gem.setRewardPresentation('game'); // at boot
|
|
702
|
+
// ...your server pays when the board is won. Later, when the player taps Claim:
|
|
703
|
+
gem.cueReward({ currencySlug: 'ARCADE_TICKETS', expected: 20, origin: claimCoinCentre });
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
- **The payout supplies the number; your cue supplies the moment.** The arcade holds what each
|
|
707
|
+
payout paid for its currency instead of flying it from the middle of your frame. Each
|
|
708
|
+
`cueReward` releases up to its `expected` of what is held (all of it when you give no amount) and
|
|
709
|
+
flies it from its `origin`; the wallet chip over your game, when you show one
|
|
710
|
+
(`setWalletVisible`), counts up as the marks land.
|
|
711
|
+
- **Either order, any number.** A cue that finds nothing held waits for the next payout of its
|
|
712
|
+
currency, as it always has, and three payouts at the end of a level can be three cues, one cue,
|
|
713
|
+
or a cue per screen.
|
|
714
|
+
- **It delays the show, never the money.** What is paid, when, and every ceiling are unchanged; the
|
|
715
|
+
arcade's menu, its header and every purchase see the player's true total the moment the platform
|
|
716
|
+
pays. Only the chip over your game shows the total minus what is still held.
|
|
717
|
+
- **Nothing stays hidden.** Whatever part of a payout no cue releases within five minutes lands
|
|
718
|
+
quietly, with no flight. A player who quits on the win screen keeps every coin.
|
|
719
|
+
- **Screen state, held across a reconnect.** `gem.setRewardPresentation('arcade')` goes back to the
|
|
720
|
+
default and lands whatever is held. It returns `true` when the arcade was told, and `false` before
|
|
721
|
+
the bridge is up (held and sent on connect) and under an embedder that presents every payout its
|
|
722
|
+
own way; there, keep cueing as usual and nothing else changes.
|
|
723
|
+
|
|
651
724
|
To draw the Gem on your own screen, use the platform's art, which ships in the package — the Gems
|
|
652
725
|
**icon** beside an amount or price, the **hero** only on a reward presentation where the gem is the
|
|
653
726
|
subject. [brand-art.md](brand-art.md) has both, how to draw them, and the rest of the rules.
|
package/docs/manifest.json
CHANGED
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"file": "arcade-economy.md",
|
|
31
|
-
"sha256": "
|
|
32
|
-
"updated": "2026-11-
|
|
31
|
+
"sha256": "108b3225a45dce776907344f8a3865b99b6e16598c6f70ddf0e8b923cf330a7e",
|
|
32
|
+
"updated": "2026-11-04T21:50:20Z"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"file": "async-opponents.md",
|
|
@@ -48,13 +48,13 @@
|
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
"file": "changelog.md",
|
|
51
|
-
"sha256": "
|
|
52
|
-
"updated": "2026-11-
|
|
51
|
+
"sha256": "20215a4410087dd6076afd787bcfd4ee29bc5bf50e56562563e32a060cd74cc4",
|
|
52
|
+
"updated": "2026-11-05T00:34:54Z"
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
"file": "daily-challenges.md",
|
|
56
|
-
"sha256": "
|
|
57
|
-
"updated": "2026-
|
|
56
|
+
"sha256": "8bc76d80cf419c196fbdb052785c4a768408118389c311537df4229be6129366",
|
|
57
|
+
"updated": "2026-11-04T21:50:20Z"
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
60
|
"file": "dedicated-server-flow.md",
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
"file": "friends.md",
|
|
71
|
-
"sha256": "
|
|
72
|
-
"updated": "2026-
|
|
71
|
+
"sha256": "fd6db8da391647677568c0d2257dc5c92cbfe311759191b5e28fa3f66747aee6",
|
|
72
|
+
"updated": "2026-11-04T23:46:11Z"
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
75
|
"file": "game-economy-tutorial.md",
|
|
@@ -78,8 +78,8 @@
|
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
80
|
"file": "getting-started.md",
|
|
81
|
-
"sha256": "
|
|
82
|
-
"updated": "2026-11-
|
|
81
|
+
"sha256": "c2f0c4fe33cc3e34120773a606179a4254963b957ab39cdda746c2f01db493d4",
|
|
82
|
+
"updated": "2026-11-05T00:34:54Z"
|
|
83
83
|
},
|
|
84
84
|
{
|
|
85
85
|
"file": "graphics-route.md",
|
|
@@ -88,8 +88,8 @@
|
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
90
|
"file": "inventory-and-catalogs.md",
|
|
91
|
-
"sha256": "
|
|
92
|
-
"updated": "2026-11-
|
|
91
|
+
"sha256": "f5aa4e405989da098317ca704b4dd8b996a1369c78ef0515ed7442886aee85fc",
|
|
92
|
+
"updated": "2026-11-04T23:38:54Z"
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
95
|
"file": "leaderboards.md",
|
|
@@ -133,8 +133,8 @@
|
|
|
133
133
|
},
|
|
134
134
|
{
|
|
135
135
|
"file": "rooms-and-matches.md",
|
|
136
|
-
"sha256": "
|
|
137
|
-
"updated": "2026-
|
|
136
|
+
"sha256": "dcbd9dda741656e5e346c2fea3240331ac19d055aede0ff27d706a63efb35500",
|
|
137
|
+
"updated": "2026-11-04T23:26:42Z"
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
"file": "saving-player-progress.md",
|
|
@@ -143,8 +143,8 @@
|
|
|
143
143
|
},
|
|
144
144
|
{
|
|
145
145
|
"file": "server-bundles.md",
|
|
146
|
-
"sha256": "
|
|
147
|
-
"updated": "2026-11-
|
|
146
|
+
"sha256": "fe9e5ced6e2e6bc4d583697c5f206f2f0431af8926eab7d8076f7f46e42864c9",
|
|
147
|
+
"updated": "2026-11-05T00:34:54Z"
|
|
148
148
|
},
|
|
149
149
|
{
|
|
150
150
|
"file": "serverless-functions-js.md",
|
|
@@ -153,13 +153,13 @@
|
|
|
153
153
|
},
|
|
154
154
|
{
|
|
155
155
|
"file": "serverless-functions.md",
|
|
156
|
-
"sha256": "
|
|
157
|
-
"updated": "2026-
|
|
156
|
+
"sha256": "fa1d161689e30d2300ab2ec7de3f3a5c9147966f559f7272b9e9b91852ecbeb4",
|
|
157
|
+
"updated": "2026-11-04T23:38:54Z"
|
|
158
158
|
},
|
|
159
159
|
{
|
|
160
160
|
"file": "trading-and-gifting.md",
|
|
161
|
-
"sha256": "
|
|
162
|
-
"updated": "2026-
|
|
161
|
+
"sha256": "24ff7ebfc10b5186527cc51b49d8f2f556b43e6ff37b06db20ce3c7efd87855a",
|
|
162
|
+
"updated": "2026-11-04T23:38:54Z"
|
|
163
163
|
},
|
|
164
164
|
{
|
|
165
165
|
"file": "vibration.md",
|
|
@@ -168,8 +168,8 @@
|
|
|
168
168
|
},
|
|
169
169
|
{
|
|
170
170
|
"file": "what-a-game-must-do.md",
|
|
171
|
-
"sha256": "
|
|
172
|
-
"updated": "2026-11-
|
|
171
|
+
"sha256": "e79c10bf3a06b7d9d85b5d6ba06a7a1cddf7a27f02d26b4bb4399ebf0d5a0644",
|
|
172
|
+
"updated": "2026-11-04T23:07:19Z"
|
|
173
173
|
}
|
|
174
174
|
]
|
|
175
175
|
}
|
|
@@ -56,7 +56,9 @@ Two engine signals are deliberately absent, and each absence is a rule rather th
|
|
|
56
56
|
display identity only, via `gem.friends` ([`friends.md`](friends.md)) — and nothing else about
|
|
57
57
|
the graph crosses the frame. No presence, no pending requests, no invite events, no mutations.
|
|
58
58
|
An invite is something happening *in* the graph, so it is delivered and rendered where the
|
|
59
|
-
graph lives, on the Arcade.
|
|
59
|
+
graph lives, on the Arcade. A game may ask the Arcade to open its own invite sheet
|
|
60
|
+
(`gem.friends.inviteToPlay`), and a friend who accepts arrives by quick-join (§5e) — but the
|
|
61
|
+
invite itself never reaches a game. Derived questions also keep being answered where the data lives:
|
|
60
62
|
the friend view in [`leaderboards.md`](leaderboards.md) re-ranks a board server-side, and a
|
|
61
63
|
party crosses into a room as an opaque grouping.
|
|
62
64
|
- **`ping_results_ready`** — the engine's measurement is a UDP echo, which a browser cannot perform.
|
|
@@ -1004,8 +1006,9 @@ but never start short", name the same slugs on `queueForMatch()` instead.
|
|
|
1004
1006
|
## 5e. Landing in a friend's room: play-with
|
|
1005
1007
|
|
|
1006
1008
|
The Arcade can open your game **to play with someone**: when the player presses Join on a friend
|
|
1007
|
-
who is playing it,
|
|
1008
|
-
|
|
1009
|
+
who is playing it, accepts an invite a friend sent from inside it
|
|
1010
|
+
([`friends.md`](friends.md#inviting-a-friend-to-play)), or answers the prompt the Arcade shows a
|
|
1011
|
+
party member when their leader starts it. Your game makes no call for this. The Arcade adds a
|
|
1009
1012
|
launch key of its own, `gemPlayWith`: an opaque ticket the platform issued for this player, which
|
|
1010
1013
|
names nobody your game can recognise. The SDK reads it once as `createGem` runs, and every
|
|
1011
1014
|
`quickJoin()` of the session passes it on: the platform tries the room of the person the ticket
|
package/docs/server-bundles.md
CHANGED
|
@@ -80,8 +80,8 @@ To check the download yourself: the package npm serves is, byte for byte, the fi
|
|
|
80
80
|
also publishes on its download host, with the hash beside it:
|
|
81
81
|
|
|
82
82
|
```bash
|
|
83
|
-
curl -fsSL -O https://downloads.gemarcade.com/sdk/
|
|
84
|
-
curl -fsSL https://downloads.gemarcade.com/sdk/
|
|
83
|
+
curl -fsSL -O https://downloads.gemarcade.com/sdk/v23.0.0/gem-web-sdk.tgz
|
|
84
|
+
curl -fsSL https://downloads.gemarcade.com/sdk/v23.0.0/gem-web-sdk.tgz.sri
|
|
85
85
|
openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
|
|
86
86
|
```
|
|
87
87
|
|
|
@@ -150,6 +150,9 @@ fires them only when you opt in:
|
|
|
150
150
|
- **A function a player called pays that player.** Its `call-api` writes the opted-in progression
|
|
151
151
|
for the player id `get-context` gave it, and the platform grants and unlocks as it does for your
|
|
152
152
|
server's write: currency on every channel, achievements on `release` only.
|
|
153
|
+
- **The function's answer should name what it paid.** The game shows the reward from that answer,
|
|
154
|
+
at once, rather than reading the inventory until it appears
|
|
155
|
+
([section 6](#6-calling-it-from-your-game)).
|
|
153
156
|
- **It pays nobody else.** It cannot write another player at all
|
|
154
157
|
(`403 function_foreign_entity_write`, the previous subsection), so it cannot pay one, directly or
|
|
155
158
|
through anything your config writes on the order's behalf. A character never earns a platform
|
|
@@ -683,6 +686,24 @@ A value JSON cannot carry, such as a `BigInt`, is refused before anything is sen
|
|
|
683
686
|
- `result.text()` reads them as UTF-8, and throws if they are not UTF-8 rather than changing them;
|
|
684
687
|
- `result.bytes` is the bytes themselves, for a function that answers in binary.
|
|
685
688
|
|
|
689
|
+
**The answer is the receipt.** When `invoke` resolves, what your function says it granted has
|
|
690
|
+
already been written: the write happens inside the function, before it answers, and a refusal is
|
|
691
|
+
its `Err`, so a resolved call is never a "maybe". Show what the answer says and let the player go
|
|
692
|
+
on at once. Re-read in the background, and redraw when the read lands:
|
|
693
|
+
|
|
694
|
+
```js
|
|
695
|
+
const { granted } = (await gem.functions.invoke(claim)).json();
|
|
696
|
+
showReward(granted); // the function's word: show it now
|
|
697
|
+
gem.me.inventory.refresh(); // drop the cache...
|
|
698
|
+
gem.me.inventory.snapshot().then(drawBag); // ...and redraw from a fresh read, in the background
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
**Never hold the player on a loop that re-reads until the grant shows.** That wait adds the
|
|
702
|
+
platform's read latency to every reward and buys nothing the answer did not already say. On an
|
|
703
|
+
Arcade that relays it, `gem.me.inventory.onChanged` also fires once the platform has the change
|
|
704
|
+
([inventory-and-catalogs.md](inventory-and-catalogs.md#6-when-to-re-read)), so a listener there can
|
|
705
|
+
do the redraw for you.
|
|
706
|
+
|
|
686
707
|
**It sends the call once.** The SDK never repeats a function call for you, because your function
|
|
687
708
|
may have granted or written something, and a retry you cannot see is how one grant becomes two.
|
|
688
709
|
Whether repeating it could help is on the error. A repeated call may grant again, and what stops
|
|
@@ -716,6 +737,22 @@ try {
|
|
|
716
737
|
}
|
|
717
738
|
```
|
|
718
739
|
|
|
740
|
+
**A call that pays a reward the player sees takes a `cueId`.** Pass the same id to
|
|
741
|
+
`gem.cueReward` for that reward, one `crypto.randomUUID()` per award, and the Gem Arcade flies it
|
|
742
|
+
once whichever of the two reaches it first:
|
|
743
|
+
|
|
744
|
+
```js
|
|
745
|
+
const cueId = crypto.randomUUID();
|
|
746
|
+
const result = await gem.functions.invoke(claim, { cueId });
|
|
747
|
+
gem.cueReward({ currencySlug: 'GEMS', expected: 15, origin, cueId });
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
The platform carries it to the payout event it shows the arcade and nowhere else: your function
|
|
751
|
+
never sees it, and neither the answer nor a receipt carries it. It is 1 to 64 letters, digits, `_`
|
|
752
|
+
or `-`; the SDK leaves out anything else with a console warning and still makes the call.
|
|
753
|
+
[inventory-and-catalogs.md](inventory-and-catalogs.md#showing-the-reward-land-in-the-arcades-wallet)
|
|
754
|
+
has the cue itself.
|
|
755
|
+
|
|
719
756
|
⚠️ **A lost connection is `retryable` and tells you nothing about whether your function ran.** The
|
|
720
757
|
call left, and no answer came back. If your function grants or writes, make it safe to call twice
|
|
721
758
|
with the same input, for instance by keying what it writes on an id the caller sends.
|
|
@@ -882,6 +919,7 @@ the last column: it is `true` exactly where this table says to retry.
|
|
|
882
919
|
| `422 function_failed` | It did not finish. `err.failure` is `trap`, `memory_exceeded`, `cpu_budget_exceeded`, `host_call_limit`, `output_too_large` or `argument_too_large` | A bug or a limit in the function. Reproduce with the same input |
|
|
883
920
|
| `504 function_timeout` | It ran past the wall clock | Do not retry: the same input takes as long again |
|
|
884
921
|
| `404 function_not_published` | Nothing is promoted on the caller's channel | `gemctl function promote <version> --channel <channel>` |
|
|
922
|
+
| `400 invalid_cue_id` | The `cue_id` query parameter is not 1 to 64 letters, digits, `_` or `-`. `gem.functions.invoke` leaves a bad `cueId` out, so only a hand-written call meets it | Fix the hand-written call |
|
|
885
923
|
| `413 function_input_too_large` | The input is over 256 KiB | Send a request, not a file |
|
|
886
924
|
| `429 rate_limited` | This caller's per-minute budget is spent | Wait `err.retryAfterSeconds` |
|
|
887
925
|
| `503 function_capacity_unavailable` | The platform was too busy to run it. Your function is not known to be broken | Retry after `err.retryAfterSeconds` |
|
|
@@ -38,15 +38,18 @@ await gem.transfers.reject(transferId);
|
|
|
38
38
|
await gem.transfers.cancel(transferId);
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
##
|
|
42
|
-
|
|
43
|
-
**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
## How the recipient hears about it
|
|
42
|
+
|
|
43
|
+
**On an Arcade that relays it, the recipient's game is told its inventory changed.** The gift
|
|
44
|
+
lands in the recipient's inventory for your game, and the Arcade sends that game the same
|
|
45
|
+
field-free hint as for any other change to its own inventory: the SDK drops its cached reads and
|
|
46
|
+
`gem.me.inventory.onChanged` fires, so a listener that re-reads shows the gift. The hint names no
|
|
47
|
+
sender, item or amount; the re-read is what tells you. See
|
|
48
|
+
[`inventory-and-catalogs.md`](inventory-and-catalogs.md#6-when-to-re-read) for the full rule.
|
|
49
|
+
|
|
50
|
+
**On an older Arcade nothing is pushed.** The recipient sees the gift the next time their game
|
|
51
|
+
re-reads its inventory, so re-read after any action that could have involved another player, and
|
|
52
|
+
design so a few seconds of not-yet-seeing-it is harmless either way.
|
|
50
53
|
|
|
51
54
|
## `transfer_policy` is your game's rule, not the platform's
|
|
52
55
|
|
|
@@ -1785,7 +1785,9 @@ opens the Gem menu — the Gem button, in whichever corner it sits — to see th
|
|
|
1785
1785
|
achievement score, and every payout
|
|
1786
1786
|
[`gem.cueReward`](inventory-and-catalogs.md#showing-the-reward-land-in-the-arcades-wallet) announces
|
|
1787
1787
|
flies into the Gem button. A payout you do not cue flies too, from the middle of your frame, so cue
|
|
1788
|
-
the ones the player earns on screen and the flight starts where they are looking.
|
|
1788
|
+
the ones the player earns on screen and the flight starts where they are looking. A payout your
|
|
1789
|
+
serverless function makes takes the same `cueId` on the cue and on `gem.functions.invoke`, so it
|
|
1790
|
+
flies once.
|
|
1789
1791
|
|
|
1790
1792
|
**A game that wants them on screen asks** (the owner, 2026-10-29):
|
|
1791
1793
|
|
|
@@ -1804,6 +1806,25 @@ gem.setWalletVisible(true, { only: ['GEMS', 'ARCADE_TICKETS'] }); // add 'score'
|
|
|
1804
1806
|
- **An Arcade from before this change ignores the call**: nothing breaks, and the wallets stay in
|
|
1805
1807
|
the Gem menu. Another embedder that draws wallets over games honours it the same way.
|
|
1806
1808
|
|
|
1809
|
+
### When a payout is shown: `gem.setRewardPresentation`
|
|
1810
|
+
|
|
1811
|
+
By default the Arcade decides when a payout is shown: a cue within about half a second of your
|
|
1812
|
+
server's write flies it from the cue's `origin`, and a payout with no cue by then flies from the
|
|
1813
|
+
middle of your frame. **If your reward moment comes after your server pays** — a Claim button on
|
|
1814
|
+
the win card, or the screen the player returns to — say so once, at boot:
|
|
1815
|
+
|
|
1816
|
+
```js
|
|
1817
|
+
gem.setRewardPresentation('game');
|
|
1818
|
+
```
|
|
1819
|
+
|
|
1820
|
+
Each payout then waits for your `gem.cueReward`, which releases up to its `expected` and flies it
|
|
1821
|
+
from its `origin`, in either order and at any moment within five minutes; whatever no cue releases
|
|
1822
|
+
by then lands quietly, with no flight. The money never waits: what is paid and when is unchanged,
|
|
1823
|
+
and the Gem menu and every purchase see the player's true total at once. `'arcade'` goes back to
|
|
1824
|
+
the default.
|
|
1825
|
+
[inventory-and-catalogs.md](inventory-and-catalogs.md#showing-the-payout-at-a-moment-of-your-own-gemsetrewardpresentationgame)
|
|
1826
|
+
has the detail.
|
|
1827
|
+
|
|
1807
1828
|
## No accidental text selection
|
|
1808
1829
|
|
|
1809
1830
|
**You write nothing for this.** A long press on a phone, a double-click on a button, a drag across
|