@indiegems/gem-web-sdk 7.6.0 → 7.8.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/dist/function.d.ts +20 -4
- package/dist/game.d.ts +25 -5
- 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-api.js +1 -1
- package/dist/gem-web-sdk.server.js +2 -2
- package/dist/index.d.ts +4 -0
- package/dist/sdk.d.ts +61 -11
- package/dist/server-api.d.ts +33 -6
- package/dist/server.d.ts +36 -6
- package/docs/api/api-reference-game.md +370 -9
- package/docs/api/api-reference-server.md +1 -1
- package/docs/api/reference.json +383 -9
- package/docs/daily-challenges.md +14 -2
- package/docs/dedicated-server-flow.md +23 -14
- package/docs/getting-started.md +1 -1
- package/docs/leaderboards.md +140 -0
- package/docs/manifest.json +14 -14
- package/docs/server-bundles.md +2 -2
- package/docs/serverless-functions.md +35 -5
- package/docs/what-a-game-must-do.md +44 -21
- package/package.json +1 -1
package/docs/leaderboards.md
CHANGED
|
@@ -28,6 +28,10 @@ response, and their rank among everyone is not the rank most screens want anyway
|
|
|
28
28
|
last runs"_. `get()` is for a standings screen. `friends()` is for a standings screen among people
|
|
29
29
|
the player knows.
|
|
30
30
|
|
|
31
|
+
**Want today's standings rather than all-time ones?** That is the same four reads over a board
|
|
32
|
+
declared `per_period` — one board for each day of a daily challenge — and it has its own section:
|
|
33
|
+
[A board for each day](#a-board-for-each-day-period-boards).
|
|
34
|
+
|
|
31
35
|
`character()` is `mine()` for a character. **Read the leg your server WRITES.** Runs carry the
|
|
32
36
|
entity that owns them, so if your server reports character-owned runs, `mine()` returns nothing and
|
|
33
37
|
`character()` is the read you want — and vice versa. That mismatch is the commonest reason one of
|
|
@@ -226,6 +230,139 @@ is the second most likely reason a board looks empty.
|
|
|
226
230
|
const theirs = await gem.leaderboards.character('time-trial', characterId);
|
|
227
231
|
```
|
|
228
232
|
|
|
233
|
+
## A board for each day — period boards
|
|
234
|
+
|
|
235
|
+
Everything above ranks **every run a board has ever been given**. A daily challenge wants something
|
|
236
|
+
else: _today's_ standings, which start empty each day and close when the day does. Declare the board
|
|
237
|
+
`per_period` and it exists once per period of a challenge — one board for each day of a daily
|
|
238
|
+
challenge, each week of a weekly one, each month of a monthly one.
|
|
239
|
+
|
|
240
|
+
You do not give the board a schedule. It takes the challenge's: the same days, the same reset
|
|
241
|
+
time, the same time-zone rule. A board cannot drift from the challenge it belongs to, because it
|
|
242
|
+
has no clock of its own.
|
|
243
|
+
|
|
244
|
+
### Declare it
|
|
245
|
+
|
|
246
|
+
Three pieces, and the middle one is the link. The run type names the **challenge** its runs belong
|
|
247
|
+
to — the `id` of a progression in the same config with a `reset_schedule` — and the board asks to
|
|
248
|
+
be split by that challenge's periods:
|
|
249
|
+
|
|
250
|
+
```yaml
|
|
251
|
+
progressions:
|
|
252
|
+
- id: 4f2a9c71-3e8b-4d15-9a62-7c0e5b1d8f43
|
|
253
|
+
slug: daily-puzzle
|
|
254
|
+
config:
|
|
255
|
+
completion_threshold: 1
|
|
256
|
+
reset_schedule: { type: daily }
|
|
257
|
+
|
|
258
|
+
run_types:
|
|
259
|
+
- id: daily-run
|
|
260
|
+
int_value_1_name: moves
|
|
261
|
+
challenge: 4f2a9c71-3e8b-4d15-9a62-7c0e5b1d8f43
|
|
262
|
+
|
|
263
|
+
leaderboards:
|
|
264
|
+
- id: daily
|
|
265
|
+
run_type: daily-run
|
|
266
|
+
order_by:
|
|
267
|
+
- column: int_value_1
|
|
268
|
+
direction: asc
|
|
269
|
+
per_period: true
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
- **If you already have a daily challenge, use its id.** The progression in
|
|
273
|
+
[daily-challenges.md](daily-challenges.md) is the same kind of thing; nothing new is needed.
|
|
274
|
+
- **The challenge does not have to be shown on your game's card.** A progression with a
|
|
275
|
+
`reset_schedule` and nothing else is enough — no `platform_display`, no stored content.
|
|
276
|
+
- **`daily`, `weekly`, `monthly` and a cron expression all work.** A progression with no
|
|
277
|
+
`reset_schedule`, or one that resets on an event, has no periods and is refused at upload.
|
|
278
|
+
- **One set of runs can back two boards.** Leave `per_period` off a second board over the same run
|
|
279
|
+
type and it ranks every run: "today" and "all time" from the same reports.
|
|
280
|
+
|
|
281
|
+
### Record a run
|
|
282
|
+
|
|
283
|
+
Nothing changes in how a run is reported — your [serverless function](serverless-functions.md) or
|
|
284
|
+
your dedicated server reports it, never the browser. What changes is that the platform files each
|
|
285
|
+
run under a period:
|
|
286
|
+
|
|
287
|
+
- **The platform decides the period, from when it receives the run and the player's own time
|
|
288
|
+
zone.** Not from the device clock, and not from the run's `occurred_at`.
|
|
289
|
+
- **Name the period the run was played for.** Send `period_id` on the run, set to the challenge's
|
|
290
|
+
`periodLocalDate` from `gem.challenges.list()` as the player's game had it. A run that finishes
|
|
291
|
+
seconds before midnight and is checked seconds after still counts for the day it was played,
|
|
292
|
+
for **10 minutes** after that day ends. Leave it out and the run goes to whatever period it
|
|
293
|
+
arrives in — tomorrow's board, for that late run.
|
|
294
|
+
- ⚠️ **A run for a period that is not open for the player is refused, and the whole batch with
|
|
295
|
+
it** — `409 run_period_closed`. That is a day that ended more than ten minutes ago, or one that
|
|
296
|
+
has not started for them. Do not retry it: tell the player the day has closed.
|
|
297
|
+
- ⚠️ **Every run is a row.** A period board ranks every run it is given, so a player who plays
|
|
298
|
+
five times today holds five rows. For one row per player, have your function record one run per
|
|
299
|
+
player per period — `gem.leaderboards.mine('daily')` tells it whether today already has one.
|
|
300
|
+
|
|
301
|
+
### Read it
|
|
302
|
+
|
|
303
|
+
Every read answers for **one period**, and says which:
|
|
304
|
+
|
|
305
|
+
```js
|
|
306
|
+
const today = await gem.leaderboards.get('daily');
|
|
307
|
+
const yesterday = await gem.leaderboards.get('daily', { periodsBack: 1 });
|
|
308
|
+
|
|
309
|
+
heading(today.period.localDate, today.period.final ? 'Final' : 'So far');
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
| Option | Reads |
|
|
313
|
+
| ----------------- | --------------------------------------------------------------------------------- |
|
|
314
|
+
| _(none)_ | the player's current period |
|
|
315
|
+
| `periodsBack: n` | `n` periods before it — yesterday for a daily, last week for a weekly. 0 to 400 |
|
|
316
|
+
| `period: id` | exactly that period, from an earlier result's `period.id` — for a shared link or a history screen |
|
|
317
|
+
|
|
318
|
+
`friends()`, `mine()` and `character()` take the same options. On `mine()` the runs, the personal
|
|
319
|
+
best and the total are that one period's, so `personalBest` is "your best today" and an empty
|
|
320
|
+
result each morning is the ordinary state.
|
|
321
|
+
|
|
322
|
+
The result's `period` is absent on an ordinary board and carries, on a period board:
|
|
323
|
+
|
|
324
|
+
| Field | What it is |
|
|
325
|
+
| -------------------- | ------------------------------------------------------------------------------- |
|
|
326
|
+
| `id` | the board's identity. Pass it back as `period` to read this same board again |
|
|
327
|
+
| `localDate` | the date — the same value the challenge gives as `periodLocalDate`. Absent for a cron challenge |
|
|
328
|
+
| `startAt`, `endsAt` | the **player's own** day, in Unix seconds |
|
|
329
|
+
| `opensAt`, `closesAt` | when the board accepts runs from **anyone**, in Unix seconds |
|
|
330
|
+
| `final` | `true` once no run can still arrive |
|
|
331
|
+
|
|
332
|
+
⚠️ **Ask the platform which day it is; never work it out.** The player's day is a date on their
|
|
333
|
+
own calendar, in the time zone the platform holds for them. A board id built from the device clock
|
|
334
|
+
— `new Date().toISOString().slice(0, 10)` — names the wrong board for every player whose day is
|
|
335
|
+
not the UTC day, and for the hours either side of midnight for everyone else. Read with no option
|
|
336
|
+
for today, and take `period.id` from the result when you need to name it.
|
|
337
|
+
|
|
338
|
+
### One day, many time zones
|
|
339
|
+
|
|
340
|
+
On the default `local` boundary each player's day is their own: it begins at their midnight.
|
|
341
|
+
**Players on the same date share one board, whatever their time zone** — the same board the
|
|
342
|
+
challenge's seed gives them all. Three consequences follow, and each one surprises somebody:
|
|
343
|
+
|
|
344
|
+
- **A day's board stays open about 50 hours.** It opens when the date begins in the earliest time
|
|
345
|
+
zone and closes when it ends in the latest. So the standings can change after _your_ day has
|
|
346
|
+
ended, and "you finished 3rd today" shown at the player's midnight can be wrong an hour later.
|
|
347
|
+
Say "so far" until `period.final` is `true`. Two or three days' boards are open at any moment.
|
|
348
|
+
- **Players whose day starts later see the earlier results first.** The puzzle is the same for
|
|
349
|
+
everyone on the date, so a player in a later time zone can see scores — and may be handed
|
|
350
|
+
solutions — before they play. If your board has to be strictly fair, put the challenge on one
|
|
351
|
+
clock for everybody: `reset_schedule: { type: daily, boundary: UTC }`. Everyone's day then turns
|
|
352
|
+
over at the same instant, and the board is open for exactly that day.
|
|
353
|
+
- **Rows carry no time.** `occurredAt` is `0` on a period board that follows each player's own time
|
|
354
|
+
zone: beside the board's date, a run's time would show where its player is. Render nothing for
|
|
355
|
+
`0`. Ranks are unaffected.
|
|
356
|
+
|
|
357
|
+
**Changing time zone does not buy a longer day.** A player's first run on a date fixes when that
|
|
358
|
+
date ends for them. Runs after that moment are refused for the date, even if the player has since
|
|
359
|
+
moved to a time zone where it is still running.
|
|
360
|
+
|
|
361
|
+
**From a serverless function or a dedicated server, name the player.** Their credential carries no
|
|
362
|
+
player, so a period read is answered in UTC unless you pass
|
|
363
|
+
`{ playerId }` — which is not that player's day on a `local` challenge. In the browser the read is
|
|
364
|
+
always for the signed-in player and `playerId` is ignored.
|
|
365
|
+
|
|
229
366
|
## Where the rows come from
|
|
230
367
|
|
|
231
368
|
Runs are reported **from the server side** — by a dedicated server during its match, or by your
|
|
@@ -262,6 +399,9 @@ a `code`, and testing `err.reason === 'bad_request'` never matches, because a 40
|
|
|
262
399
|
| `reason: 'invalid'`, `code: 'invalid_bucket'` | bucket values do not match the board's `bucket_by` | supply exactly the buckets the board declares |
|
|
263
400
|
| `reason: 'not_found'`, `code: 'leaderboard_not_found'`, on `mine()` / `character()` | **also** the entity is not one you may read — an unknown character, one you do not own, one in another game, or a malformed id | **the same code as the row above**, which is the point: the entity read answers ONE refusal for every reason that depends on the target, deliberately, because a distinguishable answer would tell a caller whether a given player plays this game. Check the entity id as well as the board id — nothing in the response separates them |
|
|
264
401
|
| `reason: 'forbidden'`, on `mine()` / `character()` / `friends()` | the caller may not act for this entity at all, or (on `friends()`) carries no player identity at all — a game server token, say. Decided from the caller's own claims, not from what was asked for | this one is worth telling apart, and it is not about the target: it will not start working because the player played more. Treat it as a bug in how the call was made, not as an empty board. `get()` never answers this — the global board needs no viewer |
|
|
402
|
+
| `reason: 'invalid'`, `code: 'invalid_period'` | `periodsBack` or `period` asked of a board that is not `per_period`; or a `period` that is not one of the challenge's, or more than 400 periods back | a board that ranks every run has no periods to select: drop the option, or declare the board `per_period`. Otherwise take the id from an earlier result's `period.id` rather than building one |
|
|
403
|
+
| `reason: 'invalid'`, `code: 'period_not_begun'` | the `period` asked for has not begun for this player, even if it has for players in time zones ahead of theirs | read with no option for the player's own current period |
|
|
404
|
+
| `TypeError` (thrown, no request sent) | `periodsBack` out of 0–400, a `period` that is not a period id, or both passed at once | pass one of them, or neither |
|
|
265
405
|
| `reason: 'invalid'`, `code: 'bad_request'` | `limit` is not an integer in 1–100 | the SDK refuses this locally with a `TypeError` before any request, so seeing it from the server means something else built the URL |
|
|
266
406
|
| `TypeError` (thrown, no request sent) | `limit` out of range, passed to `mine()` / `character()` | pass an integer 1–100, or omit it for the default 25 |
|
|
267
407
|
|
package/docs/manifest.json
CHANGED
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
"file": "daily-challenges.md",
|
|
46
|
-
"sha256": "
|
|
47
|
-
"updated": "2026-10-
|
|
46
|
+
"sha256": "7ba296ba3056407ca3c8c9757a3351f3a8062a943335ee52b4ee92e1b2721d3b",
|
|
47
|
+
"updated": "2026-10-02T18:52:17Z"
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
"file": "dedicated-server-flow.md",
|
|
51
|
-
"sha256": "
|
|
52
|
-
"updated": "2026-
|
|
51
|
+
"sha256": "6da1bfc820ab2cf79180f69f9fec4d9f377ddde690f53923e749a88ec6307e22",
|
|
52
|
+
"updated": "2026-10-02T19:04:21Z"
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
"file": "deprecation-policy.md",
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
"file": "getting-started.md",
|
|
71
|
-
"sha256": "
|
|
72
|
-
"updated": "2026-10-
|
|
71
|
+
"sha256": "d2179480be49085d828c9afbd89d1d72c00914c5607105831503f0652e521285",
|
|
72
|
+
"updated": "2026-10-02T21:18:29Z"
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
75
|
"file": "inventory-and-catalogs.md",
|
|
@@ -78,8 +78,8 @@
|
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
80
|
"file": "leaderboards.md",
|
|
81
|
-
"sha256": "
|
|
82
|
-
"updated": "2026-
|
|
81
|
+
"sha256": "4f065b915604284ec810aa3c7bc046bfd99f343332a70b35921ecf061503f867",
|
|
82
|
+
"updated": "2026-10-02T18:52:17Z"
|
|
83
83
|
},
|
|
84
84
|
{
|
|
85
85
|
"file": "match-session.md",
|
|
@@ -118,8 +118,8 @@
|
|
|
118
118
|
},
|
|
119
119
|
{
|
|
120
120
|
"file": "server-bundles.md",
|
|
121
|
-
"sha256": "
|
|
122
|
-
"updated": "2026-10-
|
|
121
|
+
"sha256": "fed144c0d7b9f8be10fc3896a7325ab19aa660441c24038f39bc02b5fdf60cb3",
|
|
122
|
+
"updated": "2026-10-02T21:18:29Z"
|
|
123
123
|
},
|
|
124
124
|
{
|
|
125
125
|
"file": "serverless-functions-js.md",
|
|
@@ -128,8 +128,8 @@
|
|
|
128
128
|
},
|
|
129
129
|
{
|
|
130
130
|
"file": "serverless-functions.md",
|
|
131
|
-
"sha256": "
|
|
132
|
-
"updated": "2026-10-
|
|
131
|
+
"sha256": "c830daf736713744909700bf93ff5ba139b8b5d8ad3c2b4e057ca813fc6179f7",
|
|
132
|
+
"updated": "2026-10-02T18:52:17Z"
|
|
133
133
|
},
|
|
134
134
|
{
|
|
135
135
|
"file": "trading-and-gifting.md",
|
|
@@ -138,8 +138,8 @@
|
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
"file": "what-a-game-must-do.md",
|
|
141
|
-
"sha256": "
|
|
142
|
-
"updated": "2026-10-
|
|
141
|
+
"sha256": "d7b66e2c04f5cea169ae4cecd3926e350e272232af32752e60a7a388dfd8c461",
|
|
142
|
+
"updated": "2026-10-02T21:01:44Z"
|
|
143
143
|
}
|
|
144
144
|
]
|
|
145
145
|
}
|
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/v7.
|
|
84
|
-
curl -fsSL https://downloads.gemarcade.com/sdk/v7.
|
|
83
|
+
curl -fsSL -O https://downloads.gemarcade.com/sdk/v7.8.0/gem-web-sdk.tgz
|
|
84
|
+
curl -fsSL https://downloads.gemarcade.com/sdk/v7.8.0/gem-web-sdk.tgz.sri
|
|
85
85
|
openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
|
|
86
86
|
```
|
|
87
87
|
|
|
@@ -183,11 +183,15 @@ do not decide whether a reward was earned. So:
|
|
|
183
183
|
zone, and on one with no reset it grants once, ever.
|
|
184
184
|
`progression_max` grants only an improvement on a best. The platform keeps that state, and the
|
|
185
185
|
input cannot reset it;
|
|
186
|
-
- if you key
|
|
187
|
-
decides: the
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
186
|
+
- if you key the order that grants with a `nonce` instead, build the key from something the
|
|
187
|
+
platform decides: the date of the player's daily, or an id the platform issued, such as a match
|
|
188
|
+
it ran. That date is `period_local_date` on your
|
|
189
|
+
[daily challenge](daily-challenges.md#the-one-call-that-answers-both), read for the caller with
|
|
190
|
+
`GET /v1/challenges?player_id=<their id>` (`daily:2026-09-26`), and it is the player's own day
|
|
191
|
+
unless the challenge's `boundary` fixes a zone. The UTC date from `now-ms` names a UTC day,
|
|
192
|
+
which for a player off UTC is not the day their daily runs on. **Never use an id from the call's
|
|
193
|
+
input.** A run id your client made up is a new id on every call, so it stops nothing. A nonce is
|
|
194
|
+
also only remembered for 72 hours, so it guards a day, not a week or a lifetime;
|
|
191
195
|
- never pass the caller's request straight through to a progression write.
|
|
192
196
|
|
|
193
197
|
### What `call-api` can reach
|
|
@@ -386,6 +390,31 @@ The path's `{game_id}` is the game id from `get-context`, `entity_id` is the pla
|
|
|
386
390
|
you, and `run_id` is a UUID you choose. A run may also carry `occurred_at`, in Unix seconds, which
|
|
387
391
|
defaults to now. A stored batch answers `200` with `{ "inserted": 1, "duplicates": 0 }`.
|
|
388
392
|
|
|
393
|
+
**A run for a daily board names its day.** When the run type belongs to a challenge — its config
|
|
394
|
+
names one with `challenge`, which is what a
|
|
395
|
+
[`per_period` board](leaderboards.md#a-board-for-each-day-period-boards) ranks — the platform
|
|
396
|
+
files the run under one period of that challenge. It decides which from when the run arrives and
|
|
397
|
+
the player's own time zone, never from `occurred_at`. Add `"period_id"` to the run, set to the
|
|
398
|
+
`periodLocalDate` the player's game read from `gem.challenges.list()` and sent you with the result:
|
|
399
|
+
|
|
400
|
+
```json
|
|
401
|
+
{
|
|
402
|
+
"run_id": "5b0d3c1e-8f7a-4c2e-9d61-0a4b7e2f9c13",
|
|
403
|
+
"entity_type": "player",
|
|
404
|
+
"entity_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
|
|
405
|
+
"run_type": "daily-run",
|
|
406
|
+
"int_value_1": 14,
|
|
407
|
+
"period_id": "2026-10-02"
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
A run that names the day it was played for still counts for that day when it arrives up to 10
|
|
412
|
+
minutes after the day ended for the player. Without `period_id` the run goes to the period it
|
|
413
|
+
arrives in. ⚠️ **`period_id` is a claim the platform checks, not a choice it obeys**: a client
|
|
414
|
+
cannot use it to post to yesterday's board, or to a day that has not begun for the player. For a
|
|
415
|
+
cron challenge the id is the period's start, as `2026-10-02T06:00:00Z`. Send no `period_id` for a
|
|
416
|
+
run type that names no challenge; it is refused there.
|
|
417
|
+
|
|
389
418
|
⚠️ **A function that records the score its caller sent, unchecked, makes your board forgeable.**
|
|
390
419
|
The platform trusts a function's runs as it trusts your dedicated server's, and it cannot tell a
|
|
391
420
|
checked score from a forwarded one. Check the run against what the player cannot forge (section 1)
|
|
@@ -396,6 +425,7 @@ before you record it, or any player can put any score they like on your board.
|
|
|
396
425
|
| `400 invalid_run` | A run failed its run type's checks. `details` names it by `index` and `run_id` | Fix that run and send the batch again |
|
|
397
426
|
| `400 unknown_run_type` | The run type is not in the active config of the channel the function was called on | Publish the run type on that channel |
|
|
398
427
|
| `400 too_many_runs` | More than 500 runs in one request | Split the batch |
|
|
428
|
+
| `409 run_period_closed` | A run's challenge period is not open for its player: the day ended more than 10 minutes ago, or has not begun for them. Nothing was written, and `details` names the run by `index` and `run_id` | Do not retry that run. Tell the player the day has closed, and send the rest of the batch without it |
|
|
399
429
|
| `403 function_foreign_entity_write` | A function a player called named another player, or a character not theirs | Record runs only for the caller and their characters |
|
|
400
430
|
| `429 rate_limited` | The budget of 60 requests a minute is spent. Nothing was written | Send more runs in each request, and fewer requests |
|
|
401
431
|
| `503 service_unavailable` | The platform could not check the budget, so it refused the request. Nothing was written | Retry shortly |
|
|
@@ -328,11 +328,11 @@ orientations:
|
|
|
328
328
|
landscape: partial
|
|
329
329
|
```
|
|
330
330
|
|
|
331
|
-
| Value
|
|
332
|
-
|
|
|
333
|
-
| `full`
|
|
334
|
-
| `partial` | a dismissible hint, once, if the other way is `full`
|
|
335
|
-
| `none`
|
|
331
|
+
| Value | What the player gets |
|
|
332
|
+
| --------- | ---------------------------------------------------------------------- |
|
|
333
|
+
| `full` | nothing — the game plays this way round |
|
|
334
|
+
| `partial` | a dismissible hint, once, if the other way is `full` |
|
|
335
|
+
| `none` | a panel over the paused game asking them to rotate, with "play anyway" |
|
|
336
336
|
|
|
337
337
|
Three consequences worth knowing before you hit them:
|
|
338
338
|
|
|
@@ -508,10 +508,14 @@ them**, so the whole migration for a stylesheet is one declaration per edge:
|
|
|
508
508
|
|
|
509
509
|
```css
|
|
510
510
|
/* before */
|
|
511
|
-
#hud {
|
|
511
|
+
#hud {
|
|
512
|
+
top: env(safe-area-inset-top, 0px);
|
|
513
|
+
}
|
|
512
514
|
|
|
513
515
|
/* after — right in the arcade, and unchanged everywhere else */
|
|
514
|
-
#hud {
|
|
516
|
+
#hud {
|
|
517
|
+
top: var(--gem-safe-top, env(safe-area-inset-top, 0px));
|
|
518
|
+
}
|
|
515
519
|
```
|
|
516
520
|
|
|
517
521
|
The `env()` fallback is not decoration. Under a parent that has not sent the message — an older
|
|
@@ -805,6 +809,25 @@ pauseMenu.onOpen = () => gem.setWalletVisible(true);
|
|
|
805
809
|
- **`false` means nobody is listening** — no connection yet, or an embedder that draws no wallet.
|
|
806
810
|
Do nothing in its place.
|
|
807
811
|
|
|
812
|
+
## No accidental text selection
|
|
813
|
+
|
|
814
|
+
**You write nothing for this.** A long press on a phone, a double-click on a button, a drag across
|
|
815
|
+
your HUD: on an ordinary page each selects text, raises the copy menu or flashes a tap highlight,
|
|
816
|
+
and in a game none of them is something the player meant. So at start-up the SDK turns all three
|
|
817
|
+
off across your document.
|
|
818
|
+
|
|
819
|
+
- **Your CSS always wins.** The SDK's rule has zero specificity, so any rule you write overrides it.
|
|
820
|
+
To make one element copyable — a room code, a share link — give it
|
|
821
|
+
`-webkit-user-select: text; user-select: text;`.
|
|
822
|
+
- **Inputs, text areas and editable content are untouched.** They still take typing, still select
|
|
823
|
+
and still paste.
|
|
824
|
+
- **It is a stylesheet the SDK adopts, not a `<style>` element**, so a strict `style-src` in your
|
|
825
|
+
own CSP does not block it. On iOS before 16.4, which lacks that API, the SDK sets the same
|
|
826
|
+
properties on `<html>` itself instead, all but the copy menu, which has no safe way to be handed
|
|
827
|
+
back to your inputs there.
|
|
828
|
+
- **To keep the browser's default instead** — a game that is mostly text the player should be able
|
|
829
|
+
to copy — pass `createGem({ selectableText: true })`.
|
|
830
|
+
|
|
808
831
|
## The Arcade's FPS overlay
|
|
809
832
|
|
|
810
833
|
The Arcade has a frame-rate toggle beside your game, and **you do not have to write anything for it
|
|
@@ -876,15 +899,15 @@ player (`gem.player`), room members, leaderboard rows, friends, profiles, showca
|
|
|
876
899
|
async opponent you read through the raw client, once you pass the row through `mapIdentity`. One
|
|
877
900
|
renderer serves all of them.
|
|
878
901
|
|
|
879
|
-
| Field
|
|
880
|
-
|
|
|
881
|
-
| `playerId`
|
|
882
|
-
| `displayName`
|
|
883
|
-
| `discriminator` | The digits that tell two same-named players apart — `0001`, no `#`
|
|
884
|
-
| `displayTag`
|
|
885
|
-
| `avatarIconId`
|
|
886
|
-
| `avatarIconUrl` | The portrait's thumbnail
|
|
887
|
-
| `badges`
|
|
902
|
+
| Field | What it is | Type |
|
|
903
|
+
| --------------- | ------------------------------------------------------------------------------------------- | ------------ |
|
|
904
|
+
| `playerId` | The player's id. **Absent on an anonymous row** | `string` |
|
|
905
|
+
| `displayName` | The name the player chose | `PlayerText` |
|
|
906
|
+
| `discriminator` | The digits that tell two same-named players apart — `0001`, no `#` | `string` |
|
|
907
|
+
| `displayTag` | The precomposed `Name#0001` | `PlayerText` |
|
|
908
|
+
| `avatarIconId` | The portrait item's id | `string` |
|
|
909
|
+
| `avatarIconUrl` | The portrait's thumbnail | `string` |
|
|
910
|
+
| `badges` | Equipped badges in display order, each `{ itemId, iconUrl }` — always a list, empty for now | array |
|
|
888
911
|
|
|
889
912
|
**The name and the tag are player-authored, so both are `PlayerText`** on every surface, not only
|
|
890
913
|
some. Convert with `text.toPlain()` where you render, never before.
|
|
@@ -937,11 +960,11 @@ While it lasts the player earns Gems and Tickets faster (never achievement point
|
|
|
937
960
|
platform raises their earning limits to match. The platform does all of that on its own side when
|
|
938
961
|
it pays; your game does nothing to make a boost work.
|
|
939
962
|
|
|
940
|
-
| Field
|
|
941
|
-
|
|
|
942
|
-
| `boost.active`
|
|
943
|
-
| `boost.multipliers`
|
|
944
|
-
| `boost.expiresInDays` | Whole days until the soonest boosted currency drops back, rounded up and **capped at 7** — `7` means "7 or more"; absent when nothing is boosted
|
|
963
|
+
| Field | What it is | Type |
|
|
964
|
+
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
|
|
965
|
+
| `boost.active` | Whether any currency earns faster than 1× right now | `boolean` |
|
|
966
|
+
| `boost.multipliers` | One `{ currencyId, currencySlug, multiplier }` per boostable currency — `multiplier` is `2` for 2×, `1` when not boosted; a currency not listed is 1× | array |
|
|
967
|
+
| `boost.expiresInDays` | Whole days until the soonest boosted currency drops back, rounded up and **capped at 7** — `7` means "7 or more"; absent when nothing is boosted | `number` |
|
|
945
968
|
|
|
946
969
|
`boost` is always present on `gem.player`. When the Arcade has not said — an older Arcade, a
|
|
947
970
|
backend without boosts, a read that has not landed yet — it reads as **not boosted**, exactly like a
|