// Fan-out adapter: drives the on-screen puppet and the real robot in // lock-step (same targets, same durations, so they stay visually in // sync). The command log lives here in embed mode — one entry per // program action regardless of how many robots execute it. import type { CommandLog } from "../util/log"; import type { LoadedEmotion, LoadedSound, PoseTargetDeg, RobotAdapter, } from "./adapter"; export class MultiplexAdapter implements RobotAdapter { readonly kind = "multi" as const; constructor( private children: RobotAdapter[], private log?: CommandLog, ) {} async goto(target: PoseTargetDeg, durationSec: number, signal?: AbortSignal): Promise { this.log?.push("goto", { target, durationSec }); await Promise.all(this.children.map((c) => c.goto(target, durationSec, signal))); } async playEmotion(emotion: LoadedEmotion, signal?: AbortSignal): Promise { this.log?.push("playEmotion", { id: emotion.id }); await Promise.all(this.children.map((c) => c.playEmotion(emotion, signal))); } async playSound(sound: LoadedSound, signal?: AbortSignal): Promise { this.log?.push("playSound", { id: sound.id }); await Promise.all(this.children.map((c) => c.playSound(sound, signal))); } antennaTouched(): boolean { return this.children.some((c) => c.antennaTouched()); } async stopAll(): Promise { this.log?.push("stopAll"); await Promise.all(this.children.map((c) => c.stopAll())); } async returnHome(): Promise { this.log?.push("returnHome"); await Promise.all(this.children.map((c) => c.returnHome())); } dispose(): void { for (const c of this.children) c.dispose(); } }