import { b as Engine } from "./behavior-B_245qRy.js"; import { s as JsonObject } from "./rng-BsXZg3D6.js"; //#region src/core/audit.d.ts /** Human-readable warnings (empty = clean). Pure JSON walk, no registry. */ interface AuditOptions { /** * Every locale table the whole PROJECT declares, unioned. * * Localization tables MERGE across scenes — a game's shared UI strings live * in the scene it boots from, and a level that adds three lines of its own * does not wipe them. Auditing one file in isolation therefore reported a key * declared in the boot scene as "nothing declares" it, and told its author to * add a string they already have. The checker passes the corpus in when it * has one. */ declaredElsewhere?: Record>; /** * Another scene in this project embeds this file with `instance`. * * That, and only that, makes a scene a FRAGMENT — a thing lit and framed by * whatever drops it in, which must not be told it will render black. * * The discriminator used to be "it has no camera", which is the same shape a * beginner's FIRST 3D scene has. Measured on a ground plane and a capsule * with no camera and no light — a guaranteed black screen: * * $ incanto-check * ok src/game.scene.json * 1/1 scene(s) valid * * Not the lighting warning, and not the camera one either. The checker walks * the whole project already (it unions `strings` and `assets` the same way), * so it can answer this by looking instead of guessing. */ instanced?: boolean; } /** * Which files in a corpus some OTHER scene embeds with `instance`. * * A fragment cannot be recognised by looking at itself — `enemy.scene.json` and * a broken first level are the same file shape. It is recognised by the project * around it, which every tool that audits more than one scene already walks. * * `resolveRef(fromFile, ref)` turns a scene-relative `instance` path into the * same spelling the corpus uses for that file, so the caller owns all path * logic and this stays runnable in a browser. */ declare function instancedScenes(corpus: { path: string; json: JsonObject; }[], resolveRef: (fromFile: string, ref: string) => string): Set; declare function auditScene(scene: JsonObject, opts?: AuditOptions): string[]; //#endregion //#region src/core/errors.d.ts /** * Stable error codes for every hard failure in the engine. * * Incanto fails loudly with machine-readable codes and messages that list valid * alternatives — AI agents self-correct on hard failures, not warnings. */ type IncantoErrorCode = "AMBIGUOUS_HANDLER" | "BAD_FORMAT" | "BAD_NODE_PATH" | "DANGLING_CONNECTION" | "DUPLICATE_BEHAVIOR" | "DUPLICATE_NODE_TYPE" | "DUPLICATE_UNIQUE_NAME" | "DUPLICATE_UID" | "NETWORK_UNAVAILABLE" | "NODE_NOT_FOUND" | "PROP_TYPE_MISMATCH" | "TREE_VIOLATION" | "UNKNOWN_ANIMATION" | "UNKNOWN_ASSET" | "UNKNOWN_CONSTANT" | "UNKNOWN_BEHAVIOR" | "UNKNOWN_HANDLER" | "UNKNOWN_NODE_TYPE" | "UNKNOWN_PROP" | "UNKNOWN_SIGNAL" | "UNRESOLVED_INSTANCE" | "WRONG_DIMENSION" | "WEBGL_UNAVAILABLE"; /** * Structured context mirrored from the prose message — tools and agents parse * THESE instead of regexing the message (which is for humans and may reword). */ interface IncantoErrorDetails { /** Absolute path of the offending node in the scene being loaded ('/Root/Child'). */ readonly path?: string; /** The offending node's uid, when the scene JSON declared one. */ readonly uid?: string; /** The offending node's type name. */ readonly nodeType?: string; /** The offending prop key. */ readonly prop?: string; /** The offending signal name. */ readonly signal?: string; /** The valid alternatives the message also lists — for programmatic self-correction. */ readonly validOptions?: readonly string[]; } declare class IncantoError extends Error { readonly code: IncantoErrorCode; readonly details: IncantoErrorDetails; constructor(code: IncantoErrorCode, message: string, details?: IncantoErrorDetails); } //#endregion //#region src/core/replay.d.ts /** * Deterministic input replay — the engine already runs on a seeded RNG and * an injectable clock, so a recording of (tick timestamps + input events) * replays BIT-IDENTICALLY into a fresh engine with the same seed and scene. * That turns "does my game still work?" into an assertion an agent can run * headlessly: record once while playing, replay forever as a regression. * * const rec = startRecording(engine); // play the game... * const replayJson = rec.stop(); // ...save this JSON * * const fresh = new Engine({ seed: SAME_SEED }); // same seed + scene JSON * fresh.setScene(loadScene(sceneJson)); * replay(fresh, replayJson); // now assert game state * * Caveats (documented, by design): game code must use `engine.rng` (not * Math.random) and dt/engine.time (not Date.now) — the engine's own rules. * Gamepad polling is not recorded (bind pads to actions; actions replay). */ /** [tickIndex, methodName, ...primitive args] */ type ReplayEvent = [number, string, ...(string | number | boolean)[]]; interface ReplayJson { /** File-format marker for forward compatibility. */ replay: 1; /** Tick timestamps (ms) exactly as the loop delivered them. */ /** * One entry per recorded FRAME: the `nowMs` it was ticked with, or `null` for * a fixed `step()`. Older recordings are all numbers and still replay. */ ticks: Array; events: ReplayEvent[]; /** * WHICH GAME this is a recording of — optional, and stamped by the tools. * * `.incanto/playtest` is keyed only by `(outcome, seed)` and never cleaned, so * the second playtest in a project either left a stale replay of a scene that * no longer exists beside the current one, or silently overwrote another * scene's. Nothing downstream could tell: `incanto-play --commands` accepted * any recording against any scene and reported success. * * Absent on an older recording, which still replays — this identifies, it * does not gate. */ scene?: { /** The scene file this was recorded against, as the tool was given it. */path?: string; /** The scene's own `name`, which survives the file being moved. */ name?: string; /** The engine that recorded it, so a replay across a format change reads odd. */ engine?: string; }; } interface Recorder { /** Detach the taps and return the recording. */ stop(): ReplayJson; /** * The tape SO FAR, without ending it. * * `stop()` was the only way to read a recording, so reading it ended it — and * a game that rewinds and keeps playing had to start a second recording whose * tick indices restart at zero, then stitch the two together itself. */ take(): ReplayJson; /** * Drop everything after tick `n` and keep recording from there — the tape * half of a REWIND. * * Returns the recording up to the cut, which is what `replay()` re-runs into * a freshly loaded scene: deterministic replay reproduces the world bit for * bit, physics included, so a rewind needs no snapshot at all. Clamped to the * tape's own ends. */ rewind(n: number): ReplayJson; } /** * Tap the engine's input entry points and tick clock. Recording starts at * the CURRENT moment — start it before the first tick for full-run replays. */ declare function startRecording(engine: Engine): Recorder; /** * Drive a fresh engine through a recording: for each recorded tick, feed * that tick's input events first, then tick with the recorded timestamp. * `onTick(index)` lets tests assert mid-run state. */ declare function replay(engine: Engine, recording: ReplayJson, opts?: { onTick?: (index: number) => void; until?: number; }): void; //#endregion export { startRecording as a, IncantoErrorDetails as c, replay as i, auditScene as l, ReplayEvent as n, IncantoError as o, ReplayJson as r, IncantoErrorCode as s, Recorder as t, instancedScenes as u };