import { c as showBootFailure, i as ParticleSim, o as ParticleView, r as SpatialPose, s as isWebGLAvailable } from "./audio-player-BRo2uvG6.js"; import { P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, jt as Node, n as BehaviorCtor, w as GameStats, x as EngineOptions } from "./behavior-B_245qRy.js"; import { t as DiagnosticSink } from "./diagnostics-Cu85N3tL.js"; import { a as SceneJson, s as JsonObject } from "./rng-BsXZg3D6.js"; import { t as EditorSwitchOptions } from "./editor-switch-Bzt0GzVp.js"; import { r as FrameStats } from "./frame-report-DNxDAb1w.js"; import { t as LoadSceneOptions } from "./loader-CbkVdXL8.js"; import { n as AnimationEntry } from "./sprite-animation-CMr6f1K2.js"; import { Group, Mesh, Object3D, Scene, Texture } from "three"; import * as RapierNs from "@dimforge/rapier2d-compat"; //#region src/2d/assets.d.ts type AssetStatus = "loading" | "ready" | "error"; interface TextureLoadCallbacks { onLoad?: () => void; onError?: (error: unknown) => void; } interface SheetInfo { texture: Texture; frameWidth: number; frameHeight: number; } /** * String-keyed asset registry for the 2D layer (Phaser's loader model). * * Scene JSON declares assets under stable keys; node props reference them as * `"$key"`. The texture loader is injectable so tests run headless. */ declare class AssetStore2D { private readonly engine; private readonly entries; private readonly loader; /** * `engine` is optional so a bare store still constructs in tests. Given one, * a texture that fails to load lands in `engine.log` — where the overlay and * `runScript()` can see it — instead of only in the browser console. */ constructor(loader?: (url: string, callbacks?: TextureLoadCallbacks) => Texture, engine?: DiagnosticSink | null); /** Load (or extend with) a scene's `assets` declarations. Hard-validates each entry. */ load(assets: Record): void; /** * Every declared asset that FAILED to load, `$ref` and url included. * * The 3D store has answered this since 0.32.0 — the 2D one only tracked a * per-key status, which the console printed and nothing else could read. A * 404'd texture is invisible, and "invisible" is the one symptom an author * cannot tell apart from "I put it in the wrong place". */ errors(): Array<{ ref: string; url: string; error: string; }>; /** Load status of a declared asset KEY (no '$'), or undefined if unknown. */ status(key: string): AssetStatus | undefined; getTexture(ref: string): Texture; /** * Hand the GPU back. * * `AssetStore3D` has had this since it was written and the 2D twin never got * one, so a game that swapped scenes or unmounted kept every texture it had * ever decoded. `Renderer2D.dispose` walked its scene graph and disposed * `material.map` instead — which frees only what is CURRENTLY drawn (a sheet * loaded and never used stayed resident), and disposes the STORE'S texture * out from under a caller who injected their own store. */ dispose(): void; getSheet(ref: string): SheetInfo; private resolve; } //#endregion //#region src/2d/nodes/node-2d.d.ts /** * Base of every 2D node. Convention (Phaser/Godot prior): * **1 unit = 1 px, +y DOWN, (0,0) top-left, positive rotation = clockwise.** * Internally mapped onto three's y-up space by negating y and the z-spin. */ declare class Node2D extends Node { static override readonly typeName: string; static readonly props: PropSchema; /** Legacy alias: old 2D scenes (and `zIndex`-trained agents) keep loading — * `zIndex` resolves to `renderOrder` at load. The schema exposes only * `renderOrder` so 2D and 3D share ONE render-order name. */ static readonly propAliases: Record; /** Pixels, y-down. */ position: number[]; /** Degrees, clockwise. */ rotation: number; /** Frozen after first sync — see Node3D.static (same semantics in 2D). */ static: boolean; scale: number[]; /** Draw order among 2D drawables (higher = on top); matches 3D `renderOrder`. */ renderOrder: number; /** Named draw-order band; `renderOrder` is the fine offset inside it. */ orderGroup: string; /** What three actually sorts by: band base + renderOrder. */ get effectiveRenderOrder(): number; visible: boolean; private _object2D; /** @internal The backing three object (lazily created). */ _ensureObject2D(): Object3D; /** @internal Override point. */ protected _createObject2D(): Object3D; /** @internal Push JSON props onto the backing object. Called every frame. */ /** * Has this node produced the thing `static: true` is about to freeze? * * `static` latches after the first sync, and a textured node's first sync * happens BEFORE its texture has decoded — `TextureLoader.load()` returns a * `Texture` with `image === undefined` and fills it asynchronously, while * `Renderer2D.render()` calls `assets.load()` and `syncTree2D()` in the same * frame. So the state frozen was `visible = false`, forever. Measured on two * identical sprites differing only in `static`: * * ``` * frame 1 Backdrop(static) visible: false Control visible: false * 600 frames later Backdrop(static) visible: false width 1 * Control visible: true width 64 * ``` * * Nothing throws: the texture loaded fine, so `assetErrors()` is empty, * `stats().errors` is 0, and `framing` reads props rather than pixels and * still calls the node on-screen. Overridden by the nodes that wait on an * image; a node with nothing to wait for is ready by definition. */ _staticReady(_assets: AssetStore2D | null): boolean; _syncObject2D(_assets: AssetStore2D | null): void; override free(): void; } //#endregion //#region src/2d/nodes/bodies-2d.d.ts /** * Shared base for physics-backed 2D nodes. Colliders are NODE PROPS: * `{shape:'rect', size:[w,h]}` · `{shape:'circle', radius}` · * `{shape:'capsule', radius, height}` — never child shape nodes. */ declare class PhysicsBody2D extends Node2D { static override readonly props: PropSchema; /** The unified collision model: every collider participant can emit these. */ static override readonly signals: readonly string[]; private _collider; /** @internal Physics resync flag — raised when `collider` is REPLACED. */ _colliderChanged: boolean; private _enabled; /** @internal Physics resync flag — raised when `enabled` is written. */ _enabledChanged: boolean; /** * Collider shape prop. Change it by REPLACING the object * (`body.collider = { shape: 'box', size: [...] }`) — physics rebuilds the * body on replacement; in-place mutation of the old object is not watched. */ get collider(): JsonObject; set collider(value: JsonObject); /** See the `enabled` prop. Writing it arms or disarms the existing collider. */ get enabled(): boolean; set enabled(value: boolean); /** Loader hook: bad collider shapes fail at LOAD, not at physics start. */ static validateJson(node: Node): void; /** @internal Set by Physics2D when the body is created. */ _physics: Physics2D | null; } /** * Immovable collider (ground, walls, platforms). * * It also carries the SURFACE it is: `friction` and `restitution` used to * live on `RigidBody2D` alone, so a trampoline, an ice patch or a sticky * landing could not be written in scene JSON, and Rapier's AVERAGE rule * halved whatever a ball brought to a plain floor. A static body's authored * value WINS the pair — restitution above 0 combines by Max, friction below * the default by Min and above it by Max — so the pad decides, whatever lands * on it. Left at the defaults, nothing changes for any existing scene. */ declare class StaticBody2D extends PhysicsBody2D { static override readonly typeName: string; static override readonly props: PropSchema; /** 0.5 plain · 0 ice (slides on and on) · 1+ glue. Away from 0.5 it wins the pair. */ friction: number; /** 0 dead · 1 trampoline. Above 0 it wins the pair: a dead ball still bounces. */ restitution: number; } /** * Sensor volume. Emits `triggerEnter(other)` / `triggerExit(other)` — the * unified collision model (solid bodies emit the same signals on contact). */ declare class Area2D extends PhysicsBody2D { static override readonly typeName: string; /** * Who is inside RIGHT NOW — the standing answer behind the enter/exit pair. * See `Area3D.overlapping`. Empty in a scene with no physics world. */ overlapping(group?: string | undefined): PhysicsBody2D[]; } /** Dynamic simulated body. */ declare class RigidBody2D extends PhysicsBody2D { static override readonly typeName: string; static override readonly props: PropSchema; mass: number; gravityScale: number; fixedRotation: boolean; friction: number; restitution: number; /** Drag, per second: velocity decays by `e^(-linearDamping·t)`. `0` = none. */ linearDamping: number; /** Rolling resistance, per second, on the spin — see `RigidBody3D.angularDamping`. */ angularDamping: number; /** px/s, y-down. Read back every step; write to launch. */ linearVelocity: number[]; /** * rad/s about the z axis — the same sign as `rotation`, which is CLOCKWISE * in this y-down space. Read back every step; write to spin. * * `RigidBody3D` got its `angularVelocity` because "a puzzle builder could * neither launch a spinning body nor read how fast a lever was swinging — * only its angle, differenced by hand". Same sentence, same defect, other * adapter: 2D had `linearVelocity` and no counterpart. `fixedRotation` pins * it at zero. */ angularVelocity: number; /** * The other name for `linearVelocity`, because it is the name every skill, * every sibling node and every other engine uses. * * `body.velocity = [0, -400]` is what `incanto-physics-and-input.md` taught * for years. On a plain TS class that assignment SUCCEEDS — it just writes a * stray field nothing reads — so a jump that never happened produced no * error, no warning and nothing to search for. `CharacterBody2D.velocity` * next to `RigidBody2D.linearVelocity` is the kind of near-miss a builder * cannot be expected to notice. * * Scene JSON still names the prop `linearVelocity` and rejects `velocity` * loudly, with the valid keys listed — a hard error corrects itself. */ get velocity(): number[]; set velocity(v: number[]); /** * Impulse in px·kg/s (y-down) — the 3D twin has had this since it shipped, * and calling it here was a TypeError. */ applyImpulse(impulse: [number, number]): void; } /** * Kinematic body on Rapier's character controller. Gravity is NOT applied * automatically (Godot semantics) — integrate `velocity` yourself, then call * `moveAndSlide()` from `fixedUpdate`. */ declare class CharacterBody2D extends PhysicsBody2D { static override readonly typeName: string; static override readonly props: PropSchema; /** See CharacterBody3D.pushes — a piston or a sweeping arm pushes; a character stops. */ pushes: number; /** * Renamed to match the 3D body, where `snapToGround` had to give the name * back to `Node3D`'s PLACEMENT prop. 2D has no placement prop, so nothing is * ambiguous here and old scenes keep loading unchanged. */ static readonly propAliases: Record; /** px/s, y-down (up = -y). */ velocity: number[]; /** Keep contact on slopes/steps while not moving upward. */ stickToGround: boolean; /** Max climbable slope angle. */ slopeLimitDeg: number; /** * How high a step this body walks up without jumping, in PIXELS. * * Rapier's character controller does not autostep unless it is asked, and in * 2D nothing asked: a ledge of ANY height — measured, ONE pixel — stopped a * walking body dead, while the mirrored `CharacterBody3D` has cleared 0.35 m * by default since 0.63. The default here is that same 0.35 m at the 2D * world's own scale (100 px = 1 m, which is why gravity defaults to 980). * `0` restores the old behaviour. */ stepHeight: number; /** See CharacterBody3D.collideWithCharacters — off, a crowd ignores itself and costs nothing. */ collideWithCharacters: boolean; /** @internal Updated by Physics2D.moveAndSlide. */ _grounded: boolean; moveAndSlide(): void; /** @internal Set by the physics step from the KCC's resolved collisions. */ _onWall: number; /** @internal */ _onCeiling: boolean; /** * Touching a wall, and which side (-1 left, +1 right, 0 neither). * * Wall jump and wall slide are unbuildable without it — not awkward to build, * impossible: `isOnFloor()` was the only surface a character could report, so * even a hand-written behavior had nothing to ask. */ isOnWall(): boolean; /** -1 the wall is to the left, +1 to the right, 0 no wall. */ wallSide(): number; /** Head hit something — cut the jump short, the way every platformer does. */ isOnCeiling(): boolean; isOnFloor(): boolean; } //#endregion //#region src/2d/physics/physics-2d.d.ts type Rapier = typeof RapierNs; interface Physics2DOptions { /** px/s², y-down. Default: scene `physics.gravity`, else [0, 980]. */ gravity?: [number, number]; /** * Build the colliders and keep them on the tree, but never advance the world * (default: `true`, i.e. simulate). See {@link Physics3DOptions.simulate} — * the scene editor uses it to draw the REAL colliders without running them. */ simulate?: boolean; } /** * Enable 2D physics for an engine. Dynamically imports Rapier (compat build, * wasm inlined) so games without physics never pay its bundle cost. */ declare function enablePhysics2D(engine: Engine, opts?: Physics2DOptions): Promise; /** * Per-engine 2D physics world (y-down pixel space, consistent with Node2D). * Each fixedUpdate: sync tree → step world → write back → drain trigger events. */ declare class Physics2D { private readonly R; readonly engine: Engine; /** Render collider outlines in the GAME view (renderers pick this up). */ debugDraw: boolean; /** Narrow those outlines to one node's subtree (see DebugLineSource). */ debugScope: object | null; readonly dimension = "2d"; private readonly unregisterDebug; private readonly warnedNoCollider; private readonly entries; private readonly byColliderHandle; private readonly world; private readonly events; private readonly kcc; private readonly disconnectScene; private readonly disconnect; private lastDt; private readonly optsGravity; private lastScene; constructor(R: Rapier, engine: Engine, opts?: Physics2DOptions); /** @internal Driven by engine.fixedUpdated. */ /** Whether the solver runs at all. False = collider outlines only. */ private readonly simulate; step(dt: number): void; /** * May `candidate` carry `rider`? * * The rule used to be "never a CharacterBody2D", which is the whole CLASS — * and a `CharacterBody2D` animated by POSITION WRITES is the documented * moving-platform path (`Oscillate`, `MoveTo`, `PathFollow`, or a behaviour of * your own). `incanto-physics-and-input.md` promises twice that a character * standing on ANY moving body is carried; it was not, and the rider reported * `isOnFloor() === true` the whole time while sliding off in world space. * * What the guard was really for is a CYCLE: two characters standing on each * other would carry each other forever. So the test is the rider's own * carrier chain, not the type — a platform that is not (transitively) riding * you can carry you. */ private canCarry; /** @internal Called by CharacterBody2D.moveAndSlide (during tree fixedUpdate). */ moveAndSlide(node: CharacterBody2D): void; /** Rapier debug segments, scaled back to pixels. Null while debugDraw is off. */ debugLines(): Float32Array | null; /** * Only the selected node's colliders (meters — the caller scales to pixels). * Rapier draws the whole world or nothing, so a chosen few are outlined from * the same shape data the solver holds. 2D colliders are rect/circle/capsule, * which is the whole switch. */ private scopedLines; /** Create newly-arrived joints; tear down departed ones. */ private syncJoints; /** Who is currently inside each sensor/body, maintained from the event drain. */ private readonly overlaps; private trackOverlap; /** * Forget every overlap whose node has left the tree. * * The lazy drop inside `overlapping()` only cleans the SETS of a key someone * asks about, and only when they ask. This clears the KEYS, which is where the * dead nodes were pinned. */ /** Drop one node from the overlap map, both as a key and as a member. */ private forgetOverlaps; /** Every overlap `node` is in ends now — the exits Rapier does not send for a disabled collider. */ private releaseOverlaps; /** Every body the armed sensor is inside of RIGHT NOW enters it — the starts Rapier does not send for a re-enabled collider. */ private armOverlaps; private pruneOverlaps; /** The bodies currently overlapping `node`. Freed nodes are dropped on read. */ overlapping(node: PhysicsBody2D): PhysicsBody2D[]; /** * Mass the solver actually uses (collider-derived unless overridden). * * The 3D twin has had this since `CharacterController3D` needed it to size an * impulse; 2D had no way to ask, so a 2D game scaling a push by mass had to * read the AUTHORED `mass` prop and hope it was the number Rapier settled on. */ massOf(node: PhysicsBody2D): number; /** The solver's rotation of a body, in degrees (the node's own unit). */ rotationOf(node: PhysicsBody2D): number; /** * Current solver velocity in px/s (fresher than the node prop mid-step). * * `linearVelocity` is written back once per step, so a behavior reading it * from inside `fixedUpdate` — after a collision, before the write-back — sees * the value from BEFORE the impact. That is exactly when a game wants to know * how hard it hit something. */ velocityOf(node: PhysicsBody2D): [number, number]; /** * A world-space impulse in px.kg/s (y-down) — the 2D twin of * `Physics3D.applyImpulse`, which the physics skill has documented for both * adapters while only one of them had it. */ applyImpulse(node: PhysicsBody2D, impulse: [number, number]): void; /** * World-space raycast in PIXELS (y-down); `dir` may be any length (it is * normalized), so `distance` and `maxLen` are pixels. `exclude` skips that body — a * shooter probing from inside its own collider needs it. Sensors (Area2D) * never block rays. Returns distance (px), the surface normal, and the hit * body — or null. */ castRay(origin: [number, number], dir: [number, number], maxLen: number, exclude?: PhysicsBody2D, opts?: { staticOnly?: boolean; }): { distance: number; normal: [number, number]; node: PhysicsBody2D | null; point: [number, number]; } | null; /** * Sweep a CIRCLE of `radius` from `origin` along `dir` — a thick ray, in * pixels. The 2D twin of `Physics3D.castSphere`. * * 3D has had this since the camera spring arm needed it and 2D never got it, * so the shared `PhysicsQuery` interface declares it OPTIONAL and every 2D * game that wanted a probe with THICKNESS — a shot that must not thread a * one-pixel gap between two tiles, a ledge feeler, a camera that should not * clip a wall it skims — had to fire several thin rays and hope. * * Same filters as `castRay`: sensors never hit, `staticOnly` ignores dynamic * and kinematic bodies, `dir` may be any length. Returns the travel distance * of the circle's CENTRE, or null when the sweep is clear. */ castSphere(origin: [number, number], dir: [number, number], radius: number, maxLen: number, exclude?: PhysicsBody2D, opts?: { staticOnly?: boolean; }): { distance: number; node: PhysicsBody2D | null; } | null; dispose(): void; private lastStructure; private lastRoot; private readonly jointSet; private readonly joints; /** The two bodies each live joint ties, so a departed joint can be unlinked. */ private readonly jointBodies; /** What a pin's limits/motor were last pushed as, so a write re-applies. */ private readonly hingeState; /** Joints refused at creation (bodies that disagree about the axis) — said once. */ private readonly refusedJoints; /** A rope's or spring's numbers as last made, so a write REMAKES the joint. */ private readonly tetherState; private tetherKey; /** * Who is jointed to whom, and whether that pair's contacts are on — see the * 3D twin: a joint's own bodies are not the world to the character * controller (not its obstacle when the contacts are off, never its ground). */ private readonly jointed; private linkJointed; private unlinkJointed; /** * Push a pin's `limits` and motor to the solver — on creation, and again * whenever either changes. Limits and `angle` are this body relative to the * target; Rapier measures the other way round, hence the flip. */ private applyHinge; private readonly bodySet; /** * Move every body to its node's current transform — outline mode only. * * Computed exactly as `ensureEntry` computes a body's first pose, so an * outline drawn in the editor is the outline the running game draws. */ private poseFromTree; private syncBodies; private ensureEntry; } //#endregion //#region src/2d/create-game.d.ts /** Minimal keyboard event source (window, or anything shaped like it). */ interface KeyboardTarget { addEventListener: (t: string, cb: (e: KeyboardEvent) => void) => void; removeEventListener: (t: string, cb: (e: KeyboardEvent) => void) => void; } interface CreateGame2DOptions { canvas: HTMLCanvasElement; /** Scene JSON (cloned internally — the same object can boot many games). */ scene: unknown; /** Behavior classes to register (hot-replace tolerant). */ behaviors?: Record; /** * Auto-register the built-in `incanto/gameplay` behaviors (Health, Pickup, * ScoreKeeper, …) — default true (batteries-included). They register BEFORE * `behaviors`, so a same-named user behavior always wins. Set false to ship * none of them. */ gameplay?: boolean; /** * Keyboard source (default: window when available). `false` disables. * Bound keys preventDefault; editable elements are ignored (see InputMap). */ keyboard?: KeyboardTarget | false; /** 'auto' (default): enable Rapier iff the tree has physics bodies. */ physics?: "auto" | boolean; /** * On-screen touch controls for the scene's `"touch"` input declarations: * 'auto' (default) shows them on coarse-pointer devices, true forces them, * false disables. They overlay `touchContainer` (default: the canvas's * parent element — give it position: relative). */ /** * Attach pointer input (mouse, finger or pen) to the canvas: positions for * `input.pointerPosition()`, buttons for `input.mousePressed()`. * `{ lockOnClick: true }` captures the cursor — rare in 2D, so it is off. * * Omitted, it is AUTO: attached when the scene contains a `Clickable`, the * same way `physics: 'auto'` turns Rapier on for a scene with bodies. It used * to default OFF, so a board of clickable tiles did nothing when clicked — * with no error, a clean `incanto-check` and a clean audit, while the skill * that introduces the behaviour says it "needs no code at all". Pass `true` * when a behaviour of your own reads `pointerPosition()`; nothing here can * see that. */ pointer?: boolean | { lockOnClick?: boolean; }; touch?: "auto" | boolean; /** * Leave the browser's touch gestures (scroll/zoom/pull-to-refresh) on the * canvas. Default false: the GAME owns them, or a drag on a phone scrolls * the page instead of playing. */ pageGestures?: boolean; touchContainer?: HTMLElement; /** @internal Test seam — replaces `document` for the touch overlay. */ _touchDoc?: { createElement(tag: string): HTMLElement; }; /** * Mount the runtime debug overlay (`incanto/debug` — explorer/inspector/ * logs). Default: false (off), and there is NO URL/query toggle — a deployed * build must never be switchable on by end users. Opt in via `debug`, gated * to development, e.g. `debug: import.meta.env.VITE_INCANTO_DEBUG === '1'`. */ debug?: boolean; /** * Start the frame loop as soon as the game is built (default `true`). * `false` hands back a game that has not run a frame; call * `game.engine.start()` once everything that must exist BEFORE the first * frame does — a `NetworkManager`, whose join takes a moment during which * every behaviour would otherwise run the game offline. See the 3D twin. */ autoStart?: boolean; /** * LIVE EDITING: the ☰ debug menu gains "edit this scene", which turns this * page into the scene editor — same window, no reload — and the editor's play * button turns it back into the game. * * DEFAULTS TO `debug`, costs nothing until clicked (the editor is a lazy * chunk), and is never reachable without it — the ☰ menu is its only * entrance. `false` opts out; `{ open }` loads the editor yourself (apps that * alias incanto to source); `{ save }` makes its save button write your file. */ editor?: boolean | EditorSwitchOptions; /** @internal Test seam — replaces `document` for the debug overlay. */ _debugDoc?: { createElement(tag: string): HTMLElement; }; seed?: number; /** * Namespace this game's persisted settings — volume, language, quality tier. * * They are keyed `incanto::` in the browser's storage and * defaulted to `settings` for everyone, so two Incanto games on one domain * shared one options screen. Measured: game A's player sets `renderScale * 0.25, muted, quality low`; game B, whose source never mentions settings, * boots at a 480x232 drawing buffer and silent. * * `SaveSlots(namespace)` already takes one for exactly this reason. */ settings?: EngineOptions["settings"]; fixedHz?: number; /** Renderer extras (pixelRatio, antialias, custom asset store). */ pixelRatio?: number; antialias?: boolean; /** * Keep the drawing buffer readable so the canvas can be screenshot. * * WebGL clears it the instant a frame composites, so `toDataURL()` — and * every screenshot built on it — is BLANK without this. Off by default: it * costs a buffer copy per frame and only a run that means to look at the * output should pay for it. `environment.rendering.preserveDrawingBuffer` * does the same from the scene. * * The 2D renderer has always honoured the scene key; only the boot-site * override was 3D-only, so a 2D game whose harness wanted a screenshot had to * edit its scene file to get one. */ preserveDrawingBuffer?: boolean; resolveScene?: LoadSceneOptions["resolveScene"]; /** @internal Test seam — replaces the Renderer2D construction. */ _rendererFactory?: (engine: Engine, canvas: HTMLCanvasElement) => GameRenderer; /** @internal Test seam — replaces the rAF scheduler. */ _scheduler?: Scheduler; } /** What createGame needs from a renderer (stats is optional for test stubs). */ /** What a renderer must do for the engine. The test seam supplies this much. */ interface GameRenderer { dispose(): void; stats?(): RendererStats; } /** * What a GAME can ask its renderer for. * * The two coordinate conversions live here rather than only on `Renderer2D`, * because `game.renderer` is what a consumer holds: a tap needs screen→world * and a pinned DOM element needs world→screen, both are documented in the * skills, and neither typechecked through the narrow interface. Found by * following my own documentation in a fresh project. */ interface Game2DRenderer extends GameRenderer { /** Canvas px → world px, through the design viewport and the camera. */ worldFromScreen(sx: number, sy: number): { x: number; y: number; }; /** World px → canvas px (pin DOM to a node). */ screenFromWorld(wx: number, wy: number): { x: number; y: number; }; } interface Game2D { engine: Engine; /** * The LIVE scene — a getter over `engine.scene`, not a snapshot. * * This used to be the scene captured at boot. `Engine.setScene` calls * `this._scene?.root.free()`, so after any swap the old handle was not merely * stale, it was EMPTIED — and a single-scene game reaches that through * `flow.restart()`, which `incanto-hud.md` wires to a Start button. Measured: * * before swap game.scene.root.children = 2 engine.scene = 2 * after swap game.scene.root.children = 0 engine.scene = 2 * game.scene.root.getNode('Flow') -> No node at 'Flow'. Children here: []. * * `game.engine.scene` always worked and three skills use it, so there were * two handles with one name and no note saying which was which. */ get scene(): Scene$1; /** * What the last drawn frame actually looks like, as numbers. * * The 3D game has answered this since 0.47 and a 2D game could not, so * `incanto-frame` and `incanto-verify`'s `draws` rung were permanently * unmeasured for half the engine's games. */ frame(opts?: { grid?: [number, number]; }): Promise; /** * The AUTHORED scene this game booted from — the JSON, not the live tree. * A running tree has moved; the editor gets what was written down. */ sourceJson: SceneJson; renderer: Game2DRenderer; physics: Physics2D | null; /** * The node under a screen pixel, or null. The raycast the renderer already * does — reachable now without holding the renderer. */ pick(x: number, y: number): Node | null; /** * Assets that did not arrive — a texture that 404'd, a sound that cannot be * played — with the ref and the reason. * * The verify loop names this as one of its four signals, and 2D never had it: * a 2D game could not ask the question at all. */ assetErrors(): Array<{ ref: string; url: string; error: string; }>; /** Engine + renderer perf counters in one read (fps/nodes/triangles/…). */ stats(): GameStats; /** * Turn this page into the scene editor — what the ☰ debug menu's "edit this * scene" calls. The game is disposed on the way out and rebooted when the * editor hands it back, so THIS handle is dead afterwards. `null` when the * game was booted with `editor: false`. */ openEditor: (() => Promise) | null; /** Tear the whole game down (renderer, loop, scene, listeners). */ dispose(): void; } /** * The one-call boot every game was hand-rolling: register → load (engine * attached, so onReady can use this.rng/this.log) → physics (auto-detected) → * input → renderer → start. The returned `dispose()` is the one-call * teardown for SPA unmounts. Audio unlocks on the first user gesture. */ declare function createGame2D(opts: CreateGame2DOptions): Promise; //#endregion //#region src/2d/library-sprite.d.ts interface SpriteFromLibraryResult { /** Scene `assets{}` entry — put it under your chosen key. */ asset: JsonObject; /** AnimatedSprite2D props (sheet/autoplay/animations) — ready to paste. */ props: JsonObject; } /** * Turn a library sprite-animation JSON into a scene-ready spritesheet asset * declaration + AnimatedSprite2D props. `autoplay` defaults to the first * LOOPING animation (idle-like), falling back to the first one. * * The animation map also gets ALIASES for the movement states a character * controller emits but the sheet does not name (`run` → `move`, `fall` → * `idle`), so the converted props and the documented * `movementStateChanged → play` wiring work together out of the box. */ declare function spriteFromLibraryMeta(meta: unknown, opts: { url: string; assetKey: string; autoplay?: string; }): SpriteFromLibraryResult; //#endregion //#region src/2d/nodes/sprite-2d.d.ts interface ResolvedSpriteTexture { texture: Texture; width: number; height: number; } /** * A textured quad. The drawable is an inner mesh child of the node's backing * object, so node children never inherit the texture-size scaling. */ declare class Sprite2D extends Node2D { static override readonly typeName: string; static override readonly props: PropSchema; /** `'$assetKey'` texture reference. Empty = hidden. */ texture: string; /** [0,0] = top-left on the origin … [1,1] = bottom-right on the origin. */ anchor: number[]; flipX: boolean; flipY: boolean; tint: string; opacity: number; private quadMesh; /** @internal The drawable quad (lazily created, attached under the backing object). */ _quad(): Mesh; protected override _createObject2D(): Object3D; /** Override point: AnimatedSprite2D substitutes its frame window here. */ protected resolveTexture(assets: AssetStore2D | null): ResolvedSpriteTexture | null; override _staticReady(assets: AssetStore2D | null): boolean; override _syncObject2D(assets: AssetStore2D | null): void; /** * The material is PER NODE and nothing freed it. * * The geometry is shared for the process (`UNIT_PLANE`, on purpose), but * every sprite makes its own `MeshBasicMaterial` — and a 2D game that spawns * and frees sprites, which is every 2D game, leaked one per node forever. * The TEXTURE is not touched: it belongs to the asset store and is shared by * every node that names the same key. * * `AnimatedSprite2D` extends this, so it is covered by the same override. */ override free(): void; } //#endregion //#region src/2d/nodes/animated-sprite-2d.d.ts /** * Spritesheet animation: pure-JSON animation map, frame selection via a UV * window on the node's own texture clone. Frames advance in `update(dt)` — * deterministic and headless-testable. */ declare class AnimatedSprite2D extends Sprite2D { static override readonly typeName: string; static override readonly signals: readonly string[]; static override readonly props: PropSchema; /** `'$assetKey'` of a spritesheet asset. */ sheet: string; animations: Record; autoplay: string; playing: boolean; /** Absolute frame index within the sheet. */ currentFrame: number; currentAnimation: string; private frameList; private frameIndex; private frameTime; private currentDef; private ownTexture; private loadedSheetRef; /** * `currentAnimation` reports the clip that is RUNNING, not the name asked * for: an alias is a rename, and a state machine that reads back `fall` while * the idle frames play is describing something that is not happening. */ play(name: string): void; stop(): void; override onReady(): void; override update(dt: number): void; /** Sheets already reported, so a bad grid says so ONCE, not every frame. */ private gridChecked; /** * Tell the author their grid does not fit, the first time it is drawn. * * Both halves of this were silent: a frame size that does not divide the * sheet slices every row further off centre, and an animation naming a frame * past the end of the grid draws whatever is at the wrong end of the image. */ private checkGrid; protected override resolveTexture(assets: AssetStore2D | null): ResolvedSpriteTexture | null; } //#endregion //#region src/2d/nodes/camera-2d.d.ts /** * 2D camera: its `position` is the view CENTER. `follow` tracks a node path * with exponential smoothing; `limits [minX,minY,maxX,maxY]` clamp the view * rect inside a world region (applied by the renderer via `clampedCenter`). */ declare class Camera2D extends Node2D { static override readonly typeName: string; static override readonly props: PropSchema; /** NodePath of a Node2D to track. */ follow: string; /** 0 = snap; 0.85–0.95 = smooth chase (per-frame retention at 60fps). */ smoothing: number; zoom: number; /** [minX, minY, maxX, maxY] world px; empty = unlimited. */ limits: number[]; current: boolean; /** * Make THIS the scene's camera and no other: the renderer takes the first * camera whose `current` is true in tree order, so setting your own without * clearing the gameplay camera's changed nothing visible. A handler a * connection can name — `triggerEnter → DoorCam.makeCurrent`. */ makeCurrent(blendSeconds?: number | undefined): void; blendSeconds: number; /** @internal `makeCurrent(seconds)` — read once by the renderer. */ _blendRequest: number | undefined; /** `zoom` clamped to a positive floor — 0/negative zoom must never produce NaN views. */ get effectiveZoom(): number; /** The `follow` path already reported as untrackable — cleared by a new one. */ private warnedFollow; override update(dt: number): void; /** View center after clamping the (vw×vh)/zoom view rect inside `limits`. */ /** * The view centre the renderer draws around, in WORLD space. * * It used to read `this.position` raw — the LOCAL prop — while every other * 2D node gets its ancestors composed for free by the three scene graph. So a * camera parented to the player, the Godot/Phaser idiom the authoring skill * explicitly permits, framed the world origin: * * ``` * player world position : [1400, 900] * renderer view centre : {"x":0,"y":0} * framing : camera /Level/Player/Cam centred [1400, 900] * 1 in view, 0 outside it * ``` * * `framing` composes full world matrices, so the instrument the skills tell * you to trust certified a view the renderer never drew. */ clampedCenter(vw: number, vh: number): { x: number; y: number; }; } //#endregion //#region src/2d/nodes/character-controller-2d.d.ts declare class CharacterController2D extends Node { static override readonly typeName: string; static readonly props: PropSchema; static readonly signals: readonly string[]; mode: string; maxSpeed: number; /** Pixels (platformer mode). */ jumpHeight: number; moveAction: string; jumpAction: string; coyoteSeconds: number; jumpBufferSeconds: number; jumpCutMultiplier: number; maxJumps: number; dashSpeed: number; dashSeconds: number; dashAction: string; crouchHeight: number; crouchSpeedMultiplier: number; crouchAction: string; /** Read-only: shorter right now — by the key, or by a ceiling. */ crouching: boolean; /** The collider height to stand back up to. */ private standingHeight; wallSlideSpeed: number; wallJumpImpulse: number[]; /** What the character is doing — drive `AnimatedSprite2D.play(state)` off it. */ state: "idle" | "run" | "jump" | "fall" | "wallSlide" | "dash" | "crouch" | "sneak"; private coyoteLeft; private bufferLeft; private jumpsUsed; private dashLeft; private dashDir; private rising; override onReady(): void; override fixedUpdate(dt: number): void; /** * Emits `movementStateChanged(state)` on a change — the 3D sibling has had * this since it shipped, and it is what drives `AnimatedSprite2D.play(...)` * from scene JSON instead of a behavior polling velocities. */ private setState; /** Actions already reported missing, so a per-frame read says it once. */ private readonly reportedActions; /** * Read an input action this controller is allowed to be missing. * * The 3D controller has done this since `optional-action.ts` was written — * whose own docstring says "`CharacterController3D` and `GameFlow` wrap every * input read" — and the 2D one did not. So a top-down 2D game, which is the * first thing anyone builds with this node and has no jump key, declared * `move` and got: * * ``` * [incanto] /Game/Player/Ctl (CharacterController2D) threw in fixedUpdate — * this node is now SKIPPED. IncantoError: Unknown input action 'jump'. * Declared actions: [move]. * ``` * * SKIPPED: the character could not move either, and every tool reported the * game as `✗ error in 1/1` with the reason only in the log. Tolerated because * a game with no jump key is the ordinary case; reported because otherwise a * TYPO in `"jumpAction"` looks exactly like a deliberate omission. */ private readAction; } //#endregion //#region src/2d/nodes/color-rect-2d.d.ts /** * A solid-colored rectangle — no texture, no asset file. The prototyping * workhorse: paddles, walls, platforms, flashes, fade overlays. Swap in a * Sprite2D when art arrives. */ declare class ColorRect2D extends Node2D { static override readonly typeName: string; static override readonly props: PropSchema; /** [width, height] in world px. */ size: number[]; color: string; opacity: number; /** [0,0] = top-left on the origin … [1,1] = bottom-right on the origin. */ anchor: number[]; private quadMesh; /** @internal The drawable quad (lazily created under the backing object). */ _quad(): Mesh; override _syncObject2D(assets: AssetStore2D | null): void; /** * The material is PER NODE and nothing freed it. * * The geometry is shared for the process (`UNIT_PLANE`, on purpose — a * `THREE.Sprite` cannot be instanced and this is the discipline that replaces * it), but every rect makes its own `MeshBasicMaterial`, and a 2D game that * spawns and frees — bullets, coins, damage flashes, a level swap — leaks one * GPU program's worth of state per node, forever. * * Four 3D nodes had exactly this and were fixed. These two 2D siblings make a * material on the same line of the same kind of `_quad()` helper, and were * not on that list. */ override free(): void; } //#endregion //#region src/2d/nodes/joint-2d.d.ts type JointType2D = "fixed" | "revolute" | "prismatic" | "rope" | "spring"; /** * A physics joint linking its PARENT body to `target` (a node path to the * other body). Godot-style placement: the joint lives as a child of body A. * * { "name": "Hinge", "type": "Joint2D", * "props": { "type": "revolute", "target": "%Anchor", "anchor": [0, -20] } } * * Types: `fixed` welds the bodies rigidly · `revolute` is a pin/hinge at the * anchors · `rope` caps the anchor distance at `length` (px; 0 = measured at * creation) · `spring` pulls toward `length` with `stiffness`/`damping`. * Anchors are LOCAL pixel offsets on each body. */ declare class Joint2D extends Node2D { static override readonly typeName: string; static override readonly props: PropSchema; type: JointType2D; target: string; anchor: number[]; targetAnchor: number[]; /** * `[min, max]` in degrees for a `revolute` swing; `[]` = no stops. Measured * as `angle` is: this body turned relative to the target, `rotation`'s sign. */ limits: number[]; /** Degrees a second the motor drives the pin at; `0` = no motor (free). */ motorSpeed: number; /** The motor's gain — how quickly it reaches and holds `motorSpeed`. */ motorStrength: number; /** * The pin's current angle in degrees — this body's rotation relative to the * target, written by physics every step. `0` unless `revolute` and simulated. */ angle: number; /** The line of a `prismatic` slide (local to this body). */ axis: number[]; /** * A `prismatic` slide's current travel in px — this body's anchor along * `axis` relative to the target's; `0` where the anchors coincide. */ travel: number; /** rope/spring rest length in px; 0 = measure body distance at creation. */ length: number; stiffness: number; damping: number; /** * Do the two linked bodies still collide with EACH OTHER? * * `null` (default) decides from the joint type, because the two families want * opposite answers: * * - **pivots** (`fixed`, `spherical`, `revolute`) → **off**. A hinge's bodies * overlap at the pivot by construction, so the contact solver fights the * joint and flings them. A puzzle builder's first hinge produced a bar that * spun chaotically forever and threw a 40 kg box thirty metres, and they * nearly filed "spherical joints inject energy": * * ``` * t= 0 Plank r=[0,0,0] Box [ 0.00, 5.50, 2.40] * t=3000 Plank r=[-66,24,64] Box [-29.70, 0.40, -4.91] * ``` * * - **tethers** (`rope`, `spring`) → **on**. These anchor a thing to the world, * and the thing has to rest on what it is tethered to. Defaulting these off * drops a barrel roped to the floor straight through it — measured, in this * repo's own conformance room. * * Set `true`/`false` to say it outright. */ collide: boolean | null; override onEnterTree(): void; /** @internal The two bodies, resolved (throws on a bad target at step time). */ _resolveBodies(): { a: PhysicsBody2D; b: PhysicsBody2D; }; } //#endregion //#region src/2d/nodes/label.d.ts /** * CanvasTexture-backed text (zero deps, WebGL-safe). Re-rasterizes only when * text props change. Headless-safe: without a DOM it simply stays hidden — * all prop/serialization logic still works. */ declare class Label extends Node2D { static override readonly typeName: string; static override readonly props: PropSchema; text: string; fontSize: number; color: string; font: string; /** 'left' | 'center' | 'right' — anchor of the text block on the node origin. */ align: string; /** * 0..1, like `Sprite2D` and `ColorRect2D`. * * Label was the one 2D drawable without it, and the gap showed up through * `FloatAway` — whose own doc names "a 2D `Label`" as a node it works on. * `FloatAway` writes `opacity` when the node has one, so on a Label the * number rose to full height at FULL opacity and then vanished in a single * frame: the pop that behavior exists to avoid. */ opacity: number; private quadMesh; private canvas; private lastKey; /** @internal */ _quad(): Mesh; override _syncObject2D(assets: AssetStore2D | null): void; private rasterize; /** * Hand the canvas and its texture back. * * LABEL_PLANE is shared by every Label in the process and stays; the material * and the CanvasTexture are this node's, and a scene that spawns floating * text left one of each behind per freed node. */ override free(): void; } //#endregion //#region src/2d/nodes/particles-2d.d.ts /** * GPU-instanced particle emitter with predefined looks: set `preset` to * `'fire' | 'smoke' | 'sparks' | 'fireworks' | 'explosion' | 'flash' | * 'lightning' | 'rain' | 'snow' | 'magic'` and every prop snaps to that * look's baseline — any prop you write in the scene overrides it. `rate: 0` * + `burst` makes a one-shot that emits `finished` (queueFree it there). * Simulation is deterministic under the engine seed. */ declare class Particles2D extends Node2D { override orderGroup: string; static override readonly typeName: string; static override readonly signals: readonly string[]; static override readonly props: PropSchema; preset: string; emitting: boolean; /** * Where a particle LIVES once it is born. * * `false` (default) — the emitter's local space: the plume moves with the * node, which is what a torch on a moving platform wants. * * `true` — the world: a particle keeps the place it was born, so dust is left * BEHIND a running player instead of glued to it, and moving a one-shot to * the next explosion (the documented `p.position = …; p.replay()` recipe) * stops dragging the previous one across the level. */ worldSpace: boolean; rate: number; burst: number; lifetime: number[]; speed: number[]; directionDeg: number; spreadDeg: number; gravity: number[]; drag: number; sizeStart: number; sizeEnd: number; colorStart: string; colorEnd: string; /** Per-particle base colours sampled by the particle's stable seed (multi- * colour confetti). Empty → the colorStart→colorEnd ramp is used unchanged. */ paletteColors: string[]; alphaStart: number; alphaEnd: number; /** Per-particle alpha twinkle frequency in Hz (0 = off) — a seed-phased sine * over the particle's age, so each sparkle glitters on its own rhythm. */ shimmer: number; blend: string; maxParticles: number; emitBox: number[]; drift: number[]; /** Loader hook: unknown presets and bad blends fail at LOAD. */ static validateJson(node: Node): void; private sim; private bursted; private finishedEmitted; /** @internal Lazily applies the preset + builds the deterministic sim. */ _ensureSim(): ParticleSim; /** Particles alive this frame — an emitter that is running, told from one * that is not. `framing()` says where it IS; this says whether it is going. */ get aliveCount(): number; /** A snapshot of the live particles — positions are in whichever space * `worldSpace` selects, relative to the emitter. */ particles(): ParticleView[]; /** The emitter's world position last frame, for world-space counter-motion. */ private lastWorld; /** Last frame's `emitting`, so turning one ON is a reportable event. */ private wasEmitting; override update(dt: number): void; /** * Undo the emitter's own motion on everything already alive. * * Ancestor-summed like the physics helpers, so it follows a moving PARENT * (dust under a player) as well as the node's own position. */ private holdWorldSpace; /** What the current mesh was built FOR — capacity and blend, the two things * baked into it. Not the draw count, which changes every frame. */ private meshKey; /** * Hand the GPU back. UNIT_PLANE is shared by every 2D particle system in the * process, so the geometry stays; the per-node material and the instance * buffers inside the InstancedMesh do not. */ override free(): void; /** Tell the engine an effect fired — the visual half of the audio record. */ private report; /** Restart a one-shot (re-burst + allow finished to fire again). */ replay(): void; /** `replay()` asked for this burst, so `emitting: false` does not hold it. */ private armedByReplay; private mesh; /** Parsed `paletteColors`, reused; rebuilt only when the hex list changes. */ private paletteCache; private paletteCacheKey; /** Parse `paletteColors` into a reused `Color[]`, rebuilt only on change; * null when empty so the caller uses the colorStart→colorEnd ramp. */ private refreshPalette; override _syncObject2D(assets: AssetStore2D | null): void; } //#endregion //#region src/2d/nodes/tile-map-2d.d.ts /** * A whole tile level as ONE node — grid render in a single draw call plus * greedy-merged static colliders. Author the level as rows of characters: * * { "name": "Level", "type": "TileMap2D", "props": { * "texture": "$tiles", "tileSize": 32, * "cells": [ * "..................", * "......111.........", * "000000000000000000" * ], * "solid": [0, 1] } } * * `.` / space = empty, digits 0-9 = atlas tile index, other chars map through * `legend` (e.g. `{"G": 12}`). The atlas is read left-to-right, top-to-bottom * in `tileSize` squares. Tiles listed in `solid` become static colliders, * greedy-merged into a few rectangles (hundreds of cells → a handful of * bodies). Cell (0,0) renders with its TOP-LEFT on this node's origin. */ declare class TileMap2D extends Node2D { static override readonly typeName: string; static override readonly props: PropSchema; texture: string; tileSize: number; columns: number; private _cells; private _legend; private _solid; /** Rebuild flags — raised when the map-shaping props are REPLACED. */ private geometryDirty; /** `texture|highestTile` already reported, so a bad grid says so once. */ private gridChecked; private collidersDirty; /** The `scale` the colliders were last built for — see `rebuildColliders`. */ private builtScale; /** Replace the whole array to change the map (mutations are not watched). */ get cells(): (string | number[])[]; set cells(value: (string | number[])[]); get legend(): Record; set legend(value: Record); private _colors; get colors(): Record; set colors(value: Record); /** * The colours, in declaration order — a cell's tile index is `COLOR_BASE + i`. * * A synthetic index rather than a second grid: everything downstream (the * collider merge, `tileAt`, `setTile`, the editor's paint tool) already * speaks in indices, and one number per cell keeps it that way. */ private palette; /** `legend` plus the colour chars, which is what every cell lookup wants. */ private allChars; /** `solid` as INDICES: an entry may name the char that painted the cell. */ private solidIndices; get solid(): number[]; set solid(value: number[]); opacity: number; private tileMesh; private builtColumns; private builtRows; /** Loader hook: bad cells fail at LOAD (unknown chars), not at render. */ static validateJson(node: Node): void; /** The parsed index grid (test/tooling hook). */ grid(): number[][]; /** * Which CELL is under this world point — `null` outside the grid. * * The one question a tile game asks constantly and this node could not * answer. `VoxelGrid3D`, the 3D grid, has had `tileAt` since it was written; * the 2D one shipped `grid()` and nothing else, so every mouse-driven tile * game hand-rolled the same arithmetic with the same two traps in it: cell * (0,0) hangs its TOP-LEFT on the origin (so a cell's CENTRE is half a tile * further on), and the origin is the node's WORLD position, not its `position` * prop. * * Composes ancestor POSITIONS, like every other world query in the engine * (`Terrain3D.heightAt`, `Water3D`); `auditScene` reports an ancestor that * scales or rotates one of these, because that moves the picture and not the * answer. */ cellAt(worldX: number, worldY: number): [number, number] | null; /** * One cell's size in WORLD pixels — `tileSize` times this node's own scale. * * `scale` is an ordinary `Node2D` prop, the renderer scales the mesh by it, * and `rebuildColliders` has always rebuilt when it changes — so at * `scale: [2, 2]` a tile is 64 px wide on screen AND in physics. These * queries divided by 32 and answered the wrong cell, in silence, which is the * picture and the collider agreeing and the decider disagreeing with both. * * Ancestor scale is not composed here, for the same reason `worldPosition` * does not: every world query in the engine reads its own props and composes * ancestor POSITIONS, and `auditScene` reports an ancestor that scales one. */ private scaledTile; /** The atlas index at a cell — `-1` for an empty cell or one off the grid. */ tileAt(cx: number, cy: number): number; /** * The CENTRE of a cell, in world pixels — for putting a highlight, a marker * or a spawned enemy on a tile. * * The centre rather than the corner, because that is where a thing goes and * because the corner is the half-tile every hand-rolled version forgets. */ worldAt(cx: number, cy: number): [number, number]; /** * Write ONE cell, by the character the row holds — dig a tile out, drop a * wall in, flip a switch. * * `cells` are strings and a mutation is not watched, so the documented way to * change a map was to rebuild the whole row by hand * (`row.slice(0, x) + c + row.slice(x + 1)`) and reassign the array. This is * that, once, correctly, and it rebuilds the geometry and the colliders. * * A CHARACTER, not an index: `legend` may map several characters to one tile, * so an index cannot say which of them you meant. `'.'` empties the cell. * Returns false for a cell off the grid, or a numeric row (those hold indices * — assign the row). */ setTile(cx: number, cy: number, char: string): boolean; override onReady(): void; override update(_dt: number): void; private rebuildColliders; /** @internal The drawable mesh (lazily created under the backing object). */ _mesh(): Mesh; override _staticReady(assets: AssetStore2D | null): boolean; override _syncObject2D(assets: AssetStore2D | null): void; /** One quad per visible tile, atlas UVs — a single draw call for the level. */ private buildGeometry; /** Test hook: rendered tile count (quads in the built geometry). */ get renderedTileCount(): number; override free(): void; } //#endregion //#region src/2d/nodes/ui-layer.d.ts declare const ANCHORS: readonly ["top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right"]; type UIAnchor = (typeof ANCHORS)[number]; /** * Screen-space container (Godot CanvasLayer): 2D descendants render in a * separate pass that ignores the world camera — HUDs, scores, menus. * * `anchor` pins the layer's origin to a screen corner/edge/center, so HUD * coordinates stay tiny offsets that survive any canvas size: a Label at * [-16, 16] under a 'top-right' layer hugs the top-right corner everywhere. */ declare class UILayer extends Node { static override readonly typeName: string; static readonly props: PropSchema; anchor: string; private group; /** @internal The three group all descendants mount under (lazily created). */ _ensureGroup(): Group; /** Loader hook: a misspelled anchor fails at LOAD, listing the valid set. */ static validateJson(node: Node): void; /** @internal Anchor origin in y-down ui px for a given ui size. */ _anchorOrigin(width: number, height: number): { x: number; y: number; }; } //#endregion //#region src/2d/register.d.ts /** * Register the 2D node taxonomy (and the core nodes). Call once in your game * entry before loading a 2D scene. Explicit — never an import side effect. */ declare function registerNodes2D(): void; //#endregion //#region src/2d/picking.d.ts /** * Pure view-transform helpers shared by Renderer2D's picking API and editor * overlays. World space is y-down pixels (the engine's 2D convention); screen * space is CSS pixels on the canvas. */ interface View2D { /** World-space view center. */ cx: number; cy: number; zoom: number; /** Canvas CSS size. */ w: number; h: number; } //#endregion //#region src/2d/renderer.d.ts interface Renderer2DOptions { canvas: HTMLCanvasElement; engine: Engine; /** Bring your own store (custom loader) — defaults to a TextureLoader-backed one. */ assets?: AssetStore2D; /** * Backbuffer scale. DEFAULTS TO 1 — Phaser parity: the browser upscales on * hiDPI screens, which reads as soft antialiased edges. Pass * `window.devicePixelRatio` for crisp retina rendering instead. */ pixelRatio?: number; /** MSAA on the canvas. Defaults to TRUE (Phaser parity). */ antialias?: boolean; /** * Keep the drawing buffer readable so the canvas can be screenshot — see * `CreateGame2DOptions`. `environment.rendering.preserveDrawingBuffer` does * the same from the scene, and an explicit option here outranks it. */ preserveDrawingBuffer?: boolean; } declare class Renderer2D { /** * When set, the world pass frames THIS view instead of the scene's active * Camera2D — tooling (the scene editor) pans/zooms freely without touching * scene data. `null` restores normal camera behavior. */ viewOverride: { cx: number; cy: number; zoom: number; } | null; private readonly viewBlend; private readonly webgl; /** * The pixels of the next drawn frame — how `incanto-frame` sees a 2D game. * * Mirrors Renderer3D exactly: read inside the render, so * `preserveDrawingBuffer` can stay off, and nudge a frame ourselves after a * moment because a browser stops handing rAF to a tab it thinks is hidden — * including a window merely covered by another one. Without the nudge the * promise never settles and the CLI reports "no page connected" about a page * that is right there. */ captureFrame(): Promise<{ pixels: Uint8Array; width: number; height: number; }>; private pendingCaptures; /** Hand the waiting captures this frame's pixels. Called at the end of render. */ private drainFrameCapture; /** The pixel ratio the scene asked for, before any render-scale setting. */ private basePixelRatio; /** * Fewer pixels for the same world — the cheapest frames on a weak device. * * `settings.renderScale` persisted and was applied by the 3D boot only, so a * 2D game's resolution slider saved a number and changed nothing, forever. */ setRenderScale(scale: number): void; private readonly worldScene; private readonly syncScratch; /** Editors set this: keep syncing `static: true` subtrees every frame. */ ignoreStatic: boolean; private readonly uiScene; private readonly worldCam; private readonly uiCam; private readonly engine; /** The texture store. Public so `renderer.assets.status(key)` is reachable. */ readonly assets: AssetStore2D; private readonly ownsAssets; private readonly loadedAssetScenes; private readonly disconnect; private readonly canvas; constructor(opts: Renderer2DOptions); private readonly debugLines; private readonly selectionLines; private syncSelectionOutline; private syncDebugLines; private render; /** UI-pass extent from the last render — the space HUD nodes are posed in. */ private lastUi; private lastViewport; /** * The view the LAST render used — and, before there has been one, the * documented default: the whole canvas, centred, at zoom 1. * * It started as `{cx: 0, cy: 0, w: 1, h: 1}`, a one-pixel canvas nobody has, * and `worldFromScreen` answered from it with a straight face. Measured in a * browser on a 960x540 canvas with the camera at (160, 80), before the first * frame: `toWorld(480, 270)` returned `[479.5, 269.5]` — the arithmetic of a * 1x1 window, and a confident number rather than a refusal. */ private lastView; /** * Has a frame been DRAWN? * * `Renderer3D` refuses every screen-space question until it has one * (`if (!this.lastCamera) return null`, in `pick`, `rayFromScreen` and * `worldFromScreen`); the flat renderer had no equivalent, so the engine-level * `toWorld`/`toScreen` and `pick` answered from a view no frame had used. The * hazard is real in exactly the place agents work: a background tab does not * run rAF, so `await createGame2D(...)` there is followed by any number of * questions with no frame behind them. */ private hasRendered; /** The world view used by the LAST render (center/zoom/canvas size). */ view(): View2D; /** * GPU counters for the LAST rendered frame — world + UI passes combined (see * the `info.reset()` in the render pass), plus GPU memory. */ stats(): RendererStats; worldFromScreen(sx: number, sy: number): { x: number; y: number; }; screenFromWorld(wx: number, wy: number): { x: number; y: number; }; /** * The topmost world-pass node under a canvas pixel (UI pass excluded). * Hits resolve through `userData.incantoNode` and prefer higher renderOrder. */ pick(sx: number, sy: number): Node | null; private hit; /** Does the UI pass draw this node (i.e. is it under a UILayer)? */ isUiNode(node: Node): boolean; /** * UI-space px → canvas px. * * A UILayer's children are posed in the UI pass's own space (origin top-left, * `uiW × uiH`), NOT in world space — so mapping them with `screenFromWorld` * puts them wherever the game camera happens to be looking. */ screenFromUi(ux: number, uy: number): { x: number; y: number; }; /** Canvas px → UI-space px (the inverse of `screenFromUi`). */ uiFromScreen(sx: number, sy: number): { x: number; y: number; }; private uiView; /** * Canvas-pixel AABB of a node's rendered world-pass objects, or null when it * has no renderable extent (plain Nodes, empty containers). */ boundsOf(node: Node): { x: number; y: number; w: number; h: number; } | null; dispose(): void; } //#endregion //#region src/2d/sync.d.ts interface Sync2DResult { activeCamera: Camera2D | null; } interface UiSize { width: number; height: number; } /** * Mirror an Incanto node tree onto two three scenes: * - `world`: camera-space drawables * - `ui`: everything under a `UILayer` (screen space, ignores the camera); * each layer mounts through its own group, positioned by its `anchor` * against `uiSize` (origin when no size is known — headless). * * Same dirty-push contract as the 3D sync; pure scene-graph math, headless-testable. */ /** Per-renderer reusable containers — cleared each frame, never reallocated. */ interface Sync2DScratch { visited: Set; cameras: Camera2D[]; emitters: Array<{ node: Node & SpatialConsumer; parent: Object3D; }>; } /** A node that accepts a per-frame spatial pose (an AudioPlayer with spatial on). */ interface SpatialConsumer { spatial: boolean; _setSpatialPose(pose: SpatialPose): void; } declare function syncTree2D(root: Node, world: Scene, ui: Scene, assets: AssetStore2D | null, uiSize?: UiSize, scratch?: Sync2DScratch, opts?: { ignoreStatic?: boolean; }): Sync2DResult; //#endregion //#region src/2d/tilemap-grid.d.ts /** * Pure tile-grid logic for TileMap2D — no three, unit-testable in node. * Grids are `number[][]` of atlas tile indices, `-1` = empty. */ interface TileRect { /** Cell coords (col, row) of the rect's top-left. */ x: number; y: number; /** Size in cells. */ w: number; h: number; } declare function parseCells(cells: readonly (string | readonly number[])[], legend: Record): number[][]; /** * Greedy rectangle merge over the solid cells: maximal horizontal runs, * extended downward while the identical run repeats. A platform level's * hundreds of solid tiles collapse to a handful of colliders. */ declare function mergeSolidRects(grid: readonly (readonly number[])[], solid: Set): TileRect[]; //#endregion export { AnimatedSprite2D, Area2D, type AssetStatus, AssetStore2D, Camera2D, CharacterBody2D, CharacterController2D, ColorRect2D, type CreateGame2DOptions, type Game2D, Joint2D, type JointType2D, Label, Node2D, Particles2D, Physics2D, type Physics2DOptions, PhysicsBody2D, Renderer2D, type Renderer2DOptions, type ResolvedSpriteTexture, RigidBody2D, type SheetInfo, Sprite2D, type SpriteFromLibraryResult, StaticBody2D, type Sync2DResult, type TextureLoadCallbacks, TileMap2D, type TileRect, type UIAnchor, UILayer, createGame2D, enablePhysics2D, isWebGLAvailable, mergeSolidRects, parseCells, registerNodes2D, showBootFailure, spriteFromLibraryMeta, syncTree2D };