@fabi-play/sdk 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/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";
@@ -347,7 +390,39 @@ export interface GameManifest {
347
390
  backdrop?: GameBackdrop[];
348
391
  /** Optional: Fabi explains the game on the TV before calibration (see GameIntro). Absent: straight to calibration. */
349
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;
350
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";
351
426
  /** The backdrops a game may declare in its manifest (its own background is always supported). */
352
427
  export type GameBackdrop = Exclude<Backdrop, "none">;
353
428
  /** The moves an intro step can show; the platform draws them with each child's avatar. Same names the SDK detects. */
@@ -391,6 +466,10 @@ export interface Box {
391
466
  * points (amount 0–1: how much of the new frame to take), and a point seen in the last frame stays visible until
392
467
  * its visibility drops well below the threshold. Keep the result per player and pass it back as prev.
393
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
+ *
394
473
  * The face (nose, eyes, ears, mouth: what places and tilts the head) is smoothed much more, and relative to the
395
474
  * shoulders: the head follows the body at once when the child moves, but face-point noise and small nods barely
396
475
  * move it.
@@ -471,12 +550,12 @@ export type SmoothedBody = (who: PlayerFrame | number, options?: Pick<DrawAvatar
471
550
  export declare function avatarDrawer(book: AvatarBook, scene: () => {
472
551
  view: BackdropView | null;
473
552
  }): DrawAvatar;
474
- /** 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). */
475
554
  export interface StartInfo {
476
555
  /** The players' age range: simpler rules for 3-5, scores and challenges for 9-12. */
477
556
  ageRange: AgeRange;
478
- /** The ids of the players (1, 2, …). */
479
- 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[];
480
559
  /** The language to show text in. */
481
560
  locale: Locale;
482
561
  /** 'tv': on a TV, children standing back; 'solo': a tablet in front of one child. */
@@ -487,6 +566,12 @@ export interface StartInfo {
487
566
  view: BackdropView | null;
488
567
  /** What the screen knows about the device (a starting tier, the display's rate), or null: see Play.device. */
489
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;
490
575
  }
491
576
  /**
492
577
  * The events of FabiPlay.on, by name, with what each listener gets. Every gesture (jump, land, squat, stand, clap,
@@ -538,6 +623,13 @@ export type EasyEvents = {
538
623
  backdrop: Backdrop;
539
624
  view: BackdropView | null;
540
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
+ };
541
633
  };
542
634
  /** An event name of FabiPlay.on. */
543
635
  export type EasyEvent = keyof EasyEvents;
@@ -554,7 +646,7 @@ export type EasyEvent = keyof EasyEvents;
554
646
  export interface Easy {
555
647
  /**
556
648
  * Listens to an event: a gesture by its name ('jump', 'clap', 'lane-left', …), 'start', 'hands', 'shape',
557
- * '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
558
650
  * stops listening.
559
651
  *
560
652
  * @example
@@ -563,11 +655,14 @@ export interface Easy {
563
655
  on<K extends EasyEvent>(event: K, listener: (data: EasyEvents[K]) => void): () => void;
564
656
  /** Say the game is loaded; done for you on page load unless the script tag says data-ready="manual". */
565
657
  ready(): void;
566
- /** 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). */
567
659
  score(score: number, player?: number): void;
568
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. */
569
661
  break(reason?: BreakReason): Promise<boolean>;
570
- /** 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
+ */
571
666
  end(score: number | {
572
667
  player: number;
573
668
  score: number;
@@ -586,6 +681,14 @@ export interface Easy {
586
681
  * clicks by staying still on a spot. The script tag's data-pointer does the same.
587
682
  */
588
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;
589
692
  /** Where the camera image is, with any backdrop; null if the host didn't say, and before start. */
590
693
  readonly view: BackdropView | null;
591
694
  /** What the screen knows about the device (see Play.device); null if it said nothing, and before start. */
@@ -653,6 +756,14 @@ export type Events = {
653
756
  backdrop: Extract<HostMessage, {
654
757
  type: "backdrop";
655
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
+ }>;
656
767
  };
657
768
  /**
658
769
  * The game's side of Fabi Play, from connect(): who plays, their movements, and how the game tells it is ready,
@@ -668,8 +779,39 @@ export interface Play {
668
779
  readonly locale: Locale;
669
780
  /** 'tv': on a TV or Chromecast, children standing back; 'solo': a tablet in front of one child. */
670
781
  readonly mode: "tv" | "solo";
671
- /** The ids of the players in this game (1, 2, …), as frames and gestures name them. */
672
- 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;
673
815
  /** The screen's sound is off: start the game muted. */
674
816
  readonly muted: boolean;
675
817
  /** The players' age range: simpler rules for 3-5, scores and challenges for 9-12. */
@@ -731,8 +873,8 @@ export interface Play {
731
873
  /** Called when backdrop or view changes; returns a function that stops listening. */
732
874
  onBackdrop(listener: (backdrop: Backdrop, view: BackdropView | null) => void): () => void;
733
875
  /**
734
- * Listens to the host: 'frame', 'gesture', 'pause', 'resume', 'break-start', 'break-done', 'backdrop' (see
735
- * 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.
736
878
  *
737
879
  * @example
738
880
  * const off = play.on('frame', (f) => (players = f.players))
@@ -743,9 +885,10 @@ export interface Play {
743
885
  /** A player's score so far, for the screen to show. */
744
886
  score(player: number, score: number): void;
745
887
  /**
746
- * The game has stopped by itself: a level is over or the game is over (never its pause menu). Fabi Play may show
747
- * an ad there, if its rules allow (break-start, then break-done); resolves when the game can go on, true if an
748
- * 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.
749
892
  */
750
893
  break(reason?: BreakReason): Promise<boolean>;
751
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-57WXWAXG.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.2.0",
3
+ "version": "0.3.0",
4
4
  "algorithm": "sha384",
5
5
  "files": {
6
- "fabi-play.js": "sha384-q1iuDi35LU3t+xtPL5vtoWd2pTTjZeKeNdtsickjpqwRGrkIqVxG48cIaGUWCoFP",
7
- "fabi-play.umd.js": "sha384-XhDlQuvhNDkT6sTpHWV3o24704da4bk5EfH4Jq3ofz+rg3C+QNu6git9XB/icMI5",
8
- "fabi-play.umd.min.js": "sha384-nyM7yG3J5Dt/2g00hGsZSe/eiMTrk7bQRYtQ0CIZhB0WJoO0aGjb7T8yy6Yr3nG4",
9
- "fabi-play.d.ts": "sha384-V23/J4JFAiTVaw+nSfWHhsRyWELfql7EU5mhtQd/caA3qpNyNr2Dz6Hb8I6Lu3Zl",
10
- "fabi-play.schema.json": "sha384-dLS2VzWC6Q+BI64galOdzvfoycQ+n6KjFXIajrirk15dwjvAgrDMuszeld/BhQgc"
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.2.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.2.0",
40
- "@fabi-play/protocol": "^0.2.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",