@fabi-play/protocol 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 +18 -0
- package/dist/index.d.ts +87 -6
- package/dist/index.js +28 -1
- package/fabi-play.schema.json +21 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,3 +74,21 @@ from this package.
|
|
|
74
74
|
## License
|
|
75
75
|
|
|
76
76
|
Fabi Play Kit License: free to use, for making games and tools for Fabi Play only. See [LICENSE](./LICENSE).
|
|
77
|
+
|
|
78
|
+
## The TV remote
|
|
79
|
+
|
|
80
|
+
A manifest's optional `inputs` (`GameInput[]`, from `GAME_INPUTS`: `body`, `remote`; absent `["body"]`, see
|
|
81
|
+
`gameInputs`) says whether the game can be played with the TV remote when there is no phone; `parseManifest`
|
|
82
|
+
requires `body` in it, no repeats and no unknown values. The screen tells the game how a session is played in
|
|
83
|
+
`init.input` (absent `'body'`) and, with `'remote'`, whether the remote points (`init.pointer`, LG's Magic Remote:
|
|
84
|
+
plain pointer and mouse events in the game's page). The screen turns keys into gestures (↑ or OK `jump`, ↓ held
|
|
85
|
+
`squat`/`stand`, ← → lanes, OK twice `clap`, Back the platform's pause) and plays one child at a time: init's
|
|
86
|
+
`players` is the child playing now (one id). Several children take turns only with games whose manifest says `"turns": true`
|
|
87
|
+
(valid only with `remote` in `inputs`; it means the game follows the turn contract; without it, one child only): `init.turns`
|
|
88
|
+
lists every child in turn order (`parseTurnOrder`; absent: `players`). The game plays a round per child; when a
|
|
89
|
+
child's round ends (a crash too) and turns remain, it asks `break('turn')`, and the screen always answers
|
|
90
|
+
`break-start` (its turn screen), `{ type: 'turn', players: [2] }` (exactly one id, the next in the order;
|
|
91
|
+
`parseTurnPlayers` checks its shape) and `break-done`, the next child's puppet recentred in the middle lane. After
|
|
92
|
+
the last round the game ends with every child's scores. `level` and `gameover` breaks never swap players; hosts
|
|
93
|
+
before turns answer `turn` as an unknown reason, at once. Games keep scores by player id. See "Playing with the TV remote" in the
|
|
94
|
+
[`@fabi-play/sdk` README](https://www.npmjs.com/package/@fabi-play/sdk).
|
package/dist/index.d.ts
CHANGED
|
@@ -307,10 +307,15 @@ export declare const wire: <T extends object>(m: T) => Wire<T>;
|
|
|
307
307
|
/**
|
|
308
308
|
* Why the game stops, when it asks for a break:
|
|
309
309
|
* - level: a level is over (the best moment);
|
|
310
|
-
* - gameover: the game is over and a new one may start
|
|
311
|
-
*
|
|
312
|
-
|
|
313
|
-
|
|
310
|
+
* - gameover: the game is over and a new one may start;
|
|
311
|
+
* - turn: with input 'remote' and turns remaining, the child playing now finished their round (whatever happened in
|
|
312
|
+
* it: a crash or a fall ends a round too). The screen always answers with break-start (its turn screen is the
|
|
313
|
+
* break, and may host an ad), then turn (the next child), then break-done. Hosts before turns answer it as an
|
|
314
|
+
* unknown reason (break-done at once), but they never send turns either.
|
|
315
|
+
* Only 'turn' ever swaps players; 'level' and 'gameover' never do. Never at the game's own pause menu: a child who
|
|
316
|
+
* stops for a moment must not get an ad for it.
|
|
317
|
+
*/
|
|
318
|
+
export type BreakReason = "level" | "gameover" | "turn";
|
|
314
319
|
export declare const BREAK_REASONS: BreakReason[];
|
|
315
320
|
/**
|
|
316
321
|
* The movements the host recognises, per player:
|
|
@@ -366,7 +371,9 @@ export type Backdrop = "video" | "none";
|
|
|
366
371
|
* image is cropped to fill the screen), with any backdrop, so avatars and hands are where the children stand (a
|
|
367
372
|
* "virtual mirror"). The screen decides it: with 'video', where the video is drawn (cover or contain); with 'none',
|
|
368
373
|
* the whole image contained at 92% of the viewport's height, bottom-aligned. Games don't compute it: they just use
|
|
369
|
-
* it.
|
|
374
|
+
* it. It is not fixed: with automatic framing (the default, see GameManifest.framing) the screen zooms in on children
|
|
375
|
+
* standing far away, up to 1.8×, so the image may reach past every edge; it changes only at calibration and in breaks
|
|
376
|
+
* (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
|
|
370
377
|
* screen at (x + p.x·w, y + p.y·h): see toScreen.
|
|
371
378
|
*/
|
|
372
379
|
export interface BackdropView {
|
|
@@ -418,6 +425,22 @@ export type HostMessage =
|
|
|
418
425
|
/** view: where the camera image is, with any backdrop (see BackdropView); absent if the screen doesn't know. */
|
|
419
426
|
/** stats: a debug host (the simulator) asks the game to say how often it draws (stats messages); absent on the TV. */
|
|
420
427
|
/** device: what the screen knows about the device (a starting tier, the display's rate); absent if it knows nothing. */
|
|
428
|
+
/**
|
|
429
|
+
* input: how this session is played (see GameInput); absent = 'body'. Only games whose manifest lists 'remote' ever
|
|
430
|
+
* get 'remote'. With 'remote', players is exactly one id: the child playing now (see turn).
|
|
431
|
+
*/
|
|
432
|
+
/**
|
|
433
|
+
* turns: only with input 'remote', and only to games whose manifest says "turns": true: every child who
|
|
434
|
+
* will play this game, one id each, in turn order, players[0] first (see parseTurnOrder); absent = players (one
|
|
435
|
+
* child, no turns). A round per child: at the end of each child's round, while turns remain, the game calls
|
|
436
|
+
* break('turn'); the screen answers break-start, turn (the next child), break-done. After the last child's round
|
|
437
|
+
* the game calls end() with every child's scores. An order that is not valid, or doesn't start with players[0],
|
|
438
|
+
* makes the session one child only.
|
|
439
|
+
*/
|
|
440
|
+
/**
|
|
441
|
+
* pointer: only with input 'remote', true when the TV's remote also points (LG's Magic Remote): it reaches the game
|
|
442
|
+
* as ordinary pointer and mouse events (hover, click with OK). Absent = false.
|
|
443
|
+
*/
|
|
421
444
|
{
|
|
422
445
|
type: "init";
|
|
423
446
|
v: number;
|
|
@@ -430,8 +453,11 @@ export type HostMessage =
|
|
|
430
453
|
view?: BackdropView;
|
|
431
454
|
stats?: boolean;
|
|
432
455
|
device?: DeviceHint;
|
|
456
|
+
input?: GameInput;
|
|
457
|
+
turns?: number[];
|
|
458
|
+
pointer?: boolean;
|
|
433
459
|
}
|
|
434
|
-
/** The backdrop or the view changed mid-game (video switched on or off, the camera image's fit changed). */
|
|
460
|
+
/** The backdrop or the view changed mid-game (video switched on or off, the camera image's fit or framing changed). */
|
|
435
461
|
| {
|
|
436
462
|
type: "backdrop";
|
|
437
463
|
backdrop: Backdrop;
|
|
@@ -470,7 +496,24 @@ export type HostMessage =
|
|
|
470
496
|
| {
|
|
471
497
|
type: "break-done";
|
|
472
498
|
shown: boolean;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Turns (input 'remote' only): from now on this child plays, in place of the one before. One remote is one child,
|
|
502
|
+
* so with several children the screen plays one at a time: the same game goes on with the next child in init's
|
|
503
|
+
* turns (players [2] after [1], …), exactly one id. Sent only inside the break of a break('turn') the game asked
|
|
504
|
+
* for (after break-start, before break-done); never at other breaks, so a round never changes hands midway. The
|
|
505
|
+
* new child's puppet starts recentred, in the middle lane (lane 0), with no lane gesture. Ids keep their meaning
|
|
506
|
+
* (slots, avatars): keep each player's score by id across turns, and end with the scores of every child who
|
|
507
|
+
* played. Never sent with input 'body', where init's players stay for the whole game.
|
|
508
|
+
*/
|
|
509
|
+
| {
|
|
510
|
+
type: "turn";
|
|
511
|
+
players: number[];
|
|
473
512
|
};
|
|
513
|
+
/** The players of a turn message, or undefined if they are not valid: exactly one integer id from 1 to MAX_PLAYERS. A copy. */
|
|
514
|
+
export declare function parseTurnPlayers(v: unknown): number[] | undefined;
|
|
515
|
+
/** init's turns, or undefined if they are not valid: 1 to MAX_PLAYERS distinct integer ids from 1 to MAX_PLAYERS, in order. A copy. */
|
|
516
|
+
export declare function parseTurnOrder(v: unknown): number[] | undefined;
|
|
474
517
|
/** Why a game is paused (see HostMessage pause). */
|
|
475
518
|
export type PauseReason = "user" | "distance";
|
|
476
519
|
export declare const PAUSE_REASONS: PauseReason[];
|
|
@@ -566,7 +609,45 @@ export interface GameManifest {
|
|
|
566
609
|
backdrop?: GameBackdrop[];
|
|
567
610
|
/** Optional: Fabi explains the game on the TV before calibration (see GameIntro). Absent: straight to calibration. */
|
|
568
611
|
intro?: GameIntro;
|
|
612
|
+
/** 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. */
|
|
613
|
+
inputs?: GameInput[];
|
|
614
|
+
/**
|
|
615
|
+
* Optional, only with 'remote' in inputs: true when the game follows the turns contract, so the screen may let
|
|
616
|
+
* several children play with the remote, a round each (init.turns): play a round with the child in players; when
|
|
617
|
+
* it ends (a crash too) and turns remain, ask break('turn') (the screen answers break-start, turn, break-done);
|
|
618
|
+
* after the last round, end with every child's scores. Absent or false: remote mode is one child only.
|
|
619
|
+
*/
|
|
620
|
+
turns?: boolean;
|
|
621
|
+
/**
|
|
622
|
+
* Optional: 'auto' (the default) lets the screen frame the children: when they stand far from the camera it zooms
|
|
623
|
+
* in on them (up to 1.8×), so every child's avatar is about the same size and reaches the same things, near or
|
|
624
|
+
* far; the view changes only at calibration and in breaks. 'fixed' keeps the whole camera image, as before (for
|
|
625
|
+
* a game drawn on exact spots of the live video).
|
|
626
|
+
*/
|
|
627
|
+
framing?: GameFraming;
|
|
569
628
|
}
|
|
629
|
+
/** How the screen frames the camera image for a game: see GameManifest.framing. */
|
|
630
|
+
export type GameFraming = "auto" | "fixed";
|
|
631
|
+
export declare const GAME_FRAMINGS: GameFraming[];
|
|
632
|
+
/** The framing of a manifest, with the default. */
|
|
633
|
+
export declare function gameFraming(m: Pick<GameManifest, "framing">): GameFraming;
|
|
634
|
+
/**
|
|
635
|
+
* How a game can be played: with the body (camera on the phone), or with the TV remote (keys become gestures).
|
|
636
|
+
*
|
|
637
|
+
* 'remote' is for TV apps without a phone (Samsung, LG, Android TV, Fire TV): one remote is one child, and with
|
|
638
|
+
* several children the screen runs turns, only for games whose manifest says "turns": true (see GameManifest.turns,
|
|
639
|
+
* BreakReason turn, HostMessage turn). The screen turns keys into the gestures the body makes (↑ or OK jump;
|
|
640
|
+
* ↓ held squat, stand on release; ← → lanes; OK twice quickly clap; Back is the platform's pause, never the game's)
|
|
641
|
+
* and animates a puppet body in the frames, so with one child most games need no code. On LG the Magic
|
|
642
|
+
* Remote also points: it reaches the game as ordinary pointer and mouse events (init.pointer says when). A game that
|
|
643
|
+
* lists 'remote' must play with those keys alone and, where it has things to pick (bubbles, answers, buttons), let a
|
|
644
|
+
* pointer click them too (pointer events, large hit areas, a hover highlight). Games aimed with the hands should not
|
|
645
|
+
* list it unless they offer a selection the keys can drive.
|
|
646
|
+
*/
|
|
647
|
+
export type GameInput = "body" | "remote";
|
|
648
|
+
export declare const GAME_INPUTS: GameInput[];
|
|
649
|
+
/** The inputs of a manifest, with the default. */
|
|
650
|
+
export declare function gameInputs(m: Pick<GameManifest, "inputs">): GameInput[];
|
|
570
651
|
/** The backdrops a game may declare in its manifest (its own background is always supported). */
|
|
571
652
|
export type GameBackdrop = Exclude<Backdrop, "none">;
|
|
572
653
|
export declare const GAME_BACKDROPS: GameBackdrop[];
|
package/dist/index.js
CHANGED
|
@@ -204,7 +204,7 @@ var TOO_CLOSE_KEY = "fabi-play.too-close-pause";
|
|
|
204
204
|
var PHASES = ["setup", "intro", "calibrate", "play", "pause", "break", "end"];
|
|
205
205
|
var NS = "fabi-play";
|
|
206
206
|
var wire = (m) => ({ ns: NS, ...m });
|
|
207
|
-
var BREAK_REASONS = ["level", "gameover"];
|
|
207
|
+
var BREAK_REASONS = ["level", "gameover", "turn"];
|
|
208
208
|
function parseView(v) {
|
|
209
209
|
if (!isObject(v)) return void 0;
|
|
210
210
|
const { x, y, w, h } = v;
|
|
@@ -227,7 +227,23 @@ function parseGameStats(v) {
|
|
|
227
227
|
if (DEVICE_TIERS.includes(v.tier)) out.tier = v.tier;
|
|
228
228
|
return out;
|
|
229
229
|
}
|
|
230
|
+
var isPlayerId = (id) => Number.isInteger(id) && id >= 1 && id <= MAX_PLAYERS;
|
|
231
|
+
function parseTurnPlayers(v) {
|
|
232
|
+
return Array.isArray(v) && v.length === 1 && isPlayerId(v[0]) ? [v[0]] : void 0;
|
|
233
|
+
}
|
|
234
|
+
function parseTurnOrder(v) {
|
|
235
|
+
if (!Array.isArray(v) || v.length < 1 || v.length > MAX_PLAYERS || !v.every(isPlayerId) || new Set(v).size !== v.length) return void 0;
|
|
236
|
+
return [...v];
|
|
237
|
+
}
|
|
230
238
|
var PAUSE_REASONS = ["user", "distance"];
|
|
239
|
+
var GAME_FRAMINGS = ["auto", "fixed"];
|
|
240
|
+
function gameFraming(m) {
|
|
241
|
+
return m.framing ?? "auto";
|
|
242
|
+
}
|
|
243
|
+
var GAME_INPUTS = ["body", "remote"];
|
|
244
|
+
function gameInputs(m) {
|
|
245
|
+
return m.inputs?.length ? m.inputs : ["body"];
|
|
246
|
+
}
|
|
231
247
|
var GAME_BACKDROPS = ["video"];
|
|
232
248
|
var MOVEMENTS = ["hands", "jump", "squat", "clap", "freeze", "lanes", "shapes"];
|
|
233
249
|
function parseManifest(data) {
|
|
@@ -248,6 +264,11 @@ function parseManifest(data) {
|
|
|
248
264
|
if (m.sdk !== PROTOCOL_VERSION) errors.push(`sdk: this version of Fabi Play uses protocol ${PROTOCOL_VERSION}`);
|
|
249
265
|
if (m.backdrop !== void 0 && !(Array.isArray(m.backdrop) && m.backdrop.every((b) => GAME_BACKDROPS.includes(b)) && new Set(m.backdrop).size === m.backdrop.length))
|
|
250
266
|
errors.push(`backdrop: optional, from ${GAME_BACKDROPS.join(", ")}, without repeats`);
|
|
267
|
+
if (m.inputs !== void 0 && !(Array.isArray(m.inputs) && m.inputs.includes("body") && m.inputs.every((i) => GAME_INPUTS.includes(i)) && new Set(m.inputs).size === m.inputs.length))
|
|
268
|
+
errors.push(`inputs: optional, from ${GAME_INPUTS.join(", ")}, without repeats, always with body`);
|
|
269
|
+
if (m.turns !== void 0 && !(typeof m.turns === "boolean" && (!m.turns || Array.isArray(m.inputs) && m.inputs.includes("remote"))))
|
|
270
|
+
errors.push('turns: optional, true or false; true only with "remote" in inputs');
|
|
271
|
+
if (m.framing !== void 0 && !GAME_FRAMINGS.includes(m.framing)) errors.push(`framing: optional, ${GAME_FRAMINGS.join(" or ")}`);
|
|
251
272
|
if (m.intro !== void 0) errors.push(...introErrors(m.intro));
|
|
252
273
|
return errors.length ? { errors } : { manifest: m };
|
|
253
274
|
}
|
|
@@ -545,6 +566,8 @@ export {
|
|
|
545
566
|
DEVICE_TIERS,
|
|
546
567
|
FABI_VOICE,
|
|
547
568
|
GAME_BACKDROPS,
|
|
569
|
+
GAME_FRAMINGS,
|
|
570
|
+
GAME_INPUTS,
|
|
548
571
|
INTRO_MAX_CHARS,
|
|
549
572
|
INTRO_MAX_STEPS,
|
|
550
573
|
INTRO_MOVES,
|
|
@@ -571,6 +594,8 @@ export {
|
|
|
571
594
|
decodePose,
|
|
572
595
|
defaultAvatarFor,
|
|
573
596
|
encodePose,
|
|
597
|
+
gameFraming,
|
|
598
|
+
gameInputs,
|
|
574
599
|
gamesFor,
|
|
575
600
|
introText,
|
|
576
601
|
isAgeRange,
|
|
@@ -589,6 +614,8 @@ export {
|
|
|
589
614
|
parseIntro,
|
|
590
615
|
parseManifest,
|
|
591
616
|
parseScreenMessage,
|
|
617
|
+
parseTurnOrder,
|
|
618
|
+
parseTurnPlayers,
|
|
592
619
|
parseView,
|
|
593
620
|
parseZone,
|
|
594
621
|
poseBytes,
|
package/fabi-play.schema.json
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
"type": "object",
|
|
6
6
|
"required": ["id", "version", "title", "description", "ageRanges", "movements", "players", "minutes", "entry", "sdk"],
|
|
7
7
|
"additionalProperties": false,
|
|
8
|
+
"anyOf": [
|
|
9
|
+
{ "not": { "required": ["turns"], "properties": { "turns": { "const": true } } } },
|
|
10
|
+
{ "required": ["inputs"], "properties": { "inputs": { "contains": { "const": "remote" } } } }
|
|
11
|
+
],
|
|
8
12
|
"properties": {
|
|
9
13
|
"$schema": {
|
|
10
14
|
"type": "string",
|
|
@@ -78,6 +82,23 @@
|
|
|
78
82
|
"uniqueItems": true,
|
|
79
83
|
"items": { "enum": ["video"] }
|
|
80
84
|
},
|
|
85
|
+
"inputs": {
|
|
86
|
+
"type": "array",
|
|
87
|
+
"description": "Optional: how the game can be played; absent = [\"body\"]. Must include \"body\" (Fabi Play is a movement platform). Add \"remote\" to be offered on TVs without a phone: the TV remote's keys become gestures (up or OK jump, down held squat, left and right lanes, OK twice clap), one child at a time; on LG the Magic Remote also points, so let what the game has to pick be clicked too.",
|
|
88
|
+
"minItems": 1,
|
|
89
|
+
"uniqueItems": true,
|
|
90
|
+
"contains": { "const": "body" },
|
|
91
|
+
"items": { "enum": ["body", "remote"] },
|
|
92
|
+
"examples": [["body", "remote"]]
|
|
93
|
+
},
|
|
94
|
+
"turns": {
|
|
95
|
+
"type": "boolean",
|
|
96
|
+
"description": "Optional, true only with \"remote\" in inputs: the game follows the turns contract, so several children may play with the remote, a round each. When a child's round ends (a crash too) and turns remain, the game asks break('turn') (the screen answers break-start, turn, break-done); after the last round it ends with every child's scores. Absent or false: remote mode is one child only."
|
|
97
|
+
},
|
|
98
|
+
"framing": {
|
|
99
|
+
"enum": ["auto", "fixed"],
|
|
100
|
+
"description": "Optional, \"auto\" by default: the screen zooms in on children standing far away (up to 1.8×), so every child's avatar is about the same size near or far; the view changes only at calibration and in breaks, never during play. \"fixed\": always the whole camera image (for a game drawn on exact spots of the live video)."
|
|
101
|
+
},
|
|
81
102
|
"intro": {
|
|
82
103
|
"type": "object",
|
|
83
104
|
"description": "Optional: Fabi explains the game on the TV before calibration, in 1 to 4 steps. Each step: one sentence (Italian and English, up to 90 characters; or one per age range) over an image of the game or a move shown with the children's avatars. The voice is generated by Fabi Play at publication from the text. Without intro the game goes straight to calibration; an invalid one is skipped by the TV.",
|