@indiegems/gem-web-sdk 7.8.0 → 7.9.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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  An **achievement** is a platform-wide recognition a player earns in your game: a name, a
4
4
  description and some **achievement points**. The arcade lists your achievements on your store page, shows each player what they have unlocked, and
5
- adds their points to their **Achievement Score**, a total that spans every game they play (§5).
5
+ adds their points to their **Achievement Score**, a total that spans every game they play (§6).
6
6
 
7
7
  There is no achievements API to call. An achievement sits on a **progression threshold your game
8
8
  already has**, and it unlocks when your server, or a serverless function the player called, moves
@@ -194,7 +194,8 @@ first write, list them from your own game config.
194
194
 
195
195
  The client caches what it reads, and an unlock happens on your server, so the client does not see
196
196
  it by itself. When your server tells the game a player unlocked something, call
197
- `gem.me.inventory.refresh()` before reading the progression again.
197
+ `gem.me.inventory.refresh()` before reading the progression again. Celebrating that unlock is your
198
+ game's job: [§5](#5-celebrating-an-unlock-is-your-games-job) says how the game learns it happened.
198
199
 
199
200
  **This read is for display.** Never send it to your server as proof that a player holds something;
200
201
  a modified client can report anything. Your server reads the standing itself.
@@ -203,7 +204,102 @@ a modified client can report anything. Your server reads the standing itself.
203
204
  threshold — it is in your config. The arcade hides its name and description from players until they
204
205
  unlock it; if your game shows its own achievement list, do the same.
205
206
 
206
- ## 5. Points are prestige: the Achievement Score
207
+ ## 5. Celebrating an unlock is your game's job
208
+
209
+ By default the arcade draws **no** card when a player unlocks one of your game's achievements — not
210
+ during play, and not when they leave the game. Your game celebrates the unlock, in its own frame, in
211
+ its own art and sound. The arcade's record is the same either way: the unlock is recorded, its
212
+ points count toward the Achievement Score (§6), and the arcade's achievement lists show it. Only the
213
+ card over your game depends on what you declare.
214
+
215
+ ### If your game has no celebration of its own
216
+
217
+ Declare the arcade's card on your store page instead, and the arcade draws it for every unlock of
218
+ one of your game's achievements:
219
+
220
+ ```yaml
221
+ unlock_celebration: arcade
222
+ ```
223
+
224
+ - **Values are `game` and `arcade`.** `game` is the default, and leaving the line out means `game`.
225
+ Any other value is refused when the store page is uploaded.
226
+ - **It is one line, on any store page you already have.** It is optional in every store-page
227
+ `schema_version`, so adding it needs no `schema_version` change.
228
+ - **Only the store page on the `release` channel is read**, and the setting takes effect when that
229
+ store page is promoted to `release` — the same channel achievements unlock on (§2). A store page
230
+ on another channel, and an experiment variant, are accepted but never read.
231
+ - **It governs the card for your game's achievements only**, and it is display-only: it changes no
232
+ unlock, no points and no reward.
233
+ - **Not both.** A game that draws its own celebration and declares `arcade` shows the player two,
234
+ one over the other.
235
+
236
+ ### Platform milestones are always the arcade's
237
+
238
+ Platform milestones — the tiers of the player's Achievement Score, which belong to no game — always
239
+ get the arcade's card, whatever your store page declares. That includes a milestone an unlock in
240
+ your game reaches, and one reached in the same moment as an unlock the arcade does not show. Your
241
+ game neither celebrates nor suppresses a milestone. A reward one of your achievements grants gets no
242
+ arcade card of its own while your store page says `game`.
243
+
244
+ ### Knowing an unlock happened
245
+
246
+ There is no unlock message from the arcade to your game. Your game learns of an unlock one of two
247
+ ways.
248
+
249
+ **On your server — the reliable path.** The order your server gets back says what it unlocked
250
+ (§3). For each `unlocked` receipt, tell your game client over your game's own channel — the response
251
+ to the request the client made, or a match message — and celebrate it there:
252
+
253
+ ```js
254
+ const unlocked = achievementReceipts(order)
255
+ .filter((r) => r.outcome === 'unlocked')
256
+ .map((r) => ({ id: r.achievementId, points: r.points }));
257
+ // Send `unlocked` back in your own response; the client celebrates each one.
258
+ ```
259
+
260
+ **In the game — the compare.** Read the progression, and compare which achievements carry an
261
+ `unlockedAt` with what the game held at its last read: one that is newly present was just unlocked.
262
+ Read once when the game starts, before any write — that first read only records what the player
263
+ already holds, and celebrates nothing. Then run the compare after **every** response from your
264
+ game's own server that may have written the progression, not only after one that reported an
265
+ unlock, because the case it exists for is the report that never arrived:
266
+
267
+ ```js
268
+ let held = null; // ids the game has seen unlocked; null until the first good read
269
+
270
+ async function checkUnlocks() {
271
+ gem.me.inventory.refresh(); // drops the cache; it returns nothing to wait for
272
+ const p = await gem.me.progressions.get('boards_cleared');
273
+ if (p?.achievementsUnavailable) return; // this read says nothing: keep the last snapshot
274
+ const now = (p?.achievements ?? []).filter((a) => a.unlockedAt !== undefined && !a.revoked);
275
+ if (!held) { held = new Set(now.map((a) => a.id)); return; } // the seed celebrates nothing
276
+ for (const a of now) if (!held.has(a.id)) { held.add(a.id); celebrate(a); }
277
+ }
278
+
279
+ // After each response from your server. A failed read is caught by the next check.
280
+ checkUnlocks().catch(() => {});
281
+ ```
282
+
283
+ Keep one snapshot per progression that carries achievements. Only add to the snapshot, never replace
284
+ it, so a read that finishes late cannot bring back an unlock already celebrated. When
285
+ `achievementsUnavailable` is true the read says nothing about which achievements are unlocked, so it
286
+ is never a change. A start-up read that is unavailable does not seed: the first good read seeds, so
287
+ an unlock in that gap is not celebrated. The unlock is still recorded.
288
+
289
+ **When it can miss.** A receipt is a report, not a trigger (§3): a retried or replayed order reports
290
+ nothing, so a celebration keyed only on an `unlocked` receipt is missed whenever the first response
291
+ never arrived. The client compare catches that case. A missed celebration loses nothing else — the
292
+ unlock, its points and its rewards are already recorded.
293
+
294
+ ### What the celebration shows
295
+
296
+ - **The unlock is yours; a platform-currency payout is not.** Any Gems or Tickets the same moment
297
+ pays still go through `gem.cueReward`, with no Gems or Tickets flight, counter or count-up of your
298
+ own — [arcade-economy.md](arcade-economy.md) has that display rule.
299
+ - **The achievement's own `points` are yours to show.** The player's Achievement Score is not: your
300
+ game never computes or restates it (§6).
301
+
302
+ ## 6. Points are prestige: the Achievement Score
207
303
 
208
304
  Achievement points are a player's permanent standing on the arcade. They are **never spent,
209
305
  bought, boosted, decayed or expired**, and they are **removed only for fraud or moderation**.
@@ -1,6 +1,6 @@
1
1
  # The browser surface — API reference
2
2
 
3
- Generated from `@indiegems/gem-web-sdk/sdk` v7.8.0. Every symbol below links to its page on the Gem Arcade developer site.
3
+ Generated from `@indiegems/gem-web-sdk/sdk` v7.9.0. Every symbol below links to its page on the Gem Arcade developer site.
4
4
 
5
5
  `@indiegems/gem-web-sdk/sdk` is the one import a game writes, wherever its code runs. `@indiegems/gem-web-sdk/game` exports exactly the symbols on this page and nothing else, for a build that wants only this half.
6
6
 
@@ -4886,6 +4886,30 @@ wallet.
4886
4886
  **Returns.** whether the message left the frame — `false` before the bridge is
4887
4887
  up, for an arcade with no wallet, and past this frame's own limit.
4888
4888
 
4889
+ ## GemFrame.setMenu
4890
+
4891
+ [GemFrame.setMenu](https://gemarcade.com/developer/api/game#gemframe-setmenu)
4892
+
4893
+ ```ts
4894
+ setMenu(settings: boolean, items?: readonly { id: string; label: string }[]): boolean
4895
+ ```
4896
+
4897
+ Tell the arcade what your game adds to its menu: whether it has a settings
4898
+ screen, and up to four entries of its own — the whole answer each time, the
4899
+ latest one winning.
4900
+
4901
+ Prefer `gem.setMenu`, which holds the state across a reconnect and calls
4902
+ your handlers when the player chooses an entry. This one sends only while
4903
+ connected, and only to an arcade that draws a menu.
4904
+
4905
+ | Parameter | Type |
4906
+ | --- | --- |
4907
+ | `settings` | `boolean` |
4908
+ | `items` | `readonly { id: string; label: string }[]` |
4909
+
4910
+ **Returns.** whether the message left the frame — `false` before the bridge is
4911
+ up, for an arcade with no menu, and past this frame's own limit.
4912
+
4889
4913
  ## GemFrame.requestShare
4890
4914
 
4891
4915
  [GemFrame.requestShare](https://gemarcade.com/developer/api/game#gemframe-requestshare)
@@ -6769,8 +6793,8 @@ The unsafe margin on each edge of your frame.
6769
6793
  readonly reserved: readonly GemReservedArea[]
6770
6794
  ```
6771
6795
 
6772
- Rectangles of your frame the arcade's own controls cover — its Back tab in
6773
- the top-left corner, and the wallets in the top-right while they are shown (see
6796
+ Rectangles of your frame the arcade's own controls cover — its menu button
6797
+ in the top-right corner, and the wallets beside it while they are shown (see
6774
6798
  `setWalletVisible`). Keep essential UI and touch targets out of them. Empty
6775
6799
  when the arcade covers nothing.
6776
6800
 
@@ -6853,6 +6877,83 @@ matched exactly, and the literal `'score'` for the achievement score. Only
6853
6877
  for a screen about one currency, such as a ticket store; leave it out and
6854
6878
  every wallet shows, which is always safe.
6855
6879
 
6880
+ ## GameMenuItem
6881
+
6882
+ [GameMenuItem](https://gemarcade.com/developer/api/game#gamemenuitem)
6883
+
6884
+ ```ts
6885
+ interface GameMenuItem
6886
+ ```
6887
+
6888
+ One entry of your own in the arcade's menu.
6889
+
6890
+ ## GameMenuItem.id
6891
+
6892
+ [GameMenuItem.id](https://gemarcade.com/developer/api/game#gamemenuitem-id)
6893
+
6894
+ ```ts
6895
+ readonly id: string
6896
+ ```
6897
+
6898
+ Your own name for the entry, 1 to 32 characters, unique among your items.
6899
+ An entry with an empty or longer id is left out.
6900
+
6901
+ ## GameMenuItem.label
6902
+
6903
+ [GameMenuItem.label](https://gemarcade.com/developer/api/game#gamemenuitem-label)
6904
+
6905
+ ```ts
6906
+ readonly label: string
6907
+ ```
6908
+
6909
+ What the player reads, **already in the player's language**
6910
+ (`gem.prefs.locale`) — the arcade does not translate it. Keep it short:
6911
+ anything past 40 characters is cut, and the arcade may shorten it further
6912
+ to fit.
6913
+
6914
+ ## GameMenuItem.onSelect
6915
+
6916
+ [GameMenuItem.onSelect](https://gemarcade.com/developer/api/game#gamemenuitem-onselect)
6917
+
6918
+ ```ts
6919
+ readonly onSelect: () => void
6920
+ ```
6921
+
6922
+ Called when the player chooses this entry. The arcade has closed its menu
6923
+ and your game is running again by the time this fires.
6924
+
6925
+ ## GameMenu
6926
+
6927
+ [GameMenu](https://gemarcade.com/developer/api/game#gamemenu)
6928
+
6929
+ ```ts
6930
+ interface GameMenu
6931
+ ```
6932
+
6933
+ What your game adds to the arcade's menu — see `gem.setMenu`.
6934
+
6935
+ ## GameMenu.onSettings
6936
+
6937
+ [GameMenu.onSettings](https://gemarcade.com/developer/api/game#gamemenu-onsettings)
6938
+
6939
+ ```ts
6940
+ readonly onSettings?: () => void
6941
+ ```
6942
+
6943
+ Open your settings screen. Give this and the arcade's menu gains a
6944
+ Settings entry that calls it; leave it out and there is no such entry.
6945
+
6946
+ ## GameMenu.items
6947
+
6948
+ [GameMenu.items](https://gemarcade.com/developer/api/game#gamemenu-items)
6949
+
6950
+ ```ts
6951
+ readonly items?: readonly GameMenuItem[]
6952
+ ```
6953
+
6954
+ Up to four entries of your own, shown in this order below the arcade's.
6955
+ A fifth and later are left out.
6956
+
6856
6957
  ## GemTerminationNotice
6857
6958
 
6858
6959
  [GemTerminationNotice](https://gemarcade.com/developer/api/game#gemterminationnotice)
@@ -7566,12 +7667,13 @@ reportMuted(muted: boolean): void
7566
7667
  ```
7567
7668
 
7568
7669
  Tell the arcade the player muted or unmuted **inside your game**, so the
7569
- arcade's own mute button shows the same thing.
7670
+ arcade's own mute entry shows the same thing.
7570
7671
 
7571
- **Every game needs its own mute control**: on a phone the arcade shows no
7572
- mute button at all, so yours is the player's only one. Call it once at start-up with the
7672
+ **Every game needs its own mute control**, in its settings: the arcade's
7673
+ menu has a mute entry, and under an embedder with no menu yours is the
7674
+ player's only one. Call it once at start-up with the
7573
7675
  setting you saved, and again every time the player changes it — and pair it
7574
- with `onPrefs`, because a press of the arcade's button arrives there as
7676
+ with `onPrefs`, because a press of the arcade's entry arrives there as
7575
7677
  `prefs.muted`, and your own control should follow it. With both halves the
7576
7678
  player has one mute instead of two that disagree.
7577
7679
 
@@ -7667,8 +7769,9 @@ ticketStore.onOpen = () => gem.setWalletVisible(true, { only: ['ARCADE_TICKETS']
7667
7769
  replaces the last. While hidden, the arcade still pops a wallet up for a
7668
7770
  moment when something happens to it — a reward you cued, a spend, a
7669
7771
  landing — and your state never cancels that. Shown wallets cover part of
7670
- your frame's top-right corner, which arrives as a rectangle in
7671
- `gem.viewport.reserved`; hiding them gives that space back. A wallet the
7772
+ your frame's top-right corner, beside the arcade's menu button, which
7773
+ arrives as a rectangle in `gem.viewport.reserved`; hiding them gives that
7774
+ space back. A wallet the
7672
7775
  arcade pops up for a moment floats over your game and moves nothing.
7673
7776
 
7674
7777
  **Never draw a platform balance yourself**, and never show a pending state
@@ -7684,6 +7787,52 @@ for one: the arcade takes every figure from the platform. If this returns
7684
7787
  up (the state is held and sent when it connects) and under an embedder
7685
7788
  that draws no wallet.
7686
7789
 
7790
+ ## Gem.setMenu
7791
+
7792
+ [Gem.setMenu](https://gemarcade.com/developer/api/game#gem-setmenu)
7793
+
7794
+ ```ts
7795
+ setMenu(menu: GameMenu): boolean
7796
+ ```
7797
+
7798
+ Put your settings, and up to four entries of your own, in the arcade's
7799
+ menu. The arcade keeps one menu button over your game, in the top-right
7800
+ corner: it is how a player leaves, mutes, goes fullscreen and reports a
7801
+ problem — and, once you call this, how they open **your** settings.
7802
+
7803
+ ```js
7804
+ const inArcadeMenu = gem.setMenu({
7805
+ onSettings: () => settingsScreen.open(),
7806
+ items: [{ id: 'how-to-play', label: t('How to play'), onSelect: () => help.open() }],
7807
+ });
7808
+ ownSettingsButton.hidden = inArcadeMenu;
7809
+ ```
7810
+
7811
+ **Do not draw a settings button of your own while this returns `true`** —
7812
+ the arcade's menu is the one place a player looks, in every game. Draw
7813
+ your own only when it returns `false` once the bridge has settled
7814
+ (`await gem.awaitBridge()`): an embedder with no menu, or no embedder at
7815
+ all. The top-left corner is yours, for a back or home button that moves
7816
+ around inside your game.
7817
+
7818
+ **It is screen state, not a toggle.** Each call is the whole answer and
7819
+ replaces the last, so call it again when the entries change — a new
7820
+ language, a screen with different options. `gem.setMenu({})` takes
7821
+ everything of yours back out. The arcade's own entries are not yours to
7822
+ change, and Return to the Arcade is always among them.
7823
+
7824
+ **The arcade pauses your game while its menu is open** (`onPause`), and
7825
+ resumes it before a handler of yours runs. A handler opens a screen; it
7826
+ should not assume the game was paused by you.
7827
+
7828
+ | Parameter | Type | Description |
7829
+ | --- | --- | --- |
7830
+ | `menu` | `GameMenu` | Your settings handler and your own entries. |
7831
+
7832
+ **Returns.** `true` when the arcade was told now; `false` before the bridge is
7833
+ up (the menu is held and sent when it connects) and under an embedder
7834
+ that draws no menu.
7835
+
7687
7836
  ## Gem.exit
7688
7837
 
7689
7838
  [Gem.exit](https://gemarcade.com/developer/api/game#gem-exit)
@@ -1,6 +1,6 @@
1
1
  # The server surface — API reference
2
2
 
3
- Generated from `@indiegems/gem-web-sdk/sdk` v7.8.0. Every symbol below links to its page on the Gem Arcade developer site.
3
+ Generated from `@indiegems/gem-web-sdk/sdk` v7.9.0. Every symbol below links to its page on the Gem Arcade developer site.
4
4
 
5
5
  `@indiegems/gem-web-sdk/sdk` is the one import a game writes, wherever its code runs. `@indiegems/gem-web-sdk/server` exports exactly the symbols on this page and nothing else, for a build that wants only this half.
6
6
 
@@ -2,7 +2,7 @@
2
2
  "schema_version": 1,
3
3
  "source": {
4
4
  "repo": "IndieGems/gem-web-sdk",
5
- "version": "7.8.0"
5
+ "version": "7.9.0"
6
6
  },
7
7
  "entries": [
8
8
  {
@@ -6130,6 +6130,36 @@
6130
6130
  "defaultValue": null,
6131
6131
  "guides": []
6132
6132
  },
6133
+ {
6134
+ "id": "GemFrame.setMenu",
6135
+ "name": "setMenu",
6136
+ "anchor": "gemframe-setmenu",
6137
+ "group": "frame",
6138
+ "url": "https://gemarcade.com/developer/api/game#gemframe-setmenu",
6139
+ "signature": "setMenu(settings: boolean, items?: readonly { id: string; label: string }[]): boolean",
6140
+ "documented": true,
6141
+ "summary": "Tell the arcade what your game adds to its menu: whether it has a settings\nscreen, and up to four entries of its own — the whole answer each time, the\nlatest one winning.\n\nPrefer `gem.setMenu`, which holds the state across a reconnect and calls\nyour handlers when the player chooses an entry. This one sends only while\nconnected, and only to an arcade that draws a menu.",
6142
+ "deprecated": null,
6143
+ "params": [
6144
+ {
6145
+ "name": "settings",
6146
+ "type": "boolean",
6147
+ "optional": false,
6148
+ "summary": null
6149
+ },
6150
+ {
6151
+ "name": "items",
6152
+ "type": "readonly { id: string; label: string }[]",
6153
+ "optional": true,
6154
+ "summary": null
6155
+ }
6156
+ ],
6157
+ "returns": "whether the message left the frame — `false` before the bridge is\n up, for an arcade with no menu, and past this frame's own limit.",
6158
+ "throws": null,
6159
+ "example": null,
6160
+ "defaultValue": null,
6161
+ "guides": []
6162
+ },
6133
6163
  {
6134
6164
  "id": "GemFrame.requestShare",
6135
6165
  "name": "requestShare",
@@ -8188,7 +8218,7 @@
8188
8218
  "url": "https://gemarcade.com/developer/api/game#gemviewport-reserved",
8189
8219
  "signature": "readonly reserved: readonly GemReservedArea[]",
8190
8220
  "documented": true,
8191
- "summary": "Rectangles of your frame the arcade's own controls cover — its Back tab in\nthe top-left corner, and the wallets in the top-right while they are shown (see\n`setWalletVisible`). Keep essential UI and touch targets out of them. Empty\nwhen the arcade covers nothing.\n\nA stylesheet reads the per-corner custom properties the SDK stamps from\nthese instead — `--gem-reserved-tl-height` and friends.",
8221
+ "summary": "Rectangles of your frame the arcade's own controls cover — its menu button\nin the top-right corner, and the wallets beside it while they are shown (see\n`setWalletVisible`). Keep essential UI and touch targets out of them. Empty\nwhen the arcade covers nothing.\n\nA stylesheet reads the per-corner custom properties the SDK stamps from\nthese instead — `--gem-reserved-tl-height` and friends.",
8192
8222
  "deprecated": null,
8193
8223
  "params": [],
8194
8224
  "returns": null,
@@ -8316,6 +8346,125 @@
8316
8346
  "defaultValue": null,
8317
8347
  "guides": []
8318
8348
  },
8349
+ {
8350
+ "id": "GameMenuItem",
8351
+ "name": "GameMenuItem",
8352
+ "anchor": "gamemenuitem",
8353
+ "group": "frame",
8354
+ "url": "https://gemarcade.com/developer/api/game#gamemenuitem",
8355
+ "signature": "interface GameMenuItem",
8356
+ "documented": true,
8357
+ "summary": "One entry of your own in the arcade's menu.",
8358
+ "deprecated": null,
8359
+ "params": [],
8360
+ "returns": null,
8361
+ "throws": null,
8362
+ "example": null,
8363
+ "defaultValue": null,
8364
+ "guides": []
8365
+ },
8366
+ {
8367
+ "id": "GameMenuItem.id",
8368
+ "name": "id",
8369
+ "anchor": "gamemenuitem-id",
8370
+ "group": "frame",
8371
+ "url": "https://gemarcade.com/developer/api/game#gamemenuitem-id",
8372
+ "signature": "readonly id: string",
8373
+ "documented": true,
8374
+ "summary": "Your own name for the entry, 1 to 32 characters, unique among your items.\nAn entry with an empty or longer id is left out.",
8375
+ "deprecated": null,
8376
+ "params": [],
8377
+ "returns": null,
8378
+ "throws": null,
8379
+ "example": null,
8380
+ "defaultValue": null,
8381
+ "guides": []
8382
+ },
8383
+ {
8384
+ "id": "GameMenuItem.label",
8385
+ "name": "label",
8386
+ "anchor": "gamemenuitem-label",
8387
+ "group": "frame",
8388
+ "url": "https://gemarcade.com/developer/api/game#gamemenuitem-label",
8389
+ "signature": "readonly label: string",
8390
+ "documented": true,
8391
+ "summary": "What the player reads, **already in the player's language**\n(`gem.prefs.locale`) — the arcade does not translate it. Keep it short:\nanything past 40 characters is cut, and the arcade may shorten it further\nto fit.",
8392
+ "deprecated": null,
8393
+ "params": [],
8394
+ "returns": null,
8395
+ "throws": null,
8396
+ "example": null,
8397
+ "defaultValue": null,
8398
+ "guides": []
8399
+ },
8400
+ {
8401
+ "id": "GameMenuItem.onSelect",
8402
+ "name": "onSelect",
8403
+ "anchor": "gamemenuitem-onselect",
8404
+ "group": "frame",
8405
+ "url": "https://gemarcade.com/developer/api/game#gamemenuitem-onselect",
8406
+ "signature": "readonly onSelect: () => void",
8407
+ "documented": true,
8408
+ "summary": "Called when the player chooses this entry. The arcade has closed its menu\nand your game is running again by the time this fires.",
8409
+ "deprecated": null,
8410
+ "params": [],
8411
+ "returns": null,
8412
+ "throws": null,
8413
+ "example": null,
8414
+ "defaultValue": null,
8415
+ "guides": []
8416
+ },
8417
+ {
8418
+ "id": "GameMenu",
8419
+ "name": "GameMenu",
8420
+ "anchor": "gamemenu",
8421
+ "group": "frame",
8422
+ "url": "https://gemarcade.com/developer/api/game#gamemenu",
8423
+ "signature": "interface GameMenu",
8424
+ "documented": true,
8425
+ "summary": "What your game adds to the arcade's menu — see `gem.setMenu`.",
8426
+ "deprecated": null,
8427
+ "params": [],
8428
+ "returns": null,
8429
+ "throws": null,
8430
+ "example": null,
8431
+ "defaultValue": null,
8432
+ "guides": []
8433
+ },
8434
+ {
8435
+ "id": "GameMenu.onSettings",
8436
+ "name": "onSettings",
8437
+ "anchor": "gamemenu-onsettings",
8438
+ "group": "frame",
8439
+ "url": "https://gemarcade.com/developer/api/game#gamemenu-onsettings",
8440
+ "signature": "readonly onSettings?: () => void",
8441
+ "documented": true,
8442
+ "summary": "Open your settings screen. Give this and the arcade's menu gains a\nSettings entry that calls it; leave it out and there is no such entry.",
8443
+ "deprecated": null,
8444
+ "params": [],
8445
+ "returns": null,
8446
+ "throws": null,
8447
+ "example": null,
8448
+ "defaultValue": null,
8449
+ "guides": []
8450
+ },
8451
+ {
8452
+ "id": "GameMenu.items",
8453
+ "name": "items",
8454
+ "anchor": "gamemenu-items",
8455
+ "group": "frame",
8456
+ "url": "https://gemarcade.com/developer/api/game#gamemenu-items",
8457
+ "signature": "readonly items?: readonly GameMenuItem[]",
8458
+ "documented": true,
8459
+ "summary": "Up to four entries of your own, shown in this order below the arcade's.\nA fifth and later are left out.",
8460
+ "deprecated": null,
8461
+ "params": [],
8462
+ "returns": null,
8463
+ "throws": null,
8464
+ "example": null,
8465
+ "defaultValue": null,
8466
+ "guides": []
8467
+ },
8319
8468
  {
8320
8469
  "id": "GemTerminationNotice",
8321
8470
  "name": "GemTerminationNotice",
@@ -9001,7 +9150,7 @@
9001
9150
  "url": "https://gemarcade.com/developer/api/game#gem-reportmuted",
9002
9151
  "signature": "reportMuted(muted: boolean): void",
9003
9152
  "documented": true,
9004
- "summary": "Tell the arcade the player muted or unmuted **inside your game**, so the\narcade's own mute button shows the same thing.\n\n**Every game needs its own mute control**: on a phone the arcade shows no\nmute button at all, so yours is the player's only one. Call it once at start-up with the\nsetting you saved, and again every time the player changes it — and pair it\nwith `onPrefs`, because a press of the arcade's button arrives there as\n`prefs.muted`, and your own control should follow it. With both halves the\nplayer has one mute instead of two that disagree.\n\n```js\nconst gem = createGem({\n // ...\n onPrefs: (prefs) => sound.setMuted(prefs.muted),\n});\ngem.reportMuted(sound.muted); // at start-up, with the saved setting\nmuteButton.onclick = () => {\n sound.setMuted(!sound.muted);\n gem.reportMuted(sound.muted); // and on every change\n};\n```\n\nSafe to call at any time. Before the bridge is up the value is held and sent\nwhen it connects; an arcade that does not mirror a game's mute is simply not\ntold; and calling it from `onPrefs` is fine — a report that matches what the\narcade just said is not sent.",
9153
+ "summary": "Tell the arcade the player muted or unmuted **inside your game**, so the\narcade's own mute entry shows the same thing.\n\n**Every game needs its own mute control**, in its settings: the arcade's\nmenu has a mute entry, and under an embedder with no menu yours is the\nplayer's only one. Call it once at start-up with the\nsetting you saved, and again every time the player changes it — and pair it\nwith `onPrefs`, because a press of the arcade's entry arrives there as\n`prefs.muted`, and your own control should follow it. With both halves the\nplayer has one mute instead of two that disagree.\n\n```js\nconst gem = createGem({\n // ...\n onPrefs: (prefs) => sound.setMuted(prefs.muted),\n});\ngem.reportMuted(sound.muted); // at start-up, with the saved setting\nmuteButton.onclick = () => {\n sound.setMuted(!sound.muted);\n gem.reportMuted(sound.muted); // and on every change\n};\n```\n\nSafe to call at any time. Before the bridge is up the value is held and sent\nwhen it connects; an arcade that does not mirror a game's mute is simply not\ntold; and calling it from `onPrefs` is fine — a report that matches what the\narcade just said is not sent.",
9005
9154
  "deprecated": null,
9006
9155
  "params": [
9007
9156
  {
@@ -9049,7 +9198,7 @@
9049
9198
  "url": "https://gemarcade.com/developer/api/game#gem-setwalletvisible",
9050
9199
  "signature": "setWalletVisible(visible: boolean, options?: WalletVisibleOptions): boolean",
9051
9200
  "documented": true,
9052
- "summary": "Show or hide the player's wallets — Gems, Tickets, the achievement score —\nthat the arcade draws over your game. **They are shown until you say\notherwise**: hide them during gameplay, show them again on your menus.\n\n```js\nlevel.onStart = () => gem.setWalletVisible(false);\nmenu.onOpen = () => gem.setWalletVisible(true);\n// A screen that sells in one currency may narrow it:\nticketStore.onOpen = () => gem.setWalletVisible(true, { only: ['ARCADE_TICKETS'] });\n```\n\n**It is screen state, not a toggle.** Each call is the whole answer and\nreplaces the last. While hidden, the arcade still pops a wallet up for a\nmoment when something happens to it — a reward you cued, a spend, a\nlanding — and your state never cancels that. Shown wallets cover part of\nyour frame's top-right corner, which arrives as a rectangle in\n`gem.viewport.reserved`; hiding them gives that space back. A wallet the\narcade pops up for a moment floats over your game and moves nothing.\n\n**Never draw a platform balance yourself**, and never show a pending state\nfor one: the arcade takes every figure from the platform. If this returns\n`false`, do nothing in its place.",
9201
+ "summary": "Show or hide the player's wallets — Gems, Tickets, the achievement score —\nthat the arcade draws over your game. **They are shown until you say\notherwise**: hide them during gameplay, show them again on your menus.\n\n```js\nlevel.onStart = () => gem.setWalletVisible(false);\nmenu.onOpen = () => gem.setWalletVisible(true);\n// A screen that sells in one currency may narrow it:\nticketStore.onOpen = () => gem.setWalletVisible(true, { only: ['ARCADE_TICKETS'] });\n```\n\n**It is screen state, not a toggle.** Each call is the whole answer and\nreplaces the last. While hidden, the arcade still pops a wallet up for a\nmoment when something happens to it — a reward you cued, a spend, a\nlanding — and your state never cancels that. Shown wallets cover part of\nyour frame's top-right corner, beside the arcade's menu button, which\narrives as a rectangle in `gem.viewport.reserved`; hiding them gives that\nspace back. A wallet the\narcade pops up for a moment floats over your game and moves nothing.\n\n**Never draw a platform balance yourself**, and never show a pending state\nfor one: the arcade takes every figure from the platform. If this returns\n`false`, do nothing in its place.",
9053
9202
  "deprecated": null,
9054
9203
  "params": [
9055
9204
  {
@@ -9071,6 +9220,30 @@
9071
9220
  "defaultValue": null,
9072
9221
  "guides": []
9073
9222
  },
9223
+ {
9224
+ "id": "Gem.setMenu",
9225
+ "name": "setMenu",
9226
+ "anchor": "gem-setmenu",
9227
+ "group": "start",
9228
+ "url": "https://gemarcade.com/developer/api/game#gem-setmenu",
9229
+ "signature": "setMenu(menu: GameMenu): boolean",
9230
+ "documented": true,
9231
+ "summary": "Put your settings, and up to four entries of your own, in the arcade's\nmenu. The arcade keeps one menu button over your game, in the top-right\ncorner: it is how a player leaves, mutes, goes fullscreen and reports a\nproblem — and, once you call this, how they open **your** settings.\n\n```js\nconst inArcadeMenu = gem.setMenu({\n onSettings: () => settingsScreen.open(),\n items: [{ id: 'how-to-play', label: t('How to play'), onSelect: () => help.open() }],\n});\nownSettingsButton.hidden = inArcadeMenu;\n```\n\n**Do not draw a settings button of your own while this returns `true`** —\nthe arcade's menu is the one place a player looks, in every game. Draw\nyour own only when it returns `false` once the bridge has settled\n(`await gem.awaitBridge()`): an embedder with no menu, or no embedder at\nall. The top-left corner is yours, for a back or home button that moves\naround inside your game.\n\n**It is screen state, not a toggle.** Each call is the whole answer and\nreplaces the last, so call it again when the entries change — a new\nlanguage, a screen with different options. `gem.setMenu({})` takes\neverything of yours back out. The arcade's own entries are not yours to\nchange, and Return to the Arcade is always among them.\n\n**The arcade pauses your game while its menu is open** (`onPause`), and\nresumes it before a handler of yours runs. A handler opens a screen; it\nshould not assume the game was paused by you.",
9232
+ "deprecated": null,
9233
+ "params": [
9234
+ {
9235
+ "name": "menu",
9236
+ "type": "GameMenu",
9237
+ "optional": false,
9238
+ "summary": "Your settings handler and your own entries."
9239
+ }
9240
+ ],
9241
+ "returns": "`true` when the arcade was told now; `false` before the bridge is\n up (the menu is held and sent when it connects) and under an embedder\n that draws no menu.",
9242
+ "throws": null,
9243
+ "example": null,
9244
+ "defaultValue": null,
9245
+ "guides": []
9246
+ },
9074
9247
  {
9075
9248
  "id": "Gem.exit",
9076
9249
  "name": "exit",
package/docs/changelog.md CHANGED
@@ -47,7 +47,7 @@ Upgrading from 7.3.1. Impact: required.
47
47
  ### Must change
48
48
 
49
49
  - The Arcade shows its wallets, Gems, Tickets and the Achievement Score, over your game by default. Call gem.setWalletVisible(false) when gameplay starts and gem.setWalletVisible(true) on your menus. On a screen that sells in one currency, show just that wallet by passing its slug in the only option.
50
- - On phones the Arcade's Back tab and wallets sit in the frame's top-left corner, and gem.viewport now reports that corner in reserved instead of folding it into the top inset. Keep the corner clear by reading gem.viewport.reserved or the --gem-reserved-tl-width and --gem-reserved-tl-height CSS variables. Put your settings top-right, and draw no back arrow of your own top-left.
50
+ - On phones the Arcade's Back tab and wallets sit in the frame's top-left corner, and gem.viewport now reports that corner in reserved instead of folding it into the top inset. Keep the corner clear by reading gem.viewport.reserved or the --gem-reserved-tl-width and --gem-reserved-tl-height CSS variables. Put your settings top-right.
51
51
  - GemFunctionError's retryAfterSeconds is now read-only. Code that assigns to it no longer compiles.
52
52
 
53
53
  ### Rebuild
@@ -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 or a history.
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, and it is shown on platform surfaces.** Your game reports progress
546
- and does not read the streak back — see below.
547
- - **Whether a player's streak is shown at all can depend on who they are.** Some players do not see
548
- streak surfaces. Do not design a mechanic whose fairness depends on every player seeing one.
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. The arcade is what displays those — the marker on your game's card, and the player's own
580
- progress, history and streak on platform surfaces. Your game does not have to render any of it.
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`. Mark a cosmetic in the
213
- config, and price it in `ARCADE_TICKETS`:
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: