@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/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// @fabi-play/sdk: what a game imports. See https://docs.fabiplay.app.
|
|
2
2
|
// Generated by scripts/types.mjs from the sources: do not edit.
|
|
3
3
|
|
|
4
|
-
/** Anything with Play's on(): a Play, or a stand-in in tests. */
|
|
5
|
-
export type On = Pick<Play, "on"
|
|
4
|
+
/** Anything with Play's on() (and, from a Play, who plays and how): a Play, or a stand-in in tests. */
|
|
5
|
+
export type On = Pick<Play, "on"> & Partial<Pick<Play, "input" | "players">>;
|
|
6
6
|
/**
|
|
7
7
|
* Mutes the game while Fabi Play shows an ad, without the game doing anything: every <audio> and <video> in the page
|
|
8
8
|
* pauses, every Web Audio context is suspended, and all resume afterwards. Contexts are seen only if this runs
|
|
@@ -37,7 +37,11 @@ export interface KeyTarget {
|
|
|
37
37
|
export declare const domKeys: (doc?: Document) => KeyTarget;
|
|
38
38
|
/** Options of keyboard(). */
|
|
39
39
|
export interface KeyboardOptions {
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Whose movements (default 1); another player can get other keys (e.g. WASD). Not given and played with the
|
|
42
|
+
* remote: the child playing now, following the turns (at a turn the lane is the middle one again, as the screen
|
|
43
|
+
* recentres the puppet).
|
|
44
|
+
*/
|
|
41
45
|
player?: number;
|
|
42
46
|
/** Where the keys go: by default the page (domKeys()). */
|
|
43
47
|
target?: KeyTarget;
|
|
@@ -99,10 +103,15 @@ export type Wire<T> = T & {
|
|
|
99
103
|
/**
|
|
100
104
|
* Why the game stops, when it asks for a break:
|
|
101
105
|
* - level: a level is over (the best moment);
|
|
102
|
-
* - gameover: the game is over and a new one may start
|
|
103
|
-
*
|
|
106
|
+
* - gameover: the game is over and a new one may start;
|
|
107
|
+
* - turn: with input 'remote' and turns remaining, the child playing now finished their round (whatever happened in
|
|
108
|
+
* it: a crash or a fall ends a round too). The screen always answers with break-start (its turn screen is the
|
|
109
|
+
* break, and may host an ad), then turn (the next child), then break-done. Hosts before turns answer it as an
|
|
110
|
+
* unknown reason (break-done at once), but they never send turns either.
|
|
111
|
+
* Only 'turn' ever swaps players; 'level' and 'gameover' never do. Never at the game's own pause menu: a child who
|
|
112
|
+
* stops for a moment must not get an ad for it.
|
|
104
113
|
*/
|
|
105
|
-
export type BreakReason = "level" | "gameover";
|
|
114
|
+
export type BreakReason = "level" | "gameover" | "turn";
|
|
106
115
|
/**
|
|
107
116
|
* The movements the host recognises, per player:
|
|
108
117
|
* - `jump`, then `land` when the feet are back on the ground;
|
|
@@ -157,7 +166,9 @@ export type Backdrop = "video" | "none";
|
|
|
157
166
|
* image is cropped to fill the screen), with any backdrop, so avatars and hands are where the children stand (a
|
|
158
167
|
* "virtual mirror"). The screen decides it: with 'video', where the video is drawn (cover or contain); with 'none',
|
|
159
168
|
* the whole image contained at 92% of the viewport's height, bottom-aligned. Games don't compute it: they just use
|
|
160
|
-
* it.
|
|
169
|
+
* it. It is not fixed: with automatic framing (the default, see GameManifest.framing) the screen zooms in on children
|
|
170
|
+
* standing far away, up to 1.8×, so the image may reach past every edge; it changes only at calibration and in breaks
|
|
171
|
+
* (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
|
|
161
172
|
* screen at (x + p.x·w, y + p.y·h): see toScreen.
|
|
162
173
|
*/
|
|
163
174
|
export interface BackdropView {
|
|
@@ -200,6 +211,22 @@ export type HostMessage =
|
|
|
200
211
|
/** view: where the camera image is, with any backdrop (see BackdropView); absent if the screen doesn't know. */
|
|
201
212
|
/** stats: a debug host (the simulator) asks the game to say how often it draws (stats messages); absent on the TV. */
|
|
202
213
|
/** device: what the screen knows about the device (a starting tier, the display's rate); absent if it knows nothing. */
|
|
214
|
+
/**
|
|
215
|
+
* input: how this session is played (see GameInput); absent = 'body'. Only games whose manifest lists 'remote' ever
|
|
216
|
+
* get 'remote'. With 'remote', players is exactly one id: the child playing now (see turn).
|
|
217
|
+
*/
|
|
218
|
+
/**
|
|
219
|
+
* turns: only with input 'remote', and only to games whose manifest says "turns": true: every child who
|
|
220
|
+
* will play this game, one id each, in turn order, players[0] first (see parseTurnOrder); absent = players (one
|
|
221
|
+
* child, no turns). A round per child: at the end of each child's round, while turns remain, the game calls
|
|
222
|
+
* break('turn'); the screen answers break-start, turn (the next child), break-done. After the last child's round
|
|
223
|
+
* the game calls end() with every child's scores. An order that is not valid, or doesn't start with players[0],
|
|
224
|
+
* makes the session one child only.
|
|
225
|
+
*/
|
|
226
|
+
/**
|
|
227
|
+
* pointer: only with input 'remote', true when the TV's remote also points (LG's Magic Remote): it reaches the game
|
|
228
|
+
* as ordinary pointer and mouse events (hover, click with OK). Absent = false.
|
|
229
|
+
*/
|
|
203
230
|
{
|
|
204
231
|
type: "init";
|
|
205
232
|
v: number;
|
|
@@ -212,8 +239,11 @@ export type HostMessage =
|
|
|
212
239
|
view?: BackdropView;
|
|
213
240
|
stats?: boolean;
|
|
214
241
|
device?: DeviceHint;
|
|
242
|
+
input?: GameInput;
|
|
243
|
+
turns?: number[];
|
|
244
|
+
pointer?: boolean;
|
|
215
245
|
}
|
|
216
|
-
/** The backdrop or the view changed mid-game (video switched on or off, the camera image's fit changed). */
|
|
246
|
+
/** The backdrop or the view changed mid-game (video switched on or off, the camera image's fit or framing changed). */
|
|
217
247
|
| {
|
|
218
248
|
type: "backdrop";
|
|
219
249
|
backdrop: Backdrop;
|
|
@@ -252,6 +282,19 @@ export type HostMessage =
|
|
|
252
282
|
| {
|
|
253
283
|
type: "break-done";
|
|
254
284
|
shown: boolean;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Turns (input 'remote' only): from now on this child plays, in place of the one before. One remote is one child,
|
|
288
|
+
* so with several children the screen plays one at a time: the same game goes on with the next child in init's
|
|
289
|
+
* turns (players [2] after [1], …), exactly one id. Sent only inside the break of a break('turn') the game asked
|
|
290
|
+
* for (after break-start, before break-done); never at other breaks, so a round never changes hands midway. The
|
|
291
|
+
* new child's puppet starts recentred, in the middle lane (lane 0), with no lane gesture. Ids keep their meaning
|
|
292
|
+
* (slots, avatars): keep each player's score by id across turns, and end with the scores of every child who
|
|
293
|
+
* played. Never sent with input 'body', where init's players stay for the whole game.
|
|
294
|
+
*/
|
|
295
|
+
| {
|
|
296
|
+
type: "turn";
|
|
297
|
+
players: number[];
|
|
255
298
|
};
|
|
256
299
|
/** Why a game is paused (see HostMessage pause). */
|
|
257
300
|
export type PauseReason = "user" | "distance";
|
|
@@ -307,7 +350,11 @@ export type Movement = "hands" | "jump" | "squat" | "clap" | "freeze" | "lanes"
|
|
|
307
350
|
* "title": { "it": "Acchiappastelle", "en": "Star catcher" },
|
|
308
351
|
* "description": { "it": "Prendi le stelle con le mani.", "en": "Catch the stars with your hands." },
|
|
309
352
|
* "ageRanges": ["3-5", "6-8"], "movements": ["hands"], "players": { "min": 1, "max": 2 },
|
|
310
|
-
* "minutes": 3, "entry": "index.html", "sdk": 1
|
|
353
|
+
* "minutes": 3, "entry": "index.html", "sdk": 1,
|
|
354
|
+
* "intro": { "steps": [
|
|
355
|
+
* { "say": { "it": "Prendi le stelle!", "en": "Catch the stars!" }, "image": "intro/stars.jpg" },
|
|
356
|
+
* { "say": { "it": "Alza le braccia!", "en": "Arms up!" }, "move": "arms-up", "practice": true }
|
|
357
|
+
* ] }
|
|
311
358
|
* }
|
|
312
359
|
*/
|
|
313
360
|
export interface GameManifest {
|
|
@@ -332,15 +379,77 @@ export interface GameManifest {
|
|
|
332
379
|
};
|
|
333
380
|
/** Minutes of a typical game, 1 to 20. */
|
|
334
381
|
minutes: number;
|
|
335
|
-
/**
|
|
382
|
+
/**
|
|
383
|
+
* Page that starts the game: a path relative to the game's address (no scheme, no leading /, no "." or ".."
|
|
384
|
+
* segments, up to 200 characters), of letters, digits, _ and -, ending in lowercase .html: index.html, play/index.html.
|
|
385
|
+
*/
|
|
336
386
|
entry: string;
|
|
337
387
|
/** Protocol version of the SDK it was built with: 1 (PROTOCOL_VERSION). */
|
|
338
388
|
sdk: number;
|
|
339
389
|
/** Optional: the backdrops it can draw over besides its own; ['video'] opts in to mirror mode. */
|
|
340
390
|
backdrop?: GameBackdrop[];
|
|
391
|
+
/** Optional: Fabi explains the game on the TV before calibration (see GameIntro). Absent: straight to calibration. */
|
|
392
|
+
intro?: GameIntro;
|
|
393
|
+
/** 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. */
|
|
394
|
+
inputs?: GameInput[];
|
|
395
|
+
/**
|
|
396
|
+
* Optional, only with 'remote' in inputs: true when the game follows the turns contract, so the screen may let
|
|
397
|
+
* several children play with the remote, a round each (init.turns): play a round with the child in players; when
|
|
398
|
+
* it ends (a crash too) and turns remain, ask break('turn') (the screen answers break-start, turn, break-done);
|
|
399
|
+
* after the last round, end with every child's scores. Absent or false: remote mode is one child only.
|
|
400
|
+
*/
|
|
401
|
+
turns?: boolean;
|
|
402
|
+
/**
|
|
403
|
+
* Optional: 'auto' (the default) lets the screen frame the children: when they stand far from the camera it zooms
|
|
404
|
+
* in on them (up to 1.8×), so every child's avatar is about the same size and reaches the same things, near or
|
|
405
|
+
* far; the view changes only at calibration and in breaks. 'fixed' keeps the whole camera image, as before (for
|
|
406
|
+
* a game drawn on exact spots of the live video).
|
|
407
|
+
*/
|
|
408
|
+
framing?: GameFraming;
|
|
341
409
|
}
|
|
410
|
+
/** How the screen frames the camera image for a game: see GameManifest.framing. */
|
|
411
|
+
export type GameFraming = "auto" | "fixed";
|
|
412
|
+
/**
|
|
413
|
+
* How a game can be played: with the body (camera on the phone), or with the TV remote (keys become gestures).
|
|
414
|
+
*
|
|
415
|
+
* 'remote' is for TV apps without a phone (Samsung, LG, Android TV, Fire TV): one remote is one child, and with
|
|
416
|
+
* several children the screen runs turns, only for games whose manifest says "turns": true (see GameManifest.turns,
|
|
417
|
+
* BreakReason turn, HostMessage turn). The screen turns keys into the gestures the body makes (↑ or OK jump;
|
|
418
|
+
* ↓ held squat, stand on release; ← → lanes; OK twice quickly clap; Back is the platform's pause, never the game's)
|
|
419
|
+
* and animates a puppet body in the frames, so with one child most games need no code. On LG the Magic
|
|
420
|
+
* Remote also points: it reaches the game as ordinary pointer and mouse events (init.pointer says when). A game that
|
|
421
|
+
* lists 'remote' must play with those keys alone and, where it has things to pick (bubbles, answers, buttons), let a
|
|
422
|
+
* pointer click them too (pointer events, large hit areas, a hover highlight). Games aimed with the hands should not
|
|
423
|
+
* list it unless they offer a selection the keys can drive.
|
|
424
|
+
*/
|
|
425
|
+
export type GameInput = "body" | "remote";
|
|
342
426
|
/** The backdrops a game may declare in its manifest (its own background is always supported). */
|
|
343
427
|
export type GameBackdrop = Exclude<Backdrop, "none">;
|
|
428
|
+
/** The moves an intro step can show; the platform draws them with each child's avatar. Same names the SDK detects. */
|
|
429
|
+
export type IntroMove = "jump" | "squat" | "clap" | "lane-left" | "lane-right" | "arms-up" | "arms-wide" | "arms-v" | "star";
|
|
430
|
+
/** One sentence in every language, or one per age range (each with every language). */
|
|
431
|
+
export type IntroText = Record<Locale, string> | Partial<Record<AgeRange, Record<Locale, string>>>;
|
|
432
|
+
/** One step of the intro: Fabi says one sentence, over an image of the game or a move shown with the avatars. */
|
|
433
|
+
export interface IntroStep {
|
|
434
|
+
say: IntroText;
|
|
435
|
+
/**
|
|
436
|
+
* An image of the game, 16:9: a path relative to the game's address with the same rules as entry (no scheme, no
|
|
437
|
+
* leading /, no "." or ".." segments), ending in lowercase .jpg or .png (no WebP: old TV browsers may not decode it): intro/track.jpg.
|
|
438
|
+
*/
|
|
439
|
+
image?: string;
|
|
440
|
+
/** Or a move, shown with the avatars. Exactly one of image and move. */
|
|
441
|
+
move?: IntroMove;
|
|
442
|
+
/** 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. */
|
|
443
|
+
practice?: boolean;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Fabi explains the game on the TV, before calibration, in 1 to 4 steps (the platform adds its own final "ready"
|
|
447
|
+
* step). Only data: Fabi Play draws it, and speaks each sentence with a voice clip generated at publication from the
|
|
448
|
+
* text (voiceKey); a text without its clip yet is shown in the bubble only.
|
|
449
|
+
*/
|
|
450
|
+
export interface GameIntro {
|
|
451
|
+
steps: IntroStep[];
|
|
452
|
+
}
|
|
344
453
|
/** Where player space (0–1) lands on the canvas. */
|
|
345
454
|
export interface Box {
|
|
346
455
|
/** Left edge, canvas units. */
|
|
@@ -357,6 +466,10 @@ export interface Box {
|
|
|
357
466
|
* points (amount 0–1: how much of the new frame to take), and a point seen in the last frame stays visible until
|
|
358
467
|
* its visibility drops well below the threshold. Keep the result per player and pass it back as prev.
|
|
359
468
|
*
|
|
469
|
+
* The easing follows the speed: a point that barely moves (a still arm, where the camera's points only tremble)
|
|
470
|
+
* takes a small share of amount, down to STILL_SHARE, so a resting arm stays still instead of wobbling; a point that
|
|
471
|
+
* moves STILL_FULL or more in a frame takes all of amount, so real movements are not delayed.
|
|
472
|
+
*
|
|
360
473
|
* The face (nose, eyes, ears, mouth: what places and tilts the head) is smoothed much more, and relative to the
|
|
361
474
|
* shoulders: the head follows the body at once when the child moves, but face-point noise and small nods barely
|
|
362
475
|
* move it.
|
|
@@ -437,12 +550,12 @@ export type SmoothedBody = (who: PlayerFrame | number, options?: Pick<DrawAvatar
|
|
|
437
550
|
export declare function avatarDrawer(book: AvatarBook, scene: () => {
|
|
438
551
|
view: BackdropView | null;
|
|
439
552
|
}): DrawAvatar;
|
|
440
|
-
/** What the 'start' event says: who plays and how. */
|
|
553
|
+
/** What the 'start' event says: who plays and how, as it was at the start (late listeners get the same; see Easy for now). */
|
|
441
554
|
export interface StartInfo {
|
|
442
555
|
/** The players' age range: simpler rules for 3-5, scores and challenges for 9-12. */
|
|
443
556
|
ageRange: AgeRange;
|
|
444
|
-
/** The ids of the players (1, 2, …). */
|
|
445
|
-
players: number[];
|
|
557
|
+
/** The ids of the players (1, 2, …) at the start, frozen; with input 'remote', the first child's turn (see the turn event). */
|
|
558
|
+
players: readonly number[];
|
|
446
559
|
/** The language to show text in. */
|
|
447
560
|
locale: Locale;
|
|
448
561
|
/** 'tv': on a TV, children standing back; 'solo': a tablet in front of one child. */
|
|
@@ -453,6 +566,12 @@ export interface StartInfo {
|
|
|
453
566
|
view: BackdropView | null;
|
|
454
567
|
/** What the screen knows about the device (a starting tier, the display's rate), or null: see Play.device. */
|
|
455
568
|
device: DeviceHint | null;
|
|
569
|
+
/** Every child in turn order, frozen (with input 'remote', one round each; see Play.turns); otherwise players. */
|
|
570
|
+
turns: readonly number[];
|
|
571
|
+
/** 'body' (the default) or 'remote': played with the TV remote, keys turned into gestures (see Play.input). */
|
|
572
|
+
input: GameInput;
|
|
573
|
+
/** With input 'remote', whether the remote also points (LG's Magic Remote; see Play.pointer). */
|
|
574
|
+
pointer: boolean;
|
|
456
575
|
}
|
|
457
576
|
/**
|
|
458
577
|
* The events of FabiPlay.on, by name, with what each listener gets. Every gesture (jump, land, squat, stand, clap,
|
|
@@ -504,6 +623,13 @@ export type EasyEvents = {
|
|
|
504
623
|
backdrop: Backdrop;
|
|
505
624
|
view: BackdropView | null;
|
|
506
625
|
};
|
|
626
|
+
/**
|
|
627
|
+
* Remote mode only, inside the break of break('turn'): the next child's turn; players (one id) is who plays from
|
|
628
|
+
* now on. Keep each player's score by id.
|
|
629
|
+
*/
|
|
630
|
+
turn: {
|
|
631
|
+
players: number[];
|
|
632
|
+
};
|
|
507
633
|
};
|
|
508
634
|
/** An event name of FabiPlay.on. */
|
|
509
635
|
export type EasyEvent = keyof EasyEvents;
|
|
@@ -520,7 +646,7 @@ export type EasyEvent = keyof EasyEvents;
|
|
|
520
646
|
export interface Easy {
|
|
521
647
|
/**
|
|
522
648
|
* Listens to an event: a gesture by its name ('jump', 'clap', 'lane-left', …), 'start', 'hands', 'shape',
|
|
523
|
-
* 'players', 'pause', 'resume', 'break-start', 'break-end', 'backdrop' (see EasyEvents). Returns a function that
|
|
649
|
+
* 'players', 'pause', 'resume', 'break-start', 'break-end', 'backdrop', 'turn' (see EasyEvents). Returns a function that
|
|
524
650
|
* stops listening.
|
|
525
651
|
*
|
|
526
652
|
* @example
|
|
@@ -529,11 +655,14 @@ export interface Easy {
|
|
|
529
655
|
on<K extends EasyEvent>(event: K, listener: (data: EasyEvents[K]) => void): () => void;
|
|
530
656
|
/** Say the game is loaded; done for you on page load unless the script tag says data-ready="manual". */
|
|
531
657
|
ready(): void;
|
|
532
|
-
/** The score so far, for the screen to show; player 1 unless said. */
|
|
658
|
+
/** The score so far, for the screen to show; player 1 unless said (with input 'remote', the child playing now). */
|
|
533
659
|
score(score: number, player?: number): void;
|
|
534
660
|
/** The game stopped by itself ('level' or 'gameover', never its pause menu): Fabi Play may show an ad. Resolves when it can go on. */
|
|
535
661
|
break(reason?: BreakReason): Promise<boolean>;
|
|
536
|
-
/**
|
|
662
|
+
/**
|
|
663
|
+
* The end: a score (player 1's; with input 'remote', the child playing now's, and for each child who played
|
|
664
|
+
* before them in turns their last score() or 0), or one per player. Stars from 0 to 3, 2 if not given.
|
|
665
|
+
*/
|
|
537
666
|
end(score: number | {
|
|
538
667
|
player: number;
|
|
539
668
|
score: number;
|
|
@@ -552,6 +681,14 @@ export interface Easy {
|
|
|
552
681
|
* clicks by staying still on a spot. The script tag's data-pointer does the same.
|
|
553
682
|
*/
|
|
554
683
|
pointer(options?: Omit<PointerOptions, "target">): void;
|
|
684
|
+
/** The players playing now (frozen; see Play.players); empty before start. */
|
|
685
|
+
readonly players: readonly number[];
|
|
686
|
+
/** Every child in turn order (frozen; see Play.turns); empty before start. */
|
|
687
|
+
readonly turns: readonly number[];
|
|
688
|
+
/** How this session is played (see Play.input); 'body' before start. */
|
|
689
|
+
readonly input: GameInput;
|
|
690
|
+
/** With input 'remote', whether the remote also points (Play.pointer; pointer() is the hand adapter); false before start. */
|
|
691
|
+
readonly hasPointer: boolean;
|
|
555
692
|
/** Where the camera image is, with any backdrop; null if the host didn't say, and before start. */
|
|
556
693
|
readonly view: BackdropView | null;
|
|
557
694
|
/** What the screen knows about the device (see Play.device); null if it said nothing, and before start. */
|
|
@@ -619,6 +756,14 @@ export type Events = {
|
|
|
619
756
|
backdrop: Extract<HostMessage, {
|
|
620
757
|
type: "backdrop";
|
|
621
758
|
}>;
|
|
759
|
+
/**
|
|
760
|
+
* Remote mode only (input 'remote'), inside the break of a break('turn'): the next child's turn, the same game goes
|
|
761
|
+
* on with this one player (see Play.players). Keep each player's score by id. The SDK passes on only a turn to the
|
|
762
|
+
* next child in Play.turns, while a 'turn' break is under way; it drops any other.
|
|
763
|
+
*/
|
|
764
|
+
turn: Extract<HostMessage, {
|
|
765
|
+
type: "turn";
|
|
766
|
+
}>;
|
|
622
767
|
};
|
|
623
768
|
/**
|
|
624
769
|
* The game's side of Fabi Play, from connect(): who plays, their movements, and how the game tells it is ready,
|
|
@@ -634,8 +779,39 @@ export interface Play {
|
|
|
634
779
|
readonly locale: Locale;
|
|
635
780
|
/** 'tv': on a TV or Chromecast, children standing back; 'solo': a tablet in front of one child. */
|
|
636
781
|
readonly mode: "tv" | "solo";
|
|
637
|
-
/**
|
|
638
|
-
|
|
782
|
+
/**
|
|
783
|
+
* The ids of the players playing now (1, 2, …), as frames and gestures name them; a frozen array, read it again
|
|
784
|
+
* after a turn. With input 'body' the same for the whole game. With input 'remote' exactly one child, and it
|
|
785
|
+
* changes only inside the break of a break('turn') (the 'turn' event, between break-start and break-done). Keep
|
|
786
|
+
* each player's score by id across turns, and end with the scores of everyone who played.
|
|
787
|
+
*/
|
|
788
|
+
readonly players: readonly number[];
|
|
789
|
+
/**
|
|
790
|
+
* Every child who plays this game, in turn order (frozen): with input 'remote' and several children, one round
|
|
791
|
+
* each, players being the one playing now (turns[0] at the start). Only for games whose manifest says
|
|
792
|
+
* "turns": true (with "remote" in inputs); otherwise remote mode is one child. The contract:
|
|
793
|
+
* 1. play a round with the child in players;
|
|
794
|
+
* 2. at the end of it (whatever happened: a crash ends a round too), if turns remain, call break('turn'): the
|
|
795
|
+
* screen always answers break-start (its turn screen), 'turn' (the next child), then break-done;
|
|
796
|
+
* 3. after the last child's round, call end() with every child's scores.
|
|
797
|
+
* 'level' and 'gameover' breaks never swap players. With 'body', one child, or hosts that don't say: players.
|
|
798
|
+
*/
|
|
799
|
+
readonly turns: readonly number[];
|
|
800
|
+
/**
|
|
801
|
+
* How this session is played: 'body' (the camera on the phone; the default, and what older hosts mean) or
|
|
802
|
+
* 'remote' (a TV without a phone, only for games whose manifest lists it in inputs): the screen turns the remote's
|
|
803
|
+
* keys into the same gestures (↑ or OK jump, ↓ held squat then stand, ← → lanes, OK twice clap) and moves a puppet
|
|
804
|
+
* body in the frames, one child at a time. Adapt if it helps: slow down, show a selection the arrows move instead
|
|
805
|
+
* of a hand cursor.
|
|
806
|
+
*/
|
|
807
|
+
readonly input: GameInput;
|
|
808
|
+
/**
|
|
809
|
+
* Only with input 'remote': the remote also points (LG's Magic Remote), reaching the game as ordinary pointer and
|
|
810
|
+
* mouse events (hover, click with OK). Then let what the game has to pick be clicked too (large hit areas, a hover
|
|
811
|
+
* highlight). false otherwise, and from hosts that don't say. Not the pointer() adapter, which makes a child's hand
|
|
812
|
+
* the mouse.
|
|
813
|
+
*/
|
|
814
|
+
readonly pointer: boolean;
|
|
639
815
|
/** The screen's sound is off: start the game muted. */
|
|
640
816
|
readonly muted: boolean;
|
|
641
817
|
/** The players' age range: simpler rules for 3-5, scores and challenges for 9-12. */
|
|
@@ -697,8 +873,8 @@ export interface Play {
|
|
|
697
873
|
/** Called when backdrop or view changes; returns a function that stops listening. */
|
|
698
874
|
onBackdrop(listener: (backdrop: Backdrop, view: BackdropView | null) => void): () => void;
|
|
699
875
|
/**
|
|
700
|
-
* Listens to the host: 'frame', 'gesture', 'pause', 'resume', 'break-start', 'break-done', 'backdrop'
|
|
701
|
-
* Events). Returns a function that stops listening.
|
|
876
|
+
* Listens to the host: 'frame', 'gesture', 'pause', 'resume', 'break-start', 'break-done', 'backdrop', 'turn'
|
|
877
|
+
* (see Events). Returns a function that stops listening.
|
|
702
878
|
*
|
|
703
879
|
* @example
|
|
704
880
|
* const off = play.on('frame', (f) => (players = f.players))
|
|
@@ -709,9 +885,10 @@ export interface Play {
|
|
|
709
885
|
/** A player's score so far, for the screen to show. */
|
|
710
886
|
score(player: number, score: number): void;
|
|
711
887
|
/**
|
|
712
|
-
* The game has stopped by itself: a level is over
|
|
713
|
-
*
|
|
714
|
-
*
|
|
888
|
+
* The game has stopped by itself: a level is over, the game is over, or (with the remote, see turns) a child's
|
|
889
|
+
* round is over and the next child's turn is due: 'turn' (never at its pause menu). Fabi Play may show an ad
|
|
890
|
+
* there, if its rules allow (break-start, then break-done; with 'turn', the turn message in between); resolves
|
|
891
|
+
* when the game can go on, true if an ad was shown. Never call it in the middle of play.
|
|
715
892
|
*/
|
|
716
893
|
break(reason?: BreakReason): Promise<boolean>;
|
|
717
894
|
/**
|
package/dist/index.js
CHANGED
|
@@ -8,11 +8,13 @@ import {
|
|
|
8
8
|
isAvatarId,
|
|
9
9
|
parseDeviceHint,
|
|
10
10
|
parseGameStats,
|
|
11
|
+
parseTurnOrder,
|
|
12
|
+
parseTurnPlayers,
|
|
11
13
|
parseView,
|
|
12
14
|
smoothBody,
|
|
13
15
|
toScreen,
|
|
14
16
|
wire
|
|
15
|
-
} from "./chunks/chunk-
|
|
17
|
+
} from "./chunks/chunk-GFNOIOSX.js";
|
|
16
18
|
|
|
17
19
|
// src/adapters.ts
|
|
18
20
|
function muteDuringBreaks(play, win = window) {
|
|
@@ -82,7 +84,8 @@ var domKeys = (doc = document) => ({
|
|
|
82
84
|
var TAP_MS = 80;
|
|
83
85
|
var MAX_HOLD_MS = 800;
|
|
84
86
|
function keyboard(play, map = DEFAULT_KEYS, options = {}) {
|
|
85
|
-
const
|
|
87
|
+
const follow = options.player === void 0 && play.input === "remote";
|
|
88
|
+
let player = options.player ?? (follow ? play.players?.[0] ?? 1 : 1);
|
|
86
89
|
const target = options.target ?? domKeys();
|
|
87
90
|
const later = options.setTimeout ?? ((fn, ms) => setTimeout(fn, ms));
|
|
88
91
|
const held = /* @__PURE__ */ new Set();
|
|
@@ -138,7 +141,17 @@ function keyboard(play, map = DEFAULT_KEYS, options = {}) {
|
|
|
138
141
|
if (p.shape === "arms-up") down("arms-up");
|
|
139
142
|
else up("arms-up");
|
|
140
143
|
};
|
|
141
|
-
const offs = [
|
|
144
|
+
const offs = [
|
|
145
|
+
play.on("gesture", onGesture),
|
|
146
|
+
play.on("frame", onFrame),
|
|
147
|
+
play.on("break-start", () => [...held].forEach(up)),
|
|
148
|
+
play.on("turn", (m) => {
|
|
149
|
+
if (!follow) return;
|
|
150
|
+
[...held].forEach(up);
|
|
151
|
+
player = m.players[0] ?? player;
|
|
152
|
+
lane = 0;
|
|
153
|
+
})
|
|
154
|
+
];
|
|
142
155
|
return () => {
|
|
143
156
|
offs.forEach((off) => off());
|
|
144
157
|
[...held].forEach(up);
|
|
@@ -313,6 +326,8 @@ function easy(connecting) {
|
|
|
313
326
|
const listeners = /* @__PURE__ */ new Map();
|
|
314
327
|
const emit = (event, data) => listeners.get(event)?.forEach((l) => l(data));
|
|
315
328
|
let play = null;
|
|
329
|
+
let start = null;
|
|
330
|
+
const scores = /* @__PURE__ */ new Map();
|
|
316
331
|
const queue = [];
|
|
317
332
|
const withPlay = (fn) => play ? fn(play) : queue.push(fn);
|
|
318
333
|
const early = new AvatarBook();
|
|
@@ -326,6 +341,7 @@ function easy(connecting) {
|
|
|
326
341
|
p.on("break-start", () => emit("break-start", {}));
|
|
327
342
|
p.on("break-done", (m) => emit("break-end", { shown: m.shown }));
|
|
328
343
|
p.onBackdrop((backdrop, view) => emit("backdrop", { backdrop, view }));
|
|
344
|
+
p.on("turn", (m) => emit("turn", { players: [...m.players] }));
|
|
329
345
|
p.on("frame", (f) => {
|
|
330
346
|
emit("players", { players: f.players });
|
|
331
347
|
for (const pl of f.players) {
|
|
@@ -336,7 +352,8 @@ function easy(connecting) {
|
|
|
336
352
|
}
|
|
337
353
|
}
|
|
338
354
|
});
|
|
339
|
-
|
|
355
|
+
start = startInfo(p);
|
|
356
|
+
emit("start", start);
|
|
340
357
|
queue.splice(0).forEach((fn) => fn(p));
|
|
341
358
|
});
|
|
342
359
|
return {
|
|
@@ -344,17 +361,33 @@ function easy(connecting) {
|
|
|
344
361
|
const set = listeners.get(event) ?? /* @__PURE__ */ new Set();
|
|
345
362
|
listeners.set(event, set);
|
|
346
363
|
set.add(listener);
|
|
347
|
-
if (event === "start" &&
|
|
364
|
+
if (event === "start" && start) listener(start);
|
|
348
365
|
return () => void set.delete(listener);
|
|
349
366
|
},
|
|
350
367
|
ready: () => withPlay((p) => p.ready()),
|
|
351
|
-
score: (score, player
|
|
368
|
+
score: (score, player) => withPlay((p) => {
|
|
369
|
+
const who = player ?? playingNow(p);
|
|
370
|
+
scores.set(who, score);
|
|
371
|
+
p.score(who, score);
|
|
372
|
+
}),
|
|
352
373
|
break: (reason) => new Promise((resolve) => withPlay((p) => void p.break(reason).then(resolve))),
|
|
353
374
|
end: (score) => withPlay(
|
|
354
|
-
(p) => p.end(typeof score === "number" ?
|
|
375
|
+
(p) => p.end(typeof score === "number" ? endOf(p, score, scores) : score.map((s) => ({ player: s.player, score: s.score, stars: s.stars ?? 2 })))
|
|
355
376
|
),
|
|
356
377
|
keys: (map, options) => withPlay((p) => void keyboard(p, map, options)),
|
|
357
378
|
pointer: (options) => withPlay((p) => void pointer(p, options)),
|
|
379
|
+
get players() {
|
|
380
|
+
return play?.players ?? none;
|
|
381
|
+
},
|
|
382
|
+
get turns() {
|
|
383
|
+
return play?.turns ?? none;
|
|
384
|
+
},
|
|
385
|
+
get input() {
|
|
386
|
+
return play?.input ?? "body";
|
|
387
|
+
},
|
|
388
|
+
get hasPointer() {
|
|
389
|
+
return play?.pointer ?? false;
|
|
390
|
+
},
|
|
358
391
|
get view() {
|
|
359
392
|
return play?.view ?? null;
|
|
360
393
|
},
|
|
@@ -368,7 +401,15 @@ function easy(connecting) {
|
|
|
368
401
|
smoothedBody: (...args) => play ? play.smoothedBody(...args) : early.smoothedBody(...args)
|
|
369
402
|
};
|
|
370
403
|
}
|
|
371
|
-
var
|
|
404
|
+
var none = Object.freeze([]);
|
|
405
|
+
var startInfo = (p) => ({ ageRange: p.ageRange, players: p.players, locale: p.locale, mode: p.mode, backdrop: p.backdrop, view: p.view, device: p.device, turns: p.turns, input: p.input, pointer: p.pointer });
|
|
406
|
+
var playingNow = (p) => p.input === "remote" ? p.players[0] ?? 1 : 1;
|
|
407
|
+
function endOf(p, score, last) {
|
|
408
|
+
const now = playingNow(p);
|
|
409
|
+
if (p.input !== "remote") return [{ player: now, score, stars: 2 }];
|
|
410
|
+
const played = p.turns.slice(0, Math.max(p.turns.indexOf(now), 0));
|
|
411
|
+
return [...played.map((player) => ({ player, score: last.get(player) ?? 0, stars: 2 })), { player: now, score, stars: 2 }];
|
|
412
|
+
}
|
|
372
413
|
|
|
373
414
|
// src/index.ts
|
|
374
415
|
var windowChannel = (win = window) => ({
|
|
@@ -382,9 +423,14 @@ function connect(channel = windowChannel()) {
|
|
|
382
423
|
return new Promise((resolve, reject) => {
|
|
383
424
|
const listeners = /* @__PURE__ */ new Map();
|
|
384
425
|
const breaks = [];
|
|
426
|
+
let inBreak = false;
|
|
427
|
+
let turned = false;
|
|
385
428
|
let started = false;
|
|
386
429
|
let backdrop = "none";
|
|
387
430
|
let view = null;
|
|
431
|
+
let input = "body";
|
|
432
|
+
let players = Object.freeze([]);
|
|
433
|
+
let turns = players;
|
|
388
434
|
const backdropListeners = /* @__PURE__ */ new Set();
|
|
389
435
|
const avatars = new AvatarBook();
|
|
390
436
|
const drawAvatar2 = avatarDrawer(avatars, () => ({ view }));
|
|
@@ -403,6 +449,11 @@ function connect(channel = windowChannel()) {
|
|
|
403
449
|
backdrop = m.backdrop === "video" ? "video" : "none";
|
|
404
450
|
view = parseView(m.view) ?? null;
|
|
405
451
|
const device = parseDeviceHint(m.device) ?? null;
|
|
452
|
+
input = m.input === "remote" ? "remote" : "body";
|
|
453
|
+
const pointer2 = input === "remote" && m.pointer === true;
|
|
454
|
+
players = Object.freeze(Array.isArray(m.players) ? [...m.players] : []);
|
|
455
|
+
const order = input === "remote" && players.length === 1 ? parseTurnOrder(m.turns) : void 0;
|
|
456
|
+
turns = Object.freeze(order && order[0] === players[0] ? order : [...players]);
|
|
406
457
|
let reported = {};
|
|
407
458
|
if (m.stats === true) {
|
|
408
459
|
let since = performance.now();
|
|
@@ -416,7 +467,12 @@ function connect(channel = windowChannel()) {
|
|
|
416
467
|
resolve({
|
|
417
468
|
locale: m.locale,
|
|
418
469
|
mode: m.mode,
|
|
419
|
-
players
|
|
470
|
+
get players() {
|
|
471
|
+
return players;
|
|
472
|
+
},
|
|
473
|
+
turns,
|
|
474
|
+
input,
|
|
475
|
+
pointer: pointer2,
|
|
420
476
|
muted: m.muted,
|
|
421
477
|
ageRange: m.ageRange ?? "6-8",
|
|
422
478
|
get backdrop() {
|
|
@@ -446,7 +502,7 @@ function connect(channel = windowChannel()) {
|
|
|
446
502
|
ready: () => channel.post(wire({ type: "ready" })),
|
|
447
503
|
score: (player, score) => channel.post(wire({ type: "score", player, score })),
|
|
448
504
|
break: (reason = "level") => new Promise((done) => {
|
|
449
|
-
breaks.push(done);
|
|
505
|
+
breaks.push({ reason, done });
|
|
450
506
|
channel.post(wire({ type: "break", reason }));
|
|
451
507
|
}),
|
|
452
508
|
end: (scores) => channel.post(wire({ type: "end", scores }))
|
|
@@ -464,8 +520,20 @@ function connect(channel = windowChannel()) {
|
|
|
464
520
|
message = { type: "backdrop", backdrop, ...view ? { view } : {} };
|
|
465
521
|
for (const l of [...backdropListeners]) l(backdrop, view);
|
|
466
522
|
}
|
|
523
|
+
if (m.type === "turn") {
|
|
524
|
+
const next = input === "remote" && inBreak && !turned && breaks[0]?.reason === "turn" ? parseTurnPlayers(m.players) : void 0;
|
|
525
|
+
const now = players[0];
|
|
526
|
+
if (!next || now === void 0 || turns.indexOf(next[0]) <= turns.indexOf(now)) return;
|
|
527
|
+
turned = true;
|
|
528
|
+
players = Object.freeze(next);
|
|
529
|
+
message = { type: "turn", players: [...next] };
|
|
530
|
+
}
|
|
531
|
+
if (m.type === "break-start") inBreak = breaks.length > 0;
|
|
467
532
|
for (const l of listeners.get(m.type) ?? []) l(message);
|
|
468
|
-
if (m.type === "break-done")
|
|
533
|
+
if (m.type === "break-done") {
|
|
534
|
+
inBreak = turned = false;
|
|
535
|
+
breaks.shift()?.done(m.shown);
|
|
536
|
+
}
|
|
469
537
|
}
|
|
470
538
|
});
|
|
471
539
|
});
|
package/dist/integrity.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "@fabi-play/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"algorithm": "sha384",
|
|
5
5
|
"files": {
|
|
6
|
-
"fabi-play.js": "sha384-
|
|
7
|
-
"fabi-play.umd.js": "sha384-
|
|
8
|
-
"fabi-play.umd.min.js": "sha384-
|
|
9
|
-
"fabi-play.d.ts": "sha384-
|
|
10
|
-
"fabi-play.schema.json": "sha384-
|
|
6
|
+
"fabi-play.js": "sha384-GQRWKvHJbNMap9RaUmZb53cutIjP+4znP6K2qV33TDz7CuB8tUEKWb8b6rIr0kDZ",
|
|
7
|
+
"fabi-play.umd.js": "sha384-GARYP8mvinQoV9l0yqHQUr6tz8dSGhHtBVCJvJT3vgzbaeheds0DbMwb4TORJkE7",
|
|
8
|
+
"fabi-play.umd.min.js": "sha384-EsqeNcQaqmkU5zfOtErKZwQdxoWhm8M5t7ra10U0o0IKRHQw37CyNC5BpLogDSHw",
|
|
9
|
+
"fabi-play.d.ts": "sha384-D99Rlv+HHC7+UQlLqOJreHqt6JDVMzzxuJbXIBVmB4I+WFER2ELU/ESCiX5qT9T6",
|
|
10
|
+
"fabi-play.schema.json": "sha384-R70ay7ddz0QDzU+YN5CiyiVSeCDdoGymdVNbbSBPUOQgDpAh4ajinUGzRLa5qq7y"
|
|
11
11
|
}
|
|
12
12
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabi-play/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "What a game imports: players, movements and the end of the game, from inside its sandboxed frame.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fabi-play",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@fabi-play/avatars": "^0.
|
|
40
|
-
"@fabi-play/protocol": "^0.
|
|
39
|
+
"@fabi-play/avatars": "^0.3.0",
|
|
40
|
+
"@fabi-play/protocol": "^0.3.0",
|
|
41
41
|
"dts-bundle-generator": "9.5.1",
|
|
42
42
|
"esbuild": "0.28.2",
|
|
43
43
|
"tsx": "4.23.15",
|