@indiegems/gem-web-sdk 8.0.0 → 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": "8.0.0"
5
+ "version": "8.1.0"
6
6
  },
7
7
  "entries": [
8
8
  {
@@ -375,6 +375,29 @@
375
375
  "defaultValue": null,
376
376
  "guides": []
377
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
+ },
378
401
  {
379
402
  "id": "GameConfig.version",
380
403
  "name": "version",
@@ -4023,7 +4046,7 @@
4023
4046
  "url": "https://gemarcade.com/developer/api/game#adoutcomekind",
4024
4047
  "signature": "type AdOutcomeKind = \"shown\" | \"no_ad\" | \"unavailable\"",
4025
4048
  "documented": true,
4026
- "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.",
4027
4050
  "deprecated": null,
4028
4051
  "params": [],
4029
4052
  "returns": null,
@@ -4113,6 +4136,155 @@
4113
4136
  }
4114
4137
  ]
4115
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
+ },
4116
4288
  {
4117
4289
  "id": "AnalyticsValue",
4118
4290
  "name": "AnalyticsValue",
@@ -6104,6 +6276,30 @@
6104
6276
  "defaultValue": null,
6105
6277
  "guides": []
6106
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
+ },
6107
6303
  {
6108
6304
  "id": "GemFrame.setWalletVisible",
6109
6305
  "name": "setWalletVisible",
@@ -6387,7 +6583,7 @@
6387
6583
  "url": "https://gemarcade.com/developer/api/game#friend-badges",
6388
6584
  "signature": "readonly badges: readonly PlayerBadge[]",
6389
6585
  "documented": true,
6390
- "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`.",
6391
6587
  "deprecated": null,
6392
6588
  "params": [],
6393
6589
  "returns": null,
@@ -8266,7 +8462,7 @@
8266
8462
  "url": "https://gemarcade.com/developer/api/game#gemplayer-badges",
8267
8463
  "signature": "readonly badges: readonly PlayerBadge[]",
8268
8464
  "documented": true,
8269
- "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`.",
8270
8466
  "deprecated": null,
8271
8467
  "params": [],
8272
8468
  "returns": null,
@@ -9189,7 +9385,7 @@
9189
9385
  "url": "https://gemarcade.com/developer/api/game#gem-ads",
9190
9386
  "signature": "readonly ads: AdsApi",
9191
9387
  "documented": true,
9192
- "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.",
9193
9389
  "deprecated": null,
9194
9390
  "params": [],
9195
9391
  "returns": null,
@@ -9544,6 +9740,30 @@
9544
9740
  "defaultValue": null,
9545
9741
  "guides": []
9546
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
+ },
9547
9767
  {
9548
9768
  "id": "Gem.cueReward",
9549
9769
  "name": "cueReward",
@@ -10789,7 +11009,7 @@
10789
11009
  "url": "https://gemarcade.com/developer/api/game#playerbadge",
10790
11010
  "signature": "interface PlayerBadge",
10791
11011
  "documented": true,
10792
- "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.",
10793
11013
  "deprecated": null,
10794
11014
  "params": [],
10795
11015
  "returns": null,
@@ -10959,7 +11179,7 @@
10959
11179
  "url": "https://gemarcade.com/developer/api/game#playeridentity-badges",
10960
11180
  "signature": "readonly badges: readonly PlayerBadge[]",
10961
11181
  "documented": true,
10962
- "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`.",
10963
11183
  "deprecated": null,
10964
11184
  "params": [],
10965
11185
  "returns": null,
@@ -13886,7 +14106,7 @@
13886
14106
  "url": "https://gemarcade.com/developer/api/game#leaderboardentry-badges",
13887
14107
  "signature": "readonly badges: readonly PlayerBadge[]",
13888
14108
  "documented": true,
13889
- "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`.",
13890
14110
  "deprecated": null,
13891
14111
  "params": [],
13892
14112
  "returns": null,
@@ -15759,7 +15979,7 @@
15759
15979
  "url": "https://gemarcade.com/developer/api/game#playerprofile-badges",
15760
15980
  "signature": "readonly badges: readonly PlayerBadge[]",
15761
15981
  "documented": true,
15762
- "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`.",
15763
15983
  "deprecated": null,
15764
15984
  "params": [],
15765
15985
  "returns": null,
@@ -18454,7 +18674,7 @@
18454
18674
  "url": "https://gemarcade.com/developer/api/game#roommember-badges",
18455
18675
  "signature": "readonly badges: readonly PlayerBadge[]",
18456
18676
  "documented": true,
18457
- "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`.",
18458
18678
  "deprecated": null,
18459
18679
  "params": [],
18460
18680
  "returns": null,
@@ -19605,7 +19825,7 @@
19605
19825
  "url": "https://gemarcade.com/developer/api/game#showcasefriendentry-badges",
19606
19826
  "signature": "readonly badges: readonly PlayerBadge[]",
19607
19827
  "documented": true,
19608
- "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`.",
19609
19829
  "deprecated": null,
19610
19830
  "params": [],
19611
19831
  "returns": null,
package/docs/changelog.md CHANGED
@@ -16,6 +16,21 @@ Its entries are grouped under up to five headings:
16
16
  - **Rebuild**: a type changed. Recompile against the new version; code that compiles today keeps compiling unless the entry says otherwise.
17
17
  - **New capability**: something a game can now do. Nothing changes unless you use it.
18
18
 
19
+ ## 8.1.0
20
+
21
+ Upgrading from 8.0.0. Impact: required.
22
+
23
+ ### Rebuild
24
+
25
+ - The list of messages a frame sends to its parent, the FrameMessage type and the GemCapability type each gained a portrait borders member. Recompiling is enough for most games. If your code handles every FrameMessage or GemCapability member exhaustively, add a case for the new one so it keeps compiling.
26
+
27
+ ### New capability
28
+
29
+ - A game can now ask whether a rewarded placement could pay right now, before it draws its reward button, by calling AdsApi.rewardedStatus with the placement. The answer is a RewardedStatus saying whether the placement is declared, whether it is grantable now, how many grants remain today, when it is next eligible, whether the held limit is reached, and when the allowance resets. Times are in epoch milliseconds. The call never rejects. A failed read, a missing play token or a bad placement resolves as not declared and not grantable, and the read gives up after ten seconds. A placement only counts as grantable when the Arcade you run in supports ads. Failures are reported once per kind through your error handler.
30
+ - A game can now choose which border image from its store page the Arcade paints beside it in portrait by calling Gem.showPortraitBorders with the image's name. The name is a plain string of at most 40 characters, and one that fails the store page's naming rule is refused whole with a console warning. The choice is held until the bridge connects, sent at most once a second with the latest choice winning, and not resent if it matches what was last posted. If the Arcade does not advertise the portraitBorders capability, nothing is sent and nothing is raised.
31
+ - If you embed a game in your own frame, GemFrame.showPortraitBorders lets you send the same border choice, or clear it. It sends only while connected and only to a parent that advertised the portraitBorders capability, and it applies the same name rule as the game side.
32
+ - If you host games, you can now pass onPortraitBorders in your host options to receive the border name a game chose. The new GemPortraitBorders message carries it, and validateGemPortraitBorders checks a message you receive. The host re-checks the name on receipt and drops one that no store page could declare before your callback sees it.
33
+
19
34
  ## 8.0.0
20
35
 
21
36
  *Written from the API facts alone; no summary was generated for this release.*
@@ -13,8 +13,8 @@ const { outcome } = await gem.ads.requestAd({
13
13
  startNextLevel();
14
14
  ```
15
15
 
16
- That is the whole surface. The rest of this page is the four things a game gets wrong when nobody
17
- tells it.
16
+ That is the whole surface, apart from one read that tells a rewarded button whether it could pay
17
+ (§3). The rest of this page is the four things a game gets wrong when nobody tells it.
18
18
 
19
19
  ## 1. The answer is usually no, and no is ordinary
20
20
 
@@ -80,6 +80,36 @@ your player is already sitting in, and it is the whole of what changes on the da
80
80
  rebuild. [`inventory-and-catalogs.md` §6](inventory-and-catalogs.md#6-when-to-re-read) is the
81
81
  general form of this.
82
82
 
83
+ **Whether a rewarded button could pay right now.** A rewarded placement is a button the player
84
+ presses, so you need to know whether to draw it. `gem.ads.rewardedStatus(placement)` reads one
85
+ placement's state from **your own** `rewarded_placements` config — its daily cap, cooldown and
86
+ `max_held` — applied to this player's use of it:
87
+
88
+ ```js
89
+ const status = await gem.ads.rewardedStatus('run-banked');
90
+ bankButton.disabled = !status.grantable;
91
+ ```
92
+
93
+ | field | what it says |
94
+ |---|---|
95
+ | `declared` | the config promoted to the channel being played names this placement |
96
+ | `grantable` | pressing the button could pay right now — **not** that an ad will play |
97
+ | `remainingToday` | grants left today under the daily cap; absent when the placement has none |
98
+ | `nextEligibleAt` | when a running cooldown ends, in epoch milliseconds; absent when none runs |
99
+ | `heldLimitReached` | the player already holds the placement's `max_held` — spend one first |
100
+ | `resetsAt` | when today's count resets, the next 00:00 UTC, in epoch milliseconds |
101
+
102
+ **`grantable` is not a promise.** A player can press a `grantable` button and get `no_ad`, for
103
+ reasons your game is not told. Read that `no_ad` like any other (§1) — never guess a reason — and
104
+ re-read your inventory whatever the ask settled as. The status grants nothing and reserves
105
+ nothing.
106
+
107
+ It never rejects, like the rest of `gem.ads`. Without a play token, when the read fails or takes
108
+ longer than ten seconds, or for a placement outside the grammar below, it answers not declared and
109
+ not grantable with no `resetsAt`, and the reason goes to your `onError` handler. It is a read of
110
+ the platform, so call it when you draw the button — on a results or death screen — and not every
111
+ frame, and draw the rest of the screen first rather than holding it on the answer.
112
+
83
113
  ## 4. The Arcade pauses your game for you
84
114
 
85
115
  While a panel is up, your game is paused the same way it is paused when a player switches tabs —
@@ -130,6 +160,12 @@ schedule that does not depend on which branch produced it, so there is nothing i
130
160
  read either. (`shown` is not held back — how long that one takes is how long your player spent
131
161
  looking at a panel.)
132
162
 
163
+ The one thing you can read is your own economy, and never from the answer:
164
+ `gem.ads.rewardedStatus` reports **your own declared** rewarded caps as the platform holds them —
165
+ the platform can hold a placement's daily cap below what you declared, never above — so you can
166
+ tell when a rewarded `no_ad` was your own cap. It never reports anything about the player, the
167
+ platform's spacing between offers, or any limit that spans games.
168
+
133
169
  ## If you do nothing
134
170
 
135
171
  Nothing happens. A game that never calls `gem.ads.requestAd` is never interrupted, and the call is
@@ -50,7 +50,7 @@ either way.
50
50
  ```jsonc
51
51
  // package.json
52
52
  "dependencies": {
53
- "@indiegems/gem-web-sdk": "8.0.0"
53
+ "@indiegems/gem-web-sdk": "8.1.0"
54
54
  }
55
55
  ```
56
56
 
@@ -355,7 +355,8 @@ What reaches the frame:
355
355
  only keys the arcade keeps for itself; it never interprets yours:
356
356
  - Store and play-page switches: `play`, `channel`, `purchase`, and `build`, which pins one
357
357
  of your own builds when you play it from your Builds page.
358
- - Platform switches: `devtools`, `devLocale`.
358
+ - Platform switches: `devtools`, `devLocale`, and `gem_test_ads`, the mode of the arcade's
359
+ rewarded TEST ads.
359
360
  - The arcade's own sign-in return marker: `social`.
360
361
  - Attribution and campaign keys, which the arcade keeps out of every game: `_s`, `utm_*`
361
362
  (every key with that prefix, matched case-sensitively, so UTM_X and utmost are yours),
@@ -8,8 +8,8 @@
8
8
  "documents": [
9
9
  {
10
10
  "file": "achievements.md",
11
- "sha256": "e67bc5d0e6abd9ff842be46fa82bf5117fce74b2eb9d28a3954919ddc8a11c73",
12
- "updated": "2026-10-02T23:09:37Z"
11
+ "sha256": "adc8bfaa769fbb283a53de1190ed85cdf4c1c9753e8cf78335b4595e98f8fad4",
12
+ "updated": "2026-10-06T22:14:30Z"
13
13
  },
14
14
  {
15
15
  "file": "analytics.md",
@@ -33,13 +33,13 @@
33
33
  },
34
34
  {
35
35
  "file": "changelog.md",
36
- "sha256": "37bae4170e89587ee8c498891fa2dac008f09fb01fcfbcf87b1310340d2cfc62",
37
- "updated": "2026-10-06T19:43:21Z"
36
+ "sha256": "f13808374edf0988e0aef58b857c82298702a67d450962522a532b4827b74c91",
37
+ "updated": "2026-10-07T16:20:42Z"
38
38
  },
39
39
  {
40
40
  "file": "cross-promotion.md",
41
- "sha256": "3a8121a9c973e4a1bfed076a3226d139f8b4e38a173a06ea9f08000c72bca4b4",
42
- "updated": "2026-09-29T17:32:40Z"
41
+ "sha256": "289cb4826a56b4e7423160e4187af65f4167c80218c2792ca0c01fbd977ecfec",
42
+ "updated": "2026-10-06T18:49:36Z"
43
43
  },
44
44
  {
45
45
  "file": "daily-challenges.md",
@@ -68,8 +68,8 @@
68
68
  },
69
69
  {
70
70
  "file": "getting-started.md",
71
- "sha256": "77ce7b2223d18267ac3ca2f49e09481752539f2674910e2cb12832825c945000",
72
- "updated": "2026-10-06T19:43:21Z"
71
+ "sha256": "222dcf81a2ed6d8fc8ffc37d5f7ef146dcf80c4b76f84fd9b8dd53ea1cbce23b",
72
+ "updated": "2026-10-07T16:20:42Z"
73
73
  },
74
74
  {
75
75
  "file": "inventory-and-catalogs.md",
@@ -118,8 +118,8 @@
118
118
  },
119
119
  {
120
120
  "file": "server-bundles.md",
121
- "sha256": "49f51b37e969b6120d2df6cf6d2f33d2856c1602fd7504a7b308a9ea21c9b502",
122
- "updated": "2026-10-06T19:43:21Z"
121
+ "sha256": "ffc7ec338f3436f73b16f8ba92a54de93e55bd5e9adbe562647e27b4867e4619",
122
+ "updated": "2026-10-07T16:20:42Z"
123
123
  },
124
124
  {
125
125
  "file": "serverless-functions-js.md",
@@ -143,8 +143,8 @@
143
143
  },
144
144
  {
145
145
  "file": "what-a-game-must-do.md",
146
- "sha256": "436a8b61c46a3de6c7d5db65293bdcd17426412742dffd21f976bf87d1418d88",
147
- "updated": "2026-10-06T17:36:25Z"
146
+ "sha256": "3d3ed15c93368a03b11c0e4e1924797e2638aff5be264a799a259ee065d2dbfc",
147
+ "updated": "2026-10-06T23:08:59Z"
148
148
  }
149
149
  ]
150
150
  }
@@ -80,8 +80,8 @@ To check the download yourself: the package npm serves is, byte for byte, the fi
80
80
  also publishes on its download host, with the hash beside it:
81
81
 
82
82
  ```bash
83
- curl -fsSL -O https://downloads.gemarcade.com/sdk/v8.0.0/gem-web-sdk.tgz
84
- curl -fsSL https://downloads.gemarcade.com/sdk/v8.0.0/gem-web-sdk.tgz.sri
83
+ curl -fsSL -O https://downloads.gemarcade.com/sdk/v8.1.0/gem-web-sdk.tgz
84
+ curl -fsSL https://downloads.gemarcade.com/sdk/v8.1.0/gem-web-sdk.tgz.sri
85
85
  openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
86
86
  ```
87
87