@indiegems/gem-web-sdk 14.0.3 → 15.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` v14.0.3, 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` v15.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
 
@@ -3553,6 +3553,72 @@ them, whatever you set here.
3553
3553
  | --- | --- |
3554
3554
  | `props` | `Readonly<Record<string, AnalyticsValue>>` |
3555
3555
 
3556
+ <a id="analyticsapi-experiment"></a>
3557
+
3558
+ ## AnalyticsApi.experiment
3559
+
3560
+ ```ts
3561
+ experiment(key: string, count: number): Promise<number>
3562
+ ```
3563
+
3564
+ Which arm of one of your experiments this player gets — the same answer on
3565
+ every call, every refresh and every device. Use it wherever you would have
3566
+ picked an arm with `Math.random()`, which deals a player a new arm on each
3567
+ visit and makes the test unreadable.
3568
+
3569
+ Pass a COUNT and get an index, `0` to `count - 1`; pass a LIST of strings
3570
+ and get one of them. A string listed twice is twice as likely.
3571
+
3572
+ ```js
3573
+ const tutorial = await gem.analytics.experiment('tutorial_length', ['short', 'long']);
3574
+ const layout = await gem.analytics.experiment('shop_layout', 3); // 0, 1 or 2
3575
+ ```
3576
+
3577
+ The selection is a hash of the key and the signed-in player's id — FNV-1a
3578
+ 32-bit over `"<key>:<playerId>"`, then MurmurHash3's 32-bit finalizer, mod
3579
+ the arm count; see
3580
+ [experimentSelection](#experimentselection), which computes the same answer for an id you
3581
+ hold. Hashing the key in is what keeps your experiments independent: the
3582
+ players in one experiment's first arm are not the same players in every
3583
+ other experiment's first arm.
3584
+
3585
+ ⚠️ **Keep the key and the arms fixed for the life of the experiment.** A new
3586
+ count or list re-deals every player. To start over on purpose, use a new
3587
+ key.
3588
+
3589
+ ⚠️ **NOTHING IS RECORDED FOR YOU.** The selection is not sent anywhere, and
3590
+ the SDK does not add it to your events. To analyse an experiment, declare a
3591
+ dimension for it on the events you want to break down, with the arms as its
3592
+ values, named after the experiment, and pass the selection there yourself:
3593
+ `gem.analytics.track('tutorial_finished', { tutorial_length: tutorial })`.
3594
+ Never the platform's `variant` field, which belongs to store-page
3595
+ experiments.
3596
+
3597
+ WAITS FOR THE PLAYER. The id arrives with the first play token, so a call
3598
+ during boot waits for it, and rejects as [Gem.awaitIdentity](#gem-awaitidentity) does
3599
+ when none arrives — outside the Arcade, the Gem Editor or the dev host.
3600
+ There is no random fallback: catch the rejection and play your control arm.
3601
+ A guest is selected by their guest id, which is stable for as long as their
3602
+ guest account lasts; once they sign in to an account they are that
3603
+ account's player, and its selection applies.
3604
+
3605
+ In a serverless function it selects for the player who invoked it, with
3606
+ no wait, and rejects when a game server invoked it.
3607
+
3608
+ Works with analytics turned off: it reads no setting and sends nothing.
3609
+
3610
+ | Parameter | Type | Description |
3611
+ | --- | --- | --- |
3612
+ | `key` | `string` | Names the experiment — any non-empty string. |
3613
+ | `count` | `number` | How many arms, an integer from 1 to 2048 — or, in the list
3614
+ form, `options`: the arms themselves, 1 to 2048 strings. |
3615
+
3616
+ **Returns.** The arm's index, or the chosen string.
3617
+
3618
+ **Throws.** TypeError for an empty key or a list holding something other than
3619
+ strings; RangeError for a count or list size outside 1 to 2048. Both
3620
+ reject before waiting for the player.
3621
+
3556
3622
  <a id="analyticsapi-environment"></a>
3557
3623
 
3558
3624
  ## AnalyticsApi.environment
@@ -3680,6 +3746,47 @@ The runtime companion to [PlatformEventName](#platformeventname), for a game tha
3680
3746
  show what it gets for free, or to check a funnel step it authored against
3681
3747
  what the platform actually emits.
3682
3748
 
3749
+ <a id="experimentselection"></a>
3750
+
3751
+ ## experimentSelection
3752
+
3753
+ ```ts
3754
+ function experimentSelection(key: string, playerId: string, count: number): number
3755
+ ```
3756
+
3757
+ Which arm of an experiment a player gets — the same answer for the same key
3758
+ and player, every time, anywhere.
3759
+
3760
+ In a game, call [AnalyticsApi.experiment](#analyticsapi-experiment) instead: it is this, with
3761
+ the signed-in player's id filled in. This form takes the id, for code that
3762
+ has one in hand — a dedicated server reproducing what a member's client
3763
+ picked, or a test.
3764
+
3765
+ Pass a COUNT and get an index, `0` to `count - 1`; pass a LIST of strings
3766
+ and get one of them. A string listed twice is twice as likely, which is how
3767
+ to weight an arm.
3768
+
3769
+ The answer is `fmix32(fnv1a32("<key>:<playerId>")) % count`: FNV-1a 32-bit
3770
+ over the UTF-8 bytes of the key, a colon and the player id, then
3771
+ MurmurHash3's 32-bit finalizer, as an unsigned integer — with a list, the
3772
+ index into it. The hash is fixed, so any other language can reproduce a
3773
+ selection; docs/analytics.md has it written out.
3774
+
3775
+ ⚠️ **Keep the key and the arms fixed for the life of the experiment.** A new
3776
+ count or list re-deals every player. To start over on purpose, use a new
3777
+ key.
3778
+
3779
+ | Parameter | Type | Description |
3780
+ | --- | --- | --- |
3781
+ | `key` | `string` | Names the experiment — any non-empty string, and the only thing
3782
+ that makes one experiment's split independent of another's. |
3783
+ | `playerId` | `string` | The player to select for — [GemIdentity.playerId](#gemidentity-playerid). |
3784
+ | `count` | `number` | How many arms, an integer from 1 to 2048 — or, in the list
3785
+ form, `options`: the arms themselves, 1 to 2048 strings. |
3786
+
3787
+ **Throws.** TypeError for an empty key or player id, or a list holding something
3788
+ other than strings; RangeError for a count or list size outside 1 to 2048.
3789
+
3683
3790
  <a id="avatar3dprice"></a>
3684
3791
 
3685
3792
  ## Avatar3dPrice
@@ -8136,6 +8243,46 @@ rather than cut it, say. The arcade shows its mute button for your game
8136
8243
  either way where it shows one, so `false` is a promise that your handler goes quiet on
8137
8244
  `muted: true`, and the release checklist holds you to it.
8138
8245
 
8246
+ <a id="gemoptions-onaudiochannels"></a>
8247
+
8248
+ ## GemOptions.onAudioChannels
8249
+
8250
+ ```ts
8251
+ readonly onAudioChannels?: (channels: GemAudioChannels) => void
8252
+ ```
8253
+
8254
+ The player turned your game's music, or its sound effects, on or off with
8255
+ the Arcade's **Music** and **Sound** switches. Passing this tells the
8256
+ Arcade your game honours the two separately, so both switches work for it.
8257
+
8258
+ Called once the Arcade's preferences first arrive, and again whenever
8259
+ either value changes. **Turn your music off while `music` is false, and
8260
+ your sound effects off while `sounds` is false**, and back on when they
8261
+ are true:
8262
+
8263
+ ```js
8264
+ createGem({
8265
+ allowedEmbedders,
8266
+ onAudioChannels: ({ music, sounds }) => {
8267
+ musicBus.gain.value = music ? 1 : 0; // the music only
8268
+ effectsBus.gain.value = sounds ? 1 : 0; // everything else
8269
+ },
8270
+ });
8271
+ ```
8272
+
8273
+ Both are `true` until the Arcade says otherwise, and under an Arcade that
8274
+ has no such switches. They are the player's Arcade switches, **not your
8275
+ game's settings**: apply them on top of your own music and sound settings
8276
+ (music plays while `music` AND your own music setting are on) and never
8277
+ save them as the player's choice in your game. `muted` is unchanged —
8278
+ still the master mute the SDK applies for you.
8279
+
8280
+ Without this option the Arcade cannot tell your music from your effects:
8281
+ it mutes your whole game when the player turns both off, and its Music
8282
+ switch flips your own `icon: 'music'` toggle when you declared one with
8283
+ [Gem.settings](#gem-settings). The same two values are always readable as
8284
+ `gem.prefs.music` and `gem.prefs.sounds`.
8285
+
8139
8286
  <a id="gemoptions-selectabletext"></a>
8140
8287
 
8141
8288
  ## GemOptions.selectableText
@@ -8614,6 +8761,38 @@ the SDK counts your document's frames and reports them itself while this
8614
8761
  is true, and the Arcade draws the readout in its own chrome. Do not draw a
8615
8762
  counter of your own over it. `false` when the Arcade never said.
8616
8763
 
8764
+ <a id="gemprefs-music"></a>
8765
+
8766
+ ## GemPrefs.music
8767
+
8768
+ ```ts
8769
+ readonly music: boolean
8770
+ ```
8771
+
8772
+ The player wants your game's music on — the Arcade's **Music** switch.
8773
+ `true` when the Arcade never said.
8774
+
8775
+ While it is false, stop your music and keep your sound effects playing.
8776
+ It sits on top of `muted` and of your own music setting, and is never
8777
+ saved as one: see [GemOptions.onAudioChannels](#gemoptions-onaudiochannels), which is how your
8778
+ game tells the Arcade it honours this.
8779
+
8780
+ <a id="gemprefs-sounds"></a>
8781
+
8782
+ ## GemPrefs.sounds
8783
+
8784
+ ```ts
8785
+ readonly sounds: boolean
8786
+ ```
8787
+
8788
+ The player wants your game's sound effects on — the Arcade's **Sound**
8789
+ switch: every sound that is not your music. `true` when the Arcade never
8790
+ said.
8791
+
8792
+ While it is false, silence your effects and keep your music playing. It
8793
+ sits on top of `muted` and of your own sound settings, and is never saved
8794
+ as one: see [GemOptions.onAudioChannels](#gemoptions-onaudiochannels).
8795
+
8617
8796
  <a id="gemprefs-locale"></a>
8618
8797
 
8619
8798
  ## GemPrefs.locale
@@ -8654,6 +8833,39 @@ const volume = saved.volume ?? gem.prefs?.recommended?.volume ?? 100;
8654
8833
  Unlike `muted` and `reducedMotion`, nothing here is applied for you, and
8655
8834
  nothing obliges you to use it. See the guide's "Game settings" section.
8656
8835
 
8836
+ <a id="gemaudiochannels"></a>
8837
+
8838
+ ## GemAudioChannels
8839
+
8840
+ ```ts
8841
+ interface GemAudioChannels
8842
+ ```
8843
+
8844
+ Which of your game's two audio channels the player wants on, from the
8845
+ Arcade's **Music** and **Sound** switches — what
8846
+ [GemOptions.onAudioChannels](#gemoptions-onaudiochannels) is called with. Both are `true` when the
8847
+ Arcade never said.
8848
+
8849
+ <a id="gemaudiochannels-music"></a>
8850
+
8851
+ ## GemAudioChannels.music
8852
+
8853
+ ```ts
8854
+ readonly music: boolean
8855
+ ```
8856
+
8857
+ Play your music. While false, stop it and keep your effects.
8858
+
8859
+ <a id="gemaudiochannels-sounds"></a>
8860
+
8861
+ ## GemAudioChannels.sounds
8862
+
8863
+ ```ts
8864
+ readonly sounds: boolean
8865
+ ```
8866
+
8867
+ Play your sound effects. While false, silence them and keep your music.
8868
+
8657
8869
  <a id="gemrecommendedsettings"></a>
8658
8870
 
8659
8871
  ## GemRecommendedSettings
@@ -1,6 +1,6 @@
1
1
  # The server surface — API reference
2
2
 
3
- Generated from `@indiegems/gem-web-sdk/sdk` v14.0.3, 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` v15.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/server` exports exactly the symbols on this page and nothing else, for a build that wants only this half.
6
6
 
@@ -2,7 +2,7 @@
2
2
  "schema_version": 2,
3
3
  "source": {
4
4
  "repo": "IndieGems/gem-web-sdk",
5
- "version": "14.0.3"
5
+ "version": "15.1.0"
6
6
  },
7
7
  "entries": [
8
8
  {
@@ -4393,6 +4393,35 @@
4393
4393
  "defaultValue": null,
4394
4394
  "guides": []
4395
4395
  },
4396
+ {
4397
+ "id": "AnalyticsApi.experiment",
4398
+ "name": "experiment",
4399
+ "anchor": "analyticsapi-experiment",
4400
+ "group": "analytics",
4401
+ "signature": "experiment(key: string, count: number): Promise<number>",
4402
+ "documented": true,
4403
+ "summary": "Which arm of one of your experiments this player gets — the same answer on\nevery call, every refresh and every device. Use it wherever you would have\npicked an arm with `Math.random()`, which deals a player a new arm on each\nvisit and makes the test unreadable.\n\nPass a COUNT and get an index, `0` to `count - 1`; pass a LIST of strings\nand get one of them. A string listed twice is twice as likely.\n\n```js\nconst tutorial = await gem.analytics.experiment('tutorial_length', ['short', 'long']);\nconst layout = await gem.analytics.experiment('shop_layout', 3); // 0, 1 or 2\n```\n\nThe selection is a hash of the key and the signed-in player's id — FNV-1a\n32-bit over `\"<key>:<playerId>\"`, then MurmurHash3's 32-bit finalizer, mod\nthe arm count; see\n[experimentSelection](api-reference-game.md#experimentselection), which computes the same answer for an id you\nhold. Hashing the key in is what keeps your experiments independent: the\nplayers in one experiment's first arm are not the same players in every\nother experiment's first arm.\n\n⚠️ **Keep the key and the arms fixed for the life of the experiment.** A new\ncount or list re-deals every player. To start over on purpose, use a new\nkey.\n\n⚠️ **NOTHING IS RECORDED FOR YOU.** The selection is not sent anywhere, and\nthe SDK does not add it to your events. To analyse an experiment, declare a\ndimension for it on the events you want to break down, with the arms as its\nvalues, named after the experiment, and pass the selection there yourself:\n`gem.analytics.track('tutorial_finished', { tutorial_length: tutorial })`.\nNever the platform's `variant` field, which belongs to store-page\nexperiments.\n\nWAITS FOR THE PLAYER. The id arrives with the first play token, so a call\nduring boot waits for it, and rejects as [Gem.awaitIdentity](api-reference-game.md#gem-awaitidentity) does\nwhen none arrives — outside the Arcade, the Gem Editor or the dev host.\nThere is no random fallback: catch the rejection and play your control arm.\nA guest is selected by their guest id, which is stable for as long as their\nguest account lasts; once they sign in to an account they are that\naccount's player, and its selection applies.\n\nIn a serverless function it selects for the player who invoked it, with\nno wait, and rejects when a game server invoked it.\n\nWorks with analytics turned off: it reads no setting and sends nothing.",
4404
+ "deprecated": null,
4405
+ "params": [
4406
+ {
4407
+ "name": "key",
4408
+ "type": "string",
4409
+ "optional": false,
4410
+ "summary": "Names the experiment — any non-empty string."
4411
+ },
4412
+ {
4413
+ "name": "count",
4414
+ "type": "number",
4415
+ "optional": false,
4416
+ "summary": "How many arms, an integer from 1 to 2048 — or, in the list\n form, `options`: the arms themselves, 1 to 2048 strings."
4417
+ }
4418
+ ],
4419
+ "returns": "The arm's index, or the chosen string.",
4420
+ "throws": "TypeError for an empty key or a list holding something other than\n strings; RangeError for a count or list size outside 1 to 2048. Both\n reject before waiting for the player.",
4421
+ "example": null,
4422
+ "defaultValue": null,
4423
+ "guides": []
4424
+ },
4396
4425
  {
4397
4426
  "id": "AnalyticsApi.environment",
4398
4427
  "name": "environment",
@@ -4496,6 +4525,41 @@
4496
4525
  "defaultValue": null,
4497
4526
  "guides": []
4498
4527
  },
4528
+ {
4529
+ "id": "experimentSelection",
4530
+ "name": "experimentSelection",
4531
+ "anchor": "experimentselection",
4532
+ "group": "analytics",
4533
+ "signature": "function experimentSelection(key: string, playerId: string, count: number): number",
4534
+ "documented": true,
4535
+ "summary": "Which arm of an experiment a player gets — the same answer for the same key\nand player, every time, anywhere.\n\nIn a game, call [AnalyticsApi.experiment](api-reference-game.md#analyticsapi-experiment) instead: it is this, with\nthe signed-in player's id filled in. This form takes the id, for code that\nhas one in hand — a dedicated server reproducing what a member's client\npicked, or a test.\n\nPass a COUNT and get an index, `0` to `count - 1`; pass a LIST of strings\nand get one of them. A string listed twice is twice as likely, which is how\nto weight an arm.\n\nThe answer is `fmix32(fnv1a32(\"<key>:<playerId>\")) % count`: FNV-1a 32-bit\nover the UTF-8 bytes of the key, a colon and the player id, then\nMurmurHash3's 32-bit finalizer, as an unsigned integer — with a list, the\nindex into it. The hash is fixed, so any other language can reproduce a\nselection; docs/analytics.md has it written out.\n\n⚠️ **Keep the key and the arms fixed for the life of the experiment.** A new\ncount or list re-deals every player. To start over on purpose, use a new\nkey.",
4536
+ "deprecated": null,
4537
+ "params": [
4538
+ {
4539
+ "name": "key",
4540
+ "type": "string",
4541
+ "optional": false,
4542
+ "summary": "Names the experiment — any non-empty string, and the only thing\n that makes one experiment's split independent of another's."
4543
+ },
4544
+ {
4545
+ "name": "playerId",
4546
+ "type": "string",
4547
+ "optional": false,
4548
+ "summary": "The player to select for — [GemIdentity.playerId](api-reference-game.md#gemidentity-playerid)."
4549
+ },
4550
+ {
4551
+ "name": "count",
4552
+ "type": "number",
4553
+ "optional": false,
4554
+ "summary": "How many arms, an integer from 1 to 2048 — or, in the list\n form, `options`: the arms themselves, 1 to 2048 strings."
4555
+ }
4556
+ ],
4557
+ "returns": null,
4558
+ "throws": "TypeError for an empty key or player id, or a list holding something\n other than strings; RangeError for a count or list size outside 1 to 2048.",
4559
+ "example": null,
4560
+ "defaultValue": null,
4561
+ "guides": []
4562
+ },
4499
4563
  {
4500
4564
  "id": "Avatar3dPrice",
4501
4565
  "name": "Avatar3dPrice",
@@ -9368,6 +9432,22 @@
9368
9432
  "defaultValue": null,
9369
9433
  "guides": []
9370
9434
  },
9435
+ {
9436
+ "id": "GemOptions.onAudioChannels",
9437
+ "name": "onAudioChannels",
9438
+ "anchor": "gemoptions-onaudiochannels",
9439
+ "group": "start",
9440
+ "signature": "readonly onAudioChannels?: (channels: GemAudioChannels) => void",
9441
+ "documented": true,
9442
+ "summary": "The player turned your game's music, or its sound effects, on or off with\nthe Arcade's **Music** and **Sound** switches. Passing this tells the\nArcade your game honours the two separately, so both switches work for it.\n\nCalled once the Arcade's preferences first arrive, and again whenever\neither value changes. **Turn your music off while `music` is false, and\nyour sound effects off while `sounds` is false**, and back on when they\nare true:\n\n```js\ncreateGem({\n allowedEmbedders,\n onAudioChannels: ({ music, sounds }) => {\n musicBus.gain.value = music ? 1 : 0; // the music only\n effectsBus.gain.value = sounds ? 1 : 0; // everything else\n },\n});\n```\n\nBoth are `true` until the Arcade says otherwise, and under an Arcade that\nhas no such switches. They are the player's Arcade switches, **not your\ngame's settings**: apply them on top of your own music and sound settings\n(music plays while `music` AND your own music setting are on) and never\nsave them as the player's choice in your game. `muted` is unchanged —\nstill the master mute the SDK applies for you.\n\nWithout this option the Arcade cannot tell your music from your effects:\nit mutes your whole game when the player turns both off, and its Music\nswitch flips your own `icon: 'music'` toggle when you declared one with\n[Gem.settings](api-reference-game.md#gem-settings). The same two values are always readable as\n`gem.prefs.music` and `gem.prefs.sounds`.",
9443
+ "deprecated": null,
9444
+ "params": [],
9445
+ "returns": null,
9446
+ "throws": null,
9447
+ "example": null,
9448
+ "defaultValue": null,
9449
+ "guides": []
9450
+ },
9371
9451
  {
9372
9452
  "id": "GemOptions.selectableText",
9373
9453
  "name": "selectableText",
@@ -9799,6 +9879,38 @@
9799
9879
  "defaultValue": null,
9800
9880
  "guides": []
9801
9881
  },
9882
+ {
9883
+ "id": "GemPrefs.music",
9884
+ "name": "music",
9885
+ "anchor": "gemprefs-music",
9886
+ "group": "player",
9887
+ "signature": "readonly music: boolean",
9888
+ "documented": true,
9889
+ "summary": "The player wants your game's music on — the Arcade's **Music** switch.\n`true` when the Arcade never said.\n\nWhile it is false, stop your music and keep your sound effects playing.\nIt sits on top of `muted` and of your own music setting, and is never\nsaved as one: see [GemOptions.onAudioChannels](api-reference-game.md#gemoptions-onaudiochannels), which is how your\ngame tells the Arcade it honours this.",
9890
+ "deprecated": null,
9891
+ "params": [],
9892
+ "returns": null,
9893
+ "throws": null,
9894
+ "example": null,
9895
+ "defaultValue": null,
9896
+ "guides": []
9897
+ },
9898
+ {
9899
+ "id": "GemPrefs.sounds",
9900
+ "name": "sounds",
9901
+ "anchor": "gemprefs-sounds",
9902
+ "group": "player",
9903
+ "signature": "readonly sounds: boolean",
9904
+ "documented": true,
9905
+ "summary": "The player wants your game's sound effects on — the Arcade's **Sound**\nswitch: every sound that is not your music. `true` when the Arcade never\nsaid.\n\nWhile it is false, silence your effects and keep your music playing. It\nsits on top of `muted` and of your own sound settings, and is never saved\nas one: see [GemOptions.onAudioChannels](api-reference-game.md#gemoptions-onaudiochannels).",
9906
+ "deprecated": null,
9907
+ "params": [],
9908
+ "returns": null,
9909
+ "throws": null,
9910
+ "example": null,
9911
+ "defaultValue": null,
9912
+ "guides": []
9913
+ },
9802
9914
  {
9803
9915
  "id": "GemPrefs.locale",
9804
9916
  "name": "locale",
@@ -9831,6 +9943,54 @@
9831
9943
  "defaultValue": null,
9832
9944
  "guides": []
9833
9945
  },
9946
+ {
9947
+ "id": "GemAudioChannels",
9948
+ "name": "GemAudioChannels",
9949
+ "anchor": "gemaudiochannels",
9950
+ "group": "player",
9951
+ "signature": "interface GemAudioChannels",
9952
+ "documented": true,
9953
+ "summary": "Which of your game's two audio channels the player wants on, from the\nArcade's **Music** and **Sound** switches — what\n[GemOptions.onAudioChannels](api-reference-game.md#gemoptions-onaudiochannels) is called with. Both are `true` when the\nArcade never said.",
9954
+ "deprecated": null,
9955
+ "params": [],
9956
+ "returns": null,
9957
+ "throws": null,
9958
+ "example": null,
9959
+ "defaultValue": null,
9960
+ "guides": []
9961
+ },
9962
+ {
9963
+ "id": "GemAudioChannels.music",
9964
+ "name": "music",
9965
+ "anchor": "gemaudiochannels-music",
9966
+ "group": "player",
9967
+ "signature": "readonly music: boolean",
9968
+ "documented": true,
9969
+ "summary": "Play your music. While false, stop it and keep your effects.",
9970
+ "deprecated": null,
9971
+ "params": [],
9972
+ "returns": null,
9973
+ "throws": null,
9974
+ "example": null,
9975
+ "defaultValue": null,
9976
+ "guides": []
9977
+ },
9978
+ {
9979
+ "id": "GemAudioChannels.sounds",
9980
+ "name": "sounds",
9981
+ "anchor": "gemaudiochannels-sounds",
9982
+ "group": "player",
9983
+ "signature": "readonly sounds: boolean",
9984
+ "documented": true,
9985
+ "summary": "Play your sound effects. While false, silence them and keep your music.",
9986
+ "deprecated": null,
9987
+ "params": [],
9988
+ "returns": null,
9989
+ "throws": null,
9990
+ "example": null,
9991
+ "defaultValue": null,
9992
+ "guides": []
9993
+ },
9834
9994
  {
9835
9995
  "id": "GemRecommendedSettings",
9836
9996
  "name": "GemRecommendedSettings",
@@ -27936,6 +28096,35 @@
27936
28096
  "defaultValue": null,
27937
28097
  "guides": []
27938
28098
  },
28099
+ {
28100
+ "id": "AnalyticsApi.experiment",
28101
+ "name": "experiment",
28102
+ "anchor": "analyticsapi-experiment",
28103
+ "group": "analytics",
28104
+ "signature": "experiment(key: string, count: number): Promise<number>",
28105
+ "documented": true,
28106
+ "summary": "Which arm of one of your experiments this player gets — the same answer on\nevery call, every refresh and every device. Use it wherever you would have\npicked an arm with `Math.random()`, which deals a player a new arm on each\nvisit and makes the test unreadable.\n\nPass a COUNT and get an index, `0` to `count - 1`; pass a LIST of strings\nand get one of them. A string listed twice is twice as likely.\n\n```js\nconst tutorial = await gem.analytics.experiment('tutorial_length', ['short', 'long']);\nconst layout = await gem.analytics.experiment('shop_layout', 3); // 0, 1 or 2\n```\n\nThe selection is a hash of the key and the signed-in player's id — FNV-1a\n32-bit over `\"<key>:<playerId>\"`, then MurmurHash3's 32-bit finalizer, mod\nthe arm count; see\n`experimentSelection`, which computes the same answer for an id you\nhold. Hashing the key in is what keeps your experiments independent: the\nplayers in one experiment's first arm are not the same players in every\nother experiment's first arm.\n\n⚠️ **Keep the key and the arms fixed for the life of the experiment.** A new\ncount or list re-deals every player. To start over on purpose, use a new\nkey.\n\n⚠️ **NOTHING IS RECORDED FOR YOU.** The selection is not sent anywhere, and\nthe SDK does not add it to your events. To analyse an experiment, declare a\ndimension for it on the events you want to break down, with the arms as its\nvalues, named after the experiment, and pass the selection there yourself:\n`gem.analytics.track('tutorial_finished', { tutorial_length: tutorial })`.\nNever the platform's `variant` field, which belongs to store-page\nexperiments.\n\nWAITS FOR THE PLAYER. The id arrives with the first play token, so a call\nduring boot waits for it, and rejects as `Gem.awaitIdentity` does\nwhen none arrives — outside the Arcade, the Gem Editor or the dev host.\nThere is no random fallback: catch the rejection and play your control arm.\nA guest is selected by their guest id, which is stable for as long as their\nguest account lasts; once they sign in to an account they are that\naccount's player, and its selection applies.\n\nIn a serverless function it selects for the player who invoked it, with\nno wait, and rejects when a game server invoked it.\n\nWorks with analytics turned off: it reads no setting and sends nothing.",
28107
+ "deprecated": null,
28108
+ "params": [
28109
+ {
28110
+ "name": "key",
28111
+ "type": "string",
28112
+ "optional": false,
28113
+ "summary": "Names the experiment — any non-empty string."
28114
+ },
28115
+ {
28116
+ "name": "count",
28117
+ "type": "number",
28118
+ "optional": false,
28119
+ "summary": "How many arms, an integer from 1 to 2048 — or, in the list\n form, `options`: the arms themselves, 1 to 2048 strings."
28120
+ }
28121
+ ],
28122
+ "returns": "The arm's index, or the chosen string.",
28123
+ "throws": "TypeError for an empty key or a list holding something other than\n strings; RangeError for a count or list size outside 1 to 2048. Both\n reject before waiting for the player.",
28124
+ "example": null,
28125
+ "defaultValue": null,
28126
+ "guides": []
28127
+ },
27939
28128
  {
27940
28129
  "id": "AnalyticsApi.environment",
27941
28130
  "name": "environment",
package/docs/changelog.md CHANGED
@@ -16,6 +16,33 @@ Its entries are grouped under up to five headings:
16
16
  - **Rebuild**: a type changed. Recompile against the new version; code that compiles today keeps compiling unless the entry says otherwise.
17
17
  - **New capability**: something a game can now do. Nothing changes unless you use it.
18
18
 
19
+ ## 15.1.0
20
+
21
+ Upgrading from 15.0.0. Impact: none.
22
+
23
+ ### New capability
24
+
25
+ - AnalyticsApi.experiment gives a player a stable experiment arm. Pass an experiment key and either an arm count or a list of options. It returns the same arm for that player on every call, refresh and device: an index from zero up to one less than the count, or one of the strings in the list. Games that picked an arm with a random number landed a player in a different arm on every refresh, so no test could be read. Switching to this call fixes that. Different experiment keys are independent of each other. Nothing is recorded for you. To analyse an experiment, declare a dimension on the events you choose and pass the selection yourself. The call rejects when there is no player.
26
+ - experimentSelection computes the same arm as AnalyticsApi.experiment from an experiment key, a player id you hold, and an arm count or list of options. Use it where the SDK is not running, such as a dedicated game server or an analytics query, to reproduce which arm a player was given.
27
+
28
+ ## 15.0.0
29
+
30
+ *Written from the API facts alone; no summary was generated for this release.*
31
+
32
+ Upgrading from 14.0.3. Impact: required.
33
+
34
+ ### Must change
35
+
36
+ - GemCapability changed its signature.
37
+ - GemHost.sendPrefs changed its signature.
38
+
39
+ ### New capability
40
+
41
+ - GemAudioChannels was added.
42
+ - GemOptions.onAudioChannels was added.
43
+ - GemPrefs.music was added.
44
+ - GemPrefs.sounds was added.
45
+
19
46
  ## 14.0.3
20
47
 
21
48
  Upgrading from 14.0.2. Impact: none.
@@ -50,7 +50,7 @@ either way.
50
50
  ```jsonc
51
51
  // package.json
52
52
  "dependencies": {
53
- "@indiegems/gem-web-sdk": "14.0.3"
53
+ "@indiegems/gem-web-sdk": "15.1.0"
54
54
  }
55
55
  ```
56
56
 
@@ -13,8 +13,8 @@
13
13
  },
14
14
  {
15
15
  "file": "analytics.md",
16
- "sha256": "94baa26268843ba926462633ddec8aaca56aabf79ee6639aaf77df68474debc7",
17
- "updated": "2026-10-17T00:33:31Z"
16
+ "sha256": "afced8f94514818eca6de3e72bdd988b2cee10925086c3bfa47c0a8ec8967454",
17
+ "updated": "2026-10-23T00:13:14Z"
18
18
  },
19
19
  {
20
20
  "file": "arcade-avatars.md",
@@ -38,8 +38,8 @@
38
38
  },
39
39
  {
40
40
  "file": "changelog.md",
41
- "sha256": "7858f383ced2f344a4f7852bd4d0ae8d3a2f114f21d2e6e7660e64dd6e2220ae",
42
- "updated": "2026-10-21T20:47:06Z"
41
+ "sha256": "6b47b52430c201a1eb48a4823b6e3ca59d1e82982428eab0ab7ac3016cc044c1",
42
+ "updated": "2026-10-23T00:49:37Z"
43
43
  },
44
44
  {
45
45
  "file": "daily-challenges.md",
@@ -68,8 +68,8 @@
68
68
  },
69
69
  {
70
70
  "file": "getting-started.md",
71
- "sha256": "5b5a3affd113400ea9aecbc6af254831ff543278c125a9a11a18fa08ed390171",
72
- "updated": "2026-10-21T20:47:06Z"
71
+ "sha256": "68ddb1e937d14ba27aadbb2cd19ae59d0cb9048ec2d4435b4d80f87e8ac1eb1a",
72
+ "updated": "2026-10-23T00:49:37Z"
73
73
  },
74
74
  {
75
75
  "file": "inventory-and-catalogs.md",
@@ -128,8 +128,8 @@
128
128
  },
129
129
  {
130
130
  "file": "server-bundles.md",
131
- "sha256": "72e4035ee007042a4bae279afd4440f36c8d64e0c750978fac6a150fad190d78",
132
- "updated": "2026-10-21T20:47:06Z"
131
+ "sha256": "c30b720cf1b65dc4e7a2ed25f1dfc86d9c86ba11d69509b45c63183649f3a7aa",
132
+ "updated": "2026-10-23T00:49:37Z"
133
133
  },
134
134
  {
135
135
  "file": "serverless-functions-js.md",
@@ -153,8 +153,8 @@
153
153
  },
154
154
  {
155
155
  "file": "what-a-game-must-do.md",
156
- "sha256": "a0f1fd58acd28c72cca3574032c087ffbe2cb34e311086c99eceec7e2f9b7f20",
157
- "updated": "2026-10-21T19:36:16Z"
156
+ "sha256": "1dae31932542cf7d49e8b1c1bc456e62cfb72204e88ea776919cca12c690c859",
157
+ "updated": "2026-10-22T15:46:02Z"
158
158
  }
159
159
  ]
160
160
  }
@@ -80,8 +80,8 @@ To check the download yourself: the package npm serves is, byte for byte, the fi
80
80
  also publishes on its download host, with the hash beside it:
81
81
 
82
82
  ```bash
83
- curl -fsSL -O https://downloads.gemarcade.com/sdk/v14.0.3/gem-web-sdk.tgz
84
- curl -fsSL https://downloads.gemarcade.com/sdk/v14.0.3/gem-web-sdk.tgz.sri
83
+ curl -fsSL -O https://downloads.gemarcade.com/sdk/v15.1.0/gem-web-sdk.tgz
84
+ curl -fsSL https://downloads.gemarcade.com/sdk/v15.1.0/gem-web-sdk.tgz.sri
85
85
  openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
86
86
  ```
87
87