@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.
- package/CHANGELOG.md +26 -0
- package/dist/conformance.d.ts +7 -1
- package/dist/game.d.ts +187 -1
- package/dist/gem-web-sdk.bridge.js +2 -2
- package/dist/gem-web-sdk.conformance.js +3 -3
- package/dist/gem-web-sdk.esm.js +3 -3
- package/dist/gem-web-sdk.function.js +3 -3
- package/dist/gem-web-sdk.game.js +3 -3
- package/dist/gem-web-sdk.min.js +3 -3
- package/dist/gem-web-sdk.sdk.js +3 -3
- package/dist/gem-web-sdk.server.js +2 -2
- package/dist/index.d.ts +171 -7
- package/dist/sdk.d.ts +187 -1
- package/docs/api/api-reference-function.md +1 -1
- package/docs/api/api-reference-game.md +762 -3
- package/docs/api/api-reference-server.md +1 -1
- package/docs/api/reference.json +1203 -190
- package/docs/arcade-and-store.md +289 -0
- package/docs/arcade-economy.md +13 -9
- package/docs/changelog.md +26 -0
- package/docs/getting-started.md +5 -3
- package/docs/inventory-and-catalogs.md +8 -4
- package/docs/manifest.json +17 -12
- package/docs/player-text-fields.json +45 -4
- package/docs/server-bundles.md +2 -2
- package/docs/what-a-game-must-do.md +36 -3
- package/package.json +1 -1
- package/schema/bridge.v1.json +735 -5
- package/schema/capabilities.public.json +28 -0
|
@@ -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)).
|
package/docs/arcade-economy.md
CHANGED
|
@@ -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
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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 "
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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.
|
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": "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
|
|
170
|
-
player watches
|
|
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
|
|
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
|
|
830
|
-
|
|
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
|
|
package/docs/manifest.json
CHANGED
|
@@ -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": "
|
|
27
|
-
"updated": "2026-10-
|
|
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": "
|
|
42
|
-
"updated": "2026-10-
|
|
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": "
|
|
72
|
-
"updated": "2026-10-
|
|
76
|
+
"sha256": "b3c215882b2ea836e79e37553277b97914f6947e8e3bbeb10e2a2e0667d2fb86",
|
|
77
|
+
"updated": "2026-10-28T21:16:25Z"
|
|
73
78
|
},
|
|
74
79
|
{
|
|
75
80
|
"file": "inventory-and-catalogs.md",
|
|
76
|
-
"sha256": "
|
|
77
|
-
"updated": "2026-10-
|
|
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": "
|
|
132
|
-
"updated": "2026-10-
|
|
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": "
|
|
157
|
-
"updated": "2026-10-
|
|
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)
|
|
4
|
-
"first release from which each has been
|
|
5
|
-
"template literal or JSON.stringify on one shows
|
|
6
|
-
"
|
|
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",
|
package/docs/server-bundles.md
CHANGED
|
@@ -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/
|
|
84
|
-
curl -fsSL https://downloads.gemarcade.com/sdk/
|
|
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.
|
|
231
|
-
|
|
232
|
-
|
|
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()`:
|