@indiegems/gem-web-sdk 8.1.0 → 9.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.
- package/CHANGELOG.md +14 -0
- package/README.md +1 -0
- package/dist/function.d.ts +1 -1
- package/dist/game.d.ts +1 -1
- package/dist/gem-web-sdk.esm.js +1 -1
- package/dist/gem-web-sdk.function.js +1 -1
- package/dist/gem-web-sdk.game.js +2 -2
- package/dist/gem-web-sdk.min.js +2 -2
- package/dist/gem-web-sdk.sdk.js +2 -2
- package/dist/gem-web-sdk.server.js +1 -1
- package/dist/sdk.d.ts +1 -1
- package/dist/server.d.ts +1 -1
- package/docs/api/api-reference-game.md +69 -24
- package/docs/api/api-reference-server.md +6 -2
- package/docs/api/reference.json +20 -20
- package/docs/changelog.md +14 -0
- package/docs/cross-promotion.md +103 -44
- package/docs/getting-started.md +1 -1
- package/docs/manifest.json +23 -18
- package/docs/match-session.md +1 -1
- package/docs/rewarded-ads.md +616 -0
- package/docs/room-shapes.md +54 -76
- package/docs/rooms-and-matches.md +2 -3
- package/docs/server-bundles.md +2 -2
- package/docs/serverless-functions.md +42 -32
- package/docs/what-a-game-must-do.md +14 -3
- package/package.json +1 -1
- package/schema/bridge.v1.json +42 -6
package/docs/api/reference.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"source": {
|
|
4
4
|
"repo": "IndieGems/gem-web-sdk",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "9.1.0"
|
|
6
6
|
},
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -3978,14 +3978,20 @@
|
|
|
3978
3978
|
"url": "https://gemarcade.com/developer/api/game#adformat",
|
|
3979
3979
|
"signature": "type AdFormat = \"interstitial\" | \"rewarded\"",
|
|
3980
3980
|
"documented": true,
|
|
3981
|
-
"summary": "Which kind of break this is.\n\n`interstitial` is a panel the player dismisses and nothing follows from.\n\n
|
|
3981
|
+
"summary": "Which kind of break this is.\n\n`interstitial` is a panel the player dismisses and nothing follows from.\n\n`rewarded` is a button the player pressed for a reward, such as a revive or\nan extra attempt, that your game config declares for the placement in\n`rewarded_placements`. The press is the player's opt-in, and the arcade\nstarts the ad straight away, so ask from the button's own handler (see\n[AdsApi.requestAd](https://gemarcade.com/developer/api/game#adsapi-requestad)). It is served where this arcade serves it, and\nanswered `no_ad` where it does not. ⚠️ The outcome grants nothing: the\nplatform grants the reward, against its own record, never your game reading\n`shown`. It arrives on your game's inventory for this player, written by the\nplatform, so re-read `gem.me.inventory` after a `rewarded` ask settles rather\nthan waiting for a signal on this pair; there will not be one.",
|
|
3982
3982
|
"deprecated": null,
|
|
3983
3983
|
"params": [],
|
|
3984
3984
|
"returns": null,
|
|
3985
3985
|
"throws": null,
|
|
3986
3986
|
"example": null,
|
|
3987
3987
|
"defaultValue": null,
|
|
3988
|
-
"guides": [
|
|
3988
|
+
"guides": [
|
|
3989
|
+
{
|
|
3990
|
+
"slug": "rewarded-ads",
|
|
3991
|
+
"anchor": null,
|
|
3992
|
+
"url": "https://gemarcade.com/developer/docs/rewarded-ads"
|
|
3993
|
+
}
|
|
3994
|
+
]
|
|
3989
3995
|
},
|
|
3990
3996
|
{
|
|
3991
3997
|
"id": "AdRequest",
|
|
@@ -4012,7 +4018,7 @@
|
|
|
4012
4018
|
"url": "https://gemarcade.com/developer/api/game#adrequest-placement",
|
|
4013
4019
|
"signature": "placement: string",
|
|
4014
4020
|
"documented": true,
|
|
4015
|
-
"summary": "Your own name for the break — `level-complete`, `between-rounds`.\n\n**1 to 32 characters of `a-z`, `0-9`, `-` and `_`.** Lowercase, no spaces,\nno accents: `'Boss Fight'` is not a placement, `'boss-fight'` is. The\ngrammar is the platform's own, and
|
|
4021
|
+
"summary": "Your own name for the break — `level-complete`, `between-rounds`.\n\n**1 to 32 characters of `a-z`, `0-9`, `-` and `_`.** Lowercase, no spaces,\nno accents: `'Boss Fight'` is not a placement, `'boss-fight'` is. The\ngrammar is the platform's own, and the SDK enforces it so a name that does\nnot fit is refused at your keyboard rather than in the field. A placement\noutside it is answered `no_ad` and reported to your `onError` handler.\n\nFor `rewarded`, it names the reward your game config declares in\n`rewarded_placements`: the platform matches it against that config, and a\nplacement the config does not declare is answered `no_ad`. For\n`interstitial`, the platform matches it against nothing; there it is for\nyour own telemetry, so two break points in your game can be told apart.\nEither way it is never shown to a player and **never sent to an ad\nnetwork**.",
|
|
4016
4022
|
"deprecated": null,
|
|
4017
4023
|
"params": [],
|
|
4018
4024
|
"returns": null,
|
|
@@ -4046,7 +4052,7 @@
|
|
|
4046
4052
|
"url": "https://gemarcade.com/developer/api/game#adoutcomekind",
|
|
4047
4053
|
"signature": "type AdOutcomeKind = \"shown\" | \"no_ad\" | \"unavailable\"",
|
|
4048
4054
|
"documented": true,
|
|
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.",
|
|
4055
|
+
"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. After a `rewarded` ask the reward may still\n have landed, with no ad, so re-read your inventory either way.\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.",
|
|
4050
4056
|
"deprecated": null,
|
|
4051
4057
|
"params": [],
|
|
4052
4058
|
"returns": null,
|
|
@@ -4114,7 +4120,7 @@
|
|
|
4114
4120
|
"url": "https://gemarcade.com/developer/api/game#adsapi-requestad",
|
|
4115
4121
|
"signature": "requestAd(req: AdRequest): Promise<AdOutcome>",
|
|
4116
4122
|
"documented": true,
|
|
4117
|
-
"summary": "Ask whether the arcade has anything to show at this break in play.\n\nNOTHING REJECTS, and the answer is usually `no_ad`. Every local refusal —\nan arcade that does not do this at all, a call while another is still in\nflight, a `placement` this module will not send — resolves `no_ad` rather\nthan throwing, so a game needs exactly one branch. Only \"no answer ever\narrived\" resolves `unavailable`.\n\nThe arcade pauses your game while a panel is up and resumes it when this\nsettles, so you do not need to pause anything yourself.\n\n⚠️ `shown` grants nothing. Never hand out a reward, an unlock or a currency\non the strength of it.",
|
|
4123
|
+
"summary": "Ask whether the arcade has anything to show at this break in play.\n\nNOTHING REJECTS, and the answer is usually `no_ad`. Every local refusal —\nan arcade that does not do this at all, a call while another is still in\nflight, a `placement` this module will not send — resolves `no_ad` rather\nthan throwing, so a game needs exactly one branch. Only \"no answer ever\narrived\" resolves `unavailable`.\n\nThe arcade pauses your game while a panel is up and resumes it when this\nsettles, so you do not need to pause anything yourself.\n\n⚠️ `shown` grants nothing. Never hand out a reward, an unlock or a currency\non the strength of it. A `rewarded` placement's reward is granted by the\nplatform into your game's inventory; re-read `gem.me.inventory` once this\nsettles, whatever it settled as.\n\n**A `rewarded` ask must come from the player's press.** The press is the\nplayer's opt-in, and the arcade starts the ad as soon as the ask arrives.\nCall this from your button's `click`, `pointerup`, `touchend` or `keydown`\nhandler, before anything that can outlast the press, such as a slow\n`await` or a timer. **Not from `pointerdown` or `touchstart`:** on a\ntouchscreen they run before the browser counts the tap, so the ask is\nrefused on a phone while the same code works at a desk with a mouse. A\ncontroller press counts when the ask follows it within a second, so ask\nfrom the frame of your input loop that saw the button go down. Where the\nbrowser reports user activation (`navigator.userActivation`), the frame\nholds none, and no controller was used in the last second, this answers\n`no_ad` without asking. It also calls your `onError` handler with\ncode `'internal'` and a message that begins\n`rewarded_requires_user_gesture`, and writes one `[gem]` line a session\nthrough your `log`. Where the browser has no such API, the ask goes ahead.\nAn `interstitial` needs no press.",
|
|
4118
4124
|
"deprecated": null,
|
|
4119
4125
|
"params": [
|
|
4120
4126
|
{
|
|
@@ -4126,15 +4132,9 @@
|
|
|
4126
4132
|
],
|
|
4127
4133
|
"returns": "The arcade's answer. Always resolves; never rejects.",
|
|
4128
4134
|
"throws": null,
|
|
4129
|
-
"example": "```ts\nconst { outcome } = await gem.ads.requestAd({\n placement: 'level-complete',\n format: 'interstitial',\n});\n// `no_ad` is the ordinary answer. There is nothing to show and nothing\n// to draw; carry straight on to the next level.\nstartNextLevel();\n```",
|
|
4135
|
+
"example": "```ts\nconst { outcome } = await gem.ads.requestAd({\n placement: 'level-complete',\n format: 'interstitial',\n});\n// `no_ad` is the ordinary answer. There is nothing to show and nothing\n// to draw; carry straight on to the next level.\nstartNextLevel();\n```\n\n```ts\n// A rewarded ask, from the button's own click: the ask comes first, and\n// the inventory read comes after it.\nwatchAdButton.addEventListener('click', async () => {\n watchAdButton.disabled = true; // only between the press and the answer\n await gem.ads.requestAd({ placement: 'dice_rolls', format: 'rewarded' });\n gem.me.inventory.refresh();\n try {\n showDice(await gem.me.inventory.snapshot());\n } finally {\n watchAdButton.disabled = false;\n }\n});\n```",
|
|
4130
4136
|
"defaultValue": null,
|
|
4131
|
-
"guides": [
|
|
4132
|
-
{
|
|
4133
|
-
"slug": "cross-promotion",
|
|
4134
|
-
"anchor": null,
|
|
4135
|
-
"url": "https://gemarcade.com/developer/docs/cross-promotion"
|
|
4136
|
-
}
|
|
4137
|
-
]
|
|
4137
|
+
"guides": []
|
|
4138
4138
|
},
|
|
4139
4139
|
{
|
|
4140
4140
|
"id": "AdsApi.rewardedStatus",
|
|
@@ -7534,9 +7534,9 @@
|
|
|
7534
7534
|
"anchor": "functionfailure",
|
|
7535
7535
|
"group": "functions",
|
|
7536
7536
|
"url": "https://gemarcade.com/developer/api/game#functionfailure",
|
|
7537
|
-
"signature": "type FunctionFailure = \"trap\" | \"memory_exceeded\" | \"cpu_budget_exceeded\" | \"host_call_limit\"",
|
|
7537
|
+
"signature": "type FunctionFailure = \"trap\" | \"memory_exceeded\" | \"cpu_budget_exceeded\" | \"host_call_limit\" | \"output_too_large\" | \"argument_too_large\"",
|
|
7538
7538
|
"documented": true,
|
|
7539
|
-
"summary": "Why a call that reached your function did not finish — carried on\n[GemFunctionError.failure](https://gemarcade.com/developer/api/game#gemfunctionerror-failure).\n\n- `trap` — a panic, an `unreachable`, an out-of-bounds access or a stack\n overflow. No stack trace comes back.\n- `memory_exceeded` — past the memory limit.\n- `cpu_budget_exceeded` — past the CPU budget.\n- `host_call_limit` — more platform API calls than one invocation may make.",
|
|
7539
|
+
"summary": "Why a call that reached your function did not finish — carried on\n[GemFunctionError.failure](https://gemarcade.com/developer/api/game#gemfunctionerror-failure).\n\n- `trap` — a panic, an `unreachable`, an out-of-bounds access or a stack\n overflow. No stack trace comes back.\n- `memory_exceeded` — past the memory limit.\n- `cpu_budget_exceeded` — past the CPU budget.\n- `host_call_limit` — more platform API calls than one invocation may make.\n- `output_too_large` — your function's result was over the 1 MiB output cap.\n- `argument_too_large` — a value your function handed to a platform call\n (`log`, `call-api`, `download-link` or `upload-link`) was over the 16 MiB\n per-call limit.",
|
|
7540
7540
|
"deprecated": null,
|
|
7541
7541
|
"params": [],
|
|
7542
7542
|
"returns": null,
|
|
@@ -7770,7 +7770,7 @@
|
|
|
7770
7770
|
"url": "https://gemarcade.com/developer/api/game#arcade_embedders",
|
|
7771
7771
|
"signature": "ARCADE_EMBEDDERS: readonly string[]",
|
|
7772
7772
|
"documented": true,
|
|
7773
|
-
"summary": "The origins the
|
|
7773
|
+
"summary": "The origins the platform embeds a game from, which every game accepts\nwithout naming them: the Gem Arcade website, the Meme Games website, and\nthe Gem Arcade app.\n\nYou do not pass these. They are always allowed, and\n[GemOptions.allowedEmbedders](https://gemarcade.com/developer/api/game#gemoptions-allowedembedders) adds to them rather than replacing them.",
|
|
7774
7774
|
"deprecated": null,
|
|
7775
7775
|
"params": [],
|
|
7776
7776
|
"returns": null,
|
|
@@ -7804,7 +7804,7 @@
|
|
|
7804
7804
|
"url": "https://gemarcade.com/developer/api/game#gemoptions-allowedembedders",
|
|
7805
7805
|
"signature": "readonly allowedEmbedders?: readonly string[]",
|
|
7806
7806
|
"documented": true,
|
|
7807
|
-
"summary": "Extra origins allowed to embed your game, **added to** the ones the Gem\nArcade uses. Most games leave this out.\n\nThe
|
|
7807
|
+
"summary": "Extra origins allowed to embed your game, **added to** the ones the Gem\nArcade uses. Most games leave this out.\n\nThe platform's websites and the Gem Arcade app are always allowed — see\n[ARCADE_EMBEDDERS](https://gemarcade.com/developer/api/game#arcade_embedders) — so do not list them. Set this only when a site\nof your own embeds your game.\n\n```js\ncreateGem({ allowedEmbedders: ['https://my-own-site.example'] })\n```\n\nA page whose origin is not allowed gets no connection, and — deliberately —\nis told nothing about why. That means a typo here looks exactly like a\nhandshake that has not arrived: pass [GemOptions.onRefused](https://gemarcade.com/developer/api/game#gemoptions-onrefused) while you\nare integrating, because it is the only place the difference shows.\n\n⚠️ **Bake these in. Never build them from anything the page can\ninfluence** — not `location`, not a query parameter, not\n`document.referrer`. A value the embedder supplies is a check that can never\nfail. Do not add a `localhost:<port>` entry for your dev host either; the\nSDK admits one on your own machine by rule.",
|
|
7808
7808
|
"deprecated": null,
|
|
7809
7809
|
"params": [],
|
|
7810
7810
|
"returns": null,
|
|
@@ -21951,9 +21951,9 @@
|
|
|
21951
21951
|
"anchor": "functionfailure",
|
|
21952
21952
|
"group": "functions",
|
|
21953
21953
|
"url": "https://gemarcade.com/developer/api/server#functionfailure",
|
|
21954
|
-
"signature": "type FunctionFailure = \"trap\" | \"memory_exceeded\" | \"cpu_budget_exceeded\" | \"host_call_limit\"",
|
|
21954
|
+
"signature": "type FunctionFailure = \"trap\" | \"memory_exceeded\" | \"cpu_budget_exceeded\" | \"host_call_limit\" | \"output_too_large\" | \"argument_too_large\"",
|
|
21955
21955
|
"documented": true,
|
|
21956
|
-
"summary": "Why a call that reached your function did not finish — carried on\n[GemFunctionError.failure](https://gemarcade.com/developer/api/server#gemfunctionerror-failure).\n\n- `trap` — a panic, an `unreachable`, an out-of-bounds access or a stack\n overflow. No stack trace comes back.\n- `memory_exceeded` — past the memory limit.\n- `cpu_budget_exceeded` — past the CPU budget.\n- `host_call_limit` — more platform API calls than one invocation may make.",
|
|
21956
|
+
"summary": "Why a call that reached your function did not finish — carried on\n[GemFunctionError.failure](https://gemarcade.com/developer/api/server#gemfunctionerror-failure).\n\n- `trap` — a panic, an `unreachable`, an out-of-bounds access or a stack\n overflow. No stack trace comes back.\n- `memory_exceeded` — past the memory limit.\n- `cpu_budget_exceeded` — past the CPU budget.\n- `host_call_limit` — more platform API calls than one invocation may make.\n- `output_too_large` — your function's result was over the 1 MiB output cap.\n- `argument_too_large` — a value your function handed to a platform call\n (`log`, `call-api`, `download-link` or `upload-link`) was over the 16 MiB\n per-call limit.",
|
|
21957
21957
|
"deprecated": null,
|
|
21958
21958
|
"params": [],
|
|
21959
21959
|
"returns": null,
|
package/docs/changelog.md
CHANGED
|
@@ -16,6 +16,20 @@ 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
|
+
## 9.1.0
|
|
20
|
+
|
|
21
|
+
Upgrading from 9.0.0. Impact: none.
|
|
22
|
+
|
|
23
|
+
Nothing in this release asks anything of a game.
|
|
24
|
+
|
|
25
|
+
## 9.0.0
|
|
26
|
+
|
|
27
|
+
Upgrading from 8.1.0. Impact: required.
|
|
28
|
+
|
|
29
|
+
### Must change
|
|
30
|
+
|
|
31
|
+
- FunctionFailure now has two more reasons, output_too_large and argument_too_large. A function that returns more than the output cap, or passes a platform call a value over the per-call size limit, used to fail as trap. It now fails with the matching new reason. Update any code that switches on FunctionFailure so it handles both new reasons. An exhaustive check over the old four reasons will stop compiling until you do. If your game treated trap as the sign of an oversized output or argument, handle the new reasons there too. Then rebuild your game against this release.
|
|
32
|
+
|
|
19
33
|
## 8.1.0
|
|
20
34
|
|
|
21
35
|
Upgrading from 8.0.0. Impact: required.
|
package/docs/cross-promotion.md
CHANGED
|
@@ -4,6 +4,11 @@ The Arcade can show a **full-screen panel about another game** at a break in you
|
|
|
4
4
|
for one; the Arcade decides whether to show anything, draws it in its own chrome over your paused
|
|
5
5
|
game, and tells you when it is done.
|
|
6
6
|
|
|
7
|
+
The same call with `format: 'rewarded'` is a button the player presses for something your game
|
|
8
|
+
gives — a revive, an extra attempt. That one has its own page,
|
|
9
|
+
[`rewarded-ads.md`](rewarded-ads.md). This page is about the interstitial; where a rule here reads
|
|
10
|
+
differently for a rewarded ask, it says so.
|
|
11
|
+
|
|
7
12
|
```js
|
|
8
13
|
const { outcome } = await gem.ads.requestAd({
|
|
9
14
|
placement: 'level-complete',
|
|
@@ -20,10 +25,10 @@ That is the whole surface, apart from one read that tells a rewarded button whet
|
|
|
20
25
|
|
|
21
26
|
`outcome` is one of exactly three values:
|
|
22
27
|
|
|
23
|
-
| outcome
|
|
24
|
-
|
|
25
|
-
| `no_ad`
|
|
26
|
-
| `shown`
|
|
28
|
+
| outcome | what happened | what your game does |
|
|
29
|
+
| ------------- | -------------------------------------- | ---------------------------------------- |
|
|
30
|
+
| `no_ad` | nothing was shown | carry on exactly as if you had not asked |
|
|
31
|
+
| `shown` | a panel was painted and has closed | carry on — and grant nothing |
|
|
27
32
|
| `unavailable` | the Arcade could not draw one just now | carry on; read it identically to `no_ad` |
|
|
28
33
|
|
|
29
34
|
**`no_ad` is the common answer**, not an edge case. There may be nothing suitable, the player may
|
|
@@ -37,48 +42,59 @@ So write the `no_ad` path as your ordinary path:
|
|
|
37
42
|
- Do **not** hold up your own flow waiting for a fill. Ask, and continue when the promise settles.
|
|
38
43
|
- Do **not** retry on a no. A second ask at the same break gets the same answer and spends a slot.
|
|
39
44
|
|
|
45
|
+
**A rewarded ask reads differently, because the player pressed your button and is waiting on it.**
|
|
46
|
+
Its `no_ad` can arrive with the reward already granted (§3), so branch on your inventory, never on
|
|
47
|
+
the outcome; and where nothing landed, a neutral line in your own UI — "No revive this time" — is
|
|
48
|
+
right, with no reason given. (When no ad was available and nothing was granted, the Arcade has
|
|
49
|
+
already shown "No ad available right now" itself.) Draw the button only when
|
|
50
|
+
`gem.ads.rewardedStatus` says it can pay (§3); [`rewarded-ads.md`
|
|
51
|
+
§5](rewarded-ads.md#5-draw-the-button-only-when-it-can-pay) is that button.
|
|
52
|
+
|
|
40
53
|
## 2. Nothing rejects
|
|
41
54
|
|
|
42
55
|
`requestAd` always resolves. There is no error branch and no `try`/`catch` to write. Even a refusal
|
|
43
56
|
of your own arguments — a `placement` outside the grammar below, a `format` outside the two —
|
|
44
|
-
resolves `no_ad`, so
|
|
45
|
-
|
|
57
|
+
resolves `no_ad`, and so does a rewarded ask made without the player's press (see
|
|
58
|
+
[Where to put the call](#where-to-put-the-call)), so your game keeps exactly one branch. (The
|
|
59
|
+
mistake is still reported to your `onError` handler, so you find out in development.)
|
|
46
60
|
|
|
47
61
|
## 3. ⚠️ `shown` grants nothing
|
|
48
62
|
|
|
49
63
|
`shown` says a panel was painted and has closed. It is **not** a receipt, **not** an entitlement,
|
|
50
64
|
and **not** permission to hand anything out.
|
|
51
65
|
|
|
52
|
-
`format: 'rewarded'`
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
frame receives, and a game that unlocks a level or grants a currency on the
|
|
56
|
-
unlock that any page embedding your game can forge.
|
|
66
|
+
That holds for `format: 'rewarded'` too, the one format that pays the player. The platform grants a
|
|
67
|
+
rewarded placement's reward itself, against its own record of the ad the Arcade played and how it
|
|
68
|
+
ended — never your game, by reading an outcome. An outcome travels over the same channel as
|
|
69
|
+
everything else your frame receives, and a game that unlocks a level or grants a currency on the
|
|
70
|
+
strength of one has an unlock that any page embedding your game can forge.
|
|
57
71
|
|
|
58
72
|
Treat a break as a break. If your game wants to give the player something for reaching the end of a
|
|
59
73
|
level, give it to them for reaching the end of the level.
|
|
60
74
|
|
|
61
|
-
**Where a rewarded grant
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
settles, whatever it settled as:
|
|
75
|
+
**Where a rewarded grant arrives.** On your game's own inventory for this player, written by the
|
|
76
|
+
platform — never by your game — and read through `gem.me.inventory`, the same place a purchase
|
|
77
|
+
lands, read the same way. (That is the game-scoped inventory `gem.me` shows you, not the player's
|
|
78
|
+
platform wallet.) There is **no fourth outcome and no extra field** on the answer. `outcome` is
|
|
79
|
+
pinned at three values because a new value would break every game bundle already in the field: its
|
|
80
|
+
validator refuses the whole message. A new field would not break anything — validators ignore
|
|
81
|
+
fields they do not know — but it is not coming either: anything on that answer can be forged by the
|
|
82
|
+
page embedding your game, so a reward carried there would be a reward anyone could hand out. So
|
|
83
|
+
re-read your snapshot once a rewarded ask settles, whatever it settled as:
|
|
71
84
|
|
|
72
85
|
```js
|
|
73
86
|
await gem.ads.requestAd({ placement: 'run-banked', format: 'rewarded' });
|
|
74
87
|
gem.me.inventory.refresh(); // drop what is cached
|
|
75
|
-
const snap = await gem.me.inventory.snapshot(); //
|
|
88
|
+
const snap = await gem.me.inventory.snapshot(); // a reward granted before the answer is here
|
|
76
89
|
```
|
|
77
90
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
A `no_ad` can come back with the reward already in that snapshot, because the platform grants
|
|
92
|
+
without an ad in some cases, so branch on the inventory and never on the outcome. And a reward that
|
|
93
|
+
an ad provider confirms from its own server can land after this re-read, so read again when the
|
|
94
|
+
screen that spends it opens; [`inventory-and-catalogs.md`
|
|
95
|
+
§6](inventory-and-catalogs.md#6-when-to-re-read) is the general form of this.
|
|
96
|
+
[`rewarded-ads.md`](rewarded-ads.md) is the rest: declaring what a placement grants, spending it,
|
|
97
|
+
drawing the button, and testing.
|
|
82
98
|
|
|
83
99
|
**Whether a rewarded button could pay right now.** A rewarded placement is a button the player
|
|
84
100
|
presses, so you need to know whether to draw it. `gem.ads.rewardedStatus(placement)` reads one
|
|
@@ -90,14 +106,14 @@ const status = await gem.ads.rewardedStatus('run-banked');
|
|
|
90
106
|
bankButton.disabled = !status.grantable;
|
|
91
107
|
```
|
|
92
108
|
|
|
93
|
-
| field
|
|
94
|
-
|
|
95
|
-
| `declared`
|
|
96
|
-
| `grantable`
|
|
97
|
-
| `remainingToday`
|
|
98
|
-
| `nextEligibleAt`
|
|
99
|
-
| `heldLimitReached` | the player already holds the placement's `max_held` — spend one first
|
|
100
|
-
| `resetsAt`
|
|
109
|
+
| field | what it says |
|
|
110
|
+
| ------------------ | -------------------------------------------------------------------------- |
|
|
111
|
+
| `declared` | the config promoted to the channel being played names this placement |
|
|
112
|
+
| `grantable` | pressing the button could pay right now — **not** that an ad will play |
|
|
113
|
+
| `remainingToday` | grants left today under the daily cap; absent when the placement has none |
|
|
114
|
+
| `nextEligibleAt` | when a running cooldown ends, in epoch milliseconds; absent when none runs |
|
|
115
|
+
| `heldLimitReached` | the player already holds the placement's `max_held` — spend one first |
|
|
116
|
+
| `resetsAt` | when today's count resets, the next 00:00 UTC, in epoch milliseconds |
|
|
101
117
|
|
|
102
118
|
**`grantable` is not a promise.** A player can press a `grantable` button and get `no_ad`, for
|
|
103
119
|
reasons your game is not told. Read that `no_ad` like any other (§1) — never guess a reason — and
|
|
@@ -126,18 +142,57 @@ At a break **the player already expects**: a level ending, a round finishing, a
|
|
|
126
142
|
return to a menu. Never mid-input, never during a timed sequence, and never on a screen the player
|
|
127
143
|
reached by accident.
|
|
128
144
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
145
|
+
**A rewarded ask comes from the player's press, and from nowhere else.** Your button is the
|
|
146
|
+
player's opt-in. The Arcade starts the ad as soon as the ask arrives, with no prompt of its own
|
|
147
|
+
first, so an ask the player did not make is an ad they did not choose. Call `requestAd` from the
|
|
148
|
+
button's own `click`, `pointerup`, `touchend` or `keydown` handler, or straight from the
|
|
149
|
+
controller press that chose it. Ask first, before any `await` or timer that could outlast the
|
|
150
|
+
press: a browser counts a tap, click or key press for only a few seconds, and the SDK counts a
|
|
151
|
+
controller press for one.
|
|
152
|
+
|
|
153
|
+
⚠️ **Not from `pointerdown` or `touchstart`.** On a touchscreen they run before the browser counts
|
|
154
|
+
the tap, so the ask is refused on a phone. With a mouse, `pointerdown` counts, so a desk test passes
|
|
155
|
+
and hides it.
|
|
156
|
+
|
|
157
|
+
Where the browser reports user activation, your frame holds none, and no controller was used in
|
|
158
|
+
the last second, the SDK answers `no_ad` without asking. It writes a `[gem] rewarded_requires_user_gesture` line to the console once, and
|
|
159
|
+
calls your `onError` handler with code `'internal'` and a message that begins
|
|
160
|
+
`rewarded_requires_user_gesture`, which is the one to match in your own error reporting:
|
|
161
|
+
|
|
162
|
+
```js
|
|
163
|
+
const gem = createGem({
|
|
164
|
+
onError: (code, message) => {
|
|
165
|
+
if (message?.startsWith('rewarded_requires_user_gesture')) {
|
|
166
|
+
reportBug('rewarded ask outside a press', message); // your own telemetry
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Where the browser cannot tell, the ask goes ahead. The check is there to catch a mistake while you
|
|
173
|
+
build; it is not a control. It catches an ask with no recent input, and cannot tell which input
|
|
174
|
+
caused the ask, so an ask from another button's handler goes through. An interstitial needs no
|
|
175
|
+
press.
|
|
176
|
+
|
|
177
|
+
**The button says it is an ad, and names the reward:** "Watch an ad for 3 dice rolls", "Watch an ad
|
|
178
|
+
to revive". Never use a label that hides the ad, or one that reads as the only way forward, such as
|
|
179
|
+
"Tap to continue". [`rewarded-ads.md` §5](rewarded-ads.md#5-draw-the-button-only-when-it-can-pay)
|
|
180
|
+
covers the button.
|
|
181
|
+
|
|
182
|
+
`placement` is your own name for that break — `level-complete`, `run-banked`, `back-to-menu`. For
|
|
183
|
+
an interstitial, the platform matches it against nothing, never shows it to a player, and does not
|
|
184
|
+
vary its decision on it; it exists so your own analytics can tell two break points apart, so name
|
|
185
|
+
it for yourself. For `format: 'rewarded'`, the placement names the reward your game config declares
|
|
186
|
+
in `rewarded_placements`, and the platform matches it against that config. Either way it **never
|
|
187
|
+
reaches an ad network**, and neither does any other string your game writes.
|
|
133
188
|
|
|
134
189
|
**It has a grammar, and it is narrow on purpose: 1 to 32 characters of `a-z`, `0-9`, `-` and `_`.**
|
|
135
190
|
Lowercase, no spaces, no accents — `'Boss Fight'` is not a placement, `'boss-fight'` is. Anything
|
|
136
191
|
else resolves `no_ad` and is reported to your `onError` handler, so you meet it while you are
|
|
137
|
-
building rather than later. The
|
|
138
|
-
|
|
139
|
-
a name that
|
|
140
|
-
|
|
192
|
+
building rather than later. The platform holds the same grammar where it reads the name: for a
|
|
193
|
+
rewarded ask the Arcade forwards the placement unchanged, and a config that declares one outside
|
|
194
|
+
the grammar is refused at upload, so a name that did not fit would be a reward your shipped game
|
|
195
|
+
could never reach. The bound is the platform's, not the SDK's, and the two agree.
|
|
141
196
|
|
|
142
197
|
```js
|
|
143
198
|
async function onLevelComplete() {
|
|
@@ -153,14 +208,18 @@ async function onLevelComplete() {
|
|
|
153
208
|
Your game never learns what was shown — no title, no image, no id, nothing about the other game.
|
|
154
209
|
You receive an outcome and nothing else.
|
|
155
210
|
|
|
156
|
-
You are also never told
|
|
211
|
+
You are also never told _why_ the answer was no. That is deliberate and permanent: a reason would
|
|
157
212
|
tell your game something about the player that game code is not told, and there will not be a
|
|
158
213
|
fourth outcome value naming one. The Arcade also settles every `no_ad` and `unavailable` on a
|
|
159
214
|
schedule that does not depend on which branch produced it, so there is nothing in the timing to
|
|
160
215
|
read either. (`shown` is not held back — how long that one takes is how long your player spent
|
|
161
216
|
looking at a panel.)
|
|
162
217
|
|
|
163
|
-
|
|
218
|
+
A rewarded ask is the one case where something else reaches your game: the reward itself, in your
|
|
219
|
+
own inventory, when one was granted. That is what the ask is for. It says that a reward landed,
|
|
220
|
+
never why, so do not read a reason into it.
|
|
221
|
+
|
|
222
|
+
The other thing you can read is your own economy, and never from the answer:
|
|
164
223
|
`gem.ads.rewardedStatus` reports **your own declared** rewarded caps as the platform holds them —
|
|
165
224
|
the platform can hold a placement's daily cap below what you declared, never above — so you can
|
|
166
225
|
tell when a rewarded `no_ad` was your own cap. It never reports anything about the player, the
|
package/docs/getting-started.md
CHANGED
package/docs/manifest.json
CHANGED
|
@@ -33,13 +33,13 @@
|
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"file": "changelog.md",
|
|
36
|
-
"sha256": "
|
|
37
|
-
"updated": "2026-10-
|
|
36
|
+
"sha256": "9c99e6e42bd7c1a386bba50f07f3fd9cbcef20bd339a3d3f6f7ca8af1aff0462",
|
|
37
|
+
"updated": "2026-10-08T20:00:17Z"
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"file": "cross-promotion.md",
|
|
41
|
-
"sha256": "
|
|
42
|
-
"updated": "2026-10-
|
|
41
|
+
"sha256": "92d4a1ffe19ceaaae1812bfdb77ebbdc04a7eca878ff28995eb6c388c7e876b3",
|
|
42
|
+
"updated": "2026-10-08T16:19:25Z"
|
|
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": "
|
|
72
|
-
"updated": "2026-10-
|
|
71
|
+
"sha256": "f622fe75e4d899bbe2bfa00103faa603e7d84f82579e7909dab16462bb2d1f72",
|
|
72
|
+
"updated": "2026-10-08T20:00:17Z"
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
75
|
"file": "inventory-and-catalogs.md",
|
|
@@ -83,8 +83,8 @@
|
|
|
83
83
|
},
|
|
84
84
|
{
|
|
85
85
|
"file": "match-session.md",
|
|
86
|
-
"sha256": "
|
|
87
|
-
"updated": "2026-
|
|
86
|
+
"sha256": "33fdef4edc170cadd2284ff399fcf16455b7691d9aeac52bd09bade24b8b051a",
|
|
87
|
+
"updated": "2026-10-07T19:27:16Z"
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
90
|
"file": "p2p-and-parties.md",
|
|
@@ -101,15 +101,20 @@
|
|
|
101
101
|
"sha256": "df1e39628ccba04b6f7bcd0b90381dfa34a4480330a6af041d9b71bcaefddb5f",
|
|
102
102
|
"updated": "2026-09-17T20:51:57Z"
|
|
103
103
|
},
|
|
104
|
+
{
|
|
105
|
+
"file": "rewarded-ads.md",
|
|
106
|
+
"sha256": "f1f88f7d3aab2b1a1df2cecab59846625907e0391542291f1fbba56ea98d6476",
|
|
107
|
+
"updated": "2026-10-08T16:19:25Z"
|
|
108
|
+
},
|
|
104
109
|
{
|
|
105
110
|
"file": "room-shapes.md",
|
|
106
|
-
"sha256": "
|
|
107
|
-
"updated": "2026-
|
|
111
|
+
"sha256": "78fad1eee7602b10a9b39f100d3daedfed6c313a884f95382233434a3206930b",
|
|
112
|
+
"updated": "2026-10-07T19:27:16Z"
|
|
108
113
|
},
|
|
109
114
|
{
|
|
110
115
|
"file": "rooms-and-matches.md",
|
|
111
|
-
"sha256": "
|
|
112
|
-
"updated": "2026-
|
|
116
|
+
"sha256": "0abf3717a42c94ec832dfa1310c2d93a9f58236fd75b0aacaab301976bffe286",
|
|
117
|
+
"updated": "2026-10-07T19:27:16Z"
|
|
113
118
|
},
|
|
114
119
|
{
|
|
115
120
|
"file": "saving-player-progress.md",
|
|
@@ -118,8 +123,8 @@
|
|
|
118
123
|
},
|
|
119
124
|
{
|
|
120
125
|
"file": "server-bundles.md",
|
|
121
|
-
"sha256": "
|
|
122
|
-
"updated": "2026-10-
|
|
126
|
+
"sha256": "df1ec4c93429f286232cc468c4f06ff6a7327491bed72ce4a1a30e2ff8aaa3ad",
|
|
127
|
+
"updated": "2026-10-08T20:00:17Z"
|
|
123
128
|
},
|
|
124
129
|
{
|
|
125
130
|
"file": "serverless-functions-js.md",
|
|
@@ -128,8 +133,8 @@
|
|
|
128
133
|
},
|
|
129
134
|
{
|
|
130
135
|
"file": "serverless-functions.md",
|
|
131
|
-
"sha256": "
|
|
132
|
-
"updated": "2026-10-
|
|
136
|
+
"sha256": "b1562b4442b69bb6895198def7b00ccd7a8c56133862a939c2cce9b6e9d30290",
|
|
137
|
+
"updated": "2026-10-07T19:27:16Z"
|
|
133
138
|
},
|
|
134
139
|
{
|
|
135
140
|
"file": "trading-and-gifting.md",
|
|
@@ -143,8 +148,8 @@
|
|
|
143
148
|
},
|
|
144
149
|
{
|
|
145
150
|
"file": "what-a-game-must-do.md",
|
|
146
|
-
"sha256": "
|
|
147
|
-
"updated": "2026-10-
|
|
151
|
+
"sha256": "ff4a1d7613adff83ad9ac9b7c6be6266374e7c2bf7245f723f800ea335a4f4cf",
|
|
152
|
+
"updated": "2026-10-07T21:34:54Z"
|
|
148
153
|
}
|
|
149
154
|
]
|
|
150
155
|
}
|
package/docs/match-session.md
CHANGED
|
@@ -204,7 +204,7 @@ next round is one call:
|
|
|
204
204
|
|
|
205
205
|
**A template's `auto_start_host` starts the first match, not the second.** It fires when the room
|
|
206
206
|
reaches `min_players`; a reopened room already has them, and nothing fires again. A shape whose
|
|
207
|
-
first match started itself — a hub, a
|
|
207
|
+
first match started itself — a hub, a matchmade lobby — still needs a client to call
|
|
208
208
|
`startMatch()` for the next one, and that client is whoever `gem.rooms.isHost` says. Watch
|
|
209
209
|
`hostChanged`: if the host closed their tab during the match, the room has a new host, and the
|
|
210
210
|
start belongs to them now.
|