@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.
@@ -1,6 +1,6 @@
1
1
  # The browser surface — API reference
2
2
 
3
- Generated from `@indiegems/gem-web-sdk/sdk` v12.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.
3
+ Generated from `@indiegems/gem-web-sdk/sdk` v13.1.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,356 @@ 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
+
3928
+ <a id="avatarrignode"></a>
3929
+
3930
+ ## AvatarRigNode
3931
+
3932
+ ```ts
3933
+ interface AvatarRigNode
3934
+ ```
3935
+
3936
+ One node of a rig family's tree: its exact, case-sensitive name, and the
3937
+ name of the node it is a DIRECT child of. `parent` is `null` for the rig's
3938
+ root, which is a root node of the scene.
3939
+
3940
+ <a id="avatarrignode-node"></a>
3941
+
3942
+ ## AvatarRigNode.node
3943
+
3944
+ ```ts
3945
+ readonly node: string
3946
+ ```
3947
+
3948
+ The node's name, exact and case-sensitive.
3949
+
3950
+ <a id="avatarrignode-parent"></a>
3951
+
3952
+ ## AvatarRigNode.parent
3953
+
3954
+ ```ts
3955
+ readonly parent: string | null
3956
+ ```
3957
+
3958
+ The node it is a direct child of; `null` for the rig's root.
3959
+
3960
+ <a id="avatarrigs"></a>
3961
+
3962
+ ## AvatarRigs
3963
+
3964
+ ```ts
3965
+ interface AvatarRigs
3966
+ ```
3967
+
3968
+ The rig families this SDK knows, by name.
3969
+
3970
+ <a id="avatarrigs-small_human"></a>
3971
+
3972
+ ## AvatarRigs.small_human
3973
+
3974
+ ```ts
3975
+ readonly small_human: readonly AvatarRigNode[]
3976
+ ```
3977
+
3978
+ The toy rig: rigid limbs on a body, eight nodes.
3979
+
3980
+ <a id="avatarrigs-adult_human"></a>
3981
+
3982
+ ## AvatarRigs.adult_human
3983
+
3984
+ ```ts
3985
+ readonly adult_human: readonly AvatarRigNode[]
3986
+ ```
3987
+
3988
+ The adult humanoid with fingers: 56 nodes, world-aligned at rest.
3989
+
3990
+ <a id="avatar_rigs"></a>
3991
+
3992
+ ## AVATAR_RIGS
3993
+
3994
+ ```ts
3995
+ AVATAR_RIGS: AvatarRigs
3996
+ ```
3997
+
3998
+ Every node each rig family requires, with the node it is a direct child of,
3999
+ parents before children. Frozen: read it, never change it.
4000
+
4001
+ The platform refuses an upload whose model is missing one of these, has one
4002
+ under another parent, or has one twice (`rig_mismatch`). Extra nodes are
4003
+ allowed anywhere. `adult_human` also requires every one of its nodes to be
4004
+ world-aligned at rest; that rule is not in this list, see the guide.
4005
+
4006
+ ```js
4007
+ import { AVATAR_RIGS } from '@indiegems/gem-web-sdk/sdk';
4008
+
4009
+ // Every joint of the worn avatar, by name.
4010
+ const joints = new Map(
4011
+ AVATAR_RIGS.adult_human.map(({ node }) => [node, gltf.scene.getObjectByName(node)]),
4012
+ );
4013
+
4014
+ // A test that one of your own models is on the rig.
4015
+ for (const { node, parent } of AVATAR_RIGS.adult_human) {
4016
+ const found = gltf.scene.getObjectByName(node);
4017
+ if (!found) throw new Error(`missing ${node}`);
4018
+ if (parent !== null && found.parent?.name !== parent) throw new Error(`${node} is not under ${parent}`);
4019
+ }
4020
+ ```
4021
+
3672
4022
  <a id="gemapierror"></a>
3673
4023
 
3674
4024
  ## GemApiError
@@ -4851,50 +5201,11 @@ a key you leave out is unchanged.
4851
5201
  interface ControlsApi
4852
5202
  ```
4853
5203
 
4854
- `gem.controls` — the player's camera and controls settings: tell the Arcade
4855
- which ones your game leaves to the player, and turn look input into a camera
4856
- turn at the player's sensitivity.
5204
+ `gem.controls` — the player's camera and controls settings: turn look input
5205
+ into a camera turn at the player's sensitivity.
4857
5206
 
4858
5207
  **Guides.** [what-a-game-must-do](../what-a-game-must-do.md)
4859
5208
 
4860
- <a id="controlsapi-declare"></a>
4861
-
4862
- ## ControlsApi.declare
4863
-
4864
- ```ts
4865
- declare(uses: GemControlsDeclaration): boolean
4866
- ```
4867
-
4868
- Tell the Arcade which of the player's camera and controls settings your
4869
- game uses, and for each one who decides it: `'player'` — you apply the
4870
- Arcade's value from `gem.prefs.recommended` (or the player's own choice
4871
- in your settings, which wins) — or `'game'` — you decide it yourself, and
4872
- the Arcade's Settings shows that row as "Set by this game". Leave out a
4873
- setting your game does not use.
4874
-
4875
- ```js
4876
- gem.controls.declare({
4877
- lookSensitivity: 'player',
4878
- gamepadLookSensitivity: 'player',
4879
- invertLookY: 'player',
4880
- touchMovement: 'game', // this game always uses a fixed stick
4881
- });
4882
- ```
4883
-
4884
- **It is screen state, not a toggle.** Each call is the whole answer and
4885
- replaces the last. The SDK holds it and sends it again after every
4886
- reconnect, so call it once at boot, and again only when the answer
4887
- changes. Any other key or value is dropped.
4888
-
4889
- | Parameter | Type |
4890
- | --- | --- |
4891
- | `uses` | `GemControlsDeclaration` |
4892
-
4893
- **Returns.** `true` when the Arcade was told now; `false` before the bridge is
4894
- up (the declaration is held and sent when it connects), outside the
4895
- Arcade, and under an embedder that does not show these settings. Never
4896
- throws.
4897
-
4898
5209
  <a id="controlsapi-look"></a>
4899
5210
 
4900
5211
  ## ControlsApi.look
@@ -4937,72 +5248,6 @@ readonly LOOK_BASE: GemLookBase
4937
5248
 
4938
5249
  The base speeds `look` applies at 1×, in radians. Frozen.
4939
5250
 
4940
- <a id="gemcontrolchoice"></a>
4941
-
4942
- ## GemControlChoice
4943
-
4944
- ```ts
4945
- type GemControlChoice = "player" | "game"
4946
- ```
4947
-
4948
- Who decides one camera or controls setting: `'player'` — your game applies
4949
- the Arcade's value, or the player's own in-game choice, which wins — or
4950
- `'game'` — your game decides it itself, and the Arcade's Settings shows the
4951
- row as "Set by this game".
4952
-
4953
- <a id="gemcontrolsdeclaration"></a>
4954
-
4955
- ## GemControlsDeclaration
4956
-
4957
- ```ts
4958
- interface GemControlsDeclaration
4959
- ```
4960
-
4961
- What `gem.controls.declare` takes: for each of the player's camera and
4962
- controls settings your game uses, who decides it. Leave a setting out when
4963
- your game does not use it at all — the Arcade then says "This game doesn't
4964
- use this" beside that row.
4965
-
4966
- <a id="gemcontrolsdeclaration-looksensitivity"></a>
4967
-
4968
- ## GemControlsDeclaration.lookSensitivity
4969
-
4970
- ```ts
4971
- readonly lookSensitivity?: GemControlChoice
4972
- ```
4973
-
4974
- Look speed for touch, mouse and pen — `gem.prefs.recommended.lookSensitivity`.
4975
-
4976
- <a id="gemcontrolsdeclaration-gamepadlooksensitivity"></a>
4977
-
4978
- ## GemControlsDeclaration.gamepadLookSensitivity
4979
-
4980
- ```ts
4981
- readonly gamepadLookSensitivity?: GemControlChoice
4982
- ```
4983
-
4984
- Look speed for a gamepad stick — `gem.prefs.recommended.gamepadLookSensitivity`.
4985
-
4986
- <a id="gemcontrolsdeclaration-invertlooky"></a>
4987
-
4988
- ## GemControlsDeclaration.invertLookY
4989
-
4990
- ```ts
4991
- readonly invertLookY?: GemControlChoice
4992
- ```
4993
-
4994
- Inverted up/down look — `gem.prefs.recommended.invertLookY`.
4995
-
4996
- <a id="gemcontrolsdeclaration-touchmovement"></a>
4997
-
4998
- ## GemControlsDeclaration.touchMovement
4999
-
5000
- ```ts
5001
- readonly touchMovement?: GemControlChoice
5002
- ```
5003
-
5004
- The touch movement thumbstick, floating or fixed — `gem.prefs.recommended.touchMovement`.
5005
-
5006
5251
  <a id="device_settings_max_bytes"></a>
5007
5252
 
5008
5253
  ## DEVICE_SETTINGS_MAX_BYTES
@@ -5196,7 +5441,7 @@ interface RewardCue
5196
5441
  ```
5197
5442
 
5198
5443
  A platform-currency reward on its way to the arcade — in the Gem Arcade, a
5199
- flight into the Gem button at the top-left of your frame — what
5444
+ flight into the Gem button, in whichever corner of your frame it sits — what
5200
5445
  `gem.cueReward` takes.
5201
5446
 
5202
5447
  <a id="rewardcue-currencyslug"></a>
@@ -5257,6 +5502,18 @@ y: number
5257
5502
 
5258
5503
  From the top edge of your viewport.
5259
5504
 
5505
+ <a id="gemcorner"></a>
5506
+
5507
+ ## GemCorner
5508
+
5509
+ ```ts
5510
+ type GemCorner = "top-left" | "top-right" | "bottom-left" | "bottom-right"
5511
+ ```
5512
+
5513
+ A corner of your frame, for `gem.setGemCorner`: where you would like the
5514
+ arcade's Gem button to sit. `'top-left'` is where it sits unless you ask.
5515
+ Physical corners: `'top-left'` is the top-left in every language.
5516
+
5260
5517
  <a id="framestate"></a>
5261
5518
 
5262
5519
  ## FrameState
@@ -5563,53 +5820,52 @@ wallet.
5563
5820
  **Returns.** whether the message left the frame — `false` before the bridge is
5564
5821
  up, for an arcade with no wallet, and past this frame's own limit.
5565
5822
 
5566
- <a id="gemframe-declarecontrols"></a>
5823
+ <a id="gemframe-setmenu"></a>
5567
5824
 
5568
- ## GemFrame.declareControls
5825
+ ## GemFrame.setMenu
5569
5826
 
5570
5827
  ```ts
5571
- declareControls(uses: GemControlsDeclaration): boolean
5828
+ setMenu(items?: readonly { id: string; label: string }[]): boolean
5572
5829
  ```
5573
5830
 
5574
- Tell the arcade which of the player's camera and controls settings your
5575
- game leaves to the player and which it decides itself — the whole answer
5576
- each time, the latest one winning.
5831
+ Tell the arcade what your game adds to its menu: up to four entries of its
5832
+ own — the whole answer each time, the latest one winning. Your settings
5833
+ are not among them: a game opens its settings from its own button.
5577
5834
 
5578
- Prefer `gem.controls.declare`, which holds the declaration across a
5579
- reconnect. This one sends only while connected, and only to an arcade
5580
- that shows these settings.
5835
+ Prefer `gem.setMenu`, which holds the state across a reconnect and calls
5836
+ your handlers when the player chooses an entry. This one sends only while
5837
+ connected, and only to an arcade that draws a menu.
5581
5838
 
5582
5839
  | Parameter | Type |
5583
5840
  | --- | --- |
5584
- | `uses` | `GemControlsDeclaration` |
5841
+ | `items` | `readonly { id: string; label: string }[]` |
5585
5842
 
5586
5843
  **Returns.** whether the message left the frame — `false` before the bridge is
5587
- up, for an arcade that does not show these settings, and past this
5588
- frame's own limit.
5844
+ up, for an arcade with no menu, and past this frame's own limit.
5589
5845
 
5590
- <a id="gemframe-setmenu"></a>
5846
+ <a id="gemframe-setgemcorner"></a>
5591
5847
 
5592
- ## GemFrame.setMenu
5848
+ ## GemFrame.setGemCorner
5593
5849
 
5594
5850
  ```ts
5595
- setMenu(settings: boolean, items?: readonly { id: string; label: string }[]): boolean
5851
+ setGemCorner(corner: GemCorner): boolean
5596
5852
  ```
5597
5853
 
5598
- Tell the arcade what your game adds to its menu: whether it has a settings
5599
- screen, and up to four entries of its own — the whole answer each time, the
5600
- latest one winning.
5854
+ Ask the arcade to put its own control over your game — the Gem button —
5855
+ in this corner of your frame: the whole answer each time, the latest one
5856
+ winning.
5601
5857
 
5602
- Prefer `gem.setMenu`, which holds the state across a reconnect and calls
5603
- your handlers when the player chooses an entry. This one sends only while
5604
- connected, and only to an arcade that draws a menu.
5858
+ Prefer `gem.setGemCorner`, which holds the corner across a reconnect.
5859
+ This one sends only while connected, and only to an arcade that can move
5860
+ its control.
5605
5861
 
5606
5862
  | Parameter | Type |
5607
5863
  | --- | --- |
5608
- | `settings` | `boolean` |
5609
- | `items` | `readonly { id: string; label: string }[]` |
5864
+ | `corner` | `GemCorner` |
5610
5865
 
5611
5866
  **Returns.** whether the message left the frame — `false` before the bridge is
5612
- up, for an arcade with no menu, and past this frame's own limit.
5867
+ up, for an arcade that cannot move its control, for a value that is not
5868
+ one of the four corners, and past this frame's own limit.
5613
5869
 
5614
5870
  <a id="gemframe-requestshare"></a>
5615
5871
 
@@ -5860,6 +6116,24 @@ loading the model is too much. Not the 2D portrait
5860
6116
  ([PlayerIdentity.avatarIconUrl](#playeridentity-avatariconurl)), which is a separate choice. Pass it
5861
6117
  through [platformIconUrl](#platformiconurl) before putting it in an `<img src>`.
5862
6118
 
6119
+ <a id="friend-avatars3d"></a>
6120
+
6121
+ ## Friend.avatars3d
6122
+
6123
+ ```ts
6124
+ readonly avatars3d?: readonly WornAvatar3d[]
6125
+ ```
6126
+
6127
+ Every 3D avatar the player wears, one per rig family, in the order the game
6128
+ declared its families. Absent or empty means none is worn — draw your
6129
+ default look.
6130
+
6131
+ [PlayerIdentity.avatar3dId](#playeridentity-avatar3did) and its two URLs are one of these: the
6132
+ avatar of the game's first declared family that has one. A game that
6133
+ accepts a single family can read either; one that accepts several picks by
6134
+ `family` here. Absent too where the platform does not send it yet — read
6135
+ the flat fields then.
6136
+
5863
6137
  <a id="friend-badges"></a>
5864
6138
 
5865
6139
  ## Friend.badges
@@ -7721,6 +7995,24 @@ loading the model is too much. Not the 2D portrait
7721
7995
  ([PlayerIdentity.avatarIconUrl](#playeridentity-avatariconurl)), which is a separate choice. Pass it
7722
7996
  through [platformIconUrl](#platformiconurl) before putting it in an `<img src>`.
7723
7997
 
7998
+ <a id="gemplayer-avatars3d"></a>
7999
+
8000
+ ## GemPlayer.avatars3d
8001
+
8002
+ ```ts
8003
+ readonly avatars3d?: readonly WornAvatar3d[]
8004
+ ```
8005
+
8006
+ Every 3D avatar the player wears, one per rig family, in the order the game
8007
+ declared its families. Absent or empty means none is worn — draw your
8008
+ default look.
8009
+
8010
+ [PlayerIdentity.avatar3dId](#playeridentity-avatar3did) and its two URLs are one of these: the
8011
+ avatar of the game's first declared family that has one. A game that
8012
+ accepts a single family can read either; one that accepts several picks by
8013
+ `family` here. Absent too where the platform does not send it yet — read
8014
+ the flat fields then.
8015
+
7724
8016
  <a id="gemplayer-badges"></a>
7725
8017
 
7726
8018
  ## GemPlayer.badges
@@ -8141,7 +8433,7 @@ readonly reserved: readonly GemReservedArea[]
8141
8433
  ```
8142
8434
 
8143
8435
  Rectangles of your frame the arcade's own controls cover — in the Gem
8144
- Arcade, its Gem button in the top-left corner, and the frame-rate readout
8436
+ Arcade, its Gem button in whichever corner it sits, and the frame-rate readout
8145
8437
  beside it while the player shows it; an embedder that draws wallets over
8146
8438
  your game adds theirs (see `setWalletVisible`). Keep essential UI and touch
8147
8439
  targets out of them. Empty when the arcade covers nothing.
@@ -8279,20 +8571,8 @@ and your game is running again by the time this fires.
8279
8571
  interface GameMenu
8280
8572
  ```
8281
8573
 
8282
- What your game adds to the arcade's menu — see `gem.setMenu`.
8283
-
8284
- <a id="gamemenu-onsettings"></a>
8285
-
8286
- ## GameMenu.onSettings
8287
-
8288
- ```ts
8289
- readonly onSettings?: () => void
8290
- ```
8291
-
8292
- Open your settings screen. Another embedder may show a Settings entry that
8293
- calls it, but **the Gem Arcade no longer does**: draw a settings button of
8294
- your own, always (top-right is the recommendation). Passing this is still
8295
- accepted and harmless.
8574
+ What your game adds to the arcade's menu — see `gem.setMenu`. Your settings
8575
+ are not part of it: they open from your own settings button.
8296
8576
 
8297
8577
  <a id="gamemenu-items"></a>
8298
8578
 
@@ -8830,6 +9110,32 @@ client sends.
8830
9110
  up, under an embedder with no avatar picker, or past six asks a minute.
8831
9111
  It never throws and never waits.
8832
9112
 
9113
+ <a id="gem-avatars3d"></a>
9114
+
9115
+ ## Gem.avatars3d
9116
+
9117
+ ```ts
9118
+ readonly avatars3d: Avatars3dApi
9119
+ ```
9120
+
9121
+ Arcade 3D avatars: what the player wears, the rig families your game
9122
+ accepts, the avatars your game sells, and buying one — which wears it.
9123
+ 2D portraits are a separate system and are not here.
9124
+
9125
+ ```js
9126
+ const draw = () => {
9127
+ const worn = gem.avatars3d.worn();
9128
+ const url = worn && platformIconUrl(worn.modelUrl);
9129
+ if (url) loadCharacter(url);
9130
+ else useDefaultCharacter();
9131
+ };
9132
+ draw();
9133
+ gem.avatars3d.onChange(draw);
9134
+ ```
9135
+
9136
+ See [Arcade avatars](../arcade-avatars.md) for the rig, the config, and
9137
+ uploading and selling avatars with gemctl.
9138
+
8833
9139
  <a id="gem-vibration"></a>
8834
9140
 
8835
9141
  ## Gem.vibration
@@ -8874,13 +9180,10 @@ readonly controls: ControlsApi
8874
9180
 
8875
9181
  The player's camera and controls settings — look sensitivity, gamepad look
8876
9182
  sensitivity, invert look and touch movement, which arrive in
8877
- `gem.prefs.recommended`. Two things to do with them:
9183
+ `gem.prefs.recommended` as the player's Arcade-wide defaults. Turn look
9184
+ input into a camera turn at the player's sensitivity:
8878
9185
 
8879
9186
  ```js
8880
- // 1. Tell the Arcade which ones your game leaves to the player.
8881
- gem.controls.declare({ lookSensitivity: 'player', invertLookY: 'player' });
8882
-
8883
- // 2. Turn look input into a camera turn at the player's sensitivity.
8884
9187
  const look = gem.controls.look();
8885
9188
  // dx, dy: the pointer's move in CSS px since the last event.
8886
9189
  const t = look.fromPointer(dx, dy, e.pointerType, camera.pitch);
@@ -9257,7 +9560,7 @@ cueReward(cue: RewardCue): boolean
9257
9560
 
9258
9561
  Tell the arcade a platform-currency reward is on its way, so it **shows it
9259
9562
  landing**: in the Gem Arcade the currency flies from `origin` into the Gem
9260
- button at the top-left of your frame, which pops as each mark lands, and
9563
+ button, wherever on your frame it sits, which pops as each mark lands, and
9261
9564
  the player's total moves once the platform confirms the grant. The Gem
9262
9565
  Arcade draws no wallet over your game: the player sees their Gems and
9263
9566
  Tickets in the Gem menu. A payout you do not cue still flies, from the
@@ -9359,10 +9662,9 @@ gem.setMenu({
9359
9662
  ```
9360
9663
 
9361
9664
  **Your settings are not in it: draw your own settings button, always** —
9362
- top-right is the recommendation. `onSettings` is still accepted, and
9363
- another embedder may show an entry for it, but the Gem Arcade no longer
9364
- shows a Settings entry, so a game whose settings open only from there has
9365
- none a player can reach.
9665
+ top-right, with your Back button just to its left, clear of
9666
+ `gem.viewport.reserved`. The Arcade's menu never opens your settings
9667
+ screen.
9366
9668
 
9367
9669
  **It is screen state, not a toggle.** Each call is the whole answer and
9368
9670
  replaces the last, so call it again when the entries change — a new
@@ -9376,12 +9678,52 @@ should not assume the game was paused by you.
9376
9678
 
9377
9679
  | Parameter | Type | Description |
9378
9680
  | --- | --- | --- |
9379
- | `menu` | `GameMenu` | Your settings handler and your own entries. |
9681
+ | `menu` | `GameMenu` | Your own entries. |
9380
9682
 
9381
9683
  **Returns.** `true` when the arcade was told now; `false` before the bridge is
9382
9684
  up (the menu is held and sent when it connects) and under an embedder
9383
9685
  that draws no menu.
9384
9686
 
9687
+ <a id="gem-setgemcorner"></a>
9688
+
9689
+ ## Gem.setGemCorner
9690
+
9691
+ ```ts
9692
+ setGemCorner(corner: GemCorner): boolean
9693
+ ```
9694
+
9695
+ Ask the arcade to put its Gem button — its one control over your game,
9696
+ top-left unless you ask — in another corner of your frame.
9697
+
9698
+ ```js
9699
+ // This game's own HUD needs the top-left, so the Gem goes top-right.
9700
+ gem.setGemCorner('top-right');
9701
+ ```
9702
+
9703
+ **Leave it top-left unless your own HUD needs that corner**, and then pick
9704
+ one corner for the whole game — at most one per orientation. A Gem that
9705
+ moves between screens is a way out the player has to look for. In
9706
+ landscape the bottom corners sit where a player's thumbs hold the sticks.
9707
+
9708
+ **Lay out around `gem.viewport.reserved`, never around the corner you
9709
+ asked for.** The arcade moves the Gem on its own terms: not while the
9710
+ player is using it or its menu is open, not more often than every few
9711
+ seconds, and never over a corner the player chose for themselves. The
9712
+ rectangles say where it actually is, and the `--gem-reserved-*` CSS
9713
+ properties follow them.
9714
+
9715
+ **It is screen state, not a toggle.** Each call is the whole answer and
9716
+ replaces the last. A value that is not one of the four corners is ignored,
9717
+ with a warning on the console, and changes nothing.
9718
+
9719
+ | Parameter | Type | Description |
9720
+ | --- | --- | --- |
9721
+ | `corner` | `GemCorner` | `'top-left'`, `'top-right'`, `'bottom-left'` or `'bottom-right'`. |
9722
+
9723
+ **Returns.** `true` when the arcade was told now; `false` before the bridge is
9724
+ up (the corner is held and sent when it connects), under an embedder that
9725
+ cannot move its control, and for a value that is not a corner.
9726
+
9385
9727
  <a id="gem-exit"></a>
9386
9728
 
9387
9729
  ## Gem.exit
@@ -10181,7 +10523,7 @@ The match this connection belongs to.
10181
10523
  ## MatchConnection.protocol
10182
10524
 
10183
10525
  ```ts
10184
- readonly protocol: "dedicated" | "p2p" | "loopback"
10526
+ readonly protocol: "p2p" | "dedicated" | "loopback"
10185
10527
  ```
10186
10528
 
10187
10529
  What is on the other end: `dedicated` (the platform's server), `p2p` (the
@@ -10368,6 +10710,84 @@ readonly iconUrl: string
10368
10710
  The badge's thumbnail. Pass it through [platformIconUrl](#platformiconurl) before
10369
10711
  putting it in an `<img src>`.
10370
10712
 
10713
+ <a id="avatar3dfamily"></a>
10714
+
10715
+ ## Avatar3dFamily
10716
+
10717
+ ```ts
10718
+ type Avatar3dFamily = "small_human" | "adult_human" | string & object
10719
+ ```
10720
+
10721
+ A rig family — the skeleton contract a 3D avatar is built on, and what a
10722
+ game declares it can animate. Two today:
10723
+
10724
+ - `small_human`, the toy rig: a `root` node with a `body` child, which holds
10725
+ `head`, `arm_l`, `arm_r`, `leg_l` and `leg_r`, and a `hand_socket` under
10726
+ `arm_r`.
10727
+ - `adult_human`, a full humanoid with fingers: 56 nodes from `root` and
10728
+ `hips` down to `toes_r`, every one world-aligned at rest, so one clip plays
10729
+ on every avatar in the family.
10730
+
10731
+ `AVATAR_RIGS` (from `@indiegems/gem-web-sdk/sdk`) lists every node of
10732
+ both, with its parent.
10733
+
10734
+ Open-ended (`| (string & {})`) so a family the platform adds later does not
10735
+ break a `switch` you compiled against this version: keep a `default` arm and
10736
+ skip a family you do not draw.
10737
+
10738
+ <a id="wornavatar3d"></a>
10739
+
10740
+ ## WornAvatar3d
10741
+
10742
+ ```ts
10743
+ interface WornAvatar3d
10744
+ ```
10745
+
10746
+ One 3D avatar a player wears — at most one per [Avatar3dFamily](#avatar3dfamily).
10747
+
10748
+ <a id="wornavatar3d-family"></a>
10749
+
10750
+ ## WornAvatar3d.family
10751
+
10752
+ ```ts
10753
+ readonly family: Avatar3dFamily
10754
+ ```
10755
+
10756
+ The rig family this avatar is built on.
10757
+
10758
+ <a id="wornavatar3d-id"></a>
10759
+
10760
+ ## WornAvatar3d.id
10761
+
10762
+ ```ts
10763
+ readonly id: string
10764
+ ```
10765
+
10766
+ The avatar's item id — a stable key for the model, never something to
10767
+ show as text.
10768
+
10769
+ <a id="wornavatar3d-modelurl"></a>
10770
+
10771
+ ## WornAvatar3d.modelUrl
10772
+
10773
+ ```ts
10774
+ readonly modelUrl: string
10775
+ ```
10776
+
10777
+ The avatar's model, a binary glTF (`.glb`) on the family's rig. Pass it
10778
+ through [platformIconUrl](#platformiconurl) before loading it.
10779
+
10780
+ <a id="wornavatar3d-iconurl"></a>
10781
+
10782
+ ## WornAvatar3d.iconUrl
10783
+
10784
+ ```ts
10785
+ readonly iconUrl: string
10786
+ ```
10787
+
10788
+ A rendered portrait of the avatar, for a roster or a menu. Pass it through
10789
+ [platformIconUrl](#platformiconurl) before putting it in an `<img src>`.
10790
+
10371
10791
  <a id="playeridentity"></a>
10372
10792
 
10373
10793
  ## PlayerIdentity
@@ -10511,6 +10931,24 @@ loading the model is too much. Not the 2D portrait
10511
10931
  ([PlayerIdentity.avatarIconUrl](#playeridentity-avatariconurl)), which is a separate choice. Pass it
10512
10932
  through [platformIconUrl](#platformiconurl) before putting it in an `<img src>`.
10513
10933
 
10934
+ <a id="playeridentity-avatars3d"></a>
10935
+
10936
+ ## PlayerIdentity.avatars3d
10937
+
10938
+ ```ts
10939
+ readonly avatars3d?: readonly WornAvatar3d[]
10940
+ ```
10941
+
10942
+ Every 3D avatar the player wears, one per rig family, in the order the game
10943
+ declared its families. Absent or empty means none is worn — draw your
10944
+ default look.
10945
+
10946
+ [PlayerIdentity.avatar3dId](#playeridentity-avatar3did) and its two URLs are one of these: the
10947
+ avatar of the game's first declared family that has one. A game that
10948
+ accepts a single family can read either; one that accepts several picks by
10949
+ `family` here. Absent too where the platform does not send it yet — read
10950
+ the flat fields then.
10951
+
10514
10952
  <a id="playeridentity-badges"></a>
10515
10953
 
10516
10954
  ## PlayerIdentity.badges
@@ -10550,7 +10988,7 @@ for (const row of opponents) {
10550
10988
 
10551
10989
  Reads `player_id`, `display_name`, `discriminator`, `display_tag`,
10552
10990
  `avatar_icon_id`, `avatar_icon_url`, `avatar_3d_id`, `avatar_3d_model_url`,
10553
- `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.
10991
+ `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.
10554
10992
 
10555
10993
  | Parameter | Type |
10556
10994
  | --- | --- |
@@ -11810,12 +12248,44 @@ a Buy button mostly does not write one.
11810
12248
  | --- | --- |
11811
12249
  | `req` | `PurchaseRequest` |
11812
12250
 
12251
+ <a id="avatar3dpurchaserequest"></a>
12252
+
12253
+ ## Avatar3dPurchaseRequest
12254
+
12255
+ ```ts
12256
+ interface Avatar3dPurchaseRequest
12257
+ ```
12258
+
12259
+ What a game asks for when it buys one of its own 3D avatars for the player
12260
+ with `gem.avatars3d.purchase`.
12261
+
12262
+ <a id="avatar3dpurchaserequest-itemid"></a>
12263
+
12264
+ ## Avatar3dPurchaseRequest.itemId
12265
+
12266
+ ```ts
12267
+ itemId: string
12268
+ ```
12269
+
12270
+ The avatar's item id — `itemId` on an `Avatar3dOffer`.
12271
+
12272
+ <a id="avatar3dpurchaserequest-currencyslug"></a>
12273
+
12274
+ ## Avatar3dPurchaseRequest.currencySlug
12275
+
12276
+ ```ts
12277
+ currencySlug: string
12278
+ ```
12279
+
12280
+ The platform currency to pay in, `GEMS` or `ARCADE_TICKETS`. The offer
12281
+ must carry a price in it.
12282
+
11813
12283
  <a id="purchaserefusalcode"></a>
11814
12284
 
11815
12285
  ## PurchaseRefusalCode
11816
12286
 
11817
12287
  ```ts
11818
- 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"
12288
+ 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"
11819
12289
  ```
11820
12290
 
11821
12291
  Every reason a purchase can be refused, on either purchase path.
@@ -11841,6 +12311,9 @@ Every reason a purchase can be refused, on either purchase path.
11841
12311
  - `not_available` — the recipe is outside its availability window, or
11842
12312
  inactive; nothing was charged. Hide or disable it until its window opens.
11843
12313
  - `recipe_locked` — the recipe's prerequisites are not met.
12314
+ - `already_owned` — the player already owns the 3D avatar
12315
+ `gem.avatars3d.purchase` asked for; nothing was charged. Only that call
12316
+ answers it.
11844
12317
  - `recipe_on_cooldown`, `recipe_max_uses_exceeded`, `item_max_stack_exceeded`,
11845
12318
  `recipe_costs_unmet`, `quantity_invalid` — the recipe or item refused this
11846
12319
  purchase, before any charge.
@@ -13127,6 +13600,24 @@ loading the model is too much. Not the 2D portrait
13127
13600
  ([PlayerIdentity.avatarIconUrl](#playeridentity-avatariconurl)), which is a separate choice. Pass it
13128
13601
  through [platformIconUrl](#platformiconurl) before putting it in an `<img src>`.
13129
13602
 
13603
+ <a id="leaderboardentry-avatars3d"></a>
13604
+
13605
+ ## LeaderboardEntry.avatars3d
13606
+
13607
+ ```ts
13608
+ readonly avatars3d?: readonly WornAvatar3d[]
13609
+ ```
13610
+
13611
+ Every 3D avatar the player wears, one per rig family, in the order the game
13612
+ declared its families. Absent or empty means none is worn — draw your
13613
+ default look.
13614
+
13615
+ [PlayerIdentity.avatar3dId](#playeridentity-avatar3did) and its two URLs are one of these: the
13616
+ avatar of the game's first declared family that has one. A game that
13617
+ accepts a single family can read either; one that accepts several picks by
13618
+ `family` here. Absent too where the platform does not send it yet — read
13619
+ the flat fields then.
13620
+
13130
13621
  <a id="leaderboardentry-badges"></a>
13131
13622
 
13132
13623
  ## LeaderboardEntry.badges
@@ -14620,6 +15111,24 @@ loading the model is too much. Not the 2D portrait
14620
15111
  ([PlayerIdentity.avatarIconUrl](#playeridentity-avatariconurl)), which is a separate choice. Pass it
14621
15112
  through [platformIconUrl](#platformiconurl) before putting it in an `<img src>`.
14622
15113
 
15114
+ <a id="playerprofile-avatars3d"></a>
15115
+
15116
+ ## PlayerProfile.avatars3d
15117
+
15118
+ ```ts
15119
+ readonly avatars3d?: readonly WornAvatar3d[]
15120
+ ```
15121
+
15122
+ Every 3D avatar the player wears, one per rig family, in the order the game
15123
+ declared its families. Absent or empty means none is worn — draw your
15124
+ default look.
15125
+
15126
+ [PlayerIdentity.avatar3dId](#playeridentity-avatar3did) and its two URLs are one of these: the
15127
+ avatar of the game's first declared family that has one. A game that
15128
+ accepts a single family can read either; one that accepts several picks by
15129
+ `family` here. Absent too where the platform does not send it yet — read
15130
+ the flat fields then.
15131
+
14623
15132
  <a id="playerprofile-badges"></a>
14624
15133
 
14625
15134
  ## PlayerProfile.badges
@@ -17206,6 +17715,24 @@ loading the model is too much. Not the 2D portrait
17206
17715
  ([PlayerIdentity.avatarIconUrl](#playeridentity-avatariconurl)), which is a separate choice. Pass it
17207
17716
  through [platformIconUrl](#platformiconurl) before putting it in an `<img src>`.
17208
17717
 
17718
+ <a id="roommember-avatars3d"></a>
17719
+
17720
+ ## RoomMember.avatars3d
17721
+
17722
+ ```ts
17723
+ readonly avatars3d?: readonly WornAvatar3d[]
17724
+ ```
17725
+
17726
+ Every 3D avatar the player wears, one per rig family, in the order the game
17727
+ declared its families. Absent or empty means none is worn — draw your
17728
+ default look.
17729
+
17730
+ [PlayerIdentity.avatar3dId](#playeridentity-avatar3did) and its two URLs are one of these: the
17731
+ avatar of the game's first declared family that has one. A game that
17732
+ accepts a single family can read either; one that accepts several picks by
17733
+ `family` here. Absent too where the platform does not send it yet — read
17734
+ the flat fields then.
17735
+
17209
17736
  <a id="roommember-badges"></a>
17210
17737
 
17211
17738
  ## RoomMember.badges
@@ -18036,6 +18563,24 @@ loading the model is too much. Not the 2D portrait
18036
18563
  ([PlayerIdentity.avatarIconUrl](#playeridentity-avatariconurl)), which is a separate choice. Pass it
18037
18564
  through [platformIconUrl](#platformiconurl) before putting it in an `<img src>`.
18038
18565
 
18566
+ <a id="showcasefriendentry-avatars3d"></a>
18567
+
18568
+ ## ShowcaseFriendEntry.avatars3d
18569
+
18570
+ ```ts
18571
+ readonly avatars3d?: readonly WornAvatar3d[]
18572
+ ```
18573
+
18574
+ Every 3D avatar the player wears, one per rig family, in the order the game
18575
+ declared its families. Absent or empty means none is worn — draw your
18576
+ default look.
18577
+
18578
+ [PlayerIdentity.avatar3dId](#playeridentity-avatar3did) and its two URLs are one of these: the
18579
+ avatar of the game's first declared family that has one. A game that
18580
+ accepts a single family can read either; one that accepts several picks by
18581
+ `family` here. Absent too where the platform does not send it yet — read
18582
+ the flat fields then.
18583
+
18039
18584
  <a id="showcasefriendentry-badges"></a>
18040
18585
 
18041
18586
  ## ShowcaseFriendEntry.badges