@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 +114 -0
- package/dist/avatars.js +1 -1
- package/dist/chunks/{chunk-QJPCNRFI.js → chunk-GFNOIOSX.js} +17 -3
- package/dist/fabi-play.d.ts +196 -23
- package/dist/fabi-play.js +1 -1
- package/dist/fabi-play.schema.json +98 -3
- package/dist/fabi-play.umd.js +92 -14
- package/dist/fabi-play.umd.min.js +1 -1
- package/dist/index.d.ts +200 -23
- package/dist/index.js +79 -11
- package/dist/integrity.json +6 -6
- package/package.json +3 -3
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
|
@@ -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
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
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,
|
package/dist/fabi-play.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
*
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
553
|
-
|
|
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'
|
|
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
|
|
628
|
-
*
|
|
629
|
-
*
|
|
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
|
/**
|