@indiegems/gem-web-sdk 12.0.0 → 13.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/dist/function.d.ts +9 -1
- package/dist/game.d.ts +39 -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 +36 -14
- package/dist/sdk.d.ts +49 -14
- package/dist/server-api.d.ts +9 -1
- package/dist/server.d.ts +10 -1
- package/docs/api/api-reference-function.md +162 -2
- package/docs/api/api-reference-game.md +529 -163
- package/docs/api/api-reference-server.md +1 -1
- package/docs/api/reference.json +773 -176
- package/docs/arcade-avatars.md +303 -0
- package/docs/changelog.md +23 -0
- package/docs/getting-started.md +1 -1
- package/docs/manifest.json +15 -10
- package/docs/saving-player-progress.md +16 -10
- package/docs/server-bundles.md +2 -2
- package/docs/what-a-game-must-do.md +53 -46
- package/package.json +1 -1
- package/schema/bridge.v1.json +198 -18
- package/schema/capabilities.public.json +6 -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,29 @@ Its entries are grouped under up to five headings:
|
|
|
16
16
|
- **Rebuild**: a type changed. Recompile against the new version; code that compiles today keeps compiling unless the entry says otherwise.
|
|
17
17
|
- **New capability**: something a game can now do. Nothing changes unless you use it.
|
|
18
18
|
|
|
19
|
+
## 13.0.0
|
|
20
|
+
|
|
21
|
+
Upgrading from 12.0.0. Impact: required.
|
|
22
|
+
|
|
23
|
+
### Must change
|
|
24
|
+
|
|
25
|
+
- The controls declaration is gone. Remove every call that declares controls, along with any code that builds a controls declaration or a control choice, because these no longer exist and the game will not compile against them. Follow the updated settings guidance for how the game now exposes settings and controls.
|
|
26
|
+
- If you embed the game in your own host, remove the controls-declare callback from the host options and callbacks, because the host no longer receives that message and the property is no longer accepted.
|
|
27
|
+
- setMenu no longer takes a settings flag as its first argument. Pass only the optional list of menu items. Also remove any onSettings handler from the game menu, since that handler is gone.
|
|
28
|
+
|
|
29
|
+
### Rebuild
|
|
30
|
+
|
|
31
|
+
- The purchase refusal code gains a new value, already_owned, for purchases of something the player already has. Recompile; if the game switches over refusal codes exhaustively, add a branch for it.
|
|
32
|
+
- The list of messages a game frame sends to its parent, the frame message type, and the capability names each gain an entry for 3D avatar purchases, including the new avatar3dPurchases capability. Recompiling is enough unless the game or host switches over these values exhaustively.
|
|
33
|
+
|
|
34
|
+
### New capability
|
|
35
|
+
|
|
36
|
+
- A game can now work with 3D avatars. The new avatars3d API lists the avatar rig families the game supports, reports the avatar a player wears per family, lists avatar offers and their prices, starts a purchase, and notifies the game when the worn avatar changes. Purchases are also available through the frame, gated by the avatar3dPurchases capability, and are answered like other purchases. Outside the Arcade, avatar purchases are unavailable immediately.
|
|
37
|
+
- The worn 3D avatar, one per rig family, is now available on the current player and on other players' data, including friends, leaderboard entries, profiles, showcase friends, player identities and room members. Use it to render other players with the avatar they wear.
|
|
38
|
+
- The game configuration can now declare the 3D avatar families the game supports through avatar_3d_families.
|
|
39
|
+
- If you embed games in your own host, you can now supply worn 3D avatars when sending player information, handle 3D avatar purchase requests with the new request callback, and validate those requests with the new validator and message type.
|
|
40
|
+
- Server-side code can now read the worn 3D avatars in avatars_3d on player and subject identity data, using the new worn avatar type.
|
|
41
|
+
|
|
19
42
|
## 12.0.0
|
|
20
43
|
|
|
21
44
|
Upgrading from 11.1.2. Impact: required.
|
package/docs/getting-started.md
CHANGED
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",
|
|
@@ -118,13 +123,13 @@
|
|
|
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",
|
|
@@ -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
|
}
|
|
@@ -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
|
|
|
@@ -891,13 +891,31 @@ arrives.
|
|
|
891
891
|
## Game settings
|
|
892
892
|
|
|
893
893
|
**Your settings belong to your game: draw a settings button of your own, and the screen it opens.**
|
|
894
|
-
The Arcade
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
894
|
+
The Arcade's menu never opens your settings — it holds the player's _Arcade-wide_ settings
|
|
895
|
+
instead — so a game with no button of its own has settings nobody can reach.
|
|
896
|
+
|
|
897
|
+
This is the whole model, and every other page that mentions settings links here:
|
|
898
|
+
|
|
899
|
+
- **The Arcade's settings are the player's Arcade-wide defaults for this device.** Your game reads
|
|
900
|
+
them from `gem.prefs.recommended` (and `muted` and `reducedMotion` from `gem.prefs`) and applies
|
|
901
|
+
them again on `onPrefs`. The Arcade never asks your game anything about them, and never shows
|
|
902
|
+
anything your game says.
|
|
903
|
+
- **Every game draws its own settings button, top-right, and its own settings screen.** The
|
|
904
|
+
Arcade's menu never opens it.
|
|
905
|
+
- **A setting starts from `recommended` until the player changes it in your game, and then the
|
|
906
|
+
player's choice in your game wins.** Save only what the player chose, never a recommended value.
|
|
907
|
+
- **Where a choice is kept.** Device configuration — volume, graphics quality, look sensitivity, a
|
|
908
|
+
touch-stick style — goes in `gem.deviceSettings`; a choice that must follow the player across
|
|
909
|
+
devices — difficulty, key bindings, accessibility toggles the game owns — goes in
|
|
910
|
+
`gem.me.settings`.
|
|
911
|
+
- **Mute and vibration are in your settings screen too**, wired both ways: your mute follows
|
|
912
|
+
`prefs.muted` and reports the player's own with `gem.reportMuted`
|
|
913
|
+
([sound](#sound-your-own-mute-control-and-the-arcades)), and your vibration toggle shows only
|
|
914
|
+
while `gem.vibration.available` ([vibration](vibration.md)).
|
|
915
|
+
|
|
916
|
+
**The button goes in the top-right corner**, the same corner in every game, so a player never has
|
|
917
|
+
to look for it. Place it with the reserved-area properties so it never sits under an Arcade
|
|
918
|
+
control, whatever the Arcade keeps in that corner on this device:
|
|
901
919
|
|
|
902
920
|
```css
|
|
903
921
|
#settings-button {
|
|
@@ -931,8 +949,8 @@ same push as `muted` and `reducedMotion`, and again whenever the player changes
|
|
|
931
949
|
| `graphicsQualityAuto` | `true` / `false` | `true`: the Arcade picked that tier for this device, so you may adapt from it. `false`: the player picked it, so hold it. |
|
|
932
950
|
|
|
933
951
|
`lookSensitivity` to `touchMovement` are the camera and controls settings: see
|
|
934
|
-
[camera and look controls](#camera-and-look-controls) for
|
|
935
|
-
|
|
952
|
+
[camera and look controls](#camera-and-look-controls) for `gem.controls.look()`, which applies the
|
|
953
|
+
two sensitivities and the invert for you. An
|
|
936
954
|
older Arcade sends `recommended` without them; the SDK then fills each with its default (`100`,
|
|
937
955
|
`100`, `false`, `'floating'`). The last two are the starting graphics quality: see
|
|
938
956
|
[graphics quality](#graphics-quality-the-arcade-picks-the-start-your-game-adapts). An older Arcade
|
|
@@ -981,8 +999,11 @@ volumeSlider.oninput = () => {
|
|
|
981
999
|
};
|
|
982
1000
|
```
|
|
983
1001
|
|
|
984
|
-
|
|
985
|
-
|
|
1002
|
+
`chosen` here is this device's configuration, so it is kept in
|
|
1003
|
+
[device settings](#device-settings-what-this-device-remembers-about-your-game). A choice that must
|
|
1004
|
+
follow the player to another device — a difficulty, key bindings, an accessibility toggle your game
|
|
1005
|
+
owns — starts from `recommended` the same way and is kept in `gem.me.settings` instead
|
|
1006
|
+
([saving player progress](saving-player-progress.md#for-something-small-reach-for-settings-instead)).
|
|
986
1007
|
|
|
987
1008
|
**Read `muted` and `reducedMotion` as before.** They are not recommendations and they did not move:
|
|
988
1009
|
`muted` is the Arcade's mute, which the SDK applies for you
|
|
@@ -995,30 +1016,12 @@ If your game turns a camera — a drag on a phone, a mouse under pointer lock, a
|
|
|
995
1016
|
stick — the player has four settings for it in the Arcade, per device, the way Roblox players do:
|
|
996
1017
|
**look sensitivity**, **gamepad look sensitivity**, **invert look up/down** and **touch movement**
|
|
997
1018
|
(a floating or a fixed thumbstick). They reach you in `gem.prefs.recommended`
|
|
998
|
-
([the table above](#start-from-the-players-arcade-wide-defaults-gemprefsrecommended))
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
means you apply the Arcade's value (or the player's own choice in your settings, which still wins);
|
|
1003
|
-
`'game'` means your game decides it, and the Arcade's Settings shows that row as "Set by this game";
|
|
1004
|
-
leave out a setting your game does not use, and the Arcade says so beside it. Call it once at boot:
|
|
1005
|
-
|
|
1006
|
-
```js
|
|
1007
|
-
gem.controls.declare({
|
|
1008
|
-
lookSensitivity: 'player',
|
|
1009
|
-
gamepadLookSensitivity: 'player',
|
|
1010
|
-
invertLookY: 'player',
|
|
1011
|
-
touchMovement: 'game', // this game always draws a fixed stick
|
|
1012
|
-
});
|
|
1013
|
-
```
|
|
1014
|
-
|
|
1015
|
-
It is screen state, like [`gem.setWalletVisible`](#the-arcades-wallet-in-the-gem-menu-never-drawn-by-you):
|
|
1016
|
-
each call is the whole answer, the latest wins, and the SDK holds it and sends it again after a
|
|
1017
|
-
reconnect. It returns `false` before the bridge is up (it is held and sent on connect) and under an
|
|
1018
|
-
embedder that does not show these settings; it never throws. A key or a value other than these is
|
|
1019
|
-
dropped.
|
|
1019
|
+
([the table above](#start-from-the-players-arcade-wide-defaults-gemprefsrecommended)) as the
|
|
1020
|
+
player's Arcade-wide defaults, and, like every [game setting](#game-settings), they are where your
|
|
1021
|
+
own settings start. There is nothing to tell the Arcade about them: its Settings show the player's
|
|
1022
|
+
defaults, never what a game uses.
|
|
1020
1023
|
|
|
1021
|
-
**
|
|
1024
|
+
**Turn look input into a camera turn — `gem.controls.look()`.** It is pure math: it listens to
|
|
1022
1025
|
nothing and moves no camera. Give it the deltas your input code already has; it returns radians to
|
|
1023
1026
|
**add**, `yaw` positive turning right and `pitch` positive looking up, at Roblox's base speeds
|
|
1024
1027
|
(`gem.controls.LOOK_BASE`: touch 1° across and 0.66° up/down per CSS px, mouse 0.5° and 0.385°,
|
|
@@ -1067,15 +1070,16 @@ function frame(dt) {
|
|
|
1067
1070
|
Do not copy them into variables at boot.
|
|
1068
1071
|
- **The player's choice in your game wins.** If your settings screen has its own look speed or
|
|
1069
1072
|
invert, pass it — `gem.controls.look({ sensitivity: 1.5, invertY: true })`, or later
|
|
1070
|
-
`look.set({ sensitivity: 1.5 })` — and that input stops following the Arcade.
|
|
1073
|
+
`look.set({ sensitivity: 1.5 })` — and that input stops following the Arcade. Keep it in
|
|
1074
|
+
`gem.deviceSettings`: a look speed is this device's configuration.
|
|
1071
1075
|
`look.set({ sensitivity: undefined })` hands it back. `sensitivity` and `gamepadSensitivity` are
|
|
1072
1076
|
multipliers (`1` is the base speed); the Arcade's percentages are divided by 100 for you.
|
|
1073
1077
|
- **Pass `pitch` for touch**, the camera's current pitch in radians (0 at the horizon, positive
|
|
1074
1078
|
looking up), or the slowdown near vertical is skipped. A mouse and a stick are never slowed.
|
|
1075
1079
|
- **`touchMovement` is yours to draw.** The SDK has no thumbstick: read
|
|
1076
|
-
`gem.prefs.recommended?.touchMovement` and draw a floating or a fixed one
|
|
1077
|
-
|
|
1078
|
-
- **No camera, no call.** A game with no camera to turn
|
|
1080
|
+
`gem.prefs.recommended?.touchMovement` and draw a floating or a fixed one. A game with only one
|
|
1081
|
+
kind draws that one and ignores the field.
|
|
1082
|
+
- **No camera, no call.** A game with no camera to turn never calls `look`.
|
|
1079
1083
|
|
|
1080
1084
|
## Graphics quality: the Arcade picks the start, your game adapts
|
|
1081
1085
|
|
|
@@ -1150,10 +1154,8 @@ gem.setMenu({
|
|
|
1150
1154
|
});
|
|
1151
1155
|
```
|
|
1152
1156
|
|
|
1153
|
-
- **
|
|
1154
|
-
|
|
1155
|
-
passes it keeps working, and another embedder may still show an entry for it — but in the Arcade
|
|
1156
|
-
it adds nothing.
|
|
1157
|
+
- **Your settings are not in it.** They open from [your own button](#game-settings), and the
|
|
1158
|
+
Arcade's menu never opens them.
|
|
1157
1159
|
- **`items` are up to four entries of your own**, shown in a group below the Arcade's. `id` is your
|
|
1158
1160
|
name for the entry (1 to 32 characters); `label` is what the player reads, **already in their
|
|
1159
1161
|
language** (`gem.prefs.locale`) — the Arcade does not translate it, cuts it at 40 characters, and
|
|
@@ -1460,9 +1462,12 @@ This is a platform rule, not advice — see
|
|
|
1460
1462
|
|
|
1461
1463
|
### Arcade avatars: the character the player wears, and changing it mid-game
|
|
1462
1464
|
|
|
1463
|
-
Some games draw **Arcade avatars
|
|
1464
|
-
|
|
1465
|
-
|
|
1465
|
+
Some games draw **Arcade avatars**: the player's look in your game is something they own and wear
|
|
1466
|
+
in the Arcade. **The whole guide for 3D avatars — rig families, declaring the ones your game
|
|
1467
|
+
accepts, uploading and selling your own, the purchase flow and loading the model — is
|
|
1468
|
+
[arcade-avatars.md](arcade-avatars.md).** This section is the summary. The Arcade's avatar picker
|
|
1469
|
+
has **two tabs**, and they are **two separate worn choices** — wearing one never changes the
|
|
1470
|
+
other:
|
|
1466
1471
|
|
|
1467
1472
|
| Tab | What the player wears | On `gem.player` and every room member |
|
|
1468
1473
|
|---|---|---|
|
|
@@ -1476,6 +1481,8 @@ are **two separate worn choices** — wearing one never changes the other:
|
|
|
1476
1481
|
(an idle, say). It never changes at its URL, so cache it by URL.
|
|
1477
1482
|
- **`avatar3dIconUrl`** is a rendered portrait of that character, for a roster or a menu where
|
|
1478
1483
|
loading the model is too much. It is **not** the 2D portrait.
|
|
1484
|
+
- **`avatars3d`** lists every worn 3D character, one per **rig family**; the three fields above
|
|
1485
|
+
are your game's first declared family's. `gem.avatars3d.worn()` reads it for you.
|
|
1479
1486
|
|
|
1480
1487
|
**Draw the 3D character, falling back to what you did before.** A game that drew characters
|
|
1481
1488
|
before the 3D tab existed keyed its own models on `avatarIconId`. Keep that mapping as the
|
|
@@ -1522,7 +1529,7 @@ is missing now (a player who takes off their 3D character has no `avatar3dId`).
|
|
|
1522
1529
|
|
|
1523
1530
|
**A "Change avatar" button belongs in your own UI**, and `gem.openAvatarPicker()` opens the
|
|
1524
1531
|
Arcade's picker from it. Pass `{ tab: '3d' }` or `{ tab: '2d' }` to open on a tab; omit it and
|
|
1525
|
-
the Arcade decides (a game
|
|
1532
|
+
the Arcade decides (a game that declares avatar families gets the 3D tab). The Arcade pauses your game
|
|
1526
1533
|
while the picker is open, saves the player's choice, sends the player again, and resumes you:
|
|
1527
1534
|
|
|
1528
1535
|
```js
|