@indiegems/gem-web-sdk 15.3.2 → 16.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.
@@ -0,0 +1,289 @@
1
+ # Arcade features and the Arcade Store
2
+
3
+ Your game can reach a few things the Arcade owns: its own app and yours in the app stores, another
4
+ game, games like yours, and the **Arcade Store**, which sells Gem packs, Boosts, passes and
5
+ bundles for real money.
6
+
7
+ ```js
8
+ // From the press on your own "More games like this" card.
9
+ async function onCardPressed(card) {
10
+ await saveNow(); // the Arcade gives you a short grace, not a long one
11
+ const { outcome } = await gem.arcade.openGame(card.gameId);
12
+ if (outcome !== 'ok') showCardAsUnavailable(card);
13
+ }
14
+
15
+ // From the press on your own "Get Gems" button.
16
+ async function onBuyPressed(offer) {
17
+ const { outcome, reason } = await gem.store.purchase(offer.skuId);
18
+ // Read through gem.me; nothing here carries an amount. `error` is "outcome unknown" (§9).
19
+ if (outcome === 'ok' || reason === 'error') await refreshBalances();
20
+ }
21
+ ```
22
+
23
+ **Your game asks; the Arcade decides.** Nothing your game sends carries a URL, a price or a card
24
+ field. The Arcade builds every link, reads every price, draws every purchase sheet and takes the
25
+ payment itself. You get an outcome.
26
+
27
+ - [1. Two surfaces, and which one is real money](#1-two-surfaces-and-which-one-is-real-money)
28
+ - [2. Nothing rejects](#2-nothing-rejects)
29
+ - [3. What is available here](#3-what-is-available-here)
30
+ - [4. Call the acting ones from a press](#4-call-the-acting-ones-from-a-press)
31
+ - [5. App links](#5-app-links)
32
+ - [6. Opening another game](#6-opening-another-game)
33
+ - [7. Similar games, as cards](#7-similar-games-as-cards)
34
+ - [8. The Arcade Store's offers](#8-the-arcade-stores-offers)
35
+ - [9. Buying from the Arcade Store](#9-buying-from-the-arcade-store)
36
+ - [10. Outcomes and reasons](#10-outcomes-and-reasons)
37
+
38
+ ## 1. Two surfaces, and which one is real money
39
+
40
+ | | **`gem.arcade`** | **`gem.store`** |
41
+ | ------------ | ------------------------------------------------ | ----------------------------------------------- |
42
+ | What it does | app-store listings, another game, similar games | the Arcade Store's own SKUs |
43
+ | Money | none | **real money**, through the Arcade's own sheet |
44
+ | Who draws it | the Arcade (a new tab, a QR code, the next game) | the Arcade Store's sheet, over your paused game |
45
+
46
+ `gem.store` is not `gem.purchases`. **`gem.purchases` spends Gems or Coins on your game's own
47
+ items** ([`inventory-and-catalogs.md`](inventory-and-catalogs.md)). **`gem.store` sells the Arcade
48
+ Store's own SKUs for the player's money.** A game that wants a "Get Gems" button uses `gem.store`;
49
+ a game selling a sword for 50 Gems uses `gem.purchases`.
50
+
51
+ ## 2. Nothing rejects
52
+
53
+ Every call resolves `{ outcome, reason? , ... }`, and none of them ever rejects. `outcome` is one of
54
+ three and never grows:
55
+
56
+ - `ok`: it happened.
57
+ - `declined`: it did not, because of a choice or a rule (the player said no, or another sheet was
58
+ open).
59
+ - `unavailable`: it cannot happen here.
60
+
61
+ `reason` is a short code that says why. The set is open: a new Arcade build may send a reason your
62
+ game has never seen, so branch on `outcome` and treat `reason` as a hint. The codes in use today are
63
+ in [§10](#10-outcomes-and-reasons).
64
+
65
+ ## 3. What is available here
66
+
67
+ An Arcade that does not offer a feature answers every call for it at once with `unavailable` /
68
+ `unsupported`, and sends nothing. Read availability before you draw a button for it:
69
+
70
+ ```js
71
+ if (gem.arcade.available.openGame) showMoreGamesSection();
72
+ if (gem.arcade.available.appLinks) showGetTheAppButton();
73
+ if (gem.store.available) showGetGemsButton();
74
+ ```
75
+
76
+ `gem.arcade.available` is `{ appLinks, openGame }`, and `gem.store.available` is one boolean. All of
77
+ them are synchronous and `false` until the bridge is up. A feature can be off for reasons you
78
+ cannot see from inside the frame: a TV, an in-app browser that cannot take payment, an app build
79
+ where it is withheld. Do not guess; read it.
80
+
81
+ ## 4. Call the acting ones from a press
82
+
83
+ Three calls put something in front of the player: `gem.arcade.openApp`, `gem.arcade.openGame` and
84
+ `gem.store.purchase`. **Call each one from the player's own press handler**, in the same turn as the
85
+ press. The Arcade checks that a person pressed, and refuses an ask nobody pressed for with
86
+ `declined` / `busy`, showing nothing.
87
+
88
+ - **Controllers count.** A gamepad press the Arcade sees counts as a press. Do not add a keyboard or
89
+ pointer workaround for pad players.
90
+ - **One at a time.** Only one of these is open at once, across both surfaces and the Arcade's other
91
+ sheets (a Gem purchase, a share, a friend challenge). A second ask while one is open is
92
+ `declined` / `busy`. The SDK answers it for you without sending.
93
+ - **Never on a timer, never on load, never in a retry loop.** None of them would have a press, so
94
+ all of them would be refused.
95
+
96
+ The reads (`appLinks`, `similarGames`, `offers`) need no press. A second call while one is open
97
+ shares the first one's answer.
98
+
99
+ ## 5. App links
100
+
101
+ ```js
102
+ const links = await gem.arcade.appLinks();
103
+ if (links.outcome === 'ok') {
104
+ if (links.arcade) drawStoreBadge('arcade', links.arcade.store); // 'app_store' | 'google_play'
105
+ if (links.game) drawStoreBadge('game', links.game.store);
106
+ }
107
+
108
+ // From the badge's press:
109
+ const { outcome, reason } = await gem.arcade.openApp('arcade'); // or 'game'
110
+ ```
111
+
112
+ `appLinks()` tells you which listings the Arcade can open from here, **by store, never by URL**, so
113
+ you can draw the right badge. `arcade` is the Gem Arcade app; `game` is your game's own app, when
114
+ one is registered for it. Either may be missing; draw only what is there.
115
+
116
+ `openApp(target)` opens one. It answers `ok` when the listing opened, `ok` / `handed_off` when the
117
+ Arcade showed a QR code for a phone instead (on a TV, say), and `unavailable` / `already_in_app`
118
+ when the player is already in that app.
119
+
120
+ The answer comes **at the hand-off**: as the listing opens, or as the QR sheet goes up. It never
121
+ waits for the player to close the sheet, and you are not told when they do. While the sheet is up
122
+ it still counts as the open acting ask, so another `openApp`, `openGame` or `store.purchase` is
123
+ `declined` / `busy` until the player closes it.
124
+
125
+ ## 6. Opening another game
126
+
127
+ ```js
128
+ const { outcome, reason } = await gem.arcade.openGame('star-hopper'); // a game id or its web address
129
+ ```
130
+
131
+ `ref` is a game's id or its web-address slug, lower-case letters, digits and hyphens. The Arcade
132
+ looks the game up, answers `ok`, and then **your game is closing**: it gets the same short save
133
+ grace as any exit, and the next game starts.
134
+
135
+ - **Save ahead of time.** The grace is 250 ms, and your game also gets the usual termination
136
+ notice. Autosave before you call `openGame`, not after.
137
+ - `unavailable` / `not_found` covers a game that does not exist, is hidden, or cannot run here. The
138
+ Arcade does not say which.
139
+ - `declined` / `same_game` is your own game.
140
+ - **Back** in the Arcade takes the player to your game's page, with Play. It never relaunches your
141
+ game by itself.
142
+
143
+ ## 7. Similar games, as cards
144
+
145
+ ```js
146
+ const { outcome, games } = await gem.arcade.similarGames({ limit: 6 }); // 1..24, default 12
147
+ if (outcome !== 'ok' || games.length === 0) hideMoreGamesSection();
148
+ for (const game of games) drawCard(game);
149
+ ```
150
+
151
+ Each card carries the game's `gameId` and, when it has one, its `slug`; its `name`, `tagline`,
152
+ `shortDescription` and `tags`; and art: `iconUrl`, `capsuleLandscapeUrl`, `capsulePortraitUrl`,
153
+ and, when the Arcade judges a clip is welcome here, `previewVideoUrl` with `previewPosterUrl`.
154
+ Pass `gameId` to `openGame` when the card is pressed.
155
+
156
+ **Hide the section when there are no results.** An empty "More games" row is worse than none.
157
+
158
+ ### The text is not yours
159
+
160
+ `name`, `tagline`, `shortDescription` and each tag are `PlayerText`, as a player's name is: another
161
+ developer wrote them. **Render them as text only**: `text.toPlain(game.name)` into `textContent`,
162
+ never into `innerHTML`, never into a template that parses markup. A name may legitimately contain
163
+ `<` or `&`.
164
+
165
+ ### Drawing a card the way the Arcade does
166
+
167
+ Cards in your game should behave like the tiles on the Arcade's home page:
168
+
169
+ - **The clip starts after 450 ms of hover, or of keyboard or pad focus. Never on touch.** One card
170
+ plays at a time.
171
+ - **The `<video>` exists only while it plays.** Create it when the clip starts and remove it when it
172
+ stops. It is `muted`, `playsinline`, has no `controls`, shows `previewPosterUrl` until it plays,
173
+ and stops on blur or when the pointer leaves.
174
+ - **No `previewVideoUrl` means no clip.** The Arcade leaves it out under reduced motion, data saver
175
+ and on phones, which your frame cannot always see. Show the poster or the capsule.
176
+ - **The accessible name is "{name}, opens game"**, with `{name}` from `text.toPlain(game.name)`.
177
+ - **Your page's Content-Security-Policy must allow the hosts the Arcade's art comes from**, in
178
+ `img-src` and `media-src`, or every card is blank. See [the media hosts](#the-media-hosts). Load
179
+ the art with `crossOrigin = 'anonymous'` if a WebGL game draws it into a canvas.
180
+
181
+ ### The media hosts
182
+
183
+ Card art, preview clips and posters, and an offer's `imageUrl` are URLs the platform hands you.
184
+ Allow the hosts it returns. On the Arcade, today, those are two:
185
+
186
+ - **`https://downloads.gemarcade.com`, the public copy.** An offer's `imageUrl` is always on it,
187
+ and released card art and clips move there once the platform has published a public copy.
188
+ - **`https://gemsprodeastus2a.blob.core.windows.net`, signed art**: a card's art and clip before
189
+ the public copy lands.
190
+
191
+ Similar games are released games, so their cards come from these two hosts wherever your game
192
+ talks to the Arcade: `gemctl serve` and the Project Editor too, when they are signed in to it.
193
+
194
+ **Name each host exactly. Never allow `https://*.blob.core.windows.net`**: that wildcard is every
195
+ Azure customer's storage, not the Arcade's.
196
+
197
+ A non-production backend serves its art from hosts of its own. These hosts are infrastructure, and
198
+ they can change: art that will not load is the sign, and the browser's console names the host your
199
+ policy refused. Allow that host rather than a wildcard.
200
+
201
+ ## 8. The Arcade Store's offers
202
+
203
+ ```js
204
+ const { outcome, reason, offers } = await gem.store.offers();
205
+ if (outcome === 'unavailable' && reason === 'sign_in_required') showSignInButton(); // gem.account.requestSignIn
206
+ for (const offer of offers) drawOffer(offer);
207
+ ```
208
+
209
+ `offers()` lists what the Arcade Store sells this player, here, right now: Gem packs, Boosts,
210
+ passes and bundles. Each offer has a `skuId`, a `kind` (`gem_pack`, `boost`, `pass` or `bundle`),
211
+ the store's own `name` and `description`, an optional `imageUrl`, a `priceDisplay`, and, for a
212
+ Gem pack, a `grant` naming the currency and amount.
213
+
214
+ - **Show `priceDisplay` exactly as given.** It is the payment rail's own price in the player's
215
+ currency, already formatted. Never parse it, compare two of them, or compute a discount from it.
216
+ There is no numeric price.
217
+ - **Re-read offers when your game resumes.** What is on sale can change while the player is away.
218
+ - **Do not imitate the Arcade's chrome.** Your offer list is yours to style; the sheet that takes
219
+ the money is the Arcade's, and a game that draws something that looks like it confuses players
220
+ about which one is real.
221
+ - `unavailable` / `store_off` means the store is not offered here (a TV, an in-app browser that
222
+ cannot take payment, a build where it is withheld). Hide your store button.
223
+
224
+ ## 9. Buying from the Arcade Store
225
+
226
+ ```js
227
+ // From the press on your offer's Buy button.
228
+ const { outcome, reason } = await gem.store.purchase(offer.skuId);
229
+ ```
230
+
231
+ `purchase(skuId)` asks the Arcade to open its store's sheet for one SKU from `offers()`. The Arcade
232
+ pauses your game, shows the offer with its own price, and takes the payment on the provider's page
233
+ or the operating system's purchase sheet. Your game is resumed before the answer arrives, so your
234
+ own celebration is visible.
235
+
236
+ - **Call it from the press** ([§4](#4-call-the-acting-ones-from-a-press)).
237
+ - **It has no timeout.** A payment takes as long as it takes, so the call waits for as long as the
238
+ Arcade is connected.
239
+ - `ok`: the player paid. **Read balances and inventory through `gem.me`**; the answer carries no
240
+ amount.
241
+ - `ok` / `pending`: the player paid and the provider has not settled yet. Listen with
242
+ `gem.store.onSettled`, and also re-read balances when your game resumes, because a purchase can
243
+ settle while your game is not running:
244
+
245
+ ```js
246
+ const stop = gem.store.onSettled(({ skuId }) => refreshBalances());
247
+ ```
248
+
249
+ - `declined` / `cancelled`: the player closed the sheet. Return to your screen quietly and **do not
250
+ ask again unprompted**: the Arcade backs off after a dismissal, and the next ask is
251
+ `declined` / `busy` for a while.
252
+ - `unavailable` with `store_off`, `sign_in_required` or `not_found` (a SKU that is not in
253
+ `offers()` right now), or with a payment provider's own failure code: nothing was bought. The
254
+ first three show nothing.
255
+ - `unavailable` / `error`: **the outcome is unknown, and the player may have paid.** You get it
256
+ when the bridge to the Arcade closes while the sheet is open, which can happen after the payment
257
+ went through, and when the Arcade has no reliable answer. Never tell the player nothing was
258
+ bought. Re-read balances and inventory through `gem.me`, keep your `gem.store.onSettled`
259
+ listener, and re-read again when your game resumes:
260
+
261
+ ```js
262
+ if (outcome === 'unavailable' && reason === 'error') await refreshBalances(); // not "nothing bought"
263
+ ```
264
+
265
+ ## 10. Outcomes and reasons
266
+
267
+ | Call | `ok` | `declined` | `unavailable` |
268
+ | ---------------- | ------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------- |
269
+ | `appLinks` | listings to draw | | `unsupported`, `error` |
270
+ | `openApp` | opened, or `handed_off` | `busy` | `already_in_app`, `unsupported`, `error` |
271
+ | `openGame` | the next game is starting | `busy`, `same_game` | `not_found`, `unsupported`, `error` |
272
+ | `similarGames` | cards, possibly none | | `unsupported`, `error` |
273
+ | `store.offers` | offers, possibly none | | `sign_in_required`, `store_off`, `unsupported`, `error` |
274
+ | `store.purchase` | paid, or `pending` | `cancelled`, `busy` | `sign_in_required`, `store_off`, `not_found`, the provider's reason, `unsupported`, `error` (outcome unknown) |
275
+
276
+ Every reason is a short code of lower-case letters and underscores. The Arcade turns a payment
277
+ provider's own code into that shape before it reaches you.
278
+
279
+ Four reasons come from the SDK itself rather than the Arcade:
280
+
281
+ - `unsupported`: this Arcade does not offer the feature. Nothing was sent.
282
+ - `invalid_argument`: the SDK refused the argument (an unknown `target`, a `ref` that is not an id
283
+ or a web address, a `limit` outside 1–24, an empty `skuId`). Nothing was sent.
284
+ - `busy`: another acting ask from your game is still open. Nothing was sent.
285
+ - `error`: no reliable answer arrived. The ask could not be sent, a read or an open waited out its
286
+ timeout (30 seconds for a read, two minutes for `openApp` and `openGame`), the bridge closed
287
+ before the answer came, or the Arcade said it had no reliable answer. For a read, try again
288
+ later. For `store.purchase` it means **the outcome is unknown**, never "nothing was bought"
289
+ ([§9](#9-buying-from-the-arcade-store)).
@@ -203,10 +203,11 @@ player got.
203
203
 
204
204
  Nothing sells Coins or achievement points, alone or in a bundle. The arcade's own store sells
205
205
  Gems, and the Gems a player bought there spend in your game exactly as the ones they earned do: your
206
- game cannot tell the two apart and prices nothing differently for either. Your game sells neither,
207
- and never sends a player to the store. The real-money
208
- product that changes what your game pays is a **Boost**, which a player buys from the arcade, never
209
- from your game:
206
+ game cannot tell the two apart and prices nothing differently for either. Your game never sells
207
+ either itself. It may put the arcade store's own Gem packs and Boosts in front of a player from a
208
+ button of its own, through `gem.store`, and the arcade prices them, shows its own sheet and takes
209
+ the money ([arcade-and-store.md](arcade-and-store.md)). The real-money product that changes what
210
+ your game pays is a **Boost**, which a player always buys on the arcade's own sheet:
210
211
 
211
212
  - **It multiplies what Gems and Coins grants pay, and the ceilings with them.** A player on a 2×
212
213
  Boost earns twice as much per grant, may earn twice as much per window and per day, and may hold
@@ -242,11 +243,14 @@ animates every payout into the right total. Gems and Coins follow the same rules
242
243
  and must not compute a maximum. To show how much more your game can pay, read
243
244
  `gem.platform.earnAllowance.get()` (one consolidated figure, display only); the arcade still tells
244
245
  the player when they reach the maximum.
245
- - **No top-up or payment screen of your own.** Never draw a "get more Gems" or "buy Coins" offer,
246
- a checkout, a wallet top-up, or anything shaped like the arcade's purchase sheet: a game screen
247
- that looks like platform payment chrome is an imitation of it, and players cannot tell the two
248
- apart. Your shop's own Buy button is yours. When a purchase is refused for
249
- `insufficient_funds`, say so in your shop's own words ("Not enough Coins") and stop there.
246
+ - **No top-up or payment screen of your own.** Never draw a "buy Coins" offer, a checkout, a
247
+ wallet top-up, or anything shaped like the arcade's purchase sheet: a game screen that looks like
248
+ platform payment chrome is an imitation of it, and players cannot tell the two apart. Your shop's
249
+ own Buy button is yours, and so is a "Get Gems" button that calls `gem.store.purchase` from the
250
+ player's press: the sheet it opens is the arcade store's own
251
+ ([arcade-and-store.md](arcade-and-store.md)). When a purchase is refused for
252
+ `insufficient_funds`, say so in your shop's own words ("Not enough Coins") and stop there: a
253
+ refusal never opens the store by itself.
250
254
  - **No amount at all when you are not connected to the arcade.** Hide every platform price and
251
255
  reward unless `gem.awaitBridge()` settled `connected`.
252
256
  - **Draw a currency with the art the SDK ships**: the icon beside an amount, the hero only on a
package/docs/changelog.md CHANGED
@@ -16,6 +16,32 @@ 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
+ ## 16.0.0
20
+
21
+ Upgrading from 15.3.3. Impact: required.
22
+
23
+ ### Must change
24
+
25
+ - If you run the parent conformance kit against your own parent, it now reports version 2. Update your test fixture to provide the new userActivation and reportError members. A parent that advertises the new app links, open game or store capabilities must also expose surfacesShown and dismiss so the new cases can run. Cases for capabilities the parent does not advertise pass vacuously, but the check that an unadvertised capability is refused always runs.
26
+
27
+ ### Rebuild
28
+
29
+ - The lists and unions of messages sent between game and parent each gained entries for app links, opening an app or game, similar games and the store. Recompiling is enough. If your code switches exhaustively over these message types, add cases for the new ones or let them fall through to your default handling.
30
+ - The capability type gained three new names: appLinks, openGame and store. Recompiling is enough. If you keep an exhaustive mapping over capabilities, add entries for these three.
31
+ - The size check for raw messages now takes an optional cap argument. Existing calls compile and behave as before, so recompiling is enough.
32
+
33
+ ### New capability
34
+
35
+ - Games can now use Gem.arcade to ask the Arcade for app-store links, to open an app listing, to open another game, and to fetch similar games as cards. An availability check tells you whether the Arcade supports each feature. Every call resolves with an outcome of ok, declined or unavailable plus a reason, and never rejects. If the Arcade does not advertise the capability, the call resolves unavailable without sending anything. Calls that act on the player need a real press from the player, and only one such call can be open at a time. Draw cards the way the Arcade draws its tiles, and treat card text as player text.
36
+ - Games can now use Gem.store to read the platform store's offers, start a purchase, and listen for purchases that settle later. Calls resolve with an outcome and never reject, and a store that is not advertised resolves as unavailable without sending anything. A purchase needs a real press from the player, and only one can be open at a time. If a purchase comes back without success, treat the result as unknown rather than as proof that nothing was bought, because the connection can close after the player has paid. Re-read the player's holdings and keep your settlement listener attached. Show prices exactly as given.
37
+ - If you embed a game with the frame class, you can now send any of the new Arcade or store asks through a single requestArcade call. It is gated on the capability the message belongs to. Answers to those asks are delivered to a new onArcadeAnswer callback that you can pass in the frame options.
38
+
39
+ ## 15.3.3
40
+
41
+ Upgrading from 15.3.2. Impact: none.
42
+
43
+ Nothing in this release asks anything of a game.
44
+
19
45
  ## 15.3.2
20
46
 
21
47
  Upgrading from 15.3.1. Impact: none.
@@ -50,7 +50,7 @@ either way.
50
50
  ```jsonc
51
51
  // package.json
52
52
  "dependencies": {
53
- "@indiegems/gem-web-sdk": "15.3.2"
53
+ "@indiegems/gem-web-sdk": "16.0.0"
54
54
  }
55
55
  ```
56
56
 
@@ -166,8 +166,10 @@ open the dev server's own address.
166
166
 
167
167
  ## 5. Tell the arcade when you are ready
168
168
 
169
- The arcade shows a loading state until your game says it is playable. If you never say so, a
170
- player watches a spinner over a game that is already running.
169
+ The arcade covers your game with its own loading screen until your game says it is playable. If
170
+ you never say so, a player watches that cover over a game that is already running. A game that
171
+ paints its own loading screen can show it instead, with one line on its store page
172
+ ([the loading contract](what-a-game-must-do.md#a-game-that-paints-its-own-loading-screen)).
171
173
 
172
174
  ```js
173
175
  gem.frame.progress(loaded, total); // while assets load
@@ -824,10 +824,12 @@ the same strings.
824
824
  Reasons are for your shop's state and your logs. Branch money on `outcome` and on a re-read of
825
825
  inventory, never on a reason, and do not retry because of one.
826
826
 
827
- ⚠️ **Never draw a top-up or payment screen of your own** — no "get more Gems", no checkout, nothing
827
+ ⚠️ **Never draw a top-up or payment screen of your own** — no checkout, no wallet top-up, nothing
828
828
  that looks like the arcade's purchase sheet or wallet. A game screen shaped like platform payment
829
- chrome is an imitation of it. Your Buy button is yours; asking a player to add currency or pay is
830
- the arcade's alone.
829
+ chrome is an imitation of it. Your Buy button is yours, and taking a player's money is the
830
+ arcade's alone: a "Get Gems" button of yours calls `gem.store.purchase` from the player's press,
831
+ and the arcade store's own sheet does the rest ([arcade-and-store.md](arcade-and-store.md)). An
832
+ `insufficient_funds` refusal never opens it by itself.
831
833
 
832
834
  ### What Coins and Gems may buy: the spend scope
833
835
 
@@ -1039,7 +1041,9 @@ remove it. A player spends it through the platform, by choosing to.
1039
1041
 
1040
1042
  **You cannot sell for real money here.** A platform currency a player _earns_ is what this surface
1041
1043
  reaches. Charging actual money is a separate platform capability and is not something a game config
1042
- grants itself.
1044
+ grants itself. What a game _can_ do is offer the arcade store's own Gem packs, Boosts, passes and
1045
+ bundles from a button of its own, through `gem.store`
1046
+ ([arcade-and-store.md](arcade-and-store.md)): the arcade prices them and takes the money.
1043
1047
 
1044
1048
  ## Errors
1045
1049
 
@@ -16,6 +16,11 @@
16
16
  "sha256": "afced8f94514818eca6de3e72bdd988b2cee10925086c3bfa47c0a8ec8967454",
17
17
  "updated": "2026-10-23T00:13:14Z"
18
18
  },
19
+ {
20
+ "file": "arcade-and-store.md",
21
+ "sha256": "35d71f81775c0e7b05c84626bbe8365b624a60fb751abe2a2b3126ba554f33ab",
22
+ "updated": "2026-10-28T21:07:26Z"
23
+ },
19
24
  {
20
25
  "file": "arcade-avatars.md",
21
26
  "sha256": "e02e435d91e389f9cc2025945ebc0ac11882bac85a4e6d5d74278f11e38fee91",
@@ -23,8 +28,8 @@
23
28
  },
24
29
  {
25
30
  "file": "arcade-economy.md",
26
- "sha256": "1ebae3aeff2480722b09bc15d8a997656238133e757c9588a0eaa9479b990270",
27
- "updated": "2026-10-25T19:46:22Z"
31
+ "sha256": "aca343bf98c0e111bca7d27f504bf71b43802b98e5cd649792dfd7c5f30b1b16",
32
+ "updated": "2026-10-28T18:37:42Z"
28
33
  },
29
34
  {
30
35
  "file": "async-opponents.md",
@@ -38,8 +43,8 @@
38
43
  },
39
44
  {
40
45
  "file": "changelog.md",
41
- "sha256": "82f8ae669046bd8ffed7700156bad11941485aa463711baaf7de8ccb8ffa4001",
42
- "updated": "2026-10-26T19:26:21Z"
46
+ "sha256": "739709af2b5a0918ea7041f6f3d50142c3182aca892bc388dee51ad1a0f64c41",
47
+ "updated": "2026-10-28T21:16:25Z"
43
48
  },
44
49
  {
45
50
  "file": "daily-challenges.md",
@@ -68,13 +73,13 @@
68
73
  },
69
74
  {
70
75
  "file": "getting-started.md",
71
- "sha256": "e30a7fbe2872cf242dce30b1d0bd5b22b0bbd30e47615b8f7c18125febd0dbef",
72
- "updated": "2026-10-26T19:26:21Z"
76
+ "sha256": "b3c215882b2ea836e79e37553277b97914f6947e8e3bbeb10e2a2e0667d2fb86",
77
+ "updated": "2026-10-28T21:16:25Z"
73
78
  },
74
79
  {
75
80
  "file": "inventory-and-catalogs.md",
76
- "sha256": "4295b4a12770584a80d94a94d95e464553569d7318229413cdfbb604b6e1f9d8",
77
- "updated": "2026-10-25T19:46:22Z"
81
+ "sha256": "e7bf9f38bf4ede778fdaca200186b782870f409949743e4e85f22f9c919cd19c",
82
+ "updated": "2026-10-28T18:37:42Z"
78
83
  },
79
84
  {
80
85
  "file": "leaderboards.md",
@@ -128,8 +133,8 @@
128
133
  },
129
134
  {
130
135
  "file": "server-bundles.md",
131
- "sha256": "1042da92cef0807c7d3810b4efeba4a74cc2efeff1d963809bacb25cb308ad21",
132
- "updated": "2026-10-26T19:26:21Z"
136
+ "sha256": "b4a7918f6b10b754976937f216ee8679260166a0377dcb0448fd42d65bdb2f31",
137
+ "updated": "2026-10-28T21:16:25Z"
133
138
  },
134
139
  {
135
140
  "file": "serverless-functions-js.md",
@@ -153,8 +158,8 @@
153
158
  },
154
159
  {
155
160
  "file": "what-a-game-must-do.md",
156
- "sha256": "c449db410f2d7d9b1c7cfb305754103a1f4f03fb62781d9a1dc1e7dfb3fa459c",
157
- "updated": "2026-10-23T22:49:46Z"
161
+ "sha256": "e020dca72e597dae0bfa1a71e3263dbb3234f9515d464944e66d7f89612f8db1",
162
+ "updated": "2026-10-28T00:47:43Z"
158
163
  }
159
164
  ]
160
165
  }
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "$comment": [
3
- "The published fields whose value is PlayerText (player-authored text), and the",
4
- "first release from which each has been PlayerText in that entry. String(), a",
5
- "template literal or JSON.stringify on one shows \"[gem: use gem.text.toPlain()]\";",
6
- "convert it with text.toPlain(). `entry` is the package export (`sdk` is",
3
+ "The published fields whose value is PlayerText (player-authored text) or a list",
4
+ "of it, and the first release from which each has been that in that entry.",
5
+ "String(), a template literal or JSON.stringify on one shows",
6
+ "\"[gem: use gem.text.toPlain()]\"; convert it, or each element of a list, with",
7
+ "text.toPlain(). `entry` is the package export (`sdk` is",
7
8
  "@indiegems/gem-web-sdk/sdk), `field` is <Type>.<property>, and `since` is null",
8
9
  "for a field no release has shipped yet. Written by",
9
10
  "`node scripts/player-text-fields.mjs`."
@@ -75,6 +76,26 @@
75
76
  "field": "Friend.displayTag",
76
77
  "since": "5.11.0"
77
78
  },
79
+ {
80
+ "entry": "game",
81
+ "field": "GameCard.name",
82
+ "since": "16.0.0"
83
+ },
84
+ {
85
+ "entry": "game",
86
+ "field": "GameCard.shortDescription",
87
+ "since": "16.0.0"
88
+ },
89
+ {
90
+ "entry": "game",
91
+ "field": "GameCard.tagline",
92
+ "since": "16.0.0"
93
+ },
94
+ {
95
+ "entry": "game",
96
+ "field": "GameCard.tags",
97
+ "since": "16.0.0"
98
+ },
78
99
  {
79
100
  "entry": "game",
80
101
  "field": "GemPlayer.displayHandle",
@@ -140,6 +161,26 @@
140
161
  "field": "Friend.displayTag",
141
162
  "since": "5.11.0"
142
163
  },
164
+ {
165
+ "entry": "sdk",
166
+ "field": "GameCard.name",
167
+ "since": "16.0.0"
168
+ },
169
+ {
170
+ "entry": "sdk",
171
+ "field": "GameCard.shortDescription",
172
+ "since": "16.0.0"
173
+ },
174
+ {
175
+ "entry": "sdk",
176
+ "field": "GameCard.tagline",
177
+ "since": "16.0.0"
178
+ },
179
+ {
180
+ "entry": "sdk",
181
+ "field": "GameCard.tags",
182
+ "since": "16.0.0"
183
+ },
143
184
  {
144
185
  "entry": "sdk",
145
186
  "field": "GemPlayer.displayHandle",
@@ -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/v15.3.2/gem-web-sdk.tgz
84
- curl -fsSL https://downloads.gemarcade.com/sdk/v15.3.2/gem-web-sdk.tgz.sri
83
+ curl -fsSL -O https://downloads.gemarcade.com/sdk/v16.0.0/gem-web-sdk.tgz
84
+ curl -fsSL https://downloads.gemarcade.com/sdk/v16.0.0/gem-web-sdk.tgz.sri
85
85
  openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
86
86
  ```
87
87
 
@@ -227,9 +227,10 @@ The parent shows its loading state until your frame says otherwise. The whole co
227
227
  sentences:
228
228
 
229
229
  - **`ready()` means "there is something worth looking at"** — your boot screen, not your
230
- world-ready. Measured on the Arcade: the parent's loading state is a plain page it abandons after
231
- ~10s regardless, so holding `ready` until world-ready hides your own, more informative screen and
232
- buys nothing.
230
+ world-ready. On the Arcade the parent's loading state is a cover of your game's key art over the
231
+ whole frame, and it stays until `ready()` arrives or your frame has gone quiet for the handshake
232
+ wait (10s, re-armed by every `progress`). Holding `ready` until world-ready hides your own, more
233
+ informative screen behind that cover and buys nothing.
233
234
  - **`ready()` is sent exactly once per session, and the SDK enforces it.** It is held while the
234
235
  bridge is down and delivered on connect, so call order cannot lose it; a call after one has gone
235
236
  out is suppressed and reported through `onDropped`. This is what makes the `finally` backstop
@@ -242,6 +243,38 @@ sentences:
242
243
  be throttled, and a progress call after `ready` is meaningless to a parent that has already
243
244
  unveiled you.
244
245
 
246
+ ### A game that paints its own loading screen
247
+
248
+ A game whose page paints its own loading screen from its very first paint can show that screen
249
+ instead of the Arcade's cover. Declare it on your store page:
250
+
251
+ ```yaml
252
+ loading_screen: game
253
+ ```
254
+
255
+ `arcade` is the default, and it is what an absent line means. With `game`, the Arcade still covers
256
+ everything up to your frame's `load` event — its own lookup and start, and your page while it
257
+ loads — and then lifts its cover at once, so your screen shows.
258
+
259
+ - **Your `index.html` must paint a full-screen loading screen in static HTML and CSS, before any
260
+ script runs** — a background that fills the frame, and something on it: your title, your logo, a
261
+ bar. Never white, and never the browser's default background, which is white. At `load` the
262
+ player sees whatever your document has painted, and a screen your script or engine draws may not
263
+ have drawn yet on a slow phone. Declare `game` only when that static screen is in place.
264
+ - **`load` waits for everything your document loads up front** — its stylesheets, images and
265
+ scripts. Keep `index.html` small, as
266
+ [the boot phase](#the-boot-phase-settle-the-bridge-before-the-heavy-bundle) already asks, or the
267
+ cover stays up while it downloads.
268
+ - **`ready()` is still required, and the rules above still hold.** Only the cover goes early. The
269
+ Arcade does not count the play until `ready()` arrives, or until the quiet deadline above passes
270
+ for a game that never sends it.
271
+ - **Values are `arcade` and `game`.** Any other value is refused when the store page is uploaded. A
272
+ `gemctl` that predates the field refuses it as an unknown field.
273
+ - **The store page on the channel being played is read**: dev's when you play on dev, release's on
274
+ release, so you can try it on dev before you promote it. An experiment variant is never read.
275
+ - **`gemctl serve` and the Gem Editor keep the Arcade's cover**, because they have no store page.
276
+ To see your own screen as players will, open your game's store page on the dev channel.
277
+
245
278
  ## Boot diagnostics: where the seconds went
246
279
 
247
280
  `gem.boot` is a timeline of named marks, one greppable line out of `gem.boot.timeline()`:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indiegems/gem-web-sdk",
3
- "version": "15.3.2",
3
+ "version": "16.0.0",
4
4
  "description": "postMessage bridge contract between gem-web and browser-playable games",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",