@indiegems/gem-web-sdk 12.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.
@@ -363,13 +363,18 @@ often rather than to retry harder.
363
363
  ## For something small, reach for settings instead
364
364
 
365
365
  `gem.me.settings` needs no declaration and is one call: `put(key, value)` with a **string**
366
- value, up to **8 KiB** each and **32** keys per player per game. It is the right home for a
367
- chosen difficulty, a control scheme, a level counter.
366
+ value, up to **8 KiB** each and **32** keys per player per game. It is the right home for a small
367
+ value that follows the player, such as a level counter, and for the player's settings that do too.
368
+
369
+ **Which store a setting goes in** ([game settings](what-a-game-must-do.md#game-settings) has the
370
+ whole model): Device configuration — volume, graphics quality, look sensitivity, a touch-stick
371
+ style — goes in `gem.deviceSettings`; a choice that must follow the player across devices —
372
+ difficulty, key bindings, accessibility toggles the game owns — goes in `gem.me.settings`. Either
373
+ way, save only what the player chose, never a value your game started from `gem.prefs.recommended`.
368
374
 
369
375
  Reach for a save when the payload is a document rather than a value — a world, an inventory, a
370
- replay. Something that belongs to **this device** rather than to the player — a volume, a graphics
371
- quality — belongs in neither; that is the next section. Progress a player would be upset to lose
372
- belongs in a save, and nowhere else.
376
+ replay. Device configuration belongs in neither; that is the next section. Progress a player would
377
+ be upset to lose belongs in a save, and nowhere else.
373
378
 
374
379
  ### Device settings: local to this device, and only that
375
380
 
@@ -380,9 +385,10 @@ browser storage cannot do this: every build of a web game runs on its own origin
380
385
  game kept in `localStorage` is gone the moment you publish again. The Arcade's own storage outlives
381
386
  your builds; that is the whole mechanism.
382
387
 
383
- **What goes here:** a master volume and the music/SFX mix, a graphics quality or resolution scale,
384
- a chosen input layout, a tip the player dismissed, a "skip intro" toggle. Configuration that is
385
- about **this screen and these speakers**.
388
+ **What goes here:** device configuration — a master volume and the music/SFX mix, a graphics
389
+ quality or resolution scale, a look sensitivity, a touch-stick style — and a tip the player
390
+ dismissed or a "skip intro" toggle. Configuration that is about **this screen, these speakers and
391
+ this input**. Key bindings and a difficulty follow the player, so they go in `gem.me.settings`.
386
392
 
387
393
  ⚠️ **What never goes here: progression, unlocks, currency, anything a player would mind losing,
388
394
  and anything that must follow them to another device.** This is not a save and it is not a
@@ -433,8 +439,8 @@ game, and a key you experiment with on `dev` cannot break a shipped build.
433
439
  | You want | Where it goes | Follows the player? |
434
440
  | -------- | ------------- | ------------------- |
435
441
  | a world, an inventory, a replay | `gem.me.saves` — a declared slot | yes |
436
- | a difficulty, a control scheme, a level counter | `gem.me.settings` — a string per key | yes |
437
- | a volume, a quality preset, a dismissed tip | `gem.deviceSettings` — one object per device | **no** |
442
+ | a difficulty, key bindings, an accessibility toggle your game owns, a level counter | `gem.me.settings` — a string per key | yes |
443
+ | a volume, a graphics quality, a look sensitivity, a touch-stick style, a dismissed tip | `gem.deviceSettings` — one object per device | **no** |
438
444
 
439
445
  ## Starting over as a new player while you test
440
446
 
@@ -80,8 +80,8 @@ To check the download yourself: the package npm serves is, byte for byte, the fi
80
80
  also publishes on its download host, with the hash beside it:
81
81
 
82
82
  ```bash
83
- curl -fsSL -O https://downloads.gemarcade.com/sdk/v12.0.0/gem-web-sdk.tgz
84
- curl -fsSL https://downloads.gemarcade.com/sdk/v12.0.0/gem-web-sdk.tgz.sri
83
+ curl -fsSL -O https://downloads.gemarcade.com/sdk/v13.1.0/gem-web-sdk.tgz
84
+ curl -fsSL https://downloads.gemarcade.com/sdk/v13.1.0/gem-web-sdk.tgz.sri
85
85
  openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
86
86
  ```
87
87
 
@@ -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
@@ -891,22 +932,38 @@ arrives.
891
932
  ## Game settings
892
933
 
893
934
  **Your settings belong to your game: draw a settings button of your own, and the screen it opens.**
894
- The Arcade does not put your settings in its menu any more — its menu holds the player's
895
- _Arcade-wide_ settings instead — so a game with no button of its own has settings nobody can reach.
896
-
897
- **Put the button in the top-right corner.** That is a recommendation, not a rule the Arcade
898
- enforces, but a player who finds settings in the same corner in every game never has to look. Place
899
- it with the reserved-area properties so it never sits under an Arcade control, whatever the Arcade
900
- keeps in that corner on this device:
935
+ The Arcade's menu never opens your settings — it holds the player's _Arcade-wide_ settings
936
+ instead — so a game with no button of its own has settings nobody can reach.
937
+
938
+ This is the whole model, and every other page that mentions settings links here:
939
+
940
+ - **The Arcade's settings are the player's Arcade-wide defaults for this device.** Your game reads
941
+ them from `gem.prefs.recommended` (and `muted` and `reducedMotion` from `gem.prefs`) and applies
942
+ them again on `onPrefs`. The Arcade never asks your game anything about them, and never shows
943
+ anything your game says.
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.
946
+ - **A setting starts from `recommended` until the player changes it in your game, and then the
947
+ player's choice in your game wins.** Save only what the player chose, never a recommended value.
948
+ - **Where a choice is kept.** Device configuration — volume, graphics quality, look sensitivity, a
949
+ touch-stick style — goes in `gem.deviceSettings`; a choice that must follow the player across
950
+ devices — difficulty, key bindings, accessibility toggles the game owns — goes in
951
+ `gem.me.settings`.
952
+ - **Mute and vibration are in your settings screen too**, wired both ways: your mute follows
953
+ `prefs.muted` and reports the player's own with `gem.reportMuted`
954
+ ([sound](#sound-your-own-mute-control-and-the-arcades)), and your vibration toggle shows only
955
+ while `gem.vibration.available` ([vibration](vibration.md)).
956
+
957
+ **The button goes in the top-right corner**, the same corner in every game, so a player never has
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.
901
961
 
902
962
  ```css
903
963
  #settings-button {
904
964
  position: absolute;
905
- top: max(
906
- var(--gem-safe-top, env(safe-area-inset-top, 0px)),
907
- var(--gem-reserved-tr-height, 0px)
908
- );
909
- 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));
910
967
  }
911
968
  ```
912
969
 
@@ -931,8 +988,8 @@ same push as `muted` and `reducedMotion`, and again whenever the player changes
931
988
  | `graphicsQualityAuto` | `true` / `false` | `true`: the Arcade picked that tier for this device, so you may adapt from it. `false`: the player picked it, so hold it. |
932
989
 
933
990
  `lookSensitivity` to `touchMovement` are the camera and controls settings: see
934
- [camera and look controls](#camera-and-look-controls) for telling the Arcade which of them your game
935
- uses, and for `gem.controls.look()`, which applies the two sensitivities and the invert for you. An
991
+ [camera and look controls](#camera-and-look-controls) for `gem.controls.look()`, which applies the
992
+ two sensitivities and the invert for you. An
936
993
  older Arcade sends `recommended` without them; the SDK then fills each with its default (`100`,
937
994
  `100`, `false`, `'floating'`). The last two are the starting graphics quality: see
938
995
  [graphics quality](#graphics-quality-the-arcade-picks-the-start-your-game-adapts). An older Arcade
@@ -981,8 +1038,11 @@ volumeSlider.oninput = () => {
981
1038
  };
982
1039
  ```
983
1040
 
984
- [Device settings](#device-settings-what-this-device-remembers-about-your-game) above is where the
985
- player's choices are kept between sessions.
1041
+ `chosen` here is this device's configuration, so it is kept in
1042
+ [device settings](#device-settings-what-this-device-remembers-about-your-game). A choice that must
1043
+ follow the player to another device — a difficulty, key bindings, an accessibility toggle your game
1044
+ owns — starts from `recommended` the same way and is kept in `gem.me.settings` instead
1045
+ ([saving player progress](saving-player-progress.md#for-something-small-reach-for-settings-instead)).
986
1046
 
987
1047
  **Read `muted` and `reducedMotion` as before.** They are not recommendations and they did not move:
988
1048
  `muted` is the Arcade's mute, which the SDK applies for you
@@ -995,30 +1055,12 @@ If your game turns a camera — a drag on a phone, a mouse under pointer lock, a
995
1055
  stick — the player has four settings for it in the Arcade, per device, the way Roblox players do:
996
1056
  **look sensitivity**, **gamepad look sensitivity**, **invert look up/down** and **touch movement**
997
1057
  (a floating or a fixed thumbstick). They reach you in `gem.prefs.recommended`
998
- ([the table above](#start-from-the-players-arcade-wide-defaults-gemprefsrecommended)). Two calls
999
- make them work.
1000
-
1001
- **1. Declare which ones your game uses — `gem.controls.declare`.** For each setting, `'player'`
1002
- means you apply the Arcade's value (or the player's own choice in your settings, which still wins);
1003
- `'game'` means your game decides it, and the Arcade's Settings shows that row as "Set by this game";
1004
- leave out a setting your game does not use, and the Arcade says so beside it. Call it once at boot:
1005
-
1006
- ```js
1007
- gem.controls.declare({
1008
- lookSensitivity: 'player',
1009
- gamepadLookSensitivity: 'player',
1010
- invertLookY: 'player',
1011
- touchMovement: 'game', // this game always draws a fixed stick
1012
- });
1013
- ```
1014
-
1015
- It is screen state, like [`gem.setWalletVisible`](#the-arcades-wallet-in-the-gem-menu-never-drawn-by-you):
1016
- each call is the whole answer, the latest wins, and the SDK holds it and sends it again after a
1017
- reconnect. It returns `false` before the bridge is up (it is held and sent on connect) and under an
1018
- embedder that does not show these settings; it never throws. A key or a value other than these is
1019
- dropped.
1058
+ ([the table above](#start-from-the-players-arcade-wide-defaults-gemprefsrecommended)) as the
1059
+ player's Arcade-wide defaults, and, like every [game setting](#game-settings), they are where your
1060
+ own settings start. There is nothing to tell the Arcade about them: its Settings show the player's
1061
+ defaults, never what a game uses.
1020
1062
 
1021
- **2. Turn look input into a camera turn — `gem.controls.look()`.** It is pure math: it listens to
1063
+ **Turn look input into a camera turn — `gem.controls.look()`.** It is pure math: it listens to
1022
1064
  nothing and moves no camera. Give it the deltas your input code already has; it returns radians to
1023
1065
  **add**, `yaw` positive turning right and `pitch` positive looking up, at Roblox's base speeds
1024
1066
  (`gem.controls.LOOK_BASE`: touch 1° across and 0.66° up/down per CSS px, mouse 0.5° and 0.385°,
@@ -1067,15 +1109,16 @@ function frame(dt) {
1067
1109
  Do not copy them into variables at boot.
1068
1110
  - **The player's choice in your game wins.** If your settings screen has its own look speed or
1069
1111
  invert, pass it — `gem.controls.look({ sensitivity: 1.5, invertY: true })`, or later
1070
- `look.set({ sensitivity: 1.5 })` — and that input stops following the Arcade.
1112
+ `look.set({ sensitivity: 1.5 })` — and that input stops following the Arcade. Keep it in
1113
+ `gem.deviceSettings`: a look speed is this device's configuration.
1071
1114
  `look.set({ sensitivity: undefined })` hands it back. `sensitivity` and `gamepadSensitivity` are
1072
1115
  multipliers (`1` is the base speed); the Arcade's percentages are divided by 100 for you.
1073
1116
  - **Pass `pitch` for touch**, the camera's current pitch in radians (0 at the horizon, positive
1074
1117
  looking up), or the slowdown near vertical is skipped. A mouse and a stick are never slowed.
1075
1118
  - **`touchMovement` is yours to draw.** The SDK has no thumbstick: read
1076
- `gem.prefs.recommended?.touchMovement` and draw a floating or a fixed one, or declare
1077
- `touchMovement: 'game'` if your game only has one kind.
1078
- - **No camera, no call.** A game with no camera to turn declares nothing and never calls `look`.
1119
+ `gem.prefs.recommended?.touchMovement` and draw a floating or a fixed one. A game with only one
1120
+ kind draws that one and ignores the field.
1121
+ - **No camera, no call.** A game with no camera to turn never calls `look`.
1079
1122
 
1080
1123
  ## Graphics quality: the Arcade picks the start, your game adapts
1081
1124
 
@@ -1139,7 +1182,8 @@ What it does, so you know what to expect:
1139
1182
 
1140
1183
  ## The Arcade's menu
1141
1184
 
1142
- 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
1143
1187
  how a player leaves (**Return to the Arcade**), sees their Gems, Tickets and achievement score, mutes,
1144
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
1145
1189
  four entries of your own:
@@ -1150,10 +1194,8 @@ gem.setMenu({
1150
1194
  });
1151
1195
  ```
1152
1196
 
1153
- - **The Arcade no longer shows a "Game settings" row for `onSettings`.** Your settings open from
1154
- [your own button](#game-settings). `gem.setMenu({ onSettings })` is still accepted, so a game that
1155
- passes it keeps working, and another embedder may still show an entry for it — but in the Arcade
1156
- it adds nothing.
1197
+ - **Your settings are not in it.** They open from [your own button](#game-settings), and the
1198
+ Arcade's menu never opens them.
1157
1199
  - **`items` are up to four entries of your own**, shown in a group below the Arcade's. `id` is your
1158
1200
  name for the entry (1 to 32 characters); `label` is what the player reads, **already in their
1159
1201
  language** (`gem.prefs.locale`) — the Arcade does not translate it, cuts it at 40 characters, and
@@ -1173,9 +1215,9 @@ gem.setMenu({
1173
1215
  settings button is there either way; what such a game also needs is a way out of its own, a control
1174
1216
  that calls `gem.exit()`.
1175
1217
 
1176
- Your own **back or home** button — the one that moves around inside your game — goes **top-left**,
1177
- which the Arcade [leaves clear](#the-arcades-own-controls-reserved-areas). It never leaves the game:
1178
- 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.
1179
1221
 
1180
1222
  ## Sound: your own mute control, and the Arcade's
1181
1223
 
@@ -1265,8 +1307,8 @@ belong to the Arcade. **Your game never draws one**, and never shows a pending s
1265
1307
  Arcade takes every figure from the platform and shows them itself.
1266
1308
 
1267
1309
  **The Gem Arcade draws no wallet over your game** (since 2026-10-11). Nothing of it sits in your top
1268
- row, at rest or when a balance moves. The player opens the Gem menu — the Gem button at the
1269
- 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
1270
1312
  [`gem.cueReward`](inventory-and-catalogs.md#showing-the-reward-land-in-the-arcades-wallet) announces
1271
1313
  flies into the Gem button. A payout you do not cue flies too, from the middle of your frame, so cue
1272
1314
  the ones the player earns on screen and the flight starts where they are looking.
@@ -1460,9 +1502,12 @@ This is a platform rule, not advice — see
1460
1502
 
1461
1503
  ### Arcade avatars: the character the player wears, and changing it mid-game
1462
1504
 
1463
- Some games draw **Arcade avatars** (store tag `arcade-avatars`): the player's look in your game is
1464
- something they own and wear in the Arcade. The Arcade's avatar picker has **two tabs**, and they
1465
- are **two separate worn choices** — wearing one never changes the other:
1505
+ Some games draw **Arcade avatars**: the player's look in your game is something they own and wear
1506
+ in the Arcade. **The whole guide for 3D avatars — rig families, declaring the ones your game
1507
+ accepts, uploading and selling your own, the purchase flow and loading the model — is
1508
+ [arcade-avatars.md](arcade-avatars.md).** This section is the summary. The Arcade's avatar picker
1509
+ has **two tabs**, and they are **two separate worn choices** — wearing one never changes the
1510
+ other:
1466
1511
 
1467
1512
  | Tab | What the player wears | On `gem.player` and every room member |
1468
1513
  |---|---|---|
@@ -1476,6 +1521,8 @@ are **two separate worn choices** — wearing one never changes the other:
1476
1521
  (an idle, say). It never changes at its URL, so cache it by URL.
1477
1522
  - **`avatar3dIconUrl`** is a rendered portrait of that character, for a roster or a menu where
1478
1523
  loading the model is too much. It is **not** the 2D portrait.
1524
+ - **`avatars3d`** lists every worn 3D character, one per **rig family**; the three fields above
1525
+ are your game's first declared family's. `gem.avatars3d.worn()` reads it for you.
1479
1526
 
1480
1527
  **Draw the 3D character, falling back to what you did before.** A game that drew characters
1481
1528
  before the 3D tab existed keyed its own models on `avatarIconId`. Keep that mapping as the
@@ -1522,7 +1569,7 @@ is missing now (a player who takes off their 3D character has no `avatar3dId`).
1522
1569
 
1523
1570
  **A "Change avatar" button belongs in your own UI**, and `gem.openAvatarPicker()` opens the
1524
1571
  Arcade's picker from it. Pass `{ tab: '3d' }` or `{ tab: '2d' }` to open on a tab; omit it and
1525
- the Arcade decides (a game tagged `arcade-avatars` gets the 3D tab). The Arcade pauses your game
1572
+ the Arcade decides (a game that declares avatar families gets the 3D tab). The Arcade pauses your game
1526
1573
  while the picker is open, saves the player's choice, sends the player again, and resumes you:
1527
1574
 
1528
1575
  ```js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indiegems/gem-web-sdk",
3
- "version": "12.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