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