@indiegems/gem-web-sdk 7.9.1 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/README.md +1 -0
- package/dist/function.d.ts +26 -1
- package/dist/game.d.ts +44 -1
- package/dist/gem-web-sdk.bridge.js +1 -1
- package/dist/gem-web-sdk.esm.js +2 -2
- package/dist/gem-web-sdk.function.js +2 -2
- package/dist/gem-web-sdk.game.js +2 -2
- package/dist/gem-web-sdk.min.js +2 -2
- package/dist/gem-web-sdk.sdk.js +2 -2
- package/dist/gem-web-sdk.server.js +2 -2
- package/dist/index.d.ts +21 -3
- package/dist/sdk.d.ts +44 -1
- package/docs/achievements.md +72 -5
- package/docs/api/api-reference-game.md +719 -24
- package/docs/api/api-reference-server.md +1 -1
- package/docs/api/reference.json +671 -15
- package/docs/changelog.md +45 -0
- package/docs/cross-promotion.md +38 -2
- package/docs/friends.md +86 -11
- package/docs/getting-started.md +12 -7
- package/docs/manifest.json +25 -20
- package/docs/saving-player-progress.md +41 -0
- package/docs/server-bundles.md +2 -2
- package/docs/serverless-functions-js.md +3 -2
- package/docs/serverless-functions.md +53 -7
- package/docs/vibration.md +141 -0
- package/docs/what-a-game-must-do.md +136 -3
- package/package.json +1 -1
- package/schema/bridge.v1.json +102 -1
- package/schema/capabilities.public.json +15 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Vibration
|
|
2
|
+
|
|
3
|
+
`navigator.vibrate` does nothing on any iPhone. It does not exist in Safari, in any other iOS
|
|
4
|
+
browser, or in the Gem Arcade iPhone app, and Firefox removed it on every platform. A game that
|
|
5
|
+
calls it vibrates on Android and nowhere else.
|
|
6
|
+
|
|
7
|
+
`gem.vibration` vibrates wherever the player's device can. It takes the same pattern as
|
|
8
|
+
`navigator.vibrate` and returns the same boolean, so switching is a rename:
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
navigator.vibrate(30); // before: Android only
|
|
12
|
+
gem.vibration.vibrate(30); // after: Android, and the Gem Arcade iPhone app
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Two rules come before everything else on this page. Both are easy to get wrong.
|
|
16
|
+
|
|
17
|
+
1. **Read `gem.vibration.available` live, after `awaitBridge()`, every time you need it.** On an
|
|
18
|
+
iPhone it is `false` until the bridge to the Arcade connects. If you build your settings screen
|
|
19
|
+
at boot and hide the vibration toggle then, it stays hidden on exactly the device this exists
|
|
20
|
+
for.
|
|
21
|
+
2. **Never call `navigator.vibrate`**, with or without a guard, and never write a fallback to it.
|
|
22
|
+
`gem.vibration.vibrate` already calls `navigator.vibrate` itself wherever that works.
|
|
23
|
+
|
|
24
|
+
## The whole integration
|
|
25
|
+
|
|
26
|
+
Your game owns its vibration toggle; the Arcade has none. Keep the toggle in
|
|
27
|
+
`gem.deviceSettings`, never in `localStorage`, which every publish of your game wipes.
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
await gem.awaitBridge();
|
|
31
|
+
|
|
32
|
+
// Your settings, kept by the Arcade so they survive your next publish.
|
|
33
|
+
let settings = (await gem.deviceSettings.wait(1_500)) ?? {};
|
|
34
|
+
|
|
35
|
+
// Build the row when the settings screen OPENS, never once at boot.
|
|
36
|
+
function openSettings() {
|
|
37
|
+
vibrationRow.hidden = !gem.vibration.available;
|
|
38
|
+
vibrationToggle.checked = settings.vibration !== false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
vibrationToggle.onchange = () => {
|
|
42
|
+
settings = { ...settings, v: 1, vibration: vibrationToggle.checked };
|
|
43
|
+
void gem.deviceSettings.put(settings);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Check your own toggle before every call. Nothing else: no fallback.
|
|
47
|
+
function onHit() {
|
|
48
|
+
if (settings.vibration !== false) gem.vibration.vibrate(30);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
There is no `available` check before `vibrate`: where vibration is not available, `vibrate` does
|
|
53
|
+
nothing and returns `false`. `available` exists to decide whether to show the toggle.
|
|
54
|
+
|
|
55
|
+
## What a pattern is
|
|
56
|
+
|
|
57
|
+
Exactly what `navigator.vibrate` takes. A number is one vibration of that many milliseconds; an
|
|
58
|
+
array alternates vibration and pause, starting with a vibration.
|
|
59
|
+
|
|
60
|
+
- **`0` or `[]` stops** a pattern that is still playing.
|
|
61
|
+
- **A new call replaces** whatever is still playing.
|
|
62
|
+
- **The pattern is capped**: at most 99 entries, each at most 10 000 ms, and **at most 10 seconds
|
|
63
|
+
in all**, vibrations and pauses together. A longer pattern is cut short, not refused.
|
|
64
|
+
- **A negative, `NaN` or infinite entry refuses the whole pattern**: `vibrate` returns `false`.
|
|
65
|
+
|
|
66
|
+
`vibrate` returns `true` when the pattern was accepted, never that a motor moved: the player may
|
|
67
|
+
have vibration switched off in their phone's settings, which your game cannot see.
|
|
68
|
+
|
|
69
|
+
## Nothing vibrates before the first tap
|
|
70
|
+
|
|
71
|
+
Browsers allow vibration only after the player has tapped or pressed a key in your game. Before
|
|
72
|
+
that, `vibrate` returns `false` and vibrates nothing, on Android and iPhone alike. So a buzz on
|
|
73
|
+
load, or on a title screen nobody has touched, never plays.
|
|
74
|
+
|
|
75
|
+
## A cookbook, by feel
|
|
76
|
+
|
|
77
|
+
On an iPhone, a vibration of 40 ms or less plays as a crisp tap, stronger the longer it is; a
|
|
78
|
+
longer one plays as a continuous buzz. On Android every entry is a buzz of that length.
|
|
79
|
+
|
|
80
|
+
| Feel | Pattern | Use it for |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
| tick | `10` | a menu cursor moving, a dial clicking |
|
|
83
|
+
| tap | `20` | a button press, a piece placed |
|
|
84
|
+
| hit | `30` to `50` | taking damage, a landing, a collision |
|
|
85
|
+
| heavy | `100` | an explosion, a boss hit; use it rarely |
|
|
86
|
+
| alert | `[200, 100, 200]` | a warning, a timer running out |
|
|
87
|
+
| cancel | `0` | stopping whatever is playing |
|
|
88
|
+
|
|
89
|
+
Keep hit feedback at about 100 ms or less. Anything longer stops feeling like feedback and starts
|
|
90
|
+
feeling like a phone call.
|
|
91
|
+
|
|
92
|
+
## Stop on pause
|
|
93
|
+
|
|
94
|
+
When your game pauses, stop anything still playing:
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
onPause: (paused) => {
|
|
98
|
+
if (paused) gem.vibration.vibrate(0);
|
|
99
|
+
},
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The Arcade also stops a running pattern itself when it pauses your game or the player leaves it,
|
|
103
|
+
but a pause your game makes on its own is yours to stop.
|
|
104
|
+
|
|
105
|
+
## Where it vibrates
|
|
106
|
+
|
|
107
|
+
| Where the player is | Vibrates? |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| An Android phone, in the Gem Arcade app or in Chrome | yes |
|
|
110
|
+
| An iPhone, in the Gem Arcade app | yes |
|
|
111
|
+
| An iPhone or iPad, in Safari or any other browser | no: `available` is `false` |
|
|
112
|
+
| An iPad, in the Gem Arcade app | no: an iPad has no vibration motor |
|
|
113
|
+
| Firefox, anywhere | no: `available` is `false` |
|
|
114
|
+
| A desktop or laptop with a mouse | no: `available` is `false` |
|
|
115
|
+
|
|
116
|
+
A laptop with a touch screen may report `available` as `true` and then vibrate nothing. That is
|
|
117
|
+
harmless; it is why the toggle is your player's to switch off.
|
|
118
|
+
|
|
119
|
+
## Gamepad rumble is separate
|
|
120
|
+
|
|
121
|
+
A controller's rumble is `GamepadHapticActuator` (`gamepad.vibrationActuator`), not
|
|
122
|
+
`navigator.vibrate`. It is unaffected by any of this, and `gem.vibration` does not drive it.
|
|
123
|
+
|
|
124
|
+
## What not to do
|
|
125
|
+
|
|
126
|
+
- **`navigator.vibrate?.(x)`.** It is still a direct call: it vibrates on Android and silently
|
|
127
|
+
nowhere else.
|
|
128
|
+
- **`if ('vibrate' in navigator)` to decide whether to show the toggle.** It hides the toggle on
|
|
129
|
+
every iPhone, where `gem.vibration` works. Use `gem.vibration.available`.
|
|
130
|
+
- **A wrapper that falls back to `navigator.vibrate`.** `gem.vibration.vibrate` already does that
|
|
131
|
+
where it works, and nowhere else does a fallback do anything.
|
|
132
|
+
- **A user-agent check** to hide the toggle on iOS. Read `available`; it already knows.
|
|
133
|
+
- **Reading `available` once at boot.** It is `false` on an iPhone until the bridge connects.
|
|
134
|
+
- **Vibrating on load or every frame.** Nothing plays before the first tap, and a call every frame
|
|
135
|
+
is past the limit of 600 a minute: past it, `vibrate` returns `false`.
|
|
136
|
+
- **Hit feedback longer than about 100 ms.**
|
|
137
|
+
- **Keeping the toggle in `localStorage`.** It is gone after your next publish; use
|
|
138
|
+
`gem.deviceSettings`.
|
|
139
|
+
|
|
140
|
+
When `vibrate` refuses a call for a reason you can fix (no tap yet, a bad pattern, the limit), the
|
|
141
|
+
SDK says so once a session through your `log` option, which is `console.warn` unless you set it.
|
|
@@ -353,6 +353,117 @@ one refused orientation, in fullscreen, once they are already the right way roun
|
|
|
353
353
|
That is best effort (iOS does not implement it, and the native app's webview declines
|
|
354
354
|
it), which is why the panel exists and why it always offers a way past.
|
|
355
355
|
|
|
356
|
+
**The same declaration decides how a computer or a television shows your game.** Neither is held
|
|
357
|
+
two ways round, so the Arcade reads your phone and tablet rows instead and, for a portrait-only
|
|
358
|
+
game, draws a centred portrait column.
|
|
359
|
+
[Art beside a portrait game](#art-beside-a-portrait-game-on-a-wide-screen-portrait-borders) below
|
|
360
|
+
has the exact rule, and the art you can put beside that column.
|
|
361
|
+
|
|
362
|
+
## Art beside a portrait game on a wide screen: portrait borders
|
|
363
|
+
|
|
364
|
+
**You need this only if your game is portrait-only and you want your own art beside it on a
|
|
365
|
+
computer or a television.** On a desktop browser, the Arcade's desktop app or a TV, the Arcade
|
|
366
|
+
draws a portrait-only game as a centred 9:16 column and fills the space either side with its own
|
|
367
|
+
bars. Portrait borders put your art there instead: you publish a named set of images with your
|
|
368
|
+
store page, and your game says which one goes with what is on screen.
|
|
369
|
+
|
|
370
|
+
### When the Arcade letterboxes a game
|
|
371
|
+
|
|
372
|
+
**Your store page's `orientations` decide it, and nothing else does.** Declaring borders never
|
|
373
|
+
turns the letterbox on, and there is no desktop orientation to declare. The rule:
|
|
374
|
+
|
|
375
|
+
1. Take `tablet` if you declared both its `portrait` and its `landscape`; otherwise `phone` if you
|
|
376
|
+
declared both of its; otherwise stop, and the game is not letterboxed.
|
|
377
|
+
2. The game is letterboxed to portrait when, on that device, `portrait` is `full` or `partial` and
|
|
378
|
+
`landscape` is `none`.
|
|
379
|
+
|
|
380
|
+
| `orientations`, each pair portrait / landscape | Letterboxed on a computer or TV |
|
|
381
|
+
| ---------------------------------------------- | ------------------------------- |
|
|
382
|
+
| nothing declared | no |
|
|
383
|
+
| tablet `full` / `none` | yes |
|
|
384
|
+
| phone `full` / `none`, no tablet | yes |
|
|
385
|
+
| phone `partial` / `none`, no tablet | yes |
|
|
386
|
+
| phone `full` / `none`, tablet `full` / `full` | no — the tablet decides |
|
|
387
|
+
| phone `full` / `none`, tablet `none` / `full` | no — the tablet decides |
|
|
388
|
+
| tablet `none` / `full` | no |
|
|
389
|
+
| tablet `partial` / `partial` | no |
|
|
390
|
+
| tablet `full` / `partial` | no |
|
|
391
|
+
|
|
392
|
+
A phone or a tablet is never letterboxed: it gets the rotate panel from the section above. On a
|
|
393
|
+
computer or TV, a game the rule does not letterbox fills the play area and its borders never show.
|
|
394
|
+
**A store page with borders and orientations that never letterbox uploads with a
|
|
395
|
+
`portrait_borders_unused` warning** — the page is accepted, and the art will not appear anywhere.
|
|
396
|
+
|
|
397
|
+
### Declaring the borders
|
|
398
|
+
|
|
399
|
+
Add them to your store page's `media`, one entry per image:
|
|
400
|
+
|
|
401
|
+
```yaml
|
|
402
|
+
media:
|
|
403
|
+
portrait_borders:
|
|
404
|
+
- name: meadow # the first entry is the default
|
|
405
|
+
src: borders/meadow.jpg
|
|
406
|
+
- name: alpine
|
|
407
|
+
src: borders/alpine.jpg
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
- **Up to 16 entries**, each with a `name` and a `src`. A name is 1 to 40 characters of `a-z`,
|
|
411
|
+
`0-9`, `_` and `-`, starts with a letter or digit, and is unique on the page.
|
|
412
|
+
- **The first entry is the default.** The Arcade shows it the moment play starts, before your game
|
|
413
|
+
has said anything, so put first the one most players should see.
|
|
414
|
+
- **16:9, at least 1920×1080; 2560×1440 is ideal.** PNG, JPEG or WebP. An AVIF, GIF or SVG uploads
|
|
415
|
+
without complaint and then fails the page's processing.
|
|
416
|
+
- **Opaque.** A transparent area shows the Arcade's own bars through it.
|
|
417
|
+
- **Keep the interesting part away from the middle.** The image is scaled to cover the whole play
|
|
418
|
+
area and centred, and your game covers the centre. How much shows either side changes with the
|
|
419
|
+
shape of the player's screen — in a window narrower than the column, the art shows above and
|
|
420
|
+
below instead — so nothing in the art can line up with your game's edge.
|
|
421
|
+
- **Upload with a current `gemctl`.** An older one refuses `portrait_borders` as an unknown key.
|
|
422
|
+
|
|
423
|
+
### Choosing one from your game
|
|
424
|
+
|
|
425
|
+
Call `gem.showPortraitBorders` at start-up and on every theme change, with the name of the border
|
|
426
|
+
that goes with what is on screen, and `null` for a theme that has no art:
|
|
427
|
+
|
|
428
|
+
```js
|
|
429
|
+
const BORDERS = new Set(['meadow', 'alpine']); // the names your store page declares
|
|
430
|
+
|
|
431
|
+
function applyTheme(id) {
|
|
432
|
+
document.body.dataset.theme = id;
|
|
433
|
+
gem.showPortraitBorders(BORDERS.has(id) ? id : null); // null: the Arcade's own bars show again
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
- **Call it unconditionally.** On a phone, or on a screen where your game is not letterboxed, the
|
|
438
|
+
Arcade keeps your choice and shows nothing, and nothing tells your game which case it is in — do
|
|
439
|
+
not look for a signal. `gem.frame.parentSupports('portraitBorders')` says whether this Arcade can
|
|
440
|
+
paint borders at all, never whether it is painting them now.
|
|
441
|
+
- **It is safe at any time.** Before the connection exists your choice is held and sent when the
|
|
442
|
+
Arcade connects. A burst of calls — a theme picker previewing on hover — reaches the Arcade as
|
|
443
|
+
its final value, at most one a second, so it never trips a rate limit. An embedder that paints
|
|
444
|
+
no borders is not told, and nothing is raised.
|
|
445
|
+
- **The Arcade owns the change.** When and how the image swaps is the Arcade's; you get no
|
|
446
|
+
acknowledgement and no say in the timing. A game that restores a saved theme just after it
|
|
447
|
+
starts shows the default for a moment, then its own.
|
|
448
|
+
- **It shows only while your game is playing** — not before play starts, and not while your key
|
|
449
|
+
art covers the play area.
|
|
450
|
+
- **A string that is not a valid name is refused on your side**, with a console warning, and never
|
|
451
|
+
sent. The SDK does not trim it, lower-case it or shorten it.
|
|
452
|
+
- **A valid name your page does not declare is ignored**, and the Arcade keeps what it is showing.
|
|
453
|
+
On the dev and test channels the Arcade's own page logs a console warning naming the value, the
|
|
454
|
+
channel whose page it looked in, and the names that page declares; on release it says nothing.
|
|
455
|
+
|
|
456
|
+
### Publishing order and experiments
|
|
457
|
+
|
|
458
|
+
- **Publish the store page before, or together with, the build that uses a new name.** A build
|
|
459
|
+
asking for a name its page does not carry yet keeps the current image until the page arrives.
|
|
460
|
+
Rolling a build back is harmless; rolling the page back can remove a name, and the Arcade then
|
|
461
|
+
keeps showing what it has.
|
|
462
|
+
- **Every arm of a store-page experiment must carry every name your build uses.** The set comes
|
|
463
|
+
from the page the player opened, so an arm without a name falls under the rule above.
|
|
464
|
+
- **The borders come from the store page on screen.** To see your dev-channel borders, open your
|
|
465
|
+
game's store page on the dev channel. `gemctl serve` does not letterbox and shows no borders.
|
|
466
|
+
|
|
356
467
|
## An achievement unlock — celebrate it yourself, or declare the Arcade's
|
|
357
468
|
|
|
358
469
|
By default the Arcade draws no card when a player unlocks one of your game's
|
|
@@ -429,8 +540,10 @@ credit for it until you say so.
|
|
|
429
540
|
entry fields few and ordinary — and never gate first play behind one. A player made to type on a
|
|
430
541
|
remote before they may play mostly does not.
|
|
431
542
|
- **A TV performance budget.** TV-browser SoCs are weak next to any desktop: ship a player-settable
|
|
432
|
-
render-scale option (render at DPR 1 and upscale), bound memory, assume a fixed 16:9
|
|
433
|
-
|
|
543
|
+
render-scale option (render at DPR 1 and upscale), bound memory, assume a fixed frame — 16:9, or
|
|
544
|
+
a centred 9:16 column when your store page makes you
|
|
545
|
+
[a letterboxed portrait game](#art-beside-a-portrait-game-on-a-wide-screen-portrait-borders) —
|
|
546
|
+
with no `orientationchange` rescue coming, and keep your heights `dvh`-safe.
|
|
434
547
|
- **Reduced motion.** TV browsers rarely expose `prefers-reduced-motion`, so the pushed
|
|
435
548
|
`gem.prefs.reducedMotion` is the only signal a TV player has; honor it. It arrives with
|
|
436
549
|
[the prefs the parent pushes](#what-the-parent-says-about-the-session-prefs-pause-and-terminate)
|
|
@@ -1016,7 +1129,15 @@ renderer serves all of them.
|
|
|
1016
1129
|
| `displayTag` | The precomposed `Name#0001` | `PlayerText` |
|
|
1017
1130
|
| `avatarIconId` | The portrait item's id | `string` |
|
|
1018
1131
|
| `avatarIconUrl` | The portrait's thumbnail | `string` |
|
|
1019
|
-
| `badges` | Equipped badges in
|
|
1132
|
+
| `badges` | Equipped badges, at most two, in the player's order, each `{ itemId, iconUrl }` | array |
|
|
1133
|
+
|
|
1134
|
+
**A player chooses what they wear**: a portrait and up to two badges, each an icon they own — one
|
|
1135
|
+
an achievement in your game or any other may have awarded them ([achievements.md](achievements.md)
|
|
1136
|
+
§1). The portrait arrives as `avatarIconId` and `avatarIconUrl`, and the badges as `badges`, in the
|
|
1137
|
+
order the player put them in. `badges` is always a list — empty for a player who wears none, and on
|
|
1138
|
+
a row whose identity is withheld — so draw what is there, never pad it to two, and key on `itemId`,
|
|
1139
|
+
never on a position. A player can also equip a title, and colours and animations for their name and
|
|
1140
|
+
title. The SDK's identity does not carry those, so draw the name in your own style.
|
|
1020
1141
|
|
|
1021
1142
|
**The name and the tag are player-authored, so both are `PlayerText`** on every surface, not only
|
|
1022
1143
|
some. Convert with `text.toPlain()` where you render, never before.
|
|
@@ -1183,6 +1304,18 @@ or after an `await`; and **never gate a reward on it**, because your game is tol
|
|
|
1183
1304
|
not even whether the player shared.
|
|
1184
1305
|
[Linking to your game](getting-started.md#linking-to-your-game) is the section.
|
|
1185
1306
|
|
|
1307
|
+
## If your game vibrates
|
|
1308
|
+
|
|
1309
|
+
Not on the list above either. `navigator.vibrate` does nothing on any iPhone, so call
|
|
1310
|
+
`gem.vibration.vibrate` instead: it takes the same pattern, returns the same boolean, and vibrates
|
|
1311
|
+
wherever the player's device can, the Gem Arcade iPhone app included.
|
|
1312
|
+
|
|
1313
|
+
Two things before you reach for it: **read `gem.vibration.available` live, after `awaitBridge()`**,
|
|
1314
|
+
to decide whether to show your vibration toggle, because on an iPhone it is `false` until the
|
|
1315
|
+
bridge connects; and **never call `navigator.vibrate`**, guarded or not, because the SDK already
|
|
1316
|
+
does wherever that works. Your game owns the toggle and keeps it in `gem.deviceSettings`.
|
|
1317
|
+
[`vibration.md`](vibration.md) is the page.
|
|
1318
|
+
|
|
1186
1319
|
## A decision the player must not make, if your game has one
|
|
1187
1320
|
|
|
1188
1321
|
Not on the list above either: most games trust the browser with everything they do and lose nothing
|
package/package.json
CHANGED
package/schema/bridge.v1.json
CHANGED
|
@@ -590,7 +590,7 @@
|
|
|
590
590
|
"kind": "array",
|
|
591
591
|
"maxItems": 2,
|
|
592
592
|
"optional": true,
|
|
593
|
-
"$comment": "Equipped badges in
|
|
593
|
+
"$comment": "Equipped badges in the order the player arranged them — never more than two, and empty when the player wears none. Each is an item id and a thumbnail URL, the same pin caveat as `avatarIconUrl`.",
|
|
594
594
|
"items": {
|
|
595
595
|
"kind": "object",
|
|
596
596
|
"fields": {
|
|
@@ -2604,6 +2604,66 @@
|
|
|
2604
2604
|
}
|
|
2605
2605
|
},
|
|
2606
2606
|
|
|
2607
|
+
{
|
|
2608
|
+
"type": "gem.vibrate",
|
|
2609
|
+
"direction": "frameToParent",
|
|
2610
|
+
"transport": "port",
|
|
2611
|
+
"$comment": [
|
|
2612
|
+
"Ask the parent to play a vibration pattern on the player's device, where",
|
|
2613
|
+
"the frame cannot: `navigator.vibrate` is undefined in every WebKit (so in",
|
|
2614
|
+
"every iOS browser and the iOS app's WKWebView) and in Firefox 129 and",
|
|
2615
|
+
"later. Gated by the `vibration` capability: sent only to a parent that",
|
|
2616
|
+
"advertised it.",
|
|
2617
|
+
"",
|
|
2618
|
+
"THE SAME VALUE `navigator.vibrate` WOULD HAVE BEEN GIVEN. `pattern` is the",
|
|
2619
|
+
"Vibration API's pattern, already validated and normalized by the SDK:",
|
|
2620
|
+
"alternating on and off durations in milliseconds, starting with on, with",
|
|
2621
|
+
"no trailing pause. An empty array CANCELS a running pattern. A new",
|
|
2622
|
+
"message replaces whatever is still playing.",
|
|
2623
|
+
"",
|
|
2624
|
+
"ONE WAY, AND NOTHING ANSWERS IT. No requestId, and no message travels",
|
|
2625
|
+
"back: the game already has its answer (`gem.vibration.vibrate` returns",
|
|
2626
|
+
"whether the message left the frame), and a reply would tell a game the",
|
|
2627
|
+
"state of the parent's own gates.",
|
|
2628
|
+
"",
|
|
2629
|
+
"WHAT THE PARENT OWES, none of which this schema can enforce:",
|
|
2630
|
+
"",
|
|
2631
|
+
" REFUSE A TOTAL OVER 10 000 ms. The sum of every entry, on and off, is",
|
|
2632
|
+
" at most 10 000. The SDK truncates to that before sending; a message",
|
|
2633
|
+
" over it was written by hand, and is refused whole, never clamped.",
|
|
2634
|
+
"",
|
|
2635
|
+
" GATE ON THE FRAME'S OWN ACTIVATION, NOT THE PAGE'S. The player's Play",
|
|
2636
|
+
" click has already activated the parent document before any game runs,",
|
|
2637
|
+
" so the parent's sticky activation proves nothing about the game. Drop",
|
|
2638
|
+
" the message until a tap inside this frame document has activated it.",
|
|
2639
|
+
"",
|
|
2640
|
+
" DROP IT WHILE THE GAME CANNOT BE SEEN: the page hidden, or the game",
|
|
2641
|
+
" paused behind the parent's own menu. Cancel a running pattern on pause",
|
|
2642
|
+
" or hide.",
|
|
2643
|
+
"",
|
|
2644
|
+
" SEND NOTHING BACK. No outcome, no acknowledgement, and no error for a",
|
|
2645
|
+
" refusal - not for an unadvertised type, a malformed or oversized",
|
|
2646
|
+
" pattern, or the rate limit - so a game cannot read the parent's gates",
|
|
2647
|
+
" or its limiter.",
|
|
2648
|
+
"",
|
|
2649
|
+
"Not an Arcade surface: the parent draws nothing and the player sees",
|
|
2650
|
+
"nothing of the platform's. It is a device effect the game would have had",
|
|
2651
|
+
"from `navigator.vibrate` on a Chromium browser."
|
|
2652
|
+
],
|
|
2653
|
+
"fields": {
|
|
2654
|
+
"pattern": {
|
|
2655
|
+
"kind": "array",
|
|
2656
|
+
"maxItems": 99,
|
|
2657
|
+
"$comment": "At least 0 entries (an empty array cancels) and at most 99, Chromium's bound. The total is at most 10 000 ms, which the parent checks: a sum has no field shape.",
|
|
2658
|
+
"items": { "kind": "int", "min": 0, "max": 10000 }
|
|
2659
|
+
}
|
|
2660
|
+
},
|
|
2661
|
+
"rateLimit": {
|
|
2662
|
+
"perMinute": 600,
|
|
2663
|
+
"$comment": "Bounds a BUGGY game - one that vibrates every frame - at ten a second. NOT a security control: a hostile game posts on the port it holds rather than calling the SDK. What bounds the motor is the parent's native throttle and its duty-cycle budget."
|
|
2664
|
+
}
|
|
2665
|
+
},
|
|
2666
|
+
|
|
2607
2667
|
{
|
|
2608
2668
|
"type": "gem.frameStats",
|
|
2609
2669
|
"direction": "frameToParent",
|
|
@@ -2655,6 +2715,47 @@
|
|
|
2655
2715
|
"rateLimit": { "perMinute": 120 }
|
|
2656
2716
|
},
|
|
2657
2717
|
|
|
2718
|
+
{
|
|
2719
|
+
"type": "gem.portraitBorders",
|
|
2720
|
+
"direction": "frameToParent",
|
|
2721
|
+
"transport": "port",
|
|
2722
|
+
"$comment": [
|
|
2723
|
+
"Which of the store page's portrait borders the parent paints beside the",
|
|
2724
|
+
"game while it letterboxes a portrait game on a wide screen. `name` is one",
|
|
2725
|
+
"of the names the page declares under `media.portrait_borders`; absent",
|
|
2726
|
+
"means clear the sides to the parent's own bars, for a theme with no art.",
|
|
2727
|
+
"",
|
|
2728
|
+
"A KEY INTO PUBLISHED ART, NEVER THE ART. The images are the store page's,",
|
|
2729
|
+
"served and reviewed like every other store image; the frame chooses only",
|
|
2730
|
+
"which one and when. The parent looks `name` up by exact match in the list",
|
|
2731
|
+
"the page it is showing declares, and never renders it as text or builds a",
|
|
2732
|
+
"URL from it. A name that list does not hold is ignored and the current",
|
|
2733
|
+
"image stays. Transitions, their timing and any minimum dwell are the",
|
|
2734
|
+
"parent's, swaps wait while a trusted overlay is up, and the frame gets no",
|
|
2735
|
+
"acknowledgement.",
|
|
2736
|
+
"",
|
|
2737
|
+
"`name` is a plain bounded string with no format: a border name may carry",
|
|
2738
|
+
"digits and hyphens, which the `code` format refuses. The SDK sends only",
|
|
2739
|
+
"names matching `^[a-z0-9][a-z0-9_-]{0,39}$`, the store page's own rule, and",
|
|
2740
|
+
"a parent checks the same rule on receipt.",
|
|
2741
|
+
"",
|
|
2742
|
+
"STATE, NOT A MOMENT. The SDK holds the game's latest choice until the",
|
|
2743
|
+
"bridge connects and sends at most one a second, latest wins, so a burst",
|
|
2744
|
+
"(a theme picker previewing on hover) delivers its final value well inside",
|
|
2745
|
+
"the limit below.",
|
|
2746
|
+
"",
|
|
2747
|
+
"Gated by `portraitBorders`, which a parent that paints borders advertises",
|
|
2748
|
+
"to EVERY game on every device, whether or not the game declares borders or",
|
|
2749
|
+
"is letterboxed right now. It is never a signal of form factor or letterbox",
|
|
2750
|
+
"state, and the parent answers nothing, so the frame learns nothing about",
|
|
2751
|
+
"how it is being shown."
|
|
2752
|
+
],
|
|
2753
|
+
"fields": {
|
|
2754
|
+
"name": { "kind": "string", "maxLength": 40, "optional": true }
|
|
2755
|
+
},
|
|
2756
|
+
"rateLimit": { "perMinute": 60 }
|
|
2757
|
+
},
|
|
2758
|
+
|
|
2658
2759
|
{
|
|
2659
2760
|
"type": "gem.error",
|
|
2660
2761
|
"direction": "both",
|
|
@@ -123,6 +123,12 @@
|
|
|
123
123
|
"gem.frameStats"
|
|
124
124
|
]
|
|
125
125
|
},
|
|
126
|
+
"portraitBorders": {
|
|
127
|
+
"implemented": true,
|
|
128
|
+
"gates": [
|
|
129
|
+
"gem.portraitBorders"
|
|
130
|
+
]
|
|
131
|
+
},
|
|
126
132
|
"engagement": {
|
|
127
133
|
"implemented": true,
|
|
128
134
|
"gates": [
|
|
@@ -191,6 +197,15 @@
|
|
|
191
197
|
"gem.requestShare"
|
|
192
198
|
]
|
|
193
199
|
},
|
|
200
|
+
"vibration": {
|
|
201
|
+
"implemented": true,
|
|
202
|
+
"gates": [
|
|
203
|
+
"gem.vibrate",
|
|
204
|
+
{
|
|
205
|
+
"namespace": "gem.vibration"
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
},
|
|
194
209
|
"gameMenu": {
|
|
195
210
|
"implemented": true,
|
|
196
211
|
"gates": [
|