@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.
@@ -0,0 +1,471 @@
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
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. There are two:
31
+
32
+ - **`small_human`**, a toy: rigid limbs on a body, eight nodes, no elbows, knees or fingers.
33
+ - **`adult_human`**, a full adult skeleton with a spine, elbows, knees, toes and fingers, 56
34
+ nodes, built so one animation plays on every avatar in the family.
35
+
36
+ Pick the one your characters are made of. A game that draws both kinds declares both (section 3).
37
+
38
+ **Never retype the node names.** `AVATAR_RIGS` lists every node of each family with the node it
39
+ is a direct child of, parents first, so your game finds its joints, and your tests check a model,
40
+ by the names the platform checks:
41
+
42
+ ```js
43
+ import { AVATAR_RIGS } from '@indiegems/gem-web-sdk/sdk';
44
+
45
+ AVATAR_RIGS.adult_human; // [{ node: 'root', parent: null }, { node: 'hips', parent: 'root' }, …]
46
+ ```
47
+
48
+ ### What every family's upload must pass
49
+
50
+ The platform checks every model when it is uploaded, and gemctl runs the same checks on your
51
+ machine first. A model is refused when it breaks any of these:
52
+
53
+ - **The names are exact and case-sensitive**, each appears **exactly once**, and each is the
54
+ **direct child** of the parent its family gives. The family's `root` is a root node of the
55
+ scene.
56
+ - **Anything else is allowed**: extra nodes anywhere (twist bones, a cape, accessories), meshes,
57
+ materials, skins and animation clips. A rig is a floor, not a ceiling.
58
+ - **Nothing outside the file.** Buffers are embedded, and an image is embedded or a `data:` URI.
59
+ An external URI is refused.
60
+ - **No compression extension.** Draco and meshopt are refused. The extensions a model may
61
+ _require_ are `KHR_materials_unlit`, `KHR_texture_transform` and `KHR_mesh_quantization`.
62
+ - **At most 2 MiB** for the `.glb`.
63
+ - **A portrait**: a still, square PNG, 256 to 1024 pixels, at most 2 MiB — what shop tiles and
64
+ rosters show where loading the model is too much.
65
+
66
+ ### `small_human`
67
+
68
+ ```
69
+ root
70
+ └── body
71
+ ├── head
72
+ ├── arm_l
73
+ ├── arm_r
74
+ │ └── hand_socket
75
+ ├── leg_l
76
+ └── leg_r
77
+ ```
78
+
79
+ `body` is the child of `root`; `head`, `arm_l`, `arm_r`, `leg_l` and `leg_r` are children of
80
+ `body`; `hand_socket` is a child of `arm_r`. A held item goes on `hand_socket`. Nothing is checked
81
+ about its pose or its size.
82
+
83
+ ### `adult_human`
84
+
85
+ `_l` and `_r` are the **character's** left and right.
86
+
87
+ ```
88
+ root the ground between the feet
89
+ └── hips
90
+ ├── spine
91
+ │ └── chest
92
+ │ └── upper_chest
93
+ │ ├── neck
94
+ │ │ └── head
95
+ │ │ └── head_socket hats and helmets
96
+ │ ├── shoulder_l
97
+ │ │ └── upper_arm_l
98
+ │ │ └── lower_arm_l
99
+ │ │ └── hand_l
100
+ │ │ ├── hand_socket_l held items
101
+ │ │ ├── thumb_1_l > thumb_2_l > thumb_3_l
102
+ │ │ ├── index_1_l > index_2_l > index_3_l
103
+ │ │ ├── middle_1_l > middle_2_l > middle_3_l
104
+ │ │ ├── ring_1_l > ring_2_l > ring_3_l
105
+ │ │ └── little_1_l > little_2_l > little_3_l
106
+ │ └── shoulder_r … the same as the left, with hand_socket_r
107
+ ├── upper_leg_l
108
+ │ └── lower_leg_l
109
+ │ └── foot_l
110
+ │ └── toes_l
111
+ └── upper_leg_r > lower_leg_r > foot_r > toes_r
112
+ ```
113
+
114
+ `a > b > c` is a chain: `b` is the child of `a`, and `c` of `b`. That is 56 nodes: `root`, six
115
+ down the middle, four in each arm, fifteen finger joints in each hand, four in each leg, and three
116
+ sockets.
117
+
118
+ **Space and units are glTF's own**: Y is up, the character faces **+Z**, and a unit is a
119
+ **metre**. Its left hand is toward +X. `root` sits at the origin, on the ground between the feet.
120
+
121
+ **The rest pose is a T-pose**: legs straight down, arms straight out to the sides (the left along
122
+ +X, the right along −X), palms down, fingers straight along the arm, and the thumb forward (+Z)
123
+ and slightly out.
124
+
125
+ **Every rig node is world-aligned at rest.** Each of the 56 has an identity rotation and a scale
126
+ of 1; only its position differs from one avatar to the next. This is what makes animation
127
+ portable: a rotation you set on a joint is measured from the same rest on every avatar, so a clip
128
+ written for one plays the same on a tall, short, thin or wide one. A model whose bones keep the
129
+ turn a modelling tool gives them (Blender, for one, exports each bone turned to point along its
130
+ length) would play every clip twisted, so the platform refuses it. On top of the checks every
131
+ family passes, an `adult_human` model is refused when:
132
+
133
+ - **a rig node is rotated** more than 2° from identity, or **scaled** more than 0.001 from 1;
134
+ - **a rig node uses `matrix`** instead of translation, rotation and scale;
135
+ - **`root` is moved** more than 1 mm from the origin;
136
+ - **the hips are not 0.6 to 1.3 m up** (the heights of `root` and `hips` added): a model exported
137
+ in centimetres is a hundred times too big, and a toy is too small.
138
+
139
+ **The sockets** are plain nodes, empties or bones. `head_socket` sits on the crown of the head,
140
+ where a cap's band rests. `hand_socket_l` and `hand_socket_r` sit at the centre of the closed
141
+ grip, inside the curled fingers. Like every rig node they are world-aligned, so a hat or a bat
142
+ modelled upright, facing +Z, with its grip at its origin, sits the same way on every avatar.
143
+ Where a socket sits is not checked; the reference rig (below) follows it.
144
+
145
+ **Keep it light for a phone.** Not checked, but every game that accepts the family loads the
146
+ model, and other players' avatars beside it: up to 15,000 triangles, one 1024 × 1024 texture
147
+ (embedded JPEG or PNG), and up to four bone influences per vertex.
148
+
149
+ **Start from the reference rig.** The package ships one:
150
+ `node_modules/@indiegems/gem-web-sdk/rigs/adult_human.glb`, a plain skinned mannequin about
151
+ 1.75 m tall, with no textures and no clips, that passes every check above. Make your game's clips
152
+ on it, and test your own avatars against it; import it by path
153
+ (`@indiegems/gem-web-sdk/rigs/adult_human.glb`) for a test scene. To export a rig of your own from
154
+ Blender with an identity rest: give every bone the same orientation in armature space (pointing
155
+ up, its local Z toward the character's front), make the sockets bones, keep the armature object
156
+ as the node `root`, and export with **+Y Up** on, `export_rest_position_armature=True` and
157
+ `export_def_bones=False`.
158
+
159
+ **Uploads wait for the platform to enable the family.** Where it is not enabled yet, an
160
+ `adult_human` upload is refused with `unknown_avatar_family`. Build and test against the
161
+ reference rig until then; nothing in your game changes when it is.
162
+
163
+ #### Pose it, or play your own clip on it
164
+
165
+ The model's own clips are its maker's and no contract. Your game brings its own animations and
166
+ plays them on the rig's names. Load the avatar as section 7 shows, then find the joints:
167
+
168
+ ```js
169
+ import * as THREE from 'three';
170
+ import { AVATAR_RIGS } from '@indiegems/gem-web-sdk/sdk';
171
+
172
+ const joints = {};
173
+ for (const { node } of AVATAR_RIGS.adult_human) joints[node] = avatar.getObjectByName(node);
174
+
175
+ joints.hand_socket_r.add(bat); // a held item, modelled upright, facing +Z, grip at its origin
176
+ joints.head_socket.add(cap);
177
+ ```
178
+
179
+ **Pose it in code** by setting rotations on named joints. Every angle is measured from the
180
+ T-pose, because the rest rotation is identity, and `quaternion.identity()` puts a joint back:
181
+
182
+ ```js
183
+ function sitAtTheWheel() {
184
+ for (const joint of Object.values(joints)) joint.quaternion.identity(); // back to the T-pose
185
+ joints.upper_leg_l.rotation.x = joints.upper_leg_r.rotation.x = -Math.PI / 2; // thighs forward
186
+ joints.lower_leg_l.rotation.x = joints.lower_leg_r.rotation.x = Math.PI / 2; // shins down
187
+ joints.upper_arm_l.rotation.y = -Math.PI / 2; // left arm forward
188
+ joints.upper_arm_r.rotation.y = Math.PI / 2; // right arm forward
189
+ for (const finger of ['index', 'middle', 'ring', 'little']) {
190
+ for (const k of [1, 2, 3]) {
191
+ joints[`${finger}_${k}_l`].rotation.z = -1.2; // curl toward the palm
192
+ joints[`${finger}_${k}_r`].rotation.z = 1.2;
193
+ }
194
+ }
195
+ }
196
+ ```
197
+
198
+ **Play a clip** you made on the reference rig, from a file of your own. Its tracks name the rig's
199
+ nodes, so a mixer on any avatar finds them:
200
+
201
+ ```js
202
+ const { animations } = await loader.loadAsync('/anims/driver.glb'); // your clips, not the avatar's
203
+ const seated = THREE.AnimationClip.findByName(animations, 'seated');
204
+ seated.tracks = seated.tracks.filter((track) => track.name.endsWith('.quaternion'));
205
+
206
+ const mixer = new THREE.AnimationMixer(avatar);
207
+ mixer.clipAction(seated).play();
208
+ // every frame: mixer.update(dt)
209
+ ```
210
+
211
+ - **Keep only the rotation tracks.** A position or scale track carries the reference rig's bone
212
+ lengths onto every avatar: a short player's hips are lifted to the reference rig's height. Move
213
+ and turn the whole character by the object that holds it.
214
+ - **Scale the object that holds the avatar, never a rig node**, when your world is not in metres.
215
+ - **One loaded copy per character on screen.** Two players wearing the same avatar need two
216
+ copies: clone a skinned model with three.js's `SkeletonUtils.clone`, never `Object3D.clone`,
217
+ which leaves both copies' meshes moving with one skeleton.
218
+
219
+ ### When an upload is refused
220
+
221
+ | What you get | What happened | What to do |
222
+ | ----------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
223
+ | `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 |
224
+ | `rig_mismatch` with `details.not_at_rest` | an `adult_human` rig node is not at rest, or the model is the wrong size | read each `{ node, reason }`, e.g. `"rotated 37°"` or `"hips at 92 m"`; apply rotation and scale, export in metres |
225
+ | `model_external_reference` | the file points at a buffer or image outside it | export as a single binary `.glb` with textures embedded |
226
+ | `model_extension_unsupported` | the file requires Draco, meshopt or another extension | export without compression |
227
+ | `unknown_avatar_family` | the family is not one the platform knows, or not enabled yet | use `small_human` or `adult_human`; an `adult_human` upload waits until it is enabled |
228
+
229
+ ## 3. Declare the families your game accepts
230
+
231
+ A game accepts no family until its config says so. Add the section to
232
+ `gem-backend-game-config.yaml`:
233
+
234
+ ```yaml
235
+ avatars_3d:
236
+ families: [small_human]
237
+ ```
238
+
239
+ Then upload the config and point each channel at it:
240
+
241
+ ```sh
242
+ gemctl config upload --wait
243
+ gemctl config promote <version> --channel dev
244
+ gemctl config promote <version> --channel test
245
+ gemctl config promote <version> --channel release --yes
246
+ ```
247
+
248
+ - **The order is meaningful.** The first family is your default: it is the one the flat
249
+ `gem.player.avatar3dId` fields describe. Up to eight families, no repeats.
250
+ - **A game can declare both families**, `families: [adult_human, small_human]`, when it draws
251
+ both kinds of character. A player wears one avatar in each family, so ask for the one you draw
252
+ by name, `worn('adult_human') ?? worn('small_human')`, and animate each on its own rig: a
253
+ clip made for one rig does not fit the other.
254
+ - **Each channel reads its own config.** A channel whose config declares no families accepts
255
+ none: the Arcade shows no 3D picker in your game there, and nothing is for sale.
256
+ - **Read it back** with `gem.avatars3d.families()`, which resolves the declared list from the
257
+ config the channel serves — `[]` when there is none.
258
+
259
+ ## 4. Upload an avatar
260
+
261
+ ```sh
262
+ gemctl item upload red-knight.glb --family small_human --portrait red-knight.png \
263
+ --slug red-knight --name "Red Knight" --description "Sworn to the hearth."
264
+ ```
265
+
266
+ A `.glb` file makes an avatar; `--family adult_human` uploads one on the adult rig. gemctl checks
267
+ the rig (and an `adult_human` model's rest pose), the size and the external references on your
268
+ machine before it uploads, so a broken model costs no upload; the platform checks again and
269
+ renders the item, and the command waits until it is `ready`.
270
+
271
+ - **An avatar cannot be changed after upload.** A fixed model is a new item with a new slug — the
272
+ old one stays owned by everyone who has it.
273
+ - **It counts toward your game's items**, which are capped at 200 per game.
274
+
275
+ ## 5. Sell it
276
+
277
+ An avatar is for sale only when you put it on sale, priced in Tickets, Gems or both:
278
+
279
+ ```sh
280
+ gemctl item sell red-knight --tickets 50 --gems 300
281
+ gemctl item retire red-knight
282
+ ```
283
+
284
+ - **Prices are whole numbers from 1 to 1,000,000**, one or two of them. A player picks which
285
+ currency to pay in when you list both.
286
+ - **At most 30 avatars are on sale at once.** Selling a 31st is refused with
287
+ `avatar_3d_sale_limit_reached`. Retiring one frees its slot immediately.
288
+ - **Retiring never takes an avatar away.** A player who bought it keeps it, keeps wearing it, and
289
+ it keeps loading in your game. Retired means only that nobody new can buy it.
290
+ - **On sale means on sale on every channel** where your game declares the avatar's family.
291
+
292
+ ### Award it as a prize
293
+
294
+ An avatar can also be **won** — an event prize, a first-run gift, the end of a journey. A won avatar
295
+ is a real Arcade avatar: the player owns it and can wear it in every game that accepts its family.
296
+ First mark it awardable, then have your server award it:
297
+
298
+ ```sh
299
+ gemctl item awardable red-knight # may be awarded
300
+ gemctl item awardable red-knight --off # stop awarding; winners keep it
301
+ ```
302
+
303
+ ```http
304
+ POST /v1/games/{game_id}/avatars-3d/{item_id}/award
305
+ { "player_id": "<uuid>", "wear": false }
306
+ → { "item_id", "player_id", "family", "newly_awarded", "worn" }
307
+ ```
308
+
309
+ - **Only your server awards.** The call takes your game's server credential, or a serverless
310
+ function's for the player who called it. A browser never awards, and a server you started with a
311
+ developer credential is refused on `release` (it works on `dev` and `test`, so you can test).
312
+ - **It works on every channel** and is written once per player and avatar: awarding it again answers
313
+ `newly_awarded: false` and changes nothing, so retrying is safe.
314
+ - **At most 30 avatars are awardable at once**, counted apart from the 30 on sale; the 31st is
315
+ refused with `avatar_3d_award_limit_reached`. An avatar may be both on sale and awardable.
316
+ - **`wear: true` puts it on** for the player, as buying does.
317
+ - Don't award an avatar through an achievement reward: achievements unlock only on `release` and
318
+ share the 2D icon budget. Unlock the achievement as usual, and award the avatar from the same
319
+ server code.
320
+
321
+ ## 6. The purchase flow
322
+
323
+ Your game draws the shop; the Arcade takes the money. Read the offers from the platform, show
324
+ them, and call `purchase` from the player's own click:
325
+
326
+ ```js
327
+ const offers = await gem.avatars3d.offers(); // what is on sale, priced by the platform
328
+
329
+ for (const offer of offers) {
330
+ const tile = shop.addTile(offer.displayName, platformIconUrl(offer.iconUrl));
331
+ for (const { currencySlug, price } of offer.prices) {
332
+ tile.addButton(`${price} ${currencySlug === 'GEMS' ? 'Gems' : 'Tickets'}`, async () => {
333
+ const result = await gem.avatars3d.purchase({ itemId: offer.itemId, currencySlug });
334
+ if (result.outcome === 'declined' && result.reason === 'already_owned') {
335
+ gem.openAvatarPicker({ tab: '3d' }); // they have it: let them put it on
336
+ } else if (result.outcome === 'unavailable') {
337
+ shop.showMessage('The store is unavailable right now.');
338
+ }
339
+ // `ok`: nothing to do here — onChange already brought the new look.
340
+ });
341
+ }
342
+ }
343
+ ```
344
+
345
+ - **Render prices from `offers()`, never from numbers of your own.** The Arcade prices the sheet
346
+ again from the platform's record, so a price you typed is a price that disagrees with it.
347
+ - **Call `purchase` from a click or key press.** The Arcade shows its confirmation sheet only on
348
+ one; anything else is answered `declined` with reason `busy`.
349
+ - **A bought avatar is worn at once.** The Arcade wears it in its family and sends the player
350
+ again **before** it answers, so `onChange` runs and `worn()` already shows it when `purchase`
351
+ resolves `ok`.
352
+ - **`ok` is checked.** The SDK asks the platform whether the player now owns the avatar before it
353
+ tells you `ok`; an answer it cannot confirm becomes `unavailable`.
354
+
355
+ Every outcome resolves; nothing rejects.
356
+
357
+ | What you get | What happened | What to do |
358
+ | --------------------------------- | ----------------------------------------------------- | ----------------------------------------------------------------- |
359
+ | `ok` | bought, worn, and confirmed with the platform | nothing — `onChange` has redrawn |
360
+ | `declined` / `already_owned` | the player owns it already | offer `gem.openAvatarPicker({ tab: '3d' })` |
361
+ | `declined` / `cancelled` | the player closed the sheet | return to your shop silently |
362
+ | `declined` / `insufficient_funds` | the player cannot afford it | return silently; never draw a top-up |
363
+ | `declined` / `not_purchasable` | not on sale, retired, or not an id from `offers()` | read `offers()` again |
364
+ | `declined` / `busy` | no click, another sheet open, or a purchase in flight | let the player press again |
365
+ | `unavailable` | no reliable answer | one neutral message; read `worn()` again; never retry on your own |
366
+
367
+ **Outside the Arcade, `purchase` asks nobody.** Opened outside any Arcade, before the bridge is
368
+ up, or under an Arcade that does not sell 3D avatars, it resolves `unavailable` with reason
369
+ `error` at once — the same as `gem.purchases.request` does there. `offers()` still reads the
370
+ platform with your game's token, so a shop can show what is on sale.
371
+
372
+ **Purchases are real on every channel.** On `dev` and `test` a purchase spends the player's real
373
+ Gems or Tickets and grants a real avatar they keep, exactly as on `release`. Test with your
374
+ organization's test accounts.
375
+
376
+ ## 7. Read the worn avatar, and load it at runtime
377
+
378
+ The SDK is the package [getting-started.md](getting-started.md#3-add-the-sdk) installs; three.js
379
+ is your own dependency.
380
+
381
+ ```js
382
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
383
+ import { platformIconUrl } from '@indiegems/gem-web-sdk/sdk';
384
+
385
+ const loader = new GLTFLoader();
386
+ let shown = null; // the model URL on screen now
387
+
388
+ async function wear() {
389
+ const worn = gem.avatars3d.worn('small_human'); // or worn(): your first declared family
390
+ const url = worn && platformIconUrl(worn.modelUrl);
391
+ if (!url) return hero.useDefaultLook(); // nothing worn, or a host the platform does not own
392
+ if (url === shown) return;
393
+ shown = url;
394
+ try {
395
+ const gltf = await loader.loadAsync(url);
396
+ if (shown === url) hero.setCharacter(gltf.scene, gltf.animations);
397
+ } catch {
398
+ if (shown === url) hero.useDefaultLook(); // a model that fails is not a reason to show nothing
399
+ }
400
+ }
401
+
402
+ hero.useDefaultLook(); // never hold the boot for a model
403
+ wear();
404
+ gem.avatars3d.onChange(wear);
405
+ ```
406
+
407
+ - **`worn()` is read live** from the player the Arcade pushed. It is `undefined` before the player
408
+ arrives, outside the Arcade, and when nothing is worn in that family — draw your default look.
409
+ - **`onChange` runs whenever what is worn changes**: the player picked another avatar in the
410
+ Arcade's picker — from your own "Change avatar" button (`gem.openAvatarPicker({ tab: '3d' })`)
411
+ or the Arcade's menu — bought one, or their avatars arrived for the first time. It does not run
412
+ for the avatar already worn when you subscribe; read `worn()` for that.
413
+ - **Pass every URL through `platformIconUrl`** before you load it. Loading a model is a request
414
+ that carries your player's `Referer`, so only the platform's own item host is allowed; anything
415
+ else reads as `undefined`. Against a development backend that includes its own item host, so
416
+ there you see your default look.
417
+ - **A model URL never changes what it serves**, so cache by URL. When one fails to load, show your
418
+ default look and do not retry in a loop; the next `onChange` is the next chance.
419
+ - **Attach and animate by the rig's names.** Look nodes up by name, never by index. This is a
420
+ `small_human` model; an `adult_human` one is posed and animated as section 2 shows:
421
+
422
+ ```js
423
+ function setCharacter(scene, clips) {
424
+ const node = (name) => scene.getObjectByName(name);
425
+ node('hand_socket').add(sword); // a held item goes on the socket
426
+ if (clips.length > 0) {
427
+ mixer = new THREE.AnimationMixer(scene); // the model's own clips, when it has them
428
+ mixer.clipAction(clips[0]).play();
429
+ } else {
430
+ walk = (t) => {
431
+ // otherwise, swing the limbs yourself
432
+ node('arm_l').rotation.x = Math.sin(t * 6) * 0.6;
433
+ node('arm_r').rotation.x = -Math.sin(t * 6) * 0.6;
434
+ node('leg_l').rotation.x = -Math.sin(t * 6) * 0.5;
435
+ node('leg_r').rotation.x = Math.sin(t * 6) * 0.5;
436
+ };
437
+ }
438
+ }
439
+ ```
440
+
441
+ Clip names are the model maker's, not a contract — play one only by a name you checked for, or
442
+ animate the named nodes, which every model in the family has.
443
+
444
+ **Other players' avatars** come the same way on every surface that shows a player: a room member,
445
+ a leaderboard row, a friend. Each carries `avatars3d` — one per family — and the flat
446
+ `avatar3dId`, `avatar3dModelUrl` and `avatar3dIconUrl` of your first family. A server reads
447
+ `avatars_3d` and `avatar_3d_id` from the room read; nothing tells a server a player changed, so
448
+ when `onChange` fires, have your client tell your server and have the server **re-read the room**
449
+ — never trust an id the client sends.
450
+
451
+ ## 8. Moving from 2D items, achievements and the `arcade-avatars` tag
452
+
453
+ Games that drew characters before 3D avatars existed did it with 2D portrait items granted by
454
+ achievements, a bundled copy of each character's model keyed on `avatarIconId`, and the store tag
455
+ `arcade-avatars`. Move to 3D avatars like this:
456
+
457
+ 1. **Upload one `small_human` avatar per old character** with `gemctl item upload`, as in
458
+ section 4.
459
+ 2. **Ask the platform team to map each old 2D item to its new avatar.** The platform then grants
460
+ every player who owns the 2D item the matching 3D avatar, and wears it for a player whose 3D
461
+ slot is empty and who wears that portrait. Players keep their 2D portraits.
462
+ 3. **Declare `avatars_3d: { families: [small_human] }` on every channel** (section 3), and **remove
463
+ the achievements and progressions that granted the 2D items**.
464
+ 4. **Put the avatars on sale** with `gemctl item sell`, and **make the ones players win
465
+ awardable** and award them from your server (section 5).
466
+ 5. **Ship a build that reads `gem.avatars3d.worn()` and loads the model at runtime** (section 7),
467
+ and delete the bundled character models and the `avatarIconId` mapping.
468
+ 6. **Remove the `arcade-avatars` tag from your store page.** It is deprecated. For a game whose
469
+ config declares no families the Arcade still reads the tag as accepting `small_human`, which
470
+ is what keeps an unmoved game working while you switch; do not rely on it once you have — a
471
+ declared family is what the Arcade reads.
package/docs/brand-art.md CHANGED
@@ -283,7 +283,7 @@ decision of that date); the ruling's other rules are unchanged. In rules:
283
283
 
284
284
  - **Keep no Gems counter.** Your game shows no Gems balance, running total or wallet of its own, on
285
285
  any screen. The arcade's wallet is the only place Gems accrue, and the only place a total is shown
286
- — in the Gem Arcade, the Gem menu, which the player opens from the Gem button at the top-left.
286
+ — in the Gem Arcade, the Gem menu, which the player opens from the Gem button.
287
287
  - **Draw no Gems flight or celebration of your own.** No particles flying to a corner, no counting-up
288
288
  number, no burst of gems. `gem.cueReward` is the animation, and the arcade draws it.
289
289
  - **Show no Gems amount at all when you are not connected to the arcade.** Without the arcade there is
@@ -308,8 +308,8 @@ decision of that date); the ruling's other rules are unchanged. In rules:
308
308
 
309
309
  A Tickets payout is cued the same way, from the ticket, with `currencySlug: 'ARCADE_TICKETS'`. The
310
310
  slug decides what flies — gems or tickets — and which total the platform moves. In the Gem Arcade
311
- every payout flies into the Gem button at the top-left of your frame; no wallet is drawn over
312
- your game.
311
+ every payout flies into the Gem button, in whichever corner of your frame it sits; no wallet is
312
+ drawn over your game.
313
313
  - **If `cueReward` returns `false`, do nothing.** There is no fallback celebration. `false` means the
314
314
  arcade will not show the payout here — the bridge is not up, or the embedder has no wallet — and
315
315
  your game does not replace it with one of its own. Your reward screen still says what was earned;
package/docs/changelog.md CHANGED
@@ -16,6 +16,43 @@ 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.1.0
20
+
21
+ Upgrading from 13.0.0. Impact: required.
22
+
23
+ ### Rebuild
24
+
25
+ - Avatar3dFamily now also accepts adult_human alongside small_human. Recompile your game. If any code switches over the family and assumes small_human is the only named value, add handling for adult_human or a sensible default.
26
+ - The list of messages a frame sends to its parent, the FrameMessage type, and the GemCapability type each gained a member for the Gem button corner (the gem.gemCorner message and the gemCorner capability). Recompile your game. If your code matches these unions exhaustively, add a case for the new member.
27
+
28
+ ### New capability
29
+
30
+ - Your game can now ask the Arcade to move its Gem button to another corner by calling setGemCorner, using the GemCorner type for the value. Check that the gemCorner capability is available before relying on it, since the Arcade decides whether to honour the request.
31
+ - The adult_human avatar rig family is now available, and AVATAR_RIGS exposes the node and parent trees of both the small_human and adult_human rigs, described by the AvatarRigNode and AvatarRigs types. The package also ships a reference rig for adult_human, reachable through a new rigs export. Use these to attach items or animations to the right nodes of either family.
32
+
33
+ ## 13.0.0
34
+
35
+ Upgrading from 12.0.0. Impact: required.
36
+
37
+ ### Must change
38
+
39
+ - 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.
40
+ - 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.
41
+ - 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.
42
+
43
+ ### Rebuild
44
+
45
+ - 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.
46
+ - 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.
47
+
48
+ ### New capability
49
+
50
+ - 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.
51
+ - 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.
52
+ - The game configuration can now declare the 3D avatar families the game supports through avatar_3d_families.
53
+ - 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.
54
+ - 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.
55
+
19
56
  ## 12.0.0
20
57
 
21
58
  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.1.0"
54
54
  }
55
55
  ```
56
56
 
@@ -178,7 +178,8 @@ gem.frame.progress(loaded, total); // while assets load
178
178
  gem.frame.ready(); // once the player can actually play
179
179
  ```
180
180
 
181
- And draw a settings button of your own — top-right is the recommendation — whose values start from
181
+ And draw a settings button of your own — top-right, with your Back button just to its left
182
+ ([the top of the screen](what-a-game-must-do.md#the-top-of-the-screen)) — whose values start from
182
183
  the player's Arcade-wide defaults, `gem.prefs.recommended`, until the player chooses their own
183
184
  ([what a game must do](what-a-game-must-do.md#game-settings)). The Arcade's menu does not open
184
185
  your settings. Outside the Arcade there is no Arcade menu at all, so also give players a way out — a
@@ -590,7 +590,7 @@ the source progression from a function the player calls, on a progression that o
590
590
 
591
591
  **Platform-currency display belongs to the arcade** (the creator's ruling, 2026-09-24) — Gems and
592
592
  Tickets alike. The arcade shows the player's platform currencies in its own chrome — in the Gem
593
- Arcade, the Gem menu the player opens from the Gem button at the top-left of your frame; no wallet
593
+ Arcade, the Gem menu the player opens from the Gem button over your frame; no wallet
594
594
  is drawn over your game — and moves a total when the platform confirms a grant. Your game keeps no counter or balance
595
595
  of a platform currency of its own, draws no flight or celebration of its own, and shows no
596
596
  platform-currency amount at all when it is not connected to the arcade.
@@ -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": "7269a0dbd515e2c52e63f9fad2b3028e136396522aa68aa177c44f7eceb2c0e0",
22
+ "updated": "2026-10-14T19:48:50Z"
23
+ },
19
24
  {
20
25
  "file": "arcade-economy.md",
21
26
  "sha256": "9058c26bc11266f235dfa3c72c80a3fc962aa9f204ef491006e6d34545f5eeeb",
@@ -28,13 +33,13 @@
28
33
  },
29
34
  {
30
35
  "file": "brand-art.md",
31
- "sha256": "87267dca53e5506b1aee77ac9819aef8025c8e0de2780e65a2ea3b7236a9c0ab",
32
- "updated": "2026-10-11T18:48:12Z"
36
+ "sha256": "44ba566da97e499f80b6c4898a5546bf7fb3dfba2290da9aa3e8d4e6056a7cce",
37
+ "updated": "2026-10-14T21:18:16Z"
33
38
  },
34
39
  {
35
40
  "file": "changelog.md",
36
- "sha256": "bf2786be31121bb229d10d98f701165aefb2e7166271299450c926401661dfd2",
37
- "updated": "2026-10-12T22:12:20Z"
41
+ "sha256": "3041e5a29aec35ed4a1d299410d9a0795a7c48958f314a6497ca2b567ccaf000",
42
+ "updated": "2026-10-14T22:00:06Z"
38
43
  },
39
44
  {
40
45
  "file": "cross-promotion.md",
@@ -68,13 +73,13 @@
68
73
  },
69
74
  {
70
75
  "file": "getting-started.md",
71
- "sha256": "19bf05cd8f7afe5e9f406af6aef090a73bb3c2fb713f06185ead36fc2bb104dc",
72
- "updated": "2026-10-12T22:12:20Z"
76
+ "sha256": "651392ad1b3bb28bd43cfb0a0df0a88b3d8c2e46a4bbb1cb9fcbbe762459f22e",
77
+ "updated": "2026-10-14T22:00:06Z"
73
78
  },
74
79
  {
75
80
  "file": "inventory-and-catalogs.md",
76
- "sha256": "fecd1299ef19bc50ea17abe96bbe49f62dc366a9fc0f354c843a42b2d5a2f9e2",
77
- "updated": "2026-10-11T18:48:12Z"
81
+ "sha256": "4251f451c48ab80bb7a9c50a6a717dfca6e057ff128d32ecb5e2cd13d28c5b7d",
82
+ "updated": "2026-10-14T21:18:16Z"
78
83
  },
79
84
  {
80
85
  "file": "leaderboards.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": "c83325c8471f0ea2578f3476660751ad7f4fd96a51eb902336e57d7553ec5e46",
132
+ "updated": "2026-10-14T22:00:06Z"
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": "7006e2e2900d71417002937d51a8d10ad9bcfdf02cd1b27f0f923e4cbe4ee79b",
157
+ "updated": "2026-10-14T21:18:16Z"
153
158
  }
154
159
  ]
155
160
  }