@indiegems/gem-web-sdk 13.0.0 → 13.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +4 -3
  3. package/dist/function.d.ts +2 -1
  4. package/dist/game.d.ts +37 -2
  5. package/dist/gem-web-sdk.bridge.js +1 -1
  6. package/dist/gem-web-sdk.esm.js +2 -2
  7. package/dist/gem-web-sdk.function.js +2 -2
  8. package/dist/gem-web-sdk.game.js +2 -2
  9. package/dist/gem-web-sdk.min.js +2 -2
  10. package/dist/gem-web-sdk.sdk.js +2 -2
  11. package/dist/gem-web-sdk.server-api.js +1 -1
  12. package/dist/gem-web-sdk.server.js +2 -2
  13. package/dist/index.d.ts +39 -5
  14. package/dist/sdk.d.ts +37 -2
  15. package/dist/server-api.d.ts +1 -0
  16. package/dist/server.d.ts +1 -0
  17. package/docs/analytics.md +6 -3
  18. package/docs/api/api-reference-function.md +38 -7
  19. package/docs/api/api-reference-game.md +381 -33
  20. package/docs/api/api-reference-server.md +23 -2
  21. package/docs/api/reference.json +401 -25
  22. package/docs/arcade-avatars.md +194 -26
  23. package/docs/brand-art.md +3 -3
  24. package/docs/changelog.md +30 -0
  25. package/docs/deprecation-policy.md +6 -0
  26. package/docs/getting-started.md +9 -5
  27. package/docs/inventory-and-catalogs.md +1 -1
  28. package/docs/manifest.json +29 -29
  29. package/docs/platform-moments.md +266 -0
  30. package/docs/rewarded-ads.md +26 -9
  31. package/docs/server-bundles.md +2 -2
  32. package/docs/serverless-functions-js.md +17 -1
  33. package/docs/serverless-functions.md +13 -7
  34. package/docs/what-a-game-must-do.md +126 -70
  35. package/package.json +6 -2
  36. package/rigs/adult_human.glb +0 -0
  37. package/schema/bridge.v1.json +160 -7
  38. package/schema/capabilities.public.json +16 -0
  39. package/schema/moment-log-line.json +109 -0
  40. package/docs/cross-promotion.md +0 -232
@@ -24,10 +24,46 @@ nothing, and nothing else.
24
24
 
25
25
  Everything your game calls is on [`gem.avatars3d`](api/api-reference-game.md#gem-avatars3d).
26
26
 
27
- ## 2. Rig families, and the `small_human` rig
27
+ ## 2. Rig families
28
28
 
29
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`:
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`
31
67
 
32
68
  ```
33
69
  root
@@ -40,29 +76,155 @@ root
40
76
  └── leg_r
41
77
  ```
42
78
 
43
- The platform checks every `small_human` model when it is uploaded and refuses one that breaks any
44
- of these:
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.
45
82
 
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.
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
59
220
 
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` |
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 |
66
228
 
67
229
  ## 3. Declare the families your game accepts
68
230
 
@@ -85,6 +247,10 @@ gemctl config promote <version> --channel release --yes
85
247
 
86
248
  - **The order is meaningful.** The first family is your default: it is the one the flat
87
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.
88
254
  - **Each channel reads its own config.** A channel whose config declares no families accepts
89
255
  none: the Arcade shows no 3D picker in your game there, and nothing is for sale.
90
256
  - **Read it back** with `gem.avatars3d.families()`, which resolves the declared list from the
@@ -97,8 +263,9 @@ gemctl item upload red-knight.glb --family small_human --portrait red-knight.png
97
263
  --slug red-knight --name "Red Knight" --description "Sworn to the hearth."
98
264
  ```
99
265
 
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
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
102
269
  renders the item, and the command waits until it is `ready`.
103
270
 
104
271
  - **An avatar cannot be changed after upload.** A fixed model is a new item with a new slug — the
@@ -249,7 +416,8 @@ gem.avatars3d.onChange(wear);
249
416
  there you see your default look.
250
417
  - **A model URL never changes what it serves**, so cache by URL. When one fails to load, show your
251
418
  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:
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:
253
421
 
254
422
  ```js
255
423
  function setCharacter(scene, clips) {
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,36 @@ 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.2.0
20
+
21
+ Upgrading from 13.1.0. Impact: required.
22
+
23
+ ### Rebuild
24
+
25
+ - The message-type lists and unions between game and Arcade gained the moment request and moment result messages, and the capability type gained moments. Recompiling is enough. If your code switches exhaustively over frame messages, parent messages or capabilities, add handling for the new members so it still compiles.
26
+
27
+ ### New capability
28
+
29
+ - A game can now signal a platform moment at a natural break through Gem.moments. You pass one of a fixed set of moment kinds and an optional placement, and the answer says whether a moment was shown. The call never rejects. If the SDK refuses locally, for example because the host does not support moments, the argument is invalid, ready has not been called, a request is already in flight, or the call was repeated or timed out, the answer is not shown. Check the moments capability before relying on it. Rate limits and the settle time are the same as for ad requests.
30
+ - On the frame side, GemFrame gains signalMoment for sending a moment request to the Arcade, and readyCalled so you can tell whether ready has been reported yet. Signalling before ready is refused. Frame options and callbacks gain onMomentResult to receive the answer.
31
+ - If you build a host that embeds games, for example a development host, you can now receive moment requests through onSignalMoment in the host options and callbacks, and answer them with answerMoment. The new moment request and result message types come with validators for incoming messages.
32
+ - The package now ships a JSON schema describing the log line that development hosts write for moments. Use it if your tooling reads or checks those log lines.
33
+ - Request options gain retryRateLimited, which defaults to true so existing behaviour is unchanged. Set it to false on a call if you want a rate-limit response handed straight back instead of being retried by the transport. Other failures are still retried. This applies in the browser SDK and the server package.
34
+
35
+ ## 13.1.0
36
+
37
+ Upgrading from 13.0.0. Impact: required.
38
+
39
+ ### Rebuild
40
+
41
+ - 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.
42
+ - 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.
43
+
44
+ ### New capability
45
+
46
+ - 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.
47
+ - 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.
48
+
19
49
  ## 13.0.0
20
50
 
21
51
  Upgrading from 12.0.0. Impact: required.
@@ -99,12 +99,18 @@ deprecated.
99
99
  | `MatchHosting.stop` | `MatchHosting.end` when the match is over, `MatchHosting.leave` to hand it to another host | `5.0.0` | Package API surface |
100
100
  | `GemPlayer.displayHandle` | `PlayerIdentity.displayTag` — the same value | `7.0.0` | Package API surface |
101
101
  | `GemPlayer.avatarUrl` | `PlayerIdentity.avatarIconUrl` — the same value | `7.0.0` | Package API surface |
102
+ | `AdRequest.format: 'interstitial'` | `gem.moments.signal` — a platform moment at the same break, from the press that leaves it | `14.0.0` | Package API surface |
102
103
  | `profile(playerId)` | The room read's per-member display identity | Not a package release | Runtime delivery only |
103
104
 
104
105
  **"For the member" is the split from section 3**, and it is the row's real answer: `4.0.0` is
105
106
  the earliest release that may take the property off the type, and it says nothing about when a
106
107
  server stops putting a value in it.
107
108
 
109
+ **The interstitial row is one value of a field, not a whole symbol.** `format: 'rewarded'` is
110
+ not deprecated, so the reference cannot mark `AdRequest.format` itself; the notice is in the
111
+ field's description and in [`platform-moments.md`](platform-moments.md#12-moving-off-format-interstitial).
112
+ It is still package surface, and section 2 governs it unchanged.
113
+
108
114
  The last row is the one that is only a guide's. `profile(playerId)` is marked deprecated in
109
115
  [`server-bundles.md`](server-bundles.md) and carries no mark in the generated reference, so
110
116
  there is no package release to name for it and section 3 is its whole rule.
@@ -50,7 +50,7 @@ either way.
50
50
  ```jsonc
51
51
  // package.json
52
52
  "dependencies": {
53
- "@indiegems/gem-web-sdk": "13.0.0"
53
+ "@indiegems/gem-web-sdk": "13.2.0"
54
54
  }
55
55
  ```
56
56
 
@@ -67,9 +67,12 @@ is involved, and nothing is built when you install.
67
67
  dedicated server bundle and a listen host all import the same path and the same names.
68
68
  `.`, `/game` and `/server` stay published as narrow doors over the same modules — a game
69
69
  wants none of them unless it knows why. `/function` is the fourth door, for code that runs as
70
- a serverless function ([serverless-functions-js.md](serverless-functions-js.md)). The function
71
- starter's build resolves `/sdk` and `/game` to it, so verification code shared with your game
72
- keeps its import.
70
+ a serverless function written in JavaScript
71
+ ([serverless-functions-js.md](serverless-functions-js.md)). JavaScript functions are disabled
72
+ for now, due to performance issues: write a new function in Rust
73
+ ([serverless-functions.md](serverless-functions.md)). A JavaScript function already published
74
+ keeps running, and its build resolves `/sdk` and `/game` to `/function`, so verification code
75
+ shared with your game keeps its import.
73
76
 
74
77
  **`dependencies`, not `devDependencies`.** The SDK is part of what your game ships — your
75
78
  bundler compiles it into the browser build and into the server bundle — so a `--production`
@@ -178,7 +181,8 @@ gem.frame.progress(loaded, total); // while assets load
178
181
  gem.frame.ready(); // once the player can actually play
179
182
  ```
180
183
 
181
- And draw a settings button of your own — top-right is the recommendation — whose values start from
184
+ And draw a settings button of your own — top-right, with your Back button just to its left
185
+ ([the top of the screen](what-a-game-must-do.md#the-top-of-the-screen)) — whose values start from
182
186
  the player's Arcade-wide defaults, `gem.prefs.recommended`, until the player chooses their own
183
187
  ([what a game must do](what-a-game-must-do.md#game-settings)). The Arcade's menu does not open
184
188
  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.
@@ -13,13 +13,13 @@
13
13
  },
14
14
  {
15
15
  "file": "analytics.md",
16
- "sha256": "57fb07c3d649c2758a35660707d31b05164b363e9a425156978aed394d086339",
17
- "updated": "2026-10-02T14:04:37Z"
16
+ "sha256": "06bc70771dec1fe04451b93f1c55643759edb389927227fae67f246e0b147af6",
17
+ "updated": "2026-10-15T00:02:19Z"
18
18
  },
19
19
  {
20
20
  "file": "arcade-avatars.md",
21
- "sha256": "355bba7936ec7f0a65d52ec65a6add323c75a992d931de9cd23319ed9d8aeb52",
22
- "updated": "2026-10-14T06:27:58Z"
21
+ "sha256": "7269a0dbd515e2c52e63f9fad2b3028e136396522aa68aa177c44f7eceb2c0e0",
22
+ "updated": "2026-10-14T19:48:50Z"
23
23
  },
24
24
  {
25
25
  "file": "arcade-economy.md",
@@ -33,18 +33,13 @@
33
33
  },
34
34
  {
35
35
  "file": "brand-art.md",
36
- "sha256": "87267dca53e5506b1aee77ac9819aef8025c8e0de2780e65a2ea3b7236a9c0ab",
37
- "updated": "2026-10-11T18:48:12Z"
36
+ "sha256": "44ba566da97e499f80b6c4898a5546bf7fb3dfba2290da9aa3e8d4e6056a7cce",
37
+ "updated": "2026-10-14T21:18:16Z"
38
38
  },
39
39
  {
40
40
  "file": "changelog.md",
41
- "sha256": "914fb511d6f15af9ccf0d71e2309dcaf35369d4e4272233927ab7e48b47921a3",
42
- "updated": "2026-10-14T06:33:54Z"
43
- },
44
- {
45
- "file": "cross-promotion.md",
46
- "sha256": "92d4a1ffe19ceaaae1812bfdb77ebbdc04a7eca878ff28995eb6c388c7e876b3",
47
- "updated": "2026-10-08T16:19:25Z"
41
+ "sha256": "f8f5f19344ac446a047ffa9862c33b0a1b580df19350b02120a5803489c29e5a",
42
+ "updated": "2026-10-15T00:36:54Z"
48
43
  },
49
44
  {
50
45
  "file": "daily-challenges.md",
@@ -58,8 +53,8 @@
58
53
  },
59
54
  {
60
55
  "file": "deprecation-policy.md",
61
- "sha256": "2923c4af22f266bb545e0218efb3ffb3c7cc7184ea538e431b6b87352c6b9e51",
62
- "updated": "2026-09-24T16:41:50Z"
56
+ "sha256": "8134d5463e2737116ed23341bf177c51cede4bd86dabec54d76a82c5bfdfade1",
57
+ "updated": "2026-10-14T22:38:02Z"
63
58
  },
64
59
  {
65
60
  "file": "friends.md",
@@ -73,13 +68,13 @@
73
68
  },
74
69
  {
75
70
  "file": "getting-started.md",
76
- "sha256": "6aef9e454064492b2164417e1b31050e0f487c48edab07d5b58b5df9bb089bff",
77
- "updated": "2026-10-14T06:33:54Z"
71
+ "sha256": "2d6b7037380aa843e0f3179071fc3d2bd8d7398d851d0d4d0a2d1a7ce1f87d5d",
72
+ "updated": "2026-10-15T00:36:54Z"
78
73
  },
79
74
  {
80
75
  "file": "inventory-and-catalogs.md",
81
- "sha256": "fecd1299ef19bc50ea17abe96bbe49f62dc366a9fc0f354c843a42b2d5a2f9e2",
82
- "updated": "2026-10-11T18:48:12Z"
76
+ "sha256": "4251f451c48ab80bb7a9c50a6a717dfca6e057ff128d32ecb5e2cd13d28c5b7d",
77
+ "updated": "2026-10-14T21:18:16Z"
83
78
  },
84
79
  {
85
80
  "file": "leaderboards.md",
@@ -96,6 +91,11 @@
96
91
  "sha256": "3887bc706d098014cefab84e9b8bd5b964841888c242a7ccb1f799a9b9723fc0",
97
92
  "updated": "2026-09-24T01:00:22Z"
98
93
  },
94
+ {
95
+ "file": "platform-moments.md",
96
+ "sha256": "c9909452a49fe50bac2266e56e5a44ad6495b8464ce1b5c889d0be39604bbff0",
97
+ "updated": "2026-10-14T22:43:26Z"
98
+ },
99
99
  {
100
100
  "file": "platform-rules.md",
101
101
  "sha256": "b259bcebc6ae12b1db9c7279a6e2adb174af9d60090bf31001e23616f6423fbf",
@@ -108,8 +108,8 @@
108
108
  },
109
109
  {
110
110
  "file": "rewarded-ads.md",
111
- "sha256": "f1f88f7d3aab2b1a1df2cecab59846625907e0391542291f1fbba56ea98d6476",
112
- "updated": "2026-10-08T16:19:25Z"
111
+ "sha256": "ab1096ca610b8f009f0aeb17c573a17fbd2ce6567963285f7b2f73c072170dc2",
112
+ "updated": "2026-10-14T22:38:02Z"
113
113
  },
114
114
  {
115
115
  "file": "room-shapes.md",
@@ -128,18 +128,18 @@
128
128
  },
129
129
  {
130
130
  "file": "server-bundles.md",
131
- "sha256": "8b19574001cefea73b1892dd864d360dc0e5dfd252e9d291f7ccb08a170b48d9",
132
- "updated": "2026-10-14T06:33:54Z"
131
+ "sha256": "adc06113f0412812ce09dfd53039247dd77db4ea702d25f02092258e21e32690",
132
+ "updated": "2026-10-15T00:36:54Z"
133
133
  },
134
134
  {
135
135
  "file": "serverless-functions-js.md",
136
- "sha256": "3904704418f9c6c246c8e1fa83f96901f6e493a3f4499b16fd6845620515e57d",
137
- "updated": "2026-10-12T17:39:05Z"
136
+ "sha256": "0436dafdc5dcb036d45073ea9c9d4cbebdb5880decc7db8012e04c3b2cfc1016",
137
+ "updated": "2026-10-14T23:28:53Z"
138
138
  },
139
139
  {
140
140
  "file": "serverless-functions.md",
141
- "sha256": "84881bcc40c6203c731ebf9092be575158d0ad3b3a65120321b695ae6558f63f",
142
- "updated": "2026-10-12T17:39:05Z"
141
+ "sha256": "ca2ed41db94acdd3553126a98395172f6f960199ce514b5ad8c877c5f5bdbe78",
142
+ "updated": "2026-10-14T23:28:53Z"
143
143
  },
144
144
  {
145
145
  "file": "trading-and-gifting.md",
@@ -153,8 +153,8 @@
153
153
  },
154
154
  {
155
155
  "file": "what-a-game-must-do.md",
156
- "sha256": "ca29417cbe0c337efe44c3d6f44f943ff27425fbb11e208faff1207d2b6f912c",
157
- "updated": "2026-10-13T14:23:52Z"
156
+ "sha256": "976cf27a12ab1ff86f685e3d4e6a0b444858ab3d780e8466c682f06121ddee2b",
157
+ "updated": "2026-10-14T22:38:02Z"
158
158
  }
159
159
  ]
160
160
  }