@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/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
- /** Whose movements (default 1); another player can get other keys (e.g. WASD). */
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
- * Never at the game's own pause menu: a child who stops for a moment must not get an ad for it.
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. Body points are in player space (0–1 of the camera image, already mirrored like the video), so a point p is on
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
- /** Page that starts the game, an .html file relative to the game's address (same site, no ".."). */
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
- /** The end: a score, or one per player. Stars from 0 to 3, 2 if not given. */
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
- /** The ids of the players in this game (1, 2, …), as frames and gestures name them. */
638
- readonly players: number[];
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' (see
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 or the game is over (never its pause menu). Fabi Play may show
713
- * an ad there, if its rules allow (break-start, then break-done); resolves when the game can go on, true if an
714
- * ad was shown. Never call it in the middle of play.
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-QJPCNRFI.js";
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 player = options.player ?? 1;
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 = [play.on("gesture", onGesture), play.on("frame", onFrame), play.on("break-start", () => [...held].forEach(up))];
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
- emit("start", startInfo(p));
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" && play) listener(startInfo(play));
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 = 1) => withPlay((p) => p.score(player, score)),
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" ? [{ player: 1, score, stars: 2 }] : score.map((s) => ({ player: s.player, score: s.score, stars: s.stars ?? 2 })))
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 startInfo = (p) => ({ ageRange: p.ageRange, players: p.players, locale: p.locale, mode: p.mode, backdrop: p.backdrop, view: p.view, device: p.device });
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: m.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") breaks.shift()?.(m.shown);
533
+ if (m.type === "break-done") {
534
+ inBreak = turned = false;
535
+ breaks.shift()?.done(m.shown);
536
+ }
469
537
  }
470
538
  });
471
539
  });
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "package": "@fabi-play/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "algorithm": "sha384",
5
5
  "files": {
6
- "fabi-play.js": "sha384-vwHpIsx7Fav+S6/jNTY6qYfWT1zq3p2NIq9zD4T56qXw+/ogk6jfdwJIs0/gFNy3",
7
- "fabi-play.umd.js": "sha384-YDcTw2S3gcyQYGCn1cRcqui3DaqF1ByJ4cvS4HBP++xY7bSuLmSc6yhRH1RQ0b9G",
8
- "fabi-play.umd.min.js": "sha384-Hhlc2ZXMa3NvLOfsHXMTXlWxSMzw8GwJgMHID4I++red4Xmg2a8n3c67alpHgPlD",
9
- "fabi-play.d.ts": "sha384-NjFh/5P1gigt+yi1P58WplUgGh2CQUSJd77W+4puFk+r1/hDB7mkXK5tYoRKTYWB",
10
- "fabi-play.schema.json": "sha384-tHySTbbJnqrjp7H93Y/NxsyIqU1QUHnMqCbx4N7hOzfUzgvXpDYOpzZLXgWTOHAU"
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.1.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.1.0",
40
- "@fabi-play/protocol": "^0.1.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",