@indiegems/gem-web-sdk 11.1.2 → 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.
- package/CHANGELOG.md +37 -0
- package/README.md +3 -1
- package/dist/function.d.ts +9 -1
- package/dist/game.d.ts +88 -13
- package/dist/gem-web-sdk.bridge.js +1 -1
- package/dist/gem-web-sdk.esm.js +2 -2
- package/dist/gem-web-sdk.function.js +2 -2
- package/dist/gem-web-sdk.game.js +2 -2
- package/dist/gem-web-sdk.min.js +2 -2
- package/dist/gem-web-sdk.sdk.js +2 -2
- package/dist/gem-web-sdk.server-api.js +1 -1
- package/dist/gem-web-sdk.server.js +2 -2
- package/dist/index.d.ts +100 -16
- package/dist/sdk.d.ts +98 -14
- package/dist/server-api.d.ts +9 -1
- package/dist/server.d.ts +10 -1
- package/docs/api/api-reference-function.md +8076 -0
- package/docs/api/api-reference-game.md +719 -165
- package/docs/api/api-reference-server.md +5 -3
- package/docs/api/reference.json +10310 -225
- package/docs/arcade-avatars.md +303 -0
- package/docs/changelog.md +37 -0
- package/docs/getting-started.md +5 -2
- package/docs/manifest.json +21 -16
- package/docs/rooms-and-matches.md +64 -1
- package/docs/saving-player-progress.md +16 -10
- package/docs/server-bundles.md +2 -2
- package/docs/serverless-functions-js.md +118 -44
- package/docs/serverless-functions.md +26 -11
- package/docs/what-a-game-must-do.md +53 -46
- package/package.json +1 -1
- package/schema/bridge.v1.json +282 -18
- package/schema/capabilities.public.json +12 -0
|
@@ -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,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.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
|
+
|
|
42
|
+
## 12.0.0
|
|
43
|
+
|
|
44
|
+
Upgrading from 11.1.2. Impact: required.
|
|
45
|
+
|
|
46
|
+
### Rebuild
|
|
47
|
+
|
|
48
|
+
- The list of capability names gained ticketEvents. Recompile your game. If your code switches exhaustively over capability names, add a case for the new one.
|
|
49
|
+
- The list of messages the parent sends to the frame, and the union of parent message types, each gained a ticket event message. Recompile your game. If your code switches exhaustively over these message names or types, handle or ignore the new one.
|
|
50
|
+
|
|
51
|
+
### New capability
|
|
52
|
+
|
|
53
|
+
- A party member's game can now show the matchmaking ticket its party is queued on. Read the ticket through the rooms manager, or listen for the new partyQueued room event. The view is read-only: it has a status, a party size, two instants and an outcome, and it cannot be cancelled. A matched ticket does not carry the room. The room is reported separately once the SDK has confirmed the player is seated in it. The view ends on its own as lost once the ticket's lifetime has passed with no ending heard. For the player who queued, the ticket still comes from the platform's own answer.
|
|
54
|
+
- Games that build their own frame can now pass an onTicketEvent callback to receive ticket state changes pushed by the parent. It is delivered only when the parent relays ticket events and advertises the new ticketEvents capability. Under a parent that does not, nothing changes and the callback is never called. Ticket events name no player and do not carry the templates a ticket named.
|
|
55
|
+
|
|
19
56
|
## 11.1.2
|
|
20
57
|
|
|
21
58
|
Upgrading from 11.1.1. Impact: none.
|
package/docs/getting-started.md
CHANGED
|
@@ -50,7 +50,7 @@ either way.
|
|
|
50
50
|
```jsonc
|
|
51
51
|
// package.json
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@indiegems/gem-web-sdk": "
|
|
53
|
+
"@indiegems/gem-web-sdk": "13.0.0"
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
@@ -66,7 +66,10 @@ is involved, and nothing is built when you install.
|
|
|
66
66
|
**`/sdk` is the import, from every seat.** It carries the whole surface, so client code, a
|
|
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
|
-
wants none of them unless it knows why.
|
|
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
73
|
|
|
71
74
|
**`dependencies`, not `devDependencies`.** The SDK is part of what your game ships — your
|
|
72
75
|
bundler compiles it into the browser build and into the server bundle — so a `--production`
|
package/docs/manifest.json
CHANGED
|
@@ -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": "
|
|
37
|
-
"updated": "2026-10-
|
|
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": "
|
|
72
|
-
"updated": "2026-10-
|
|
76
|
+
"sha256": "6aef9e454064492b2164417e1b31050e0f487c48edab07d5b58b5df9bb089bff",
|
|
77
|
+
"updated": "2026-10-14T06:33:54Z"
|
|
73
78
|
},
|
|
74
79
|
{
|
|
75
80
|
"file": "inventory-and-catalogs.md",
|
|
@@ -113,28 +118,28 @@
|
|
|
113
118
|
},
|
|
114
119
|
{
|
|
115
120
|
"file": "rooms-and-matches.md",
|
|
116
|
-
"sha256": "
|
|
117
|
-
"updated": "2026-10-
|
|
121
|
+
"sha256": "29cf42a4334d3d9702e6a7f6e04c52fb2b414af3340043c2f98b2bfafa7dc64d",
|
|
122
|
+
"updated": "2026-10-12T19:59:08Z"
|
|
118
123
|
},
|
|
119
124
|
{
|
|
120
125
|
"file": "saving-player-progress.md",
|
|
121
|
-
"sha256": "
|
|
122
|
-
"updated": "2026-10-
|
|
126
|
+
"sha256": "11ab0684247cfc43d6d8faba6dbb897192354af5f5622b16fd96abcb156ca0cd",
|
|
127
|
+
"updated": "2026-10-12T21:21:05Z"
|
|
123
128
|
},
|
|
124
129
|
{
|
|
125
130
|
"file": "server-bundles.md",
|
|
126
|
-
"sha256": "
|
|
127
|
-
"updated": "2026-10-
|
|
131
|
+
"sha256": "8b19574001cefea73b1892dd864d360dc0e5dfd252e9d291f7ccb08a170b48d9",
|
|
132
|
+
"updated": "2026-10-14T06:33:54Z"
|
|
128
133
|
},
|
|
129
134
|
{
|
|
130
135
|
"file": "serverless-functions-js.md",
|
|
131
|
-
"sha256": "
|
|
132
|
-
"updated": "2026-10-
|
|
136
|
+
"sha256": "3904704418f9c6c246c8e1fa83f96901f6e493a3f4499b16fd6845620515e57d",
|
|
137
|
+
"updated": "2026-10-12T17:39:05Z"
|
|
133
138
|
},
|
|
134
139
|
{
|
|
135
140
|
"file": "serverless-functions.md",
|
|
136
|
-
"sha256": "
|
|
137
|
-
"updated": "2026-10-
|
|
141
|
+
"sha256": "84881bcc40c6203c731ebf9092be575158d0ad3b3a65120321b695ae6558f63f",
|
|
142
|
+
"updated": "2026-10-12T17:39:05Z"
|
|
138
143
|
},
|
|
139
144
|
{
|
|
140
145
|
"file": "trading-and-gifting.md",
|
|
@@ -148,8 +153,8 @@
|
|
|
148
153
|
},
|
|
149
154
|
{
|
|
150
155
|
"file": "what-a-game-must-do.md",
|
|
151
|
-
"sha256": "
|
|
152
|
-
"updated": "2026-10-
|
|
156
|
+
"sha256": "ca29417cbe0c337efe44c3d6f44f943ff27425fbb11e208faff1207d2b6f912c",
|
|
157
|
+
"updated": "2026-10-13T14:23:52Z"
|
|
153
158
|
}
|
|
154
159
|
]
|
|
155
160
|
}
|
|
@@ -140,6 +140,13 @@ frame that advertises `autoStartFailure`. It is not a sixth `gem.roomEvent` beca
|
|
|
140
140
|
refuses an enum value it does not know, and reports that to the game as a protocol error; an unknown
|
|
141
141
|
message type it ignores. The game sees it as `matchAutoStartFailed` (§4.4).
|
|
142
142
|
|
|
143
|
+
**The matchmaking-ticket pushes cross the same way**, as `gem.ticketEvent`, to a frame that
|
|
144
|
+
advertises `ticketEvents`. The platform tells every player a ticket seats when it is created,
|
|
145
|
+
matched, or ended without a match, and the parent relays each one for the frame's own game: the
|
|
146
|
+
ticket's id and new status, plus the party size and the two instants on creation, the reason on an
|
|
147
|
+
ending without a match, and the room to look in on a match. The templates the ticket named do not
|
|
148
|
+
cross, and no player is named on either side of the bridge. What a game does with it is §5c.
|
|
149
|
+
|
|
143
150
|
There is exactly one filtering point and this is it. The engine keeps a full match event in native
|
|
144
151
|
code and hands its scripting layer an allowlisted subset, because native code can hold a secret
|
|
145
152
|
from a sandboxed script. **The frame-side SDK cannot** — it shares a JavaScript realm with the game,
|
|
@@ -794,11 +801,15 @@ every game.
|
|
|
794
801
|
platform sends the push a moment before it marks the ticket. So on a page that relays, a match is
|
|
795
802
|
noticed in about the time the push takes, not at the next heartbeat. A tab returning to the
|
|
796
803
|
foreground is the same cue. Like every relay it is a hint: nothing is entered on its strength.
|
|
804
|
+
**A relayed ticket push is the same cue**, so an ending without a match is noticed as quickly as a
|
|
805
|
+
match is. The ticket's outcome still comes from the platform's answer, never from the push.
|
|
797
806
|
- **It refuses a second thing in flight.** While a ticket waits, `create`, `join`, `joinByCode`,
|
|
798
807
|
`quickJoin` and another `queueForMatch` are refused locally.
|
|
799
808
|
- **It cancels the ticket when the tab closes.** See below.
|
|
800
809
|
- **It raises no `seated` for the ticket's own room.** The owner is told by the outcome. Party
|
|
801
|
-
members, who hold no ticket, are told by `seated` as they are for any room their leader enters
|
|
810
|
+
members, who hold no ticket, are told by `seated` as they are for any room their leader enters —
|
|
811
|
+
and, on a page that relays ticket events, by `partyQueued` while the party waits. See
|
|
812
|
+
[A party member's view](#a-party-members-view-of-the-ticket).
|
|
802
813
|
|
|
803
814
|
### Queueing from inside a room
|
|
804
815
|
|
|
@@ -862,6 +873,58 @@ seat stands, so `myRooms()` shows the room and `join(id)` enters it.
|
|
|
862
873
|
`cancel()` resolves the ticket's real outcome, and that may be `matched`. Backing out from there is
|
|
863
874
|
`gem.rooms.leave()`.
|
|
864
875
|
|
|
876
|
+
### A party member's view of the ticket
|
|
877
|
+
|
|
878
|
+
A party queues as **one ticket, and it is the leader's**. Only the leader calls `queueForMatch()`,
|
|
879
|
+
and only a ticket's owner can read it, keep it alive or cancel it. So a member's game holds no
|
|
880
|
+
ticket — and without help it knows nothing about the queue its player is standing in until the match
|
|
881
|
+
pulls them in.
|
|
882
|
+
|
|
883
|
+
On an arcade page that relays ticket events, the member's game is told. When the leader queues,
|
|
884
|
+
`gem.rooms.partyTicket` is set and `partyQueued` is raised with it:
|
|
885
|
+
|
|
886
|
+
```js
|
|
887
|
+
gem.rooms.on('partyQueued', async (ticket) => {
|
|
888
|
+
showSearching({ expiresAt: ticket.expiresAt, canCancel: false });
|
|
889
|
+
const outcome = await ticket.outcome;
|
|
890
|
+
if (outcome.status !== 'matched') backToMenu(outcome.status);
|
|
891
|
+
// `matched`: the room arrives through your `seated` handler.
|
|
892
|
+
});
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
- **It is a view. There is no `cancel()`.** The ticket is the leader's to cancel. A member who wants
|
|
896
|
+
out leaves the party, which ends the ticket for everyone with reason `party_changed`. Draw the
|
|
897
|
+
searching screen without a leave-queue button.
|
|
898
|
+
- **It costs no request.** Nothing is heartbeated and nothing is polled for it; everything the view
|
|
899
|
+
says came from a relayed push.
|
|
900
|
+
- **`matched` carries no room.** The room is reported the way a party's seat always is, the `seated`
|
|
901
|
+
event, and that handler is where a game joins it. The SDK looks for the seat as soon as it hears
|
|
902
|
+
`matched`, and reports it only after reading the room over the game's own token and finding this
|
|
903
|
+
player in it.
|
|
904
|
+
- **It ends by itself.** The pushes are sent once. A member's game that missed the ending would
|
|
905
|
+
otherwise show a searching screen for ever, so the view ends as `lost` a few seconds after the
|
|
906
|
+
ticket's lifetime has run out with nothing more heard. A seat found by the SDK's own seat check
|
|
907
|
+
ends it as `matched`, whether or not that push arrived.
|
|
908
|
+
- **Never set beside `currentTicket`.** The player who queued holds the ticket itself and gets no
|
|
909
|
+
`partyQueued`.
|
|
910
|
+
|
|
911
|
+
| `status` | `reason` | What happened |
|
|
912
|
+
| --- | --- | --- |
|
|
913
|
+
| `matched` | — | The party is seated and the match is starting. `seated` has the room. |
|
|
914
|
+
| `expired` | `timed_out` | Nobody to play with before the ticket's expiry. |
|
|
915
|
+
| `expired` | `heartbeat_lapsed` | The leader's game stopped keeping the ticket alive: a closed, throttled or offline tab. |
|
|
916
|
+
| `cancelled` | `requested` | The leader cancelled. |
|
|
917
|
+
| `cancelled` | `party_changed` | The party gained or lost a member while it waited. |
|
|
918
|
+
| `failed` | `handoff_failed` | A match formed and could not be started, several times over. |
|
|
919
|
+
| `lost` | — | The SDK's own conclusion: the ticket's lifetime ran out and no ending was heard. |
|
|
920
|
+
|
|
921
|
+
⚠️ **Build it as an extra, never as a step.** The view exists only where the page relays ticket
|
|
922
|
+
events, and only for a game that was already running when the leader queued: a push sent before the
|
|
923
|
+
game was listening is not sent again. In every other case `partyTicket` stays `null`, no
|
|
924
|
+
`partyQueued` is raised, and the member is carried into the match by `seated` with no lead-up. That
|
|
925
|
+
path has to keep working, so the `seated` handler is what enters the match and the searching screen
|
|
926
|
+
is only something shown on the way.
|
|
927
|
+
|
|
865
928
|
### The refusals when queueing
|
|
866
929
|
|
|
867
930
|
`queueForMatch()` itself rejects with `GemApiError` when no ticket was made. Branch on `code`:
|
|
@@ -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
|
-
|
|
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.
|
|
371
|
-
|
|
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
|
|
384
|
-
|
|
385
|
-
about **this screen
|
|
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,
|
|
437
|
-
| a volume, a quality
|
|
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
|
|
package/docs/server-bundles.md
CHANGED
|
@@ -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/
|
|
84
|
-
curl -fsSL https://downloads.gemarcade.com/sdk/
|
|
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
|
|