@indiegems/gem-web-sdk 7.9.0 → 8.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 +99 -0
- package/README.md +1 -0
- package/dist/function.d.ts +26 -1
- package/dist/game.d.ts +32 -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 +1 -1
- package/dist/index.d.ts +12 -3
- package/dist/sdk.d.ts +32 -1
- package/docs/api/api-reference-game.md +435 -15
- package/docs/api/api-reference-server.md +1 -1
- package/docs/api/reference.json +443 -7
- package/docs/changelog.md +99 -0
- package/docs/friends.md +86 -11
- package/docs/getting-started.md +10 -6
- package/docs/manifest.json +21 -16
- 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 +106 -63
- package/package.json +1 -1
- package/schema/bridge.v1.json +60 -0
- package/schema/capabilities.public.json +9 -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.
|
|
@@ -571,73 +571,117 @@ the `var()` above, or read `gem.viewport`. The release checklist looks for the b
|
|
|
571
571
|
|
|
572
572
|
### The arcade's own controls: reserved areas
|
|
573
573
|
|
|
574
|
-
On a phone your game is full-bleed, and the arcade keeps one
|
|
575
|
-
**top-right corner**:
|
|
574
|
+
On a phone your game is full-bleed, and the arcade keeps one row of its own over the top of it:
|
|
576
575
|
|
|
577
|
-
- **The menu button**, in the corner
|
|
576
|
+
- **The menu button**, in the **top-right corner**, always there. It opens
|
|
578
577
|
[the Arcade's menu](#the-arcades-menu-your-settings-live-in-it): Return to the Arcade, your
|
|
579
578
|
settings, mute and the rest.
|
|
580
|
-
- **The player's wallets** (Gems, Tickets, their achievement score),
|
|
581
|
-
|
|
582
|
-
|
|
579
|
+
- **The player's wallets** (Gems, Tickets, their achievement score), in the **middle of the row**,
|
|
580
|
+
[always there](#the-arcades-wallet-always-shown-never-drawn), during play as well as on your
|
|
581
|
+
menus. They are translucent, so your game shows through them — but they are still the Arcade's,
|
|
582
|
+
and still taps the Arcade takes.
|
|
583
583
|
|
|
584
|
-
**The top
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
584
|
+
**The top of the screen has one layout, in every game:**
|
|
585
|
+
|
|
586
|
+
| Where | Whose | What goes there |
|
|
587
|
+
| -------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
588
|
+
| **Top-left** | yours | Your **Home / Back** buttons, drawn when a screen needs them, in the space to the left of the wallets. They move around _inside_ your game — back to your level select or title screen — and never leave it. |
|
|
589
|
+
| **Middle and top-right** | the Arcade's | Its wallets and its menu button. Nothing of yours. |
|
|
590
|
+
| **Centre and right, below the Arcade's row** | yours | Your **information**: score, moves, a timer, a level name. It starts under the bottom of the Arcade's row, never beside it or under it. |
|
|
591
|
+
|
|
592
|
+
Keep the left for Home / Back even on a screen that has neither today: a player learns where "back"
|
|
593
|
+
is once, across every game, and information that sits there on one screen teaches them the wrong
|
|
594
|
+
thing on the next.
|
|
595
|
+
|
|
596
|
+
**Put no settings, pause-menu or burger button of your own top-right**: your settings open from the
|
|
597
|
+
Arcade's menu, and a second button under the Arcade's would mean something else in every game.
|
|
598
|
+
|
|
599
|
+
**How much room the top-left has.** The menu button is about 48px across — the size of a Home or
|
|
600
|
+
Back button. With every wallet shown, the narrowest phones (360px wide) leave roughly 68px to the
|
|
601
|
+
left of the wallets: room for **one** button. Two buttons, or a wide one, go on their own row below
|
|
602
|
+
the Arcade's. Do not copy those numbers — read the space, as below.
|
|
588
603
|
|
|
589
604
|
Those rectangles of your frame are **reserved**: keep essential UI and every touch target out of
|
|
590
605
|
them. **Never hard-code their size or height.** They differ by phone, and in native fullscreen the
|
|
591
606
|
whole row may rise into the camera band — read them, as below.
|
|
592
607
|
|
|
593
|
-
An inset cannot say that — as a top inset, a
|
|
608
|
+
An inset cannot say that — as a top inset, a button a few dozen pixels wide would take your whole top
|
|
594
609
|
strip. So `gem.viewport` also carries the rectangles, in the same CSS pixels of your frame, measured
|
|
595
|
-
from its top-left corner:
|
|
610
|
+
from its top-left corner — one per wallet shown, and one for the menu button:
|
|
596
611
|
|
|
597
612
|
```json
|
|
598
613
|
{
|
|
599
614
|
"insets": { "top": 47, "right": 0, "bottom": 34, "left": 0 },
|
|
600
|
-
"reserved": [
|
|
615
|
+
"reserved": [
|
|
616
|
+
{ "x": 68, "y": 51, "width": 72, "height": 28 },
|
|
617
|
+
{ "x": 144, "y": 51, "width": 72, "height": 28 },
|
|
618
|
+
{ "x": 220, "y": 51, "width": 72, "height": 28 },
|
|
619
|
+
{ "x": 304, "y": 51, "width": 48, "height": 48 }
|
|
620
|
+
]
|
|
601
621
|
}
|
|
602
622
|
```
|
|
603
623
|
|
|
604
624
|
**The SDK turns them into one pair of custom properties per corner**, written beside
|
|
605
625
|
`--gem-safe-*` before anything else hears the push: `--gem-reserved-tl-width` and
|
|
606
626
|
`--gem-reserved-tl-height`, and the same pair for `tr`, `bl` and `br` — how far that corner's
|
|
607
|
-
reserved rectangles reach in from its two edges.
|
|
627
|
+
reserved rectangles reach in from its two edges. A rectangle belongs to the corner its centre lies
|
|
628
|
+
nearest to.
|
|
629
|
+
|
|
630
|
+
⚠️ **The wallets sit in the middle, so they count toward BOTH top corners.** A wallet left of
|
|
631
|
+
centre is a top-left rectangle, one right of centre a top-right one. While wallets are shown,
|
|
632
|
+
`--gem-reserved-tl-height` is the bottom of the wallets and `--gem-reserved-tl-width` reaches to the
|
|
633
|
+
middle of the screen — even though the corner itself, left of the first wallet, is clear. So:
|
|
608
634
|
|
|
609
635
|
```css
|
|
610
|
-
/* your
|
|
636
|
+
/* your information — score, moves, a timer — below the WHOLE of the arcade's row: the taller of
|
|
637
|
+
the two top corners. It moves up by itself when gameplay hides the wallets. */
|
|
638
|
+
#info {
|
|
639
|
+
top: max(
|
|
640
|
+
var(--gem-safe-top, env(safe-area-inset-top, 0px)),
|
|
641
|
+
var(--gem-reserved-tl-height, 0px),
|
|
642
|
+
var(--gem-reserved-tr-height, 0px)
|
|
643
|
+
);
|
|
644
|
+
right: var(--gem-safe-right, 0px);
|
|
645
|
+
}
|
|
646
|
+
/* ONE Home or Back button, level with the arcade's row, in the clear corner to the wallets' left */
|
|
611
647
|
#back {
|
|
612
648
|
top: var(--gem-safe-top, env(safe-area-inset-top, 0px));
|
|
613
649
|
left: var(--gem-safe-left, 0px);
|
|
614
650
|
}
|
|
615
|
-
/* a
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
padding-right: var(--gem-reserved-tr-width, 0px);
|
|
651
|
+
/* a ROW of your own buttons does not fit beside the wallets: start it below the arcade's row,
|
|
652
|
+
like your information */
|
|
653
|
+
#nav {
|
|
654
|
+
top: max(
|
|
655
|
+
var(--gem-safe-top, env(safe-area-inset-top, 0px)),
|
|
656
|
+
var(--gem-reserved-tl-height, 0px),
|
|
657
|
+
var(--gem-reserved-tr-height, 0px)
|
|
658
|
+
);
|
|
624
659
|
}
|
|
625
660
|
```
|
|
626
661
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
662
|
+
The per-corner properties cannot say how wide the clear corner is — only that the corner's
|
|
663
|
+
rectangles reach in to the middle. To know whether your button fits beside the wallets, read the
|
|
664
|
+
rectangles from script: the clear width at the top-left is the smallest `x` among
|
|
665
|
+
`gem.viewport.reserved` (the whole width when the list is empty).
|
|
631
666
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
667
|
+
```js
|
|
668
|
+
const reserved = gem.viewport?.reserved ?? [];
|
|
669
|
+
const clearLeft = reserved.length ? Math.min(...reserved.map((r) => r.x)) : window.innerWidth;
|
|
670
|
+
nav.classList.toggle('below-arcade-row', clearLeft < nav.offsetWidth + 8);
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
All eight properties are written on every push, `0px` for a clear corner, so a rotation never leaves
|
|
674
|
+
a stale value; they are never set under an embedder that sends no viewport, which is what the `0px`
|
|
675
|
+
fallback is for. `gem.viewport.reserved` is `[]` when nothing is covered.
|
|
676
|
+
|
|
677
|
+
**Lay out against it whenever it changes.** The wallets' rectangles are there for the whole
|
|
678
|
+
session on the Arcade's phone layout, and their number and width can change as the player's
|
|
679
|
+
balances and currencies do. Read every corner, not only the ones this page names today: a layout that honours
|
|
680
|
+
all four is right under any embedder.
|
|
637
681
|
|
|
638
682
|
**Older SDKs get it folded into the top inset.** A game on an SDK from before reserved areas cannot
|
|
639
683
|
read the rectangles, so the arcade sends it none and instead raises `insets.top` — and so
|
|
640
|
-
`--gem-safe-top` — past the bottom of the
|
|
684
|
+
`--gem-safe-top` — past the bottom of the row. That game keeps working, at the cost of a full-width
|
|
641
685
|
band; upgrading gives the band back.
|
|
642
686
|
|
|
643
687
|
## Which store-page experiment brought this player in: cohorts
|
|
@@ -854,38 +898,25 @@ its music out than cut it, say. The Arcade still shows its button for your game
|
|
|
854
898
|
so this is a promise: your `onPrefs` handler must go quiet on `muted: true` and come back on `false`,
|
|
855
899
|
and the release checklist checks that it does.
|
|
856
900
|
|
|
857
|
-
## The Arcade's wallet:
|
|
901
|
+
## The Arcade's wallet: always shown, never drawn
|
|
858
902
|
|
|
859
903
|
The player's platform balances — Gems, Tickets (`ARCADE_TICKETS`) and their achievement score —
|
|
860
904
|
belong to the Arcade. **Your game never draws one**, and never shows a pending state for one: the
|
|
861
|
-
Arcade takes every figure from the platform and draws the wallets itself,
|
|
862
|
-
the top
|
|
863
|
-
|
|
864
|
-
**
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
as often as you like: before the Arcade connects the state is held and sent when it does, and
|
|
877
|
-
again after any reconnect; a repeat of the current state is not re-sent.
|
|
878
|
-
- **Shown wallets take space; a pop does not.** Shown, they widen the top-right
|
|
879
|
-
[reserved area](#the-arcades-own-controls-reserved-areas) beside the menu button, so lay your
|
|
880
|
-
screen out against `--gem-reserved-tr-*`; hidden, it shrinks back to the button alone. A pop floats
|
|
881
|
-
over your game and never moves its layout.
|
|
882
|
-
- **Narrowing is optional.** On a screen that sells in one currency — a ticket store — you may show
|
|
883
|
-
only that one: `gem.setWalletVisible(true, { only: ['ARCADE_TICKETS'] })`, with `'score'` for the
|
|
884
|
-
achievement score. Use it only there; leaving it out shows every wallet, which is always safe.
|
|
885
|
-
- **Where the Arcade docks its wallets beside your frame** — a desktop page — they are always on
|
|
886
|
-
show, and this changes nothing. Call it anyway; the same code is right everywhere.
|
|
887
|
-
- **`false` means nobody is listening** — no connection yet, or an embedder that draws no wallet.
|
|
888
|
-
Do nothing in its place.
|
|
905
|
+
Arcade takes every figure from the platform and draws the wallets itself, in the middle of the row
|
|
906
|
+
across the top of your frame on a phone, with its menu button at the right end.
|
|
907
|
+
|
|
908
|
+
**The Arcade shows every wallet the whole time, during play too** (since 2026-10-04). They are
|
|
909
|
+
small, translucent chips in the middle of the top row, and they are
|
|
910
|
+
[reserved rectangles](#the-arcades-own-controls-reserved-areas): lay every screen out below both
|
|
911
|
+
`--gem-reserved-tl-height` and `--gem-reserved-tr-height`, gameplay included. There is nothing for
|
|
912
|
+
your game to call.
|
|
913
|
+
|
|
914
|
+
⚠️ **`gem.setWalletVisible` no longer hides anything in the Arcade.** It was how a game hid the
|
|
915
|
+
wallets for gameplay and showed them on its menus, and narrowed them to one currency on a store
|
|
916
|
+
screen. The Arcade still accepts the call — it returns `true`, and existing games need no change —
|
|
917
|
+
but the wallets stay up whatever it is told. **Do not design a screen around the top row being
|
|
918
|
+
clear during play**: it is not. Another embedder may still honour the call, so leaving it in is
|
|
919
|
+
harmless; new code does not need it.
|
|
889
920
|
|
|
890
921
|
## No accidental text selection
|
|
891
922
|
|
|
@@ -1152,6 +1183,18 @@ or after an `await`; and **never gate a reward on it**, because your game is tol
|
|
|
1152
1183
|
not even whether the player shared.
|
|
1153
1184
|
[Linking to your game](getting-started.md#linking-to-your-game) is the section.
|
|
1154
1185
|
|
|
1186
|
+
## If your game vibrates
|
|
1187
|
+
|
|
1188
|
+
Not on the list above either. `navigator.vibrate` does nothing on any iPhone, so call
|
|
1189
|
+
`gem.vibration.vibrate` instead: it takes the same pattern, returns the same boolean, and vibrates
|
|
1190
|
+
wherever the player's device can, the Gem Arcade iPhone app included.
|
|
1191
|
+
|
|
1192
|
+
Two things before you reach for it: **read `gem.vibration.available` live, after `awaitBridge()`**,
|
|
1193
|
+
to decide whether to show your vibration toggle, because on an iPhone it is `false` until the
|
|
1194
|
+
bridge connects; and **never call `navigator.vibrate`**, guarded or not, because the SDK already
|
|
1195
|
+
does wherever that works. Your game owns the toggle and keeps it in `gem.deviceSettings`.
|
|
1196
|
+
[`vibration.md`](vibration.md) is the page.
|
|
1197
|
+
|
|
1155
1198
|
## A decision the player must not make, if your game has one
|
|
1156
1199
|
|
|
1157
1200
|
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
|
@@ -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",
|