@indiegems/gem-web-sdk 13.0.0 → 13.2.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +4 -3
  3. package/dist/function.d.ts +2 -1
  4. package/dist/game.d.ts +37 -2
  5. package/dist/gem-web-sdk.bridge.js +1 -1
  6. package/dist/gem-web-sdk.esm.js +2 -2
  7. package/dist/gem-web-sdk.function.js +2 -2
  8. package/dist/gem-web-sdk.game.js +2 -2
  9. package/dist/gem-web-sdk.min.js +2 -2
  10. package/dist/gem-web-sdk.sdk.js +2 -2
  11. package/dist/gem-web-sdk.server-api.js +1 -1
  12. package/dist/gem-web-sdk.server.js +2 -2
  13. package/dist/index.d.ts +39 -5
  14. package/dist/sdk.d.ts +37 -2
  15. package/dist/server-api.d.ts +1 -0
  16. package/dist/server.d.ts +1 -0
  17. package/docs/analytics.md +6 -3
  18. package/docs/api/api-reference-function.md +38 -7
  19. package/docs/api/api-reference-game.md +381 -33
  20. package/docs/api/api-reference-server.md +23 -2
  21. package/docs/api/reference.json +401 -25
  22. package/docs/arcade-avatars.md +194 -26
  23. package/docs/brand-art.md +3 -3
  24. package/docs/changelog.md +30 -0
  25. package/docs/deprecation-policy.md +6 -0
  26. package/docs/getting-started.md +9 -5
  27. package/docs/inventory-and-catalogs.md +1 -1
  28. package/docs/manifest.json +29 -29
  29. package/docs/platform-moments.md +266 -0
  30. package/docs/rewarded-ads.md +26 -9
  31. package/docs/server-bundles.md +2 -2
  32. package/docs/serverless-functions-js.md +17 -1
  33. package/docs/serverless-functions.md +13 -7
  34. package/docs/what-a-game-must-do.md +126 -70
  35. package/package.json +6 -2
  36. package/rigs/adult_human.glb +0 -0
  37. package/schema/bridge.v1.json +160 -7
  38. package/schema/capabilities.public.json +16 -0
  39. package/schema/moment-log-line.json +109 -0
  40. package/docs/cross-promotion.md +0 -232
@@ -45,7 +45,7 @@ const link = await gem.match.connect(); // one link to the authority, whoever th
45
45
  No `baseUrl`. The Arcade names the backend on every token and the SDK routes there — see
46
46
  "which backend" below.
47
47
 
48
- ## The five things that are yours
48
+ ## The things that are yours
49
49
 
50
50
  ### 1. The embedder allowlist
51
51
 
@@ -154,6 +154,28 @@ The SDK reports; it does not decide. A stale build (`versionUpdateAvailable`), a
154
154
  allocate, a token the parent refused — what a game does about each is a game decision, and a platform
155
155
  layer that acted would be making it for you.
156
156
 
157
+ ### 6. A platform moment at every break
158
+
159
+ **Every game signals a platform moment from every press that leaves a natural break** — Next on a
160
+ level-complete screen, Retry on a game-over screen, Continue on a match's results, Back to menu —
161
+ whether or not it has a shop of its own. The Arcade decides whether to show one of its own offers
162
+ over your paused game, and usually shows nothing:
163
+
164
+ ```js
165
+ async function onNextPressed() {
166
+ startLevelTransition(); // your own fade, in parallel
167
+ await gem.moments.signal({ kind: 'level_end' });
168
+ startNextLevel(); // settled either way; `shown` grants nothing
169
+ }
170
+ ```
171
+
172
+ The SDK cannot place this call for you, because only your game knows where its breaks are. **The
173
+ release checklist requires it** at every important break (MOM-01), and never at the wrong time —
174
+ on boot, on a timer, mid-input or twice at one break (MOM-02). The platform does not enforce it:
175
+ nothing refuses to publish or run a game without one. Nothing rejects either, so there is no error
176
+ branch to write. It is the platform's moment, never your own offer: your shop and your rewarded
177
+ buttons stay yours. [`platform-moments.md`](platform-moments.md) is the page.
178
+
157
179
  ## The boot phase: settle the bridge before the heavy bundle
158
180
 
159
181
  The invariant, stated plainly, because it is the hardest-won lesson in this document:
@@ -708,46 +730,54 @@ the `var()` above, or read `gem.viewport`. The release checklist looks for the b
708
730
  The Arcade keeps **one control of its own over every game**, on every device — a phone's full-bleed
709
731
  game, a framed or full-window game on a desktop, a tablet or a TV in native fullscreen:
710
732
 
711
- - **The Gem button**, the Gem Arcade's gem, in the **top-left corner**, always there (a touch target, about
712
- 50px across). It opens [the Arcade's menu](#the-arcades-menu): Return to the Arcade, the player's Gems,
713
- Tickets and achievement score, mute, the player's Arcade-wide settings and the rest. Not your
714
- settings: those are [your own button](#game-settings).
733
+ - **The Gem button**, the Gem Arcade's gem, always there (a touch target, about 50px across), in
734
+ the **top-left corner** unless your game [asks for another](#moving-the-gem-gemsetgemcorner). It
735
+ opens [the Arcade's menu](#the-arcades-menu): Return to the Arcade, the player's Gems, Tickets and
736
+ achievement score, mute, the player's Arcade-wide settings and the rest. Not your settings: those
737
+ are [your own button](#game-settings).
715
738
  - **Nothing else.** The player's wallets (Gems, Tickets, their achievement score) are **not** drawn
716
739
  over your game: the player opens the Gem menu to see them, and every platform-currency payout
717
740
  flies into the Gem button ([the Arcade's wallet](#the-arcades-wallet-in-the-gem-menu-never-drawn-by-you)).
718
741
  When the player switches the frame-rate readout on, it sits beside the gem and is reserved the
719
742
  same way.
720
743
 
721
- **The top of the screen has one layout, in every game:**
744
+ ### The top of the screen
722
745
 
723
- | Where | Whose | What goes there |
724
- | ------------------------------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
725
- | **Top-left corner** | the Arcade's | Its Gem button. Nothing of yours under it. |
726
- | **Top-left, right of the Gem** | yours | Your **Home / Back** buttons, drawn when a screen needs them, level with the Gem and just to its right. They move around _inside_ your game — back to your level select or title screen — and never leave it. |
727
- | **Centre of the top row** | yours | Your **information**: score, moves, a timer, a level name. |
728
- | **Top-right** | yours | Your **settings** button ([Game settings](#game-settings)). |
746
+ **The top of the screen has one layout, in every game, wherever the Gem sits.** A player learns
747
+ where "back" and "settings" are once, across every game:
729
748
 
730
- Keep the spot beside the Gem for Home / Back even on a screen that has neither today: a player
731
- learns where "back" is once, across every game, and information that sits there on one screen
732
- teaches them the wrong thing on the next.
749
+ | Where | Whose | What goes there |
750
+ | ------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
751
+ | **The Gem's corner** | the Arcade's | Its Gem button — top-left unless you [ask for another corner](#moving-the-gem-gemsetgemcorner). Nothing of yours under it. |
752
+ | **Top-right** | yours | Your **settings** button ([Game settings](#game-settings)) at the end of the row, and your **Back / Home** button just to its left, level with it and about 16px apart, so a fingertip lands on one and not the other. |
753
+ | **Centre of the top row** | yours | Your **information**: a title, score, moves, a timer, a level name. |
733
754
 
734
- **Your settings button goes top-right**, clear of any Arcade control: under the Gem Arcade there is
735
- none in that corner, but another embedder may keep one there. [Game settings](#game-settings) shows
736
- how to place it.
755
+ - **Back / Home is drawn when a screen needs it, always in that spot.** It moves around _inside_
756
+ your game — back to your level select or title screen — and never leaves it: leaving is the
757
+ Arcade's menu, so a player always knows which control does which.
758
+ - **Whichever of your controls shares the Gem's corner sits level with the Gem, on the side toward
759
+ the centre.** With the Gem top-left, nothing of yours does. With the Gem top-right, your Back and
760
+ settings sit just left of it.
761
+ - **Your settings button is there on every screen.** It goes top-right even where no Arcade control
762
+ is: another embedder may keep one in that corner, so place it from the reserved areas below and it
763
+ moves aside for whatever is there.
737
764
 
738
- Those rectangles of your frame are **reserved**: keep essential UI and every touch target out of
739
- them. **Never hard-code their size or position.** They differ by phone, and in native fullscreen the
740
- gem may rise into the camera band — read them, as below.
765
+ The parts of your frame under the Gem and any other Arcade control are **reserved**: keep
766
+ essential UI and every touch target out of them. **Never hard-code their size or position.** They
767
+ differ by phone, orientation and mode. The Gem sits at the edge of the safe area and never rises
768
+ into the camera band, but that edge is somewhere else on every device — read them, as below.
741
769
 
742
770
  An inset cannot say that — as a left or top inset, a button a few dozen pixels wide would take a whole
743
771
  strip of your screen. So `gem.viewport` also carries the rectangles, in the same CSS pixels of your
744
772
  frame, measured from its top-left corner — one for the Gem button, and one for the frame-rate
745
- readout while the player shows it:
773
+ readout while the player shows it. Each is grown out to the frame edges it sits nearest, because the
774
+ sliver between the Gem and the edge is no place for a button either. On a notched phone in portrait,
775
+ with the Gem top-left:
746
776
 
747
777
  ```json
748
778
  {
749
779
  "insets": { "top": 47, "right": 0, "bottom": 34, "left": 0 },
750
- "reserved": [{ "x": 8, "y": 55, "width": 52, "height": 52 }]
780
+ "reserved": [{ "x": 0, "y": 0, "width": 60, "height": 99 }]
751
781
  }
752
782
  ```
753
783
 
@@ -755,35 +785,37 @@ readout while the player shows it:
755
785
  `--gem-safe-*` before anything else hears the push: `--gem-reserved-tl-width` and
756
786
  `--gem-reserved-tl-height`, and the same pair for `tr`, `bl` and `br` — how far that corner's
757
787
  reserved rectangles reach in from its two edges. A rectangle belongs to the corner its centre lies
758
- nearest to. Under the Gem Arcade the Gem button is a top-left rectangle, so:
788
+ nearest to. Lay the top row out between the two top corners, and it is right wherever the Gem is:
759
789
 
760
790
  ```css
761
- /* ONE Home or Back button, level with the Gem, just to its right */
762
- #back {
791
+ /* The top row: clear of whatever the Arcade reserves in either top corner */
792
+ #top-row {
793
+ position: absolute;
763
794
  top: var(--gem-safe-top, env(safe-area-inset-top, 0px));
764
795
  left: max(var(--gem-safe-left, 0px), var(--gem-reserved-tl-width, 0px));
796
+ right: max(var(--gem-safe-right, 0px), var(--gem-reserved-tr-width, 0px));
797
+ display: flex;
798
+ align-items: center;
799
+ gap: 16px;
765
800
  }
766
- /* your information — score, moves, a timer — clear of every top corner the arcade reserves */
767
- #info {
768
- top: max(
769
- var(--gem-safe-top, env(safe-area-inset-top, 0px)),
770
- var(--gem-reserved-tr-height, 0px)
771
- );
772
- left: max(var(--gem-safe-left, 0px), var(--gem-reserved-tl-width, 0px));
773
- right: var(--gem-safe-right, 0px);
801
+ /* your information takes the middle; Back, then settings, at the right end */
802
+ #top-row .info {
803
+ flex: 1;
804
+ text-align: center;
774
805
  }
775
806
  ```
776
807
 
777
808
  The per-corner properties say how far a corner's rectangles reach, not how much room is left beside
778
- them. To know whether a row of your own buttons fits beside the Gem, read the rectangles from
779
- script: the room to the right of the top-left ones is the frame's width less the furthest right
780
- edge among them.
809
+ them. To know whether your top row fits between the Arcade's controls, read the rectangles from
810
+ script: the room is what lies between the top rectangles on the left and those on the right.
781
811
 
782
812
  ```js
783
- const reserved = gem.viewport?.reserved ?? [];
784
- const topLeft = reserved.filter((r) => r.x + r.width / 2 < window.innerWidth / 2);
785
- const from = topLeft.length ? Math.max(...topLeft.map((r) => r.x + r.width)) : 0;
786
- nav.classList.toggle('below-arcade-row', window.innerWidth - from < nav.offsetWidth + 8);
813
+ const w = window.innerWidth;
814
+ const top = (gem.viewport?.reserved ?? []).filter((r) => r.y === 0);
815
+ const isLeft = (r) => r.x + r.width / 2 < w / 2;
816
+ const from = Math.max(0, ...top.filter(isLeft).map((r) => r.x + r.width));
817
+ const to = Math.min(w, ...top.filter((r) => !isLeft(r)).map((r) => r.x));
818
+ row.classList.toggle('below-arcade-row', to - from < row.scrollWidth + 8);
787
819
  ```
788
820
 
789
821
  All eight properties are written on every push, `0px` for a clear corner, so a rotation never leaves
@@ -791,8 +823,39 @@ a stale value; they are never set under an embedder that sends no viewport, whic
791
823
  fallback is for. `gem.viewport.reserved` is `[]` when nothing is covered.
792
824
 
793
825
  **Lay out against it whenever it changes.** The Gem button's rectangle is there for the whole
794
- session, and it moves with a rotation, a resize or the frame-rate readout. Read every corner, not
795
- only the one this page names today: a layout that honours all four is right under any embedder.
826
+ session, and it moves with a rotation, a resize, the frame-rate readout and a change of corner. Read
827
+ every corner, not only the one you asked for: a layout that honours all four is right under any
828
+ embedder.
829
+
830
+ ### Moving the Gem: `gem.setGemCorner`
831
+
832
+ **Leave the Gem top-left unless your own HUD needs that corner.** If it does, ask for another:
833
+
834
+ ```js
835
+ // This game's minimap needs the top-left, so the Gem goes bottom-left.
836
+ gem.setGemCorner('bottom-left'); // 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
837
+ ```
838
+
839
+ - **One corner per game**, at most one per orientation (one in portrait, another in landscape).
840
+ Call it once at boot, and again on a rotation if the two differ. A Gem that moves from screen to
841
+ screen is a way out the player has to look for.
842
+ - **In landscape the bottom corners are where a player's thumbs hold the sticks.** Ask for one only
843
+ if your controls do not live there.
844
+ - **Lay out around the reserved rectangles you receive, never around the corner you asked for.**
845
+ The Arcade moves the Gem on its own terms: not while the player is using it or its menu is open,
846
+ no more often than every few seconds, and a corner the player chooses for themselves, should the
847
+ Arcade ever offer that, wins over yours. The rectangles — and the `--gem-reserved-*` properties —
848
+ say where the Gem is now; a game that reads them is right whatever the Arcade decided, and a game
849
+ that assumed its corner is not.
850
+ - **The top row does not change.** Back and settings stay top-right and information top-centre,
851
+ wherever the Gem goes ([the top of the screen](#the-top-of-the-screen)).
852
+ - **It is screen state, not a toggle**: each call is the whole answer, held before the Arcade
853
+ connects and sent again after any reconnect. A value that is not one of the four corners is
854
+ ignored, with a warning on the console.
855
+
856
+ `gem.setGemCorner` returns `true` when the Arcade was told, and `false` before it connects and
857
+ under an embedder that cannot move its Gem. On `false` the Gem stays where the embedder puts it,
858
+ and the rectangles say where that is: there is nothing else to do.
796
859
 
797
860
  **Older SDKs get it folded into the top inset.** A game on an SDK from before reserved areas cannot
798
861
  read the rectangles, so the arcade sends it none and instead raises `insets.top` — and so
@@ -900,8 +963,8 @@ This is the whole model, and every other page that mentions settings links here:
900
963
  them from `gem.prefs.recommended` (and `muted` and `reducedMotion` from `gem.prefs`) and applies
901
964
  them again on `onPrefs`. The Arcade never asks your game anything about them, and never shows
902
965
  anything your game says.
903
- - **Every game draws its own settings button, top-right, and its own settings screen.** The
904
- Arcade's menu never opens it.
966
+ - **Every game draws its own settings button, top-right ([the top of the
967
+ screen](#the-top-of-the-screen)), and its own settings screen.** The Arcade's menu never opens it.
905
968
  - **A setting starts from `recommended` until the player changes it in your game, and then the
906
969
  player's choice in your game wins.** Save only what the player chose, never a recommended value.
907
970
  - **Where a choice is kept.** Device configuration — volume, graphics quality, look sensitivity, a
@@ -914,17 +977,15 @@ This is the whole model, and every other page that mentions settings links here:
914
977
  while `gem.vibration.available` ([vibration](vibration.md)).
915
978
 
916
979
  **The button goes in the top-right corner**, the same corner in every game, so a player never has
917
- to look for it. Place it with the reserved-area properties so it never sits under an Arcade
918
- control, whatever the Arcade keeps in that corner on this device:
980
+ to look for it, with your Back button just to its left when a screen has one. Place it with the
981
+ reserved-area properties so it never sits under an Arcade control: if the Gem is in that corner, the
982
+ button sits level with it, just inside it.
919
983
 
920
984
  ```css
921
985
  #settings-button {
922
986
  position: absolute;
923
- top: max(
924
- var(--gem-safe-top, env(safe-area-inset-top, 0px)),
925
- var(--gem-reserved-tr-height, 0px)
926
- );
927
- right: var(--gem-safe-right, 0px);
987
+ top: var(--gem-safe-top, env(safe-area-inset-top, 0px));
988
+ right: max(var(--gem-safe-right, 0px), var(--gem-reserved-tr-width, 0px));
928
989
  }
929
990
  ```
930
991
 
@@ -1143,7 +1204,8 @@ What it does, so you know what to expect:
1143
1204
 
1144
1205
  ## The Arcade's menu
1145
1206
 
1146
- The Arcade keeps one button over every game, on every device: the Gem, at the top-left. Its menu is
1207
+ The Arcade keeps one button over every game, on every device: the Gem, top-left unless your game
1208
+ [asks for another corner](#moving-the-gem-gemsetgemcorner). Its menu is
1147
1209
  how a player leaves (**Return to the Arcade**), sees their Gems, Tickets and achievement score, mutes,
1148
1210
  goes fullscreen, reports a problem and changes their Arcade-wide settings — the ones that reach you as [`gem.prefs.recommended`](#game-settings). You can add up to
1149
1211
  four entries of your own:
@@ -1175,9 +1237,9 @@ gem.setMenu({
1175
1237
  settings button is there either way; what such a game also needs is a way out of its own, a control
1176
1238
  that calls `gem.exit()`.
1177
1239
 
1178
- Your own **back or home** button — the one that moves around inside your game — goes **top-left**,
1179
- which the Arcade [leaves clear](#the-arcades-own-controls-reserved-areas). It never leaves the game:
1180
- leaving is the Arcade's menu, so a player always knows which control does which.
1240
+ Your own **back or home** button — the one that moves around inside your game — goes **top-right,
1241
+ just left of your settings button** ([the top of the screen](#the-top-of-the-screen)). It never
1242
+ leaves the game: leaving is the Arcade's menu, so a player always knows which control does which.
1181
1243
 
1182
1244
  ## Sound: your own mute control, and the Arcade's
1183
1245
 
@@ -1267,8 +1329,8 @@ belong to the Arcade. **Your game never draws one**, and never shows a pending s
1267
1329
  Arcade takes every figure from the platform and shows them itself.
1268
1330
 
1269
1331
  **The Gem Arcade draws no wallet over your game** (since 2026-10-11). Nothing of it sits in your top
1270
- row, at rest or when a balance moves. The player opens the Gem menu — the Gem button at the
1271
- top-left — to see their Gems, Tickets and achievement score, and every payout
1332
+ row, at rest or when a balance moves. The player opens the Gem menu — the Gem button, in whichever
1333
+ corner it sits — to see their Gems, Tickets and achievement score, and every payout
1272
1334
  [`gem.cueReward`](inventory-and-catalogs.md#showing-the-reward-land-in-the-arcades-wallet) announces
1273
1335
  flies into the Gem button. A payout you do not cue flies too, from the middle of your frame, so cue
1274
1336
  the ones the player earns on screen and the flight starts where they are looking.
@@ -1611,20 +1673,14 @@ running entirely in the browser cannot offer competition, and read
1611
1673
  [its safety rule](async-opponents.md#7-draw-a-set-at-match-start) before you render anything a
1612
1674
  drawn record carries, because those strings were typed by another player.
1613
1675
 
1614
- ## A break in play, if you want one to be worth something
1615
-
1616
- Not on the list above either, and a game that never asks is never interrupted. If yours wants a
1617
- break to carry a promotion for another game on the Arcade, `gem.ads.requestAd` asks for one and the
1618
- Arcade draws it in its own chrome over your paused game.
1676
+ ## A rewarded button, if your game has a reward to offer for an ad
1619
1677
 
1620
- Three things before you reach for it: the answer is **usually no**, and no is the ordinary path
1621
- rather than a failure; nothing rejects, so there is no error branch to write; and `shown` grants
1622
- nothing, so a reward must never be handed out on the strength of it.
1623
- [`cross-promotion.md`](cross-promotion.md) is the page.
1678
+ Not on the list above either: a game with no rewarded button is complete without one. (A platform
1679
+ moment at every break is on it — [§6 above](#6-a-platform-moment-at-every-break).)
1624
1680
 
1625
- A button the player presses for a reward — a revive, an extra attempt — is the same call with
1626
- `format: 'rewarded'`, made from that press itself. The button says it is an ad and names the
1627
- reward, and the Arcade starts the ad as soon as the ask arrives. Your game config declares what
1681
+ A button the player presses for a reward — a revive, an extra attempt — is
1682
+ `gem.ads.requestAd({ placement, format: 'rewarded' })`, made from that press itself. The button
1683
+ says it is an ad and names the reward, and the Arcade starts the ad as soon as the ask arrives. Your game config declares what
1628
1684
  each such button grants, the platform writes the reward into your game's inventory, and your game
1629
1685
  reads it there rather than from the answer.
1630
1686
  [`rewarded-ads.md`](rewarded-ads.md) is that page.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indiegems/gem-web-sdk",
3
- "version": "13.0.0",
3
+ "version": "13.2.0",
4
4
  "description": "postMessage bridge contract between gem-web and browser-playable games",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -40,7 +40,9 @@
40
40
  "import": "./dist/gem-web-sdk.quality.js"
41
41
  },
42
42
  "./schema": "./schema/bridge.v1.json",
43
- "./brand/*": "./brand/*"
43
+ "./schema/moment-log-line.json": "./schema/moment-log-line.json",
44
+ "./brand/*": "./brand/*",
45
+ "./rigs/*": "./rigs/*"
44
46
  },
45
47
  "files": [
46
48
  "bin",
@@ -49,7 +51,9 @@
49
51
  "!dist/gem-web-sdk.devtools.min.js",
50
52
  "schema/bridge.v1.json",
51
53
  "schema/capabilities.public.json",
54
+ "schema/moment-log-line.json",
52
55
  "brand/*.png",
56
+ "rigs/*.glb",
53
57
  "docs/*.md",
54
58
  "docs/manifest.json",
55
59
  "docs/api/*.md",
Binary file
@@ -684,9 +684,9 @@
684
684
  "optional": true,
685
685
  "$comment": [
686
686
  "Every 3D character the player WEARS, one per RIG FAMILY (gem-api",
687
- "`avatars_3d`). A family is a rig contract —",
688
- "`small_human` is the first — and a player wears at most one character",
689
- "per family, so a game that accepts several families reads the one it",
687
+ "`avatars_3d`). A family is a rig contract — `small_human` and",
688
+ "`adult_human` today — and a player wears at most one character per",
689
+ "family, so a game that accepts several families reads the one it",
690
690
  "draws by `family`.",
691
691
  "",
692
692
  "THE FLAT FIELDS STAY, AND THEY ARE ONE OF THESE. `avatar3dId`,",
@@ -720,7 +720,7 @@
720
720
  "family": {
721
721
  "kind": "string",
722
722
  "maxLength": 32,
723
- "$comment": "The rig family (gem-api `family`), e.g. `small_human`. A lowercase code, checked by the frame rather than by a format here, so one family name this frame does not recognise costs the entry and never the message."
723
+ "$comment": "The rig family (gem-api `family`), e.g. `small_human` or `adult_human`. A lowercase code, checked by the frame rather than by a format here, so one family name this frame does not recognise costs the entry and never the message."
724
724
  },
725
725
  "id": {
726
726
  "kind": "string",
@@ -2681,8 +2681,8 @@
2681
2681
  "moved (or is about to move) a progression mapped onto a platform currency,",
2682
2682
  "and the game would like the arcade to show it landing: the currency flying",
2683
2683
  "from `origin` into the arcade's chrome — in the Gem Arcade, into its Gem",
2684
- "button at the frame's top-left — and the total moving when the platform",
2685
- "confirms the grant.",
2684
+ "button, in whichever corner of the frame it sits — and the total moving",
2685
+ "when the platform confirms the grant.",
2686
2686
  "",
2687
2687
  "A PRESENTATION CUE, NOT A CLAIM. Nothing here is trusted as a number. The",
2688
2688
  "amount that lands and the player's new total reach the parent from gem-api",
@@ -3008,6 +3008,53 @@
3008
3008
  "rateLimit": { "perMinute": 60 }
3009
3009
  },
3010
3010
 
3011
+ {
3012
+ "type": "gem.gemCorner",
3013
+ "direction": "frameToParent",
3014
+ "transport": "port",
3015
+ "$comment": [
3016
+ "The corner of the frame the game would like the arcade's own control",
3017
+ "over it to sit in - in the Gem Arcade, the Gem button that opens its",
3018
+ "menu. With no message at all it sits top-left, which is also the",
3019
+ "recommendation: a game asks for another corner only when its own HUD",
3020
+ "needs that one.",
3021
+ "",
3022
+ "SCREEN STATE, NOT A TOGGLE, like `gem.menu`. Each message is the whole",
3023
+ "answer and the latest one wins. The SDK holds it from before the bridge",
3024
+ "is up and re-sends it after every handshake, so a parent that reconnects",
3025
+ "puts the control where the game had asked.",
3026
+ "",
3027
+ "A REQUEST, APPLIED ON THE PARENT'S TERMS. The parent holds a move while",
3028
+ "the player is using the control or one of its surfaces is open, keeps a",
3029
+ "minimum dwell between moves and applies only the latest request after",
3030
+ "one, and puts a player's own choice of corner, should it ever offer one,",
3031
+ "above the game's. So a game never lays out around the corner it asked",
3032
+ "for: it lays out around the rectangles `gem.viewport` sends in",
3033
+ "`reserved`, which say where the control actually is.",
3034
+ "",
3035
+ "ONE ENUM, AND IT ONLY GROWS. A later anchor is a new value behind a",
3036
+ "capability of its own, sent only to a parent that advertised that",
3037
+ "capability, so `v` does not move; a parent that advertised only",
3038
+ "`gemCorner` is only ever sent these four.",
3039
+ "",
3040
+ "A PRESENTATION REQUEST ONLY, and not an authorization control. The game",
3041
+ "picks one of four places the parent defines, never a coordinate, a size,",
3042
+ "a visibility or a look; the parent's dwell and holds, not this rate",
3043
+ "limit, are what keep a game from making the control run away.",
3044
+ "",
3045
+ "Gated by `gemCorner`: sent only to a parent that advertised it. To any",
3046
+ "other, `gem.setGemCorner` returns false."
3047
+ ],
3048
+ "fields": {
3049
+ "corner": {
3050
+ "kind": "enum",
3051
+ "values": ["top-left", "top-right", "bottom-left", "bottom-right"],
3052
+ "$comment": "Physical, not logical: `left` is the frame's left edge in every language, so a right-to-left locale never mirrors a game's choice."
3053
+ }
3054
+ },
3055
+ "rateLimit": { "perMinute": 12 }
3056
+ },
3057
+
3011
3058
  {
3012
3059
  "type": "gem.requestShare",
3013
3060
  "direction": "frameToParent",
@@ -3948,7 +3995,7 @@
3948
3995
  "format": {
3949
3996
  "kind": "enum",
3950
3997
  "values": ["interstitial", "rewarded"],
3951
- "$comment": "A closed set, so a typo is refused at the boundary rather than left for the parent to interpret. `rewarded` IS SERVED WHERE THE PARENT SERVES IT, and answered `no_ad` where it does not. Either way a `shown` grants nothing - see gem.adResult's outcome: the platform grants a rewarded placement's reward itself, into the game's own inventory, against its own record. A game that grants a reward on an outcome has a reward any page that can post on this port can forge."
3998
+ "$comment": "A closed set, so a typo is refused at the boundary rather than left for the parent to interpret. `interstitial` IS DEPRECATED in favour of gem.signalMoment: it keeps working, a parent routes it into the same decision as a platform moment and records it as a legacy one, and 14.0.0 is the earliest SDK release that may stop sending it. `rewarded` IS SERVED WHERE THE PARENT SERVES IT, and answered `no_ad` where it does not. Either way a `shown` grants nothing - see gem.adResult's outcome: the platform grants a rewarded placement's reward itself, into the game's own inventory, against its own record. A game that grants a reward on an outcome has a reward any page that can post on this port can forge."
3952
3999
  }
3953
4000
  },
3954
4001
  "rateLimit": {
@@ -4046,6 +4093,112 @@
4046
4093
  },
4047
4094
  "rateLimit": { "perMinute": 12 }
4048
4095
  },
4096
+ {
4097
+ "type": "gem.signalMoment",
4098
+ "direction": "frameToParent",
4099
+ "transport": "port",
4100
+ "$comment": [
4101
+ "The game has reached a natural break in play - a level, a run or a match",
4102
+ "has ended, or the player is going back to a menu - and is leaving it.",
4103
+ "Gated by the `moments` capability.",
4104
+ "",
4105
+ "A SIGNAL, NOT AN ASK FOR ANYTHING. The parent decides, in its own",
4106
+ "server-side arbiter, whether to show one of ITS OWN offers over the paused",
4107
+ "game at this break - a break video, the app, a platform store offer, a",
4108
+ "subscription, another game - and almost always decides to show nothing.",
4109
+ "The game never names what it would like shown, and never routes its own",
4110
+ "offer through this: its own shop and its own rewarded buttons are its own",
4111
+ "business, on its own cadence, in its own UI.",
4112
+ "",
4113
+ "IT SUPERSEDES `gem.requestAd` WITH `format: 'interstitial'`, which keeps",
4114
+ "working through the N-1 window. A parent routes both into the same",
4115
+ "decision and holds ONE single-flight shared by both message types,",
4116
+ "because a hostile frame can post both at once.",
4117
+ "",
4118
+ "WHAT THE PARENT OWES, none of which this schema can enforce. Everything",
4119
+ "gem.requestAd's comment says of an interstitial holds here word for word:",
4120
+ "advertise `moments` as a deployment-wide fact; render in your own chrome",
4121
+ "over the game paused with `gem.pause`; answer every signal exactly once;",
4122
+ "settle every `shown: false` on a schedule that does not depend on which",
4123
+ "branch produced it; and treat every field as untrusted text. Two more:",
4124
+ "",
4125
+ " `kind` IS A LABEL AND DECIDES NOTHING OF VALUE. It is for analytics,",
4126
+ " the dev-host log and the release checklist. A frame-to-parent field is",
4127
+ " routing only (DESIGN.md section 9), so anything that matters - a",
4128
+ " multiplayer rule, say - is derived from the parent's own state, never",
4129
+ " from what the game says the break was. Re-validate it yourself: the",
4130
+ " frame posts on the port directly.",
4131
+ "",
4132
+ " REFUSE RATHER THAN QUEUE. A signal that arrives while the player has",
4133
+ " something of the parent's open, or while the parent itself has paused",
4134
+ " the game, is answered `shown: false`, through the same floor as every",
4135
+ " other no. It is never held to paint after the player closes something.",
4136
+ "",
4137
+ "THE `perMinute` BELOW BOUNDS A BUGGY GAME, NOT A HOSTILE ONE, for the",
4138
+ "reason gem.requestAd's gives. The SDK also refuses a second signal within",
4139
+ "10 s of the last answer and one before `gem.ready`, which is the sender",
4140
+ "keeping a promise, not a guarantee a parent holds."
4141
+ ],
4142
+ "fields": {
4143
+ "requestId": {
4144
+ "kind": "string",
4145
+ "maxLength": 64,
4146
+ "$comment": "Opaque and frame-chosen, and echo-only, exactly as gem.requestAd's. Never keyed on, and not safe to log unescaped."
4147
+ },
4148
+ "kind": {
4149
+ "kind": "enum",
4150
+ "values": ["level_end", "run_end", "match_end", "menu_return", "other_break"],
4151
+ "$comment": "Which break this is. CLOSED NOW, AND PINNED BY A TEST ON THE SDK SIDE: a new value on a frame-to-parent enum is breaking under DESIGN.md section 9, because an N-1 parent's generated validator refuses the whole message. `level_end` - a level, stage, wave or puzzle finished, won or lost. `run_end` - game over, death, or a run banked. `match_end` - a multiplayer round or match finished and the player is leaving its results. `menu_return` - the player chose to go back to the title, a main menu or level select; a return, never the first arrival. `other_break` - any other natural stop, defined by exclusion. A LABEL ONLY: it never decides anything of value on the platform."
4152
+ },
4153
+ "placement": {
4154
+ "kind": "string",
4155
+ "maxLength": 32,
4156
+ "optional": true,
4157
+ "$comment": "The game's own name for the screen it is leaving - 'world-1-3', 'results'. UNTRUSTED TEXT. Shown ONLY in a development host's log, so a reviewer can map moments to screens. NEVER FORWARDED TO THE PLATFORM, NEVER STORED, NEVER RENDERED TO A PLAYER AND NEVER SENT TO AN AD NETWORK: a game-authored string that fits this grammar fits a dashless UUID too. The SDK sends only `^[a-z0-9_-]{1,32}$`; codegen emits type and length only, so the charset is the sender's to keep and the receiver's to re-check."
4158
+ }
4159
+ },
4160
+ "rateLimit": {
4161
+ "perMinute": 6,
4162
+ "$comment": "As gem.requestAd's: bounds a BUGGY game, not a hostile one. Breaks in play come minutes apart; six a minute is generous for them and mean for a loop."
4163
+ }
4164
+ },
4165
+ {
4166
+ "type": "gem.momentResult",
4167
+ "direction": "parentToFrame",
4168
+ "transport": "port",
4169
+ "$comment": [
4170
+ "The answer to one gem.signalMoment. Gated by the `moments` capability.",
4171
+ "",
4172
+ "ONE BOOLEAN, AND NO REASON. `shown: true` - the parent painted something",
4173
+ "of its own over the paused game and it has closed. `shown: false` -",
4174
+ "nothing was painted, which is the ordinary answer. Either way the game",
4175
+ "carries on exactly as it would have; `shown` GRANTS NOTHING and tells the",
4176
+ "game nothing about what was shown or why nothing was.",
4177
+ "",
4178
+ "NO REASON, EVER, for the reason gem.adResult carries none: a `false`",
4179
+ "absorbs the feature being off, pacing, a new player's grace, an",
4180
+ "entitlement, an age and a consent state, and a reason naming any of them",
4181
+ "hands untrusted game code a fact about a person. The boolean is permanent",
4182
+ "for the same reason the ad pair's three values are.",
4183
+ "",
4184
+ "THE VALUE IS NOT THE WHOLE CHANNEL. As gem.adResult's comment says, a",
4185
+ "parent settles every `shown: false` on a schedule that does not depend on",
4186
+ "which branch produced it. `shown: true` is not delayed: its latency is a",
4187
+ "player reading a panel."
4188
+ ],
4189
+ "fields": {
4190
+ "requestId": {
4191
+ "kind": "string",
4192
+ "maxLength": 64,
4193
+ "$comment": "Echoes the gem.signalMoment that asked. Echo-only."
4194
+ },
4195
+ "shown": {
4196
+ "kind": "boolean",
4197
+ "$comment": "Whether the parent painted anything at this break. It grants nothing, and a game never branches anything of value on it."
4198
+ }
4199
+ },
4200
+ "rateLimit": { "perMinute": 12 }
4201
+ },
4049
4202
  {
4050
4203
  "type": "gem.requestSignIn",
4051
4204
  "direction": "frameToParent",
@@ -236,6 +236,22 @@
236
236
  "gates": [
237
237
  "gem.controlsDeclare"
238
238
  ]
239
+ },
240
+ "gemCorner": {
241
+ "implemented": true,
242
+ "gates": [
243
+ "gem.gemCorner"
244
+ ]
245
+ },
246
+ "moments": {
247
+ "implemented": true,
248
+ "gates": [
249
+ "gem.signalMoment",
250
+ "gem.momentResult",
251
+ {
252
+ "namespace": "gem.moments"
253
+ }
254
+ ]
239
255
  }
240
256
  }
241
257
  }