@hopple/sdk 0.1.0-rc.3 → 1.0.0-next.12
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/README.md +220 -44
- package/index.d.ts +1093 -879
- package/index.js +2 -29
- package/manifest.d.ts +1107 -0
- package/manifest.js +906 -0
- package/manifest.md +134 -27
- package/package.json +9 -2
- package/config.schema.json +0 -784
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ name and type is documented where it is declared, in the package's
|
|
|
27
27
|
- [Input](#input)
|
|
28
28
|
- [Audio](#audio)
|
|
29
29
|
- [Pause](#pause)
|
|
30
|
+
- [Match](#match)
|
|
30
31
|
- [Checklist](#checklist)
|
|
31
32
|
|
|
32
33
|
## What a game is
|
|
@@ -73,16 +74,16 @@ The SDK is not part of your game. It is **the runtime**, which whatever runs
|
|
|
73
74
|
the game serves beside it, and your page loads as its first script:
|
|
74
75
|
|
|
75
76
|
```html
|
|
76
|
-
<script src="_rt/
|
|
77
|
+
<script src="_rt/hopple.js"></script>
|
|
77
78
|
```
|
|
78
79
|
|
|
79
80
|
The runtime defines the global `Hopple`, holding everything this guide
|
|
80
81
|
describes. Because it is served rather than built into the game, a fix to the
|
|
81
82
|
SDK (input, on-screen controls, audio, layout, the loop, save) reaches every
|
|
82
|
-
game without a rebuild.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
its own origin, and a host like itch.io under a path, with `_rt/` beside
|
|
83
|
+
game without a rebuild. Which release a game gets is its track's: the major
|
|
84
|
+
its manifest's `api` names, or `next`, built with the previews it declares. A change that would break games is a
|
|
85
|
+
new major, which a game takes by changing its `api`. Keep the address
|
|
86
|
+
relative, as above: Hopple serves a game at the root of its own origin, and a host like itch.io under a path, with `_rt/` beside
|
|
86
87
|
`index.html` either way.
|
|
87
88
|
|
|
88
89
|
- **With a bundler** (Vite, esbuild, webpack, Rollup, Parcel, Bun):
|
|
@@ -95,13 +96,13 @@ its own origin, and a host like itch.io under a path, with `_rt/` beside
|
|
|
95
96
|
(`const sdk = await Hopple.init()` in a module script or an async function).
|
|
96
97
|
|
|
97
98
|
`hopple check`, `hopple publish` and the server refuse a page that doesn't
|
|
98
|
-
load the runtime first, or that carries a copy of the SDK.
|
|
99
|
-
before the runtime keep the SDK they bundled, and get no runtime fixes until
|
|
100
|
-
they are rebuilt this way.
|
|
99
|
+
load the runtime first, or that carries a copy of the SDK.
|
|
101
100
|
|
|
102
101
|
A `hopple.config.json` can point its `"$schema"` at
|
|
103
|
-
`./node_modules
|
|
104
|
-
`hopple.config.ts` is checked by its type,
|
|
102
|
+
`./node_modules/hopple/config.schema.json` for an editor to check it; a
|
|
103
|
+
`hopple.config.ts` is checked by its type,
|
|
104
|
+
`import type { HoppleConfig } from 'hopple'`. Both are the CLI's, which reads
|
|
105
|
+
the config.
|
|
105
106
|
|
|
106
107
|
```ts
|
|
107
108
|
import { init } from '@hopple/sdk';
|
|
@@ -140,7 +141,9 @@ Options for `init()`, all optional and rarely needed:
|
|
|
140
141
|
|
|
141
142
|
| Option | Meaning |
|
|
142
143
|
|---|---|
|
|
143
|
-
| `onError` | Called with problems the game should know about but that don't reject a call: a refused analytics event, a
|
|
144
|
+
| `onError` | Called with problems the game should know about but that don't reject a call: a refused analytics event, a save value that JSON would silently change. Defaults to `console.error`. |
|
|
145
|
+
| `timeoutMs` | How long each request waits for the host. Default 10 s. The handshake always waits at least 10 s, whatever this says. |
|
|
146
|
+
| `save` | How to merge two saves made on two devices: `{ merge }`. See [Save](#save). |
|
|
144
147
|
|
|
145
148
|
The host is told about your errors too: from the moment `init()` is called,
|
|
146
149
|
the SDK reports every exception nothing caught, every promise rejection nothing
|
|
@@ -149,9 +152,6 @@ handled, and everything it passes to `onError`, with its stack. That is how
|
|
|
149
152
|
console. Nothing is suppressed: the console prints everything as before. An
|
|
150
153
|
error thrown before `init()` is called, or a script that doesn't parse, can't
|
|
151
154
|
be reported, so look in the browser console when a game never starts.
|
|
152
|
-
| `timeoutMs` | How long each request waits for the host. Default 10 s. The handshake always waits at least 10 s, whatever this says. |
|
|
153
|
-
| `transport` | A channel of your own. By default the SDK finds the host itself. |
|
|
154
|
-
| `scope` | The window to use. For tests. |
|
|
155
155
|
|
|
156
156
|
Everything else hangs off the returned object: `sdk.lifecycle`, `sdk.layout`,
|
|
157
157
|
`sdk.save`, and so on. It also has `sdk.ping()` (resolves with the round-trip
|
|
@@ -361,6 +361,7 @@ sdk.save.available; // false where this host keeps no
|
|
|
361
361
|
sdk.save.status; // what is stored, before you fetch it (see below)
|
|
362
362
|
sdk.save.on((now, previous) => redraw(now)); // status changed: a read, a write or a clear
|
|
363
363
|
const saved = await sdk.save.read(); // { version, state } | null
|
|
364
|
+
sdk.save.onChange((now, why) => restore(now)); // the save changed under the game: play on from this
|
|
364
365
|
await sdk.save.write(state, { version: 2 }); // resolves once the host says it is safely stored
|
|
365
366
|
await sdk.save.clear();
|
|
366
367
|
sdk.save.maxBytes; // the largest save this host accepts
|
|
@@ -377,6 +378,47 @@ sdk.save.maxBytes; // the largest save this host acce
|
|
|
377
378
|
|
|
378
379
|
It follows the game's own reads, writes and clears.
|
|
379
380
|
|
|
381
|
+
**Two ways to load.** A signed-in player's save comes from a server, which
|
|
382
|
+
may take a moment:
|
|
383
|
+
|
|
384
|
+
- **Wait for it:** `await sdk.save.read()` before the first frame. The host
|
|
385
|
+
waits for the server (a few seconds at most, and not at all offline) and
|
|
386
|
+
answers with the player's save.
|
|
387
|
+
- **Start at once:** listen with `onChange`, call `read()` without awaiting
|
|
388
|
+
it, and apply whatever either hands over. The game plays from nothing, or
|
|
389
|
+
from this device's save, and takes the player's save when it comes.
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
const restore = (s: SaveState | null) => { progress = s ? (s.state as Progress) : fresh(); };
|
|
393
|
+
sdk.save.onChange(restore); // before read()
|
|
394
|
+
sdk.save.read().then(restore);
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**Listen with `onChange` either way.** The save can also change while the
|
|
398
|
+
game plays: the server answered later than the host would wait, or a page left
|
|
399
|
+
open hears that another device played on. `onChange(save, why)` hands the game
|
|
400
|
+
the save to play on from then, and why:
|
|
401
|
+
|
|
402
|
+
| `why` | What happened |
|
|
403
|
+
|---|---|
|
|
404
|
+
| `'arrived'` | Another device's save, built on what the game had. With a `merge`, merged with what the game last saved. |
|
|
405
|
+
| `'merged'` | Two saves that went their own ways, made one by the game's `merge`. |
|
|
406
|
+
| `'gone'` | Thrown away on another device, with nothing made here since. `save` is `null`. |
|
|
407
|
+
|
|
408
|
+
To keep the game's own instead (progress it hasn't saved yet, or a New game
|
|
409
|
+
the player chose a moment before a save arrived), write it from the listener:
|
|
410
|
+
it takes the handed save's place.
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
sdk.save.onChange((now, why) => {
|
|
414
|
+
if (why === 'arrived' && justStartedNewGame) void sdk.save.write(progress); // the New game wins
|
|
415
|
+
else restore(now);
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
A game that does not listen is never written over: its next write is kept
|
|
420
|
+
beside the save that arrived, for its `merge` or the player to settle.
|
|
421
|
+
|
|
380
422
|
How to use it:
|
|
381
423
|
|
|
382
424
|
- **Save as you go, not only at the end.** Nothing saves on your behalf, and a
|
|
@@ -386,17 +428,47 @@ How to use it:
|
|
|
386
428
|
- **`version` is your own save format number.** It is stored next to the data,
|
|
387
429
|
so you can check it before deciding whether to read an old save. There is no
|
|
388
430
|
migration hook: write the `if` yourself.
|
|
389
|
-
- **Strings are stored as they are
|
|
390
|
-
`JSON.stringify`.**
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
431
|
+
- **Strings are stored as they are, and a `Uint8Array` as its bytes; anything
|
|
432
|
+
else goes through `JSON.stringify`.** Each comes back as it went in: a
|
|
433
|
+
string, a `Uint8Array`, or the parsed value. So a `Map`, a `Date` or a typed
|
|
434
|
+
array inside an object won't come back the way it went in. Inside
|
|
435
|
+
`hopple dev`, the SDK warns (via `onError`) when you save something JSON
|
|
436
|
+
would change.
|
|
437
|
+
- **Bytes pack a big save small.** Write a `Uint8Array` with
|
|
438
|
+
[`@hopple/bytes`](https://www.npmjs.com/package/@hopple/bytes) (varints, floats, strings, and flags a
|
|
439
|
+
bit each), and it is stored and counted as those bytes.
|
|
440
|
+
- **Every host accepts at least 64 KiB** (`SAVE_BYTES`), counted in UTF-8 for
|
|
441
|
+
a string or JSON and in bytes for a `Uint8Array`. A larger save is rejected
|
|
442
|
+
with `too_large` before it is sent.
|
|
443
|
+
|
|
444
|
+
**Two devices.** A signed-in player's save follows them to every device. When
|
|
445
|
+
two devices each saved without seeing the other's progress (one was offline),
|
|
446
|
+
the host keeps both and asks the game to merge them, with the `merge` passed to
|
|
447
|
+
`init()`:
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
const sdk = await init({
|
|
451
|
+
save: {
|
|
452
|
+
merge: (mine, theirs) => ({ version: 2, state: combine(mine.state, theirs.state) }),
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
sdk.save.onChange((now) => restore(now)); // merged while playing: play on from this
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
- `merge(mine, theirs)` is pure and answers with the save to keep in place of
|
|
459
|
+
both. Throw where the two can't be merged, and the host asks the player
|
|
460
|
+
which to keep, as it does for a game with no `merge`.
|
|
461
|
+
- Asked while the game loads, the merge is what `read()` resolves with. Asked
|
|
462
|
+
while it plays, it goes to `onChange` listeners, and waits for the next load
|
|
463
|
+
where there are none.
|
|
464
|
+
- A save that arrives while the game plays is merged with what the game last
|
|
465
|
+
saved too, where it has a `merge`, before `onChange` hears it.
|
|
466
|
+
- Inside `hopple dev`, the SDK warns (via `onError`) when the save changed
|
|
467
|
+
while nothing listened with `onChange`.
|
|
396
468
|
|
|
397
469
|
| Error | Meaning |
|
|
398
470
|
|---|---|
|
|
399
|
-
| `unsupported` | This host has nowhere to store saves (`available` is `false`)
|
|
471
|
+
| `unsupported` | This host has nowhere to store saves (`available` is `false`), or is too old to keep a `Uint8Array`. |
|
|
400
472
|
| `too_large` | Over `sdk.save.maxBytes`. |
|
|
401
473
|
| `rate_limited` | Saving too often, usually from inside the game loop. |
|
|
402
474
|
| `bad_params` | The state can't be turned into JSON at all (a cycle, a `BigInt`). |
|
|
@@ -477,7 +549,8 @@ sdk.share.offer({
|
|
|
477
549
|
meta: { level: 3, from: 'results-screen' }, // kept by the platform, never handed back
|
|
478
550
|
});
|
|
479
551
|
|
|
480
|
-
sdk.share.data; // the
|
|
552
|
+
sdk.share.data; // the string from the link that opened this run, or null
|
|
553
|
+
sdk.share.bytes; // the Uint8Array from the link that opened this run, or null
|
|
481
554
|
sdk.share.available; // whether this host can make links at all
|
|
482
555
|
```
|
|
483
556
|
|
|
@@ -492,16 +565,21 @@ sdk.share.available; // whether this host can make links at all
|
|
|
492
565
|
- **Show your Share button from `sdk.share.available`**, never from
|
|
493
566
|
`sdk.share.data`: `null` there only means this player didn't arrive by a
|
|
494
567
|
link.
|
|
495
|
-
- **Treat `sdk.share.data` as untrusted input.**
|
|
496
|
-
game. Check
|
|
497
|
-
|
|
498
|
-
|
|
568
|
+
- **Treat `sdk.share.data` and `sdk.share.bytes` as untrusted input.** They
|
|
569
|
+
come from outside your game. Check them as strictly as you would any user
|
|
570
|
+
input.
|
|
571
|
+
- `data` is a string, which comes back as `sdk.share.data`, or a `Uint8Array`,
|
|
572
|
+
which comes back as `sdk.share.bytes`. Either way it is at most 512 bytes
|
|
573
|
+
(`SHARE_BYTES`) on every host. Bytes pack a board or a replay far tighter
|
|
574
|
+
than text: see [`@hopple/bytes`](https://www.npmjs.com/package/@hopple/bytes). Put anything bigger in
|
|
575
|
+
your save.
|
|
499
576
|
- `meta` is any JSON object, at most 1024 bytes of JSON (`SHARE_META_BYTES`) on
|
|
500
577
|
every host. No game ever reads it back.
|
|
501
578
|
|
|
502
579
|
Errors go to `onError`: `too_large`, `rate_limited` (the host won't offer a
|
|
503
|
-
link right now), `bad_params` (`data` isn't a string
|
|
504
|
-
object)
|
|
580
|
+
link right now), `bad_params` (`data` isn't a string or a `Uint8Array`, or
|
|
581
|
+
`meta` isn't a JSON object), `unsupported` (bytes, on a host too old to carry
|
|
582
|
+
them). On a host that can't make links, `offer` does nothing.
|
|
505
583
|
|
|
506
584
|
The website and the dev shell support sharing.
|
|
507
585
|
|
|
@@ -518,7 +596,9 @@ sdk.panel.available; // false where there are none
|
|
|
518
596
|
sdk.panel.open('scores'); // returns nothing: what happens arrives through sdk.panel.on
|
|
519
597
|
```
|
|
520
598
|
|
|
521
|
-
The possible pages are `about`, `scores`, `achievements` and `input`.
|
|
599
|
+
The possible pages are `about`, `scores`, `achievements` and `input`. A game
|
|
600
|
+
that plays over the network also has `room`, the player's party, from
|
|
601
|
+
[`@hopple/sdk-preview-network`](https://www.npmjs.com/package/@hopple/sdk-preview-network).
|
|
522
602
|
|
|
523
603
|
- **Check `sdk.panel.pages` before drawing a button for a page.** It is known
|
|
524
604
|
from the start, so you can decide while laying out your menu. If the host has
|
|
@@ -601,21 +681,42 @@ Call `read()` **once per frame**. It reports presses that started and ended
|
|
|
601
681
|
since the previous call (so a tap shorter than a frame isn't lost), which means
|
|
602
682
|
a second call in the same frame sees none of them.
|
|
603
683
|
|
|
604
|
-
### Showing
|
|
684
|
+
### Showing which button to press
|
|
605
685
|
|
|
606
686
|
```ts
|
|
607
|
-
prompt.textContent = `Press ${sdk.input.
|
|
687
|
+
prompt.textContent = `Press ${sdk.input.glyph('shoot')} to shoot`; // written once
|
|
688
|
+
prompt.setAttribute('aria-label', `Press ${sdk.input.describe('shoot')} to shoot`);
|
|
608
689
|
|
|
609
|
-
|
|
610
|
-
sdk.input.
|
|
690
|
+
ctx.font = `20px 'My Font', sans-serif, ${sdk.input.font}`; // on a canvas
|
|
691
|
+
ctx.fillText(`Press ${sdk.input.glyph('shoot')} to shoot`, x, y);
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
```css
|
|
695
|
+
body { font-family: 'My Font', sans-serif, hopple-keys; }
|
|
611
696
|
```
|
|
612
697
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
`Z`
|
|
616
|
-
on
|
|
617
|
-
the player rebinds
|
|
618
|
-
|
|
698
|
+
`sdk.input.glyph(action)` is one character, drawn by a font the SDK builds:
|
|
699
|
+
every key or button bound to the action for the device in the player's hands —
|
|
700
|
+
`[Z] / [Space]` as keycaps, a pad's face buttons as a diamond with the one to
|
|
701
|
+
press filled, the same on every controller, the on-screen button on a touch
|
|
702
|
+
screen. When the player rebinds the action or picks up another device the SDK
|
|
703
|
+
swaps the font, so text in the DOM is right without being touched, and a canvas
|
|
704
|
+
is right the next time it is painted (a canvas drawn once redraws on
|
|
705
|
+
`sdk.input.on`). `glyph('pad.start')` draws a button that is none of your
|
|
706
|
+
actions.
|
|
707
|
+
|
|
708
|
+
**Name `hopple-keys` (`sdk.input.font`) last in every font stack that prints
|
|
709
|
+
a glyph**, the canvas's included. A stack without it draws a box; one that
|
|
710
|
+
names it first changes the line height of its text in some browsers. Children
|
|
711
|
+
inherit a `body` rule, form controls don't unless they set `font: inherit`.
|
|
712
|
+
Under `hopple dev` a glyph printed anywhere else is reported to `onError`.
|
|
713
|
+
|
|
714
|
+
`sdk.input.describe(action)` is the same in words (`Z or Space`, `bottom face
|
|
715
|
+
button`), for an `aria-label` and for text drawn without fonts.
|
|
716
|
+
`sdk.input.mapping[action].label` still names one binding in text, and
|
|
717
|
+
`sdk.input.scheme` is what the player is using: `'keyboard' | 'mouse' | 'touch'
|
|
718
|
+
| 'gamepad'`. `sdk.input.on` fires when the player rebinds a key (`mapping`
|
|
719
|
+
changed) or switches device (`scheme` changed).
|
|
619
720
|
|
|
620
721
|
### On-screen controls
|
|
621
722
|
|
|
@@ -631,8 +732,74 @@ sdk.input.controls.showing; // whether controls are actually visible
|
|
|
631
732
|
- Hiding them only stops drawing them; keys and gamepads keep working.
|
|
632
733
|
- While shown, they appear in `sdk.layout.obstructions` as reserved
|
|
633
734
|
rectangles, so code that avoids the host's buttons avoids them too.
|
|
634
|
-
- They are drawn in a shadow root inside your frame, so
|
|
635
|
-
|
|
735
|
+
- They are drawn in a shadow root inside your frame, so they don't appear in
|
|
736
|
+
your DOM and your CSS only reaches them where it is meant to: through
|
|
737
|
+
`hopple-controls::part(…)` and the `--hopple-pad-*` properties (see
|
|
738
|
+
[Styling them](#styling-the-on-screen-controls)).
|
|
739
|
+
|
|
740
|
+
### Styling the on-screen controls
|
|
741
|
+
|
|
742
|
+
```css
|
|
743
|
+
hopple-controls {
|
|
744
|
+
--hopple-pad-tint: #2b1d0ecc; /* a button's fill */
|
|
745
|
+
--hopple-pad-edge: #f5c542; /* rims, the knob */
|
|
746
|
+
--hopple-pad-ink: #fff8e1; /* labels and icons */
|
|
747
|
+
--hopple-pad-radius: 14px;
|
|
748
|
+
--hopple-pad-font: 700 16px/1 'My Font', sans-serif, hopple-keys;
|
|
749
|
+
}
|
|
750
|
+
hopple-controls::part(face pressed) { translate: 0 2px; }
|
|
751
|
+
hopple-controls::part(face action-jump) { background: url(ui/jump.png) center / 70% no-repeat; }
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
```ts
|
|
755
|
+
sdk.input.icon('jump', 'M12 3 4 13h5v8h6v-8h5z'); // a filled path in a 24×24 box, or null
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
- Parts: `face`, `label` and `icon` on a button; `group` behind a group of
|
|
759
|
+
buttons; `well`, `knob`, `groove` and `arrow` on a stick. Each also carries
|
|
760
|
+
`action-<id>` and `control-<id>`, and the states `pressed`, `held`,
|
|
761
|
+
`floating` and `resting`.
|
|
762
|
+
- `<hopple-controls>` has `layout`, `sticks` and `look` attributes, and a
|
|
763
|
+
stick has `--hopple-stick-x` and `--hopple-stick-y` (−1 to 1) for a game to
|
|
764
|
+
read.
|
|
765
|
+
- Sizes (`--hopple-pad-size`, `-stick`, `-track`, `-gap`, `-inset`) are held
|
|
766
|
+
between a size a thumb can press and one that leaves the game room. How big
|
|
767
|
+
one control is next to the others is its `scale` in the manifest. What a
|
|
768
|
+
part draws never changes where a touch lands.
|
|
769
|
+
- A player can turn a game's look off, for every game or for one, and gets the
|
|
770
|
+
plain controls back: test that they still read well over the game.
|
|
771
|
+
- Under `hopple dev`, a rule that names a part or a property the controls don't
|
|
772
|
+
have is reported to `onError`.
|
|
773
|
+
|
|
774
|
+
### Several players on one device
|
|
775
|
+
|
|
776
|
+
A manifest's `input.player` declares what each local player does, and
|
|
777
|
+
`players.local` how many may play on one device: one set of actions every
|
|
778
|
+
player has a copy of, bound once for every pad, and the parts of the keyboard
|
|
779
|
+
players share (`keyboard`, each with a title and its keys).
|
|
780
|
+
|
|
781
|
+
```ts
|
|
782
|
+
sdk.input.joining = true; // the screen where players join
|
|
783
|
+
sdk.input.onPlayers((players) => drawRoster(players));
|
|
784
|
+
for (const player of sdk.input.players) { // the first player is always there
|
|
785
|
+
const now = player.read(); // once a frame, as sdk.input.read()
|
|
786
|
+
if (now.started('bomb')) dropBomb(player.slot);
|
|
787
|
+
label[player.slot].textContent = player.glyph('bomb'); // their own key or button
|
|
788
|
+
}
|
|
789
|
+
sdk.input.joining = false; // play starts
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
- **The first player hears every controller nobody claimed**, so a player
|
|
793
|
+
alone loses nothing between devices. Setting `joining` gives them the one
|
|
794
|
+
they last used; a join action pressed on a free pad or part of the keyboard
|
|
795
|
+
seats a new player with it, up to `mostPlayers`, and is nobody else's press.
|
|
796
|
+
- **`slot`** is a player's place from 0, kept until `leave()`.
|
|
797
|
+
**`controller`** is what they hold, or `null` once their pad went away:
|
|
798
|
+
they keep their place, and the next free pad pressed is theirs again.
|
|
799
|
+
- **`sdk.input.read()` and `onAction` answer for player actions too**, as
|
|
800
|
+
anybody pressing them, for menus any player may work.
|
|
801
|
+
- **In a match**, the device tells the match how many play on it, and
|
|
802
|
+
`mostPlayers` takes no more than the match has room for.
|
|
636
803
|
|
|
637
804
|
### Text fields
|
|
638
805
|
|
|
@@ -725,6 +892,14 @@ sdk.pause.on(async (paused) => { // the host asked; return a promise to make i
|
|
|
725
892
|
});
|
|
726
893
|
```
|
|
727
894
|
|
|
895
|
+
## Match
|
|
896
|
+
|
|
897
|
+
Play over the network, in matches: `sdk.match`. It is a preview, not stable
|
|
898
|
+
yet, so it is a package of its own,
|
|
899
|
+
[`@hopple/sdk-preview-network`](https://www.npmjs.com/package/@hopple/sdk-preview-network), which
|
|
900
|
+
the game installs and declares in its manifest as `previews.network`, at its
|
|
901
|
+
revision. A game that does not declare it has no `sdk.match`.
|
|
902
|
+
|
|
728
903
|
## Checklist
|
|
729
904
|
|
|
730
905
|
- [ ] Let a rejected `init()` through rather than catching it.
|
|
@@ -738,10 +913,11 @@ sdk.pause.on(async (paused) => { // the host asked; return a promise to make i
|
|
|
738
913
|
- [ ] Submit every run's score. Only show a rank when you get one.
|
|
739
914
|
- [ ] Share and open the panel only from a player's press.
|
|
740
915
|
- [ ] Validate `sdk.share.data` like any untrusted input.
|
|
741
|
-
- [ ] Print button
|
|
916
|
+
- [ ] Print button prompts with `sdk.input.glyph` (and `sdk.input.describe` for words), and read input once per frame.
|
|
742
917
|
- [ ] Route every sound through `sdk.audio.output` or `media`, or multiply it by `sdk.audio.volume(channel)`, and obey mute.
|
|
743
918
|
- [ ] Keep `sdk.pause` up to date, separately from the loop.
|
|
744
|
-
- [ ]
|
|
919
|
+
- [ ] For network play, install `@hopple/sdk-preview-network` and declare `previews.network`, leave the lobby and invites to the host, and start matches from a player's press.
|
|
920
|
+
- [ ] Load the runtime as the page's first script, `<script src="_rt/hopple.js"></script>`, and import `@hopple/sdk` rather than bundling a copy.
|
|
745
921
|
- [ ] Refer to every file relatively, and publish the output directory.
|
|
746
922
|
|
|
747
923
|
## License
|