@indiegems/gem-web-sdk 11.1.2 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  # The browser surface — API reference
2
2
 
3
- Generated from `@indiegems/gem-web-sdk/sdk` v11.1.2, the version of the package this file ships in. Every symbol below carries an explicit anchor, its `anchor` in `reference.json` beside this file.
3
+ Generated from `@indiegems/gem-web-sdk/sdk` v13.0.0, the version of the package this file ships in. Every symbol below carries an explicit anchor, its `anchor` in `reference.json` beside this file.
4
4
 
5
5
  `@indiegems/gem-web-sdk/sdk` is the one import a game writes, wherever its code runs. `@indiegems/gem-web-sdk/game` exports exactly the symbols on this page and nothing else, for a build that wants only this half.
6
6
 
@@ -3669,6 +3669,262 @@ The runtime companion to [PlatformEventName](#platformeventname), for a game tha
3669
3669
  show what it gets for free, or to check a funnel step it authored against
3670
3670
  what the platform actually emits.
3671
3671
 
3672
+ <a id="avatar3dprice"></a>
3673
+
3674
+ ## Avatar3dPrice
3675
+
3676
+ ```ts
3677
+ interface Avatar3dPrice
3678
+ ```
3679
+
3680
+ One price of a 3D avatar on sale, in one platform currency.
3681
+
3682
+ <a id="avatar3dprice-currencyslug"></a>
3683
+
3684
+ ## Avatar3dPrice.currencySlug
3685
+
3686
+ ```ts
3687
+ readonly currencySlug: string
3688
+ ```
3689
+
3690
+ `GEMS` or `ARCADE_TICKETS` — what to pass as `currencySlug`.
3691
+
3692
+ <a id="avatar3dprice-price"></a>
3693
+
3694
+ ## Avatar3dPrice.price
3695
+
3696
+ ```ts
3697
+ readonly price: number
3698
+ ```
3699
+
3700
+ The price, a whole number of that currency.
3701
+
3702
+ <a id="avatar3doffer"></a>
3703
+
3704
+ ## Avatar3dOffer
3705
+
3706
+ ```ts
3707
+ interface Avatar3dOffer
3708
+ ```
3709
+
3710
+ One of your game's 3D avatars on sale, as the platform prices it.
3711
+
3712
+ <a id="avatar3doffer-itemid"></a>
3713
+
3714
+ ## Avatar3dOffer.itemId
3715
+
3716
+ ```ts
3717
+ readonly itemId: string
3718
+ ```
3719
+
3720
+ The avatar's item id — what [Avatars3dApi.purchase](#avatars3dapi-purchase) takes.
3721
+
3722
+ <a id="avatar3doffer-slug"></a>
3723
+
3724
+ ## Avatar3dOffer.slug
3725
+
3726
+ ```ts
3727
+ readonly slug: string
3728
+ ```
3729
+
3730
+ The slug you uploaded it under. A key, not display text.
3731
+
3732
+ <a id="avatar3doffer-family"></a>
3733
+
3734
+ ## Avatar3dOffer.family
3735
+
3736
+ ```ts
3737
+ readonly family: Avatar3dFamily
3738
+ ```
3739
+
3740
+ The rig family the model is built on.
3741
+
3742
+ <a id="avatar3doffer-displayname"></a>
3743
+
3744
+ ## Avatar3dOffer.displayName
3745
+
3746
+ ```ts
3747
+ readonly displayName: string
3748
+ ```
3749
+
3750
+ The avatar's name, as you uploaded it.
3751
+
3752
+ <a id="avatar3doffer-description"></a>
3753
+
3754
+ ## Avatar3dOffer.description
3755
+
3756
+ ```ts
3757
+ readonly description?: string
3758
+ ```
3759
+
3760
+ The avatar's description, when it has one.
3761
+
3762
+ <a id="avatar3doffer-iconurl"></a>
3763
+
3764
+ ## Avatar3dOffer.iconUrl
3765
+
3766
+ ```ts
3767
+ readonly iconUrl: string
3768
+ ```
3769
+
3770
+ A rendered portrait of the avatar, for a shop tile. Pass it through
3771
+ `platformIconUrl` before putting it in an `<img src>`.
3772
+
3773
+ <a id="avatar3doffer-modelurl"></a>
3774
+
3775
+ ## Avatar3dOffer.modelUrl
3776
+
3777
+ ```ts
3778
+ readonly modelUrl: string
3779
+ ```
3780
+
3781
+ The avatar's model, for a preview. Pass it through `platformIconUrl`
3782
+ before loading it.
3783
+
3784
+ <a id="avatar3doffer-prices"></a>
3785
+
3786
+ ## Avatar3dOffer.prices
3787
+
3788
+ ```ts
3789
+ readonly prices: readonly Avatar3dPrice[]
3790
+ ```
3791
+
3792
+ One or two prices, each in a currency a player can pay with. Show each
3793
+ one the player can choose; pass the chosen one's `currencySlug` to
3794
+ [Avatars3dApi.purchase](#avatars3dapi-purchase).
3795
+
3796
+ <a id="avatars3dapi"></a>
3797
+
3798
+ ## Avatars3dApi
3799
+
3800
+ ```ts
3801
+ interface Avatars3dApi
3802
+ ```
3803
+
3804
+ Arcade 3D avatars — `gem.avatars3d`. What the player wears, the rig families
3805
+ your game accepts, the avatars you sell, and buying one.
3806
+
3807
+ ```js
3808
+ function draw() {
3809
+ const worn = gem.avatars3d.worn();
3810
+ if (worn) loadCharacter(platformIconUrl(worn.modelUrl));
3811
+ else useDefaultCharacter();
3812
+ }
3813
+ draw();
3814
+ gem.avatars3d.onChange(draw);
3815
+ ```
3816
+
3817
+ The guide is `docs/arcade-avatars.md`: the rig, the config, uploading and
3818
+ selling with gemctl, and loading the model at runtime.
3819
+
3820
+ <a id="avatars3dapi-families"></a>
3821
+
3822
+ ## Avatars3dApi.families
3823
+
3824
+ ```ts
3825
+ families(): Promise<readonly Avatar3dFamily[]>
3826
+ ```
3827
+
3828
+ The rig families your game accepts, in the order your published config
3829
+ declares them (`avatars_3d.families`) — the first is your default.
3830
+
3831
+ Resolves `[]` when your game has no published config on this channel, or
3832
+ its config declares no families. Loads the config through
3833
+ `gem.gameConfig.load()` and shares that one fetch; rejects when the config
3834
+ cannot be loaded, as `load()` does.
3835
+
3836
+ <a id="avatars3dapi-worn"></a>
3837
+
3838
+ ## Avatars3dApi.worn
3839
+
3840
+ ```ts
3841
+ worn(family?: Avatar3dFamily): WornAvatar3d | undefined
3842
+ ```
3843
+
3844
+ The 3D avatar the player wears — in `family` when you name one, otherwise
3845
+ in your game's first declared family that has one. `undefined` when none
3846
+ is worn there, before the player arrives, and outside the Arcade: draw
3847
+ your default look.
3848
+
3849
+ Read live from [Gem.player](#gem-player), so it changes when the player wears
3850
+ another avatar or buys one; [Avatars3dApi.onChange](#avatars3dapi-onchange) tells you when.
3851
+
3852
+ | Parameter | Type |
3853
+ | --- | --- |
3854
+ | `family` | `Avatar3dFamily` |
3855
+
3856
+ <a id="avatars3dapi-offers"></a>
3857
+
3858
+ ## Avatars3dApi.offers
3859
+
3860
+ ```ts
3861
+ offers(): Promise<readonly Avatar3dOffer[]>
3862
+ ```
3863
+
3864
+ Your game's 3D avatars on sale on this channel, with their prices, read
3865
+ from the platform with your game's own token. Render your shop from this,
3866
+ never from prices of your own.
3867
+
3868
+ Resolves `[]` when you have nothing on sale. Rejects with a `GemApiError`
3869
+ when the platform cannot be read — a shop that cannot load says so.
3870
+
3871
+ <a id="avatars3dapi-purchase"></a>
3872
+
3873
+ ## Avatars3dApi.purchase
3874
+
3875
+ ```ts
3876
+ purchase(request: Avatar3dPurchaseRequest): Promise<PurchaseOutcome>
3877
+ ```
3878
+
3879
+ Buy one of your 3D avatars for the player and put it on. The Arcade
3880
+ prices it again, shows its own confirmation sheet, takes the currency and
3881
+ wears the avatar; your game then receives the player again, with the new
3882
+ avatar worn, and [Avatars3dApi.onChange](#avatars3dapi-onchange) runs.
3883
+
3884
+ Call it from the player's own click or key press — the Arcade shows no
3885
+ sheet without one and answers `declined` / `busy`.
3886
+
3887
+ NOTHING REJECTS. Every outcome resolves, exactly as on
3888
+ `gem.purchases.request`:
3889
+
3890
+ - `ok` — bought and worn, and the platform confirmed the player owns it.
3891
+ - `declined` — definitely not bought. `reason` says why:
3892
+ `already_owned` (the player has it — offer to wear it with
3893
+ `gem.openAvatarPicker({ tab: '3d' })`), `cancelled`, `insufficient_funds`,
3894
+ `no_price_in_currency`, `not_purchasable` (not on sale, or not an id from
3895
+ [Avatars3dApi.offers](#avatars3dapi-offers)), `busy`, and the rest of
3896
+ `PurchaseRefusalCode`. Return to your shop silently.
3897
+ - `unavailable` — no reliable answer. Show one neutral message, never
3898
+ retry on your own, and read [Avatars3dApi.worn](#avatars3dapi-worn) again.
3899
+
3900
+ **Outside the Arcade** — opened at the top level of a page with no Arcade
3901
+ around it, or under an Arcade that does not sell 3D avatars yet — it
3902
+ resolves `unavailable` with reason `error` at once and asks nobody.
3903
+
3904
+ | Parameter | Type |
3905
+ | --- | --- |
3906
+ | `request` | `Avatar3dPurchaseRequest` |
3907
+
3908
+ <a id="avatars3dapi-onchange"></a>
3909
+
3910
+ ## Avatars3dApi.onChange
3911
+
3912
+ ```ts
3913
+ onChange(listener: (worn: readonly WornAvatar3d[]) => void): () => void
3914
+ ```
3915
+
3916
+ Call `listener` whenever the avatars the player wears change — they wore
3917
+ another one in the Arcade's picker, bought one, or their avatars arrived
3918
+ for the first time — with every worn avatar, one per family. Not called
3919
+ for the avatars already worn when you subscribe: read
3920
+ [Avatars3dApi.worn](#avatars3dapi-worn) for those.
3921
+
3922
+ | Parameter | Type |
3923
+ | --- | --- |
3924
+ | `listener` | `(worn: readonly WornAvatar3d[]) => void` |
3925
+
3926
+ **Returns.** a function that stops the calls. Calling it twice is harmless.
3927
+
3672
3928
  <a id="gemapierror"></a>
3673
3929
 
3674
3930
  ## GemApiError
@@ -3832,8 +4088,10 @@ the callback instead.
3832
4088
 
3833
4089
  - `ready` — a usable token is held.
3834
4090
  - `pending` — no token yet, or one is being fetched.
3835
- - `degraded` — the last attempt failed but another may succeed. Keep playing;
3836
- a retry is worthwhile.
4091
+ - `degraded` — the last attempt failed but another may succeed, and the SDK
4092
+ makes it on its own: once any wait the platform named has passed, or
4093
+ otherwise at growing intervals of up to a minute. Keep playing. `ready`
4094
+ follows when an attempt succeeds, with no call of yours needed to cause it.
3837
4095
  - `lost` — terminal for this session. Signing back in is the only way
3838
4096
  forward, and retrying will not produce one.
3839
4097
 
@@ -4849,50 +5107,11 @@ a key you leave out is unchanged.
4849
5107
  interface ControlsApi
4850
5108
  ```
4851
5109
 
4852
- `gem.controls` — the player's camera and controls settings: tell the Arcade
4853
- which ones your game leaves to the player, and turn look input into a camera
4854
- turn at the player's sensitivity.
5110
+ `gem.controls` — the player's camera and controls settings: turn look input
5111
+ into a camera turn at the player's sensitivity.
4855
5112
 
4856
5113
  **Guides.** [what-a-game-must-do](../what-a-game-must-do.md)
4857
5114
 
4858
- <a id="controlsapi-declare"></a>
4859
-
4860
- ## ControlsApi.declare
4861
-
4862
- ```ts
4863
- declare(uses: GemControlsDeclaration): boolean
4864
- ```
4865
-
4866
- Tell the Arcade which of the player's camera and controls settings your
4867
- game uses, and for each one who decides it: `'player'` — you apply the
4868
- Arcade's value from `gem.prefs.recommended` (or the player's own choice
4869
- in your settings, which wins) — or `'game'` — you decide it yourself, and
4870
- the Arcade's Settings shows that row as "Set by this game". Leave out a
4871
- setting your game does not use.
4872
-
4873
- ```js
4874
- gem.controls.declare({
4875
- lookSensitivity: 'player',
4876
- gamepadLookSensitivity: 'player',
4877
- invertLookY: 'player',
4878
- touchMovement: 'game', // this game always uses a fixed stick
4879
- });
4880
- ```
4881
-
4882
- **It is screen state, not a toggle.** Each call is the whole answer and
4883
- replaces the last. The SDK holds it and sends it again after every
4884
- reconnect, so call it once at boot, and again only when the answer
4885
- changes. Any other key or value is dropped.
4886
-
4887
- | Parameter | Type |
4888
- | --- | --- |
4889
- | `uses` | `GemControlsDeclaration` |
4890
-
4891
- **Returns.** `true` when the Arcade was told now; `false` before the bridge is
4892
- up (the declaration is held and sent when it connects), outside the
4893
- Arcade, and under an embedder that does not show these settings. Never
4894
- throws.
4895
-
4896
5115
  <a id="controlsapi-look"></a>
4897
5116
 
4898
5117
  ## ControlsApi.look
@@ -4935,72 +5154,6 @@ readonly LOOK_BASE: GemLookBase
4935
5154
 
4936
5155
  The base speeds `look` applies at 1×, in radians. Frozen.
4937
5156
 
4938
- <a id="gemcontrolchoice"></a>
4939
-
4940
- ## GemControlChoice
4941
-
4942
- ```ts
4943
- type GemControlChoice = "player" | "game"
4944
- ```
4945
-
4946
- Who decides one camera or controls setting: `'player'` — your game applies
4947
- the Arcade's value, or the player's own in-game choice, which wins — or
4948
- `'game'` — your game decides it itself, and the Arcade's Settings shows the
4949
- row as "Set by this game".
4950
-
4951
- <a id="gemcontrolsdeclaration"></a>
4952
-
4953
- ## GemControlsDeclaration
4954
-
4955
- ```ts
4956
- interface GemControlsDeclaration
4957
- ```
4958
-
4959
- What `gem.controls.declare` takes: for each of the player's camera and
4960
- controls settings your game uses, who decides it. Leave a setting out when
4961
- your game does not use it at all — the Arcade then says "This game doesn't
4962
- use this" beside that row.
4963
-
4964
- <a id="gemcontrolsdeclaration-looksensitivity"></a>
4965
-
4966
- ## GemControlsDeclaration.lookSensitivity
4967
-
4968
- ```ts
4969
- readonly lookSensitivity?: GemControlChoice
4970
- ```
4971
-
4972
- Look speed for touch, mouse and pen — `gem.prefs.recommended.lookSensitivity`.
4973
-
4974
- <a id="gemcontrolsdeclaration-gamepadlooksensitivity"></a>
4975
-
4976
- ## GemControlsDeclaration.gamepadLookSensitivity
4977
-
4978
- ```ts
4979
- readonly gamepadLookSensitivity?: GemControlChoice
4980
- ```
4981
-
4982
- Look speed for a gamepad stick — `gem.prefs.recommended.gamepadLookSensitivity`.
4983
-
4984
- <a id="gemcontrolsdeclaration-invertlooky"></a>
4985
-
4986
- ## GemControlsDeclaration.invertLookY
4987
-
4988
- ```ts
4989
- readonly invertLookY?: GemControlChoice
4990
- ```
4991
-
4992
- Inverted up/down look — `gem.prefs.recommended.invertLookY`.
4993
-
4994
- <a id="gemcontrolsdeclaration-touchmovement"></a>
4995
-
4996
- ## GemControlsDeclaration.touchMovement
4997
-
4998
- ```ts
4999
- readonly touchMovement?: GemControlChoice
5000
- ```
5001
-
5002
- The touch movement thumbstick, floating or fixed — `gem.prefs.recommended.touchMovement`.
5003
-
5004
5157
  <a id="device_settings_max_bytes"></a>
5005
5158
 
5006
5159
  ## DEVICE_SETTINGS_MAX_BYTES
@@ -5561,41 +5714,17 @@ wallet.
5561
5714
  **Returns.** whether the message left the frame — `false` before the bridge is
5562
5715
  up, for an arcade with no wallet, and past this frame's own limit.
5563
5716
 
5564
- <a id="gemframe-declarecontrols"></a>
5565
-
5566
- ## GemFrame.declareControls
5567
-
5568
- ```ts
5569
- declareControls(uses: GemControlsDeclaration): boolean
5570
- ```
5571
-
5572
- Tell the arcade which of the player's camera and controls settings your
5573
- game leaves to the player and which it decides itself — the whole answer
5574
- each time, the latest one winning.
5575
-
5576
- Prefer `gem.controls.declare`, which holds the declaration across a
5577
- reconnect. This one sends only while connected, and only to an arcade
5578
- that shows these settings.
5579
-
5580
- | Parameter | Type |
5581
- | --- | --- |
5582
- | `uses` | `GemControlsDeclaration` |
5583
-
5584
- **Returns.** whether the message left the frame — `false` before the bridge is
5585
- up, for an arcade that does not show these settings, and past this
5586
- frame's own limit.
5587
-
5588
5717
  <a id="gemframe-setmenu"></a>
5589
5718
 
5590
5719
  ## GemFrame.setMenu
5591
5720
 
5592
5721
  ```ts
5593
- setMenu(settings: boolean, items?: readonly { id: string; label: string }[]): boolean
5722
+ setMenu(items?: readonly { id: string; label: string }[]): boolean
5594
5723
  ```
5595
5724
 
5596
- Tell the arcade what your game adds to its menu: whether it has a settings
5597
- screen, and up to four entries of its own — the whole answer each time, the
5598
- latest one winning.
5725
+ Tell the arcade what your game adds to its menu: up to four entries of its
5726
+ own — the whole answer each time, the latest one winning. Your settings
5727
+ are not among them: a game opens its settings from its own button.
5599
5728
 
5600
5729
  Prefer `gem.setMenu`, which holds the state across a reconnect and calls
5601
5730
  your handlers when the player chooses an entry. This one sends only while
@@ -5603,7 +5732,6 @@ connected, and only to an arcade that draws a menu.
5603
5732
 
5604
5733
  | Parameter | Type |
5605
5734
  | --- | --- |
5606
- | `settings` | `boolean` |
5607
5735
  | `items` | `readonly { id: string; label: string }[]` |
5608
5736
 
5609
5737
  **Returns.** whether the message left the frame — `false` before the bridge is
@@ -5858,6 +5986,24 @@ loading the model is too much. Not the 2D portrait
5858
5986
  ([PlayerIdentity.avatarIconUrl](#playeridentity-avatariconurl)), which is a separate choice. Pass it
5859
5987
  through [platformIconUrl](#platformiconurl) before putting it in an `<img src>`.
5860
5988
 
5989
+ <a id="friend-avatars3d"></a>
5990
+
5991
+ ## Friend.avatars3d
5992
+
5993
+ ```ts
5994
+ readonly avatars3d?: readonly WornAvatar3d[]
5995
+ ```
5996
+
5997
+ Every 3D avatar the player wears, one per rig family, in the order the game
5998
+ declared its families. Absent or empty means none is worn — draw your
5999
+ default look.
6000
+
6001
+ [PlayerIdentity.avatar3dId](#playeridentity-avatar3did) and its two URLs are one of these: the
6002
+ avatar of the game's first declared family that has one. A game that
6003
+ accepts a single family can read either; one that accepts several picks by
6004
+ `family` here. Absent too where the platform does not send it yet — read
6005
+ the flat fields then.
6006
+
5861
6007
  <a id="friend-badges"></a>
5862
6008
 
5863
6009
  ## Friend.badges
@@ -7719,6 +7865,24 @@ loading the model is too much. Not the 2D portrait
7719
7865
  ([PlayerIdentity.avatarIconUrl](#playeridentity-avatariconurl)), which is a separate choice. Pass it
7720
7866
  through [platformIconUrl](#platformiconurl) before putting it in an `<img src>`.
7721
7867
 
7868
+ <a id="gemplayer-avatars3d"></a>
7869
+
7870
+ ## GemPlayer.avatars3d
7871
+
7872
+ ```ts
7873
+ readonly avatars3d?: readonly WornAvatar3d[]
7874
+ ```
7875
+
7876
+ Every 3D avatar the player wears, one per rig family, in the order the game
7877
+ declared its families. Absent or empty means none is worn — draw your
7878
+ default look.
7879
+
7880
+ [PlayerIdentity.avatar3dId](#playeridentity-avatar3did) and its two URLs are one of these: the
7881
+ avatar of the game's first declared family that has one. A game that
7882
+ accepts a single family can read either; one that accepts several picks by
7883
+ `family` here. Absent too where the platform does not send it yet — read
7884
+ the flat fields then.
7885
+
7722
7886
  <a id="gemplayer-badges"></a>
7723
7887
 
7724
7888
  ## GemPlayer.badges
@@ -8277,20 +8441,8 @@ and your game is running again by the time this fires.
8277
8441
  interface GameMenu
8278
8442
  ```
8279
8443
 
8280
- What your game adds to the arcade's menu — see `gem.setMenu`.
8281
-
8282
- <a id="gamemenu-onsettings"></a>
8283
-
8284
- ## GameMenu.onSettings
8285
-
8286
- ```ts
8287
- readonly onSettings?: () => void
8288
- ```
8289
-
8290
- Open your settings screen. Another embedder may show a Settings entry that
8291
- calls it, but **the Gem Arcade no longer does**: draw a settings button of
8292
- your own, always (top-right is the recommendation). Passing this is still
8293
- accepted and harmless.
8444
+ What your game adds to the arcade's menu — see `gem.setMenu`. Your settings
8445
+ are not part of it: they open from your own settings button.
8294
8446
 
8295
8447
  <a id="gamemenu-items"></a>
8296
8448
 
@@ -8828,6 +8980,32 @@ client sends.
8828
8980
  up, under an embedder with no avatar picker, or past six asks a minute.
8829
8981
  It never throws and never waits.
8830
8982
 
8983
+ <a id="gem-avatars3d"></a>
8984
+
8985
+ ## Gem.avatars3d
8986
+
8987
+ ```ts
8988
+ readonly avatars3d: Avatars3dApi
8989
+ ```
8990
+
8991
+ Arcade 3D avatars: what the player wears, the rig families your game
8992
+ accepts, the avatars your game sells, and buying one — which wears it.
8993
+ 2D portraits are a separate system and are not here.
8994
+
8995
+ ```js
8996
+ const draw = () => {
8997
+ const worn = gem.avatars3d.worn();
8998
+ const url = worn && platformIconUrl(worn.modelUrl);
8999
+ if (url) loadCharacter(url);
9000
+ else useDefaultCharacter();
9001
+ };
9002
+ draw();
9003
+ gem.avatars3d.onChange(draw);
9004
+ ```
9005
+
9006
+ See [Arcade avatars](../arcade-avatars.md) for the rig, the config, and
9007
+ uploading and selling avatars with gemctl.
9008
+
8831
9009
  <a id="gem-vibration"></a>
8832
9010
 
8833
9011
  ## Gem.vibration
@@ -8872,13 +9050,10 @@ readonly controls: ControlsApi
8872
9050
 
8873
9051
  The player's camera and controls settings — look sensitivity, gamepad look
8874
9052
  sensitivity, invert look and touch movement, which arrive in
8875
- `gem.prefs.recommended`. Two things to do with them:
9053
+ `gem.prefs.recommended` as the player's Arcade-wide defaults. Turn look
9054
+ input into a camera turn at the player's sensitivity:
8876
9055
 
8877
9056
  ```js
8878
- // 1. Tell the Arcade which ones your game leaves to the player.
8879
- gem.controls.declare({ lookSensitivity: 'player', invertLookY: 'player' });
8880
-
8881
- // 2. Turn look input into a camera turn at the player's sensitivity.
8882
9057
  const look = gem.controls.look();
8883
9058
  // dx, dy: the pointer's move in CSS px since the last event.
8884
9059
  const t = look.fromPointer(dx, dy, e.pointerType, camera.pitch);
@@ -9357,10 +9532,8 @@ gem.setMenu({
9357
9532
  ```
9358
9533
 
9359
9534
  **Your settings are not in it: draw your own settings button, always** —
9360
- top-right is the recommendation. `onSettings` is still accepted, and
9361
- another embedder may show an entry for it, but the Gem Arcade no longer
9362
- shows a Settings entry, so a game whose settings open only from there has
9363
- none a player can reach.
9535
+ top-right, clear of `gem.viewport.reserved`. The Arcade's menu never opens
9536
+ your settings screen.
9364
9537
 
9365
9538
  **It is screen state, not a toggle.** Each call is the whole answer and
9366
9539
  replaces the last, so call it again when the entries change — a new
@@ -9374,7 +9547,7 @@ should not assume the game was paused by you.
9374
9547
 
9375
9548
  | Parameter | Type | Description |
9376
9549
  | --- | --- | --- |
9377
- | `menu` | `GameMenu` | Your settings handler and your own entries. |
9550
+ | `menu` | `GameMenu` | Your own entries. |
9378
9551
 
9379
9552
  **Returns.** `true` when the arcade was told now; `false` before the bridge is
9380
9553
  up (the menu is held and sent when it connects) and under an embedder
@@ -10366,6 +10539,76 @@ readonly iconUrl: string
10366
10539
  The badge's thumbnail. Pass it through [platformIconUrl](#platformiconurl) before
10367
10540
  putting it in an `<img src>`.
10368
10541
 
10542
+ <a id="avatar3dfamily"></a>
10543
+
10544
+ ## Avatar3dFamily
10545
+
10546
+ ```ts
10547
+ type Avatar3dFamily = "small_human" | string & object
10548
+ ```
10549
+
10550
+ A rig family — the skeleton contract a 3D avatar is built on, and what a
10551
+ game declares it can animate. `small_human` is the first: a `root` node
10552
+ with a `body` child, which holds `head`, `arm_l`, `arm_r`, `leg_l` and
10553
+ `leg_r`, and a `hand_socket` under `arm_r`.
10554
+
10555
+ Open-ended (`| (string & {})`) so a family the platform adds later does not
10556
+ break a `switch` you compiled against this version: keep a `default` arm and
10557
+ skip a family you do not draw.
10558
+
10559
+ <a id="wornavatar3d"></a>
10560
+
10561
+ ## WornAvatar3d
10562
+
10563
+ ```ts
10564
+ interface WornAvatar3d
10565
+ ```
10566
+
10567
+ One 3D avatar a player wears — at most one per [Avatar3dFamily](#avatar3dfamily).
10568
+
10569
+ <a id="wornavatar3d-family"></a>
10570
+
10571
+ ## WornAvatar3d.family
10572
+
10573
+ ```ts
10574
+ readonly family: Avatar3dFamily
10575
+ ```
10576
+
10577
+ The rig family this avatar is built on.
10578
+
10579
+ <a id="wornavatar3d-id"></a>
10580
+
10581
+ ## WornAvatar3d.id
10582
+
10583
+ ```ts
10584
+ readonly id: string
10585
+ ```
10586
+
10587
+ The avatar's item id — a stable key for the model, never something to
10588
+ show as text.
10589
+
10590
+ <a id="wornavatar3d-modelurl"></a>
10591
+
10592
+ ## WornAvatar3d.modelUrl
10593
+
10594
+ ```ts
10595
+ readonly modelUrl: string
10596
+ ```
10597
+
10598
+ The avatar's model, a binary glTF (`.glb`) on the family's rig. Pass it
10599
+ through [platformIconUrl](#platformiconurl) before loading it.
10600
+
10601
+ <a id="wornavatar3d-iconurl"></a>
10602
+
10603
+ ## WornAvatar3d.iconUrl
10604
+
10605
+ ```ts
10606
+ readonly iconUrl: string
10607
+ ```
10608
+
10609
+ A rendered portrait of the avatar, for a roster or a menu. Pass it through
10610
+ [platformIconUrl](#platformiconurl) before putting it in an `<img src>`.
10611
+
10369
10612
  <a id="playeridentity"></a>
10370
10613
 
10371
10614
  ## PlayerIdentity
@@ -10509,6 +10752,24 @@ loading the model is too much. Not the 2D portrait
10509
10752
  ([PlayerIdentity.avatarIconUrl](#playeridentity-avatariconurl)), which is a separate choice. Pass it
10510
10753
  through [platformIconUrl](#platformiconurl) before putting it in an `<img src>`.
10511
10754
 
10755
+ <a id="playeridentity-avatars3d"></a>
10756
+
10757
+ ## PlayerIdentity.avatars3d
10758
+
10759
+ ```ts
10760
+ readonly avatars3d?: readonly WornAvatar3d[]
10761
+ ```
10762
+
10763
+ Every 3D avatar the player wears, one per rig family, in the order the game
10764
+ declared its families. Absent or empty means none is worn — draw your
10765
+ default look.
10766
+
10767
+ [PlayerIdentity.avatar3dId](#playeridentity-avatar3did) and its two URLs are one of these: the
10768
+ avatar of the game's first declared family that has one. A game that
10769
+ accepts a single family can read either; one that accepts several picks by
10770
+ `family` here. Absent too where the platform does not send it yet — read
10771
+ the flat fields then.
10772
+
10512
10773
  <a id="playeridentity-badges"></a>
10513
10774
 
10514
10775
  ## PlayerIdentity.badges
@@ -10548,7 +10809,7 @@ for (const row of opponents) {
10548
10809
 
10549
10810
  Reads `player_id`, `display_name`, `discriminator`, `display_tag`,
10550
10811
  `avatar_icon_id`, `avatar_icon_url`, `avatar_3d_id`, `avatar_3d_model_url`,
10551
- `avatar_3d_icon_url` and `badges`, and nothing else from the row. Anything missing, empty or of the wrong type is absent in the result.
10812
+ `avatar_3d_icon_url`, `avatars_3d` and `badges`, and nothing else from the row. Anything missing, empty or of the wrong type is absent in the result.
10552
10813
 
10553
10814
  | Parameter | Type |
10554
10815
  | --- | --- |
@@ -11808,12 +12069,44 @@ a Buy button mostly does not write one.
11808
12069
  | --- | --- |
11809
12070
  | `req` | `PurchaseRequest` |
11810
12071
 
12072
+ <a id="avatar3dpurchaserequest"></a>
12073
+
12074
+ ## Avatar3dPurchaseRequest
12075
+
12076
+ ```ts
12077
+ interface Avatar3dPurchaseRequest
12078
+ ```
12079
+
12080
+ What a game asks for when it buys one of its own 3D avatars for the player
12081
+ with `gem.avatars3d.purchase`.
12082
+
12083
+ <a id="avatar3dpurchaserequest-itemid"></a>
12084
+
12085
+ ## Avatar3dPurchaseRequest.itemId
12086
+
12087
+ ```ts
12088
+ itemId: string
12089
+ ```
12090
+
12091
+ The avatar's item id — `itemId` on an `Avatar3dOffer`.
12092
+
12093
+ <a id="avatar3dpurchaserequest-currencyslug"></a>
12094
+
12095
+ ## Avatar3dPurchaseRequest.currencySlug
12096
+
12097
+ ```ts
12098
+ currencySlug: string
12099
+ ```
12100
+
12101
+ The platform currency to pay in, `GEMS` or `ARCADE_TICKETS`. The offer
12102
+ must carry a price in it.
12103
+
11811
12104
  <a id="purchaserefusalcode"></a>
11812
12105
 
11813
12106
  ## PurchaseRefusalCode
11814
12107
 
11815
12108
  ```ts
11816
- type PurchaseRefusalCode = "unknown_currency" | "currency_not_in_game_payable" | "not_purchasable" | "no_price_in_currency" | "busy" | "cancelled" | "insufficient_funds" | "price_mismatch" | "config_version_stale" | "platform_currency_unavailable" | "not_available" | "recipe_locked" | "recipe_on_cooldown" | "recipe_max_uses_exceeded" | "item_max_stack_exceeded" | "recipe_costs_unmet" | "quantity_invalid" | "error"
12109
+ type PurchaseRefusalCode = "unknown_currency" | "currency_not_in_game_payable" | "not_purchasable" | "no_price_in_currency" | "busy" | "cancelled" | "insufficient_funds" | "price_mismatch" | "config_version_stale" | "platform_currency_unavailable" | "not_available" | "recipe_locked" | "already_owned" | "recipe_on_cooldown" | "recipe_max_uses_exceeded" | "item_max_stack_exceeded" | "recipe_costs_unmet" | "quantity_invalid" | "error"
11817
12110
  ```
11818
12111
 
11819
12112
  Every reason a purchase can be refused, on either purchase path.
@@ -11839,6 +12132,9 @@ Every reason a purchase can be refused, on either purchase path.
11839
12132
  - `not_available` — the recipe is outside its availability window, or
11840
12133
  inactive; nothing was charged. Hide or disable it until its window opens.
11841
12134
  - `recipe_locked` — the recipe's prerequisites are not met.
12135
+ - `already_owned` — the player already owns the 3D avatar
12136
+ `gem.avatars3d.purchase` asked for; nothing was charged. Only that call
12137
+ answers it.
11842
12138
  - `recipe_on_cooldown`, `recipe_max_uses_exceeded`, `item_max_stack_exceeded`,
11843
12139
  `recipe_costs_unmet`, `quantity_invalid` — the recipe or item refused this
11844
12140
  purchase, before any charge.
@@ -13125,6 +13421,24 @@ loading the model is too much. Not the 2D portrait
13125
13421
  ([PlayerIdentity.avatarIconUrl](#playeridentity-avatariconurl)), which is a separate choice. Pass it
13126
13422
  through [platformIconUrl](#platformiconurl) before putting it in an `<img src>`.
13127
13423
 
13424
+ <a id="leaderboardentry-avatars3d"></a>
13425
+
13426
+ ## LeaderboardEntry.avatars3d
13427
+
13428
+ ```ts
13429
+ readonly avatars3d?: readonly WornAvatar3d[]
13430
+ ```
13431
+
13432
+ Every 3D avatar the player wears, one per rig family, in the order the game
13433
+ declared its families. Absent or empty means none is worn — draw your
13434
+ default look.
13435
+
13436
+ [PlayerIdentity.avatar3dId](#playeridentity-avatar3did) and its two URLs are one of these: the
13437
+ avatar of the game's first declared family that has one. A game that
13438
+ accepts a single family can read either; one that accepts several picks by
13439
+ `family` here. Absent too where the platform does not send it yet — read
13440
+ the flat fields then.
13441
+
13128
13442
  <a id="leaderboardentry-badges"></a>
13129
13443
 
13130
13444
  ## LeaderboardEntry.badges
@@ -14618,6 +14932,24 @@ loading the model is too much. Not the 2D portrait
14618
14932
  ([PlayerIdentity.avatarIconUrl](#playeridentity-avatariconurl)), which is a separate choice. Pass it
14619
14933
  through [platformIconUrl](#platformiconurl) before putting it in an `<img src>`.
14620
14934
 
14935
+ <a id="playerprofile-avatars3d"></a>
14936
+
14937
+ ## PlayerProfile.avatars3d
14938
+
14939
+ ```ts
14940
+ readonly avatars3d?: readonly WornAvatar3d[]
14941
+ ```
14942
+
14943
+ Every 3D avatar the player wears, one per rig family, in the order the game
14944
+ declared its families. Absent or empty means none is worn — draw your
14945
+ default look.
14946
+
14947
+ [PlayerIdentity.avatar3dId](#playeridentity-avatar3did) and its two URLs are one of these: the
14948
+ avatar of the game's first declared family that has one. A game that
14949
+ accepts a single family can read either; one that accepts several picks by
14950
+ `family` here. Absent too where the platform does not send it yet — read
14951
+ the flat fields then.
14952
+
14621
14953
  <a id="playerprofile-badges"></a>
14622
14954
 
14623
14955
  ## PlayerProfile.badges
@@ -15307,6 +15639,36 @@ room. A double fire — the push and the reconcile landing together, or the
15307
15639
  event arriving just as the player presses your own button — costs one
15308
15640
  redundant request and never a second seat.
15309
15641
 
15642
+ <a id="roomeventmap-partyqueued"></a>
15643
+
15644
+ ## RoomEventMap.partyQueued
15645
+
15646
+ ```ts
15647
+ partyQueued: [PartyTicket]
15648
+ ```
15649
+
15650
+ This player's party is queued for a match: its leader called
15651
+ `queueForMatch`, and this player is on that ticket.
15652
+
15653
+ The ticket is a view — show a searching screen from it, and await its
15654
+ `outcome` to take the screen down. It has no `cancel()`: the ticket is
15655
+ the leader's.
15656
+
15657
+ ```js
15658
+ gem.rooms.on('partyQueued', async (ticket) => {
15659
+ showSearching({ canCancel: false });
15660
+ const outcome = await ticket.outcome;
15661
+ if (outcome.status !== 'matched') backToMenu(outcome.status);
15662
+ });
15663
+ ```
15664
+
15665
+ When the outcome is `matched` the room arrives as `seated`, as it does
15666
+ for any seat this player's party was given.
15667
+
15668
+ NOT RAISED FOR THE PLAYER WHO QUEUED — they hold the ticket itself — and
15669
+ not raised at all under an arcade page that does not relay ticket
15670
+ events. See [PartyTicket](#partyticket).
15671
+
15310
15672
  <a id="roomeventmap-listed"></a>
15311
15673
 
15312
15674
  ## RoomEventMap.listed
@@ -16125,6 +16487,25 @@ Set from the moment [RoomManager.queueForMatch](#roommanager-queueformatch) answ
16125
16487
  ticket ends — so it is `null` again by the time a matched ticket's room is
16126
16488
  [RoomManager.currentRoom](#roommanager-currentroom).
16127
16489
 
16490
+ <a id="roommanager-partyticket"></a>
16491
+
16492
+ ## RoomManager.partyTicket
16493
+
16494
+ ```ts
16495
+ readonly partyTicket: PartyTicket | null
16496
+ ```
16497
+
16498
+ The matchmaking ticket this player's PARTY is waiting on, or `null` — a
16499
+ ticket this game did not create, which it can show and cannot cancel.
16500
+
16501
+ Set when the arcade page reports that the party's leader queued, and
16502
+ `null` again once that ticket ends. Never set beside
16503
+ [RoomManager.currentTicket](#roommanager-currentticket): the player who queued holds that one.
16504
+ The `partyQueued` event is raised when this is set.
16505
+
16506
+ Always `null` under a page that does not relay ticket events. See
16507
+ [PartyTicket](#partyticket).
16508
+
16128
16509
  <a id="roommanager-currentmatch"></a>
16129
16510
 
16130
16511
  ## RoomManager.currentMatch
@@ -16549,6 +16930,143 @@ Safe to call more than once, and after the ticket has ended.
16549
16930
 
16550
16931
  **Returns.** The same promise as [MatchTicket.outcome](#matchticket-outcome).
16551
16932
 
16933
+ <a id="partyticketstatus"></a>
16934
+
16935
+ ## PartyTicketStatus
16936
+
16937
+ ```ts
16938
+ type PartyTicketStatus = "queued" | "matched" | "expired" | "cancelled" | "failed" | "lost"
16939
+ ```
16940
+
16941
+ Where a party's ticket is, as far as this player's game has been told:
16942
+ `queued` while it waits, and anything else for good.
16943
+
16944
+ - `matched` — the party is seated and the match is starting. The room arrives
16945
+ as a `seated` event.
16946
+ - `expired` — it waited too long, or its owner's game stopped keeping it alive.
16947
+ - `cancelled` — the leader backed out, or the party changed.
16948
+ - `failed` — the platform formed a match and could not start it, repeatedly.
16949
+ - `lost` — nothing more was heard about it by the time it must have ended.
16950
+
16951
+ <a id="partyticketoutcome"></a>
16952
+
16953
+ ## PartyTicketOutcome
16954
+
16955
+ ```ts
16956
+ type PartyTicketOutcome = { status: "matched" } | { status: "expired"; reason: string } | { status: "cancelled"; reason: string } | { status: "failed"; reason: string } | { status: "lost" }
16957
+ ```
16958
+
16959
+ How a party's ticket ended. Branch on `status`.
16960
+
16961
+ `matched` CARRIES NO ROOM. The room is reported the way a party's seat always
16962
+ is — the `seated` event — and that is where a game joins it. The SDK looks
16963
+ for the seat the moment it hears `matched`.
16964
+
16965
+ `reason` says why, where the platform gave one: `timed_out` or
16966
+ `heartbeat_lapsed` for `expired`, `requested` or `party_changed` for
16967
+ `cancelled`, `handoff_failed` for `failed`. Treat a value you do not
16968
+ recognise as the status alone.
16969
+
16970
+ `lost` is the SDK's own ending, not the platform's: the ticket's lifetime
16971
+ ran out and no ending was heard. Whatever happened, the party is not
16972
+ waiting any more.
16973
+
16974
+ <a id="partyticket"></a>
16975
+
16976
+ ## PartyTicket
16977
+
16978
+ ```ts
16979
+ interface PartyTicket
16980
+ ```
16981
+
16982
+ The matchmaking ticket this player's party is waiting on — a ticket this
16983
+ game did not create. What `gem.rooms.partyTicket` holds, and what the
16984
+ `partyQueued` event hands you.
16985
+
16986
+ READ-ONLY, ON PURPOSE. There is no `cancel()`: a party is one ticket and it
16987
+ is its leader's to cancel. A player who wants out leaves the party, which
16988
+ ends the ticket for everyone.
16989
+
16990
+ ```js
16991
+ gem.rooms.on('partyQueued', async (ticket) => {
16992
+ showSearching({ canCancel: false });
16993
+ const outcome = await ticket.outcome;
16994
+ if (outcome.status !== 'matched') backToMenu(outcome.status);
16995
+ // `matched`: your `seated` handler has the room.
16996
+ });
16997
+ ```
16998
+
16999
+ ONLY WHERE THE ARCADE PAGE RELAYS IT. Under a page that does not, there is
17000
+ no party ticket and no `partyQueued`, and a member is carried into the match
17001
+ by `seated` with no lead-up. Build the searching screen as an extra, never
17002
+ as a step the match depends on.
17003
+
17004
+ **Guides.** [rooms-and-matches](../rooms-and-matches.md)
17005
+
17006
+ <a id="partyticket-id"></a>
17007
+
17008
+ ## PartyTicket.id
17009
+
17010
+ ```ts
17011
+ readonly id: string
17012
+ ```
17013
+
17014
+ The ticket's id. For logs and bug reports; nothing you call takes it.
17015
+
17016
+ <a id="partyticket-status"></a>
17017
+
17018
+ ## PartyTicket.status
17019
+
17020
+ ```ts
17021
+ readonly status: PartyTicketStatus
17022
+ ```
17023
+
17024
+ Where the ticket is, as far as this game has been told. Changes once, from
17025
+ `queued` to how it ended.
17026
+
17027
+ <a id="partyticket-partysize"></a>
17028
+
17029
+ ## PartyTicket.partySize
17030
+
17031
+ ```ts
17032
+ readonly partySize: number
17033
+ ```
17034
+
17035
+ How many players the ticket seats: the size of the party. 0 when the page
17036
+ did not say.
17037
+
17038
+ <a id="partyticket-queuedat"></a>
17039
+
17040
+ ## PartyTicket.queuedAt
17041
+
17042
+ ```ts
17043
+ readonly queuedAt: number
17044
+ ```
17045
+
17046
+ When the ticket joined the queue, in Unix seconds. 0 when the page did not
17047
+ say.
17048
+
17049
+ <a id="partyticket-expiresat"></a>
17050
+
17051
+ ## PartyTicket.expiresAt
17052
+
17053
+ ```ts
17054
+ readonly expiresAt: number
17055
+ ```
17056
+
17057
+ When the ticket gives up if it is still waiting, in Unix seconds. Good for
17058
+ a countdown. 0 when the page did not say.
17059
+
17060
+ <a id="partyticket-outcome"></a>
17061
+
17062
+ ## PartyTicket.outcome
17063
+
17064
+ ```ts
17065
+ readonly outcome: Promise<PartyTicketOutcome>
17066
+ ```
17067
+
17068
+ How the ticket ends. Settles exactly once and never rejects.
17069
+
16552
17070
  <a id="roomstatus"></a>
16553
17071
 
16554
17072
  ## RoomStatus
@@ -17018,6 +17536,24 @@ loading the model is too much. Not the 2D portrait
17018
17536
  ([PlayerIdentity.avatarIconUrl](#playeridentity-avatariconurl)), which is a separate choice. Pass it
17019
17537
  through [platformIconUrl](#platformiconurl) before putting it in an `<img src>`.
17020
17538
 
17539
+ <a id="roommember-avatars3d"></a>
17540
+
17541
+ ## RoomMember.avatars3d
17542
+
17543
+ ```ts
17544
+ readonly avatars3d?: readonly WornAvatar3d[]
17545
+ ```
17546
+
17547
+ Every 3D avatar the player wears, one per rig family, in the order the game
17548
+ declared its families. Absent or empty means none is worn — draw your
17549
+ default look.
17550
+
17551
+ [PlayerIdentity.avatar3dId](#playeridentity-avatar3did) and its two URLs are one of these: the
17552
+ avatar of the game's first declared family that has one. A game that
17553
+ accepts a single family can read either; one that accepts several picks by
17554
+ `family` here. Absent too where the platform does not send it yet — read
17555
+ the flat fields then.
17556
+
17021
17557
  <a id="roommember-badges"></a>
17022
17558
 
17023
17559
  ## RoomMember.badges
@@ -17848,6 +18384,24 @@ loading the model is too much. Not the 2D portrait
17848
18384
  ([PlayerIdentity.avatarIconUrl](#playeridentity-avatariconurl)), which is a separate choice. Pass it
17849
18385
  through [platformIconUrl](#platformiconurl) before putting it in an `<img src>`.
17850
18386
 
18387
+ <a id="showcasefriendentry-avatars3d"></a>
18388
+
18389
+ ## ShowcaseFriendEntry.avatars3d
18390
+
18391
+ ```ts
18392
+ readonly avatars3d?: readonly WornAvatar3d[]
18393
+ ```
18394
+
18395
+ Every 3D avatar the player wears, one per rig family, in the order the game
18396
+ declared its families. Absent or empty means none is worn — draw your
18397
+ default look.
18398
+
18399
+ [PlayerIdentity.avatar3dId](#playeridentity-avatar3did) and its two URLs are one of these: the
18400
+ avatar of the game's first declared family that has one. A game that
18401
+ accepts a single family can read either; one that accepts several picks by
18402
+ `family` here. Absent too where the platform does not send it yet — read
18403
+ the flat fields then.
18404
+
17851
18405
  <a id="showcasefriendentry-badges"></a>
17852
18406
 
17853
18407
  ## ShowcaseFriendEntry.badges