@fabi-play/sdk 0.2.0 → 0.3.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/README.md CHANGED
@@ -80,6 +80,85 @@ of the game, or a move shown with the children's own avatars, which they can als
80
80
  - **An invalid intro** is reported by the check (the Studio, `parseManifest`, the JSON Schema) and skipped by the
81
81
  TV: it never blocks the game.
82
82
 
83
+ ## Where the children are: the view and framing
84
+
85
+ `play.view` is where the camera image lies on your page, so each child's avatar and hands are where the child stands.
86
+ Draw anything that belongs to a child at `play.toScreen(point)` (a wrist, the head, the avatar's box), never at your
87
+ own mapping of player space.
88
+
89
+ The view is not fixed. By default the screen frames the children: when they stand far from the camera it zooms in on
90
+ them, up to 1.8×, so every child's avatar is about the same size and reaches the same things, near or far. It decides
91
+ the zoom at calibration and changes it only in breaks (pause, level, turn), never during play; the image may then
92
+ reach past every edge of the page. Listen for it:
93
+
94
+ ```js
95
+ play.onBackdrop((backdrop, view) => layoutAround(view))
96
+ ```
97
+
98
+ A game drawn on exact spots of the live video can keep the whole image with `"framing": "fixed"` in `fabi-play.json`.
99
+
100
+ ## Playing with the TV remote
101
+
102
+ On TVs (Samsung, LG, Android TV, Fire TV) Fabi Play can be played without a phone: with the TV remote. A game
103
+ opts in with `inputs` in `fabi-play.json`:
104
+
105
+ ```json
106
+ "inputs": ["body", "remote"]
107
+ ```
108
+
109
+ - **`inputs`** is optional; absent means `["body"]`. It always includes `"body"` (Fabi Play is a movement
110
+ platform), lists each input once, and knows only `body` and `remote`: anything else is an error in the check
111
+ (the Studio, `parseManifest`, the JSON Schema).
112
+ - **With one child, most games need no code.** The screen turns the remote's keys into the gestures the body makes, and animates a
113
+ puppet body in the frames (as the simulator does), so `gesture` events, `drawAvatar` and the keyboard adapter
114
+ (`data-keys`, `FabiPlay.keys()`) work as with a camera:
115
+
116
+ | Key | Gesture |
117
+ | --- | --- |
118
+ | ↑ or OK | `jump` (then `land`) |
119
+ | ↓ held | `squat`, then `stand` on release |
120
+ | ← / → | `lane-left` / `lane-right` (stepping back to the middle is `lane-center`, as with the body) |
121
+ | OK twice quickly | `clap` |
122
+ | Back | Fabi Play's pause (a `pause` message), never the game's |
123
+
124
+ Nothing else is made: no hands aiming, no shapes, no freeze. The game never gets the keys themselves.
125
+ - **The Magic Remote (LG) points too.** It reaches the game as ordinary pointer and mouse events: hover, and a
126
+ click with OK. `play.pointer` is `true` when the remote points (only with `"remote"`). A game that lists
127
+ `"remote"` must work with the arrow keys alone and, where it has things to pick (bubbles, answers, buttons), let a
128
+ pointer click them as well: pointer events, large hit areas, a hover highlight. Fabi Play's own menus, pickers
129
+ and turn screen are clickable too.
130
+ - **One remote is one child; several children take turns, a round each, if the game follows the contract.**
131
+ Turns are not free: the game must follow the steps below, and say so in `fabi-play.json` with
132
+ `"turns": true` (only next to `"remote"` in `inputs`): `"inputs": ["body", "remote"], "turns": true`. A game
133
+ with `"turns": true` follows the turn contract. Only then does the screen offer several children with the
134
+ remote; without it, remote mode is one child only.
135
+
136
+ The game sees one child at a time. `play.players` is the child playing now (exactly one id, a frozen array: read
137
+ it again after a turn), and `play.turns` is every child who plays, in order (`[1, 2, 3]`; one child, or the body:
138
+ the same as `play.players`). The contract:
139
+ 1. Play a round (a race, a level) with the child in `play.players`.
140
+ 2. When that child's round ends, whatever happened in it (a crash or a fall ends a round too), and turns remain,
141
+ call `play.break('turn')`. The screen always answers `break-start` (its turn screen is the break, and may
142
+ show an ad), then a `turn` message (`play.on('turn', ({ players }) => …)`, or `FabiPlay.on('turn', …)`),
143
+ after which `play.players` is the next child, then `break-done`. Start that child's round.
144
+ 3. After the last child's round, call `play.end()` with every child's scores:
145
+ `play.end([{ player: 1, … }, { player: 2, … }])`.
146
+
147
+ Only `break('turn')` swaps children: `break('level')` and `break('gameover')` never do, so a round never changes
148
+ hands midway. The SDK takes a `turn` only inside a `'turn'` break, and only to the next child in `play.turns`; it
149
+ drops any other. Player ids keep their meaning (slots, avatars): keep each child's score by id. At a turn the next
150
+ child's puppet starts recentred, in the middle lane, with no lane gesture (the keyboard adapter resets its lane
151
+ too). With the simple API, `score(n)`
152
+ without a player goes to the child playing now, and `end(n)` gives the child playing now `n` and each child
153
+ before them their last `score()` (or 0). A body session never gets a `turn`: its players stay for the whole
154
+ game.
155
+ - **Adapt with `play.input`** (`'body'`, the default and what older screens mean, or `'remote'`; also in the
156
+ `start` event with `pointer`, and live as `FabiPlay.input`, `FabiPlay.hasPointer`, `FabiPlay.players`,
157
+ `FabiPlay.turns`): slow the game down a little, show a selection the arrows move (and a pointer
158
+ clicks) instead of a hand cursor, drop what the remote can't do.
159
+ - **Games aimed with the hands** (`movements: ["hands"]`, the `pointer()` adapter) should not list `"remote"`
160
+ unless they offer a remote-friendly selection: a child with a remote has no hand cursor.
161
+
83
162
  Docs: https://docs.fabiplay.app.
84
163
 
85
164
  ## License
package/dist/avatars.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  drawShapes,
7
7
  frameOf,
8
8
  iconShapes
9
- } from "./chunks/chunk-57WXWAXG.js";
9
+ } from "./chunks/chunk-GFNOIOSX.js";
10
10
  export {
11
11
  BAND_BODY,
12
12
  DEFAULT_AVATAR,
@@ -57,6 +57,14 @@ function parseGameStats(v) {
57
57
  if (DEVICE_TIERS.includes(v.tier)) out.tier = v.tier;
58
58
  return out;
59
59
  }
60
+ var isPlayerId = (id) => Number.isInteger(id) && id >= 1 && id <= MAX_PLAYERS;
61
+ function parseTurnPlayers(v) {
62
+ return Array.isArray(v) && v.length === 1 && isPlayerId(v[0]) ? [v[0]] : void 0;
63
+ }
64
+ function parseTurnOrder(v) {
65
+ if (!Array.isArray(v) || v.length < 1 || v.length > MAX_PLAYERS || !v.every(isPlayerId) || new Set(v).size !== v.length) return void 0;
66
+ return [...v];
67
+ }
60
68
  var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
61
69
  var isNumber = (v) => typeof v === "number" && Number.isFinite(v);
62
70
  var POSE_HEADER = 12;
@@ -2004,9 +2012,11 @@ function smoothBody(prev, next, amount = 0.5) {
2004
2012
  const wasSeen = pv >= 0.5;
2005
2013
  const isSeen = nv >= 0.5;
2006
2014
  if (wasSeen && nv >= 0.3) out[i + 2] = Math.max(nv, 0.5);
2007
- if (wasSeen && isSeen && Math.hypot(next[i] - prev[i], next[i + 1] - prev[i + 1]) < 0.15) {
2008
- out[i] = prev[i] + (next[i] - prev[i]) * amount;
2009
- out[i + 1] = prev[i + 1] + (next[i + 1] - prev[i + 1]) * amount;
2015
+ const step = Math.hypot(next[i] - prev[i], next[i + 1] - prev[i + 1]);
2016
+ if (wasSeen && isSeen && step < 0.15) {
2017
+ const k = amount * Math.min(1, STILL_SHARE + (1 - STILL_SHARE) * step / STILL_FULL);
2018
+ out[i] = prev[i] + (next[i] - prev[i]) * k;
2019
+ out[i + 1] = prev[i + 1] + (next[i + 1] - prev[i + 1]) * k;
2010
2020
  } else if (wasSeen && !isSeen && out[i + 2] >= 0.5) {
2011
2021
  out[i] = prev[i];
2012
2022
  out[i + 1] = prev[i + 1];
@@ -2015,6 +2025,8 @@ function smoothBody(prev, next, amount = 0.5) {
2015
2025
  smoothFace(prev, next, out, amount * FACE_SMOOTHING);
2016
2026
  return out;
2017
2027
  }
2028
+ var STILL_SHARE = 0.1;
2029
+ var STILL_FULL = 0.03;
2018
2030
  var FACE_SMOOTHING = 0.35;
2019
2031
  var FACE_POINTS = 11;
2020
2032
  function smoothFace(prev, next, out, amount) {
@@ -2098,6 +2110,8 @@ export {
2098
2110
  toScreen,
2099
2111
  parseDeviceHint,
2100
2112
  parseGameStats,
2113
+ parseTurnPlayers,
2114
+ parseTurnOrder,
2101
2115
  drawShapes,
2102
2116
  BAND_BODY,
2103
2117
  frameOf,
@@ -7,8 +7,8 @@
7
7
 
8
8
  /** The Fabi Play SDK's types, for script-tag games: FabiPlay.PlayerFrame, FabiPlay.Point, FabiPlay.Gesture, … */
9
9
  declare namespace FabiPlay {
10
- /** Anything with Play's on(): a Play, or a stand-in in tests. */
11
- export type On = Pick<Play, "on">;
10
+ /** Anything with Play's on() (and, from a Play, who plays and how): a Play, or a stand-in in tests. */
11
+ export type On = Pick<Play, "on"> & Partial<Pick<Play, "input" | "players">>;
12
12
  /** What the child does, as the keyboard adapter names it. */
13
13
  export type Action = "jump" | "squat" | "left" | "right" | "clap" | "arms-up";
14
14
  /** Action → key code (KeyboardEvent.code: Space, Enter, ArrowUp, KeyW, Digit1…). */
@@ -29,7 +29,11 @@ declare namespace FabiPlay {
29
29
  }
30
30
  /** Options of keyboard(). */
31
31
  export interface KeyboardOptions {
32
- /** Whose movements (default 1); another player can get other keys (e.g. WASD). */
32
+ /**
33
+ * Whose movements (default 1); another player can get other keys (e.g. WASD). Not given and played with the
34
+ * remote: the child playing now, following the turns (at a turn the lane is the middle one again, as the screen
35
+ * recentres the puppet).
36
+ */
33
37
  player?: number;
34
38
  /** Where the keys go: by default the page (domKeys()). */
35
39
  target?: KeyTarget;
@@ -82,10 +86,15 @@ declare namespace FabiPlay {
82
86
  /**
83
87
  * Why the game stops, when it asks for a break:
84
88
  * - level: a level is over (the best moment);
85
- * - gameover: the game is over and a new one may start.
86
- * Never at the game's own pause menu: a child who stops for a moment must not get an ad for it.
89
+ * - gameover: the game is over and a new one may start;
90
+ * - turn: with input 'remote' and turns remaining, the child playing now finished their round (whatever happened in
91
+ * it: a crash or a fall ends a round too). The screen always answers with break-start (its turn screen is the
92
+ * break, and may host an ad), then turn (the next child), then break-done. Hosts before turns answer it as an
93
+ * unknown reason (break-done at once), but they never send turns either.
94
+ * Only 'turn' ever swaps players; 'level' and 'gameover' never do. Never at the game's own pause menu: a child who
95
+ * stops for a moment must not get an ad for it.
87
96
  */
88
- export type BreakReason = "level" | "gameover";
97
+ export type BreakReason = "level" | "gameover" | "turn";
89
98
  /**
90
99
  * The movements the host recognises, per player:
91
100
  * - `jump`, then `land` when the feet are back on the ground;
@@ -140,7 +149,9 @@ declare namespace FabiPlay {
140
149
  * image is cropped to fill the screen), with any backdrop, so avatars and hands are where the children stand (a
141
150
  * "virtual mirror"). The screen decides it: with 'video', where the video is drawn (cover or contain); with 'none',
142
151
  * the whole image contained at 92% of the viewport's height, bottom-aligned. Games don't compute it: they just use
143
- * it. Body points are in player space (0–1 of the camera image, already mirrored like the video), so a point p is on
152
+ * it. It is not fixed: with automatic framing (the default, see GameManifest.framing) the screen zooms in on children
153
+ * standing far away, up to 1.8×, so the image may reach past every edge; it changes only at calibration and in breaks
154
+ * (pause, level, turn), never during play, and arrives in a backdrop message. Body points are in player space (0–1 of the camera image, already mirrored like the video), so a point p is on
144
155
  * screen at (x + p.x·w, y + p.y·h): see toScreen.
145
156
  */
146
157
  export interface BackdropView {
@@ -183,6 +194,22 @@ declare namespace FabiPlay {
183
194
  /** view: where the camera image is, with any backdrop (see BackdropView); absent if the screen doesn't know. */
184
195
  /** stats: a debug host (the simulator) asks the game to say how often it draws (stats messages); absent on the TV. */
185
196
  /** device: what the screen knows about the device (a starting tier, the display's rate); absent if it knows nothing. */
197
+ /**
198
+ * input: how this session is played (see GameInput); absent = 'body'. Only games whose manifest lists 'remote' ever
199
+ * get 'remote'. With 'remote', players is exactly one id: the child playing now (see turn).
200
+ */
201
+ /**
202
+ * turns: only with input 'remote', and only to games whose manifest says "turns": true: every child who
203
+ * will play this game, one id each, in turn order, players[0] first (see parseTurnOrder); absent = players (one
204
+ * child, no turns). A round per child: at the end of each child's round, while turns remain, the game calls
205
+ * break('turn'); the screen answers break-start, turn (the next child), break-done. After the last child's round
206
+ * the game calls end() with every child's scores. An order that is not valid, or doesn't start with players[0],
207
+ * makes the session one child only.
208
+ */
209
+ /**
210
+ * pointer: only with input 'remote', true when the TV's remote also points (LG's Magic Remote): it reaches the game
211
+ * as ordinary pointer and mouse events (hover, click with OK). Absent = false.
212
+ */
186
213
  {
187
214
  type: "init";
188
215
  v: number;
@@ -195,8 +222,11 @@ declare namespace FabiPlay {
195
222
  view?: BackdropView;
196
223
  stats?: boolean;
197
224
  device?: DeviceHint;
225
+ input?: GameInput;
226
+ turns?: number[];
227
+ pointer?: boolean;
198
228
  }
199
- /** The backdrop or the view changed mid-game (video switched on or off, the camera image's fit changed). */
229
+ /** The backdrop or the view changed mid-game (video switched on or off, the camera image's fit or framing changed). */
200
230
  | {
201
231
  type: "backdrop";
202
232
  backdrop: Backdrop;
@@ -235,6 +265,19 @@ declare namespace FabiPlay {
235
265
  | {
236
266
  type: "break-done";
237
267
  shown: boolean;
268
+ }
269
+ /**
270
+ * Turns (input 'remote' only): from now on this child plays, in place of the one before. One remote is one child,
271
+ * so with several children the screen plays one at a time: the same game goes on with the next child in init's
272
+ * turns (players [2] after [1], …), exactly one id. Sent only inside the break of a break('turn') the game asked
273
+ * for (after break-start, before break-done); never at other breaks, so a round never changes hands midway. The
274
+ * new child's puppet starts recentred, in the middle lane (lane 0), with no lane gesture. Ids keep their meaning
275
+ * (slots, avatars): keep each player's score by id across turns, and end with the scores of every child who
276
+ * played. Never sent with input 'body', where init's players stay for the whole game.
277
+ */
278
+ | {
279
+ type: "turn";
280
+ players: number[];
238
281
  };
239
282
  /** Why a game is paused (see HostMessage pause). */
240
283
  export type PauseReason = "user" | "distance";
@@ -330,7 +373,39 @@ declare namespace FabiPlay {
330
373
  backdrop?: GameBackdrop[];
331
374
  /** Optional: Fabi explains the game on the TV before calibration (see GameIntro). Absent: straight to calibration. */
332
375
  intro?: GameIntro;
376
+ /** Optional: how it can be played; absent = ['body']. Must include 'body' (Fabi Play is a movement platform); add 'remote' to be offered without a phone. */
377
+ inputs?: GameInput[];
378
+ /**
379
+ * Optional, only with 'remote' in inputs: true when the game follows the turns contract, so the screen may let
380
+ * several children play with the remote, a round each (init.turns): play a round with the child in players; when
381
+ * it ends (a crash too) and turns remain, ask break('turn') (the screen answers break-start, turn, break-done);
382
+ * after the last round, end with every child's scores. Absent or false: remote mode is one child only.
383
+ */
384
+ turns?: boolean;
385
+ /**
386
+ * Optional: 'auto' (the default) lets the screen frame the children: when they stand far from the camera it zooms
387
+ * in on them (up to 1.8×), so every child's avatar is about the same size and reaches the same things, near or
388
+ * far; the view changes only at calibration and in breaks. 'fixed' keeps the whole camera image, as before (for
389
+ * a game drawn on exact spots of the live video).
390
+ */
391
+ framing?: GameFraming;
333
392
  }
393
+ /** How the screen frames the camera image for a game: see GameManifest.framing. */
394
+ export type GameFraming = "auto" | "fixed";
395
+ /**
396
+ * How a game can be played: with the body (camera on the phone), or with the TV remote (keys become gestures).
397
+ *
398
+ * 'remote' is for TV apps without a phone (Samsung, LG, Android TV, Fire TV): one remote is one child, and with
399
+ * several children the screen runs turns, only for games whose manifest says "turns": true (see GameManifest.turns,
400
+ * BreakReason turn, HostMessage turn). The screen turns keys into the gestures the body makes (↑ or OK jump;
401
+ * ↓ held squat, stand on release; ← → lanes; OK twice quickly clap; Back is the platform's pause, never the game's)
402
+ * and animates a puppet body in the frames, so with one child most games need no code. On LG the Magic
403
+ * Remote also points: it reaches the game as ordinary pointer and mouse events (init.pointer says when). A game that
404
+ * lists 'remote' must play with those keys alone and, where it has things to pick (bubbles, answers, buttons), let a
405
+ * pointer click them too (pointer events, large hit areas, a hover highlight). Games aimed with the hands should not
406
+ * list it unless they offer a selection the keys can drive.
407
+ */
408
+ export type GameInput = "body" | "remote";
334
409
  /** The backdrops a game may declare in its manifest (its own background is always supported). */
335
410
  export type GameBackdrop = Exclude<Backdrop, "none">;
336
411
  /** The moves an intro step can show; the platform draws them with each child's avatar. Same names the SDK detects. */
@@ -394,12 +469,12 @@ declare namespace FabiPlay {
394
469
  export type DrawAvatar = (ctx: CanvasRenderingContext2D, who: PlayerFrame | Body, player?: number | string, box?: AvatarBox | null, options?: DrawAvatarOptions) => Body | null;
395
470
  /** A frame's body smoothed (and interpolated) for its player, or a player id's last one; null if none. */
396
471
  export type SmoothedBody = (who: PlayerFrame | number, options?: Pick<DrawAvatarOptions, "smooth" | "interpolate">) => Body | null;
397
- /** What the 'start' event says: who plays and how. */
472
+ /** What the 'start' event says: who plays and how, as it was at the start (late listeners get the same; see Easy for now). */
398
473
  export interface StartInfo {
399
474
  /** The players' age range: simpler rules for 3-5, scores and challenges for 9-12. */
400
475
  ageRange: AgeRange;
401
- /** The ids of the players (1, 2, …). */
402
- players: number[];
476
+ /** The ids of the players (1, 2, …) at the start, frozen; with input 'remote', the first child's turn (see the turn event). */
477
+ players: readonly number[];
403
478
  /** The language to show text in. */
404
479
  locale: Locale;
405
480
  /** 'tv': on a TV, children standing back; 'solo': a tablet in front of one child. */
@@ -410,6 +485,12 @@ declare namespace FabiPlay {
410
485
  view: BackdropView | null;
411
486
  /** What the screen knows about the device (a starting tier, the display's rate), or null: see Play.device. */
412
487
  device: DeviceHint | null;
488
+ /** Every child in turn order, frozen (with input 'remote', one round each; see Play.turns); otherwise players. */
489
+ turns: readonly number[];
490
+ /** 'body' (the default) or 'remote': played with the TV remote, keys turned into gestures (see Play.input). */
491
+ input: GameInput;
492
+ /** With input 'remote', whether the remote also points (LG's Magic Remote; see Play.pointer). */
493
+ pointer: boolean;
413
494
  }
414
495
  /**
415
496
  * The events of FabiPlay.on, by name, with what each listener gets. Every gesture (jump, land, squat, stand, clap,
@@ -461,6 +542,13 @@ declare namespace FabiPlay {
461
542
  backdrop: Backdrop;
462
543
  view: BackdropView | null;
463
544
  };
545
+ /**
546
+ * Remote mode only, inside the break of break('turn'): the next child's turn; players (one id) is who plays from
547
+ * now on. Keep each player's score by id.
548
+ */
549
+ turn: {
550
+ players: number[];
551
+ };
464
552
  };
465
553
  /** An event name of FabiPlay.on. */
466
554
  export type EasyEvent = keyof EasyEvents;
@@ -477,7 +565,7 @@ declare namespace FabiPlay {
477
565
  export interface Easy {
478
566
  /**
479
567
  * Listens to an event: a gesture by its name ('jump', 'clap', 'lane-left', …), 'start', 'hands', 'shape',
480
- * 'players', 'pause', 'resume', 'break-start', 'break-end', 'backdrop' (see EasyEvents). Returns a function that
568
+ * 'players', 'pause', 'resume', 'break-start', 'break-end', 'backdrop', 'turn' (see EasyEvents). Returns a function that
481
569
  * stops listening.
482
570
  *
483
571
  * @example
@@ -486,11 +574,14 @@ declare namespace FabiPlay {
486
574
  on<K extends EasyEvent>(event: K, listener: (data: EasyEvents[K]) => void): () => void;
487
575
  /** Say the game is loaded; done for you on page load unless the script tag says data-ready="manual". */
488
576
  ready(): void;
489
- /** The score so far, for the screen to show; player 1 unless said. */
577
+ /** The score so far, for the screen to show; player 1 unless said (with input 'remote', the child playing now). */
490
578
  score(score: number, player?: number): void;
491
579
  /** The game stopped by itself ('level' or 'gameover', never its pause menu): Fabi Play may show an ad. Resolves when it can go on. */
492
580
  break(reason?: BreakReason): Promise<boolean>;
493
- /** The end: a score, or one per player. Stars from 0 to 3, 2 if not given. */
581
+ /**
582
+ * The end: a score (player 1's; with input 'remote', the child playing now's, and for each child who played
583
+ * before them in turns their last score() or 0), or one per player. Stars from 0 to 3, 2 if not given.
584
+ */
494
585
  end(score: number | {
495
586
  player: number;
496
587
  score: number;
@@ -509,6 +600,14 @@ declare namespace FabiPlay {
509
600
  * clicks by staying still on a spot. The script tag's data-pointer does the same.
510
601
  */
511
602
  pointer(options?: Omit<PointerOptions, "target">): void;
603
+ /** The players playing now (frozen; see Play.players); empty before start. */
604
+ readonly players: readonly number[];
605
+ /** Every child in turn order (frozen; see Play.turns); empty before start. */
606
+ readonly turns: readonly number[];
607
+ /** How this session is played (see Play.input); 'body' before start. */
608
+ readonly input: GameInput;
609
+ /** With input 'remote', whether the remote also points (Play.pointer; pointer() is the hand adapter); false before start. */
610
+ readonly hasPointer: boolean;
512
611
  /** Where the camera image is, with any backdrop; null if the host didn't say, and before start. */
513
612
  readonly view: BackdropView | null;
514
613
  /** What the screen knows about the device (see Play.device); null if it said nothing, and before start. */
@@ -568,6 +667,14 @@ declare namespace FabiPlay {
568
667
  backdrop: Extract<HostMessage, {
569
668
  type: "backdrop";
570
669
  }>;
670
+ /**
671
+ * Remote mode only (input 'remote'), inside the break of a break('turn'): the next child's turn, the same game goes
672
+ * on with this one player (see Play.players). Keep each player's score by id. The SDK passes on only a turn to the
673
+ * next child in Play.turns, while a 'turn' break is under way; it drops any other.
674
+ */
675
+ turn: Extract<HostMessage, {
676
+ type: "turn";
677
+ }>;
571
678
  };
572
679
  /**
573
680
  * The game's side of Fabi Play, from connect(): who plays, their movements, and how the game tells it is ready,
@@ -583,8 +690,39 @@ declare namespace FabiPlay {
583
690
  readonly locale: Locale;
584
691
  /** 'tv': on a TV or Chromecast, children standing back; 'solo': a tablet in front of one child. */
585
692
  readonly mode: "tv" | "solo";
586
- /** The ids of the players in this game (1, 2, …), as frames and gestures name them. */
587
- readonly players: number[];
693
+ /**
694
+ * The ids of the players playing now (1, 2, …), as frames and gestures name them; a frozen array, read it again
695
+ * after a turn. With input 'body' the same for the whole game. With input 'remote' exactly one child, and it
696
+ * changes only inside the break of a break('turn') (the 'turn' event, between break-start and break-done). Keep
697
+ * each player's score by id across turns, and end with the scores of everyone who played.
698
+ */
699
+ readonly players: readonly number[];
700
+ /**
701
+ * Every child who plays this game, in turn order (frozen): with input 'remote' and several children, one round
702
+ * each, players being the one playing now (turns[0] at the start). Only for games whose manifest says
703
+ * "turns": true (with "remote" in inputs); otherwise remote mode is one child. The contract:
704
+ * 1. play a round with the child in players;
705
+ * 2. at the end of it (whatever happened: a crash ends a round too), if turns remain, call break('turn'): the
706
+ * screen always answers break-start (its turn screen), 'turn' (the next child), then break-done;
707
+ * 3. after the last child's round, call end() with every child's scores.
708
+ * 'level' and 'gameover' breaks never swap players. With 'body', one child, or hosts that don't say: players.
709
+ */
710
+ readonly turns: readonly number[];
711
+ /**
712
+ * How this session is played: 'body' (the camera on the phone; the default, and what older hosts mean) or
713
+ * 'remote' (a TV without a phone, only for games whose manifest lists it in inputs): the screen turns the remote's
714
+ * keys into the same gestures (↑ or OK jump, ↓ held squat then stand, ← → lanes, OK twice clap) and moves a puppet
715
+ * body in the frames, one child at a time. Adapt if it helps: slow down, show a selection the arrows move instead
716
+ * of a hand cursor.
717
+ */
718
+ readonly input: GameInput;
719
+ /**
720
+ * Only with input 'remote': the remote also points (LG's Magic Remote), reaching the game as ordinary pointer and
721
+ * mouse events (hover, click with OK). Then let what the game has to pick be clicked too (large hit areas, a hover
722
+ * highlight). false otherwise, and from hosts that don't say. Not the pointer() adapter, which makes a child's hand
723
+ * the mouse.
724
+ */
725
+ readonly pointer: boolean;
588
726
  /** The screen's sound is off: start the game muted. */
589
727
  readonly muted: boolean;
590
728
  /** The players' age range: simpler rules for 3-5, scores and challenges for 9-12. */
@@ -646,8 +784,8 @@ declare namespace FabiPlay {
646
784
  /** Called when backdrop or view changes; returns a function that stops listening. */
647
785
  onBackdrop(listener: (backdrop: Backdrop, view: BackdropView | null) => void): () => void;
648
786
  /**
649
- * Listens to the host: 'frame', 'gesture', 'pause', 'resume', 'break-start', 'break-done', 'backdrop' (see
650
- * Events). Returns a function that stops listening.
787
+ * Listens to the host: 'frame', 'gesture', 'pause', 'resume', 'break-start', 'break-done', 'backdrop', 'turn'
788
+ * (see Events). Returns a function that stops listening.
651
789
  *
652
790
  * @example
653
791
  * const off = play.on('frame', (f) => (players = f.players))
@@ -658,9 +796,10 @@ declare namespace FabiPlay {
658
796
  /** A player's score so far, for the screen to show. */
659
797
  score(player: number, score: number): void;
660
798
  /**
661
- * The game has stopped by itself: a level is over or the game is over (never its pause menu). Fabi Play may show
662
- * an ad there, if its rules allow (break-start, then break-done); resolves when the game can go on, true if an
663
- * ad was shown. Never call it in the middle of play.
799
+ * The game has stopped by itself: a level is over, the game is over, or (with the remote, see turns) a child's
800
+ * round is over and the next child's turn is due: 'turn' (never at its pause menu). Fabi Play may show an ad
801
+ * there, if its rules allow (break-start, then break-done; with 'turn', the turn message in between); resolves
802
+ * when the game can go on, true if an ad was shown. Never call it in the middle of play.
664
803
  */
665
804
  break(reason?: BreakReason): Promise<boolean>;
666
805
  /**