@indiegems/gem-web-sdk 13.0.0 → 13.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +4 -3
  3. package/dist/function.d.ts +2 -1
  4. package/dist/game.d.ts +37 -2
  5. package/dist/gem-web-sdk.bridge.js +1 -1
  6. package/dist/gem-web-sdk.esm.js +2 -2
  7. package/dist/gem-web-sdk.function.js +2 -2
  8. package/dist/gem-web-sdk.game.js +2 -2
  9. package/dist/gem-web-sdk.min.js +2 -2
  10. package/dist/gem-web-sdk.sdk.js +2 -2
  11. package/dist/gem-web-sdk.server-api.js +1 -1
  12. package/dist/gem-web-sdk.server.js +2 -2
  13. package/dist/index.d.ts +39 -5
  14. package/dist/sdk.d.ts +37 -2
  15. package/dist/server-api.d.ts +1 -0
  16. package/dist/server.d.ts +1 -0
  17. package/docs/analytics.md +6 -3
  18. package/docs/api/api-reference-function.md +38 -7
  19. package/docs/api/api-reference-game.md +381 -33
  20. package/docs/api/api-reference-server.md +23 -2
  21. package/docs/api/reference.json +401 -25
  22. package/docs/arcade-avatars.md +194 -26
  23. package/docs/brand-art.md +3 -3
  24. package/docs/changelog.md +30 -0
  25. package/docs/deprecation-policy.md +6 -0
  26. package/docs/getting-started.md +9 -5
  27. package/docs/inventory-and-catalogs.md +1 -1
  28. package/docs/manifest.json +29 -29
  29. package/docs/platform-moments.md +266 -0
  30. package/docs/rewarded-ads.md +26 -9
  31. package/docs/server-bundles.md +2 -2
  32. package/docs/serverless-functions-js.md +17 -1
  33. package/docs/serverless-functions.md +13 -7
  34. package/docs/what-a-game-must-do.md +126 -70
  35. package/package.json +6 -2
  36. package/rigs/adult_human.glb +0 -0
  37. package/schema/bridge.v1.json +160 -7
  38. package/schema/capabilities.public.json +16 -0
  39. package/schema/moment-log-line.json +109 -0
  40. package/docs/cross-promotion.md +0 -232
@@ -2,7 +2,7 @@
2
2
  "schema_version": 2,
3
3
  "source": {
4
4
  "repo": "IndieGems/gem-web-sdk",
5
- "version": "13.0.0"
5
+ "version": "13.2.0"
6
6
  },
7
7
  "entries": [
8
8
  {
@@ -53,9 +53,13 @@
53
53
  "key": "deviceSettings",
54
54
  "title": "Settings kept on this device"
55
55
  },
56
+ {
57
+ "key": "moments",
58
+ "title": "Platform moments"
59
+ },
56
60
  {
57
61
  "key": "ads",
58
- "title": "Cross-promotion"
62
+ "title": "Rewarded ads, and the deprecated interstitial"
59
63
  },
60
64
  {
61
65
  "key": "account",
@@ -382,7 +386,7 @@
382
386
  "defaultValue": null,
383
387
  "guides": [
384
388
  {
385
- "slug": "cross-promotion",
389
+ "slug": "rewarded-ads",
386
390
  "anchor": null
387
391
  }
388
392
  ]
@@ -3767,7 +3771,7 @@
3767
3771
  "group": "ads",
3768
3772
  "signature": "type AdFormat = \"interstitial\" | \"rewarded\"",
3769
3773
  "documented": true,
3770
- "summary": "Which kind of break this is.\n\n`interstitial` is a panel the player dismisses and nothing follows from.\n\n`rewarded` is a button the player pressed for a reward, such as a revive or\nan extra attempt, that your game config declares for the placement in\n`rewarded_placements`. The press is the player's opt-in, and the arcade\nstarts the ad straight away, so ask from the button's own handler (see\n[AdsApi.requestAd](api-reference-game.md#adsapi-requestad)). It is served where this arcade serves it, and\nanswered `no_ad` where it does not. ⚠️ The outcome grants nothing: the\nplatform grants the reward, against its own record, never your game reading\n`shown`. It arrives on your game's inventory for this player, written by the\nplatform, so re-read `gem.me.inventory` after a `rewarded` ask settles rather\nthan waiting for a signal on this pair; there will not be one.",
3774
+ "summary": "Which kind of break this is.\n\n`interstitial` is a panel the player dismisses and nothing follows from.\n**Deprecated:** signal a platform moment with `gem.moments.signal` instead.\nIt keeps working — the arcade treats it as a platform moment — and `14.0.0`\nis the earliest release that may remove it (`docs/deprecation-policy.md`).\n\n`rewarded` is a button the player pressed for a reward, such as a revive or\nan extra attempt, that your game config declares for the placement in\n`rewarded_placements`. The press is the player's opt-in, and the arcade\nstarts the ad straight away, so ask from the button's own handler (see\n[AdsApi.requestAd](api-reference-game.md#adsapi-requestad)). It is served where this arcade serves it, and\nanswered `no_ad` where it does not. ⚠️ The outcome grants nothing: the\nplatform grants the reward, against its own record, never your game reading\n`shown`. It arrives on your game's inventory for this player, written by the\nplatform, so re-read `gem.me.inventory` after a `rewarded` ask settles rather\nthan waiting for a signal on this pair; there will not be one.",
3771
3775
  "deprecated": null,
3772
3776
  "params": [],
3773
3777
  "returns": null,
@@ -3884,7 +3888,7 @@
3884
3888
  "group": "ads",
3885
3889
  "signature": "interface AdsApi",
3886
3890
  "documented": true,
3887
- "summary": "The cross-promotion surface. `gem.ads` is one of these.",
3891
+ "summary": "Rewarded ads, and the deprecated interstitial ask. `gem.ads` is one of these.",
3888
3892
  "deprecated": null,
3889
3893
  "params": [],
3890
3894
  "returns": null,
@@ -3900,7 +3904,7 @@
3900
3904
  "group": "ads",
3901
3905
  "signature": "requestAd(req: AdRequest): Promise<AdOutcome>",
3902
3906
  "documented": true,
3903
- "summary": "Ask whether the arcade has anything to show at this break in play.\n\nNOTHING REJECTS, and the answer is usually `no_ad`. Every local refusal —\nan arcade that does not do this at all, a call while another is still in\nflight, a `placement` this module will not send — resolves `no_ad` rather\nthan throwing, so a game needs exactly one branch. Only \"no answer ever\narrived\" resolves `unavailable`.\n\nThe arcade pauses your game while a panel is up and resumes it when this\nsettles, so you do not need to pause anything yourself.\n\n⚠️ `shown` grants nothing. Never hand out a reward, an unlock or a currency\non the strength of it. A `rewarded` placement's reward is granted by the\nplatform into your game's inventory; re-read `gem.me.inventory` once this\nsettles, whatever it settled as.\n\n**A `rewarded` ask must come from the player's press.** The press is the\nplayer's opt-in, and the arcade starts the ad as soon as the ask arrives.\nCall this from your button's `click`, `pointerup`, `touchend` or `keydown`\nhandler, before anything that can outlast the press, such as a slow\n`await` or a timer. **Not from `pointerdown` or `touchstart`:** on a\ntouchscreen they run before the browser counts the tap, so the ask is\nrefused on a phone while the same code works at a desk with a mouse. A\ncontroller press counts when the ask follows it within a second, so ask\nfrom the frame of your input loop that saw the button go down. Where the\nbrowser reports user activation (`navigator.userActivation`), the frame\nholds none, and no controller was used in the last second, this answers\n`no_ad` without asking. It also calls your `onError` handler with\ncode `'internal'` and a message that begins\n`rewarded_requires_user_gesture`, and writes one `[gem]` line a session\nthrough your `log`. Where the browser has no such API, the ask goes ahead.\nAn `interstitial` needs no press.",
3907
+ "summary": "Ask whether the arcade has anything to show at this break in play.\n\n**For a break in play, signal a platform moment with\n`gem.moments.signal` instead: `format: 'interstitial'` is deprecated.** It\nkeeps working until at least `14.0.0`. A `rewarded` ask is unchanged.\n\nNOTHING REJECTS, and the answer is usually `no_ad`. Every local refusal —\nan arcade that does not do this at all, a call while another is still in\nflight, a `placement` this module will not send — resolves `no_ad` rather\nthan throwing, so a game needs exactly one branch. Only \"no answer ever\narrived\" resolves `unavailable`.\n\nThe arcade pauses your game while a panel is up and resumes it when this\nsettles, so you do not need to pause anything yourself.\n\n⚠️ `shown` grants nothing. Never hand out a reward, an unlock or a currency\non the strength of it. A `rewarded` placement's reward is granted by the\nplatform into your game's inventory; re-read `gem.me.inventory` once this\nsettles, whatever it settled as.\n\n**A `rewarded` ask must come from the player's press.** The press is the\nplayer's opt-in, and the arcade starts the ad as soon as the ask arrives.\nCall this from your button's `click`, `pointerup`, `touchend` or `keydown`\nhandler, before anything that can outlast the press, such as a slow\n`await` or a timer. **Not from `pointerdown` or `touchstart`:** on a\ntouchscreen they run before the browser counts the tap, so the ask is\nrefused on a phone while the same code works at a desk with a mouse. A\ncontroller press counts when the ask follows it within a second, so ask\nfrom the frame of your input loop that saw the button go down. Where the\nbrowser reports user activation (`navigator.userActivation`), the frame\nholds none, and no controller was used in the last second, this answers\n`no_ad` without asking. It also calls your `onError` handler with\ncode `'internal'` and a message that begins\n`rewarded_requires_user_gesture`, and writes one `[gem]` line a session\nthrough your `log`. Where the browser has no such API, the ask goes ahead.\nAn `interstitial` needs no press.",
3904
3908
  "deprecated": null,
3905
3909
  "params": [
3906
3910
  {
@@ -3912,7 +3916,7 @@
3912
3916
  ],
3913
3917
  "returns": "The arcade's answer. Always resolves; never rejects.",
3914
3918
  "throws": null,
3915
- "example": "```ts\nconst { outcome } = await gem.ads.requestAd({\n placement: 'level-complete',\n format: 'interstitial',\n});\n// `no_ad` is the ordinary answer. There is nothing to show and nothing\n// to draw; carry straight on to the next level.\nstartNextLevel();\n```\n\n```ts\n// A rewarded ask, from the button's own click: the ask comes first, and\n// the inventory read comes after it.\nwatchAdButton.addEventListener('click', async () => {\n watchAdButton.disabled = true; // only between the press and the answer\n await gem.ads.requestAd({ placement: 'dice_rolls', format: 'rewarded' });\n gem.me.inventory.refresh();\n try {\n showDice(await gem.me.inventory.snapshot());\n } finally {\n watchAdButton.disabled = false;\n }\n});\n```",
3919
+ "example": "```ts\n// A rewarded ask, from the button's own click: the ask comes first, and\n// the inventory read comes after it.\nwatchAdButton.addEventListener('click', async () => {\n watchAdButton.disabled = true; // only between the press and the answer\n await gem.ads.requestAd({ placement: 'dice_rolls', format: 'rewarded' });\n gem.me.inventory.refresh();\n try {\n showDice(await gem.me.inventory.snapshot());\n } finally {\n watchAdButton.disabled = false;\n }\n});\n```",
3916
3920
  "defaultValue": null,
3917
3921
  "guides": []
3918
3922
  },
@@ -3939,7 +3943,7 @@
3939
3943
  "defaultValue": null,
3940
3944
  "guides": [
3941
3945
  {
3942
- "slug": "cross-promotion",
3946
+ "slug": "rewarded-ads",
3943
3947
  "anchor": null
3944
3948
  }
3945
3949
  ]
@@ -4319,7 +4323,7 @@
4319
4323
  "group": "analytics",
4320
4324
  "signature": "class AnalyticsApi",
4321
4325
  "documented": true,
4322
- "summary": "Your game's own analytics — the events you declared in your game config,\nplus the `gem_*` session and match events the SDK emits for you.\n\nReached as `gem.analytics`. [AnalyticsApi.track](api-reference-game.md#analyticsapi-track) is synchronous, never\nthrows and never rejects: it puts an event on an in-memory queue, and the\nSDK posts that queue on its own timer, when it reaches 50 events, and on the\nway out of the page.\n\n```js\ngem.analytics.setContext({ build: 'v1.4.2' });\n\ngem.analytics.track('level_completed', {\n outcome: 'win', // a dimension — a declared, closed set\n difficulty: 'normal', // a dimension\n duration_ms: 48_120, // a measure — summed, averaged, min and max\n score: 3_100, // a measure\n});\n```\n\n⚠️ **Every key has to be declared.** `analytics_events` in your game config\nis what says which events exist, which dimensions each has and which values\neach dimension may take. An undeclared name or key is not stored — it comes\nback as a rejection, which the SDK prints to the console during development.\n\n⚠️ **Detail older than 30 days is gone.** Raw events live 30 days; the daily\ncounters live 400. Anything you want to be able to ask about a year from now\nhas to be a declared dimension today.\n\n⚠️ **These are your numbers, not the platform's.** Client events are\ngame-asserted browser facts. They feed your analytics and nothing the\nplatform treats as authoritative — not rewards, not dailies, not store\nranking.",
4326
+ "summary": "Your game's own analytics — the events you declared in your game config,\nplus the `gem_*` session and match events the SDK emits for you.\n\nReached as `gem.analytics`. [AnalyticsApi.track](api-reference-game.md#analyticsapi-track) is synchronous, never\nthrows and never rejects: it puts an event on an in-memory queue, and the\nSDK posts that queue on its own timer, when it reaches 50 events, and on the\nway out of the page.\n\n```js\ngem.analytics.setContext({ build: 'v1.4.2' });\n\ngem.analytics.track('level_completed', {\n outcome: 'win', // a dimension — a declared, closed set\n difficulty: 'normal', // a dimension\n duration_ms: 48_120, // a measure — summed, averaged, min and max\n score: 3_100, // a measure\n});\n```\n\n⚠️ **Every key has to be declared.** `analytics_events` in your game config\nis what says which events exist, which dimensions each has and which values\neach dimension may take. An undeclared name or key is not stored — it comes\nback as a rejection, which the SDK prints to the console during development.\n\n⚠️ **Detail older than 60 days is gone.** Raw events live 60 days; the daily\ncounters live 400. Anything you want to be able to ask about a year from now\nhas to be a declared dimension today.\n\n⚠️ **These are your numbers, not the platform's.** Client events are\ngame-asserted browser facts. They feed your analytics and nothing the\nplatform treats as authoritative — not rewards, not dailies, not store\nranking.",
4323
4327
  "deprecated": null,
4324
4328
  "params": [],
4325
4329
  "returns": null,
@@ -4408,7 +4412,7 @@
4408
4412
  "group": "analytics",
4409
4413
  "signature": "flush(options?: AnalyticsFlushOptions): Promise<AnalyticsFlushResult>",
4410
4414
  "documented": true,
4411
- "summary": "Send what is queued now, and find out what happened to it.\n\nResolves with the platform's own answer, INCLUDING per-event rejections, which\narrive inside a 200 — see [AnalyticsFlushResult.rejected](api-reference-game.md#analyticsflushresult-rejected). You do\nnot have to call this: the SDK flushes on its own, and on the way out of\nthe page.\n\nA refusal that could clear on its own — a 429 from the rate limiter, a 503\nwhile the platform's idempotency store is unreachable — KEEPS the queue\nand rejects, so the next flush sends the same events rather than losing\nthem. A refusal that cannot clear discards the batch it was sent for,\nbecause resending it can only fail identically.",
4415
+ "summary": "Send what is queued now, and find out what happened to it.\n\nResolves with the platform's own answer, INCLUDING per-event rejections, which\narrive inside a 200 — see [AnalyticsFlushResult.rejected](api-reference-game.md#analyticsflushresult-rejected). You do\nnot have to call this: the SDK flushes on its own, and on the way out of\nthe page.\n\nA refusal that could clear on its own — a 429 from the rate limiter, a 503\nwhile the platform's idempotency store is unreachable — KEEPS the queue\nand rejects, so the next flush sends the same events rather than losing\nthem. The SDK's own flushes then hold off for the longer of the server's\n`Retry-After` and a cooldown that doubles from 10 seconds, up to five\nminutes. A refusal that cannot clear discards the batch it was sent for,\nbecause resending it can only fail identically.\n\nOne flush at a time: called while another is still on the wire, this\nresolves at once with nothing sent, and what is queued goes out after it.",
4412
4416
  "deprecated": null,
4413
4417
  "params": [
4414
4418
  {
@@ -4797,6 +4801,118 @@
4797
4801
  "defaultValue": null,
4798
4802
  "guides": []
4799
4803
  },
4804
+ {
4805
+ "id": "AvatarRigNode",
4806
+ "name": "AvatarRigNode",
4807
+ "anchor": "avatarrignode",
4808
+ "group": "avatars3d",
4809
+ "signature": "interface AvatarRigNode",
4810
+ "documented": true,
4811
+ "summary": "One node of a rig family's tree: its exact, case-sensitive name, and the\nname of the node it is a DIRECT child of. `parent` is `null` for the rig's\nroot, which is a root node of the scene.",
4812
+ "deprecated": null,
4813
+ "params": [],
4814
+ "returns": null,
4815
+ "throws": null,
4816
+ "example": null,
4817
+ "defaultValue": null,
4818
+ "guides": []
4819
+ },
4820
+ {
4821
+ "id": "AvatarRigNode.node",
4822
+ "name": "node",
4823
+ "anchor": "avatarrignode-node",
4824
+ "group": "avatars3d",
4825
+ "signature": "readonly node: string",
4826
+ "documented": true,
4827
+ "summary": "The node's name, exact and case-sensitive.",
4828
+ "deprecated": null,
4829
+ "params": [],
4830
+ "returns": null,
4831
+ "throws": null,
4832
+ "example": null,
4833
+ "defaultValue": null,
4834
+ "guides": []
4835
+ },
4836
+ {
4837
+ "id": "AvatarRigNode.parent",
4838
+ "name": "parent",
4839
+ "anchor": "avatarrignode-parent",
4840
+ "group": "avatars3d",
4841
+ "signature": "readonly parent: string | null",
4842
+ "documented": true,
4843
+ "summary": "The node it is a direct child of; `null` for the rig's root.",
4844
+ "deprecated": null,
4845
+ "params": [],
4846
+ "returns": null,
4847
+ "throws": null,
4848
+ "example": null,
4849
+ "defaultValue": null,
4850
+ "guides": []
4851
+ },
4852
+ {
4853
+ "id": "AvatarRigs",
4854
+ "name": "AvatarRigs",
4855
+ "anchor": "avatarrigs",
4856
+ "group": "avatars3d",
4857
+ "signature": "interface AvatarRigs",
4858
+ "documented": true,
4859
+ "summary": "The rig families this SDK knows, by name.",
4860
+ "deprecated": null,
4861
+ "params": [],
4862
+ "returns": null,
4863
+ "throws": null,
4864
+ "example": null,
4865
+ "defaultValue": null,
4866
+ "guides": []
4867
+ },
4868
+ {
4869
+ "id": "AvatarRigs.small_human",
4870
+ "name": "small_human",
4871
+ "anchor": "avatarrigs-small_human",
4872
+ "group": "avatars3d",
4873
+ "signature": "readonly small_human: readonly AvatarRigNode[]",
4874
+ "documented": true,
4875
+ "summary": "The toy rig: rigid limbs on a body, eight nodes.",
4876
+ "deprecated": null,
4877
+ "params": [],
4878
+ "returns": null,
4879
+ "throws": null,
4880
+ "example": null,
4881
+ "defaultValue": null,
4882
+ "guides": []
4883
+ },
4884
+ {
4885
+ "id": "AvatarRigs.adult_human",
4886
+ "name": "adult_human",
4887
+ "anchor": "avatarrigs-adult_human",
4888
+ "group": "avatars3d",
4889
+ "signature": "readonly adult_human: readonly AvatarRigNode[]",
4890
+ "documented": true,
4891
+ "summary": "The adult humanoid with fingers: 56 nodes, world-aligned at rest.",
4892
+ "deprecated": null,
4893
+ "params": [],
4894
+ "returns": null,
4895
+ "throws": null,
4896
+ "example": null,
4897
+ "defaultValue": null,
4898
+ "guides": []
4899
+ },
4900
+ {
4901
+ "id": "AVATAR_RIGS",
4902
+ "name": "AVATAR_RIGS",
4903
+ "anchor": "avatar_rigs",
4904
+ "group": "avatars3d",
4905
+ "signature": "AVATAR_RIGS: AvatarRigs",
4906
+ "documented": true,
4907
+ "summary": "Every node each rig family requires, with the node it is a direct child of,\nparents before children. Frozen: read it, never change it.\n\nThe platform refuses an upload whose model is missing one of these, has one\nunder another parent, or has one twice (`rig_mismatch`). Extra nodes are\nallowed anywhere. `adult_human` also requires every one of its nodes to be\nworld-aligned at rest; that rule is not in this list, see the guide.\n\n```js\nimport { AVATAR_RIGS } from '@indiegems/gem-web-sdk/sdk';\n\n// Every joint of the worn avatar, by name.\nconst joints = new Map(\n AVATAR_RIGS.adult_human.map(({ node }) => [node, gltf.scene.getObjectByName(node)]),\n);\n\n// A test that one of your own models is on the rig.\nfor (const { node, parent } of AVATAR_RIGS.adult_human) {\n const found = gltf.scene.getObjectByName(node);\n if (!found) throw new Error(`missing ${node}`);\n if (parent !== null && found.parent?.name !== parent) throw new Error(`${node} is not under ${parent}`);\n}\n```",
4908
+ "deprecated": null,
4909
+ "params": [],
4910
+ "returns": null,
4911
+ "throws": null,
4912
+ "example": null,
4913
+ "defaultValue": null,
4914
+ "guides": []
4915
+ },
4800
4916
  {
4801
4917
  "id": "GemApiError",
4802
4918
  "name": "GemApiError",
@@ -6346,7 +6462,7 @@
6346
6462
  "group": "platform",
6347
6463
  "signature": "interface RewardCue",
6348
6464
  "documented": true,
6349
- "summary": "A platform-currency reward on its way to the arcade — in the Gem Arcade, a\nflight into the Gem button at the top-left of your frame — what\n`gem.cueReward` takes.",
6465
+ "summary": "A platform-currency reward on its way to the arcade — in the Gem Arcade, a\nflight into the Gem button, in whichever corner of your frame it sits — what\n`gem.cueReward` takes.",
6350
6466
  "deprecated": null,
6351
6467
  "params": [],
6352
6468
  "returns": null,
@@ -6435,6 +6551,22 @@
6435
6551
  "defaultValue": null,
6436
6552
  "guides": []
6437
6553
  },
6554
+ {
6555
+ "id": "GemCorner",
6556
+ "name": "GemCorner",
6557
+ "anchor": "gemcorner",
6558
+ "group": "frame",
6559
+ "signature": "type GemCorner = \"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\"",
6560
+ "documented": true,
6561
+ "summary": "A corner of your frame, for `gem.setGemCorner`: where you would like the\narcade's Gem button to sit. `'top-left'` is where it sits unless you ask.\nPhysical corners: `'top-left'` is the top-left in every language.",
6562
+ "deprecated": null,
6563
+ "params": [],
6564
+ "returns": null,
6565
+ "throws": null,
6566
+ "example": null,
6567
+ "defaultValue": null,
6568
+ "guides": []
6569
+ },
6438
6570
  {
6439
6571
  "id": "FrameState",
6440
6572
  "name": "FrameState",
@@ -6769,6 +6901,29 @@
6769
6901
  "defaultValue": null,
6770
6902
  "guides": []
6771
6903
  },
6904
+ {
6905
+ "id": "GemFrame.setGemCorner",
6906
+ "name": "setGemCorner",
6907
+ "anchor": "gemframe-setgemcorner",
6908
+ "group": "frame",
6909
+ "signature": "setGemCorner(corner: GemCorner): boolean",
6910
+ "documented": true,
6911
+ "summary": "Ask the arcade to put its own control over your game — the Gem button —\nin this corner of your frame: the whole answer each time, the latest one\nwinning.\n\nPrefer `gem.setGemCorner`, which holds the corner across a reconnect.\nThis one sends only while connected, and only to an arcade that can move\nits control.",
6912
+ "deprecated": null,
6913
+ "params": [
6914
+ {
6915
+ "name": "corner",
6916
+ "type": "GemCorner",
6917
+ "optional": false,
6918
+ "summary": null
6919
+ }
6920
+ ],
6921
+ "returns": "whether the message left the frame — `false` before the bridge is\n up, for an arcade that cannot move its control, for a value that is not\n one of the four corners, and past this frame's own limit.",
6922
+ "throws": null,
6923
+ "example": null,
6924
+ "defaultValue": null,
6925
+ "guides": []
6926
+ },
6772
6927
  {
6773
6928
  "id": "GemFrame.requestShare",
6774
6929
  "name": "requestShare",
@@ -9401,7 +9556,7 @@
9401
9556
  "group": "player",
9402
9557
  "signature": "readonly reserved: readonly GemReservedArea[]",
9403
9558
  "documented": true,
9404
- "summary": "Rectangles of your frame the arcade's own controls cover — in the Gem\nArcade, its Gem button in the top-left corner, and the frame-rate readout\nbeside it while the player shows it; an embedder that draws wallets over\nyour game adds theirs (see `setWalletVisible`). Keep essential UI and touch\ntargets out of them. Empty when the arcade covers nothing.\n\nA stylesheet reads the per-corner custom properties the SDK stamps from\nthese instead — `--gem-reserved-tl-height` and friends.",
9559
+ "summary": "Rectangles of your frame the arcade's own controls cover — in the Gem\nArcade, its Gem button in whichever corner it sits, and the frame-rate readout\nbeside it while the player shows it; an embedder that draws wallets over\nyour game adds theirs (see `setWalletVisible`). Keep essential UI and touch\ntargets out of them. Empty when the arcade covers nothing.\n\nA stylesheet reads the per-corner custom properties the SDK stamps from\nthese instead — `--gem-reserved-tl-height` and friends.",
9405
9560
  "deprecated": null,
9406
9561
  "params": [],
9407
9562
  "returns": null,
@@ -9956,6 +10111,27 @@
9956
10111
  "defaultValue": null,
9957
10112
  "guides": []
9958
10113
  },
10114
+ {
10115
+ "id": "Gem.moments",
10116
+ "name": "moments",
10117
+ "anchor": "gem-moments",
10118
+ "group": "start",
10119
+ "signature": "readonly moments: MomentsApi",
10120
+ "documented": true,
10121
+ "summary": "Platform moments — **every game, at every break.** Signal one from the\nplayer's press that leaves a natural break in play (Next, Retry, Back to\nmenu), and the arcade may show one of its own offers over your paused\ngame. It usually shows nothing.\n\nRequired by the release checklist at every important break, whether or\nnot your game has a shop of its own. Never resolves a reason, never\nrejects, and `shown` grants nothing.",
10122
+ "deprecated": null,
10123
+ "params": [],
10124
+ "returns": null,
10125
+ "throws": null,
10126
+ "example": null,
10127
+ "defaultValue": null,
10128
+ "guides": [
10129
+ {
10130
+ "slug": "platform-moments",
10131
+ "anchor": null
10132
+ }
10133
+ ]
10134
+ },
9959
10135
  {
9960
10136
  "id": "Gem.ads",
9961
10137
  "name": "ads",
@@ -9963,7 +10139,7 @@
9963
10139
  "group": "start",
9964
10140
  "signature": "readonly ads: AdsApi",
9965
10141
  "documented": true,
9966
- "summary": "One full-screen cross-promotion at a natural break in play, rendered by\nthe arcade over your paused game.\n\nAsk at a break the player already expects — a level ending, a round\nfinishing — never mid-input. **The answer is usually no, and that is\nordinary rather than a failure:** carry straight on, and draw no\nplaceholder while you wait.\n\nNothing here rejects. The arcade pauses your game while a panel is up and\nresumes it when the promise settles, so you do not pause anything yourself.\n\n⚠️ `shown` **grants nothing.** It is not a receipt and not an entitlement,\nand a reward must never be handed out on the strength of it.\n\nYou are told no reason for a no, and there is nothing to infer from one\nabout the player. What you can read is your own economy:\n`rewardedStatus(placement)` says whether a rewarded placement could pay\nright now under your own declared caps — never whether an ad will play.",
10142
+ "summary": "Rewarded ads, and the deprecated interstitial ask.\n\n**`format: 'interstitial'` is deprecated: signal a platform moment with\n`gem.moments.signal` instead.** It keeps working until at least `14.0.0`\n(`docs/deprecation-policy.md`); the arcade treats it as a platform moment.\n\nA rewarded ask comes from the player's press on your own button. **The\nanswer is usually no, and that is ordinary rather than a failure:** carry\nstraight on, and draw no placeholder while you wait.\n\nNothing here rejects. The arcade pauses your game while a panel is up and\nresumes it when the promise settles, so you do not pause anything yourself.\n\n⚠️ `shown` **grants nothing.** It is not a receipt and not an entitlement,\nand a reward must never be handed out on the strength of it.\n\nYou are told no reason for a no, and there is nothing to infer from one\nabout the player. What you can read is your own economy:\n`rewardedStatus(placement)` says whether a rewarded placement could pay\nright now under your own declared caps — never whether an ad will play.",
9967
10143
  "deprecated": null,
9968
10144
  "params": [],
9969
10145
  "returns": null,
@@ -9972,7 +10148,7 @@
9972
10148
  "defaultValue": null,
9973
10149
  "guides": [
9974
10150
  {
9975
- "slug": "cross-promotion",
10151
+ "slug": "rewarded-ads",
9976
10152
  "anchor": null
9977
10153
  }
9978
10154
  ]
@@ -10412,7 +10588,7 @@
10412
10588
  "group": "start",
10413
10589
  "signature": "cueReward(cue: RewardCue): boolean",
10414
10590
  "documented": true,
10415
- "summary": "Tell the arcade a platform-currency reward is on its way, so it **shows it\nlanding**: in the Gem Arcade the currency flies from `origin` into the Gem\nbutton at the top-left of your frame, which pops as each mark lands, and\nthe player's total moves once the platform confirms the grant. The Gem\nArcade draws no wallet over your game: the player sees their Gems and\nTickets in the Gem menu. A payout you do not cue still flies, from the\nmiddle of your frame, so cue the ones the player earns on screen.\n\nCall it the moment your game decides to award — when your server has moved\n(or is about to move) the progression mapped onto a platform currency —\nand pass the centre of that currency's icon (or hero) on your screen, so\nthe flight starts from the mark the player is looking at:\n\n```js\nconst { x, y, width, height } = rewardGem.getBoundingClientRect(); // the gem's own box\ngem.cueReward({\n currencySlug: 'GEMS', // or 'ARCADE_TICKETS', from the ticket\n expected: 15,\n origin: { x: x + width / 2, y: y + height / 2 },\n});\n```\n\n**This is the platform-currency animation, and the only one.** The display\nof every platform currency — Gems and Tickets alike — belongs to the\narcade: a game draws no flight or celebration of its own for one and keeps\nno counter of one. **If this returns `false`, do nothing** — there is no\nfallback celebration.\n\n**The arcade never takes your word for the amount.** What lands, and the\nplayer's new total, come from the platform itself; `expected` is drawn as\nprovisional, and a cue the platform never confirms fades on its own — a\ngrant clamped to zero, say, or one the receipt reports `suppressed`. So\nthe cue can only ever make the wallet move for a reward that really\nlanded — which it does on every channel, dev and test included.",
10591
+ "summary": "Tell the arcade a platform-currency reward is on its way, so it **shows it\nlanding**: in the Gem Arcade the currency flies from `origin` into the Gem\nbutton, wherever on your frame it sits, which pops as each mark lands, and\nthe player's total moves once the platform confirms the grant. The Gem\nArcade draws no wallet over your game: the player sees their Gems and\nTickets in the Gem menu. A payout you do not cue still flies, from the\nmiddle of your frame, so cue the ones the player earns on screen.\n\nCall it the moment your game decides to award — when your server has moved\n(or is about to move) the progression mapped onto a platform currency —\nand pass the centre of that currency's icon (or hero) on your screen, so\nthe flight starts from the mark the player is looking at:\n\n```js\nconst { x, y, width, height } = rewardGem.getBoundingClientRect(); // the gem's own box\ngem.cueReward({\n currencySlug: 'GEMS', // or 'ARCADE_TICKETS', from the ticket\n expected: 15,\n origin: { x: x + width / 2, y: y + height / 2 },\n});\n```\n\n**This is the platform-currency animation, and the only one.** The display\nof every platform currency — Gems and Tickets alike — belongs to the\narcade: a game draws no flight or celebration of its own for one and keeps\nno counter of one. **If this returns `false`, do nothing** — there is no\nfallback celebration.\n\n**The arcade never takes your word for the amount.** What lands, and the\nplayer's new total, come from the platform itself; `expected` is drawn as\nprovisional, and a cue the platform never confirms fades on its own — a\ngrant clamped to zero, say, or one the receipt reports `suppressed`. So\nthe cue can only ever make the wallet move for a reward that really\nlanded — which it does on every channel, dev and test included.",
10416
10592
  "deprecated": null,
10417
10593
  "params": [
10418
10594
  {
@@ -10464,7 +10640,7 @@
10464
10640
  "group": "start",
10465
10641
  "signature": "setMenu(menu: GameMenu): boolean",
10466
10642
  "documented": true,
10467
- "summary": "Put up to four entries of your own in the arcade's menu. The arcade keeps\none menu button over your game: it is how a player leaves, mutes, goes\nfullscreen, reports a problem and changes their Arcade-wide settings.\n\n```js\ngem.setMenu({\n items: [{ id: 'how-to-play', label: t('How to play'), onSelect: () => help.open() }],\n});\n```\n\n**Your settings are not in it: draw your own settings button, always** —\ntop-right, clear of `gem.viewport.reserved`. The Arcade's menu never opens\nyour settings screen.\n\n**It is screen state, not a toggle.** Each call is the whole answer and\nreplaces the last, so call it again when the entries change — a new\nlanguage, a screen with different options. `gem.setMenu({})` takes\neverything of yours back out. The arcade's own entries are not yours to\nchange, and Return to the Arcade is always among them.\n\n**The arcade pauses your game while its menu is open** (`onPause`), and\nresumes it before a handler of yours runs. A handler opens a screen; it\nshould not assume the game was paused by you.",
10643
+ "summary": "Put up to four entries of your own in the arcade's menu. The arcade keeps\none menu button over your game: it is how a player leaves, mutes, goes\nfullscreen, reports a problem and changes their Arcade-wide settings.\n\n```js\ngem.setMenu({\n items: [{ id: 'how-to-play', label: t('How to play'), onSelect: () => help.open() }],\n});\n```\n\n**Your settings are not in it: draw your own settings button, always** —\ntop-right, with your Back button just to its left, clear of\n`gem.viewport.reserved`. The Arcade's menu never opens your settings\nscreen.\n\n**It is screen state, not a toggle.** Each call is the whole answer and\nreplaces the last, so call it again when the entries change — a new\nlanguage, a screen with different options. `gem.setMenu({})` takes\neverything of yours back out. The arcade's own entries are not yours to\nchange, and Return to the Arcade is always among them.\n\n**The arcade pauses your game while its menu is open** (`onPause`), and\nresumes it before a handler of yours runs. A handler opens a screen; it\nshould not assume the game was paused by you.",
10468
10644
  "deprecated": null,
10469
10645
  "params": [
10470
10646
  {
@@ -10480,6 +10656,29 @@
10480
10656
  "defaultValue": null,
10481
10657
  "guides": []
10482
10658
  },
10659
+ {
10660
+ "id": "Gem.setGemCorner",
10661
+ "name": "setGemCorner",
10662
+ "anchor": "gem-setgemcorner",
10663
+ "group": "start",
10664
+ "signature": "setGemCorner(corner: GemCorner): boolean",
10665
+ "documented": true,
10666
+ "summary": "Ask the arcade to put its Gem button — its one control over your game,\ntop-left unless you ask — in another corner of your frame.\n\n```js\n// This game's own HUD needs the top-left, so the Gem goes top-right.\ngem.setGemCorner('top-right');\n```\n\n**Leave it top-left unless your own HUD needs that corner**, and then pick\none corner for the whole game — at most one per orientation. A Gem that\nmoves between screens is a way out the player has to look for. In\nlandscape the bottom corners sit where a player's thumbs hold the sticks.\n\n**Lay out around `gem.viewport.reserved`, never around the corner you\nasked for.** The arcade moves the Gem on its own terms: not while the\nplayer is using it or its menu is open, not more often than every few\nseconds, and never over a corner the player chose for themselves. The\nrectangles say where it actually is, and the `--gem-reserved-*` CSS\nproperties follow them.\n\n**It is screen state, not a toggle.** Each call is the whole answer and\nreplaces the last. A value that is not one of the four corners is ignored,\nwith a warning on the console, and changes nothing.",
10667
+ "deprecated": null,
10668
+ "params": [
10669
+ {
10670
+ "name": "corner",
10671
+ "type": "GemCorner",
10672
+ "optional": false,
10673
+ "summary": "`'top-left'`, `'top-right'`, `'bottom-left'` or `'bottom-right'`."
10674
+ }
10675
+ ],
10676
+ "returns": "`true` when the arcade was told now; `false` before the bridge is\n up (the corner is held and sent when it connects), under an embedder that\n cannot move its control, and for a value that is not a corner.",
10677
+ "throws": null,
10678
+ "example": null,
10679
+ "defaultValue": null,
10680
+ "guides": []
10681
+ },
10483
10682
  {
10484
10683
  "id": "Gem.exit",
10485
10684
  "name": "exit",
@@ -11364,7 +11563,7 @@
11364
11563
  "name": "protocol",
11365
11564
  "anchor": "matchconnection-protocol",
11366
11565
  "group": "match",
11367
- "signature": "readonly protocol: \"dedicated\" | \"p2p\" | \"loopback\"",
11566
+ "signature": "readonly protocol: \"p2p\" | \"dedicated\" | \"loopback\"",
11368
11567
  "documented": true,
11369
11568
  "summary": "What is on the other end: `dedicated` (the platform's server), `p2p` (the\nhosting player's browser), or `loopback` (this browser — you are the host,\nand this is the in-process link to your own bundle).",
11370
11569
  "deprecated": null,
@@ -11619,9 +11818,9 @@
11619
11818
  "name": "Avatar3dFamily",
11620
11819
  "anchor": "avatar3dfamily",
11621
11820
  "group": "avatars3d",
11622
- "signature": "type Avatar3dFamily = \"small_human\" | string & object",
11821
+ "signature": "type Avatar3dFamily = \"small_human\" | \"adult_human\" | string & object",
11623
11822
  "documented": true,
11624
- "summary": "A rig family — the skeleton contract a 3D avatar is built on, and what a\ngame declares it can animate. `small_human` is the first: a `root` node\nwith a `body` child, which holds `head`, `arm_l`, `arm_r`, `leg_l` and\n`leg_r`, and a `hand_socket` under `arm_r`.\n\nOpen-ended (`| (string & {})`) so a family the platform adds later does not\nbreak a `switch` you compiled against this version: keep a `default` arm and\nskip a family you do not draw.",
11823
+ "summary": "A rig family — the skeleton contract a 3D avatar is built on, and what a\ngame declares it can animate. Two today:\n\n- `small_human`, the toy rig: a `root` node with a `body` child, which holds\n `head`, `arm_l`, `arm_r`, `leg_l` and `leg_r`, and a `hand_socket` under\n `arm_r`.\n- `adult_human`, a full humanoid with fingers: 56 nodes from `root` and\n `hips` down to `toes_r`, every one world-aligned at rest, so one clip plays\n on every avatar in the family.\n\n`AVATAR_RIGS` (from `@indiegems/gem-web-sdk/sdk`) lists every node of\nboth, with its parent.\n\nOpen-ended (`| (string & {})`) so a family the platform adds later does not\nbreak a `switch` you compiled against this version: keep a `default` arm and\nskip a family you do not draw.",
11625
11824
  "deprecated": null,
11626
11825
  "params": [],
11627
11826
  "returns": null,
@@ -11948,6 +12147,151 @@
11948
12147
  "defaultValue": null,
11949
12148
  "guides": []
11950
12149
  },
12150
+ {
12151
+ "id": "MomentKind",
12152
+ "name": "MomentKind",
12153
+ "anchor": "momentkind",
12154
+ "group": "moments",
12155
+ "signature": "type MomentKind = \"level_end\" | \"run_end\" | \"match_end\" | \"menu_return\" | \"other_break\"",
12156
+ "documented": true,
12157
+ "summary": "Which kind of break a platform moment is. A closed set — there will not be a\nsixth:\n\n- `level_end` — a level, stage, wave or puzzle finished, won or lost.\n- `run_end` — game over, death, or a run banked, in an endless, arcade or\n roguelike game.\n- `match_end` — a multiplayer round or match finished and the player is\n leaving its results.\n- `menu_return` — the player chose to go back to the title, a main menu or\n level select. A return, never the first arrival.\n- `other_break` — any other natural stop: a story chapter, a between-waves\n shop.\n\nIt is a label, for the arcade's analytics, the dev host's log and the release\nchecklist. It never changes what the arcade decides for your game.",
12158
+ "deprecated": null,
12159
+ "params": [],
12160
+ "returns": null,
12161
+ "throws": null,
12162
+ "example": null,
12163
+ "defaultValue": null,
12164
+ "guides": [
12165
+ {
12166
+ "slug": "platform-moments",
12167
+ "anchor": null
12168
+ }
12169
+ ]
12170
+ },
12171
+ {
12172
+ "id": "MomentSignal",
12173
+ "name": "MomentSignal",
12174
+ "anchor": "momentsignal",
12175
+ "group": "moments",
12176
+ "signature": "interface MomentSignal",
12177
+ "documented": true,
12178
+ "summary": "A platform moment: which kind of break the player is leaving, and optionally\nyour own name for the screen.",
12179
+ "deprecated": null,
12180
+ "params": [],
12181
+ "returns": null,
12182
+ "throws": null,
12183
+ "example": null,
12184
+ "defaultValue": null,
12185
+ "guides": []
12186
+ },
12187
+ {
12188
+ "id": "MomentSignal.kind",
12189
+ "name": "kind",
12190
+ "anchor": "momentsignal-kind",
12191
+ "group": "moments",
12192
+ "signature": "kind: MomentKind",
12193
+ "documented": true,
12194
+ "summary": "Which kind of break this is. Required.",
12195
+ "deprecated": null,
12196
+ "params": [],
12197
+ "returns": null,
12198
+ "throws": null,
12199
+ "example": null,
12200
+ "defaultValue": null,
12201
+ "guides": []
12202
+ },
12203
+ {
12204
+ "id": "MomentSignal.placement",
12205
+ "name": "placement",
12206
+ "anchor": "momentsignal-placement",
12207
+ "group": "moments",
12208
+ "signature": "placement?: string",
12209
+ "documented": true,
12210
+ "summary": "Optional: your own name for the screen the player is leaving —\n`world-1-3`, `results`. **1 to 32 characters of `a-z`, `0-9`, `-` and\n`_`.** It appears only in the dev host's log, so a reviewer can map each\nmoment to a screen. It is never sent to the platform, never stored, never\nshown to a player and never sent to an ad network.",
12211
+ "deprecated": null,
12212
+ "params": [],
12213
+ "returns": null,
12214
+ "throws": null,
12215
+ "example": null,
12216
+ "defaultValue": null,
12217
+ "guides": []
12218
+ },
12219
+ {
12220
+ "id": "MomentResult",
12221
+ "name": "MomentResult",
12222
+ "anchor": "momentresult",
12223
+ "group": "moments",
12224
+ "signature": "interface MomentResult",
12225
+ "documented": true,
12226
+ "summary": "What the arcade answers at a platform moment.",
12227
+ "deprecated": null,
12228
+ "params": [],
12229
+ "returns": null,
12230
+ "throws": null,
12231
+ "example": null,
12232
+ "defaultValue": null,
12233
+ "guides": []
12234
+ },
12235
+ {
12236
+ "id": "MomentResult.shown",
12237
+ "name": "shown",
12238
+ "anchor": "momentresult-shown",
12239
+ "group": "moments",
12240
+ "signature": "readonly shown: boolean",
12241
+ "documented": true,
12242
+ "summary": "Whether the arcade showed anything of its own at this break. Usually\n`false`. Either way, carry on exactly as you would have. **It grants\nnothing**, and it never says what was shown or why nothing was.",
12243
+ "deprecated": null,
12244
+ "params": [],
12245
+ "returns": null,
12246
+ "throws": null,
12247
+ "example": null,
12248
+ "defaultValue": null,
12249
+ "guides": []
12250
+ },
12251
+ {
12252
+ "id": "MomentsApi",
12253
+ "name": "MomentsApi",
12254
+ "anchor": "momentsapi",
12255
+ "group": "moments",
12256
+ "signature": "interface MomentsApi",
12257
+ "documented": true,
12258
+ "summary": "Platform moments. `gem.moments` is one of these.",
12259
+ "deprecated": null,
12260
+ "params": [],
12261
+ "returns": null,
12262
+ "throws": null,
12263
+ "example": null,
12264
+ "defaultValue": null,
12265
+ "guides": []
12266
+ },
12267
+ {
12268
+ "id": "MomentsApi.signal",
12269
+ "name": "signal",
12270
+ "anchor": "momentsapi-signal",
12271
+ "group": "moments",
12272
+ "signature": "signal(moment: MomentSignal): Promise<MomentResult>",
12273
+ "documented": true,
12274
+ "summary": "Signal a platform moment: the player is leaving a natural break in play.\nThe arcade may show one of its own offers over your paused game, and\nusually shows nothing.\n\n**Call it from the player's press that LEAVES the break** — Next, Retry,\nBack to menu — once per break. Start your own transition in parallel, and\nstart play when this settles. **Never** mid-input, during a timed\nsequence, on a timer, on boot or the first arrival at the title, or from a\nplayer-opened pause menu (choosing Quit to menu from it is a\n`menu_return`). No press is required by the SDK; the rule is about when a\nbreak really is one.\n\nNOTHING REJECTS. It resolves `{ shown: false }` without asking for an\narcade that does not do platform moments, a `kind` or `placement` outside\nthe grammar, a call before `gem.frame.ready()`, a call while another is in\nflight, a call within 10 seconds of the last answer, or no answer within\nthree minutes — and each of those, except an arcade that does not do it\n(said once a session), is reported to your `onError` handler with code\n`'internal'` and a message beginning with a stable token:\n`moments_unsupported`, `moments_invalid_argument`, `moments_before_ready`,\n`moments_in_flight`, `moments_called_repeatedly` or `moments_timeout`.\n\nThe arcade pauses and resumes your game through the ordinary\n`gem.pause`, so you do not pause anything yourself.\n\n⚠️ `shown` grants nothing. Never hand out a reward, an unlock or a currency\non it, and never pop your own offer at a break that settled `shown: true`.",
12275
+ "deprecated": null,
12276
+ "params": [
12277
+ {
12278
+ "name": "moment",
12279
+ "type": "MomentSignal",
12280
+ "optional": false,
12281
+ "summary": "Which kind of break, and optionally your name for the screen."
12282
+ }
12283
+ ],
12284
+ "returns": "Whether the arcade showed anything. Always resolves; never rejects.",
12285
+ "throws": null,
12286
+ "example": "```ts\n// From the press that LEAVES the break — Next, Retry, Back to menu.\nasync function onNextPressed() {\n startLevelTransition(); // your fade or loading beat, in parallel\n await gem.moments.signal({ kind: 'level_end', placement: 'world-1-3' });\n startNextLevel(); // settled either way; never grant anything on `shown`\n}\n```",
12287
+ "defaultValue": null,
12288
+ "guides": [
12289
+ {
12290
+ "slug": "platform-moments",
12291
+ "anchor": null
12292
+ }
12293
+ ]
12294
+ },
11951
12295
  {
11952
12296
  "id": "DedicatedServerFailure",
11953
12297
  "name": "DedicatedServerFailure",
@@ -21628,6 +21972,22 @@
21628
21972
  "defaultValue": null,
21629
21973
  "guides": []
21630
21974
  },
21975
+ {
21976
+ "id": "RequestOptions.retryRateLimited",
21977
+ "name": "retryRateLimited",
21978
+ "anchor": "requestoptions-retryratelimited",
21979
+ "group": "calls",
21980
+ "signature": "readonly retryRateLimited?: boolean",
21981
+ "documented": true,
21982
+ "summary": "Set `false` to make a 429 final for this call instead of waiting out\n`Retry-After` and trying again.\n\nFor a caller that keeps its own queue and its own cooldown, such as a\nbatch sender on a timer: retrying inside the call only spends more of the\nsame limit, and holds the caller's queue while it does. Every other\nfailure is retried exactly as before. Defaults to `true`.",
21983
+ "deprecated": null,
21984
+ "params": [],
21985
+ "returns": null,
21986
+ "throws": null,
21987
+ "example": null,
21988
+ "defaultValue": null,
21989
+ "guides": []
21990
+ },
21631
21991
  {
21632
21992
  "id": "VersionedGetOptions",
21633
21993
  "name": "VersionedGetOptions",
@@ -23810,7 +24170,7 @@
23810
24170
  "group": "analytics",
23811
24171
  "signature": "flush(signal?: AbortSignal): Promise<AnalyticsFlushResult>",
23812
24172
  "documented": true,
23813
- "summary": "Send what is queued now, and find out what happened to it.\n\nResolves with the platform's own answer, INCLUDING per-event rejections, which\narrive inside a 200 rather than as an error. A refusal that could clear on\nits own — a 429, a 503 — keeps the queue and rejects; one that cannot\ndiscards the batch, because resending it can only fail the same way.",
24173
+ "summary": "Send what is queued now, and find out what happened to it.\n\nResolves with the platform's own answer, INCLUDING per-event rejections, which\narrive inside a 200 rather than as an error. A refusal that could clear on\nits own — a 429, a 503 — keeps the queue and rejects, and the SDK's own\nflushes then hold off for the longer of the server's `Retry-After` and a\ncooldown that doubles from 10 seconds, up to five minutes; one that cannot\ndiscards the batch, because resending it can only fail the same way.\n\nOne flush at a time: called while another is on the wire, this waits for\nit and then sends what is left.",
23814
24174
  "deprecated": null,
23815
24175
  "params": [
23816
24176
  {
@@ -26365,7 +26725,7 @@
26365
26725
  "usageRoots": [
26366
26726
  "GemFunction"
26367
26727
  ],
26368
- "install": "A function imports the same package your game already depends on, written out in [getting-started](../getting-started.md#3-add-the-sdk) — the only place the install is written out. Your function’s build bundles this module, so the version your function runs is the one your `package.json` names: pin the version your game pins. A function published importing the SDK from the engine runs the engine’s built-in copy instead; [serverless-functions-js](../serverless-functions-js.md) says what that means.",
26728
+ "install": "⚠️ **JavaScript functions are disabled for now, due to performance issues.** Uploading a new JavaScript function is refused with `400 js_functions_disabled`: write a new function in Rust, as [serverless-functions](../serverless-functions.md#3-writing-one) shows. A JavaScript function already published keeps running, and this page is what it imports.\n\nA function imports the same package your game already depends on, written out in [getting-started](../getting-started.md#3-add-the-sdk) — the only place the install is written out. Your function’s build bundles this module, so the version your function runs is the one your `package.json` names: pin the version your game pins. A function published importing the SDK from the engine runs the engine’s built-in copy instead; [serverless-functions-js](../serverless-functions-js.md) says what that means.",
26369
26729
  "entrySymbol": "createGem",
26370
26730
  "groups": [
26371
26731
  {
@@ -26685,7 +27045,7 @@
26685
27045
  "group": "analytics",
26686
27046
  "signature": "class AnalyticsApi",
26687
27047
  "documented": true,
26688
- "summary": "Your game's own analytics — the events you declared in your game config,\nplus the `gem_*` session and match events the SDK emits for you.\n\nReached as `gem.analytics`. [AnalyticsApi.track](api-reference-function.md#analyticsapi-track) is synchronous, never\nthrows and never rejects: it puts an event on an in-memory queue, and the\nSDK posts that queue on its own timer, when it reaches 50 events, and on the\nway out of the page.\n\n```js\ngem.analytics.setContext({ build: 'v1.4.2' });\n\ngem.analytics.track('level_completed', {\n outcome: 'win', // a dimension — a declared, closed set\n difficulty: 'normal', // a dimension\n duration_ms: 48_120, // a measure — summed, averaged, min and max\n score: 3_100, // a measure\n});\n```\n\n⚠️ **Every key has to be declared.** `analytics_events` in your game config\nis what says which events exist, which dimensions each has and which values\neach dimension may take. An undeclared name or key is not stored — it comes\nback as a rejection, which the SDK prints to the console during development.\n\n⚠️ **Detail older than 30 days is gone.** Raw events live 30 days; the daily\ncounters live 400. Anything you want to be able to ask about a year from now\nhas to be a declared dimension today.\n\n⚠️ **These are your numbers, not the platform's.** Client events are\ngame-asserted browser facts. They feed your analytics and nothing the\nplatform treats as authoritative — not rewards, not dailies, not store\nranking.",
27048
+ "summary": "Your game's own analytics — the events you declared in your game config,\nplus the `gem_*` session and match events the SDK emits for you.\n\nReached as `gem.analytics`. [AnalyticsApi.track](api-reference-function.md#analyticsapi-track) is synchronous, never\nthrows and never rejects: it puts an event on an in-memory queue, and the\nSDK posts that queue on its own timer, when it reaches 50 events, and on the\nway out of the page.\n\n```js\ngem.analytics.setContext({ build: 'v1.4.2' });\n\ngem.analytics.track('level_completed', {\n outcome: 'win', // a dimension — a declared, closed set\n difficulty: 'normal', // a dimension\n duration_ms: 48_120, // a measure — summed, averaged, min and max\n score: 3_100, // a measure\n});\n```\n\n⚠️ **Every key has to be declared.** `analytics_events` in your game config\nis what says which events exist, which dimensions each has and which values\neach dimension may take. An undeclared name or key is not stored — it comes\nback as a rejection, which the SDK prints to the console during development.\n\n⚠️ **Detail older than 60 days is gone.** Raw events live 60 days; the daily\ncounters live 400. Anything you want to be able to ask about a year from now\nhas to be a declared dimension today.\n\n⚠️ **These are your numbers, not the platform's.** Client events are\ngame-asserted browser facts. They feed your analytics and nothing the\nplatform treats as authoritative — not rewards, not dailies, not store\nranking.",
26689
27049
  "deprecated": null,
26690
27050
  "params": [],
26691
27051
  "returns": null,
@@ -26774,7 +27134,7 @@
26774
27134
  "group": "analytics",
26775
27135
  "signature": "flush(options?: AnalyticsFlushOptions): Promise<AnalyticsFlushResult>",
26776
27136
  "documented": true,
26777
- "summary": "Send what is queued now, and find out what happened to it.\n\nResolves with the platform's own answer, INCLUDING per-event rejections, which\narrive inside a 200 — see [AnalyticsFlushResult.rejected](api-reference-function.md#analyticsflushresult-rejected). You do\nnot have to call this: the SDK flushes on its own, and on the way out of\nthe page.\n\nA refusal that could clear on its own — a 429 from the rate limiter, a 503\nwhile the platform's idempotency store is unreachable — KEEPS the queue\nand rejects, so the next flush sends the same events rather than losing\nthem. A refusal that cannot clear discards the batch it was sent for,\nbecause resending it can only fail identically.",
27137
+ "summary": "Send what is queued now, and find out what happened to it.\n\nResolves with the platform's own answer, INCLUDING per-event rejections, which\narrive inside a 200 — see [AnalyticsFlushResult.rejected](api-reference-function.md#analyticsflushresult-rejected). You do\nnot have to call this: the SDK flushes on its own, and on the way out of\nthe page.\n\nA refusal that could clear on its own — a 429 from the rate limiter, a 503\nwhile the platform's idempotency store is unreachable — KEEPS the queue\nand rejects, so the next flush sends the same events rather than losing\nthem. The SDK's own flushes then hold off for the longer of the server's\n`Retry-After` and a cooldown that doubles from 10 seconds, up to five\nminutes. A refusal that cannot clear discards the batch it was sent for,\nbecause resending it can only fail identically.\n\nOne flush at a time: called while another is still on the wire, this\nresolves at once with nothing sent, and what is queued goes out after it.",
26778
27138
  "deprecated": null,
26779
27139
  "params": [
26780
27140
  {
@@ -27078,6 +27438,22 @@
27078
27438
  "defaultValue": null,
27079
27439
  "guides": []
27080
27440
  },
27441
+ {
27442
+ "id": "RequestOptions.retryRateLimited",
27443
+ "name": "retryRateLimited",
27444
+ "anchor": "requestoptions-retryratelimited",
27445
+ "group": "calls",
27446
+ "signature": "readonly retryRateLimited?: boolean",
27447
+ "documented": true,
27448
+ "summary": "Set `false` to make a 429 final for this call instead of waiting out\n`Retry-After` and trying again.\n\nFor a caller that keeps its own queue and its own cooldown, such as a\nbatch sender on a timer: retrying inside the call only spends more of the\nsame limit, and holds the caller's queue while it does. Every other\nfailure is retried exactly as before. Defaults to `true`.",
27449
+ "deprecated": null,
27450
+ "params": [],
27451
+ "returns": null,
27452
+ "throws": null,
27453
+ "example": null,
27454
+ "defaultValue": null,
27455
+ "guides": []
27456
+ },
27081
27457
  {
27082
27458
  "id": "VersionedGetOptions",
27083
27459
  "name": "VersionedGetOptions",
@@ -32069,9 +32445,9 @@
32069
32445
  "name": "Avatar3dFamily",
32070
32446
  "anchor": "avatar3dfamily",
32071
32447
  "group": "identity",
32072
- "signature": "type Avatar3dFamily = \"small_human\" | string & object",
32448
+ "signature": "type Avatar3dFamily = \"small_human\" | \"adult_human\" | string & object",
32073
32449
  "documented": true,
32074
- "summary": "A rig family — the skeleton contract a 3D avatar is built on, and what a\ngame declares it can animate. `small_human` is the first: a `root` node\nwith a `body` child, which holds `head`, `arm_l`, `arm_r`, `leg_l` and\n`leg_r`, and a `hand_socket` under `arm_r`.\n\nOpen-ended (`| (string & {})`) so a family the platform adds later does not\nbreak a `switch` you compiled against this version: keep a `default` arm and\nskip a family you do not draw.",
32450
+ "summary": "A rig family — the skeleton contract a 3D avatar is built on, and what a\ngame declares it can animate. Two today:\n\n- `small_human`, the toy rig: a `root` node with a `body` child, which holds\n `head`, `arm_l`, `arm_r`, `leg_l` and `leg_r`, and a `hand_socket` under\n `arm_r`.\n- `adult_human`, a full humanoid with fingers: 56 nodes from `root` and\n `hips` down to `toes_r`, every one world-aligned at rest, so one clip plays\n on every avatar in the family.\n\n`AVATAR_RIGS` (from `@indiegems/gem-web-sdk/sdk`) lists every node of\nboth, with its parent.\n\nOpen-ended (`| (string & {})`) so a family the platform adds later does not\nbreak a `switch` you compiled against this version: keep a `default` arm and\nskip a family you do not draw.",
32075
32451
  "deprecated": null,
32076
32452
  "params": [],
32077
32453
  "returns": null,