| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124 |
- import type { ContentBundle } from "@nengine/core";
- import { arrowDirection } from "@nengine/render";
- /**
- * A pending save produced by an open editor model. `content` is the
- * stringified YAML to persist; `error` models a model that cannot serialize
- * (invalid edits never write — the playtest must fail loudly before boot).
- */
- export type PlaytestFlush = { path: string; content: string } | { path: string; error: string };
- /** Collects the dirty models currently open in the editor (empty = nothing to save). */
- export type PlaytestFlushCollector = () => PlaytestFlush[];
- export interface SaveAllResult {
- flushed: string[];
- failures: { path: string; error: string }[];
- }
- /** Persist every dirty model; a rejecting write or an un-serializable model is
- * recorded as a failure (never thrown), so a single bad file cannot half-save. */
- export async function saveAllFlushes(
- flushes: PlaytestFlush[],
- write: (path: string, content: string) => Promise<void>,
- ): Promise<SaveAllResult> {
- const flushed: string[] = [];
- const failures: { path: string; error: string }[] = [];
- for (const flush of flushes) {
- if ("error" in flush) {
- failures.push({ path: flush.path, error: flush.error });
- continue;
- }
- try {
- await write(flush.path, flush.content);
- flushed.push(flush.path);
- } catch (err) {
- failures.push({ path: flush.path, error: err instanceof Error ? err.message : String(err) });
- }
- }
- return { flushed, failures };
- }
- export type PlaytestPhase = "idle" | "saving" | "compiling" | "booting" | "playing" | "stopping";
- export interface PlaytestSessionDeps {
- collectFlushes: PlaytestFlushCollector;
- write: (path: string, content: string) => Promise<void>;
- compile: () => Promise<ContentBundle>;
- boot: (bundle: ContentBundle) => Promise<void>;
- teardown: () => Promise<void> | void;
- onPhase?: (phase: PlaytestPhase) => void;
- }
- export interface PlaytestStartResult {
- ok: boolean;
- saved: string[];
- error?: string;
- }
- export interface PlaytestSession {
- readonly phase: PlaytestPhase;
- start(): Promise<PlaytestStartResult>;
- stop(): Promise<void>;
- }
- /** The playtest lifecycle (E5): save-all → compile → boot → playing, with
- * stop() destroying the booted instance. Pure orchestration — every I/O side
- * is an injected dep, so the whole sequence is node-testable. */
- export function createPlaytestSession(deps: PlaytestSessionDeps): PlaytestSession {
- let phase: PlaytestPhase = "idle";
- function setPhase(next: PlaytestPhase): void {
- phase = next;
- deps.onPhase?.(next);
- }
- async function start(): Promise<PlaytestStartResult> {
- if (phase !== "idle") {
- return { ok: false, saved: [], error: "playtest already running" };
- }
- setPhase("saving");
- const result = await saveAllFlushes(deps.collectFlushes(), deps.write);
- if (result.failures.length > 0) {
- setPhase("idle");
- const lines = result.failures.map((f) => `- ${f.path}: ${f.error}`).join("\n");
- return { ok: false, saved: result.flushed, error: `save-all failed:\n${lines}` };
- }
- setPhase("compiling");
- let bundle: ContentBundle;
- try {
- bundle = await deps.compile();
- } catch (err) {
- setPhase("idle");
- return { ok: false, saved: result.flushed, error: err instanceof Error ? err.message : String(err) };
- }
- setPhase("booting");
- try {
- await deps.boot(bundle);
- } catch (err) {
- setPhase("idle");
- return { ok: false, saved: result.flushed, error: err instanceof Error ? err.message : String(err) };
- }
- setPhase("playing");
- return { ok: true, saved: result.flushed };
- }
- async function stop(): Promise<void> {
- if (phase === "idle") return;
- setPhase("stopping");
- try {
- await deps.teardown();
- } finally {
- setPhase("idle");
- }
- }
- return { get phase() { return phase; }, start, stop };
- }
- /** Map a keydown key to a raw `"key:Arrow*"` input string, or undefined for
- * non-arrow keys (the local-sync input mapping, D25). */
- export function keydownInput(key: string): string | undefined {
- const input = `key:${key}`;
- return arrowDirection(input) ? input : undefined;
- }
|