@indiegems/gem-web-sdk 8.0.0 → 9.0.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 +23 -0
- package/README.md +1 -0
- package/dist/function.d.ts +1 -1
- package/dist/game.d.ts +14 -2
- package/dist/gem-web-sdk.bridge.js +1 -1
- package/dist/gem-web-sdk.esm.js +2 -2
- package/dist/gem-web-sdk.function.js +2 -2
- 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 +2 -2
- package/dist/index.d.ts +12 -3
- package/dist/sdk.d.ts +14 -2
- package/dist/server.d.ts +1 -1
- package/docs/achievements.md +72 -5
- package/docs/api/api-reference-game.md +329 -39
- package/docs/api/api-reference-server.md +6 -2
- package/docs/api/reference.json +241 -21
- package/docs/changelog.md +23 -0
- package/docs/cross-promotion.md +87 -31
- package/docs/getting-started.md +3 -2
- package/docs/manifest.json +25 -20
- package/docs/match-session.md +1 -1
- package/docs/rewarded-ads.md +464 -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 +132 -5
- package/package.json +1 -1
- package/schema/bridge.v1.json +72 -7
- package/schema/capabilities.public.json +6 -0
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',
|
|
@@ -13,8 +18,8 @@ const { outcome } = await gem.ads.requestAd({
|
|
|
13
18
|
startNextLevel();
|
|
14
19
|
```
|
|
15
20
|
|
|
16
|
-
That is the whole surface
|
|
17
|
-
tells it.
|
|
21
|
+
That is the whole surface, apart from one read that tells a rewarded button whether it could pay
|
|
22
|
+
(§3). The rest of this page is the four things a game gets wrong when nobody tells it.
|
|
18
23
|
|
|
19
24
|
## 1. The answer is usually no, and no is ordinary
|
|
20
25
|
|
|
@@ -37,6 +42,12 @@ 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. [`rewarded-ads.md`
|
|
49
|
+
§5](rewarded-ads.md#5-draw-the-button-and-name-the-reward) is that button.
|
|
50
|
+
|
|
40
51
|
## 2. Nothing rejects
|
|
41
52
|
|
|
42
53
|
`requestAd` always resolves. There is no error branch and no `try`/`catch` to write. Even a refusal
|
|
@@ -49,36 +60,68 @@ resolves `no_ad`, so your game keeps exactly one branch. (The mistake is still r
|
|
|
49
60
|
`shown` says a panel was painted and has closed. It is **not** a receipt, **not** an entitlement,
|
|
50
61
|
and **not** permission to hand anything out.
|
|
51
62
|
|
|
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.
|
|
63
|
+
That holds for `format: 'rewarded'` too, the one format that pays the player. The platform grants a
|
|
64
|
+
rewarded placement's reward itself, against its own record of what the player did on the Arcade's
|
|
65
|
+
offer sheet — never your game, by reading an outcome. An outcome travels over the same channel as
|
|
66
|
+
everything else your frame receives, and a game that unlocks a level or grants a currency on the
|
|
67
|
+
strength of one has an unlock that any page embedding your game can forge.
|
|
57
68
|
|
|
58
69
|
Treat a break as a break. If your game wants to give the player something for reaching the end of a
|
|
59
70
|
level, give it to them for reaching the end of the level.
|
|
60
71
|
|
|
61
|
-
**Where a rewarded grant
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
settles, whatever it settled as:
|
|
72
|
+
**Where a rewarded grant arrives.** On your game's own inventory for this player, written by the
|
|
73
|
+
platform — never by your game — and read through `gem.me.inventory`, the same place a purchase
|
|
74
|
+
lands, read the same way. (That is the game-scoped inventory `gem.me` shows you, not the player's
|
|
75
|
+
platform wallet.) There is **no fourth outcome and no extra field** on the answer. `outcome` is
|
|
76
|
+
pinned at three values because a new value would break every game bundle already in the field: its
|
|
77
|
+
validator refuses the whole message. A new field would not break anything — validators ignore
|
|
78
|
+
fields they do not know — but it is not coming either: anything on that answer can be forged by the
|
|
79
|
+
page embedding your game, so a reward carried there would be a reward anyone could hand out. So
|
|
80
|
+
re-read your snapshot once a rewarded ask settles, whatever it settled as:
|
|
71
81
|
|
|
72
82
|
```js
|
|
73
83
|
await gem.ads.requestAd({ placement: 'run-banked', format: 'rewarded' });
|
|
74
84
|
gem.me.inventory.refresh(); // drop what is cached
|
|
75
|
-
const snap = await gem.me.inventory.snapshot(); //
|
|
85
|
+
const snap = await gem.me.inventory.snapshot(); // a reward granted before the answer is here
|
|
76
86
|
```
|
|
77
87
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
A `no_ad` can come back with the reward already in that snapshot, because the platform grants
|
|
89
|
+
without an ad in some cases, so branch on the inventory and never on the outcome. And a reward that
|
|
90
|
+
an ad provider confirms from its own server can land after this re-read, so read again when the
|
|
91
|
+
screen that spends it opens; [`inventory-and-catalogs.md`
|
|
92
|
+
§6](inventory-and-catalogs.md#6-when-to-re-read) is the general form of this.
|
|
93
|
+
[`rewarded-ads.md`](rewarded-ads.md) is the rest: declaring what a placement grants, spending it,
|
|
94
|
+
drawing the button, and testing.
|
|
95
|
+
|
|
96
|
+
**Whether a rewarded button could pay right now.** A rewarded placement is a button the player
|
|
97
|
+
presses, so you need to know whether to draw it. `gem.ads.rewardedStatus(placement)` reads one
|
|
98
|
+
placement's state from **your own** `rewarded_placements` config — its daily cap, cooldown and
|
|
99
|
+
`max_held` — applied to this player's use of it:
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
const status = await gem.ads.rewardedStatus('run-banked');
|
|
103
|
+
bankButton.disabled = !status.grantable;
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
| field | what it says |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `declared` | the config promoted to the channel being played names this placement |
|
|
109
|
+
| `grantable` | pressing the button could pay right now — **not** that an ad will play |
|
|
110
|
+
| `remainingToday` | grants left today under the daily cap; absent when the placement has none |
|
|
111
|
+
| `nextEligibleAt` | when a running cooldown ends, in epoch milliseconds; absent when none runs |
|
|
112
|
+
| `heldLimitReached` | the player already holds the placement's `max_held` — spend one first |
|
|
113
|
+
| `resetsAt` | when today's count resets, the next 00:00 UTC, in epoch milliseconds |
|
|
114
|
+
|
|
115
|
+
**`grantable` is not a promise.** A player can press a `grantable` button and get `no_ad`, for
|
|
116
|
+
reasons your game is not told. Read that `no_ad` like any other (§1) — never guess a reason — and
|
|
117
|
+
re-read your inventory whatever the ask settled as. The status grants nothing and reserves
|
|
118
|
+
nothing.
|
|
119
|
+
|
|
120
|
+
It never rejects, like the rest of `gem.ads`. Without a play token, when the read fails or takes
|
|
121
|
+
longer than ten seconds, or for a placement outside the grammar below, it answers not declared and
|
|
122
|
+
not grantable with no `resetsAt`, and the reason goes to your `onError` handler. It is a read of
|
|
123
|
+
the platform, so call it when you draw the button — on a results or death screen — and not every
|
|
124
|
+
frame, and draw the rest of the screen first rather than holding it on the answer.
|
|
82
125
|
|
|
83
126
|
## 4. The Arcade pauses your game for you
|
|
84
127
|
|
|
@@ -94,20 +137,23 @@ If you have not wired the pause at all yet, that is the thing to fix first;
|
|
|
94
137
|
|
|
95
138
|
At a break **the player already expects**: a level ending, a round finishing, a run being banked, a
|
|
96
139
|
return to a menu. Never mid-input, never during a timed sequence, and never on a screen the player
|
|
97
|
-
reached by accident.
|
|
140
|
+
reached by accident. (A rewarded ask goes where the player pressed your button for it, and nowhere
|
|
141
|
+
else; [`rewarded-ads.md`](rewarded-ads.md) covers the button.)
|
|
98
142
|
|
|
99
|
-
`placement` is your own name for that break — `level-complete`, `run-banked`, `back-to-menu`.
|
|
100
|
-
platform matches it against nothing
|
|
101
|
-
decision on it
|
|
102
|
-
yourself.
|
|
143
|
+
`placement` is your own name for that break — `level-complete`, `run-banked`, `back-to-menu`. For
|
|
144
|
+
an interstitial, the platform matches it against nothing, never shows it to a player, and does not
|
|
145
|
+
vary its decision on it; it exists so your own analytics can tell two break points apart, so name
|
|
146
|
+
it for yourself. For `format: 'rewarded'`, the placement names the reward your game config declares
|
|
147
|
+
in `rewarded_placements`, and the platform matches it against that config. Either way it **never
|
|
148
|
+
reaches an ad network**, and neither does any other string your game writes.
|
|
103
149
|
|
|
104
150
|
**It has a grammar, and it is narrow on purpose: 1 to 32 characters of `a-z`, `0-9`, `-` and `_`.**
|
|
105
151
|
Lowercase, no spaces, no accents — `'Boss Fight'` is not a placement, `'boss-fight'` is. Anything
|
|
106
152
|
else resolves `no_ad` and is reported to your `onError` handler, so you meet it while you are
|
|
107
|
-
building rather than later. The
|
|
108
|
-
|
|
109
|
-
a name that
|
|
110
|
-
|
|
153
|
+
building rather than later. The platform holds the same grammar where it reads the name: for a
|
|
154
|
+
rewarded ask the Arcade forwards the placement unchanged, and a config that declares one outside
|
|
155
|
+
the grammar is refused at upload, so a name that did not fit would be a reward your shipped game
|
|
156
|
+
could never reach. The bound is the platform's, not the SDK's, and the two agree.
|
|
111
157
|
|
|
112
158
|
```js
|
|
113
159
|
async function onLevelComplete() {
|
|
@@ -130,6 +176,16 @@ schedule that does not depend on which branch produced it, so there is nothing i
|
|
|
130
176
|
read either. (`shown` is not held back — how long that one takes is how long your player spent
|
|
131
177
|
looking at a panel.)
|
|
132
178
|
|
|
179
|
+
A rewarded ask is the one case where something else reaches your game: the reward itself, in your
|
|
180
|
+
own inventory, when one was granted. That is what the ask is for. It says that a reward landed,
|
|
181
|
+
never why, so do not read a reason into it.
|
|
182
|
+
|
|
183
|
+
The other thing you can read is your own economy, and never from the answer:
|
|
184
|
+
`gem.ads.rewardedStatus` reports **your own declared** rewarded caps as the platform holds them —
|
|
185
|
+
the platform can hold a placement's daily cap below what you declared, never above — so you can
|
|
186
|
+
tell when a rewarded `no_ad` was your own cap. It never reports anything about the player, the
|
|
187
|
+
platform's spacing between offers, or any limit that spans games.
|
|
188
|
+
|
|
133
189
|
## If you do nothing
|
|
134
190
|
|
|
135
191
|
Nothing happens. A game that never calls `gem.ads.requestAd` is never interrupted, and the call is
|
package/docs/getting-started.md
CHANGED
|
@@ -50,7 +50,7 @@ either way.
|
|
|
50
50
|
```jsonc
|
|
51
51
|
// package.json
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@indiegems/gem-web-sdk": "
|
|
53
|
+
"@indiegems/gem-web-sdk": "9.0.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),
|
package/docs/manifest.json
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"documents": [
|
|
9
9
|
{
|
|
10
10
|
"file": "achievements.md",
|
|
11
|
-
"sha256": "
|
|
12
|
-
"updated": "2026-10-
|
|
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": "
|
|
37
|
-
"updated": "2026-10-
|
|
36
|
+
"sha256": "dd8afde50d0e34052ebdb8b894d30faa4814d4209fc62644fdc5ab53a1395e32",
|
|
37
|
+
"updated": "2026-10-08T00:55:05Z"
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"file": "cross-promotion.md",
|
|
41
|
-
"sha256": "
|
|
42
|
-
"updated": "2026-
|
|
41
|
+
"sha256": "4336d72595115950919361c36ab1af9beaa787c3c57d27fea27d42dac69d81cf",
|
|
42
|
+
"updated": "2026-10-07T19:15:38Z"
|
|
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": "95f645a73bfb522537fe14562d4a87b10a7f06af036c5658996c05bddced875b",
|
|
72
|
+
"updated": "2026-10-08T00:55:05Z"
|
|
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": "39332bd7b08adbb33d3b6382142d214e17d8f24a7776061a97f372d658e48974",
|
|
107
|
+
"updated": "2026-10-07T19:17:55Z"
|
|
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": "584ace965c1be20ca90ee1cd3beff7c1473f1ca9bccef428b3efb6846b2315a7",
|
|
127
|
+
"updated": "2026-10-08T00:55:05Z"
|
|
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": "7dc6bdc55e3cfbc88d667bbc2ebf908e357a1b6d768d5e3f4ce486ded614c1bf",
|
|
152
|
+
"updated": "2026-10-07T19:15:38Z"
|
|
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.
|