@fabi-play/sdk 0.1.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
@@ -45,6 +45,120 @@ From the CDN, pinned to an exact version with its Subresource Integrity hash (li
45
45
  | `fabi-play.d.ts` | types for script-tag games (`FabiPlay.PlayerFrame`, …) |
46
46
  | `fabi-play.schema.json` | the JSON Schema of `fabi-play.json` |
47
47
 
48
+ ## Fabi explains your game (intro)
49
+
50
+ Before calibration, Fabi (the mascot) can explain your game on the TV in a few steps: a speech bubble over an image
51
+ of the game, or a move shown with the children's own avatars, which they can also try. You write only data, in
52
+ `fabi-play.json`:
53
+
54
+ ```json
55
+ "intro": { "steps": [
56
+ { "say": { "it": "Si corre nel bosco! Ognuno ha la sua pista e raccoglie le stelle.", "en": "Let's run in the woods! Everyone has a track and collects stars." }, "image": "intro/track.jpg" },
57
+ { "say": { "it": "Tronco? Salta! Provate adesso.", "en": "A log? Jump! Try it now." }, "move": "jump", "practice": true },
58
+ { "say": { "it": "Ramo basso? Giù!", "en": "Low branch? Down!" }, "move": "squat" }
59
+ ] }
60
+ ```
61
+
62
+ - **1 to 4 steps.** Fabi Play adds its own last step ("Ready?") and goes on to calibration.
63
+ - **`say`**: one sentence, in Italian and English, up to 90 characters each (an emoji counts as one), with no spaces
64
+ at either end and no control characters. Or one per age range, when the little
65
+ ones need simpler words: `{ "3-5": { "it": "…", "en": "…" }, "9-12": { "it": "…", "en": "…" } }` (at least one
66
+ range; a child of a range you left out gets the closest one you wrote, the younger if two are as close).
67
+ - **Each step has an `image` or a `move`, not both.**
68
+ - `image`: a picture of the game, 16:9, at least 1280×720, `.jpg` or `.png` (no WebP: old TV browsers may not
69
+ decode it; use JPG, quality about 80, for screenshots and PNG only for flat art), a path
70
+ relative to the game's address like `entry`: no scheme (`https://`), no leading `/`, no `.` or `..` segments,
71
+ letters, digits, `_` and `-`, extension in lowercase (`intro/track.jpg`, not `intro/Track.JPG`).
72
+ - `move`: one of `jump`, `squat`, `clap`, `lane-left`, `lane-right`, `arms-up`, `arms-wide`, `arms-v`, `star`,
73
+ drawn with each child's avatar.
74
+ - `practice` (optional, only with a `move`): `true` and the children try it, each gets a tick, and the step moves
75
+ on when all did, or after 8 seconds. Default `false`: Fabi only shows it.
76
+ - **The voice is Fabi Play's.** Fabi speaks each sentence with a clip Fabi Play generates from the text when the
77
+ game is published. Changing a text needs a new clip: until it is made, the TV shows that sentence in the bubble
78
+ only.
79
+ - **No `intro`**: the game goes straight to calibration, as before.
80
+ - **An invalid intro** is reported by the check (the Studio, `parseManifest`, the JSON Schema) and skipped by the
81
+ TV: it never blocks the game.
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
+
48
162
  Docs: https://docs.fabiplay.app.
49
163
 
50
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-QJPCNRFI.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";
@@ -290,7 +333,11 @@ declare namespace FabiPlay {
290
333
  * "title": { "it": "Acchiappastelle", "en": "Star catcher" },
291
334
  * "description": { "it": "Prendi le stelle con le mani.", "en": "Catch the stars with your hands." },
292
335
  * "ageRanges": ["3-5", "6-8"], "movements": ["hands"], "players": { "min": 1, "max": 2 },
293
- * "minutes": 3, "entry": "index.html", "sdk": 1
336
+ * "minutes": 3, "entry": "index.html", "sdk": 1,
337
+ * "intro": { "steps": [
338
+ * { "say": { "it": "Prendi le stelle!", "en": "Catch the stars!" }, "image": "intro/stars.jpg" },
339
+ * { "say": { "it": "Alza le braccia!", "en": "Arms up!" }, "move": "arms-up", "practice": true }
340
+ * ] }
294
341
  * }
295
342
  */
296
343
  export interface GameManifest {
@@ -315,15 +362,77 @@ declare namespace FabiPlay {
315
362
  };
316
363
  /** Minutes of a typical game, 1 to 20. */
317
364
  minutes: number;
318
- /** Page that starts the game, an .html file relative to the game's address (same site, no ".."). */
365
+ /**
366
+ * Page that starts the game: a path relative to the game's address (no scheme, no leading /, no "." or ".."
367
+ * segments, up to 200 characters), of letters, digits, _ and -, ending in lowercase .html: index.html, play/index.html.
368
+ */
319
369
  entry: string;
320
370
  /** Protocol version of the SDK it was built with: 1 (PROTOCOL_VERSION). */
321
371
  sdk: number;
322
372
  /** Optional: the backdrops it can draw over besides its own; ['video'] opts in to mirror mode. */
323
373
  backdrop?: GameBackdrop[];
374
+ /** Optional: Fabi explains the game on the TV before calibration (see GameIntro). Absent: straight to calibration. */
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;
324
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";
325
409
  /** The backdrops a game may declare in its manifest (its own background is always supported). */
326
410
  export type GameBackdrop = Exclude<Backdrop, "none">;
411
+ /** The moves an intro step can show; the platform draws them with each child's avatar. Same names the SDK detects. */
412
+ export type IntroMove = "jump" | "squat" | "clap" | "lane-left" | "lane-right" | "arms-up" | "arms-wide" | "arms-v" | "star";
413
+ /** One sentence in every language, or one per age range (each with every language). */
414
+ export type IntroText = Record<Locale, string> | Partial<Record<AgeRange, Record<Locale, string>>>;
415
+ /** One step of the intro: Fabi says one sentence, over an image of the game or a move shown with the avatars. */
416
+ export interface IntroStep {
417
+ say: IntroText;
418
+ /**
419
+ * An image of the game, 16:9: a path relative to the game's address with the same rules as entry (no scheme, no
420
+ * leading /, no "." or ".." segments), ending in lowercase .jpg or .png (no WebP: old TV browsers may not decode it): intro/track.jpg.
421
+ */
422
+ image?: string;
423
+ /** Or a move, shown with the avatars. Exactly one of image and move. */
424
+ move?: IntroMove;
425
+ /** Only with move: the children try it and each gets a tick; the step moves on when all did, or after 8 s. Default false: Fabi only shows it. */
426
+ practice?: boolean;
427
+ }
428
+ /**
429
+ * Fabi explains the game on the TV, before calibration, in 1 to 4 steps (the platform adds its own final "ready"
430
+ * step). Only data: Fabi Play draws it, and speaks each sentence with a voice clip generated at publication from the
431
+ * text (voiceKey); a text without its clip yet is shown in the bubble only.
432
+ */
433
+ export interface GameIntro {
434
+ steps: IntroStep[];
435
+ }
327
436
  /** Where player space (0–1) lands on the canvas. */
328
437
  export interface Box {
329
438
  /** Left edge, canvas units. */
@@ -360,12 +469,12 @@ declare namespace FabiPlay {
360
469
  export type DrawAvatar = (ctx: CanvasRenderingContext2D, who: PlayerFrame | Body, player?: number | string, box?: AvatarBox | null, options?: DrawAvatarOptions) => Body | null;
361
470
  /** A frame's body smoothed (and interpolated) for its player, or a player id's last one; null if none. */
362
471
  export type SmoothedBody = (who: PlayerFrame | number, options?: Pick<DrawAvatarOptions, "smooth" | "interpolate">) => Body | null;
363
- /** 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). */
364
473
  export interface StartInfo {
365
474
  /** The players' age range: simpler rules for 3-5, scores and challenges for 9-12. */
366
475
  ageRange: AgeRange;
367
- /** The ids of the players (1, 2, …). */
368
- 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[];
369
478
  /** The language to show text in. */
370
479
  locale: Locale;
371
480
  /** 'tv': on a TV, children standing back; 'solo': a tablet in front of one child. */
@@ -376,6 +485,12 @@ declare namespace FabiPlay {
376
485
  view: BackdropView | null;
377
486
  /** What the screen knows about the device (a starting tier, the display's rate), or null: see Play.device. */
378
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;
379
494
  }
380
495
  /**
381
496
  * The events of FabiPlay.on, by name, with what each listener gets. Every gesture (jump, land, squat, stand, clap,
@@ -427,6 +542,13 @@ declare namespace FabiPlay {
427
542
  backdrop: Backdrop;
428
543
  view: BackdropView | null;
429
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
+ };
430
552
  };
431
553
  /** An event name of FabiPlay.on. */
432
554
  export type EasyEvent = keyof EasyEvents;
@@ -443,7 +565,7 @@ declare namespace FabiPlay {
443
565
  export interface Easy {
444
566
  /**
445
567
  * Listens to an event: a gesture by its name ('jump', 'clap', 'lane-left', …), 'start', 'hands', 'shape',
446
- * '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
447
569
  * stops listening.
448
570
  *
449
571
  * @example
@@ -452,11 +574,14 @@ declare namespace FabiPlay {
452
574
  on<K extends EasyEvent>(event: K, listener: (data: EasyEvents[K]) => void): () => void;
453
575
  /** Say the game is loaded; done for you on page load unless the script tag says data-ready="manual". */
454
576
  ready(): void;
455
- /** 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). */
456
578
  score(score: number, player?: number): void;
457
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. */
458
580
  break(reason?: BreakReason): Promise<boolean>;
459
- /** 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
+ */
460
585
  end(score: number | {
461
586
  player: number;
462
587
  score: number;
@@ -475,6 +600,14 @@ declare namespace FabiPlay {
475
600
  * clicks by staying still on a spot. The script tag's data-pointer does the same.
476
601
  */
477
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;
478
611
  /** Where the camera image is, with any backdrop; null if the host didn't say, and before start. */
479
612
  readonly view: BackdropView | null;
480
613
  /** What the screen knows about the device (see Play.device); null if it said nothing, and before start. */
@@ -534,6 +667,14 @@ declare namespace FabiPlay {
534
667
  backdrop: Extract<HostMessage, {
535
668
  type: "backdrop";
536
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
+ }>;
537
678
  };
538
679
  /**
539
680
  * The game's side of Fabi Play, from connect(): who plays, their movements, and how the game tells it is ready,
@@ -549,8 +690,39 @@ declare namespace FabiPlay {
549
690
  readonly locale: Locale;
550
691
  /** 'tv': on a TV or Chromecast, children standing back; 'solo': a tablet in front of one child. */
551
692
  readonly mode: "tv" | "solo";
552
- /** The ids of the players in this game (1, 2, …), as frames and gestures name them. */
553
- 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;
554
726
  /** The screen's sound is off: start the game muted. */
555
727
  readonly muted: boolean;
556
728
  /** The players' age range: simpler rules for 3-5, scores and challenges for 9-12. */
@@ -612,8 +784,8 @@ declare namespace FabiPlay {
612
784
  /** Called when backdrop or view changes; returns a function that stops listening. */
613
785
  onBackdrop(listener: (backdrop: Backdrop, view: BackdropView | null) => void): () => void;
614
786
  /**
615
- * Listens to the host: 'frame', 'gesture', 'pause', 'resume', 'break-start', 'break-done', 'backdrop' (see
616
- * 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.
617
789
  *
618
790
  * @example
619
791
  * const off = play.on('frame', (f) => (players = f.players))
@@ -624,9 +796,10 @@ declare namespace FabiPlay {
624
796
  /** A player's score so far, for the screen to show. */
625
797
  score(player: number, score: number): void;
626
798
  /**
627
- * The game has stopped by itself: a level is over or the game is over (never its pause menu). Fabi Play may show
628
- * an ad there, if its rules allow (break-start, then break-done); resolves when the game can go on, true if an
629
- * 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.
630
803
  */
631
804
  break(reason?: BreakReason): Promise<boolean>;
632
805
  /**