@indiegems/gem-web-sdk 7.9.1 → 8.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.
@@ -2,7 +2,7 @@
2
2
  "schema_version": 1,
3
3
  "source": {
4
4
  "repo": "IndieGems/gem-web-sdk",
5
- "version": "7.9.1"
5
+ "version": "8.1.0"
6
6
  },
7
7
  "entries": [
8
8
  {
@@ -61,6 +61,10 @@
61
61
  "key": "share",
62
62
  "title": "Sharing a link to your game"
63
63
  },
64
+ {
65
+ "key": "vibration",
66
+ "title": "Vibrating the device"
67
+ },
64
68
  {
65
69
  "key": "config",
66
70
  "title": "Your published config"
@@ -371,6 +375,29 @@
371
375
  "defaultValue": null,
372
376
  "guides": []
373
377
  },
378
+ {
379
+ "id": "GameConfig.rewarded_placements",
380
+ "name": "rewarded_placements",
381
+ "anchor": "gameconfig-rewarded_placements",
382
+ "group": "config",
383
+ "url": "https://gemarcade.com/developer/api/game#gameconfig-rewarded_placements",
384
+ "signature": "rewarded_placements?: SnapshotRewardedPlacement[]",
385
+ "documented": true,
386
+ "summary": "Your rewarded placements — what pressing a rewarded button grants. An\narray, one row per placement, each naming itself in `placement`, with its\n`grants`, the caps the platform enforces on it (`max_per_day`,\n`cooldown_seconds`, `max_held`) and what happens where no ad serves\n(`without_ads`).\n\nRead it to LABEL your button — \"+1 Revive\" — never to decide a reward. A\ngrant is an `item_grant`, with a `quantity` of the item its\n`item_definition_id` names, or a `currency_grant`, with an `amount` of the\ncurrency its `currency_id` names; the word on the button is that\ndefinition's `display_name`, from [GameConfig.item](https://gemarcade.com/developer/api/game#gameconfig-item) or\n[GameConfig.currency](https://gemarcade.com/developer/api/game#gameconfig-currency):\n\n```js\nconst row = config?.rewarded_placements?.find((p) => p.placement === 'classic_revive');\nconst grant = row?.grants[0];\nconst label =\n grant?.type === 'item_grant'\n ? `+${grant.quantity} ${config.item(grant.item_definition_id)?.display_name ?? ''}`\n : grant?.type === 'currency_grant'\n ? `+${grant.amount} ${config.currency(grant.currency_id)?.display_name ?? ''}`\n : undefined;\n```\n\nWhether an ask grants anything is the platform's decision, made against\nthis config server-side; the grant lands on your game's inventory, and\nre-reading `gem.me.inventory` is the only way to learn that it did. Whether\nthe button could pay right now is [AdsApi.rewardedStatus](https://gemarcade.com/developer/api/game#adsapi-rewardedstatus).\n\nAbsent when your game declares none.",
387
+ "deprecated": null,
388
+ "params": [],
389
+ "returns": null,
390
+ "throws": null,
391
+ "example": null,
392
+ "defaultValue": null,
393
+ "guides": [
394
+ {
395
+ "slug": "cross-promotion",
396
+ "anchor": null,
397
+ "url": "https://gemarcade.com/developer/docs/cross-promotion"
398
+ }
399
+ ]
400
+ },
374
401
  {
375
402
  "id": "GameConfig.version",
376
403
  "name": "version",
@@ -4019,7 +4046,7 @@
4019
4046
  "url": "https://gemarcade.com/developer/api/game#adoutcomekind",
4020
4047
  "signature": "type AdOutcomeKind = \"shown\" | \"no_ad\" | \"unavailable\"",
4021
4048
  "documented": true,
4022
- "summary": "The complete vocabulary of answers. Three values, and there will not be a\nfourth.\n\n- `shown` — a panel was painted and has now closed. Resume. **It grants\n nothing.**\n- `no_ad` — nothing was painted. Carry on exactly as if you had not asked.\n This is the common answer.\n- `unavailable` — the arcade could not render just now. Not a softer no; read\n it exactly as you read `no_ad`.\n\nNo reason is given for a `no_ad`, and one will never be added: a reason would\ntell your game something about the player — an age, a consent state, a\nsubscription — that game code is deliberately not told.",
4049
+ "summary": "The complete vocabulary of answers. Three values, and there will not be a\nfourth.\n\n- `shown` — a panel was painted and has now closed. Resume. **It grants\n nothing.**\n- `no_ad` — nothing was painted. Carry on exactly as if you had not asked.\n This is the common answer.\n- `unavailable` — the arcade could not render just now. Not a softer no; read\n it exactly as you read `no_ad`.\n\nNo reason is given for a `no_ad`, and one will never be added: a reason would\ntell your game something about the player — an age, a consent state, a\nsubscription — that game code is deliberately not told.\n\nThe one thing you can learn is about **your own economy**, and not from this\nanswer: [AdsApi.rewardedStatus](https://gemarcade.com/developer/api/game#adsapi-rewardedstatus) reports your own declared rewarded caps\nas the platform holds them (a platform-wide per-placement maximum, where one\nis set, can lower a cap). It never reports anything about the player.",
4023
4050
  "deprecated": null,
4024
4051
  "params": [],
4025
4052
  "returns": null,
@@ -4109,6 +4136,155 @@
4109
4136
  }
4110
4137
  ]
4111
4138
  },
4139
+ {
4140
+ "id": "AdsApi.rewardedStatus",
4141
+ "name": "rewardedStatus",
4142
+ "anchor": "adsapi-rewardedstatus",
4143
+ "group": "ads",
4144
+ "url": "https://gemarcade.com/developer/api/game#adsapi-rewardedstatus",
4145
+ "signature": "rewardedStatus(placement: string): Promise<RewardedStatus>",
4146
+ "documented": true,
4147
+ "summary": "Whether a rewarded placement could pay right now, so you can decide whether\nto draw its button. **It never says whether an ad will play.**\n\nThe answer comes from your own `rewarded_placements` config — its daily\ncap, cooldown and `max_held` — applied to this player's use of it, as the\nplatform holds them. Nothing in it describes the player. `grantable: true`\nmeans the platform would grant if an ad is watched or, where no ad serves,\nunder the placement's `without_ads: grant`; a player can still press the\nbutton and get `no_ad`, for reasons your game is not told.\n\nNOTHING REJECTS, like everything else on `gem.ads`. Without a play token,\nwhen the read fails or takes longer than ten seconds, or for a `placement`\noutside the grammar [AdRequest.placement](https://gemarcade.com/developer/api/game#adrequest-placement) documents, it resolves not\ndeclared and not grantable, with no `resetsAt`, and the reason goes to your\n`onError` handler.\n\nIt is a read of the platform, so call it when you draw the button — on a\nresults or death screen — not every frame. Calls for one placement that\noverlap share one request. Draw the rest of the screen first rather than\nholding it on this answer.\n\n⚠️ It grants nothing and reserves nothing. The reward is granted by the\nplatform when the player asks with `requestAd`; re-read `gem.me.inventory`\nafter that ask settles, whatever it settled as.",
4148
+ "deprecated": null,
4149
+ "params": [
4150
+ {
4151
+ "name": "placement",
4152
+ "type": "string",
4153
+ "optional": false,
4154
+ "summary": "A placement your `rewarded_placements` config declares."
4155
+ }
4156
+ ],
4157
+ "returns": "The placement's state. Always resolves; never rejects.",
4158
+ "throws": null,
4159
+ "example": "```ts\nconst status = await gem.ads.rewardedStatus('classic_revive');\nreviveButton.disabled = !status.grantable;\nif (status.nextEligibleAt !== undefined) {\n reviveButton.title = `Back at ${new Date(status.nextEligibleAt).toLocaleTimeString()}`;\n}\n```",
4160
+ "defaultValue": null,
4161
+ "guides": [
4162
+ {
4163
+ "slug": "cross-promotion",
4164
+ "anchor": null,
4165
+ "url": "https://gemarcade.com/developer/docs/cross-promotion"
4166
+ }
4167
+ ]
4168
+ },
4169
+ {
4170
+ "id": "RewardedStatus",
4171
+ "name": "RewardedStatus",
4172
+ "anchor": "rewardedstatus",
4173
+ "group": "ads",
4174
+ "url": "https://gemarcade.com/developer/api/game#rewardedstatus",
4175
+ "signature": "interface RewardedStatus",
4176
+ "documented": true,
4177
+ "summary": "What [AdsApi.rewardedStatus](https://gemarcade.com/developer/api/game#adsapi-rewardedstatus) answers: the state of one rewarded\nplacement for this player, from your own declared caps.\n\nEvery figure comes from **your** `rewarded_placements` config — `max_per_day`,\n`cooldown_seconds`, `max_held` — applied to this player's use of it, as the\nplatform holds them. The platform can hold a placement's daily cap below what\nyou declared, never above it. Nothing here describes the player.\n\n⚠️ **It never says whether an ad will play.** Whether one does depends on\nthings your game is deliberately not told. A player can see your button,\npress it, and get `no_ad`: read that `no_ad` like any other, and never guess\na reason.",
4178
+ "deprecated": null,
4179
+ "params": [],
4180
+ "returns": null,
4181
+ "throws": null,
4182
+ "example": null,
4183
+ "defaultValue": null,
4184
+ "guides": []
4185
+ },
4186
+ {
4187
+ "id": "RewardedStatus.declared",
4188
+ "name": "declared",
4189
+ "anchor": "rewardedstatus-declared",
4190
+ "group": "ads",
4191
+ "url": "https://gemarcade.com/developer/api/game#rewardedstatus-declared",
4192
+ "signature": "readonly declared: boolean",
4193
+ "documented": true,
4194
+ "summary": "Whether this channel's published config declares the placement.\n\n`false` for a placement your config does not name, a config that is not\npromoted to the channel being played, and every answer the platform did not\ngive (see [RewardedStatus.resetsAt](https://gemarcade.com/developer/api/game#rewardedstatus-resetsat)).",
4195
+ "deprecated": null,
4196
+ "params": [],
4197
+ "returns": null,
4198
+ "throws": null,
4199
+ "example": null,
4200
+ "defaultValue": null,
4201
+ "guides": []
4202
+ },
4203
+ {
4204
+ "id": "RewardedStatus.grantable",
4205
+ "name": "grantable",
4206
+ "anchor": "rewardedstatus-grantable",
4207
+ "group": "ads",
4208
+ "url": "https://gemarcade.com/developer/api/game#rewardedstatus-grantable",
4209
+ "signature": "readonly grantable: boolean",
4210
+ "documented": true,
4211
+ "summary": "Whether pressing your button could pay right now: the platform would grant\nthe reward if an ad is watched or, where no ad serves, under the\nplacement's `without_ads: grant`.\n\n**Not a promise that an ad will play, and not a promise of a reward.** It\nis `false` whenever a cap is spent, a cooldown is running, the player holds\n`max_held`, rewarded is off on this channel, or the arcade running your\ngame does not take ad requests at all.",
4212
+ "deprecated": null,
4213
+ "params": [],
4214
+ "returns": null,
4215
+ "throws": null,
4216
+ "example": null,
4217
+ "defaultValue": null,
4218
+ "guides": []
4219
+ },
4220
+ {
4221
+ "id": "RewardedStatus.remainingToday",
4222
+ "name": "remainingToday",
4223
+ "anchor": "rewardedstatus-remainingtoday",
4224
+ "group": "ads",
4225
+ "url": "https://gemarcade.com/developer/api/game#rewardedstatus-remainingtoday",
4226
+ "signature": "readonly remainingToday?: number",
4227
+ "documented": true,
4228
+ "summary": "Grants left today under the placement's daily cap, a non-negative integer.\n\nABSENT — no key, never a sentinel — when the placement has no daily cap,\nand on any answer where `declared` is `false`.",
4229
+ "deprecated": null,
4230
+ "params": [],
4231
+ "returns": null,
4232
+ "throws": null,
4233
+ "example": null,
4234
+ "defaultValue": null,
4235
+ "guides": []
4236
+ },
4237
+ {
4238
+ "id": "RewardedStatus.nextEligibleAt",
4239
+ "name": "nextEligibleAt",
4240
+ "anchor": "rewardedstatus-nexteligibleat",
4241
+ "group": "ads",
4242
+ "url": "https://gemarcade.com/developer/api/game#rewardedstatus-nexteligibleat",
4243
+ "signature": "readonly nextEligibleAt?: number",
4244
+ "documented": true,
4245
+ "summary": "When the placement's cooldown ends, in epoch milliseconds — render it with\n`new Date(nextEligibleAt)`.\n\nABSENT when no cooldown is running.",
4246
+ "deprecated": null,
4247
+ "params": [],
4248
+ "returns": null,
4249
+ "throws": null,
4250
+ "example": null,
4251
+ "defaultValue": null,
4252
+ "guides": []
4253
+ },
4254
+ {
4255
+ "id": "RewardedStatus.heldLimitReached",
4256
+ "name": "heldLimitReached",
4257
+ "anchor": "rewardedstatus-heldlimitreached",
4258
+ "group": "ads",
4259
+ "url": "https://gemarcade.com/developer/api/game#rewardedstatus-heldlimitreached",
4260
+ "signature": "readonly heldLimitReached: boolean",
4261
+ "documented": true,
4262
+ "summary": "Whether the player already holds the placement's `max_held` of the item it\ngrants. Spend one first.",
4263
+ "deprecated": null,
4264
+ "params": [],
4265
+ "returns": null,
4266
+ "throws": null,
4267
+ "example": null,
4268
+ "defaultValue": null,
4269
+ "guides": []
4270
+ },
4271
+ {
4272
+ "id": "RewardedStatus.resetsAt",
4273
+ "name": "resetsAt",
4274
+ "anchor": "rewardedstatus-resetsat",
4275
+ "group": "ads",
4276
+ "url": "https://gemarcade.com/developer/api/game#rewardedstatus-resetsat",
4277
+ "signature": "readonly resetsAt?: number",
4278
+ "documented": true,
4279
+ "summary": "When the daily count resets — the next 00:00 UTC — in epoch milliseconds.\nTake it from here rather than computing midnight in game code.\n\nABSENT when the platform gave no answer: no play token yet, a read that\nfailed or took longer than ten seconds, or a platform that does not serve\nthis read. Then the rest of the status reads not declared and not\ngrantable, and the reason went to your `onError` handler.",
4280
+ "deprecated": null,
4281
+ "params": [],
4282
+ "returns": null,
4283
+ "throws": null,
4284
+ "example": null,
4285
+ "defaultValue": null,
4286
+ "guides": []
4287
+ },
4112
4288
  {
4113
4289
  "id": "AnalyticsValue",
4114
4290
  "name": "AnalyticsValue",
@@ -6100,6 +6276,30 @@
6100
6276
  "defaultValue": null,
6101
6277
  "guides": []
6102
6278
  },
6279
+ {
6280
+ "id": "GemFrame.showPortraitBorders",
6281
+ "name": "showPortraitBorders",
6282
+ "anchor": "gemframe-showportraitborders",
6283
+ "group": "frame",
6284
+ "url": "https://gemarcade.com/developer/api/game#gemframe-showportraitborders",
6285
+ "signature": "showPortraitBorders(name: string | null): boolean",
6286
+ "documented": true,
6287
+ "summary": "Tell the arcade which of your store page's portrait borders to paint beside\nyour game, or `null` for none.\n\nPrefer `gem.showPortraitBorders`, which holds your choice until the bridge\nis up and paces a burst of changes for you. This one sends every call it\naccepts, only while connected, and only to an arcade that paints borders.",
6288
+ "deprecated": null,
6289
+ "params": [
6290
+ {
6291
+ "name": "name",
6292
+ "type": "string | null",
6293
+ "optional": false,
6294
+ "summary": null
6295
+ }
6296
+ ],
6297
+ "returns": "whether the message left the frame — `false` for a string that is\n not a border name (with a console warning), before the bridge is up, for\n an arcade that paints no borders, and past this frame's own limit.",
6298
+ "throws": null,
6299
+ "example": null,
6300
+ "defaultValue": null,
6301
+ "guides": []
6302
+ },
6103
6303
  {
6104
6304
  "id": "GemFrame.setWalletVisible",
6105
6305
  "name": "setWalletVisible",
@@ -6184,6 +6384,30 @@
6184
6384
  "defaultValue": null,
6185
6385
  "guides": []
6186
6386
  },
6387
+ {
6388
+ "id": "GemFrame.requestVibrate",
6389
+ "name": "requestVibrate",
6390
+ "anchor": "gemframe-requestvibrate",
6391
+ "group": "frame",
6392
+ "url": "https://gemarcade.com/developer/api/game#gemframe-requestvibrate",
6393
+ "signature": "requestVibrate(pattern: readonly number[]): boolean",
6394
+ "documented": true,
6395
+ "summary": "Ask the arcade to play a vibration pattern where this frame has no\n`navigator.vibrate` of its own. Nothing comes back.\n\nPrefer `gem.vibration.vibrate`, which calls `navigator.vibrate` itself\nwhere it exists, normalizes the pattern and caps its total length. This one\nchecks only the message's bounds, sends only while connected, and only to\nan arcade that handles vibration.",
6396
+ "deprecated": null,
6397
+ "params": [
6398
+ {
6399
+ "name": "pattern",
6400
+ "type": "readonly number[]",
6401
+ "optional": false,
6402
+ "summary": null
6403
+ }
6404
+ ],
6405
+ "returns": "whether the message left the frame — `false` before the bridge is\n up, for an arcade that does not handle vibration, and past this frame's\n own limit of 600 a minute.",
6406
+ "throws": null,
6407
+ "example": null,
6408
+ "defaultValue": null,
6409
+ "guides": []
6410
+ },
6187
6411
  {
6188
6412
  "id": "GemFrame.reportFrameStats",
6189
6413
  "name": "reportFrameStats",
@@ -6359,7 +6583,7 @@
6359
6583
  "url": "https://gemarcade.com/developer/api/game#friend-badges",
6360
6584
  "signature": "readonly badges: readonly PlayerBadge[]",
6361
6585
  "documented": true,
6362
- "summary": "The player's equipped badges, in display order. Always a list, and empty\nuntil platform achievements equip any.",
6586
+ "summary": "The player's equipped badges, at most two, in the order the player\narranged them. Always a list — empty when they wear none, or when their\nidentity is withheld — so draw what is there and key on `itemId`.",
6363
6587
  "deprecated": null,
6364
6588
  "params": [],
6365
6589
  "returns": null,
@@ -6444,7 +6668,7 @@
6444
6668
  "url": "https://gemarcade.com/developer/api/game#listfriendsoptions-limit",
6445
6669
  "signature": "readonly limit?: number",
6446
6670
  "documented": true,
6447
- "summary": "Friends per page — default 50, at most 200. A player holds at most 1000\nfriends, so five max-size pages is the whole list.",
6671
+ "summary": "Friends per page — default 50, at most 200. A player holds at most 1000\nfriends, so five max-size pages is the whole list. A larger value is\nclamped to 200; a value that is not a non-negative integer is refused with\n`invalid`.",
6448
6672
  "deprecated": null,
6449
6673
  "params": [],
6450
6674
  "returns": null,
@@ -6487,6 +6711,261 @@
6487
6711
  "defaultValue": null,
6488
6712
  "guides": []
6489
6713
  },
6714
+ {
6715
+ "id": "FriendChallenge",
6716
+ "name": "FriendChallenge",
6717
+ "anchor": "friendchallenge",
6718
+ "group": "friends",
6719
+ "url": "https://gemarcade.com/developer/api/game#friendchallenge",
6720
+ "signature": "interface FriendChallenge",
6721
+ "documented": true,
6722
+ "summary": "One friend challenge this player received, in THIS game on THIS channel.\n\nWhat the platform holds, not what the card said: there is no rendered line,\nbecause your game renders its own copy from [FriendChallenge.template](https://gemarcade.com/developer/api/game#friendchallenge-template)\nand [FriendChallenge.params](https://gemarcade.com/developer/api/game#friendchallenge-params). Who sent it is\n[FriendChallenge.from](https://gemarcade.com/developer/api/game#friendchallenge-from), resolved by the platform from its own records.",
6723
+ "deprecated": null,
6724
+ "params": [],
6725
+ "returns": null,
6726
+ "throws": null,
6727
+ "example": null,
6728
+ "defaultValue": null,
6729
+ "guides": []
6730
+ },
6731
+ {
6732
+ "id": "FriendChallenge.id",
6733
+ "name": "id",
6734
+ "anchor": "friendchallenge-id",
6735
+ "group": "friends",
6736
+ "url": "https://gemarcade.com/developer/api/game#friendchallenge-id",
6737
+ "signature": "readonly id: string",
6738
+ "documented": true,
6739
+ "summary": "The challenge's id — the value the Arcade puts in the `gemChallenge`\nlaunch key, and what [FriendsApi.markAnswered](https://gemarcade.com/developer/api/game#friendsapi-markanswered) takes. Opaque: compare\nit, store it, never parse it.",
6740
+ "deprecated": null,
6741
+ "params": [],
6742
+ "returns": null,
6743
+ "throws": null,
6744
+ "example": null,
6745
+ "defaultValue": null,
6746
+ "guides": []
6747
+ },
6748
+ {
6749
+ "id": "FriendChallenge.from",
6750
+ "name": "from",
6751
+ "anchor": "friendchallenge-from",
6752
+ "group": "friends",
6753
+ "url": "https://gemarcade.com/developer/api/game#friendchallenge-from",
6754
+ "signature": "readonly from: Friend",
6755
+ "documented": true,
6756
+ "summary": "Who sent it, with the same display identity a [Friend](https://gemarcade.com/developer/api/game#friend) row carries.\n\nThe ONLY name to show. Never read a name out of\n[FriendChallenge.params](https://gemarcade.com/developer/api/game#friendchallenge-params): a name in a param is one nobody checked.",
6757
+ "deprecated": null,
6758
+ "params": [],
6759
+ "returns": null,
6760
+ "throws": null,
6761
+ "example": null,
6762
+ "defaultValue": null,
6763
+ "guides": []
6764
+ },
6765
+ {
6766
+ "id": "FriendChallenge.template",
6767
+ "name": "template",
6768
+ "anchor": "friendchallenge-template",
6769
+ "group": "friends",
6770
+ "url": "https://gemarcade.com/developer/api/game#friendchallenge-template",
6771
+ "signature": "readonly template: string",
6772
+ "documented": true,
6773
+ "summary": "The `friend_challenge_templates[].key` it was sent with — which of your\ntemplates, so your game knows what was asked.",
6774
+ "deprecated": null,
6775
+ "params": [],
6776
+ "returns": null,
6777
+ "throws": null,
6778
+ "example": null,
6779
+ "defaultValue": null,
6780
+ "guides": []
6781
+ },
6782
+ {
6783
+ "id": "FriendChallenge.params",
6784
+ "name": "params",
6785
+ "anchor": "friendchallenge-params",
6786
+ "group": "friends",
6787
+ "url": "https://gemarcade.com/developer/api/game#friendchallenge-params",
6788
+ "signature": "readonly params: Readonly<Record<string, string | number>>",
6789
+ "documented": true,
6790
+ "summary": "The params exactly as sent: integers as numbers, date, enum and code\nparams as strings. The template constrained them when the challenge was\nsent, but the config may have changed since, so check each one against\nwhat your game expects before it does anything — as you would a launch\nkey.",
6791
+ "deprecated": null,
6792
+ "params": [],
6793
+ "returns": null,
6794
+ "throws": null,
6795
+ "example": null,
6796
+ "defaultValue": null,
6797
+ "guides": []
6798
+ },
6799
+ {
6800
+ "id": "FriendChallenge.createdAt",
6801
+ "name": "createdAt",
6802
+ "anchor": "friendchallenge-createdat",
6803
+ "group": "friends",
6804
+ "url": "https://gemarcade.com/developer/api/game#friendchallenge-createdat",
6805
+ "signature": "readonly createdAt: number",
6806
+ "documented": true,
6807
+ "summary": "When it was sent, in Unix seconds.",
6808
+ "deprecated": null,
6809
+ "params": [],
6810
+ "returns": null,
6811
+ "throws": null,
6812
+ "example": null,
6813
+ "defaultValue": null,
6814
+ "guides": []
6815
+ },
6816
+ {
6817
+ "id": "FriendChallenge.openedAt",
6818
+ "name": "openedAt",
6819
+ "anchor": "friendchallenge-openedat",
6820
+ "group": "friends",
6821
+ "url": "https://gemarcade.com/developer/api/game#friendchallenge-openedat",
6822
+ "signature": "readonly openedAt?: number",
6823
+ "documented": true,
6824
+ "summary": "When the player pressed Play on it in the Arcade, in Unix seconds. Absent\nuntil they have.",
6825
+ "deprecated": null,
6826
+ "params": [],
6827
+ "returns": null,
6828
+ "throws": null,
6829
+ "example": null,
6830
+ "defaultValue": null,
6831
+ "guides": []
6832
+ },
6833
+ {
6834
+ "id": "FriendChallenge.answeredAt",
6835
+ "name": "answeredAt",
6836
+ "anchor": "friendchallenge-answeredat",
6837
+ "group": "friends",
6838
+ "url": "https://gemarcade.com/developer/api/game#friendchallenge-answeredat",
6839
+ "signature": "readonly answeredAt?: number",
6840
+ "documented": true,
6841
+ "summary": "When a game marked it answered with [FriendsApi.markAnswered](https://gemarcade.com/developer/api/game#friendsapi-markanswered), in\nUnix seconds. Absent until one has — so present on a relaunch of a\nchallenge already played, which is how a replay is told from a first\nattempt.",
6842
+ "deprecated": null,
6843
+ "params": [],
6844
+ "returns": null,
6845
+ "throws": null,
6846
+ "example": null,
6847
+ "defaultValue": null,
6848
+ "guides": []
6849
+ },
6850
+ {
6851
+ "id": "FriendChallengesPage",
6852
+ "name": "FriendChallengesPage",
6853
+ "anchor": "friendchallengespage",
6854
+ "group": "friends",
6855
+ "url": "https://gemarcade.com/developer/api/game#friendchallengespage",
6856
+ "signature": "interface FriendChallengesPage",
6857
+ "documented": true,
6858
+ "summary": "One page of the friend challenges this player holds in this game.",
6859
+ "deprecated": null,
6860
+ "params": [],
6861
+ "returns": null,
6862
+ "throws": null,
6863
+ "example": null,
6864
+ "defaultValue": null,
6865
+ "guides": []
6866
+ },
6867
+ {
6868
+ "id": "FriendChallengesPage.challenges",
6869
+ "name": "challenges",
6870
+ "anchor": "friendchallengespage-challenges",
6871
+ "group": "friends",
6872
+ "url": "https://gemarcade.com/developer/api/game#friendchallengespage-challenges",
6873
+ "signature": "readonly challenges: readonly FriendChallenge[]",
6874
+ "documented": true,
6875
+ "summary": "The page's challenges, newest first, answered ones last.",
6876
+ "deprecated": null,
6877
+ "params": [],
6878
+ "returns": null,
6879
+ "throws": null,
6880
+ "example": null,
6881
+ "defaultValue": null,
6882
+ "guides": []
6883
+ },
6884
+ {
6885
+ "id": "FriendChallengesPage.nextCursor",
6886
+ "name": "nextCursor",
6887
+ "anchor": "friendchallengespage-nextcursor",
6888
+ "group": "friends",
6889
+ "url": "https://gemarcade.com/developer/api/game#friendchallengespage-nextcursor",
6890
+ "signature": "readonly nextCursor?: string",
6891
+ "documented": true,
6892
+ "summary": "Present exactly when another page exists — pass it back as\n[ListFriendChallengesOptions.cursor](https://gemarcade.com/developer/api/game#listfriendchallengesoptions-cursor) to continue. Opaque: mint\nnothing, store nothing, compare nothing.",
6893
+ "deprecated": null,
6894
+ "params": [],
6895
+ "returns": null,
6896
+ "throws": null,
6897
+ "example": null,
6898
+ "defaultValue": null,
6899
+ "guides": []
6900
+ },
6901
+ {
6902
+ "id": "ListFriendChallengesOptions",
6903
+ "name": "ListFriendChallengesOptions",
6904
+ "anchor": "listfriendchallengesoptions",
6905
+ "group": "friends",
6906
+ "url": "https://gemarcade.com/developer/api/game#listfriendchallengesoptions",
6907
+ "signature": "interface ListFriendChallengesOptions",
6908
+ "documented": true,
6909
+ "summary": "What [FriendsApi.inbox](https://gemarcade.com/developer/api/game#friendsapi-inbox) takes.",
6910
+ "deprecated": null,
6911
+ "params": [],
6912
+ "returns": null,
6913
+ "throws": null,
6914
+ "example": null,
6915
+ "defaultValue": null,
6916
+ "guides": []
6917
+ },
6918
+ {
6919
+ "id": "ListFriendChallengesOptions.limit",
6920
+ "name": "limit",
6921
+ "anchor": "listfriendchallengesoptions-limit",
6922
+ "group": "friends",
6923
+ "url": "https://gemarcade.com/developer/api/game#listfriendchallengesoptions-limit",
6924
+ "signature": "readonly limit?: number",
6925
+ "documented": true,
6926
+ "summary": "Challenges per page — default 50, at most 200. The platform keeps at most\n200 per player, so one max-size page is the whole inbox. A larger value is\nclamped to 200; a value that is not a non-negative integer is refused with\n`invalid`.",
6927
+ "deprecated": null,
6928
+ "params": [],
6929
+ "returns": null,
6930
+ "throws": null,
6931
+ "example": null,
6932
+ "defaultValue": null,
6933
+ "guides": []
6934
+ },
6935
+ {
6936
+ "id": "ListFriendChallengesOptions.cursor",
6937
+ "name": "cursor",
6938
+ "anchor": "listfriendchallengesoptions-cursor",
6939
+ "group": "friends",
6940
+ "url": "https://gemarcade.com/developer/api/game#listfriendchallengesoptions-cursor",
6941
+ "signature": "readonly cursor?: string",
6942
+ "documented": true,
6943
+ "summary": "The previous page's [FriendChallengesPage.nextCursor](https://gemarcade.com/developer/api/game#friendchallengespage-nextcursor), verbatim. A\ncursor the platform did not mint is refused with `invalid`.",
6944
+ "deprecated": null,
6945
+ "params": [],
6946
+ "returns": null,
6947
+ "throws": null,
6948
+ "example": null,
6949
+ "defaultValue": null,
6950
+ "guides": []
6951
+ },
6952
+ {
6953
+ "id": "ListFriendChallengesOptions.signal",
6954
+ "name": "signal",
6955
+ "anchor": "listfriendchallengesoptions-signal",
6956
+ "group": "friends",
6957
+ "url": "https://gemarcade.com/developer/api/game#listfriendchallengesoptions-signal",
6958
+ "signature": "readonly signal?: AbortSignal",
6959
+ "documented": true,
6960
+ "summary": "Abort the read.",
6961
+ "deprecated": null,
6962
+ "params": [],
6963
+ "returns": null,
6964
+ "throws": null,
6965
+ "example": null,
6966
+ "defaultValue": null,
6967
+ "guides": []
6968
+ },
6490
6969
  {
6491
6970
  "id": "FriendChallengeRequest",
6492
6971
  "name": "FriendChallengeRequest",
@@ -6801,7 +7280,7 @@
6801
7280
  "url": "https://gemarcade.com/developer/api/game#friendsapi",
6802
7281
  "signature": "class FriendsApi",
6803
7282
  "documented": true,
6804
- "summary": "Reading the signed-in player's accepted friends, and asking the Arcade to\nsend one of them a friend challenge.\n\nReached as `gem.friends`. Display identity only, and every accepted friend\nappears whether or not they have played this game — an invite-a-friend flow\nneeds exactly the ones who have not. The graph itself — requests, accepts,\nblocks — is managed on the Arcade, not here. So is a friend challenge's\nsend: [FriendsApi.challenge](https://gemarcade.com/developer/api/game#friendsapi-challenge) asks, and the Arcade confirms with the\nplayer and sends.\n\n```js\nlet page = await gem.friends.list();\nrender(page.friends);\nwhile (page.nextCursor) {\n page = await gem.friends.list({ cursor: page.nextCursor });\n render(page.friends);\n}\n```",
7283
+ "summary": "Reading the signed-in player's accepted friends, asking the Arcade to send\none of them a friend challenge, and reading the friend challenges this\nplayer received in this game.\n\nReached as `gem.friends`. Display identity only, and every accepted friend\nappears whether or not they have played this game — an invite-a-friend flow\nneeds exactly the ones who have not. The graph itself — requests, accepts,\nblocks — is managed on the Arcade, not here. So is a friend challenge's\nsend: [FriendsApi.challenge](https://gemarcade.com/developer/api/game#friendsapi-challenge) asks, and the Arcade confirms with the\nplayer and sends. The receiving side is [FriendsApi.launchedChallenge](https://gemarcade.com/developer/api/game#friendsapi-launchedchallenge)\n(the challenge this frame was launched with), [FriendsApi.inbox](https://gemarcade.com/developer/api/game#friendsapi-inbox) and\n[FriendsApi.markAnswered](https://gemarcade.com/developer/api/game#friendsapi-markanswered).\n\n```js\nlet page = await gem.friends.list();\nrender(page.friends);\nwhile (page.nextCursor) {\n page = await gem.friends.list({ cursor: page.nextCursor });\n render(page.friends);\n}\n```",
6805
7284
  "deprecated": null,
6806
7285
  "params": [],
6807
7286
  "returns": null,
@@ -6829,7 +7308,7 @@
6829
7308
  }
6830
7309
  ],
6831
7310
  "returns": null,
6832
- "throws": "`invalid` when the cursor is not one a previous page minted,\n `forbidden` in a context with no player — this is a player surface, and\n a game server has no friend list.",
7311
+ "throws": "`invalid` when the cursor is not one a previous page minted or\n `limit` is not a non-negative integer,\n `forbidden` in a context with no player — this is a player surface, and\n a game server has no friend list.",
6833
7312
  "example": null,
6834
7313
  "defaultValue": null,
6835
7314
  "guides": []
@@ -6858,6 +7337,78 @@
6858
7337
  "defaultValue": null,
6859
7338
  "guides": []
6860
7339
  },
7340
+ {
7341
+ "id": "FriendsApi.inbox",
7342
+ "name": "inbox",
7343
+ "anchor": "friendsapi-inbox",
7344
+ "group": "friends",
7345
+ "url": "https://gemarcade.com/developer/api/game#friendsapi-inbox",
7346
+ "signature": "inbox(options?: ListFriendChallengesOptions): Promise<FriendChallengesPage>",
7347
+ "documented": true,
7348
+ "summary": "Read one page of the friend challenges this player holds in THIS game on\nTHIS channel — in every state. Answered challenges are INCLUDED, and sort\nafter the rest; within each part, newest first.\n\nPass the result's `nextCursor` back as `cursor` until it stops arriving.\nA guest holds no challenges, so a guest's page is empty rather than a\nrefusal — build the screen for the empty state, not around an error.\n\nFor the one challenge this frame was launched with, use\n[FriendsApi.launchedChallenge](https://gemarcade.com/developer/api/game#friendsapi-launchedchallenge) rather than searching this list.",
7349
+ "deprecated": null,
7350
+ "params": [
7351
+ {
7352
+ "name": "options",
7353
+ "type": "ListFriendChallengesOptions",
7354
+ "optional": false,
7355
+ "summary": null
7356
+ }
7357
+ ],
7358
+ "returns": null,
7359
+ "throws": "`invalid` when the cursor is not one a previous page minted or\n `limit` is not a non-negative integer,\n `forbidden` in a context with no player of this game — a game server,\n or a token that is not a game-scoped player token.",
7360
+ "example": "```ts\nconst page = await gem.friends.inbox();\nfor (const c of page.challenges) {\n if (c.template !== 'beat_code_jar') continue;\n renderChallengeRow(c.from, c.params, c.answeredAt !== undefined);\n}\n```",
7361
+ "defaultValue": null,
7362
+ "guides": []
7363
+ },
7364
+ {
7365
+ "id": "FriendsApi.launchedChallenge",
7366
+ "name": "launchedChallenge",
7367
+ "anchor": "friendsapi-launchedchallenge",
7368
+ "group": "friends",
7369
+ "url": "https://gemarcade.com/developer/api/game#friendsapi-launchedchallenge",
7370
+ "signature": "launchedChallenge(options?: { signal?: AbortSignal }): Promise<FriendChallenge | null>",
7371
+ "documented": true,
7372
+ "summary": "The friend challenge this frame was launched with, or `null`.\n\nWhen a player opens a friend challenge in the Arcade, the Arcade launches\nyour game with the challenge's params as launch keys plus its own\n`gemChallenge` key. This reads that key — never parse it yourself — and\nlooks the challenge up, so you get the sender from the platform's records\nand the params as they were sent.\n\nThe key is read ONCE, when `createGem` runs, so cleaning your URL with\n`history.replaceState` afterwards changes nothing here.\n\nRESOLVES `null`, never rejects, for every launch that is not this\nplayer's challenge in this game: no `gemChallenge` key, a value that is\nnot a challenge id, a challenge that is not this player's (a link\nsomeone shared), one from another game or channel, and every guest\nlaunch. Treat `null` as an ordinary launch.\n\nA RELAUNCH REPLAYS. Pressing Play again in the Arcade relaunches the\nsame challenge, so the same one comes back; it carries\n[FriendChallenge.answeredAt](https://gemarcade.com/developer/api/game#friendchallenge-answeredat) once a game has called\n[FriendsApi.markAnswered](https://gemarcade.com/developer/api/game#friendsapi-markanswered), which is how a replay is told from a\nfirst attempt. Asks the platform on every call, so `answeredAt` is\ncurrent.",
7373
+ "deprecated": null,
7374
+ "params": [
7375
+ {
7376
+ "name": "options",
7377
+ "type": "{ signal?: AbortSignal }",
7378
+ "optional": false,
7379
+ "summary": null
7380
+ }
7381
+ ],
7382
+ "returns": null,
7383
+ "throws": "What any platform read throws: `forbidden` in a context with no\n player of this game, `unavailable` and `rate_limited` when the platform\n cannot answer now — and `not_found` for a 404 that is NOT the platform's\n uniform challenge answer (no `inbox_item_not_found` code), which is a\n platform that does not serve this read rather than an ordinary launch.",
7384
+ "example": "```ts\nconst challenge = await gem.friends.launchedChallenge();\nif (challenge?.template === 'beat_code_jar') {\n // Check every param before it does anything: the config may have changed since.\n const jar = challenge.params.jar;\n if (typeof jar === 'string' && /^[A-Z0-9]{7}$/.test(jar)) {\n startJar(jar, { from: challenge.from, replay: challenge.answeredAt !== undefined });\n }\n}\n```",
7385
+ "defaultValue": null,
7386
+ "guides": []
7387
+ },
7388
+ {
7389
+ "id": "FriendsApi.markAnswered",
7390
+ "name": "markAnswered",
7391
+ "anchor": "friendsapi-markanswered",
7392
+ "group": "friends",
7393
+ "url": "https://gemarcade.com/developer/api/game#friendsapi-markanswered",
7394
+ "signature": "markAnswered(id: string): Promise<void>",
7395
+ "documented": true,
7396
+ "summary": "Mark one of this player's friend challenges answered — the player has\nplayed it. Call it when your game has finished the attempt the challenge\nasked for, win or lose.\n\nIdempotent: a second call changes nothing, and a challenge keeps the\nfirst time it was answered. The sender is told nothing — there is no read\nreceipt — so this is your game's own record, which\n[FriendChallenge.answeredAt](https://gemarcade.com/developer/api/game#friendchallenge-answeredat) reads back.",
7397
+ "deprecated": null,
7398
+ "params": [
7399
+ {
7400
+ "name": "id",
7401
+ "type": "string",
7402
+ "optional": false,
7403
+ "summary": "A [FriendChallenge.id](https://gemarcade.com/developer/api/game#friendchallenge-id), from\n [FriendsApi.launchedChallenge](https://gemarcade.com/developer/api/game#friendsapi-launchedchallenge) or [FriendsApi.inbox](https://gemarcade.com/developer/api/game#friendsapi-inbox)."
7404
+ }
7405
+ ],
7406
+ "returns": null,
7407
+ "throws": "`not_found` when no challenge of this player's in this game and\n channel has that id — one answer for unknown and someone else's;\n `invalid` when `id` cannot be a challenge id at all, before anything is\n sent; `forbidden` in a context with no player of this game;\n `rate_limited` past the platform's per-player budget.",
7408
+ "example": null,
7409
+ "defaultValue": null,
7410
+ "guides": []
7411
+ },
6861
7412
  {
6862
7413
  "id": "FunctionInvokeOptions",
6863
7414
  "name": "FunctionInvokeOptions",
@@ -7911,7 +8462,7 @@
7911
8462
  "url": "https://gemarcade.com/developer/api/game#gemplayer-badges",
7912
8463
  "signature": "readonly badges: readonly PlayerBadge[]",
7913
8464
  "documented": true,
7914
- "summary": "The player's equipped badges, in display order. Always a list, and empty\nuntil platform achievements equip any.",
8465
+ "summary": "The player's equipped badges, at most two, in the order the player\narranged them. Always a list — empty when they wear none, or when their\nidentity is withheld — so draw what is there and key on `itemId`.",
7915
8466
  "deprecated": null,
7916
8467
  "params": [],
7917
8468
  "returns": null,
@@ -8834,7 +9385,7 @@
8834
9385
  "url": "https://gemarcade.com/developer/api/game#gem-ads",
8835
9386
  "signature": "readonly ads: AdsApi",
8836
9387
  "documented": true,
8837
- "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.",
9388
+ "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.",
8838
9389
  "deprecated": null,
8839
9390
  "params": [],
8840
9391
  "returns": null,
@@ -8895,6 +9446,29 @@
8895
9446
  }
8896
9447
  ]
8897
9448
  },
9449
+ {
9450
+ "id": "Gem.vibration",
9451
+ "name": "vibration",
9452
+ "anchor": "gem-vibration",
9453
+ "group": "start",
9454
+ "url": "https://gemarcade.com/developer/api/game#gem-vibration",
9455
+ "signature": "readonly vibration: VibrationApi",
9456
+ "documented": true,
9457
+ "summary": "Vibrate the player's device — call this instead of `navigator.vibrate`,\nwhich does nothing on any iPhone. It takes the same pattern and returns the\nsame boolean, calls `navigator.vibrate` itself where that works, and asks\nthe arcade to vibrate where it does not.\n\nRead [VibrationApi.available](https://gemarcade.com/developer/api/game#vibrationapi-available) live, after `awaitBridge()`, to decide\nwhether to show your game's vibration toggle — on an iPhone it is `false`\nuntil the bridge connects — and check that toggle before every call:\n\n```js\nawait gem.awaitBridge();\nvibrationRow.hidden = !gem.vibration.available;\n\nfunction onHit() {\n if (settings.vibration) gem.vibration.vibrate(30);\n}\n```\n\n- **Never call `navigator.vibrate`**, guarded or not, and never fall back\n to it: this already does.\n- **Nothing vibrates before the player's first tap** in your game; the\n call returns `false` until then.\n- **Stop on pause** with `gem.vibration.vibrate(0)`.",
9458
+ "deprecated": null,
9459
+ "params": [],
9460
+ "returns": null,
9461
+ "throws": null,
9462
+ "example": null,
9463
+ "defaultValue": null,
9464
+ "guides": [
9465
+ {
9466
+ "slug": "vibration",
9467
+ "anchor": null,
9468
+ "url": "https://gemarcade.com/developer/docs/vibration"
9469
+ }
9470
+ ]
9471
+ },
8898
9472
  {
8899
9473
  "id": "Gem.analytics",
8900
9474
  "name": "analytics",
@@ -8983,7 +9557,7 @@
8983
9557
  "url": "https://gemarcade.com/developer/api/game#gem-friends",
8984
9558
  "signature": "readonly friends: FriendsApi",
8985
9559
  "documented": true,
8986
- "summary": "The signed-in player's accepted friends — display identity only,\nread-only, paginated — and the one ask that reaches past them:\n[FriendsApi.challenge](https://gemarcade.com/developer/api/game#friendsapi-challenge), which asks the Arcade to send a friend a\nchallenge after the player confirms it in the Arcade's own sheet.\n\nEvery accepted friend appears whether or not they have played this game;\nan invite-a-friend flow needs exactly the ones who have not. The graph\nitself — requests, accepts, blocks, presence — stays on the Arcade: there\nis nothing here to mutate and nothing that says who is online.\n\nA player surface only. In a context with no player — a game server — the\nplatform refuses the read; a server has no friend list.",
9560
+ "summary": "The signed-in player's accepted friends — display identity only,\nread-only, paginated — and the one ask that reaches past them:\n[FriendsApi.challenge](https://gemarcade.com/developer/api/game#friendsapi-challenge), which asks the Arcade to send a friend a\nchallenge after the player confirms it in the Arcade's own sheet. The\nreceiving side reads the challenges this player holds in this game:\n[FriendsApi.launchedChallenge](https://gemarcade.com/developer/api/game#friendsapi-launchedchallenge) for the one this frame was launched\nwith, [FriendsApi.inbox](https://gemarcade.com/developer/api/game#friendsapi-inbox) for all of them, and\n[FriendsApi.markAnswered](https://gemarcade.com/developer/api/game#friendsapi-markanswered) once one has been played.\n\nEvery accepted friend appears whether or not they have played this game;\nan invite-a-friend flow needs exactly the ones who have not. The graph\nitself — requests, accepts, blocks, presence — stays on the Arcade: there\nis nothing here to mutate but a challenge's own answered mark, and nothing\nthat says who is online.\n\nA player surface only. In a context with no player — a game server — the\nplatform refuses the read; a server has no friend list.",
8987
9561
  "deprecated": null,
8988
9562
  "params": [],
8989
9563
  "returns": null,
@@ -9166,6 +9740,30 @@
9166
9740
  "defaultValue": null,
9167
9741
  "guides": []
9168
9742
  },
9743
+ {
9744
+ "id": "Gem.showPortraitBorders",
9745
+ "name": "showPortraitBorders",
9746
+ "anchor": "gem-showportraitborders",
9747
+ "group": "start",
9748
+ "url": "https://gemarcade.com/developer/api/game#gem-showportraitborders",
9749
+ "signature": "showPortraitBorders(name: string | null): void",
9750
+ "documented": true,
9751
+ "summary": "Choose which of your store page's **portrait borders** the arcade paints on\neither side of your game, or `null` for none.\n\nOn a desktop browser, the arcade's desktop app or a television, the arcade\nshows a portrait-only game as a centred portrait column. With borders\ndeclared on your store page (`media.portrait_borders`), it paints one of\nthose images beside the column instead of its own bars. Call this at\nstart-up and on every theme change, with the border that goes with what is\non screen:\n\n```js\nconst BORDERS = new Set(['meadow', 'alpine']); // the names your store page declares\n\nfunction applyTheme(id) {\n document.body.dataset.theme = id;\n gem.showPortraitBorders(BORDERS.has(id) ? id : null); // null: a theme with no art\n}\n```\n\nUntil your first call the arcade shows the page's first border. Whether\nyour game is letterboxed at all is decided by your store page's\n`orientations`, never by this call, so call it unconditionally: before the\nconnection exists your choice is held and sent when it connects, a burst of\nchanges (a picker previewing on hover) reaches the arcade as its final\nvalue, and an arcade that paints no borders is simply not told. The arcade\nowns the change itself — when and how the image swaps — and sends no\nacknowledgement.\n\nA name is 1 to 40 characters of `a-z`, `0-9`, `_` and `-`, starting with a\nletter or digit — the store page's own rule. Anything else is refused with\na console warning and never sent; a valid name your page does not declare\nis ignored by the arcade, which keeps the image it is showing.",
9752
+ "deprecated": null,
9753
+ "params": [
9754
+ {
9755
+ "name": "name",
9756
+ "type": "string | null",
9757
+ "optional": false,
9758
+ "summary": null
9759
+ }
9760
+ ],
9761
+ "returns": null,
9762
+ "throws": null,
9763
+ "example": null,
9764
+ "defaultValue": null,
9765
+ "guides": []
9766
+ },
9169
9767
  {
9170
9768
  "id": "Gem.cueReward",
9171
9769
  "name": "cueReward",
@@ -10411,7 +11009,7 @@
10411
11009
  "url": "https://gemarcade.com/developer/api/game#playerbadge",
10412
11010
  "signature": "interface PlayerBadge",
10413
11011
  "documented": true,
10414
- "summary": "One badge a player has equipped — an icon the platform shows beside their\nname.\n\nAlways an empty list today; platform achievements fill it with at most two.",
11012
+ "summary": "One badge a player has equipped — an icon the platform shows beside their\nname.\n\nA player wears at most two, in the order they arranged them. Each is an icon\nthey own — an achievement in any game may have awarded it.",
10415
11013
  "deprecated": null,
10416
11014
  "params": [],
10417
11015
  "returns": null,
@@ -10581,7 +11179,7 @@
10581
11179
  "url": "https://gemarcade.com/developer/api/game#playeridentity-badges",
10582
11180
  "signature": "readonly badges: readonly PlayerBadge[]",
10583
11181
  "documented": true,
10584
- "summary": "The player's equipped badges, in display order. Always a list, and empty\nuntil platform achievements equip any.",
11182
+ "summary": "The player's equipped badges, at most two, in the order the player\narranged them. Always a list — empty when they wear none, or when their\nidentity is withheld — so draw what is there and key on `itemId`.",
10585
11183
  "deprecated": null,
10586
11184
  "params": [],
10587
11185
  "returns": null,
@@ -13221,6 +13819,64 @@
13221
13819
  "defaultValue": null,
13222
13820
  "guides": []
13223
13821
  },
13822
+ {
13823
+ "id": "VibrationApi",
13824
+ "name": "VibrationApi",
13825
+ "anchor": "vibrationapi",
13826
+ "group": "vibration",
13827
+ "url": "https://gemarcade.com/developer/api/game#vibrationapi",
13828
+ "signature": "interface VibrationApi",
13829
+ "documented": true,
13830
+ "summary": "`gem.vibration` — vibrate the player's device, on every platform that can.\nCall it instead of `navigator.vibrate`, which does nothing on any iPhone.",
13831
+ "deprecated": null,
13832
+ "params": [],
13833
+ "returns": null,
13834
+ "throws": null,
13835
+ "example": null,
13836
+ "defaultValue": null,
13837
+ "guides": []
13838
+ },
13839
+ {
13840
+ "id": "VibrationApi.available",
13841
+ "name": "available",
13842
+ "anchor": "vibrationapi-available",
13843
+ "group": "vibration",
13844
+ "url": "https://gemarcade.com/developer/api/game#vibrationapi-available",
13845
+ "signature": "readonly available: boolean",
13846
+ "documented": true,
13847
+ "summary": "Whether this device can vibrate for your game: decide from this whether to\nshow your game's vibration toggle. Read it live, every time — after\n`awaitBridge()`, and again when your settings screen opens — never once at\nboot: on an iPhone it is `false` until the bridge connects.\n\n`false` on a desktop with a mouse, in iOS Safari, in Firefox, before the\nbridge is up where vibration goes through the arcade, and after the\nsession ends there.",
13848
+ "deprecated": null,
13849
+ "params": [],
13850
+ "returns": null,
13851
+ "throws": null,
13852
+ "example": null,
13853
+ "defaultValue": null,
13854
+ "guides": []
13855
+ },
13856
+ {
13857
+ "id": "VibrationApi.vibrate",
13858
+ "name": "vibrate",
13859
+ "anchor": "vibrationapi-vibrate",
13860
+ "group": "vibration",
13861
+ "url": "https://gemarcade.com/developer/api/game#vibrationapi-vibrate",
13862
+ "signature": "vibrate(pattern: number | readonly number[]): boolean",
13863
+ "documented": true,
13864
+ "summary": "Vibrate: takes what `navigator.vibrate` takes and returns what it returns.\n`pattern` is a duration in milliseconds, or an array of alternating\nvibration and pause durations, starting with a vibration. `0` or `[]`\nstops a pattern that is still playing, and every call replaces it.\n\nThe pattern is capped at 99 entries of at most 10 000 ms each, and at\n10 000 ms in all: a longer one is cut short, not refused.\n\nCheck your game's own vibration toggle before every call. Returns `false`,\nand vibrates nothing, when [VibrationApi.available](https://gemarcade.com/developer/api/game#vibrationapi-available) is false, before\nthe player's first tap or key press in your game, for a pattern with a\nnegative or non-finite entry, and past 600 calls a minute; each of the last\nthree says so once a session through your `log`. `true` means the pattern\nwas accepted, never that a motor moved.",
13865
+ "deprecated": null,
13866
+ "params": [
13867
+ {
13868
+ "name": "pattern",
13869
+ "type": "number | readonly number[]",
13870
+ "optional": false,
13871
+ "summary": null
13872
+ }
13873
+ ],
13874
+ "returns": null,
13875
+ "throws": null,
13876
+ "example": null,
13877
+ "defaultValue": null,
13878
+ "guides": []
13879
+ },
13224
13880
  {
13225
13881
  "id": "LeaderboardPeriod.id",
13226
13882
  "name": "id",
@@ -13450,7 +14106,7 @@
13450
14106
  "url": "https://gemarcade.com/developer/api/game#leaderboardentry-badges",
13451
14107
  "signature": "readonly badges: readonly PlayerBadge[]",
13452
14108
  "documented": true,
13453
- "summary": "The player's equipped badges, in display order. Always a list, and empty\nuntil platform achievements equip any.",
14109
+ "summary": "The player's equipped badges, at most two, in the order the player\narranged them. Always a list — empty when they wear none, or when their\nidentity is withheld — so draw what is there and key on `itemId`.",
13454
14110
  "deprecated": null,
13455
14111
  "params": [],
13456
14112
  "returns": null,
@@ -15323,7 +15979,7 @@
15323
15979
  "url": "https://gemarcade.com/developer/api/game#playerprofile-badges",
15324
15980
  "signature": "readonly badges: readonly PlayerBadge[]",
15325
15981
  "documented": true,
15326
- "summary": "The player's equipped badges, in display order. Always a list, and empty\nuntil platform achievements equip any.",
15982
+ "summary": "The player's equipped badges, at most two, in the order the player\narranged them. Always a list — empty when they wear none, or when their\nidentity is withheld — so draw what is there and key on `itemId`.",
15327
15983
  "deprecated": null,
15328
15984
  "params": [],
15329
15985
  "returns": null,
@@ -18018,7 +18674,7 @@
18018
18674
  "url": "https://gemarcade.com/developer/api/game#roommember-badges",
18019
18675
  "signature": "readonly badges: readonly PlayerBadge[]",
18020
18676
  "documented": true,
18021
- "summary": "The player's equipped badges, in display order. Always a list, and empty\nuntil platform achievements equip any.",
18677
+ "summary": "The player's equipped badges, at most two, in the order the player\narranged them. Always a list — empty when they wear none, or when their\nidentity is withheld — so draw what is there and key on `itemId`.",
18022
18678
  "deprecated": null,
18023
18679
  "params": [],
18024
18680
  "returns": null,
@@ -19169,7 +19825,7 @@
19169
19825
  "url": "https://gemarcade.com/developer/api/game#showcasefriendentry-badges",
19170
19826
  "signature": "readonly badges: readonly PlayerBadge[]",
19171
19827
  "documented": true,
19172
- "summary": "The player's equipped badges, in display order. Always a list, and empty\nuntil platform achievements equip any.",
19828
+ "summary": "The player's equipped badges, at most two, in the order the player\narranged them. Always a list — empty when they wear none, or when their\nidentity is withheld — so draw what is there and key on `itemId`.",
19173
19829
  "deprecated": null,
19174
19830
  "params": [],
19175
19831
  "returns": null,