@indiegems/gem-web-sdk 12.0.0 → 13.0.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.
@@ -0,0 +1,303 @@
1
+ # Arcade avatars: draw the 3D character a player wears, and sell your own
2
+
3
+ Read this if your game draws the player as a character and you want that character to be the one
4
+ they wear across the Arcade — or you want to sell characters of your own. A game that draws no
5
+ player character needs none of it.
6
+
7
+ **This page is about 3D avatars only.** The Arcade also has 2D portraits — the picture beside a
8
+ player's name — and they are a separate system: wearing a 3D avatar never changes the portrait,
9
+ and nothing here reads or changes one. Portraits are in
10
+ [what-a-game-must-do.md](what-a-game-must-do.md#showing-a-player-one-identity-on-every-surface).
11
+
12
+ ## 1. What a 3D avatar is
13
+
14
+ A 3D avatar is an Arcade item: a binary glTF model (`.glb`) built on a **rig family**, plus a
15
+ rendered portrait of it. A player owns as many as they like and **wears one per family**. What
16
+ they wear follows them into every game that accepts that family, and changes when they pick
17
+ another in the Arcade's avatar picker or buy one.
18
+
19
+ **The platform's item is the only copy that counts.** Your game reads the worn avatar's model URL
20
+ at runtime and loads it — there is no shared avatar library to install and no copy to bundle. A
21
+ model in your build is a model that drifts from what the player owns, and an avatar another game
22
+ sells could never be in your build at all. Ship your own **default look** for a player who wears
23
+ nothing, and nothing else.
24
+
25
+ Everything your game calls is on [`gem.avatars3d`](api/api-reference-game.md#gem-avatars3d).
26
+
27
+ ## 2. Rig families, and the `small_human` rig
28
+
29
+ A family is a skeleton contract: the node names and parent-child shape every model in it has, so a
30
+ game that animates the family animates every avatar in it. The first family is `small_human`:
31
+
32
+ ```
33
+ root
34
+ └── body
35
+ ├── head
36
+ ├── arm_l
37
+ ├── arm_r
38
+ │ └── hand_socket
39
+ ├── leg_l
40
+ └── leg_r
41
+ ```
42
+
43
+ The platform checks every `small_human` model when it is uploaded and refuses one that breaks any
44
+ of these:
45
+
46
+ - **The names are exact and case-sensitive**, and each of the eight appears **exactly once**.
47
+ `root` is a root node of the scene, `body` is its child, `head`, `arm_l`, `arm_r`, `leg_l` and
48
+ `leg_r` are children of `body`, and `hand_socket` is a child of `arm_r`. A held item goes on
49
+ `hand_socket`.
50
+ - **Anything else is allowed**: extra nodes anywhere, meshes, materials, skins and animation
51
+ clips. A rig is a floor, not a ceiling.
52
+ - **Nothing outside the file.** Buffers are embedded, and an image is embedded or a `data:` URI.
53
+ An external URI is refused.
54
+ - **No compression extension.** Draco and meshopt are refused. The extensions a model may
55
+ _require_ are `KHR_materials_unlit`, `KHR_texture_transform` and `KHR_mesh_quantization`.
56
+ - **At most 2 MiB** for the `.glb`.
57
+ - **A portrait**: a still, square PNG, 256 to 1024 pixels, at most 2 MiB — what shop tiles and
58
+ rosters show where loading the model is too much.
59
+
60
+ | What you get | What happened | What to do |
61
+ | ----------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
62
+ | `rig_mismatch` | a node is missing, under the wrong parent, or named twice | read `details.missing`, `misplaced` and `duplicated`, rename in your modelling tool, upload again |
63
+ | `model_external_reference` | the file points at a buffer or image outside it | export as a single binary `.glb` with textures embedded |
64
+ | `model_extension_unsupported` | the file requires Draco, meshopt or another extension | export without compression |
65
+ | `unknown_avatar_family` | the family name is not one the platform knows | use `small_human` |
66
+
67
+ ## 3. Declare the families your game accepts
68
+
69
+ A game accepts no family until its config says so. Add the section to
70
+ `gem-backend-game-config.yaml`:
71
+
72
+ ```yaml
73
+ avatars_3d:
74
+ families: [small_human]
75
+ ```
76
+
77
+ Then upload the config and point each channel at it:
78
+
79
+ ```sh
80
+ gemctl config upload --wait
81
+ gemctl config promote <version> --channel dev
82
+ gemctl config promote <version> --channel test
83
+ gemctl config promote <version> --channel release --yes
84
+ ```
85
+
86
+ - **The order is meaningful.** The first family is your default: it is the one the flat
87
+ `gem.player.avatar3dId` fields describe. Up to eight families, no repeats.
88
+ - **Each channel reads its own config.** A channel whose config declares no families accepts
89
+ none: the Arcade shows no 3D picker in your game there, and nothing is for sale.
90
+ - **Read it back** with `gem.avatars3d.families()`, which resolves the declared list from the
91
+ config the channel serves — `[]` when there is none.
92
+
93
+ ## 4. Upload an avatar
94
+
95
+ ```sh
96
+ gemctl item upload red-knight.glb --family small_human --portrait red-knight.png \
97
+ --slug red-knight --name "Red Knight" --description "Sworn to the hearth."
98
+ ```
99
+
100
+ A `.glb` file makes an avatar. gemctl checks the rig, the size and the external references on
101
+ your machine before it uploads, so a broken model costs no upload; the platform checks again and
102
+ renders the item, and the command waits until it is `ready`.
103
+
104
+ - **An avatar cannot be changed after upload.** A fixed model is a new item with a new slug — the
105
+ old one stays owned by everyone who has it.
106
+ - **It counts toward your game's items**, which are capped at 200 per game.
107
+
108
+ ## 5. Sell it
109
+
110
+ An avatar is for sale only when you put it on sale, priced in Tickets, Gems or both:
111
+
112
+ ```sh
113
+ gemctl item sell red-knight --tickets 50 --gems 300
114
+ gemctl item retire red-knight
115
+ ```
116
+
117
+ - **Prices are whole numbers from 1 to 1,000,000**, one or two of them. A player picks which
118
+ currency to pay in when you list both.
119
+ - **At most 30 avatars are on sale at once.** Selling a 31st is refused with
120
+ `avatar_3d_sale_limit_reached`. Retiring one frees its slot immediately.
121
+ - **Retiring never takes an avatar away.** A player who bought it keeps it, keeps wearing it, and
122
+ it keeps loading in your game. Retired means only that nobody new can buy it.
123
+ - **On sale means on sale on every channel** where your game declares the avatar's family.
124
+
125
+ ### Award it as a prize
126
+
127
+ An avatar can also be **won** — an event prize, a first-run gift, the end of a journey. A won avatar
128
+ is a real Arcade avatar: the player owns it and can wear it in every game that accepts its family.
129
+ First mark it awardable, then have your server award it:
130
+
131
+ ```sh
132
+ gemctl item awardable red-knight # may be awarded
133
+ gemctl item awardable red-knight --off # stop awarding; winners keep it
134
+ ```
135
+
136
+ ```http
137
+ POST /v1/games/{game_id}/avatars-3d/{item_id}/award
138
+ { "player_id": "<uuid>", "wear": false }
139
+ → { "item_id", "player_id", "family", "newly_awarded", "worn" }
140
+ ```
141
+
142
+ - **Only your server awards.** The call takes your game's server credential, or a serverless
143
+ function's for the player who called it. A browser never awards, and a server you started with a
144
+ developer credential is refused on `release` (it works on `dev` and `test`, so you can test).
145
+ - **It works on every channel** and is written once per player and avatar: awarding it again answers
146
+ `newly_awarded: false` and changes nothing, so retrying is safe.
147
+ - **At most 30 avatars are awardable at once**, counted apart from the 30 on sale; the 31st is
148
+ refused with `avatar_3d_award_limit_reached`. An avatar may be both on sale and awardable.
149
+ - **`wear: true` puts it on** for the player, as buying does.
150
+ - Don't award an avatar through an achievement reward: achievements unlock only on `release` and
151
+ share the 2D icon budget. Unlock the achievement as usual, and award the avatar from the same
152
+ server code.
153
+
154
+ ## 6. The purchase flow
155
+
156
+ Your game draws the shop; the Arcade takes the money. Read the offers from the platform, show
157
+ them, and call `purchase` from the player's own click:
158
+
159
+ ```js
160
+ const offers = await gem.avatars3d.offers(); // what is on sale, priced by the platform
161
+
162
+ for (const offer of offers) {
163
+ const tile = shop.addTile(offer.displayName, platformIconUrl(offer.iconUrl));
164
+ for (const { currencySlug, price } of offer.prices) {
165
+ tile.addButton(`${price} ${currencySlug === 'GEMS' ? 'Gems' : 'Tickets'}`, async () => {
166
+ const result = await gem.avatars3d.purchase({ itemId: offer.itemId, currencySlug });
167
+ if (result.outcome === 'declined' && result.reason === 'already_owned') {
168
+ gem.openAvatarPicker({ tab: '3d' }); // they have it: let them put it on
169
+ } else if (result.outcome === 'unavailable') {
170
+ shop.showMessage('The store is unavailable right now.');
171
+ }
172
+ // `ok`: nothing to do here — onChange already brought the new look.
173
+ });
174
+ }
175
+ }
176
+ ```
177
+
178
+ - **Render prices from `offers()`, never from numbers of your own.** The Arcade prices the sheet
179
+ again from the platform's record, so a price you typed is a price that disagrees with it.
180
+ - **Call `purchase` from a click or key press.** The Arcade shows its confirmation sheet only on
181
+ one; anything else is answered `declined` with reason `busy`.
182
+ - **A bought avatar is worn at once.** The Arcade wears it in its family and sends the player
183
+ again **before** it answers, so `onChange` runs and `worn()` already shows it when `purchase`
184
+ resolves `ok`.
185
+ - **`ok` is checked.** The SDK asks the platform whether the player now owns the avatar before it
186
+ tells you `ok`; an answer it cannot confirm becomes `unavailable`.
187
+
188
+ Every outcome resolves; nothing rejects.
189
+
190
+ | What you get | What happened | What to do |
191
+ | --------------------------------- | ----------------------------------------------------- | ----------------------------------------------------------------- |
192
+ | `ok` | bought, worn, and confirmed with the platform | nothing — `onChange` has redrawn |
193
+ | `declined` / `already_owned` | the player owns it already | offer `gem.openAvatarPicker({ tab: '3d' })` |
194
+ | `declined` / `cancelled` | the player closed the sheet | return to your shop silently |
195
+ | `declined` / `insufficient_funds` | the player cannot afford it | return silently; never draw a top-up |
196
+ | `declined` / `not_purchasable` | not on sale, retired, or not an id from `offers()` | read `offers()` again |
197
+ | `declined` / `busy` | no click, another sheet open, or a purchase in flight | let the player press again |
198
+ | `unavailable` | no reliable answer | one neutral message; read `worn()` again; never retry on your own |
199
+
200
+ **Outside the Arcade, `purchase` asks nobody.** Opened outside any Arcade, before the bridge is
201
+ up, or under an Arcade that does not sell 3D avatars, it resolves `unavailable` with reason
202
+ `error` at once — the same as `gem.purchases.request` does there. `offers()` still reads the
203
+ platform with your game's token, so a shop can show what is on sale.
204
+
205
+ **Purchases are real on every channel.** On `dev` and `test` a purchase spends the player's real
206
+ Gems or Tickets and grants a real avatar they keep, exactly as on `release`. Test with your
207
+ organization's test accounts.
208
+
209
+ ## 7. Read the worn avatar, and load it at runtime
210
+
211
+ The SDK is the package [getting-started.md](getting-started.md#3-add-the-sdk) installs; three.js
212
+ is your own dependency.
213
+
214
+ ```js
215
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
216
+ import { platformIconUrl } from '@indiegems/gem-web-sdk/sdk';
217
+
218
+ const loader = new GLTFLoader();
219
+ let shown = null; // the model URL on screen now
220
+
221
+ async function wear() {
222
+ const worn = gem.avatars3d.worn('small_human'); // or worn(): your first declared family
223
+ const url = worn && platformIconUrl(worn.modelUrl);
224
+ if (!url) return hero.useDefaultLook(); // nothing worn, or a host the platform does not own
225
+ if (url === shown) return;
226
+ shown = url;
227
+ try {
228
+ const gltf = await loader.loadAsync(url);
229
+ if (shown === url) hero.setCharacter(gltf.scene, gltf.animations);
230
+ } catch {
231
+ if (shown === url) hero.useDefaultLook(); // a model that fails is not a reason to show nothing
232
+ }
233
+ }
234
+
235
+ hero.useDefaultLook(); // never hold the boot for a model
236
+ wear();
237
+ gem.avatars3d.onChange(wear);
238
+ ```
239
+
240
+ - **`worn()` is read live** from the player the Arcade pushed. It is `undefined` before the player
241
+ arrives, outside the Arcade, and when nothing is worn in that family — draw your default look.
242
+ - **`onChange` runs whenever what is worn changes**: the player picked another avatar in the
243
+ Arcade's picker — from your own "Change avatar" button (`gem.openAvatarPicker({ tab: '3d' })`)
244
+ or the Arcade's menu — bought one, or their avatars arrived for the first time. It does not run
245
+ for the avatar already worn when you subscribe; read `worn()` for that.
246
+ - **Pass every URL through `platformIconUrl`** before you load it. Loading a model is a request
247
+ that carries your player's `Referer`, so only the platform's own item host is allowed; anything
248
+ else reads as `undefined`. Against a development backend that includes its own item host, so
249
+ there you see your default look.
250
+ - **A model URL never changes what it serves**, so cache by URL. When one fails to load, show your
251
+ default look and do not retry in a loop; the next `onChange` is the next chance.
252
+ - **Attach and animate by the rig's names.** Look nodes up by name, never by index:
253
+
254
+ ```js
255
+ function setCharacter(scene, clips) {
256
+ const node = (name) => scene.getObjectByName(name);
257
+ node('hand_socket').add(sword); // a held item goes on the socket
258
+ if (clips.length > 0) {
259
+ mixer = new THREE.AnimationMixer(scene); // the model's own clips, when it has them
260
+ mixer.clipAction(clips[0]).play();
261
+ } else {
262
+ walk = (t) => {
263
+ // otherwise, swing the limbs yourself
264
+ node('arm_l').rotation.x = Math.sin(t * 6) * 0.6;
265
+ node('arm_r').rotation.x = -Math.sin(t * 6) * 0.6;
266
+ node('leg_l').rotation.x = -Math.sin(t * 6) * 0.5;
267
+ node('leg_r').rotation.x = Math.sin(t * 6) * 0.5;
268
+ };
269
+ }
270
+ }
271
+ ```
272
+
273
+ Clip names are the model maker's, not a contract — play one only by a name you checked for, or
274
+ animate the named nodes, which every model in the family has.
275
+
276
+ **Other players' avatars** come the same way on every surface that shows a player: a room member,
277
+ a leaderboard row, a friend. Each carries `avatars3d` — one per family — and the flat
278
+ `avatar3dId`, `avatar3dModelUrl` and `avatar3dIconUrl` of your first family. A server reads
279
+ `avatars_3d` and `avatar_3d_id` from the room read; nothing tells a server a player changed, so
280
+ when `onChange` fires, have your client tell your server and have the server **re-read the room**
281
+ — never trust an id the client sends.
282
+
283
+ ## 8. Moving from 2D items, achievements and the `arcade-avatars` tag
284
+
285
+ Games that drew characters before 3D avatars existed did it with 2D portrait items granted by
286
+ achievements, a bundled copy of each character's model keyed on `avatarIconId`, and the store tag
287
+ `arcade-avatars`. Move to 3D avatars like this:
288
+
289
+ 1. **Upload one `small_human` avatar per old character** with `gemctl item upload`, as in
290
+ section 4.
291
+ 2. **Ask the platform team to map each old 2D item to its new avatar.** The platform then grants
292
+ every player who owns the 2D item the matching 3D avatar, and wears it for a player whose 3D
293
+ slot is empty and who wears that portrait. Players keep their 2D portraits.
294
+ 3. **Declare `avatars_3d: { families: [small_human] }` on every channel** (section 3), and **remove
295
+ the achievements and progressions that granted the 2D items**.
296
+ 4. **Put the avatars on sale** with `gemctl item sell`, and **make the ones players win
297
+ awardable** and award them from your server (section 5).
298
+ 5. **Ship a build that reads `gem.avatars3d.worn()` and loads the model at runtime** (section 7),
299
+ and delete the bundled character models and the `avatarIconId` mapping.
300
+ 6. **Remove the `arcade-avatars` tag from your store page.** It is deprecated. For a game whose
301
+ config declares no families the Arcade still reads the tag as accepting `small_human`, which
302
+ is what keeps an unmoved game working while you switch; do not rely on it once you have — a
303
+ declared family is what the Arcade reads.
package/docs/changelog.md CHANGED
@@ -16,6 +16,29 @@ Its entries are grouped under up to five headings:
16
16
  - **Rebuild**: a type changed. Recompile against the new version; code that compiles today keeps compiling unless the entry says otherwise.
17
17
  - **New capability**: something a game can now do. Nothing changes unless you use it.
18
18
 
19
+ ## 13.0.0
20
+
21
+ Upgrading from 12.0.0. Impact: required.
22
+
23
+ ### Must change
24
+
25
+ - The controls declaration is gone. Remove every call that declares controls, along with any code that builds a controls declaration or a control choice, because these no longer exist and the game will not compile against them. Follow the updated settings guidance for how the game now exposes settings and controls.
26
+ - If you embed the game in your own host, remove the controls-declare callback from the host options and callbacks, because the host no longer receives that message and the property is no longer accepted.
27
+ - setMenu no longer takes a settings flag as its first argument. Pass only the optional list of menu items. Also remove any onSettings handler from the game menu, since that handler is gone.
28
+
29
+ ### Rebuild
30
+
31
+ - The purchase refusal code gains a new value, already_owned, for purchases of something the player already has. Recompile; if the game switches over refusal codes exhaustively, add a branch for it.
32
+ - The list of messages a game frame sends to its parent, the frame message type, and the capability names each gain an entry for 3D avatar purchases, including the new avatar3dPurchases capability. Recompiling is enough unless the game or host switches over these values exhaustively.
33
+
34
+ ### New capability
35
+
36
+ - A game can now work with 3D avatars. The new avatars3d API lists the avatar rig families the game supports, reports the avatar a player wears per family, lists avatar offers and their prices, starts a purchase, and notifies the game when the worn avatar changes. Purchases are also available through the frame, gated by the avatar3dPurchases capability, and are answered like other purchases. Outside the Arcade, avatar purchases are unavailable immediately.
37
+ - The worn 3D avatar, one per rig family, is now available on the current player and on other players' data, including friends, leaderboard entries, profiles, showcase friends, player identities and room members. Use it to render other players with the avatar they wear.
38
+ - The game configuration can now declare the 3D avatar families the game supports through avatar_3d_families.
39
+ - If you embed games in your own host, you can now supply worn 3D avatars when sending player information, handle 3D avatar purchase requests with the new request callback, and validate those requests with the new validator and message type.
40
+ - Server-side code can now read the worn 3D avatars in avatars_3d on player and subject identity data, using the new worn avatar type.
41
+
19
42
  ## 12.0.0
20
43
 
21
44
  Upgrading from 11.1.2. Impact: required.
@@ -50,7 +50,7 @@ either way.
50
50
  ```jsonc
51
51
  // package.json
52
52
  "dependencies": {
53
- "@indiegems/gem-web-sdk": "12.0.0"
53
+ "@indiegems/gem-web-sdk": "13.0.0"
54
54
  }
55
55
  ```
56
56
 
@@ -16,6 +16,11 @@
16
16
  "sha256": "57fb07c3d649c2758a35660707d31b05164b363e9a425156978aed394d086339",
17
17
  "updated": "2026-10-02T14:04:37Z"
18
18
  },
19
+ {
20
+ "file": "arcade-avatars.md",
21
+ "sha256": "355bba7936ec7f0a65d52ec65a6add323c75a992d931de9cd23319ed9d8aeb52",
22
+ "updated": "2026-10-14T06:27:58Z"
23
+ },
19
24
  {
20
25
  "file": "arcade-economy.md",
21
26
  "sha256": "9058c26bc11266f235dfa3c72c80a3fc962aa9f204ef491006e6d34545f5eeeb",
@@ -33,8 +38,8 @@
33
38
  },
34
39
  {
35
40
  "file": "changelog.md",
36
- "sha256": "bf2786be31121bb229d10d98f701165aefb2e7166271299450c926401661dfd2",
37
- "updated": "2026-10-12T22:12:20Z"
41
+ "sha256": "914fb511d6f15af9ccf0d71e2309dcaf35369d4e4272233927ab7e48b47921a3",
42
+ "updated": "2026-10-14T06:33:54Z"
38
43
  },
39
44
  {
40
45
  "file": "cross-promotion.md",
@@ -68,8 +73,8 @@
68
73
  },
69
74
  {
70
75
  "file": "getting-started.md",
71
- "sha256": "19bf05cd8f7afe5e9f406af6aef090a73bb3c2fb713f06185ead36fc2bb104dc",
72
- "updated": "2026-10-12T22:12:20Z"
76
+ "sha256": "6aef9e454064492b2164417e1b31050e0f487c48edab07d5b58b5df9bb089bff",
77
+ "updated": "2026-10-14T06:33:54Z"
73
78
  },
74
79
  {
75
80
  "file": "inventory-and-catalogs.md",
@@ -118,13 +123,13 @@
118
123
  },
119
124
  {
120
125
  "file": "saving-player-progress.md",
121
- "sha256": "08778cccc7d7a9a26a005a070bb2cc98784752ebec9dfce9873ef3d4e674e1e0",
122
- "updated": "2026-10-05T16:53:25Z"
126
+ "sha256": "11ab0684247cfc43d6d8faba6dbb897192354af5f5622b16fd96abcb156ca0cd",
127
+ "updated": "2026-10-12T21:21:05Z"
123
128
  },
124
129
  {
125
130
  "file": "server-bundles.md",
126
- "sha256": "52054ceeb56b7130bfaf3461c2d81a9bd3b3dcd0f95097753ebfffc08f7a1cbb",
127
- "updated": "2026-10-12T22:12:20Z"
131
+ "sha256": "8b19574001cefea73b1892dd864d360dc0e5dfd252e9d291f7ccb08a170b48d9",
132
+ "updated": "2026-10-14T06:33:54Z"
128
133
  },
129
134
  {
130
135
  "file": "serverless-functions-js.md",
@@ -148,8 +153,8 @@
148
153
  },
149
154
  {
150
155
  "file": "what-a-game-must-do.md",
151
- "sha256": "275ca0984cdeb9d52221df0700516f85d3b24d41457ee09e0e1326f6ed3c8112",
152
- "updated": "2026-10-12T16:40:39Z"
156
+ "sha256": "ca29417cbe0c337efe44c3d6f44f943ff27425fbb11e208faff1207d2b6f912c",
157
+ "updated": "2026-10-13T14:23:52Z"
153
158
  }
154
159
  ]
155
160
  }
@@ -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.0.0/gem-web-sdk.tgz
84
+ curl -fsSL https://downloads.gemarcade.com/sdk/v13.0.0/gem-web-sdk.tgz.sri
85
85
  openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
86
86
  ```
87
87
 
@@ -891,13 +891,31 @@ arrives.
891
891
  ## Game settings
892
892
 
893
893
  **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:
894
+ The Arcade's menu never opens your settings — it holds the player's _Arcade-wide_ settings
895
+ instead — so a game with no button of its own has settings nobody can reach.
896
+
897
+ This is the whole model, and every other page that mentions settings links here:
898
+
899
+ - **The Arcade's settings are the player's Arcade-wide defaults for this device.** Your game reads
900
+ them from `gem.prefs.recommended` (and `muted` and `reducedMotion` from `gem.prefs`) and applies
901
+ them again on `onPrefs`. The Arcade never asks your game anything about them, and never shows
902
+ 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.
905
+ - **A setting starts from `recommended` until the player changes it in your game, and then the
906
+ player's choice in your game wins.** Save only what the player chose, never a recommended value.
907
+ - **Where a choice is kept.** Device configuration — volume, graphics quality, look sensitivity, a
908
+ touch-stick style — goes in `gem.deviceSettings`; a choice that must follow the player across
909
+ devices — difficulty, key bindings, accessibility toggles the game owns — goes in
910
+ `gem.me.settings`.
911
+ - **Mute and vibration are in your settings screen too**, wired both ways: your mute follows
912
+ `prefs.muted` and reports the player's own with `gem.reportMuted`
913
+ ([sound](#sound-your-own-mute-control-and-the-arcades)), and your vibration toggle shows only
914
+ while `gem.vibration.available` ([vibration](vibration.md)).
915
+
916
+ **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:
901
919
 
902
920
  ```css
903
921
  #settings-button {
@@ -931,8 +949,8 @@ same push as `muted` and `reducedMotion`, and again whenever the player changes
931
949
  | `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
950
 
933
951
  `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
952
+ [camera and look controls](#camera-and-look-controls) for `gem.controls.look()`, which applies the
953
+ two sensitivities and the invert for you. An
936
954
  older Arcade sends `recommended` without them; the SDK then fills each with its default (`100`,
937
955
  `100`, `false`, `'floating'`). The last two are the starting graphics quality: see
938
956
  [graphics quality](#graphics-quality-the-arcade-picks-the-start-your-game-adapts). An older Arcade
@@ -981,8 +999,11 @@ volumeSlider.oninput = () => {
981
999
  };
982
1000
  ```
983
1001
 
984
- [Device settings](#device-settings-what-this-device-remembers-about-your-game) above is where the
985
- player's choices are kept between sessions.
1002
+ `chosen` here is this device's configuration, so it is kept in
1003
+ [device settings](#device-settings-what-this-device-remembers-about-your-game). A choice that must
1004
+ follow the player to another device — a difficulty, key bindings, an accessibility toggle your game
1005
+ owns — starts from `recommended` the same way and is kept in `gem.me.settings` instead
1006
+ ([saving player progress](saving-player-progress.md#for-something-small-reach-for-settings-instead)).
986
1007
 
987
1008
  **Read `muted` and `reducedMotion` as before.** They are not recommendations and they did not move:
988
1009
  `muted` is the Arcade's mute, which the SDK applies for you
@@ -995,30 +1016,12 @@ If your game turns a camera — a drag on a phone, a mouse under pointer lock, a
995
1016
  stick — the player has four settings for it in the Arcade, per device, the way Roblox players do:
996
1017
  **look sensitivity**, **gamepad look sensitivity**, **invert look up/down** and **touch movement**
997
1018
  (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.
1019
+ ([the table above](#start-from-the-players-arcade-wide-defaults-gemprefsrecommended)) as the
1020
+ player's Arcade-wide defaults, and, like every [game setting](#game-settings), they are where your
1021
+ own settings start. There is nothing to tell the Arcade about them: its Settings show the player's
1022
+ defaults, never what a game uses.
1020
1023
 
1021
- **2. Turn look input into a camera turn — `gem.controls.look()`.** It is pure math: it listens to
1024
+ **Turn look input into a camera turn — `gem.controls.look()`.** It is pure math: it listens to
1022
1025
  nothing and moves no camera. Give it the deltas your input code already has; it returns radians to
1023
1026
  **add**, `yaw` positive turning right and `pitch` positive looking up, at Roblox's base speeds
1024
1027
  (`gem.controls.LOOK_BASE`: touch 1° across and 0.66° up/down per CSS px, mouse 0.5° and 0.385°,
@@ -1067,15 +1070,16 @@ function frame(dt) {
1067
1070
  Do not copy them into variables at boot.
1068
1071
  - **The player's choice in your game wins.** If your settings screen has its own look speed or
1069
1072
  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.
1073
+ `look.set({ sensitivity: 1.5 })` — and that input stops following the Arcade. Keep it in
1074
+ `gem.deviceSettings`: a look speed is this device's configuration.
1071
1075
  `look.set({ sensitivity: undefined })` hands it back. `sensitivity` and `gamepadSensitivity` are
1072
1076
  multipliers (`1` is the base speed); the Arcade's percentages are divided by 100 for you.
1073
1077
  - **Pass `pitch` for touch**, the camera's current pitch in radians (0 at the horizon, positive
1074
1078
  looking up), or the slowdown near vertical is skipped. A mouse and a stick are never slowed.
1075
1079
  - **`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`.
1080
+ `gem.prefs.recommended?.touchMovement` and draw a floating or a fixed one. A game with only one
1081
+ kind draws that one and ignores the field.
1082
+ - **No camera, no call.** A game with no camera to turn never calls `look`.
1079
1083
 
1080
1084
  ## Graphics quality: the Arcade picks the start, your game adapts
1081
1085
 
@@ -1150,10 +1154,8 @@ gem.setMenu({
1150
1154
  });
1151
1155
  ```
1152
1156
 
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.
1157
+ - **Your settings are not in it.** They open from [your own button](#game-settings), and the
1158
+ Arcade's menu never opens them.
1157
1159
  - **`items` are up to four entries of your own**, shown in a group below the Arcade's. `id` is your
1158
1160
  name for the entry (1 to 32 characters); `label` is what the player reads, **already in their
1159
1161
  language** (`gem.prefs.locale`) — the Arcade does not translate it, cuts it at 40 characters, and
@@ -1460,9 +1462,12 @@ This is a platform rule, not advice — see
1460
1462
 
1461
1463
  ### Arcade avatars: the character the player wears, and changing it mid-game
1462
1464
 
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:
1465
+ Some games draw **Arcade avatars**: the player's look in your game is something they own and wear
1466
+ in the Arcade. **The whole guide for 3D avatars — rig families, declaring the ones your game
1467
+ accepts, uploading and selling your own, the purchase flow and loading the model — is
1468
+ [arcade-avatars.md](arcade-avatars.md).** This section is the summary. The Arcade's avatar picker
1469
+ has **two tabs**, and they are **two separate worn choices** — wearing one never changes the
1470
+ other:
1466
1471
 
1467
1472
  | Tab | What the player wears | On `gem.player` and every room member |
1468
1473
  |---|---|---|
@@ -1476,6 +1481,8 @@ are **two separate worn choices** — wearing one never changes the other:
1476
1481
  (an idle, say). It never changes at its URL, so cache it by URL.
1477
1482
  - **`avatar3dIconUrl`** is a rendered portrait of that character, for a roster or a menu where
1478
1483
  loading the model is too much. It is **not** the 2D portrait.
1484
+ - **`avatars3d`** lists every worn 3D character, one per **rig family**; the three fields above
1485
+ are your game's first declared family's. `gem.avatars3d.worn()` reads it for you.
1479
1486
 
1480
1487
  **Draw the 3D character, falling back to what you did before.** A game that drew characters
1481
1488
  before the 3D tab existed keyed its own models on `avatarIconId`. Keep that mapping as the
@@ -1522,7 +1529,7 @@ is missing now (a player who takes off their 3D character has no `avatar3dId`).
1522
1529
 
1523
1530
  **A "Change avatar" button belongs in your own UI**, and `gem.openAvatarPicker()` opens the
1524
1531
  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
1532
+ the Arcade decides (a game that declares avatar families gets the 3D tab). The Arcade pauses your game
1526
1533
  while the picker is open, saves the player's choice, sends the player again, and resumes you:
1527
1534
 
1528
1535
  ```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.0.0",
4
4
  "description": "postMessage bridge contract between gem-web and browser-playable games",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",