@indiegems/gem-web-sdk 7.8.1 → 7.9.1
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 +69 -0
- package/dist/game.d.ts +22 -1
- package/dist/gem-web-sdk.bridge.js +1 -1
- package/dist/gem-web-sdk.esm.js +2 -2
- package/dist/gem-web-sdk.function.js +2 -2
- package/dist/gem-web-sdk.game.js +2 -2
- package/dist/gem-web-sdk.min.js +2 -2
- package/dist/gem-web-sdk.sdk.js +2 -2
- package/dist/gem-web-sdk.server.js +2 -2
- package/dist/index.d.ts +32 -4
- package/dist/sdk.d.ts +22 -1
- package/docs/achievements.md +99 -3
- package/docs/api/api-reference-game.md +164 -11
- package/docs/api/api-reference-server.md +1 -1
- package/docs/api/reference.json +177 -4
- package/docs/changelog.md +69 -0
- package/docs/daily-challenges.md +9 -8
- package/docs/game-economy-tutorial.md +3 -2
- package/docs/getting-started.md +8 -4
- package/docs/manifest.json +14 -14
- package/docs/server-bundles.md +2 -2
- package/docs/what-a-game-must-do.md +180 -69
- package/package.json +1 -1
- package/schema/bridge.v1.json +104 -2
- package/schema/capabilities.public.json +7 -0
package/docs/changelog.md
CHANGED
|
@@ -16,6 +16,75 @@ Its entries are grouped under up to five headings:
|
|
|
16
16
|
- **Rebuild**: a type changed. Recompile against the new version; code that compiles today keeps compiling unless the entry says otherwise.
|
|
17
17
|
- **New capability**: something a game can now do. Nothing changes unless you use it.
|
|
18
18
|
|
|
19
|
+
## 7.9.1
|
|
20
|
+
|
|
21
|
+
Upgrading from 7.9.0. Impact: none.
|
|
22
|
+
|
|
23
|
+
Nothing in this release asks anything of a game.
|
|
24
|
+
|
|
25
|
+
## 7.9.0
|
|
26
|
+
|
|
27
|
+
Upgrading from 7.8.1. Impact: required.
|
|
28
|
+
|
|
29
|
+
### Must change
|
|
30
|
+
|
|
31
|
+
- A link your game shares can no longer carry a parameter named build. The Arcade now reads that key for itself and removes it before your game starts, so the share request refuses it with a TypeError that names the key. Rename the parameter if your game shares one by that name.
|
|
32
|
+
|
|
33
|
+
### Rebuild
|
|
34
|
+
|
|
35
|
+
- GemCapability gained gameMenu, and the bridge's message lists and unions in both directions gained the two new menu messages. A switch that assumes it knows every GemCapability value must handle the new one. Other code that compiles today keeps compiling.
|
|
36
|
+
|
|
37
|
+
### New capability
|
|
38
|
+
|
|
39
|
+
- gem.setMenu adds to the Arcade's menu, the one control the Arcade keeps over a game in its top right corner. Give onSettings and the menu gains a settings entry that opens your own settings screen, and give items for up to four entries of your own, each with an id, a label you have already translated and an onSelect handler. Each call is the whole answer and replaces the last, so call it again when the entries change. It returns true when the Arcade was told. After awaitBridge has settled, draw a settings button of your own only when it returns false. The Arcade pauses your game while its menu is open and resumes it before your handler runs.
|
|
40
|
+
|
|
41
|
+
## 7.8.1
|
|
42
|
+
|
|
43
|
+
Upgrading from 7.8.0. Impact: none.
|
|
44
|
+
|
|
45
|
+
Nothing in this release asks anything of a game.
|
|
46
|
+
|
|
47
|
+
## 7.8.0
|
|
48
|
+
|
|
49
|
+
Upgrading from 7.7.0. Impact: recommended.
|
|
50
|
+
|
|
51
|
+
### May stop
|
|
52
|
+
|
|
53
|
+
- The SDK now stops accidental text selection, the long-press copy menu and the tap highlight across your game's document, so a game can drop its own rules for that. Your own style rules always win, and inputs, text areas and editable content stay typeable and selectable. Text a player could select and copy before no longer is, unless your own styles make it selectable. Pass selectableText as true to createGem only if your game is mostly text a player should be able to copy.
|
|
54
|
+
|
|
55
|
+
### New capability
|
|
56
|
+
|
|
57
|
+
- A room says whether it is self-hosted in selfHosted: its dedicated server runs on the machine of the player who created it, as it does in the Gem editor. The SDK asks for such a room on your behalf where the embedder hosts locally, and there is no option for a game to set. It is false on every room a player in the Arcade is in.
|
|
58
|
+
|
|
59
|
+
## 7.7.0
|
|
60
|
+
|
|
61
|
+
Upgrading from 7.6.0. Impact: required.
|
|
62
|
+
|
|
63
|
+
### Must change
|
|
64
|
+
|
|
65
|
+
- In the server API types, occurred_at on a leaderboard row and on an entity leaderboard row is now optional and may be null. Code that assumes it is always a number must handle it missing. LeaderboardResponse and EntityLeaderboardResponse are now written as the shared LeaderboardPeriodFields joined with their own fields, so every field they had is still there.
|
|
66
|
+
|
|
67
|
+
### Rebuild
|
|
68
|
+
|
|
69
|
+
- Server API unions gained values. OrderSource has earning_reset, and InventoryChange has a new earning_limits_reset change. A switch that assumes it knows every OrderSource value or every InventoryChange type must handle the new one. Other code that compiles today keeps compiling.
|
|
70
|
+
- The server API reads getLeaderboard and getEntityLeaderboard accept three more options, periods_back, period and player_id, and the option types GetLeaderboardOptions and EntityLeaderboardOptions now extend the new PeriodOptions. Code that compiles today keeps compiling.
|
|
71
|
+
|
|
72
|
+
### New capability
|
|
73
|
+
|
|
74
|
+
- gem.leaderboards can read a board scoped to one period of a challenge, such as today's board or this week's. Every read takes periodsBack, where 0 is the current period and 1 is the one before, or period, the id of a period from an earlier result. Name at most one of them. A result from such a board carries period, which says which period it is, when it opens and closes, and whether it is final. A serverless function or a dedicated server passes playerId so the board answers for that player's own day. On a board that is not scoped to periods, both selectors are refused rather than ignored.
|
|
75
|
+
|
|
76
|
+
## 7.6.0
|
|
77
|
+
|
|
78
|
+
Upgrading from 7.5.0. Impact: required.
|
|
79
|
+
|
|
80
|
+
### Must change
|
|
81
|
+
|
|
82
|
+
- createGem now throws GemUnframedError when the page is opened on its own instead of inside a frame. Before, such a game constructed and then waited for a bridge that never came, with no player, no saves and no config. Run your game where it is framed and handed an identity: the Arcade, the Gem editor or the Gem dev host. A game that is only ever played there changes nothing. When the framing page is not one the game allows, the refusal is now final: awaitBridge, awaitIdentity, awaitPlayer and awaitCohorts answer at once instead of timing out.
|
|
83
|
+
|
|
84
|
+
### New capability
|
|
85
|
+
|
|
86
|
+
- A serverless function written in JavaScript can use the SDK through a new function entry of the package. createGem there returns the same platform-backed calls a browser game makes, bound to the player who called the function. Nothing is retried, a conditional save is refused before it is sent, and gem.functions and gem.gameConfig throw because a function has neither. This entry is in the package only.
|
|
87
|
+
|
|
19
88
|
## 7.5.0
|
|
20
89
|
|
|
21
90
|
Upgrading from 7.4.2. Impact: required.
|
package/docs/daily-challenges.md
CHANGED
|
@@ -246,14 +246,13 @@ you almost certainly do not want to:
|
|
|
246
246
|
| Storefront marker, My Games | yes | yes |
|
|
247
247
|
| Rewards, platform grants | yes | yes |
|
|
248
248
|
| The platform-wide **streak** | yes | **no** — a streak of its own, on your store page |
|
|
249
|
-
| The player's history calendar| yes | no |
|
|
250
249
|
| Label in the arcade | localised in every language the arcade speaks | generic |
|
|
251
250
|
| Period length | a day, a week, a month | must be between **1 and 31 days** |
|
|
252
251
|
|
|
253
252
|
A streak spans games, which only works when every game agrees on what the player's "today" is. A
|
|
254
253
|
custom cron period is yours alone and cannot be compared with anything, so a cron challenge is
|
|
255
254
|
**game-local**: it shows progress, pays and keeps its own streak, and it never joins the
|
|
256
|
-
platform-wide streak
|
|
255
|
+
platform-wide streak.
|
|
257
256
|
|
|
258
257
|
**If you wrote `0 0 * * *`, write `daily`.** The upload stores it as `daily` either way — which puts
|
|
259
258
|
it on each player's own calendar, not on UTC midnight. Reach for a custom cron only when the period
|
|
@@ -542,10 +541,11 @@ Three things to know before you build a mechanic on it:
|
|
|
542
541
|
- **One completion is a day.** A player who finishes three of your challenges advances their streak
|
|
543
542
|
once, the same as a player who finishes one. A streak measures days the player showed up, not how
|
|
544
543
|
much a game offered.
|
|
545
|
-
- **A streak is the platform's
|
|
546
|
-
|
|
547
|
-
- **
|
|
548
|
-
|
|
544
|
+
- **A streak is the platform's.** Your game reports progress and does not read the streak back —
|
|
545
|
+
see below.
|
|
546
|
+
- **The arcade does not display the platform-wide streak.** The platform counts it, but no arcade
|
|
547
|
+
surface shows it to the player today. Do not tell players about it, and do not design a mechanic
|
|
548
|
+
that depends on a player seeing it.
|
|
549
549
|
|
|
550
550
|
Challenges on a custom cron are game-local: each keeps a streak of its own, shown on your game's
|
|
551
551
|
store page, and none advances the platform-wide one ([section 5a](#5a-use-daily-weekly-or-monthly-avoid-cron-for-a-challenge)).
|
|
@@ -576,8 +576,9 @@ counts for the day it was played. [A board for each day](leaderboards.md#a-board
|
|
|
576
576
|
is the page.
|
|
577
577
|
|
|
578
578
|
**Their per-period history and their streak — no.** There is no history read and no streak read in
|
|
579
|
-
the SDK
|
|
580
|
-
|
|
579
|
+
the SDK, and the arcade has no history calendar either. What the arcade displays is the marker on
|
|
580
|
+
your game's card and the player's progress in the current period — plus, for a cron challenge, its
|
|
581
|
+
own streak on your store page. Your game does not have to render any of it.
|
|
581
582
|
|
|
582
583
|
## 11. When an upload is refused
|
|
583
584
|
|
|
@@ -209,8 +209,9 @@ the one you attempted, rather than resending the purchase.
|
|
|
209
209
|
|
|
210
210
|
Your shop can also sell for the arcade's own currencies, and the platform draws one line through
|
|
211
211
|
them: **Tickets buy cosmetics, Gems buy anything.** Each platform currency has a spend scope — Tickets'
|
|
212
|
-
is `cosmetic_only`, Gems' is `any`.
|
|
213
|
-
config, and price it in
|
|
212
|
+
is `cosmetic_only`, Gems' is `any`. Which currencies your game may price in, and what each may buy,
|
|
213
|
+
is `gemctl platform config`'s answer. Mark a cosmetic in the config, and price it in
|
|
214
|
+
`ARCADE_TICKETS`:
|
|
214
215
|
|
|
215
216
|
```yaml
|
|
216
217
|
items:
|
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": "7.
|
|
53
|
+
"@indiegems/gem-web-sdk": "7.9.1"
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
@@ -175,7 +175,10 @@ gem.frame.progress(loaded, total); // while assets load
|
|
|
175
175
|
gem.frame.ready(); // once the player can actually play
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
-
And
|
|
178
|
+
And tell the Arcade where your settings are — `gem.setMenu({ onSettings })` puts them in the
|
|
179
|
+
Arcade's own menu, top-right, beside Return to the Arcade
|
|
180
|
+
([what a game must do](what-a-game-must-do.md#the-arcades-menu-your-settings-live-in-it)). Outside
|
|
181
|
+
the Arcade there is no such menu, so also give players a way out — a control that calls `gem.exit()`. A game a player
|
|
179
182
|
cannot leave is one they leave by closing the tab.
|
|
180
183
|
|
|
181
184
|
## 6. Play it
|
|
@@ -347,8 +350,9 @@ What reaches the frame:
|
|
|
347
350
|
- **The query only.** A fragment (`#…`) is never forwarded.
|
|
348
351
|
- **Not the arcade's own keys.** These are stripped and never reach the game, and they are the
|
|
349
352
|
only keys the arcade keeps for itself; it never interprets yours:
|
|
350
|
-
- Store and play-page switches: `play`, `channel`, `purchase
|
|
351
|
-
|
|
353
|
+
- Store and play-page switches: `play`, `channel`, `purchase`, and `build`, which pins one
|
|
354
|
+
of your own builds when you play it from your Builds page.
|
|
355
|
+
- Platform switches: `devtools`, `devLocale`.
|
|
352
356
|
- The arcade's own sign-in return marker: `social`.
|
|
353
357
|
- Attribution and campaign keys, which the arcade keeps out of every game: `_s`, `utm_*`
|
|
354
358
|
(every key with that prefix, matched case-sensitively, so UTM_X and utmost are yours),
|
package/docs/manifest.json
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"documents": [
|
|
9
9
|
{
|
|
10
10
|
"file": "achievements.md",
|
|
11
|
-
"sha256": "
|
|
12
|
-
"updated": "2026-
|
|
11
|
+
"sha256": "e67bc5d0e6abd9ff842be46fa82bf5117fce74b2eb9d28a3954919ddc8a11c73",
|
|
12
|
+
"updated": "2026-10-02T23:09:37Z"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"file": "analytics.md",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"file": "changelog.md",
|
|
36
|
-
"sha256": "
|
|
37
|
-
"updated": "2026-10-
|
|
36
|
+
"sha256": "8a2e021addc2980433e722ba6ca2c7f5fa81cb8eac921e0bda28e44e3484d09d",
|
|
37
|
+
"updated": "2026-10-05T17:22:40Z"
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"file": "cross-promotion.md",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
"file": "daily-challenges.md",
|
|
46
|
-
"sha256": "
|
|
47
|
-
"updated": "2026-10-
|
|
46
|
+
"sha256": "c198868e2cb1f1b5d9164bb5b1d23bf3f502987e45e10163c8b048a8eba24667",
|
|
47
|
+
"updated": "2026-10-02T23:01:00Z"
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
"file": "dedicated-server-flow.md",
|
|
@@ -63,13 +63,13 @@
|
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
65
|
"file": "game-economy-tutorial.md",
|
|
66
|
-
"sha256": "
|
|
67
|
-
"updated": "2026-10-
|
|
66
|
+
"sha256": "08588571cd6529d81bd064f65bc3158997418aeb88ce00ae281735c8b44bff1f",
|
|
67
|
+
"updated": "2026-10-02T19:05:02Z"
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
"file": "getting-started.md",
|
|
71
|
-
"sha256": "
|
|
72
|
-
"updated": "2026-10-
|
|
71
|
+
"sha256": "3c38e5b14c78b70b107d5e5b5499fa9340975d602642a8eb91f2dd969d1b605b",
|
|
72
|
+
"updated": "2026-10-05T17:22:40Z"
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
75
|
"file": "inventory-and-catalogs.md",
|
|
@@ -118,8 +118,8 @@
|
|
|
118
118
|
},
|
|
119
119
|
{
|
|
120
120
|
"file": "server-bundles.md",
|
|
121
|
-
"sha256": "
|
|
122
|
-
"updated": "2026-10-
|
|
121
|
+
"sha256": "13f5846ad8ee3d27e83916b8d95b4d3fdfcbeb85f94e095cc0c04e14dc2577b2",
|
|
122
|
+
"updated": "2026-10-05T17:22:40Z"
|
|
123
123
|
},
|
|
124
124
|
{
|
|
125
125
|
"file": "serverless-functions-js.md",
|
|
@@ -138,8 +138,8 @@
|
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
"file": "what-a-game-must-do.md",
|
|
141
|
-
"sha256": "
|
|
142
|
-
"updated": "2026-10-
|
|
141
|
+
"sha256": "f44c1fd73678b1b569c59bc94bb55cfa2bdb957d798f5985ea5b3024865ed4a6",
|
|
142
|
+
"updated": "2026-10-04T19:51:47Z"
|
|
143
143
|
}
|
|
144
144
|
]
|
|
145
145
|
}
|
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/v7.
|
|
84
|
-
curl -fsSL https://downloads.gemarcade.com/sdk/v7.
|
|
83
|
+
curl -fsSL -O https://downloads.gemarcade.com/sdk/v7.9.1/gem-web-sdk.tgz
|
|
84
|
+
curl -fsSL https://downloads.gemarcade.com/sdk/v7.9.1/gem-web-sdk.tgz.sri
|
|
85
85
|
openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
|
|
86
86
|
```
|
|
87
87
|
|
|
@@ -353,6 +353,33 @@ one refused orientation, in fullscreen, once they are already the right way roun
|
|
|
353
353
|
That is best effort (iOS does not implement it, and the native app's webview declines
|
|
354
354
|
it), which is why the panel exists and why it always offers a way past.
|
|
355
355
|
|
|
356
|
+
## An achievement unlock — celebrate it yourself, or declare the Arcade's
|
|
357
|
+
|
|
358
|
+
By default the Arcade draws no card when a player unlocks one of your game's
|
|
359
|
+
achievements: **the celebration is your game's**, in its own art and sound. If your
|
|
360
|
+
game has no celebration of its own, declare the Arcade's card on your store page
|
|
361
|
+
instead:
|
|
362
|
+
|
|
363
|
+
```yaml
|
|
364
|
+
unlock_celebration: arcade
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
`game` is the default, and it is what an absent line means.
|
|
368
|
+
|
|
369
|
+
- **Opt in when** your game has no unlock celebration of its own, so its players
|
|
370
|
+
still see their unlocks.
|
|
371
|
+
- **Do not opt in when** your game draws its own. The Arcade's card then lands on top
|
|
372
|
+
of yours, and the player sees two.
|
|
373
|
+
- **Platform milestones** — the Achievement Score tiers, which belong to no game —
|
|
374
|
+
always get the Arcade's card, whatever you declare.
|
|
375
|
+
- **Only the store page on `release` is read**, and it takes effect when that page is
|
|
376
|
+
promoted. It is display-only, and it needs no `schema_version` change.
|
|
377
|
+
- **Gems or Tickets paid in the same moment** still go through `gem.cueReward`, with
|
|
378
|
+
no flight of your own.
|
|
379
|
+
|
|
380
|
+
How your game learns an unlock happened, and the field's full rules, are in
|
|
381
|
+
[achievements.md §5](achievements.md#5-celebrating-an-unlock-is-your-games-job).
|
|
382
|
+
|
|
356
383
|
## A television, if a player puts you on one
|
|
357
384
|
|
|
358
385
|
The Arcade runs in TV browsers and on TV hardware, so a game can find itself ten feet from its
|
|
@@ -435,7 +462,9 @@ there is no route by which the frame could request one. `createGem` forwards all
|
|
|
435
462
|
arcade cannot release yours from outside the frame. Keep it captured and the player cannot click
|
|
436
463
|
the dialog sitting on top of your game.
|
|
437
464
|
|
|
438
|
-
**Save on pause.** When
|
|
465
|
+
**Save on pause.** When a player on a phone chooses Return to the Arcade in
|
|
466
|
+
[the Arcade's menu](#the-arcades-menu-your-settings-live-in-it), or presses the device's Back, your
|
|
467
|
+
game is **parked**, not closed:
|
|
439
468
|
it receives `onPause(true)` and stays loaded while the player browses. They may come back and
|
|
440
469
|
press Continue — `onPause(false)`, and play resumes where it stopped — or the parked game may be
|
|
441
470
|
ended up to five minutes later. That end usually comes with `onWillTerminate` first, **but not
|
|
@@ -542,66 +571,117 @@ the `var()` above, or read `gem.viewport`. The release checklist looks for the b
|
|
|
542
571
|
|
|
543
572
|
### The arcade's own controls: reserved areas
|
|
544
573
|
|
|
545
|
-
On a phone your game is full-bleed, and the arcade keeps one row of its own
|
|
546
|
-
|
|
574
|
+
On a phone your game is full-bleed, and the arcade keeps one row of its own over the top of it:
|
|
575
|
+
|
|
576
|
+
- **The menu button**, in the **top-right corner**, always there. It opens
|
|
577
|
+
[the Arcade's menu](#the-arcades-menu-your-settings-live-in-it): Return to the Arcade, your
|
|
578
|
+
settings, mute and the rest.
|
|
579
|
+
- **The player's wallets** (Gems, Tickets, their achievement score), in the **middle of the row**,
|
|
580
|
+
[always there](#the-arcades-wallet-always-shown-never-drawn), during play as well as on your
|
|
581
|
+
menus. They are translucent, so your game shows through them — but they are still the Arcade's,
|
|
582
|
+
and still taps the Arcade takes.
|
|
547
583
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
584
|
+
**The top of the screen has one layout, in every game:**
|
|
585
|
+
|
|
586
|
+
| Where | Whose | What goes there |
|
|
587
|
+
| -------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
588
|
+
| **Top-left** | yours | Your **Home / Back** buttons, drawn when a screen needs them, in the space to the left of the wallets. They move around _inside_ your game — back to your level select or title screen — and never leave it. |
|
|
589
|
+
| **Middle and top-right** | the Arcade's | Its wallets and its menu button. Nothing of yours. |
|
|
590
|
+
| **Centre and right, below the Arcade's row** | yours | Your **information**: score, moves, a timer, a level name. It starts under the bottom of the Arcade's row, never beside it or under it. |
|
|
591
|
+
|
|
592
|
+
Keep the left for Home / Back even on a screen that has neither today: a player learns where "back"
|
|
593
|
+
is once, across every game, and information that sits there on one screen teaches them the wrong
|
|
594
|
+
thing on the next.
|
|
595
|
+
|
|
596
|
+
**Put no settings, pause-menu or burger button of your own top-right**: your settings open from the
|
|
597
|
+
Arcade's menu, and a second button under the Arcade's would mean something else in every game.
|
|
598
|
+
|
|
599
|
+
**How much room the top-left has.** The menu button is about 48px across — the size of a Home or
|
|
600
|
+
Back button. With every wallet shown, the narrowest phones (360px wide) leave roughly 68px to the
|
|
601
|
+
left of the wallets: room for **one** button. Two buttons, or a wide one, go on their own row below
|
|
602
|
+
the Arcade's. Do not copy those numbers — read the space, as below.
|
|
554
603
|
|
|
555
604
|
Those rectangles of your frame are **reserved**: keep essential UI and every touch target out of
|
|
556
605
|
them. **Never hard-code their size or height.** They differ by phone, and in native fullscreen the
|
|
557
606
|
whole row may rise into the camera band — read them, as below.
|
|
558
607
|
|
|
559
|
-
An inset cannot say that — as a top inset, a
|
|
608
|
+
An inset cannot say that — as a top inset, a button a few dozen pixels wide would take your whole top
|
|
560
609
|
strip. So `gem.viewport` also carries the rectangles, in the same CSS pixels of your frame, measured
|
|
561
|
-
from its top-left corner:
|
|
610
|
+
from its top-left corner — one per wallet shown, and one for the menu button:
|
|
562
611
|
|
|
563
612
|
```json
|
|
564
613
|
{
|
|
565
614
|
"insets": { "top": 47, "right": 0, "bottom": 34, "left": 0 },
|
|
566
|
-
"reserved": [
|
|
615
|
+
"reserved": [
|
|
616
|
+
{ "x": 68, "y": 51, "width": 72, "height": 28 },
|
|
617
|
+
{ "x": 144, "y": 51, "width": 72, "height": 28 },
|
|
618
|
+
{ "x": 220, "y": 51, "width": 72, "height": 28 },
|
|
619
|
+
{ "x": 304, "y": 51, "width": 48, "height": 48 }
|
|
620
|
+
]
|
|
567
621
|
}
|
|
568
622
|
```
|
|
569
623
|
|
|
570
624
|
**The SDK turns them into one pair of custom properties per corner**, written beside
|
|
571
625
|
`--gem-safe-*` before anything else hears the push: `--gem-reserved-tl-width` and
|
|
572
626
|
`--gem-reserved-tl-height`, and the same pair for `tr`, `bl` and `br` — how far that corner's
|
|
573
|
-
reserved rectangles reach in from its two edges.
|
|
627
|
+
reserved rectangles reach in from its two edges. A rectangle belongs to the corner its centre lies
|
|
628
|
+
nearest to.
|
|
629
|
+
|
|
630
|
+
⚠️ **The wallets sit in the middle, so they count toward BOTH top corners.** A wallet left of
|
|
631
|
+
centre is a top-left rectangle, one right of centre a top-right one. While wallets are shown,
|
|
632
|
+
`--gem-reserved-tl-height` is the bottom of the wallets and `--gem-reserved-tl-width` reaches to the
|
|
633
|
+
middle of the screen — even though the corner itself, left of the first wallet, is clear. So:
|
|
574
634
|
|
|
575
635
|
```css
|
|
576
|
-
/*
|
|
577
|
-
|
|
578
|
-
|
|
636
|
+
/* your information — score, moves, a timer — below the WHOLE of the arcade's row: the taller of
|
|
637
|
+
the two top corners. It moves up by itself when gameplay hides the wallets. */
|
|
638
|
+
#info {
|
|
639
|
+
top: max(
|
|
640
|
+
var(--gem-safe-top, env(safe-area-inset-top, 0px)),
|
|
641
|
+
var(--gem-reserved-tl-height, 0px),
|
|
642
|
+
var(--gem-reserved-tr-height, 0px)
|
|
643
|
+
);
|
|
579
644
|
right: var(--gem-safe-right, 0px);
|
|
580
645
|
}
|
|
581
|
-
/*
|
|
582
|
-
#
|
|
583
|
-
top:
|
|
646
|
+
/* ONE Home or Back button, level with the arcade's row, in the clear corner to the wallets' left */
|
|
647
|
+
#back {
|
|
648
|
+
top: var(--gem-safe-top, env(safe-area-inset-top, 0px));
|
|
584
649
|
left: var(--gem-safe-left, 0px);
|
|
585
650
|
}
|
|
586
|
-
/*
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
651
|
+
/* a ROW of your own buttons does not fit beside the wallets: start it below the arcade's row,
|
|
652
|
+
like your information */
|
|
653
|
+
#nav {
|
|
654
|
+
top: max(
|
|
655
|
+
var(--gem-safe-top, env(safe-area-inset-top, 0px)),
|
|
656
|
+
var(--gem-reserved-tl-height, 0px),
|
|
657
|
+
var(--gem-reserved-tr-height, 0px)
|
|
658
|
+
);
|
|
590
659
|
}
|
|
591
660
|
```
|
|
592
661
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
662
|
+
The per-corner properties cannot say how wide the clear corner is — only that the corner's
|
|
663
|
+
rectangles reach in to the middle. To know whether your button fits beside the wallets, read the
|
|
664
|
+
rectangles from script: the clear width at the top-left is the smallest `x` among
|
|
665
|
+
`gem.viewport.reserved` (the whole width when the list is empty).
|
|
666
|
+
|
|
667
|
+
```js
|
|
668
|
+
const reserved = gem.viewport?.reserved ?? [];
|
|
669
|
+
const clearLeft = reserved.length ? Math.min(...reserved.map((r) => r.x)) : window.innerWidth;
|
|
670
|
+
nav.classList.toggle('below-arcade-row', clearLeft < nav.offsetWidth + 8);
|
|
671
|
+
```
|
|
597
672
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
673
|
+
All eight properties are written on every push, `0px` for a clear corner, so a rotation never leaves
|
|
674
|
+
a stale value; they are never set under an embedder that sends no viewport, which is what the `0px`
|
|
675
|
+
fallback is for. `gem.viewport.reserved` is `[]` when nothing is covered.
|
|
676
|
+
|
|
677
|
+
**Lay out against it whenever it changes.** The wallets' rectangles are there for the whole
|
|
678
|
+
session on the Arcade's phone layout, and their number and width can change as the player's
|
|
679
|
+
balances and currencies do. Read every corner, not only the ones this page names today: a layout that honours
|
|
680
|
+
all four is right under any embedder.
|
|
601
681
|
|
|
602
682
|
**Older SDKs get it folded into the top inset.** A game on an SDK from before reserved areas cannot
|
|
603
683
|
read the rectangles, so the arcade sends it none and instead raises `insets.top` — and so
|
|
604
|
-
`--gem-safe-top` — past the bottom of the
|
|
684
|
+
`--gem-safe-top` — past the bottom of the row. That game keeps working, at the cost of a full-width
|
|
605
685
|
band; upgrading gives the band back.
|
|
606
686
|
|
|
607
687
|
## Which store-page experiment brought this player in: cohorts
|
|
@@ -693,18 +773,62 @@ and anything that must follow them, is a save or `gem.me.settings`:
|
|
|
693
773
|
draws the line in full. Never block your first frame on it either: apply defaults, then apply what
|
|
694
774
|
arrives.
|
|
695
775
|
|
|
776
|
+
## The Arcade's menu: your settings live in it
|
|
777
|
+
|
|
778
|
+
The Arcade keeps one menu button over every game, top-right, on every device. Its menu is how a
|
|
779
|
+
player leaves (**Return to the Arcade**), mutes, goes fullscreen and reports a problem — and how they
|
|
780
|
+
open **your settings**, once you say you have some:
|
|
781
|
+
|
|
782
|
+
```js
|
|
783
|
+
const inArcadeMenu = gem.setMenu({
|
|
784
|
+
onSettings: () => settingsScreen.open(),
|
|
785
|
+
items: [{ id: 'how-to-play', label: t('How to play'), onSelect: () => help.open() }],
|
|
786
|
+
});
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
- **`onSettings` puts a Settings entry in the menu.** The Arcade closes its menu, resumes your game,
|
|
790
|
+
and calls it; you draw your own settings screen, as you always did. Leave it out and the menu has
|
|
791
|
+
no Settings entry.
|
|
792
|
+
- **`items` are up to four entries of your own**, shown in a group below the Arcade's. `id` is your
|
|
793
|
+
name for the entry (1 to 32 characters); `label` is what the player reads, **already in their
|
|
794
|
+
language** (`gem.prefs.locale`) — the Arcade does not translate it, cuts it at 40 characters, and
|
|
795
|
+
may shorten it further. A fifth entry is left out.
|
|
796
|
+
- **It is screen state, not a toggle**, like the wallets: each call is the whole answer and replaces
|
|
797
|
+
the last. Call it again when the language changes or a screen has different options;
|
|
798
|
+
`gem.setMenu({})` takes everything of yours back out. Before the Arcade connects the menu is held
|
|
799
|
+
and sent when it does, and again after any reconnect.
|
|
800
|
+
- **The Arcade pauses your game while its menu is open** — `onPause(true)`, then `onPause(false)`
|
|
801
|
+
before a handler of yours runs. Nothing else is needed from you.
|
|
802
|
+
- **You cannot remove or reorder the Arcade's entries**, and Return to the Arcade is always there.
|
|
803
|
+
On a phone it **parks** your game, as above; on a desktop, a tablet and a television it ends the
|
|
804
|
+
session, with `onWillTerminate` first. Either way your game heard `onPause(true)` when the menu
|
|
805
|
+
opened: save there — it is the edge you are sure to get.
|
|
806
|
+
|
|
807
|
+
**Do not draw a settings button of your own while `gem.setMenu` returns `true`.** One menu, in one
|
|
808
|
+
place, in every game is the point. Draw your own **only when it returns `false` after the bridge has
|
|
809
|
+
settled** — an embedder with no menu, or your game running outside any embedder:
|
|
810
|
+
|
|
811
|
+
```js
|
|
812
|
+
await gem.awaitBridge();
|
|
813
|
+
ownSettingsButton.hidden = gem.setMenu({ onSettings: () => settingsScreen.open() });
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
Your own **back or home** button — the one that moves around inside your game — goes **top-left**,
|
|
817
|
+
which the Arcade [leaves clear](#the-arcades-own-controls-reserved-areas). It never leaves the game:
|
|
818
|
+
leaving is the Arcade's menu, so a player always knows which control does which.
|
|
819
|
+
|
|
696
820
|
## Sound: your own mute control, and the Arcade's
|
|
697
821
|
|
|
698
|
-
**Every game needs its own mute control**, in its settings
|
|
699
|
-
|
|
700
|
-
|
|
822
|
+
**Every game needs its own mute control**, in its settings screen. The Arcade's menu has a mute
|
|
823
|
+
entry on every device, but a player who opens your settings expects sound to be there too — and
|
|
824
|
+
under an embedder with no menu, yours is the only one there is.
|
|
701
825
|
|
|
702
826
|
Wire it both ways, so the player has one mute rather than two that disagree:
|
|
703
827
|
|
|
704
828
|
```js
|
|
705
829
|
const gem = createGem({
|
|
706
830
|
// ...
|
|
707
|
-
// The Arcade's mute changed
|
|
831
|
+
// The Arcade's mute changed: follow it.
|
|
708
832
|
onPrefs: (prefs) => sound.setMuted(prefs.muted),
|
|
709
833
|
});
|
|
710
834
|
|
|
@@ -724,14 +848,14 @@ sent when the Arcade connects; an embedder with no mute button is simply not tol
|
|
|
724
848
|
button saying one thing while the game does the other; a game that follows `prefs.muted` but never
|
|
725
849
|
reports leaves the Arcade's button stale after the player uses yours.
|
|
726
850
|
|
|
727
|
-
### The Arcade's mute
|
|
851
|
+
### The Arcade's mute entry
|
|
728
852
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
853
|
+
The Arcade's menu carries a mute entry on every device, for games whose SDK can honour it — an SDK
|
|
854
|
+
that silences your game says so when it connects, and a game built on an older one gets no entry
|
|
855
|
+
rather than one that does nothing. When the player presses it,
|
|
732
856
|
`gem.prefs.muted` arrives as `true` and the SDK silences your page itself — every Web Audio graph and
|
|
733
857
|
every `<audio>`/`<video>` element — then gives the sound back when they press it again. With the two
|
|
734
|
-
calls above, the Arcade's
|
|
858
|
+
calls above, the Arcade's entry and yours stay in step whichever the player uses.
|
|
735
859
|
|
|
736
860
|
- **It turns the volume down; it does not stop your clock.** Your `AudioContext` keeps running, so
|
|
737
861
|
anything scheduled against `currentTime` stays in time. Do not answer a mute with `ctx.suspend()` in
|
|
@@ -774,38 +898,25 @@ its music out than cut it, say. The Arcade still shows its button for your game
|
|
|
774
898
|
so this is a promise: your `onPrefs` handler must go quiet on `muted: true` and come back on `false`,
|
|
775
899
|
and the release checklist checks that it does.
|
|
776
900
|
|
|
777
|
-
## The Arcade's wallet:
|
|
901
|
+
## The Arcade's wallet: always shown, never drawn
|
|
778
902
|
|
|
779
903
|
The player's platform balances — Gems, Tickets (`ARCADE_TICKETS`) and their achievement score —
|
|
780
904
|
belong to the Arcade. **Your game never draws one**, and never shows a pending state for one: the
|
|
781
|
-
Arcade takes every figure from the platform and draws the wallets itself, in the
|
|
782
|
-
of your frame on a phone.
|
|
783
|
-
|
|
784
|
-
**
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
as often as you like: before the Arcade connects the state is held and sent when it does, and
|
|
797
|
-
again after any reconnect; a repeat of the current state is not re-sent.
|
|
798
|
-
- **Shown wallets take space; a pop does not.** Shown, they reserve the top-right
|
|
799
|
-
[corner](#the-arcades-own-controls-reserved-areas), so lay your menu out against
|
|
800
|
-
`--gem-reserved-tr-*` — your settings button sits below them; hidden, the corner is yours again and
|
|
801
|
-
the same `var()` moves the button up. A pop floats over your game and never moves its layout.
|
|
802
|
-
- **Narrowing is optional.** On a screen that sells in one currency — a ticket store — you may show
|
|
803
|
-
only that one: `gem.setWalletVisible(true, { only: ['ARCADE_TICKETS'] })`, with `'score'` for the
|
|
804
|
-
achievement score. Use it only there; leaving it out shows every wallet, which is always safe.
|
|
805
|
-
- **Where the Arcade docks its wallets beside your frame** — a desktop page — they are always on
|
|
806
|
-
show, and this changes nothing. Call it anyway; the same code is right everywhere.
|
|
807
|
-
- **`false` means nobody is listening** — no connection yet, or an embedder that draws no wallet.
|
|
808
|
-
Do nothing in its place.
|
|
905
|
+
Arcade takes every figure from the platform and draws the wallets itself, in the middle of the row
|
|
906
|
+
across the top of your frame on a phone, with its menu button at the right end.
|
|
907
|
+
|
|
908
|
+
**The Arcade shows every wallet the whole time, during play too** (since 2026-10-04). They are
|
|
909
|
+
small, translucent chips in the middle of the top row, and they are
|
|
910
|
+
[reserved rectangles](#the-arcades-own-controls-reserved-areas): lay every screen out below both
|
|
911
|
+
`--gem-reserved-tl-height` and `--gem-reserved-tr-height`, gameplay included. There is nothing for
|
|
912
|
+
your game to call.
|
|
913
|
+
|
|
914
|
+
⚠️ **`gem.setWalletVisible` no longer hides anything in the Arcade.** It was how a game hid the
|
|
915
|
+
wallets for gameplay and showed them on its menus, and narrowed them to one currency on a store
|
|
916
|
+
screen. The Arcade still accepts the call — it returns `true`, and existing games need no change —
|
|
917
|
+
but the wallets stay up whatever it is told. **Do not design a screen around the top row being
|
|
918
|
+
clear during play**: it is not. Another embedder may still honour the call, so leaving it in is
|
|
919
|
+
harmless; new code does not need it.
|
|
809
920
|
|
|
810
921
|
## No accidental text selection
|
|
811
922
|
|