@indiegems/gem-web-sdk 20.1.0 → 21.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +1 -0
  3. package/brand/ticket-hero-128.png +0 -0
  4. package/brand/ticket-hero-256.png +0 -0
  5. package/brand/ticket-hero-384.png +0 -0
  6. package/brand/ticket-hero-512.png +0 -0
  7. package/brand/ticket-hero-768.png +0 -0
  8. package/brand/ticket-icon-20-1.5x.png +0 -0
  9. package/brand/ticket-icon-20-1x.png +0 -0
  10. package/brand/ticket-icon-20-2x.png +0 -0
  11. package/brand/ticket-icon-20-3x.png +0 -0
  12. package/brand/ticket-icon-32-1.5x.png +0 -0
  13. package/brand/ticket-icon-32-1x.png +0 -0
  14. package/brand/ticket-icon-32-2x.png +0 -0
  15. package/brand/ticket-icon-32-3x.png +0 -0
  16. package/brand/ticket-icon-ingame-20-1.5x.png +0 -0
  17. package/brand/ticket-icon-ingame-20-1x.png +0 -0
  18. package/brand/ticket-icon-ingame-20-2x.png +0 -0
  19. package/brand/ticket-icon-ingame-20-3x.png +0 -0
  20. package/brand/ticket-icon-ingame-32-1.5x.png +0 -0
  21. package/brand/ticket-icon-ingame-32-1x.png +0 -0
  22. package/brand/ticket-icon-ingame-32-2x.png +0 -0
  23. package/brand/ticket-icon-ingame-32-3x.png +0 -0
  24. package/brand/ticket-pile-1-128.png +0 -0
  25. package/brand/ticket-pile-1-256.png +0 -0
  26. package/brand/ticket-pile-1-384.png +0 -0
  27. package/brand/ticket-pile-1-512.png +0 -0
  28. package/brand/ticket-pile-1-768.png +0 -0
  29. package/brand/ticket-pile-2-128.png +0 -0
  30. package/brand/ticket-pile-2-256.png +0 -0
  31. package/brand/ticket-pile-2-384.png +0 -0
  32. package/brand/ticket-pile-2-512.png +0 -0
  33. package/brand/ticket-pile-2-768.png +0 -0
  34. package/dist/conformance.d.ts +1 -1
  35. package/dist/dev-tools.d.ts +1 -0
  36. package/dist/function.d.ts +11 -3
  37. package/dist/game.d.ts +79 -12
  38. package/dist/gem-web-sdk.bridge.js +2 -2
  39. package/dist/gem-web-sdk.conformance.js +3 -3
  40. package/dist/gem-web-sdk.dev-tools.js +2 -0
  41. package/dist/gem-web-sdk.esm.js +3 -3
  42. package/dist/gem-web-sdk.function.js +3 -3
  43. package/dist/gem-web-sdk.game.js +3 -3
  44. package/dist/gem-web-sdk.min.js +3 -3
  45. package/dist/gem-web-sdk.sdk.js +3 -3
  46. package/dist/gem-web-sdk.server.js +2 -2
  47. package/dist/index.d.ts +103 -48
  48. package/dist/sdk.d.ts +79 -12
  49. package/docs/analytics.md +12 -0
  50. package/docs/api/api-reference-function.md +14 -2
  51. package/docs/api/api-reference-game.md +357 -21
  52. package/docs/api/api-reference-server.md +1 -1
  53. package/docs/api/reference.json +413 -16
  54. package/docs/arcade-economy.md +2 -2
  55. package/docs/brand-art.md +65 -16
  56. package/docs/changelog.md +40 -0
  57. package/docs/game-economy-tutorial.md +6 -3
  58. package/docs/getting-started.md +122 -30
  59. package/docs/graphics-route.md +188 -0
  60. package/docs/inventory-and-catalogs.md +69 -27
  61. package/docs/manifest.json +23 -18
  62. package/docs/server-bundles.md +3 -3
  63. package/docs/what-a-game-must-do.md +33 -2
  64. package/package.json +5 -1
  65. package/schema/bridge.v1.json +263 -12
  66. package/schema/capabilities.public.json +26 -0
@@ -2,7 +2,7 @@
2
2
  "schema_version": 2,
3
3
  "source": {
4
4
  "repo": "IndieGems/gem-web-sdk",
5
- "version": "20.1.0"
5
+ "version": "21.1.0"
6
6
  },
7
7
  "entries": [
8
8
  {
@@ -85,6 +85,10 @@
85
85
  "key": "vibration",
86
86
  "title": "Vibrating the device"
87
87
  },
88
+ {
89
+ "key": "graphics",
90
+ "title": "WebGL or WebGPU"
91
+ },
88
92
  {
89
93
  "key": "controls",
90
94
  "title": "Camera and look controls"
@@ -2564,7 +2568,7 @@
2564
2568
  "group": "saves",
2565
2569
  "signature": "purchase(request: ConfiguredPurchaseRequest, options?: PurchaseOptions): Promise<ConfiguredPurchaseResult>",
2566
2570
  "documented": true,
2567
- "summary": "Purchase an item or recipe through the game's authenticated SDK transport.\n\nOne call covers both direct payment types:\n\n- `payment: { kind: 'configured' }` runs a recipe and charges the costs in\n your game config.\n- `payment: { kind: 'platform_currency', currencySlug: 'GEMS' }` resolves\n the target's published platform price and spends the player's platform\n balance directly. It does not send a bridge message or open the platform\n purchase sheet; your game owns any confirmation UI.\n\nThe game supplies no amount, config version or nonce. The SDK resolves the\ncurrent published price and config version, mints the nonce, and the\nplatform checks all of them again before it debits anything.\n\nA REFUSAL CARRIES A `code` — branch on `err.code`, in the vocabulary\n`gem.purchases.request` answers in (`PurchaseRefusalCode`). Before\nsending a `platform_currency` payment the SDK itself throws a\n`PurchaseRefusal` (a `RangeError`) for `unknown_currency`,\n`not_purchasable`, `no_price_in_currency` or `quantity_invalid`, with a\nmessage naming the active slugs or what the target is priced in. The\nplatform's own refusals arrive as a `GemApiError` whose `code` is its wire\ncode — `insufficient_funds`, `recipe_locked`, `price_mismatch`, … Never\ndraw a top-up or payment screen of your own for either.",
2571
+ "summary": "Purchase an item or recipe through the game's authenticated SDK transport.\n\nOne call covers both direct payment types:\n\n- `payment: { kind: 'configured' }` runs a recipe and charges the costs in\n your game config.\n- `payment: { kind: 'platform_currency', currencySlug: 'GEMS' }` resolves\n the target's published platform price and spends the player's platform\n balance directly. It does not send a bridge message or open the platform\n purchase sheet; your game owns any confirmation UI.\n\nThe game supplies no amount, config version or nonce. The SDK resolves the\ncurrent published price and config version, mints the nonce, and the\nplatform checks all of them again before it debits anything.\n\nA REFUSAL CARRIES A `code` — branch on `err.code`, in the vocabulary\n`gem.purchases.request` answers in (`PurchaseRefusalCode`). Before\nsending a `platform_currency` payment the SDK itself throws a\n`PurchaseRefusal` (a `RangeError`) for `unknown_currency`,\n`not_purchasable`, `no_price_in_currency` or `quantity_invalid`, with a\nmessage naming the active slugs or what the target is priced in. The\nplatform's own refusals arrive as a `GemApiError` whose `code` is its wire\ncode — `insufficient_funds`, `recipe_locked`, `price_mismatch`, … Never\ndraw a top-up or payment screen of your own for either.\n\n`guest_upgrade_required` means the player is a guest, and the platform\nlets only an account spend. TREAT IT LIKE A DECLINE AND DRAW NOTHING: the\nSDK has already handed the attempt to the arcade, which offers the player\nan account over your game. The call rejects at once rather than waiting\nfor them; if they make one and buy, [PurchaseApi.onSettled](api-reference-game.md#purchaseapi-onsettled) hears it.",
2568
2572
  "deprecated": null,
2569
2573
  "params": [
2570
2574
  {
@@ -4733,9 +4737,9 @@
4733
4737
  "name": "PlatformEventName",
4734
4738
  "anchor": "platformeventname",
4735
4739
  "group": "analytics",
4736
- "signature": "type PlatformEventName = \"gem_session_start\" | \"gem_session_end\" | \"gem_pause\" | \"gem_resume\" | \"gem_boot_ready\" | \"gem_match_started\" | \"gem_match_ended\" | \"gem_auth_lost\"",
4740
+ "signature": "type PlatformEventName = \"gem_session_start\" | \"gem_session_end\" | \"gem_pause\" | \"gem_resume\" | \"gem_boot_ready\" | \"gem_match_started\" | \"gem_match_ended\" | \"gem_auth_lost\" | \"gem_graphics_context_lost\"",
4737
4741
  "documented": true,
4738
- "summary": "Every event the SDK emits by itself.\n\nA game never sends these — `gem.analytics.track` refuses the whole\n`gem_` namespace — but the names are published because a game reading its\nown funnel needs the vocabulary, and because a funnel declared in the game\nconfig names them as steps.\n\n| Event | Dimensions | Measures |\n|---|---|---|\n| `gem_session_start` | — | — |\n| `gem_session_end` | `reason` ([SessionEndReason](api-reference-game.md#sessionendreason)) | `duration_ms` |\n| `gem_pause` / `gem_resume` | — | — |\n| `gem_boot_ready` | — | `boot_ms` |\n| `gem_match_started` / `gem_match_ended` | `transport` ([MatchTransport](api-reference-game.md#matchtransport)) | — |\n| `gem_auth_lost` | — | — |",
4742
+ "summary": "Every event the SDK emits by itself.\n\nA game never sends these — `gem.analytics.track` refuses the whole\n`gem_` namespace — but the names are published because a game reading its\nown funnel needs the vocabulary, and because a funnel declared in the game\nconfig names them as steps.\n\n| Event | Dimensions | Measures |\n|---|---|---|\n| `gem_session_start` | — | — |\n| `gem_session_end` | `reason` ([SessionEndReason](api-reference-game.md#sessionendreason)) | `duration_ms` |\n| `gem_pause` / `gem_resume` | — | — |\n| `gem_boot_ready` | — | `boot_ms` |\n| `gem_match_started` / `gem_match_ended` | `transport` ([MatchTransport](api-reference-game.md#matchtransport)) | — |\n| `gem_auth_lost` | — | — |\n| `gem_graphics_context_lost` | `gpu`, `host`, `route` | `context_ms`, `hidden`, `nth_loss` |\n\n`gem_graphics_context_lost` is the browser taking a WebGL context away, or a\nWebGPU device (`route` is then `webgpu`) — usually a GPU driver fault, sized\nper device across every game (`analytics/graphics.ts` lists the dimension\nvalues).",
4739
4743
  "deprecated": null,
4740
4744
  "params": [],
4741
4745
  "returns": null,
@@ -9088,9 +9092,9 @@
9088
9092
  "name": "requestShare",
9089
9093
  "anchor": "gemframe-requestshare",
9090
9094
  "group": "frame",
9091
- "signature": "requestShare(params: readonly { key: string; value: string }[]): boolean",
9095
+ "signature": "requestShare(params: readonly { key: string; value: string }[], requestId?: string): boolean",
9092
9096
  "documented": true,
9093
- "summary": "Ask the arcade to share a link to your game, opened with these launch\nparameters. The arcade builds the link and shows its own share sheet, copy\nor Share panel; nothing comes back, not even whether the player shared.\n\nPrefer `gem.share.request`, which checks each key and value against the\narcade's rules and throws on the one that would be refused. This one checks\nonly the message's bounds, sends only while connected, and only to an\narcade that handles share.",
9097
+ "summary": "Ask the arcade to share a link to your game, opened with these launch\nparameters. The arcade builds the link and shows its own share sheet, copy\nor Share panel; nothing about the player comes back, not even whether they\nshared.\n\nPrefer `gem.share.request`, which checks each key and value against the\narcade's rules, throws on the one that would be refused, and rejects with\nthe arcade's own refusal. This one checks only the message's bounds, sends\nonly while connected, and only to an arcade that handles share.",
9094
9098
  "deprecated": null,
9095
9099
  "params": [
9096
9100
  {
@@ -9098,6 +9102,12 @@
9098
9102
  "type": "readonly { key: string; value: string }[]",
9099
9103
  "optional": false,
9100
9104
  "summary": null
9105
+ },
9106
+ {
9107
+ "name": "requestId",
9108
+ "type": "string",
9109
+ "optional": true,
9110
+ "summary": "Pass one only to an arcade that advertised\n `shareResult`, which answers it with one `gem.shareResult` through\n `onShareResult`; any other arcade ignores it. Omit it and nothing\n answers."
9101
9111
  }
9102
9112
  ],
9103
9113
  "returns": "whether the message left the frame — `false` before the bridge is\n up, for an arcade that does not handle share, and past this frame's own\n limit of six a minute.",
@@ -13116,6 +13126,27 @@
13116
13126
  "defaultValue": null,
13117
13127
  "guides": []
13118
13128
  },
13129
+ {
13130
+ "id": "Gem.graphics",
13131
+ "name": "graphics",
13132
+ "anchor": "gem-graphics",
13133
+ "group": "start",
13134
+ "signature": "readonly graphics: GraphicsApi",
13135
+ "documented": true,
13136
+ "summary": "Which renderer to make — WebGPU or WebGL — on this device, and whether\nthe device is listed as broken on WebGL. Ask\n`gem.graphics.preferredRoute()` just before you make your first renderer.\n\nWebGL is the answer almost everywhere; `webgpu` only on a listed device,\nwhere WebGPU is available. A listed device that ends up on WebGL draws the\ngame's safe WebGL recipe. The list is the platform's, so a new entry\nreaches your live game without a new build.",
13137
+ "deprecated": null,
13138
+ "params": [],
13139
+ "returns": null,
13140
+ "throws": null,
13141
+ "example": null,
13142
+ "defaultValue": null,
13143
+ "guides": [
13144
+ {
13145
+ "slug": "graphics-route",
13146
+ "anchor": null
13147
+ }
13148
+ ]
13149
+ },
13119
13150
  {
13120
13151
  "id": "Gem.moments",
13121
13152
  "name": "moments",
@@ -13186,7 +13217,7 @@
13186
13217
  "group": "start",
13187
13218
  "signature": "readonly share: ShareApi",
13188
13219
  "documented": true,
13189
- "summary": "Ask the arcade to share a link to your game — opened at the puzzle, board\nor level your launch parameters name — from your own Share button.\n\nThe arcade builds the link, which unfurls with your game's card, and shares\nit through its own share sheet, a copy, or its own Share panel. Show your\nbutton only where [ShareApi.available](api-reference-game.md#shareapi-available) is true, and read it again in\nthe click handler, because `request` throws once it has gone false:\n\n```js\nshareButton.hidden = !gem.share.available;\nshareButton.onclick = () => {\n if (!gem.share.available) return;\n gem.share.request({ params: { jar: 'JAR-K7P3QXM', n: '2' } });\n};\n```\n\n- **Call it synchronously from the click** — never from a timer, after an\n `await`, or on a game event.\n- **Never gate progress or a reward on sharing.** You are told nothing\n back, not even whether the player shared.\n- **No personal identifiers in `params`.** The link is public.\n- **The share text is the arcade's own**; your game supplies none.",
13220
+ "summary": "Ask the arcade to share a link to your game — opened at the puzzle, board\nor level your launch parameters name — from your own Share button.\n\nThe arcade builds the link, which unfurls with your game's card, and shares\nit through its own share sheet, a copy, or its own Share panel. Show your\nbutton only where [ShareApi.available](api-reference-game.md#shareapi-available) is true, and read it again in\nthe click handler, because `request` throws once it has gone false:\n\n```js\nshareButton.hidden = !gem.share.available;\nshareButton.onclick = () => {\n if (!gem.share.available) return;\n gem.share.request({ params: { jar: 'JAR-K7P3QXM', n: '2' } }).catch((err) => {\n // A ShareRefusal: err.reason ('reserved_key', 'round_trip', 'too_long')\n // and err.key say what to fix. Log it; show the player nothing.\n console.warn(err);\n });\n};\n```\n\n- **Call it synchronously from the click** — never from a timer, after an\n `await`, or on a game event.\n- **Never gate progress or a reward on sharing.** Nothing about the player\n comes back, not even whether they shared. The promise rejects only when\n the arcade refused the share, with a [ShareRefusal](api-reference-game.md#sharerefusal) naming why.\n- **No personal identifiers in `params`.** The link is public.\n- **The share text is the arcade's own**; your game supplies none.",
13190
13221
  "deprecated": null,
13191
13222
  "params": [],
13192
13223
  "returns": null,
@@ -14361,6 +14392,205 @@
14361
14392
  }
14362
14393
  ]
14363
14394
  },
14395
+ {
14396
+ "id": "GemGraphicsRoute",
14397
+ "name": "GemGraphicsRoute",
14398
+ "anchor": "gemgraphicsroute",
14399
+ "group": "graphics",
14400
+ "signature": "type GemGraphicsRoute = \"webgpu\" | \"webgl\"",
14401
+ "documented": true,
14402
+ "summary": "Which renderer to make: `webgpu` or `webgl`.",
14403
+ "deprecated": null,
14404
+ "params": [],
14405
+ "returns": null,
14406
+ "throws": null,
14407
+ "example": null,
14408
+ "defaultValue": null,
14409
+ "guides": []
14410
+ },
14411
+ {
14412
+ "id": "GemPreferredRoute",
14413
+ "name": "GemPreferredRoute",
14414
+ "anchor": "gempreferredroute",
14415
+ "group": "graphics",
14416
+ "signature": "interface GemPreferredRoute",
14417
+ "documented": true,
14418
+ "summary": "What `gem.graphics.preferredRoute()` answers.",
14419
+ "deprecated": null,
14420
+ "params": [],
14421
+ "returns": null,
14422
+ "throws": null,
14423
+ "example": null,
14424
+ "defaultValue": null,
14425
+ "guides": []
14426
+ },
14427
+ {
14428
+ "id": "GemPreferredRoute.route",
14429
+ "name": "route",
14430
+ "anchor": "gempreferredroute-route",
14431
+ "group": "graphics",
14432
+ "signature": "readonly route: GemGraphicsRoute",
14433
+ "documented": true,
14434
+ "summary": "Which renderer to make. `webgpu` only when `listed` is true and the\nbrowser hands out a WebGPU adapter; `webgl` everywhere else.",
14435
+ "deprecated": null,
14436
+ "params": [],
14437
+ "returns": null,
14438
+ "throws": null,
14439
+ "example": null,
14440
+ "defaultValue": null,
14441
+ "guides": []
14442
+ },
14443
+ {
14444
+ "id": "GemPreferredRoute.listed",
14445
+ "name": "listed",
14446
+ "anchor": "gempreferredroute-listed",
14447
+ "group": "graphics",
14448
+ "signature": "readonly listed: boolean",
14449
+ "documented": true,
14450
+ "summary": "Whether the platform lists this device as broken on WebGL. When true and\n`route` is `webgl` (no WebGPU here), or your WebGPU renderer fails to\nstart, draw your safe WebGL recipe — for every listed device today, no\nshadow maps — on every WebGL canvas the page makes, menus included.",
14451
+ "deprecated": null,
14452
+ "params": [],
14453
+ "returns": null,
14454
+ "throws": null,
14455
+ "example": null,
14456
+ "defaultValue": null,
14457
+ "guides": []
14458
+ },
14459
+ {
14460
+ "id": "GemGraphicsRouteEntry",
14461
+ "name": "GemGraphicsRouteEntry",
14462
+ "anchor": "gemgraphicsrouteentry",
14463
+ "group": "graphics",
14464
+ "signature": "interface GemGraphicsRouteEntry",
14465
+ "documented": true,
14466
+ "summary": "One device that should draw with WebGPU, as the platform's list names it.\nAn absent `route` or `host` matches any.",
14467
+ "deprecated": null,
14468
+ "params": [],
14469
+ "returns": null,
14470
+ "throws": null,
14471
+ "example": null,
14472
+ "defaultValue": null,
14473
+ "guides": []
14474
+ },
14475
+ {
14476
+ "id": "GemGraphicsRouteEntry.gpu",
14477
+ "name": "gpu",
14478
+ "anchor": "gemgraphicsrouteentry-gpu",
14479
+ "group": "graphics",
14480
+ "signature": "readonly gpu: string",
14481
+ "documented": true,
14482
+ "summary": "The GPU family, e.g. `powervr_d_series`, `adreno`, `mali`.",
14483
+ "deprecated": null,
14484
+ "params": [],
14485
+ "returns": null,
14486
+ "throws": null,
14487
+ "example": null,
14488
+ "defaultValue": null,
14489
+ "guides": []
14490
+ },
14491
+ {
14492
+ "id": "GemGraphicsRouteEntry.route",
14493
+ "name": "route",
14494
+ "anchor": "gemgraphicsrouteentry-route",
14495
+ "group": "graphics",
14496
+ "signature": "readonly route?: string",
14497
+ "documented": true,
14498
+ "summary": "The route WebGL takes there, e.g. `angle_gles` (OpenGL ES),\n`native_gl` (no ANGLE, as the Arcade's Android app reports) or\n`angle_vulkan`. Absent matches any route.",
14499
+ "deprecated": null,
14500
+ "params": [],
14501
+ "returns": null,
14502
+ "throws": null,
14503
+ "example": null,
14504
+ "defaultValue": null,
14505
+ "guides": []
14506
+ },
14507
+ {
14508
+ "id": "GemGraphicsRouteEntry.host",
14509
+ "name": "host",
14510
+ "anchor": "gemgraphicsrouteentry-host",
14511
+ "group": "graphics",
14512
+ "signature": "readonly host?: string",
14513
+ "documented": true,
14514
+ "summary": "`android_webview` (an Android app's WebView, the Arcade app's included) or\n`other`. Absent matches either.",
14515
+ "deprecated": null,
14516
+ "params": [],
14517
+ "returns": null,
14518
+ "throws": null,
14519
+ "example": null,
14520
+ "defaultValue": null,
14521
+ "guides": []
14522
+ },
14523
+ {
14524
+ "id": "PreferredRouteOptions",
14525
+ "name": "PreferredRouteOptions",
14526
+ "anchor": "preferredrouteoptions",
14527
+ "group": "graphics",
14528
+ "signature": "interface PreferredRouteOptions",
14529
+ "documented": true,
14530
+ "summary": "Options for [GraphicsApi.preferredRoute](api-reference-game.md#graphicsapi-preferredroute).",
14531
+ "deprecated": null,
14532
+ "params": [],
14533
+ "returns": null,
14534
+ "throws": null,
14535
+ "example": null,
14536
+ "defaultValue": null,
14537
+ "guides": []
14538
+ },
14539
+ {
14540
+ "id": "PreferredRouteOptions.timeoutMs",
14541
+ "name": "timeoutMs",
14542
+ "anchor": "preferredrouteoptions-timeoutms",
14543
+ "group": "graphics",
14544
+ "signature": "readonly timeoutMs?: number",
14545
+ "documented": true,
14546
+ "summary": "The longest the answer may take, in milliseconds. Default 1000, at most\n5000. When it runs out, the answer comes from what is known by then.",
14547
+ "deprecated": null,
14548
+ "params": [],
14549
+ "returns": null,
14550
+ "throws": null,
14551
+ "example": null,
14552
+ "defaultValue": null,
14553
+ "guides": []
14554
+ },
14555
+ {
14556
+ "id": "GraphicsApi",
14557
+ "name": "GraphicsApi",
14558
+ "anchor": "graphicsapi",
14559
+ "group": "graphics",
14560
+ "signature": "interface GraphicsApi",
14561
+ "documented": true,
14562
+ "summary": "Which renderer to make. Reached as `gem.graphics`.",
14563
+ "deprecated": null,
14564
+ "params": [],
14565
+ "returns": null,
14566
+ "throws": null,
14567
+ "example": null,
14568
+ "defaultValue": null,
14569
+ "guides": []
14570
+ },
14571
+ {
14572
+ "id": "GraphicsApi.preferredRoute",
14573
+ "name": "preferredRoute",
14574
+ "anchor": "graphicsapi-preferredroute",
14575
+ "group": "graphics",
14576
+ "signature": "preferredRoute(options?: PreferredRouteOptions): Promise<GemPreferredRoute>",
14577
+ "documented": true,
14578
+ "summary": "Which renderer to make, and whether this device is listed. Ask once, just\nbefore you make your first renderer, and follow it for every canvas.\n\n`route` is `webgl` almost everywhere. It is `webgpu` only on a device the\nplatform lists as broken on WebGL (today the PowerVR D-Series GPU in the\nPixel 10, on every route but Vulkan), and only when WebGPU is available\nthere. `listed` is true on such a device: when `route` is `webgl` there,\nor making the WebGPU renderer fails, draw your safe WebGL recipe.\n\nResolves within `timeoutMs` (default 1000 ms) and never rejects; normally\nat once after `gem.awaitBridge()`. The launch key `gemGraphics=webgpu`,\n`gemGraphics=safe` or `gemGraphics=webgl` forces the answer, for testing.",
14579
+ "deprecated": null,
14580
+ "params": [
14581
+ {
14582
+ "name": "options",
14583
+ "type": "PreferredRouteOptions",
14584
+ "optional": true,
14585
+ "summary": "See [PreferredRouteOptions](api-reference-game.md#preferredrouteoptions)."
14586
+ }
14587
+ ],
14588
+ "returns": "`{ route, listed }`.",
14589
+ "throws": null,
14590
+ "example": null,
14591
+ "defaultValue": null,
14592
+ "guides": []
14593
+ },
14364
14594
  {
14365
14595
  "id": "GemMatchServerModule",
14366
14596
  "name": "GemMatchServerModule",
@@ -16712,6 +16942,125 @@
16712
16942
  "defaultValue": null,
16713
16943
  "guides": []
16714
16944
  },
16945
+ {
16946
+ "id": "PurchaseApi.onSettled",
16947
+ "name": "onSettled",
16948
+ "anchor": "purchaseapi-onsettled",
16949
+ "group": "purchases",
16950
+ "signature": "onSettled(listener: (settlement: PurchaseSettlement) => void): () => void",
16951
+ "documented": true,
16952
+ "summary": "Hear about a purchase of yours that completed AFTER its own call answered\n— returns a function that stops listening.\n\nThe case it exists for: a guest's `gem.me.mutations.purchase` is refused\n`guest_upgrade_required` (your call rejects at once, and you show\nnothing), the arcade offers the player an account, and they make one and\nconfirm the arcade's sheet a minute later — or after a reload, when the\ncall that asked is gone. This is how your game hears that it happened.\n\nTHE SDK CONFIRMS IT FIRST. A listener is called only once the platform\nitself, asked with your game's own token, says the purchase was granted;\na claim it cannot confirm is never reported. The player's inventory is\nre-read either way, so `gem.me.inventory.onChanged` fires too. Treat it\nlike an `ok`: grant feedback, then re-read inventory rather than assuming\nwhat landed.\n\nHears nothing on an arcade that does not support it (the\n`purchaseSettled` capability), and never for a purchase an `ok` already\nreported.",
16953
+ "deprecated": null,
16954
+ "params": [
16955
+ {
16956
+ "name": "listener",
16957
+ "type": "(settlement: PurchaseSettlement) => void",
16958
+ "optional": false,
16959
+ "summary": null
16960
+ }
16961
+ ],
16962
+ "returns": null,
16963
+ "throws": null,
16964
+ "example": null,
16965
+ "defaultValue": null,
16966
+ "guides": []
16967
+ },
16968
+ {
16969
+ "id": "PurchaseSettlement",
16970
+ "name": "PurchaseSettlement",
16971
+ "anchor": "purchasesettlement",
16972
+ "group": "purchases",
16973
+ "signature": "interface PurchaseSettlement",
16974
+ "documented": true,
16975
+ "summary": "A purchase of one of your game's own items or recipes that completed after\nthe call that asked for it had answered. What [PurchaseApi.onSettled](api-reference-game.md#purchaseapi-onsettled)\nhears.",
16976
+ "deprecated": null,
16977
+ "params": [],
16978
+ "returns": null,
16979
+ "throws": null,
16980
+ "example": null,
16981
+ "defaultValue": null,
16982
+ "guides": []
16983
+ },
16984
+ {
16985
+ "id": "PurchaseSettlement.kind",
16986
+ "name": "kind",
16987
+ "anchor": "purchasesettlement-kind",
16988
+ "group": "purchases",
16989
+ "signature": "readonly kind: \"item\" | \"recipe\"",
16990
+ "documented": true,
16991
+ "summary": "`item` or `recipe`.",
16992
+ "deprecated": null,
16993
+ "params": [],
16994
+ "returns": null,
16995
+ "throws": null,
16996
+ "example": null,
16997
+ "defaultValue": null,
16998
+ "guides": []
16999
+ },
17000
+ {
17001
+ "id": "PurchaseSettlement.targetId",
17002
+ "name": "targetId",
17003
+ "anchor": "purchasesettlement-targetid",
17004
+ "group": "purchases",
17005
+ "signature": "readonly targetId: string",
17006
+ "documented": true,
17007
+ "summary": "The item-definition or recipe id that was bought.",
17008
+ "deprecated": null,
17009
+ "params": [],
17010
+ "returns": null,
17011
+ "throws": null,
17012
+ "example": null,
17013
+ "defaultValue": null,
17014
+ "guides": []
17015
+ },
17016
+ {
17017
+ "id": "PurchaseSettlement.catalogId",
17018
+ "name": "catalogId",
17019
+ "anchor": "purchasesettlement-catalogid",
17020
+ "group": "purchases",
17021
+ "signature": "readonly catalogId?: string",
17022
+ "documented": true,
17023
+ "summary": "The recipe's catalog; absent for an item.",
17024
+ "deprecated": null,
17025
+ "params": [],
17026
+ "returns": null,
17027
+ "throws": null,
17028
+ "example": null,
17029
+ "defaultValue": null,
17030
+ "guides": []
17031
+ },
17032
+ {
17033
+ "id": "PurchaseSettlement.quantity",
17034
+ "name": "quantity",
17035
+ "anchor": "purchasesettlement-quantity",
17036
+ "group": "purchases",
17037
+ "signature": "readonly quantity: number",
17038
+ "documented": true,
17039
+ "summary": "How many were bought. Always 1 for a recipe.",
17040
+ "deprecated": null,
17041
+ "params": [],
17042
+ "returns": null,
17043
+ "throws": null,
17044
+ "example": null,
17045
+ "defaultValue": null,
17046
+ "guides": []
17047
+ },
17048
+ {
17049
+ "id": "PurchaseSettlement.purchaseId",
17050
+ "name": "purchaseId",
17051
+ "anchor": "purchasesettlement-purchaseid",
17052
+ "group": "purchases",
17053
+ "signature": "readonly purchaseId: string",
17054
+ "documented": true,
17055
+ "summary": "The platform's own id for this purchase, which the SDK confirmed with the\nplatform before calling you. A handle for logs and support, as on\n[PurchaseOutcome](api-reference-game.md#purchaseoutcome)'s `ok`; nothing reads it back.",
17056
+ "deprecated": null,
17057
+ "params": [],
17058
+ "returns": null,
17059
+ "throws": null,
17060
+ "example": null,
17061
+ "defaultValue": null,
17062
+ "guides": []
17063
+ },
16715
17064
  {
16716
17065
  "id": "Avatar3dPurchaseRequest",
16717
17066
  "name": "Avatar3dPurchaseRequest",
@@ -16765,9 +17114,9 @@
16765
17114
  "name": "PurchaseRefusalCode",
16766
17115
  "anchor": "purchaserefusalcode",
16767
17116
  "group": "purchases",
16768
- "signature": "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\"",
17117
+ "signature": "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\" | \"guest_upgrade_required\" | \"recipe_on_cooldown\" | \"recipe_max_uses_exceeded\" | \"item_max_stack_exceeded\" | \"recipe_costs_unmet\" | \"quantity_invalid\" | \"error\"",
16769
17118
  "documented": true,
16770
- "summary": "Every reason a purchase can be refused, on either purchase path.\n\n- `unknown_currency` — the slug names no active platform currency. There are\n no aliases: `TICKETS` is the retired name for Gems and is refused, never\n resolved. Write `GEMS` or `ARCADE_TICKETS`.\n- `currency_not_in_game_payable` — the currency exists but cannot pay for game\n items.\n- `not_purchasable` — the target is not in the purchase catalog, or cannot be\n bought (not published on this channel, inactive, or its config does not\n convert).\n- `no_price_in_currency` — the target has prices, but none in that currency.\n- `busy` — another purchase is in flight, a sheet is already open, the\n platform is backing off after a cancel, or the ask came without a player's\n click or key press.\n- `cancelled` — the player dismissed the platform sheet.\n- `insufficient_funds` — the player cannot afford it.\n- `price_mismatch`, `config_version_stale` — the price or config changed\n between reading the catalog and buying; nothing was charged. Re-read.\n- `platform_currency_unavailable` — the currency is not available to this\n purchase right now.\n- `not_available` — the recipe is outside its availability window, or\n inactive; nothing was charged. Hide or disable it until its window opens.\n- `recipe_locked` — the recipe's prerequisites are not met.\n- `already_owned` — the player already owns the 3D avatar\n `gem.avatars3d.purchase` asked for; nothing was charged. Only that call\n answers it.\n- `recipe_on_cooldown`, `recipe_max_uses_exceeded`, `item_max_stack_exceeded`,\n `recipe_costs_unmet`, `quantity_invalid` — the recipe or item refused this\n purchase, before any charge.\n- `error` — anything else, including a reason this SDK version does not know\n and an answer that never arrived.\n\nFOR YOUR LOGS AND YOUR SHOP'S STATE, NEVER FOR A PAYMENT SCREEN. Grey out a\nlocked recipe; fix a slug. Never draw a top-up, \"buy more\" or payment UI of\nyour own — only the arcade may, and a game screen that looks like one is an\nimitation of platform payment chrome.",
17119
+ "summary": "Every reason a purchase can be refused, on either purchase path.\n\n- `unknown_currency` — the slug names no active platform currency. There are\n no aliases: `TICKETS` is the retired name for Gems and is refused, never\n resolved. Write `GEMS` or `ARCADE_TICKETS`.\n- `currency_not_in_game_payable` — the currency exists but cannot pay for game\n items.\n- `not_purchasable` — the target is not in the purchase catalog, or cannot be\n bought (not published on this channel, inactive, or its config does not\n convert).\n- `no_price_in_currency` — the target has prices, but none in that currency.\n- `busy` — another purchase is in flight, a sheet is already open, the\n platform is backing off after a cancel, or the ask came without a player's\n click or key press.\n- `cancelled` — the player dismissed the platform sheet.\n- `insufficient_funds` — the player cannot afford it.\n- `price_mismatch`, `config_version_stale` — the price or config changed\n between reading the catalog and buying; nothing was charged. Re-read.\n- `platform_currency_unavailable` — the currency is not available to this\n purchase right now.\n- `not_available` — the recipe is outside its availability window, or\n inactive; nothing was charged. Hide or disable it until its window opens.\n- `recipe_locked` — the recipe's prerequisites are not met.\n- `already_owned` — the player already owns the 3D avatar\n `gem.avatars3d.purchase` asked for; nothing was charged. Only that call\n answers it.\n- `guest_upgrade_required` — the player is playing as a guest, and the\n platform lets only an account spend. Nothing was charged. TREAT IT EXACTLY\n LIKE A DECLINE, AND DRAW NOTHING: the arcade offers the player an account\n itself, over your game, and a sign-up or \"log in to buy\" screen of your own\n would be a second one. A game on `gem.me.mutations.purchase` gets it as a\n `GemApiError` after the SDK has already handed the attempt to the arcade.\n- `recipe_on_cooldown`, `recipe_max_uses_exceeded`, `item_max_stack_exceeded`,\n `recipe_costs_unmet`, `quantity_invalid` — the recipe or item refused this\n purchase, before any charge.\n- `error` — anything else, including a reason this SDK version does not know\n and an answer that never arrived.\n\nFOR YOUR LOGS AND YOUR SHOP'S STATE, NEVER FOR A PAYMENT SCREEN. Grey out a\nlocked recipe; fix a slug. Never draw a top-up, \"buy more\" or payment UI of\nyour own — only the arcade may, and a game screen that looks like one is an\nimitation of platform payment chrome.",
16771
17120
  "deprecated": null,
16772
17121
  "params": [],
16773
17122
  "returns": null,
@@ -17720,6 +18069,54 @@
17720
18069
  "defaultValue": null,
17721
18070
  "guides": []
17722
18071
  },
18072
+ {
18073
+ "id": "ShareRefusal",
18074
+ "name": "ShareRefusal",
18075
+ "anchor": "sharerefusal",
18076
+ "group": "share",
18077
+ "signature": "class ShareRefusal",
18078
+ "documented": true,
18079
+ "summary": "The arcade refused a share, and why. `gem.share.request`'s promise rejects\nwith it. Fix the params it names: the same share is refused again.",
18080
+ "deprecated": null,
18081
+ "params": [],
18082
+ "returns": null,
18083
+ "throws": null,
18084
+ "example": null,
18085
+ "defaultValue": null,
18086
+ "guides": []
18087
+ },
18088
+ {
18089
+ "id": "ShareRefusal.reason",
18090
+ "name": "reason",
18091
+ "anchor": "sharerefusal-reason",
18092
+ "group": "share",
18093
+ "signature": "readonly reason: string",
18094
+ "documented": true,
18095
+ "summary": "Why, as a machine code. OPEN-ENDED, so keep a `default` arm:\n\n- `reserved_key` — [ShareRefusal.key](api-reference-game.md#sharerefusal-key) is a key the arcade keeps for\n itself and does not carry in a share link.\n- `round_trip` — the link the arcade built does not read back as what you\n asked for, or [ShareRefusal.key](api-reference-game.md#sharerefusal-key) is too long to carry.\n- `too_long` — the whole link is over 2 KB. Share fewer or shorter params.\n- `error` — the arcade refused without saying why.",
18096
+ "deprecated": null,
18097
+ "params": [],
18098
+ "returns": null,
18099
+ "throws": null,
18100
+ "example": null,
18101
+ "defaultValue": null,
18102
+ "guides": []
18103
+ },
18104
+ {
18105
+ "id": "ShareRefusal.key",
18106
+ "name": "key",
18107
+ "anchor": "sharerefusal-key",
18108
+ "group": "share",
18109
+ "signature": "readonly key?: string",
18110
+ "documented": true,
18111
+ "summary": "The param key that caused it, exactly as you passed it, when one key did.",
18112
+ "deprecated": null,
18113
+ "params": [],
18114
+ "returns": null,
18115
+ "throws": null,
18116
+ "example": null,
18117
+ "defaultValue": null,
18118
+ "guides": []
18119
+ },
17723
18120
  {
17724
18121
  "id": "ShareApi",
17725
18122
  "name": "ShareApi",
@@ -17757,9 +18154,9 @@
17757
18154
  "name": "request",
17758
18155
  "anchor": "shareapi-request",
17759
18156
  "group": "share",
17760
- "signature": "request(request?: ShareRequest): void",
18157
+ "signature": "request(request?: ShareRequest): Promise<void>",
17761
18158
  "documented": true,
17762
- "summary": "Ask the arcade to share a link that opens your game with `params`. Call it\nsynchronously from your own Share button's click handler. It returns\nnothing, and nothing ever comes back — not even whether the player shared —\nso never gate progress or a reward on it.",
18159
+ "summary": "Ask the arcade to share a link that opens your game with `params`. Call it\nsynchronously from your own Share button's click handler. Nothing about the\nplayer ever comes back — not even whether they shared — so never gate\nprogress or a reward on it.",
17763
18160
  "deprecated": null,
17764
18161
  "params": [
17765
18162
  {
@@ -17769,8 +18166,8 @@
17769
18166
  "summary": null
17770
18167
  }
17771
18168
  ],
17772
- "returns": null,
17773
- "throws": "`Error` when [ShareApi.available](api-reference-game.md#shareapi-available) is false, and `TypeError`,\n naming the parameter, for one the arcade would refuse: more than 16, a\n key that is not a letter followed by up to 63 letters, digits, `_` or\n `-`, a key the arcade reserves for itself, or a value that is not a\n string, is over 512 characters, or holds a control, format or unpaired\n surrogate character. Nothing is sent when it throws.",
18169
+ "returns": "a promise that REJECTS with a [ShareRefusal](api-reference-game.md#sharerefusal) when the arcade\n refused the share, naming the reason and the param, and otherwise\n resolves with nothing. Resolving does not mean the player shared, or saw\n a sheet at all. An older arcade never refuses out loud, so there it\n resolves at once.",
18170
+ "throws": "`Error` when [ShareApi.available](api-reference-game.md#shareapi-available) is false, and `TypeError`,\n naming the parameter, for one the arcade would refuse: more than 16, a\n key that is not a letter followed by up to 63 letters, digits, `_` or\n `-`, a key the arcade keeps for itself, or a value that is not a string,\n is over 512 characters, or holds a control, format or unpaired surrogate\n character. Nothing is sent when it throws. Which keys the arcade keeps\n depends on the arcade: a current one carries reserved keys such as\n `invite` and refuses only `embedder` (and `_s`, which is not a valid key\n anyway); an older one refuses all of them.",
17774
18171
  "example": null,
17775
18172
  "defaultValue": null,
17776
18173
  "guides": []
@@ -30371,9 +30768,9 @@
30371
30768
  "name": "PlatformEventName",
30372
30769
  "anchor": "platformeventname",
30373
30770
  "group": "analytics",
30374
- "signature": "type PlatformEventName = \"gem_session_start\" | \"gem_session_end\" | \"gem_pause\" | \"gem_resume\" | \"gem_boot_ready\" | \"gem_match_started\" | \"gem_match_ended\" | \"gem_auth_lost\"",
30771
+ "signature": "type PlatformEventName = \"gem_session_start\" | \"gem_session_end\" | \"gem_pause\" | \"gem_resume\" | \"gem_boot_ready\" | \"gem_match_started\" | \"gem_match_ended\" | \"gem_auth_lost\" | \"gem_graphics_context_lost\"",
30375
30772
  "documented": true,
30376
- "summary": "Every event the SDK emits by itself.\n\nA game never sends these — `gem.analytics.track` refuses the whole\n`gem_` namespace — but the names are published because a game reading its\nown funnel needs the vocabulary, and because a funnel declared in the game\nconfig names them as steps.\n\n| Event | Dimensions | Measures |\n|---|---|---|\n| `gem_session_start` | — | — |\n| `gem_session_end` | `reason` ([SessionEndReason](api-reference-function.md#sessionendreason)) | `duration_ms` |\n| `gem_pause` / `gem_resume` | — | — |\n| `gem_boot_ready` | — | `boot_ms` |\n| `gem_match_started` / `gem_match_ended` | `transport` (`MatchTransport`) | — |\n| `gem_auth_lost` | — | — |",
30773
+ "summary": "Every event the SDK emits by itself.\n\nA game never sends these — `gem.analytics.track` refuses the whole\n`gem_` namespace — but the names are published because a game reading its\nown funnel needs the vocabulary, and because a funnel declared in the game\nconfig names them as steps.\n\n| Event | Dimensions | Measures |\n|---|---|---|\n| `gem_session_start` | — | — |\n| `gem_session_end` | `reason` ([SessionEndReason](api-reference-function.md#sessionendreason)) | `duration_ms` |\n| `gem_pause` / `gem_resume` | — | — |\n| `gem_boot_ready` | — | `boot_ms` |\n| `gem_match_started` / `gem_match_ended` | `transport` (`MatchTransport`) | — |\n| `gem_auth_lost` | — | — |\n| `gem_graphics_context_lost` | `gpu`, `host`, `route` | `context_ms`, `hidden`, `nth_loss` |\n\n`gem_graphics_context_lost` is the browser taking a WebGL context away, or a\nWebGPU device (`route` is then `webgpu`) — usually a GPU driver fault, sized\nper device across every game (`analytics/graphics.ts` lists the dimension\nvalues).",
30377
30774
  "deprecated": null,
30378
30775
  "params": [],
30379
30776
  "returns": null,
@@ -33417,7 +33814,7 @@
33417
33814
  "group": "inventory",
33418
33815
  "signature": "purchase(request: ConfiguredPurchaseRequest, options?: PurchaseOptions): Promise<ConfiguredPurchaseResult>",
33419
33816
  "documented": true,
33420
- "summary": "Purchase an item or recipe through the game's authenticated SDK transport.\n\nOne call covers both direct payment types:\n\n- `payment: { kind: 'configured' }` runs a recipe and charges the costs in\n your game config.\n- `payment: { kind: 'platform_currency', currencySlug: 'GEMS' }` resolves\n the target's published platform price and spends the player's platform\n balance directly. It does not send a bridge message or open the platform\n purchase sheet; your game owns any confirmation UI.\n\nThe game supplies no amount, config version or nonce. The SDK resolves the\ncurrent published price and config version, mints the nonce, and the\nplatform checks all of them again before it debits anything.\n\nA REFUSAL CARRIES A `code` — branch on `err.code`, in the vocabulary\n`gem.purchases.request` answers in (`PurchaseRefusalCode`). Before\nsending a `platform_currency` payment the SDK itself throws a\n`PurchaseRefusal` (a `RangeError`) for `unknown_currency`,\n`not_purchasable`, `no_price_in_currency` or `quantity_invalid`, with a\nmessage naming the active slugs or what the target is priced in. The\nplatform's own refusals arrive as a `GemApiError` whose `code` is its wire\ncode — `insufficient_funds`, `recipe_locked`, `price_mismatch`, … Never\ndraw a top-up or payment screen of your own for either.",
33817
+ "summary": "Purchase an item or recipe through the game's authenticated SDK transport.\n\nOne call covers both direct payment types:\n\n- `payment: { kind: 'configured' }` runs a recipe and charges the costs in\n your game config.\n- `payment: { kind: 'platform_currency', currencySlug: 'GEMS' }` resolves\n the target's published platform price and spends the player's platform\n balance directly. It does not send a bridge message or open the platform\n purchase sheet; your game owns any confirmation UI.\n\nThe game supplies no amount, config version or nonce. The SDK resolves the\ncurrent published price and config version, mints the nonce, and the\nplatform checks all of them again before it debits anything.\n\nA REFUSAL CARRIES A `code` — branch on `err.code`, in the vocabulary\n`gem.purchases.request` answers in (`PurchaseRefusalCode`). Before\nsending a `platform_currency` payment the SDK itself throws a\n`PurchaseRefusal` (a `RangeError`) for `unknown_currency`,\n`not_purchasable`, `no_price_in_currency` or `quantity_invalid`, with a\nmessage naming the active slugs or what the target is priced in. The\nplatform's own refusals arrive as a `GemApiError` whose `code` is its wire\ncode — `insufficient_funds`, `recipe_locked`, `price_mismatch`, … Never\ndraw a top-up or payment screen of your own for either.\n\n`guest_upgrade_required` means the player is a guest, and the platform\nlets only an account spend. TREAT IT LIKE A DECLINE AND DRAW NOTHING: the\nSDK has already handed the attempt to the arcade, which offers the player\nan account over your game. The call rejects at once rather than waiting\nfor them; if they make one and buy, `PurchaseApi.onSettled` hears it.",
33421
33818
  "deprecated": null,
33422
33819
  "params": [
33423
33820
  {
@@ -116,8 +116,8 @@ them only for fraud or moderation ([achievements.md](achievements.md)).
116
116
  ## 5. Spending: price each item in the currency it deserves
117
117
 
118
118
  Any game may sell an item or a recipe for a platform currency, with no server and no approval: the
119
- item or recipe carries `platform_prices`, and the player pays through the arcade's purchase sheet or
120
- your own purchase screen
119
+ item or recipe carries `platform_prices`, and the player pays through the arcade's purchase sheet,
120
+ `gem.purchases.request`
121
121
  ([inventory-and-catalogs.md](inventory-and-catalogs.md#16-selling-for-a-platform-currency)). What a
122
122
  currency may price is a platform setting on that currency, its **spend scope**:
123
123