@indiegems/gem-web-sdk 13.0.0 → 13.1.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.
@@ -708,46 +708,54 @@ the `var()` above, or read `gem.viewport`. The release checklist looks for the b
708
708
  The Arcade keeps **one control of its own over every game**, on every device — a phone's full-bleed
709
709
  game, a framed or full-window game on a desktop, a tablet or a TV in native fullscreen:
710
710
 
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).
711
+ - **The Gem button**, the Gem Arcade's gem, always there (a touch target, about 50px across), in
712
+ the **top-left corner** unless your game [asks for another](#moving-the-gem-gemsetgemcorner). It
713
+ opens [the Arcade's menu](#the-arcades-menu): Return to the Arcade, the player's Gems, Tickets and
714
+ achievement score, mute, the player's Arcade-wide settings and the rest. Not your settings: those
715
+ are [your own button](#game-settings).
715
716
  - **Nothing else.** The player's wallets (Gems, Tickets, their achievement score) are **not** drawn
716
717
  over your game: the player opens the Gem menu to see them, and every platform-currency payout
717
718
  flies into the Gem button ([the Arcade's wallet](#the-arcades-wallet-in-the-gem-menu-never-drawn-by-you)).
718
719
  When the player switches the frame-rate readout on, it sits beside the gem and is reserved the
719
720
  same way.
720
721
 
721
- **The top of the screen has one layout, in every game:**
722
+ ### The top of the screen
722
723
 
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)). |
724
+ **The top of the screen has one layout, in every game, wherever the Gem sits.** A player learns
725
+ where "back" and "settings" are once, across every game:
729
726
 
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.
727
+ | Where | Whose | What goes there |
728
+ | ------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
729
+ | **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. |
730
+ | **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. |
731
+ | **Centre of the top row** | yours | Your **information**: a title, score, moves, a timer, a level name. |
733
732
 
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.
733
+ - **Back / Home is drawn when a screen needs it, always in that spot.** It moves around _inside_
734
+ your game — back to your level select or title screen — and never leaves it: leaving is the
735
+ Arcade's menu, so a player always knows which control does which.
736
+ - **Whichever of your controls shares the Gem's corner sits level with the Gem, on the side toward
737
+ the centre.** With the Gem top-left, nothing of yours does. With the Gem top-right, your Back and
738
+ settings sit just left of it.
739
+ - **Your settings button is there on every screen.** It goes top-right even where no Arcade control
740
+ is: another embedder may keep one in that corner, so place it from the reserved areas below and it
741
+ moves aside for whatever is there.
737
742
 
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.
743
+ The parts of your frame under the Gem and any other Arcade control are **reserved**: keep
744
+ essential UI and every touch target out of them. **Never hard-code their size or position.** They
745
+ differ by phone, orientation and mode. The Gem sits at the edge of the safe area and never rises
746
+ into the camera band, but that edge is somewhere else on every device — read them, as below.
741
747
 
742
748
  An inset cannot say that — as a left or top inset, a button a few dozen pixels wide would take a whole
743
749
  strip of your screen. So `gem.viewport` also carries the rectangles, in the same CSS pixels of your
744
750
  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:
751
+ readout while the player shows it. Each is grown out to the frame edges it sits nearest, because the
752
+ sliver between the Gem and the edge is no place for a button either. On a notched phone in portrait,
753
+ with the Gem top-left:
746
754
 
747
755
  ```json
748
756
  {
749
757
  "insets": { "top": 47, "right": 0, "bottom": 34, "left": 0 },
750
- "reserved": [{ "x": 8, "y": 55, "width": 52, "height": 52 }]
758
+ "reserved": [{ "x": 0, "y": 0, "width": 60, "height": 99 }]
751
759
  }
752
760
  ```
753
761
 
@@ -755,35 +763,37 @@ readout while the player shows it:
755
763
  `--gem-safe-*` before anything else hears the push: `--gem-reserved-tl-width` and
756
764
  `--gem-reserved-tl-height`, and the same pair for `tr`, `bl` and `br` — how far that corner's
757
765
  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:
766
+ nearest to. Lay the top row out between the two top corners, and it is right wherever the Gem is:
759
767
 
760
768
  ```css
761
- /* ONE Home or Back button, level with the Gem, just to its right */
762
- #back {
769
+ /* The top row: clear of whatever the Arcade reserves in either top corner */
770
+ #top-row {
771
+ position: absolute;
763
772
  top: var(--gem-safe-top, env(safe-area-inset-top, 0px));
764
773
  left: max(var(--gem-safe-left, 0px), var(--gem-reserved-tl-width, 0px));
774
+ right: max(var(--gem-safe-right, 0px), var(--gem-reserved-tr-width, 0px));
775
+ display: flex;
776
+ align-items: center;
777
+ gap: 16px;
765
778
  }
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);
779
+ /* your information takes the middle; Back, then settings, at the right end */
780
+ #top-row .info {
781
+ flex: 1;
782
+ text-align: center;
774
783
  }
775
784
  ```
776
785
 
777
786
  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.
787
+ them. To know whether your top row fits between the Arcade's controls, read the rectangles from
788
+ script: the room is what lies between the top rectangles on the left and those on the right.
781
789
 
782
790
  ```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);
791
+ const w = window.innerWidth;
792
+ const top = (gem.viewport?.reserved ?? []).filter((r) => r.y === 0);
793
+ const isLeft = (r) => r.x + r.width / 2 < w / 2;
794
+ const from = Math.max(0, ...top.filter(isLeft).map((r) => r.x + r.width));
795
+ const to = Math.min(w, ...top.filter((r) => !isLeft(r)).map((r) => r.x));
796
+ row.classList.toggle('below-arcade-row', to - from < row.scrollWidth + 8);
787
797
  ```
788
798
 
789
799
  All eight properties are written on every push, `0px` for a clear corner, so a rotation never leaves
@@ -791,8 +801,39 @@ a stale value; they are never set under an embedder that sends no viewport, whic
791
801
  fallback is for. `gem.viewport.reserved` is `[]` when nothing is covered.
792
802
 
793
803
  **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.
804
+ session, and it moves with a rotation, a resize, the frame-rate readout and a change of corner. Read
805
+ every corner, not only the one you asked for: a layout that honours all four is right under any
806
+ embedder.
807
+
808
+ ### Moving the Gem: `gem.setGemCorner`
809
+
810
+ **Leave the Gem top-left unless your own HUD needs that corner.** If it does, ask for another:
811
+
812
+ ```js
813
+ // This game's minimap needs the top-left, so the Gem goes bottom-left.
814
+ gem.setGemCorner('bottom-left'); // 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
815
+ ```
816
+
817
+ - **One corner per game**, at most one per orientation (one in portrait, another in landscape).
818
+ Call it once at boot, and again on a rotation if the two differ. A Gem that moves from screen to
819
+ screen is a way out the player has to look for.
820
+ - **In landscape the bottom corners are where a player's thumbs hold the sticks.** Ask for one only
821
+ if your controls do not live there.
822
+ - **Lay out around the reserved rectangles you receive, never around the corner you asked for.**
823
+ The Arcade moves the Gem on its own terms: not while the player is using it or its menu is open,
824
+ no more often than every few seconds, and a corner the player chooses for themselves, should the
825
+ Arcade ever offer that, wins over yours. The rectangles — and the `--gem-reserved-*` properties —
826
+ say where the Gem is now; a game that reads them is right whatever the Arcade decided, and a game
827
+ that assumed its corner is not.
828
+ - **The top row does not change.** Back and settings stay top-right and information top-centre,
829
+ wherever the Gem goes ([the top of the screen](#the-top-of-the-screen)).
830
+ - **It is screen state, not a toggle**: each call is the whole answer, held before the Arcade
831
+ connects and sent again after any reconnect. A value that is not one of the four corners is
832
+ ignored, with a warning on the console.
833
+
834
+ `gem.setGemCorner` returns `true` when the Arcade was told, and `false` before it connects and
835
+ under an embedder that cannot move its Gem. On `false` the Gem stays where the embedder puts it,
836
+ and the rectangles say where that is: there is nothing else to do.
796
837
 
797
838
  **Older SDKs get it folded into the top inset.** A game on an SDK from before reserved areas cannot
798
839
  read the rectangles, so the arcade sends it none and instead raises `insets.top` — and so
@@ -900,8 +941,8 @@ This is the whole model, and every other page that mentions settings links here:
900
941
  them from `gem.prefs.recommended` (and `muted` and `reducedMotion` from `gem.prefs`) and applies
901
942
  them again on `onPrefs`. The Arcade never asks your game anything about them, and never shows
902
943
  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.
944
+ - **Every game draws its own settings button, top-right ([the top of the
945
+ screen](#the-top-of-the-screen)), and its own settings screen.** The Arcade's menu never opens it.
905
946
  - **A setting starts from `recommended` until the player changes it in your game, and then the
906
947
  player's choice in your game wins.** Save only what the player chose, never a recommended value.
907
948
  - **Where a choice is kept.** Device configuration — volume, graphics quality, look sensitivity, a
@@ -914,17 +955,15 @@ This is the whole model, and every other page that mentions settings links here:
914
955
  while `gem.vibration.available` ([vibration](vibration.md)).
915
956
 
916
957
  **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:
958
+ to look for it, with your Back button just to its left when a screen has one. Place it with the
959
+ reserved-area properties so it never sits under an Arcade control: if the Gem is in that corner, the
960
+ button sits level with it, just inside it.
919
961
 
920
962
  ```css
921
963
  #settings-button {
922
964
  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);
965
+ top: var(--gem-safe-top, env(safe-area-inset-top, 0px));
966
+ right: max(var(--gem-safe-right, 0px), var(--gem-reserved-tr-width, 0px));
928
967
  }
929
968
  ```
930
969
 
@@ -1143,7 +1182,8 @@ What it does, so you know what to expect:
1143
1182
 
1144
1183
  ## The Arcade's menu
1145
1184
 
1146
- The Arcade keeps one button over every game, on every device: the Gem, at the top-left. Its menu is
1185
+ The Arcade keeps one button over every game, on every device: the Gem, top-left unless your game
1186
+ [asks for another corner](#moving-the-gem-gemsetgemcorner). Its menu is
1147
1187
  how a player leaves (**Return to the Arcade**), sees their Gems, Tickets and achievement score, mutes,
1148
1188
  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
1189
  four entries of your own:
@@ -1175,9 +1215,9 @@ gem.setMenu({
1175
1215
  settings button is there either way; what such a game also needs is a way out of its own, a control
1176
1216
  that calls `gem.exit()`.
1177
1217
 
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.
1218
+ Your own **back or home** button — the one that moves around inside your game — goes **top-right,
1219
+ just left of your settings button** ([the top of the screen](#the-top-of-the-screen)). It never
1220
+ leaves the game: leaving is the Arcade's menu, so a player always knows which control does which.
1181
1221
 
1182
1222
  ## Sound: your own mute control, and the Arcade's
1183
1223
 
@@ -1267,8 +1307,8 @@ belong to the Arcade. **Your game never draws one**, and never shows a pending s
1267
1307
  Arcade takes every figure from the platform and shows them itself.
1268
1308
 
1269
1309
  **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
1310
+ row, at rest or when a balance moves. The player opens the Gem menu — the Gem button, in whichever
1311
+ corner it sits — to see their Gems, Tickets and achievement score, and every payout
1272
1312
  [`gem.cueReward`](inventory-and-catalogs.md#showing-the-reward-land-in-the-arcades-wallet) announces
1273
1313
  flies into the Gem button. A payout you do not cue flies too, from the middle of your frame, so cue
1274
1314
  the ones the player earns on screen and the flight starts where they are looking.
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.1.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,8 @@
40
40
  "import": "./dist/gem-web-sdk.quality.js"
41
41
  },
42
42
  "./schema": "./schema/bridge.v1.json",
43
- "./brand/*": "./brand/*"
43
+ "./brand/*": "./brand/*",
44
+ "./rigs/*": "./rigs/*"
44
45
  },
45
46
  "files": [
46
47
  "bin",
@@ -50,6 +51,7 @@
50
51
  "schema/bridge.v1.json",
51
52
  "schema/capabilities.public.json",
52
53
  "brand/*.png",
54
+ "rigs/*.glb",
53
55
  "docs/*.md",
54
56
  "docs/manifest.json",
55
57
  "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",
@@ -236,6 +236,12 @@
236
236
  "gates": [
237
237
  "gem.controlsDeclare"
238
238
  ]
239
+ },
240
+ "gemCorner": {
241
+ "implemented": true,
242
+ "gates": [
243
+ "gem.gemCorner"
244
+ ]
239
245
  }
240
246
  }
241
247
  }