@indiegems/gem-web-sdk 10.1.0 → 10.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1310,6 +1310,44 @@ your own page. It carries a day count and never a date, and nothing about what t
1310
1310
  This is a platform rule, not advice — see
1311
1311
  [platform rules §8](platform-rules.md#8-a-players-boost-is-for-display-never-a-gate).
1312
1312
 
1313
+ ### Arcade avatars: the character the player wears, and changing it mid-game
1314
+
1315
+ Some games draw **Arcade avatars**: whole characters the player owns as Arcade portrait items and
1316
+ wears as their Arcade portrait (store tag `arcade-avatars`). Such a game keys its own model on
1317
+ **`avatarIconId`**, the worn item's id — on `gem.player` for the signed-in player, as on every room
1318
+ member — rather than on the thumbnail.
1319
+
1320
+ **Redraw from `onPlayer`, not from boot alone.** The Arcade sends the player again whenever the
1321
+ portrait they wear changes, including mid-game from the Arcade's own menu, without your game
1322
+ asking. `onPlayer` runs and `gem.player` holds the new value; a field missing from the new player
1323
+ is missing now.
1324
+
1325
+ **A "Change avatar" button belongs in your own UI**, and `gem.openAvatarPicker()` opens the
1326
+ Arcade's picker from it. The Arcade pauses your game while the picker is open, saves the player's
1327
+ choice, sends the player again, and resumes you:
1328
+
1329
+ ```js
1330
+ const gem = createGem({ onPlayer: (p) => hero.wear(avatarOfItem(p.avatarIconId)) });
1331
+
1332
+ changeAvatar.hidden = !gem.avatarPickerAvailable; // read it live
1333
+ changeAvatar.onclick = () => {
1334
+ if (!gem.openAvatarPicker()) changeAvatar.hidden = true;
1335
+ };
1336
+ ```
1337
+
1338
+ - **`gem.avatarPickerAvailable`** says whether the Arcade will open its picker here. It is `false`
1339
+ outside the Arcade, before the bridge is up, and under an embedder that has no picker.
1340
+ - **`gem.openAvatarPicker()`** returns `true` when the Arcade was asked and `false` when it was not
1341
+ (the same cases, or more than six asks a minute). It never throws and never waits.
1342
+ - **Nothing answers it.** The outcome is the new player through `onPlayer`. A player who closes the
1343
+ picker unchanged produces nothing, so never wait for an answer or poll `gem.player`.
1344
+
1345
+ **Your server is not told.** A server-authoritative game reads each member's `avatar_icon_id` from
1346
+ the room read (`GET /v1/games/{game_id}/game-rooms/{game_room_id}`, or `gem.room.get()` on a
1347
+ dedicated server), and nothing pushes a change to it. So when `onPlayer` brings a different
1348
+ `avatarIconId`, have your client tell your server, and have the server **re-read the room** —
1349
+ never trust the id the client sends.
1350
+
1313
1351
  ## Which backend — no longer yours, and what the override is for
1314
1352
 
1315
1353
  The Arcade names the API root on every `gem.token`, and the SDK sends there. Your game never types
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indiegems/gem-web-sdk",
3
- "version": "10.1.0",
3
+ "version": "10.2.0",
4
4
  "description": "postMessage bridge contract between gem-web and browser-playable games",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -547,7 +547,17 @@
547
547
  "",
548
548
  "ADDITIVE, so no `v` bump: every new field is optional (an older parent",
549
549
  "sends none of them and must still be accepted), and an older frame",
550
- "ignores fields it does not know (rule 4)."
550
+ "ignores fields it does not know (rule 4).",
551
+ "",
552
+ "RE-SENT WHENEVER IT CHANGES, and always the whole message. The parent",
553
+ "sends it after the handshake, again whenever the identity changes (a",
554
+ "sign-in in place), and again whenever the WORN PORTRAIT changes — the",
555
+ "player picked another one in the parent's own avatar picker, whether a",
556
+ "game opened it (`gem.requestAvatarPicker`) or the player did, mid-game,",
557
+ "from the parent's menu. A frame replaces its whole copy with each one; a",
558
+ "field absent from the latest message is absent now. There is no other",
559
+ "signal that the portrait changed, and no reply to the request that",
560
+ "opened the picker: this message is the outcome."
551
561
  ],
552
562
  "fields": {
553
563
  "untrusted": {
@@ -586,6 +596,30 @@
586
596
  "optional": true,
587
597
  "$comment": "The portrait's THUMBNAIL (gem-api `avatar_icon_url`). Absent means no portrait; a parent never sends an empty string. Structurally validated only — https (or loopback), no userinfo, parsed rather than regex-matched. It is NOT origin-pinned: the validator is a pure function with no configuration, so it cannot know the CDN origin. An earlier version of this comment claimed a pin that the generated code did not emit — a documented control that did not exist. THE CONSUMER MUST PIN THE ORIGIN before putting this in an <img src> — the SDK's `platformIconUrl` does — or a hostile parent supplies a request-time beacon carrying the Referer."
588
598
  },
599
+ "avatarIconId": {
600
+ "kind": "string",
601
+ "maxLength": 64,
602
+ "optional": true,
603
+ "$comment": [
604
+ "The item-definition id of the portrait the player WEARS (gem-api",
605
+ "`avatar_icon_id`, a UUID) — the same id a game-room member's",
606
+ "`avatar_icon_id` carries, so a game that draws whole characters as",
607
+ "Arcade avatars (store tag `arcade-avatars`) can map it to its own",
608
+ "model rather than to the thumbnail.",
609
+ "",
610
+ "Platform-assigned, so it sits on the trusted side of the `untrusted`",
611
+ "split, beside `playerId` and the badges' `itemId`. Absent means no",
612
+ "portrait is worn, or an older parent; a parent never sends an empty",
613
+ "string. Sent with `avatarIconUrl` and describing the same item.",
614
+ "",
615
+ "BOUNDED HERE, SHAPE-CHECKED BY THE FRAME. The schema has no pattern",
616
+ "kind, and a format here would cost a frame the WHOLE message for one",
617
+ "bad id — its name and portrait with it. So the validator bounds the",
618
+ "length only, and the frame keeps the value only when it is a UUID",
619
+ "(`8-4-4-4-12` hex, either case), dropping anything else to absent,",
620
+ "the way it drops a non-digit `discriminator`."
621
+ ]
622
+ },
589
623
  "badges": {
590
624
  "kind": "array",
591
625
  "maxItems": 2,
@@ -2673,6 +2707,64 @@
2673
2707
  }
2674
2708
  },
2675
2709
 
2710
+ {
2711
+ "type": "gem.requestAvatarPicker",
2712
+ "direction": "frameToParent",
2713
+ "transport": "port",
2714
+ "$comment": [
2715
+ "Ask the arcade to open its own avatar picker over the game, so the",
2716
+ "player can change the portrait they wear without leaving it — the",
2717
+ "\"Change avatar\" button of a game that draws Arcade avatars. Gated by",
2718
+ "the `avatarPicker` capability: sent only to a parent that advertised",
2719
+ "it.",
2720
+ "",
2721
+ "WHY THE PARENT OPENS IT. Wearing a portrait is an account act: the",
2722
+ "account surface refuses the game-scoped token by construction, and the",
2723
+ "list of portraits a player owns is theirs, not the game's. So the game",
2724
+ "ASKS, and the parent, which holds the account session, renders the",
2725
+ "picker in its own chrome and saves the choice.",
2726
+ "",
2727
+ "NO FIELDS, AND NO REPLY. The game names nothing — not an item, not a",
2728
+ "filter, not a line of text: which portraits are offered and what the",
2729
+ "picker says are the parent's. And nothing answers this message. THE",
2730
+ "OUTCOME IS A NEW `gem.player`: when the worn portrait changes, the",
2731
+ "parent re-sends it with the new `avatarIconId` and `avatarIconUrl`, as",
2732
+ "it does for a change the player makes from the parent's menu without",
2733
+ "the game asking. A reply would be a second copy of an identity fact",
2734
+ "(`gem.signInResult`'s argument), and the picker may stay open for as",
2735
+ "long as the player browses, which no wait should be sized for. A",
2736
+ "player who closes it unchanged produces nothing at all.",
2737
+ "",
2738
+ "WHAT THE PARENT OWES, none of which this schema can enforce:",
2739
+ "",
2740
+ " ADVERTISE IT ONLY WHERE IT OPENS. `avatarPicker` is the game's cue to",
2741
+ " show its own Change avatar button, so a parent with no picker on this",
2742
+ " surface does not advertise it. A player who cannot wear a portrait",
2743
+ " yet (no account) is the parent's to explain in its own chrome.",
2744
+ "",
2745
+ " PAUSE THE GAME while the picker is open (`gem.pause`) and lift it when",
2746
+ " the picker closes, returning focus to the frame. When the portrait",
2747
+ " changed, send `gem.player` BEFORE lifting the pause, so the game",
2748
+ " redraws before it runs again.",
2749
+ "",
2750
+ " ONE AT A TIME. An ask arriving while the picker, or any other sheet",
2751
+ " that holds the stage, is open is dropped. A parent may also drop an",
2752
+ " ask that arrives without transient user activation in the frame, or",
2753
+ " too soon after the player closed the last one.",
2754
+ "",
2755
+ " SEND NOTHING BACK for a refusal — no error, no acknowledgement.",
2756
+ "",
2757
+ "NOT AN AUTHORIZATION CONTROL. The player's own choice in the parent's",
2758
+ "picker is the control, and the account-session save is the",
2759
+ "enforcement."
2760
+ ],
2761
+ "fields": {},
2762
+ "rateLimit": {
2763
+ "perMinute": 6,
2764
+ "$comment": "Bounds a BUGGY game. NOT a security control: a hostile game posts on the port it holds rather than calling the SDK. What stops a game nagging is the parent's one-at-a-time rule and its freedom to drop an ask."
2765
+ }
2766
+ },
2767
+
2676
2768
  {
2677
2769
  "type": "gem.vibrate",
2678
2770
  "direction": "frameToParent",
@@ -197,6 +197,12 @@
197
197
  "gem.requestShare"
198
198
  ]
199
199
  },
200
+ "avatarPicker": {
201
+ "implemented": true,
202
+ "gates": [
203
+ "gem.requestAvatarPicker"
204
+ ]
205
+ },
200
206
  "vibration": {
201
207
  "implemented": true,
202
208
  "gates": [