import { b as Engine, d as PropSchema, jt as Node, n as BehaviorCtor, t as Behavior } from "./behavior-B_245qRy.js"; import { c as JsonValue, s as JsonObject } from "./rng-BsXZg3D6.js"; //#region src/gameplay/chase.d.ts /** * Home in on a target node — the workhorse enemy AI. Each frame moves toward * `target.position` at `speed`, stopping once within `stopRange` (melee reach). * Dimension-agnostic. * * - `reachedTarget` fires once when it first enters `stopRange` (re-arms after * it leaves), so wire it to an attack. * - `loseRange` (>0) gives up the chase when the target gets that far away, * emitting `lostTarget` once. * - `facePath`/`turnSpeed` turn a skin toward the direction of travel — the job * `CharacterController3D` does for the player and nobody did for an NPC. * - `moveParent` (default false) moves the parent node instead of this one. * A node carries ONE behavior, so an enemy whose ROOT must hold `Health` * (e.g. for `Health.freeOnDeath` clone-safe cleanup) puts `Chase` on a CHILD * and sets `moveParent:true` — the AI child drives the whole entity. Distance * is measured from the moved node (the parent). */ declare class Chase extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; target: string; targetGroup: string; retargetEvery: number; speed: number; /** The group member currently chased (`targetGroup`), or null. */ private picked; /** Read-only: the node being chased this frame, or null (out of sight, none in the group, no `target`). */ get chasing(): Node | null; private current; private sincePick; stopRange: number; loseRange: number; moveParent: boolean; requireSight: boolean; ground: boolean | null; facePath: string; /** Signed fall speed accumulated while a ground chaser is off the floor. */ private fall; turnSpeed: number; private inRange; private lost; override onReady(): void; /** * A walker falls off what it walks off. `CharacterBody3D` applies no gravity * of its own ("integrate velocity yourself"), and a chaser that ignores the * target's height would otherwise walk off a ledge and keep its altitude — * measured on tps-3d's 0.6 m Ramp, where the old chase glided down over five * seconds only because the player happened to stand lower. Scene gravity * (default 9.81), accumulated while `isOnFloor()` says no, reset when it says * yes; the KCC's snap-to-ground handles the last 10 cm. */ private fallStep; /** Off the floor with nowhere to chase: still fall. */ private dropIfAirborne; /** The nearest live member of `targetGroup` — re-picked every `retargetEvery` seconds, never the mover itself. */ private nearestOfGroup; /** `requireSight`: a static-only ray from half a metre over the mover to half a metre over the target reaches it. */ private canSee; /** See `ground`: an explicit answer, else "is the mover a 3D kinematic capsule". */ private walksTheGround; override update(dt: number): void; } //#endregion //#region src/gameplay/chase-camera.d.ts /** * A camera BEHIND something that turns — a car, a boat, a tank, a horse. * * `FollowCamera` keeps a fixed world offset, which is right for a top-down or * side view and wrong for a vehicle: the offset has to swing round with the * target's heading, and lazily, or every corner snaps the view. Four examples * (a delivery van, a harbour boat, an errand car, a race car) wrote this same * behaviour by hand — `back`, `up`, a smoothed yaw — before it existed. * * The heading comes from the target's rotation BASIS, not from `rotation[1]`: * a tilted body's Euler XYZ triple reads `[180, 180−yaw, 180]` past 90° of * yaw, and a camera that read the middle number swung to the wrong side. Aim * the camera with `Camera3D.lookAt` (or `Camera2D.follow`); this only places it. */ declare class ChaseCamera extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; target: string; back: number; up: number; smoothing: number; /** The heading the camera currently trails (radians) — a harness reads it. */ heading: number | null; override onReady(): void; override update(dt: number): void; private placed; /** Put the camera on its mark NOW (a scene start, a respawn) instead of flying there. */ snap(): void; } //#endregion //#region src/gameplay/collector.d.ts /** * A tally for the "collect N things" pattern, living on the collector itself * (usually the player). Adds its node to `group` at ready so Pickups whose * `collectorGroup` matches will count it as a collector. * * Wire a Pickup's `collected(value, other) → Collector.collect` (the value is * the first arg) — `total` accumulates and `totalChanged(total)` fires. * (Pickup also notifies a global ScoreKeeper; Collector is the per-actor tally, * e.g. for split-screen or "each player's coins".) */ declare class Collector extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; group: string; /** Running sum of collected values. */ total: number; override onReady(): void; /** Add `value` to the tally and emit `totalChanged`. */ collect(value: number): void; /** How many have been picked up so far. */ override serialize(): JsonValue; override deserialize(data: JsonValue): void; /** The counter on screen. `collect(0)` used to be the only way to repaint it. */ override announce(): void; } //#endregion //#region src/gameplay/currency.d.ts /** * Money. Earn it, spend it, and refuse what you cannot afford. * * `ScoreKeeper` is the closest thing the library had and it is a SCORE: it * counts up, it has no spend, no affordability question, and its counter is * already wired to the win condition. So a tower defense built on 0.65.0 wrote * its own wallet — and so does every shop, every build mode, every upgrade tree * and every economy game. * * `spend` is the whole point: it returns whether it went through, and emits * `refused` when it did not, so "you cannot afford that" is a wire rather than * a comparison every caller repeats and one caller forgets. */ declare class Currency extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; amount: number; max: number; /** Can this be paid right now? */ canAfford(cost: number): boolean; /** Pay `cost` if it is there. Returns whether it went through. */ spend(cost: number): boolean; /** * Take payment in — and say what did NOT fit. * * This clamped at `max` and emitted `earned(gain)` anyway, so a purse with a * ceiling took a coin it could not hold and told the game it had. Measured on * a game whose purse holds four: the fifth pickup vanished off the floor, the * HUD said `4 / 4`, and nothing anywhere had the pickup. The game then wrote * `if (purse.amount >= purse.max)` itself — the comparison `spend`'s * `refused` exists so that nobody repeats. * * `earned` is now what was actually TAKEN, and `refused` carries the * remainder. A purse with no ceiling never refuses, which is every purse * written before this. */ earn(gain: number): void; /** How much more this purse can hold (Infinity with no ceiling). */ get room(): number; /** What has already been complained about, so a loop says it once. */ private complained; /** * A zero is ordinary; a NEGATIVE is a caller that flipped a sign. * * Reported rather than thrown: this is called from game code inside a frame, * and a throw here quarantines the node holding the player's money. */ private refuseNonsense; /** Set it outright (a shop that grants, a cheat, a restore). */ setAmount(next: number): void; override serialize(): JsonValue; override deserialize(data: JsonValue): void; /** The wallet on screen — `setAmount`'s own doc comment already named a restore. */ override announce(): void; } //#endregion //#region src/gameplay/health.d.ts /** * Hit points with regeneration and post-hit invulnerability (i-frames) — * the universal "this thing can be hurt and can die" behavior. * * Other behaviors hurt it through `damage(n)` (e.g. `DamageOnContact` finds a * target's Health and calls it); games heal/kill via `heal(n)` / `kill()`. * Wire its `died` signal to a `ScoreKeeper.loseLife`, a respawn, etc. * * `freeOnDeath` (default false) `queueFree()`s the Health node itself when it * dies. Because it is NODE-LOCAL it clones perfectly — this is THE way to free * dying SPAWNED entities. Scene-level `connections` (e.g. `died → queueFree`) * are NOT copied onto Spawner/WaveSpawner clones, so a `died` connection only * ever wires the template, never the live clones; `freeOnDeath` needs no * connection and so works on every clone. * * Dimension-agnostic: no node geometry, just numbers — testable headlessly. */ declare class Health extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; max: number; regenPerSec: number; invulnerableFor: number; freeOnDeath: boolean; freeParent: boolean; knockback: number; knockbackSeconds: number; knockUp: number; staggerSeconds: number; applyKnockback: boolean; /** Unit direction of the last hit that had a source, source → this body (ground plane). */ hitDirection: number[]; private staggerLeft; private kickLeft; private kick; private kickFresh; /** True while a hit's stagger holds this body's movers and controllers. */ get staggered(): boolean; /** True while a hit's shove is still moving this body. */ get knocked(): boolean; /** The shove's current velocity (decaying), empty when not knocked — a controller that drives the body reads it. */ get kickVelocity(): number[]; /** The vertical part of the shove, ONCE (a lift is an impulse, not a per-frame write); 0 after. */ takeLift(): number; /** A body driven by a character controller gets its shove from the controller, not from here. */ private drivenByController; /** The body a hit moves: this node if it is spatial, else the nearest spatial ancestor (a Health on a child `Hp` node moves its parent). */ private bodyNode; /** Current hit points (set to `max` on ready). */ current: number; private invulnTimer; private dead; /** True once `current` has hit 0 — `died` fires at most once per life. */ get isDead(): boolean; /** Seconds of i-frames remaining (0 = vulnerable). */ get invulnerableRemaining(): number; override onReady(): void; override update(dt: number): void; /** Apply `n` damage. No-op while invulnerable or already dead. Emits `damaged`. */ /** * The shove, frame by frame, decaying linearly — written EVERY frame like a * dash, because a controller brakes toward its own pace and a one-frame * velocity dies. A dynamic body gets its velocity written (the vertical part * once, as a lift); a kinematic or plain node is displaced through * `moveBody` with `force`, so it still slides along walls. */ private applyKick; damage(n: number, from?: readonly number[] | undefined): void; /** * Be immune for `seconds` from NOW — a dodge roll, a spawn, a cutscene. * * `invulnerableFor` grants i-frames only AFTER a hit; this is the window * before one. It extends a window already open and never shortens it, so a * second dodge mid-window cannot expose you. Dead or not, it is harmless. */ protect(seconds: number): void; /** Restore `n` hit points (clamped to `max`). No-op when dead. Emits `healed`. */ heal(n: number): void; /** * Bring a dead one back, at `hp` (default: full). * * There was no way back. `dead` is private, `isDead` is a readonly getter, * and `damage`, `heal` and `regenPerSec` all no-op once it is set — so the * three-lives loop every game wants, and that * `incanto-your-first-game.md` teaches (`lives: 3` plus * `died → ScoreKeeper.loseLife`), could spend exactly ONE life. After the * first death the player was a walking corpse: full HP bar, immune to every * enemy, `died` never firing again, lives frozen, and the game quietly * unlosable. Every checker was green on it, and `incanto-playtest` even * reported `lost in 19/20` because a single `died` reads as a loss. * * **Wire `reviveFull`, not this, to `lifeLost`.** `ScoreKeeper.loseLife()` * emits `lifeLost` WITH the remaining life count, and this method's first * parameter is the HP to come back at — so the documented recipe revived at * 2 HP, then 1, then 0, and nothing warned (the wire-arity guard fires only * when a handler is UNDER-fed, and `hp` is optional): * * ``` * death 1: lives=2 hp=2 * death 2: lives=1 hp=1 * death 3: lives=0 hp=0 dead=false <- alive at zero HP * ``` * * Reach for `revive(hp)` when your game genuinely wants a partial comeback. * * A no-op on the living — reviving someone who never died would silently * refill their health, which is a different feature and not this one. */ revive(hp?: number): void; /** * Bring a dead one back at FULL health — the handler a life-lost wire wants. * * It exists because `revive` takes an optional `hp` and `lifeLost` carries the * life count, so wiring the two silently made "you have 2 lives left" mean * "come back at 2 HP". This takes no arguments, so no signal's payload can * ever change what it means. * * ```json * { "signal": "lifeLost", "from": "Score", "to": "Player", "handler": "reviveFull" } * ``` */ reviveFull(): void; /** Drop to 0 and die immediately (ignores i-frames). */ kill(): void; /** * The one funnel every change goes through — and therefore the one place a * HUD can be told about. * * `damaged`/`healed` both lead with the DELTA, which is the number a bar must * not show; wire either to `UiBar.setValue` and the bar paints the damage as * the health and looks like it works. Regeneration is worse: it moves * `current` through here and emits NEITHER, so a regen bar sat still while * the character healed. `healthChanged(current, max)` carries the max too, so * a raised ceiling needs no second wire. * * Silent when nothing moved: a bar that repaints on nothing is noise. */ private setCurrent; private die; /** Current HP and whether death already fired — `max` comes back from JSON. */ override serialize(): JsonValue; override deserialize(data: JsonValue): void; /** * Repaint the bar. Not `died` — a save is being read, nobody just died. * * This one needed the hook most: `setCurrent` is private AND early-returns * when the value is unchanged, so no call a game could make would repaint a * restored bar. It read 100 at 38 HP until the next hit teleported it to 37. */ override announce(): void; } //#endregion //#region src/gameplay/damage-on-contact.d.ts /** * Deals damage to whatever it touches — projectiles, spikes, lava, enemy * hitboxes. Must sit on an Area2D/Area3D (listens to `triggerEnter`). * * On overlap it finds the contacted entity's `Health` behavior (see * `findHealth` for the exact search order) and calls `damage(amount)`, then * emits `dealtDamage(amount, targetNode)`. * * - `targetGroup` (default '' = anything) gates who can be hurt: only a target * whose Health-owner node is in that group takes damage. This is how you stop * enemies killing each other — give the player's weapon `targetGroup:'enemy'` * and each enemy's contact hitbox `targetGroup:'player'`. * - `oncePerTarget` (default) prevents re-hitting the same node WHILE it stays * in contact (a stationary hazard would otherwise drain a resting body every * frame). It is per overlap, not per lifetime: leave and come back, or disarm * the hitbox and arm it again over the same foe, and the hit lands again. * - `repeatEvery` (seconds, 0 = off) re-damages targets that STAY overlapped — * lava pools, poison clouds, an enemy standing on you. Contact events fire * only on entry/exit; this is the "and it keeps hurting" knob. * - `destroySelf` frees the hazard after a hit (single-use projectiles), and * `freeParent` makes that free the PARENT — a bullet whose hitbox is a child, * which is the only shape a node-carries-one-script engine allows. * * SCORING PATTERN (clone-safe): wire the KILLER's `dealtDamage` → * `ScoreKeeper.addScore`. The weapon is usually a non-cloned node (it lives on * the player), so a scene connection on it survives — unlike a connection on a * spawned enemy, which never clones. */ declare class DamageOnContact extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; amount: number; targetGroup: string; oncePerTarget: boolean; repeatEvery: number; destroySelf: boolean; freeParent: boolean; private readonly hit; /** Targets currently overlapping → seconds until their next repeat tick. */ private readonly inside; override onReady(): void; override update(dt: number): void; private tryDamage; } //#endregion //#region src/gameplay/face-target.d.ts /** * Turn toward the nearest thing in a group, and say when you are on it. * * A turret is the shape every tower defense, sentry gun, security camera and * idle-NPC-that-watches-you needs, and it could not be composed. `Chase` * already finds the nearest target and turns toward it — and then MOVES, which * is the one thing a turret must not do; `Patrol`/`Chase`'s `facePath` only * turns as a side effect of locomotion, so a stationary node cannot use it. * A tower defense built on 0.65.0 hand-wrote acquisition (~20 lines) and * turn-and-gate-the-shot (~15) because of that. * * `aimed` fires when the facing settles onto the target and `lostAim` when it * comes off, so "only fire while actually pointed at it" is a wire rather than * an angle comparison you write yourself. */ declare class FaceTarget extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; targetGroup: string; range: number; facePath: string; turnSpeed: number; aimTolerance: number; /** The node currently being tracked, or null. */ target: Node | null; /** True while the facing is within `aimTolerance` of the target. */ onTarget: boolean; override onReady(): void; override update(dt: number): void; private nearest; /** Is the turret's forward within tolerance of the direction to the target? */ private pointedAt; private setAim; } //#endregion //#region src/gameplay/follow-camera.d.ts /** * Make the node it sits on chase a target's position — THE camera-follow * behavior. Put it on a `Camera2D`/`Camera3D` (whose `position` is the view * center/eye) and point `target` at the player; every frame the camera lerps * toward `target.position + offset`. * * Dimension-agnostic: works on `[x,y]` (2D) and `[x,y,z]` (3D) positions. * * - `smoothing` (0..1) is a per-frame retention factor: 0 = instant snap, * 0.85–0.95 = smooth drag (frame-rate independent at a 60fps reference). * - `deadzone` keeps the camera still until the target drifts that far from the * desired point — no jitter when the player makes tiny moves. */ declare class FollowCamera extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; target: string; offset: number[]; smoothing: number; deadzone: number; private shakeRng; override onReady(): void; private shakeMag; private shakeT; private shakeFalloff; /** Kick the camera (impacts, explosions). Composes with the follow. */ shake(magnitude: number, seconds?: number): void; override update(dt: number): void; } //#endregion //#region src/gameplay/interactable.d.ts /** * A proximity-gated "press to use" — doors, levers, chests, NPCs. Each frame, * if an actor in `actorGroup` is within `range` (distance on `position` arrays, * 2D or 3D) and the `action` input was just pressed, emits * `interacted(actor)` (the nearest in-range actor). * * Wire `interacted → YourBehavior.someMethod`, or read it from a connection. */ declare class Interactable extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; action: string; range: number; actorGroup: string; prompt: string; focus: "any" | "nearest" | "facing"; coneDeg: number; /** * The actor this Interactable would answer RIGHT NOW, or null — updated every * frame, with `focused(actor)` / `unfocused(actor)` on the edges. A HUD reads * the focused one's `prompt`. */ focusedBy: Node | null; /** Actions already reported missing, so a per-frame read says it once. */ private readonly reportedActions; override onReady(): void; override update(): void; /** Is THIS the Interactable `actor` means — the nearest (that it faces) of every one in reach? */ private claims; } //#endregion //#region src/gameplay/lifetime.d.ts /** * Self-destruct after a fixed time — bullets, particles, temporary spawns, * pickups that vanish. Accumulates `dt`; on elapse emits `expired` then * `queueFree()`s its node. * * `freeParent: true` frees the node's PARENT instead — the shape a bullet needs, * since a node carries ONE script so `Projectile`, the damage and the timer have * to live on separate nodes. * * With `startOnSignal: true` the countdown is armed manually via `startTimer()` * (wire a signal → `startTimer`), so the lifetime begins on an event rather * than at spawn. */ declare class Lifetime extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; seconds: number; startOnSignal: boolean; freeParent: boolean; private elapsed; private running; private fired; override onReady(): void; /** Arm (or re-arm) the countdown from zero. */ startTimer(): void; override update(dt: number): void; } //#endregion //#region src/gameplay/mount.d.ts /** * Get on something and drive it; get off beside it. * * A horse, a car, a boat, a turret seat: the player's controller lets go, the * player's body PARKS (collider off, hidden, weightless, riding along so * everything that targets the player follows), the player's `skin` moves onto * the steed's `saddle`, and the steed's `drive` — a `CharacterController3D` or * a `Vehicle3D` — is enabled. `dismount()` puts everything back beside the * steed. Two examples (an errand car, a saddled horse) wrote these sixty lines * before it existed, and both forgot something the other remembered. * * Wire it from an `Interactable`: `interacted → Mount.toggle`. */ declare class Mount extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; rider: string; steed: string; drive: string; walk: string; skin: string; saddle: string; skinOnSaddle: number[]; skinOnRider: number[]; dismountAt: number[]; /** True while the rider is on. */ mounted: boolean; mounts: number; private parked; override onReady(): void; private riderNode; private steedNode; /** The first child (or the named one) carrying a boolean `enabled` and a `yaw` or a `throttle` — a controller or a vehicle. */ private switchOf; private skinNode; private saddleNode; /** Get on. False when already on. */ mount(): boolean; /** Get off beside the steed. False when not on. */ dismount(): boolean; /** On if off, off if on — the handler an `Interactable`'s `interacted` wires to. */ toggle(): void; override update(): void; } //#endregion //#region src/gameplay/tween.d.ts /** Easing curves shared by the tween behaviors (MoveTo, …). Pure math. */ type EaseName = "linear" | "easeIn" | "easeOut" | "easeInOut"; //#endregion //#region src/gameplay/move-to.d.ts /** * Tween the node from where it starts to a fixed `to` position over `duration` * seconds, through an easing curve — opening doors, sliding platforms, UI * pop-ins, scripted moves. Emits `arrived` once at the end. * * `startOnSignal: true` arms it manually via `start()` (wire a signal → `start`) * so the move plays on an event; otherwise it begins at ready. Dimension- * agnostic (`[x,y]` / `[x,y,z]`). */ declare class MoveTo extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; to: number[]; duration: number; ease: EaseName; startOnSignal: boolean; private from; private elapsed; private running; private arrivedFired; override onReady(): void; /** Begin (or restart) the move from the node's CURRENT position. */ start(): void; /** * Land on `to` NOW and say `arrived` (once). A skipped cutscene must leave * the world as the whole cutscene would have — the door it opens, open. * Wire `skipped → Door.finish`. */ finish(): void; override update(dt: number): void; } //#endregion //#region src/gameplay/oscillate.d.ts declare const AXES: readonly ["x", "y", "z"]; type Axis = (typeof AXES)[number]; declare const MODES: readonly ["position", "rotation", "scale"]; type Mode = (typeof MODES)[number]; /** * Continuous sine motion around a value — floating platforms, bobbing pickups, * spinning/pulsing coins. Drives one `axis` of the node's `position`, `rotation` * (`mode: 'rotation'` — a "spin" when on z), or `scale` (a "pulse"): * * value = start + amplitude · sin(2π · frequency · t) * * The baseline (`start`) is captured at ready, so it layers on top of authored * transforms. Dimension-agnostic; on a 2D node `rotation` is the scalar spin * (use `axis: 'z'`). */ declare class Oscillate extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; axis: Axis; amplitude: number; frequency: number; mode: Mode; private base; private time; override onReady(): void; override update(dt: number): void; /** Whether the target channel is the node's scalar 2D rotation. */ private isScalarRotation; private readChannel; private writeChannel; } //#endregion //#region src/gameplay/patrol.d.ts /** * Walk the node along a fixed list of waypoints at constant speed — guards, * platforms, moving hazards. `points` are positions (`[x,y]` / `[x,y,z]`) OR * node paths whose `position` is read each frame (so you can author markers in * the scene). On arrival emits `reachedPoint(index)`; `pauseAt` holds at each * point before moving on. * * - `loop` (default) — after the last point, head back to the first. * - `mode: 'pingpong'` — reverse direction at each end instead of wrapping. * - `moveParent` (default false) moves the parent node instead of this one, * exactly as `Chase` does. A node carries ONE behavior, so a guard that must * patrol AND hold `Health` on its body puts `Patrol` on a CHILD. Without * this the patrolling body had to carry `Patrol` itself and could hold * nothing else — so a patrolling enemy could not be hurt. * - `facePath`/`turnSpeed` turn a skin toward the direction of travel, the job * `CharacterController3D` does for the player and nobody did for an NPC. */ declare class Patrol extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; points: JsonValue[]; speed: number; loop: boolean; mode: string; pauseAt: number; moveParent: boolean; facePath: string; turnSpeed: number; private index; private direction; private pauseTimer; override onReady(): void; override update(dt: number): void; /** Turn the skin toward the step just taken. */ private face; /** Waypoints already reported dead, so a per-frame stall says it once. */ private readonly reportedDead; /** * Say which waypoint the route died on. * * A `points` entry can be a node path, and a misspelled one stops the * patroller the moment the index reaches it — `advance()` is never called, so * it never moves again — with nothing printed. `points` is an ARRAY, so it * cannot carry `nodePath: true`, and `nodeRefWarnings` only inspects string * props: every other node-path prop in the engine is covered by that lint and * this one is invisible to it. */ private reportDeadWaypoint; /** * Resolve waypoint `i` to a position in the MOVER's frame, reading node paths * live. * * A node-path waypoint used to return the marker's raw `position`, which is * parent-relative — and it was then fed to `moveToward(mover.position, …)`, * i.e. read in the MOVER's frame. So a walker and its markers under different * parents walked a shifted copy of the path, silently, with the marker nodes * sitting right there in the scene. Measured on a tower-defence lane * organised the ordinary way (markers under `/root/Path`, spawns under a * container at [-50, 90]): every creep stacked motionless 90 px below and * 72 px short of the keep, `incanto-check` `1/1 scene(s) valid`, zero errors, * zero warnings, and the wave could be neither won nor lost. * * `Chase` has converted through world space since the identical bug was found * there ("comparing two nodes' raw `position` only worked while they happened * to share a parent"), and a `FollowCamera` aimed at a node uses its world * position too. Patrol was the outlier. * * A LITERAL array stays local, because that is the only frame an author can * mean when they type numbers into a behaviour's props. * * `reportDeadWaypoint` could not see this: it fires when a path resolves to * NOTHING, and a path that resolves into the wrong frame resolves fine. */ private pointAt; private advance; } //#endregion //#region src/gameplay/phases.d.ts /** * A boss that changes at two thirds and again at a third. * * Every other common wiring in this engine became declarative — score to * screen, damage to a health bar, waves, win, lose, the save, the restore — and * a fight's PHASES stayed the one thing that needed TypeScript. The shipped * boss reads `healthChanged(current, max)`, divides, compares against two * constants and calls its own `enterPhase`; every boss anyone builds writes * those eight lines again. * * This is the threshold half and only that half. WHAT a phase does is the * game's business, and it is reached through connections: * * ```json * { "name": "Mood", "type": "Node", "uid": "n_…", * "script": { "name": "Phases", "props": { "at": [0.66, 0.33] } } } * ``` * ```json * { "signal": "phase2", "from": "Boss/Mood", "to": "Adds", "handler": "start" }, * { "signal": "phase3", "from": "Boss/Mood", "to": "Roar", "handler": "play" }, * { "signal": "phaseChanged", "from": "Boss/Mood", "to": "Hud", "handler": "onPhase" } * ``` * * The numbered signals exist because a connection cannot filter on a NUMBER: a * `filter` gates on a node's group or tags, so `phaseChanged(2)` could only ever * reach one handler that switches. `phase2`…`phase5` is what makes "phase two * starts the adds" a line in the scene file. */ declare class Phases extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; at: number[]; health: string; /** Which phase the fight is in: 1 until the first threshold is crossed. */ phase: number; private watched; override onReady(): void; /** Wire this to a `healthChanged` yourself when the health is somewhere odd. */ read(current: number, max: number): void; /** The phase survives a save, like every other run-state number here. */ override serialize(): { phase: number; }; override deserialize(data: unknown): void; override announce(): void; } //#endregion //#region src/gameplay/pickup.d.ts /** * A collectible that vanishes when a collector overlaps it. Must sit on an * Area2D/Area3D (it listens to the unified `triggerEnter`). * * On overlap with a node in `collectorGroup`, emits `collected(value, other)` * then `queueFree()`s itself. Wire `collected → ScoreKeeper.addScore` (the * value is the first arg) or `collected → Collector.collect`. * * `other` is the ENTITY, not whichever of its parts touched — see * {@link findCollector}. A player is a body with children (a reach sensor, a * hitbox), and the part that enters first is rarely the part carrying the group. */ declare class Pickup extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; value: number; kind: string; collectorGroup: string; private collectedAlready; override onReady(): void; private tryCollect; } //#endregion //#region src/gameplay/projectile.d.ts /** * Constant-velocity motion in a straight line — bullets, arrows, thrown rocks. * Pure movement by design: pair it with `DamageOnContact` (deal damage on hit), * `Lifetime` (auto-despawn), and an Area collider on the same node. Compose, * don't conflate. * * `direction` is either an explicit vector (`[x,y]` / `[x,y,z]`, normalized) or * the string `'forward'`, derived from the node's `rotation` (2D: degrees * clockwise, +x at 0). It is a union-typed prop, so its schema default is * `null` (the engine's "any JSON" escape hatch) and `null` means `'forward'`. * `gravity` adds a constant downward (+y, the 2D y-down convention) pull, for * lobbed/arcing shots. * * **Every prop here is live.** `speed` and `direction` re-bake the heading when * written, and the heading is resolved on the FIRST STEP rather than at * `onReady` — because `onReady` is before the one place a game aims a spawned * bullet. `SpawnSource.spawn` ends with `parent.addChild(clone)` and `Spawner` * emits `spawned(node)` after that, so a handler that sets a rotation or a * direction was always too late. Measured at speed 100 over 60 frames: * * ``` * A rotation 0 direction [0,1] -> pos [ 0, 100] authored: obeyed * B rotation 90 direction "forward" -> pos [ 0, 100] authored: obeyed * C rotation 90 set after addChild -> pos [100, 0] IGNORED * D direction [0,1] set after add -> pos [100, 0] IGNORED — and it read * back [0,1] while * travelling +x * ``` * * What is deliberately NOT live is `rotation` after that first step: a * projectile that spins for looks would have its spin become its trajectory. * Call `aim()` to re-derive from the current rotation on purpose. */ declare class Projectile extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; private _speed; private _direction; gravity: number; /** The straight-line part, `direction × speed`. Empty until the first step. */ private velocity; /** * The +y accumulated by `gravity`, kept APART from `velocity`. * * Re-aiming replaces the straight-line part; the fall is a fact about the * world, not about the shot, so an arrow re-aimed halfway down keeps falling * rather than snapping back to level. */ private fall; /** Units per second along `direction`. Writable at any time. */ get speed(): number; set speed(value: number); /** A vector (`[x,y(,z)]`) or `'forward'`. Writable at any time. */ get direction(): JsonValue; set direction(value: JsonValue); private get armed(); /** * Re-derive the heading from `direction` (or, for `'forward'`, from the * node's CURRENT rotation) and bake it into the velocity. * * This is what a `spawned(node)` handler calls after turning the clone, and * the only way a `'forward'` projectile ever re-reads its rotation. */ /** * Bake the velocity from `direction` × `speed` — plus `carry`, the SHOOTER's * velocity, so a shell fired from a tank doing 16 m/s flies with it instead * of at the standing tank's speed (a tank could outrun its own shot; a * sideways shell landed behind what the barrel pointed at). Added once, at * launch; a later `speed` or `direction` write re-bakes with the same carry. */ aim(direction?: JsonValue | undefined, carry?: readonly number[] | undefined): void; private carry; override onReady(): void; override update(dt: number): void; /** Resolve `direction` to a unit vector, deriving 'forward' from rotation. */ private resolveDirection; private forwardFromRotation; } //#endregion //#region src/gameplay/save-point.d.ts /** * The save, as something the scene can point a signal at. * * Every other common wiring in this engine became declarative — score to * screen, damage to health bar, waves, win and lose — and saving stayed the one * thing that needed TypeScript. The skill's own autosave example is a * connection to `Game.onCheckpoint`, a method the author has to go and write. * * "*When* to save is a design decision and only your game knows" is right, and * it is untouched here: the SCENE still says when, by choosing which signal to * wire — a level exit's `triggerEnter`, `won`, a key. What it gets is something * to wire TO. * * ```json * { "name": "Save", "type": "Node", "uid": "n_…", * "script": { "name": "SavePoint", "props": { "game": "vault", "slot": "1" } } } * ``` * ```json * { "signal": "triggerEnter", "from": "Level/Exit", "to": "Save", "handler": "save" } * ``` */ declare class SavePoint extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; game: string; slot: string; label: string; scene: string; restoreOnReady: boolean; probeOnReady: boolean; keep: string[]; /** Poses read out of the slot, waiting for `announce()` to put them back. */ private keptPoses; /** Seconds this run has been playing — written into the slot as `playtime`. */ private elapsed; private slots; private store; /** * Has this run already said the saves are going nowhere? * * `saved(slot)` is the engine's own instrument for "it saved", and it fired * identically in a private window where the write landed in a Map that dies * with the tab. In-session everything looked healthy — the slot read back, * `slotScene()` answered — and across the reload the score went 5 to 0 and * `list()` went `["1"]` to `[]`, with `engine.log.entries().length = 0` and * `stats().errors = 0` in every phase. Byte-identical to a healthy control. * * So it goes through `engine.log`, not just the console: that is what * `incanto-logs` reads and what the `says` rung reports. */ private warnedVolatile; private pendingRestore; private pendingProbe; override onReady(): void; override update(dt: number): void; /** * Is there anything to continue? Emits `hasSave(label, playtime, scene)` or * `noSave`, and touches nothing. * * The label leads because that is what a menu shows; an unlabelled slot * falls back to its scene key, so `hasSave → SlotInfo.setText` always says * something rather than nothing. */ probe(): void; /** Seconds this run has been playing, including time carried over a continue. */ get playtime(): number; /** * The scene key this slot was saved from — what a loader needs to route. * Null when the slot is empty. */ slotScene(): string | null; /** * Write the run to its slot. Emits `saved(slotId)`. * * Wire it to whatever marks the moment. Extra signal arguments are ignored, * so `triggerEnter → save` works as-is. */ save(): void; /** * Give this scene the slot's state back. Emits `restored(count)`, or * `noSave` when the slot is empty — which is what greys out a Continue button. * * Restoring does NOT route: if the slot names another scene, that is your * loader's business, and `slot.scene` is the key it needs. */ restore(): void; /** Forget this slot — a "delete save" button. */ clear(): void; /** The run's own clock — and the poses the scene asked to keep. */ override serialize(): JsonValue; override deserialize(data: JsonValue): void; /** * Put the kept nodes back where the run left them. * * In `announce()` and not in `deserialize()`, for the reason the hook exists: * a pose written mid-pass can be overwritten by another behavior's restore * (a camera rig, a controller reading its target), and announce runs after * the whole pass has landed. */ override announce(): void; /** Read the poses the scene named — reporting the ones it cannot. */ private readPoses; } //#endregion //#region src/gameplay/score-keeper.d.ts /** * The game's state hub — score, lives, and win/lose detection. Put it on the * Root/Game node and wire gameplay signals into its methods (e.g. a Pickup's * `collected → addScore`, a Health's `died → loseLife`). * * - `score` rises via `addScore` / `setScore`; reaching `scoreToWin` (>0) * emits `won` once. * - `lives` (>0) shrinks via `loseLife`; reaching 0 emits `lost` once. */ declare class ScoreKeeper extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; score: number; lives: number; scoreToWin: number; private hasWon; private hasLost; /** * Whether the run is over — readable, not only heard. A stealth yard's HUD * and a melee arena's both read `score.won` for their banner and got * `undefined`, silently, because only the signals were public. */ get won(): boolean; get lost(): boolean; override onReady(): void; /** Add `n` to the score (negative subtracts), emit `scoreChanged`, check win. */ addScore(n: number): void; /** Set the score to `value`, emit `scoreChanged`, check win. */ setScore(value: number): void; /** Lose one life (no-op when lives disabled or already lost). Emits `lifeLost`, then `lost` at 0. */ loseLife(): void; /** The run's progress. `scoreToWin` is balance and comes back from JSON. */ override serialize(): JsonValue; override deserialize(data: JsonValue): void; /** * The score line and the lives counter. NOT `won`/`lost`. * * Re-firing those on load is how a Continue lands straight on the game-over * screen it was loaded to escape — the flags come back through `hasWon` / * `hasLost` and the game reads them, rather than being told again. */ override announce(): void; } //#endregion //#region src/gameplay/spawner.d.ts /** * Drip-feed clones of a template into the scene on a timer — enemy generators, * pickup fountains, particle emitters. `prefab` is a node PATH to a template * (usually a hidden `visible: false` child) that gets cloned each `interval`. * * - `max` caps LIVE instances (0 = unlimited); the count drops automatically as * spawned children free themselves (e.g. via `Lifetime` or `Health.died`), so * the spawner refills. * - `total` caps LIFETIME spawns (0 = infinite); on the last one it emits * `finished` and stops. * - `autoStart` (default) begins ticking at ready; otherwise call `start()`. * * Emits `spawned(node)` per spawn. Methods: `spawn()`, `start()`, `stop()`. */ declare class Spawner extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; prefab: string; interval: number; max: number; at: number[]; autoStart: boolean; total: number; into: string; private readonly source; private template; private readonly live; private timer; private running; private spawnedCount; private done; /** Live (un-freed) spawned instances. */ get liveCount(): number; /** @internal The detached template node (test/debug only). */ _templateForTest(): Node; override onEnterTree(): void; override onReady(): void; /** Begin (or resume) interval spawning. */ start(): void; /** Pause interval spawning (spawn() still works). */ stop(): void; /** Spawn one immediately (ignores the timer; still respects max/total). */ spawn(): Node | null; override update(dt: number): void; /** Drop instances that have been freed (parent cleared on free/queueFree). */ private prune; } //#endregion //#region src/gameplay/wander.d.ts /** * Aimless roaming inside a circle around the spawn point — idle critters, * ambient wildlife, restless guards. Picks a random destination within `radius` * of where it started, walks there at `speed`, then (every `changeEvery` * seconds, or on arrival) picks a new one. Uses `this.rng`, so a seeded engine * wanders identically every run (replayable, test-stable). * * Dimension-agnostic: roams in the plane of however many position components * the node has (2D `[x,y]`, 3D `[x,z]` ground plane keeping y). */ declare class Wander extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; speed: number; radius: number; changeEvery: number; private origin; private goal; private timer; override onReady(): void; override update(dt: number): void; private pickGoal; } //#endregion //#region src/gameplay/wave-spawner.d.ts /** * Sequenced enemy waves — the survivor/tower-defense backbone. Each entry in * `waves` is `{ prefab, count, interval, delayBefore }`: after `delayBefore` * seconds it spawns `count` clones of `prefab` (a hidden template path) one * every `interval` seconds, then waits until they're ALL cleared (freed) before * starting the next wave. * * Signals: `spawned(node)` per clone, `waveStarted(i)` when a wave begins * spawning, `waveCleared(i)` when its last instance frees, `allCleared` after * the final wave clears. */ declare class WaveSpawner extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; waves: JsonValue[]; autoStart: boolean; private readonly source; private parsed; private readonly live; private waveIndex; /** The wave now running or being cleared, 1-based — what a HUD prints as `wave 2 / 5`. `0` before the first. */ get wave(): number; /** How many waves were declared. */ get waveCount(): number; private phase; private timer; private spawnedThisWave; private announced; override onEnterTree(): void; override onReady(): void; /** Start (or restart from) wave 0. */ start(): void; /** * Which wave you were on — and nothing else. * * `behaviorsWithoutSave` is documented as "the list to read before shipping", * and nobody had read it against this repo's own examples. Five of them put a * `WaveSpawner` at the centre of the game (`fps-3d`, `tps-3d`, `tps-3d-neon`, * `tps-3d-ruins`, `dogfight-3d`), and every one of them saved a run on wave 3 * and continued it on wave 1 — silently, because a save reloads the scene * from source and a fresh `onReady` honestly starts at the beginning. * * The wave RESTARTS rather than resuming mid-spawn, which is the same * checkpoint rule the rest of the save system follows: spawned entities are * never restored, so resuming "four enemies into wave 3" would mean resuming * a wave whose enemies do not exist. `waveStarted` fires again for the HUD. */ override serialize(): JsonValue; override deserialize(data: JsonValue): void; override update(dt: number): void; private beginWave; private prune; private parseWave; } //#endregion //#region src/gameplay/zombie-ai.d.ts /** * The staple "zombie / monster" AI: SHAMBLE around on its own, then LOCK ON and * CHARGE a target (the player) once it wanders within `aggroRange` — giving up * again past `deAggroRange` (hysteresis, so it doesn't flicker at the boundary). * One behavior covers both phases so a single node can be a complete enemy * (`Wander` + `Chase` can't co-exist — a node carries ONE behavior). * * While NOT aggroed it roams: with `goalTarget` set it DRIFTS toward that node * (e.g. the objective the horde is marching on) with random lateral jitter, so it * reads as wandering yet still advances; without one it roams around its spawn. * * Emits `movementStateChanged('idle'|'walk'|'run')` on every change so a skin can * swap animation clips (shamble while roaming, sprint while charging), plus * `enteredAggro` / `exitedAggro` (wire to a growl, a glow, a speed-up). * * Dimension-agnostic; in 3D it moves only in the ground plane (x,z) and leaves * the up axis (y) to physics, so a CharacterBody3D settles on terrain. * * `moveParent` (default false) moves the parent instead of this node — the * AI-on-a-child pattern, so the entity ROOT can hold `Health` (clone-safe * `freeOnDeath`) while this child drives movement. */ declare class ZombieAI extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; aggroTarget: string; aggroRange: number; deAggroRange: number; chaseSpeed: number; wanderSpeed: number; wanderRadius: number; wanderChangeEvery: number; goalTarget: string; stopRange: number; moveParent: boolean; private aggro; private started; private home; private goal; private timer; private lastState; /** The node we actually move (the parent under moveParent, else this one). */ private mover; private effectiveDeAggro; override onReady(): void; override update(dt: number): void; /** Move `mover` toward `to` in the GROUND PLANE; returns true once within * stopRange (no overshoot). Leaves the up axis (3D y) to physics. */ private approach; /** Distance ignoring the up axis in 3D (so terrain height never blocks aggro). */ private groundDistance; private setState; private pickGoal; } //#endregion //#region src/gameplay/ballistics.d.ts /** * The two questions a lobbed shot asks: **at what angle**, and **where will it * land**. * * `Projectile.gravity` has flown arcing shells since it shipped, and nothing * could aim one: `Turret` pointed straight at its target, so a gun firing a * falling shell at anything further than a few metres dropped it short — a * measured 66 m short, on an artillery duel built from the package. Every game * that wanted a mortar, a grenade, a catapult or a lobbed spell wrote this * arithmetic itself, and a preview drawn with slightly different arithmetic * lands somewhere the shell does not. */ /** * The unit direction that puts a shot from `from` onto `to`, or `null` when * the speed cannot reach it. * * `arc` picks the root: `'low'` is the flat, fast one (a rifle grenade), * `'high'` the mortar's — same landing point, a much taller flight, and the * one that clears a wall in between. * * Dimension-agnostic, like everything else here: a 3D shot spreads over x/z * with +y up, a 2D one over x with −y up (the y-down screen convention * `Projectile.gravity` already follows). */ declare function ballisticAim(from: readonly number[], to: readonly number[], speed: number, gravity: number, arc?: "low" | "high"): number[] | null; /** The angle that carries a shot FURTHEST — what a gun fires when it cannot reach. */ declare function maxRangeAim(from: readonly number[], to: readonly number[]): number[] | null; interface BallisticPathOptions { /** How long to walk, in seconds (default 12). */ seconds?: number; /** The step — leave it at the engine's fixed step so the walk matches the flight. */ dt?: number; /** Called with each point; return true to stop (the ground, a wall, the map edge). */ stop?: (at: readonly number[]) => boolean; } /** * Walk the flight a `Projectile` with this `speed`, `direction` and `gravity` * WOULD fly — the same integration, step for step. * * That is the point: an aiming line drawn from the closed-form parabola drifts * from the shell the engine actually flies (Euler with the fall accumulated * first), and the player aims by the line. Same arithmetic, same landing. */ declare function ballisticPath(from: readonly number[], direction: readonly number[], speed: number, gravity: number, options?: BallisticPathOptions | undefined): number[][]; //#endregion //#region src/gameplay/blast.d.ts declare class Blast extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; radius: number; damage: number; falloff: "linear" | "none"; groups: string; when: string; self: boolean; throughWalls: boolean; private went; /** Has it gone off? A keg explodes once. */ get spent(): boolean; private get carrier(); override onReady(): void; /** Go off now: everything with a `Health` inside `radius` takes its share. */ blast(): number; /** Who is even a candidate: the named groups, or every HEALTH in the tree. */ private candidates; /** * `throughWalls: false` — a static-only ray, the same one * `Turret.requireSight` casts, with the two things a BLAST has to know that * a turret does not. * * A bomb lies on the ground, so its ray starts inside the floor's collider * and Rapier reports a hit at zero distance: every victim looked shielded, * by the ground the bomb was standing on. And a crate solid enough to walk * into is a collider between the blast and the crate's own centre, so the * first thing a bomb could never destroy was the box beside it. Neither is a * wall: a hit at the origin is what the blast is sitting on, and a hit that * belongs to the target is the target. */ private reaches; } //#endregion //#region src/gameplay/checkpoint.d.ts /** * A checkpoint: touch it and a `Respawn` puts you back HERE from now on. * * Sits on an `Area2D`/`Area3D` (listens to `triggerEnter`). `respawn` names the * `Respawn` node whose `to` becomes this checkpoint's world position, less * `dropBelow` so the character lands ON the flag rather than inside it. * `group` gates who can light it; `once` (default) lights it a single time. * Emits `activated(other)`. * * Built after a platformer built from the tarball wrote exactly this in nine * lines — the ninth being the one that found `Respawn.to` was read once at * ready. A checkpoint is the second most common thing a platformer has, and it * should be one node with two props, not a behaviour every game rewrites. */ declare class Checkpoint extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; respawn: string; group: string; once: boolean; dropBelow: number; /** True after the first activation. */ lit: boolean; /** * Where this checkpoint sent the catcher, and WHEN it did. * * The position is not derivable at restore: a checkpoint can be lit while its * own node has since moved (a lift, a boat), and `dropBelow` is applied once * at lighting. The clock is what orders three lit checkpoints back into the * order the run walked them. */ private sentTo; private litAt; override onReady(): void; /** * What a save has to carry: WHICH checkpoint the run had reached. * * `Checkpoint` had none of this, so a continued run came back with every * checkpoint dark and the `Respawn` pointing at the level's start — the first * fall after a continue undid the whole session. It was not in the skill's * list of built-ins that save either, so nothing said it would not. */ override serialize(): JsonValue; override deserialize(data: JsonValue): void; /** * Point the catcher back where the run left it. * * Every lit checkpoint sharing a catcher runs this, and they must not fight: * each one writes only if no lit sibling on the same `Respawn` was walked * LATER, so all of them reach the same answer and the order they announce in * does not matter. */ override announce(): void; /** Was another lit checkpoint on this same catcher reached after this one? */ private laterSiblingLit; } //#endregion //#region src/gameplay/day-night.d.ts /** * A live day/night cycle — drives the scene's `environment` (sun elevation + * azimuth, ambient, exposure) through a full 24-hour loop. Attach anywhere * (the scene root is natural); requires a 3D scene with an atmosphere sky. * * { "script": { "name": "DayNight", "props": { "daySeconds": 240, "startHour": 9 } } } * * `hour` (0-24, readable/writable) is the single source of truth — set it to * jump to a time of day, or set `paused: true` and animate it yourself. * Emits `dayPhaseChanged('dawn'|'day'|'dusk'|'night')` on transitions — hook * spawners/lights/music to it. */ type DayPhase = "dawn" | "day" | "dusk" | "night"; declare class DayNight extends Behavior { static readonly signals: readonly string[]; static readonly props: PropSchema; daySeconds: number; startHour: number; maxSunElevationDeg: number; nightDarkness: number; paused: boolean; /** Current clock hour (0-24). Write it to jump the time of day. */ hour: number; private phase; private applyClock; private baseAmbient; private baseExposure; private captured; /** Said once: there was no sky to move when this first looked. */ private reportedNoSky; /** * What time it is. A save that forgets it returns you at dawn. * * `startHour` is the scene's opening, not the run's clock: a fresh `onReady` * cannot know that this player has been out all night. `paused` goes with it * because a game that stops the clock for a cutscene must not resume ticking * on load. */ override serialize(): JsonValue; override deserialize(data: JsonValue): void; override onReady(): void; override update(dt: number): void; private applyHour; } declare function phaseOf(hour: number): DayPhase; //#endregion //#region src/gameplay/float-away.d.ts /** * Rise, fade, and be gone — the second half of a damage number. * * `Label3D` gave a game text standing in the world; every game then writes the * same eight lines to move it up and fade it out, and the interesting part is * not the eight lines, it is the three things they all get wrong: the node is * freed on the frame it becomes invisible rather than a second later, the fade * fights whatever else writes `opacity`, and a hundred numbers a fight each * leave a node behind because nothing ever frees them. * * ```json * { "name": "Hit", "type": "Label3D", * "props": { "text": "12", "height": 0.35, "color": "#ff5a5a" }, * "script": { "name": "FloatAway", "props": { "rise": 1.2, "seconds": 0.7 } } } * ``` * * Spawning one is then `duplicateNode` + set `text` + `position` — no per-frame * code of your own, and no cleanup to forget. * * It writes `opacity` and the node's own `position`, so it composes with * anything that does not. Works on any node with those props: a `Label3D`, a * `Sprite3D` pickup puff, a 2D `Label`. */ declare class FloatAway extends Behavior { static override readonly props: { /** World units travelled over the whole life (negative sinks). */rise: { default: number; }; /** How long the whole thing takes. */ seconds: { default: number; }; /** Fraction of the life spent at full opacity before the fade starts. */ hold: { default: number; }; /** Sideways drift, so ten numbers at once do not stack into one. */ drift: { default: number; }; /** Free the node when it finishes. Off for something you re-use. */ freeOnEnd: { default: boolean; }; }; rise: number; seconds: number; hold: number; drift: number; freeOnEnd: boolean; private elapsed; private start; private done; override onReady(): void; override update(dt: number): void; } //#endregion //#region src/gameplay/game-flow.d.ts /** * Reload the CURRENT scene from its source JSON — fresh nodes, reset physics, * rewired input. The restart primitive every game-over screen wants. */ declare function restartScene(engine: Engine): void; type GameFlowState = "playing" | "paused" | "gameover" | "won"; /** * Swap to another scene behind a black fade (title→game→next level). * Headless (no DOM) the swap is immediate. Restores timeScale to 1. * * ONE AT A TIME. A door is usually a trigger, and a player who walks along a * wall of them fires several inside one fade. Measured on three doorways passed * in about a second: * * ``` * setScene calls: 3 | order: ["crypt","garden","forge"] * final scene : forge * ``` * * Three complete scenes loaded and the player landed in the third — a level * they never walked to, from a door they never reached. Further calls while a * travel is in flight are IGNORED rather than queued: you already went through * a door, and the second one is the same input arriving twice. * * The outgoing scene also stops while the screen is black. It kept simulating * through the fade, so a player who triggered the door mid-fall died in the * level they were leaving — and the checkpoint that death wrote was for the * level they had left. */ declare function goToScene(engine: Engine, sceneJson: JsonObject, opts?: { fadeSeconds?: number; }): void; /** * The win/lose/restart state machine 6+ examples hand-rolled as `over`/`win` * booleans. Attach to any node (the scene root is natural): * * const flow = root.behavior as GameFlow; // script: { "name": "GameFlow" } * flow.gameOver('YOU DIED'); // freezes time, sticky banner * flow.win('AREA CLEAR'); * flow.pause(); flow.resume(); * * While in `gameover`/`won`, pressing `restartAction` (default action name * 'restart' — declare it in the scene input map, or leave undeclared and call * `flow.restart()` yourself) reloads the scene from source. Banners render * through a `%Banner` UiBanner when one exists; otherwise states are silent * (drive your own UI off the `flowChanged` signal). */ declare class GameFlow extends Behavior { static readonly signals: readonly string[]; static readonly props: PropSchema; /** Input action that restarts from gameover/won (declare it in `input{}`). */ restartAction: string; /** Freeze `engine.timeScale` on gameover/won (banner UI keeps rendering). */ freezeOnEnd: boolean; /** Where the flow looks for a UiBanner ('' = never). */ bannerPath: string; /** * Input action that toggles pause (declare it in `input{}`; undeclared is * fine and means pause is API-only). */ pauseAction: string; /** Actions already reported missing, so a per-frame poll says it once. */ private readonly reportedActions; /** * Poll an action this flow is allowed to be missing, and say so once when the * author named one that does not exist. * * Tolerated because `pause()` and `restart()` stay API-only in a game with no * such key. Reported because a typed-out `"restartAction": "retry"` that is * never declared leaves a frozen game-over screen that answers nothing, and * the engine has already thrown the sentence that fixes it. */ private readAction; /** * A node shown while paused and hidden otherwise — a `UiPanel` of buttons in * practice. '' turns the whole thing off. * * This lives here rather than in each game because it is identical in all of * them, and a pause menu that costs a script is a pause menu no generated * game will have. `UiPanel` appeared in zero template scenes before this. */ pausePanelPath: string; /** * A screen shown at BOOT, holding the world until something resumes. * * '' (the default) is a game that starts playing, which is every game this * engine shipped until now. Naming a panel here is the whole of a title * screen: the flow pauses in `onReady`, shows this instead of the pause * panel, and a `PLAY` button wires straight to `resume` — * * ```json * { "from": "%Play", "signal": "pressed", "to": ".", "handler": "resume" } * ``` * * Every shipped shell wrote that in TypeScript, together with the eighty * lines below it, and `incanto-hud.md` told authors to copy one. */ titlePanelPath: string; /** * Where this game keeps its saves — a `SaveSlots` namespace. `''` = no saving. * * `SaveSlots`, `captureState` and `restoreState` have existed since the save * round and no shipped starter used any of them: a scaffolded game could not * be closed and come back. The API was never the problem; the SEQUENCE was — * write a slot, reload the scene from source, wait for the reload, restore by * uid — and every game that wanted a save menu had to get that right before it * had one. `restart()` is already the engine's verb for "reload this scene", * wired from a button with no TypeScript; `save()` and `continueFrom()` are * its two siblings. * * WHEN to save is still yours. The skill's rule stands — there is no autosave * — you wire `save` to whatever signal marks the moment: a checkpoint, a level * end, a button in the pause menu. */ saveSlots: string; /** Lazily built, so a game with no `saveSlots` never touches storage. */ private slots; private store; /** * Write this scene's behaviour state into a slot. `false` = nothing was * written, and the log says why. */ save(slot?: unknown): boolean; /** Is there something in this slot to come back to? */ hasSave(slot?: unknown): boolean; /** * Reload this scene and hand every behaviour its state back. * * The order is the whole feature: `restoreState` has to run AFTER the reload * and after `onReady`, because `onReady` is where a behaviour sets its * starting values and restoring first would be overwritten. */ continueFrom(slot?: unknown): boolean; /** * Screens pushed OVER the base one — options, credits, controls. * * The genuinely hard part of a shell is BACK. Options opens from the title * AND from the pause menu and has to return to whichever asked, which is a * stack; every game that hand-rolls it eventually gets a screen stuck. Two * connection rows now: * * ```json * { "from": "%TitleOptions", "signal": "pressed", "to": ".", * "handler": "screen", "args": ["%Options"] }, * { "from": "%Back", "signal": "pressed", "to": ".", "handler": "back" } * ``` */ private readonly screens; /** * Every screen this flow has ever raised — the set it is allowed to HIDE. * * Popping the stack before syncing would drop the closing screen out of the * list of panels to consider, so `back()` showed the title again and left * Options on top of it. A screen that has been opened stays this flow's to * close. */ private readonly everShown; /** * Is the BASE screen the title rather than the pause menu? * * Both are "the world is held and a panel is up", and only this tells them * apart — so `back()` from Options knows which one to bring back. */ private onTitle; /** * Push a screen over whatever is showing, and hold the world while it is up. * * A connection handler, so a button reaches it with no script: * `{"handler": "screen", "args": ["%Options"]}` — the bound argument is the * panel's node path. * * Opening a screen during PLAY freezes the world (an inventory, a map), and * `back()` gives it back. Opening one while already paused keeps the pause. */ screen(path: unknown): void; /** * Close the top screen and show whoever asked for it. * * With nothing left on the stack this returns to the base screen — the title * at boot, the pause menu while paused — and if there is no base screen * either, it resumes: a screen opened during play closes back into the game. */ back(): void; state: GameFlowState; private frozeScale; /** Dropped when this flow's node leaves the tree — see `onReady`. */ private unwatchScene; override onExitTree(): void; override onReady(): void; /** * The panel follows the state; nothing else may own its visibility. * * …and while it is up, the layer it lives on takes ARROW KEYS AND THE PAD. * `HudLayer.focusNavigation` is off by default and rightly so — a game whose * HUD happens to contain a button must not lose its arrow keys the moment one * exists — and `incanto-hud.md` says "turn it on for the screens that ARE * menus and off again when play resumes". A pause panel IS that screen, and * this is the one place in the engine that knows when it opens and closes, so * every game with a pause menu was writing the same two lines or shipping a * menu no controller could reach. * * The layer's own value is remembered and put back: a game that turned * navigation on for its own reasons keeps it after a pause. */ private syncPausePanel; /** The layer's own `focusNavigation`, while this flow is borrowing it. */ private navigationWas; gameOver(text?: string, color?: string): void; win(text?: string, color?: string): void; pause(): void; resume(): void; restart(): void; /** Fade to another scene (next level, back to title). */ goToScene(sceneJson: JsonObject, opts?: { fadeSeconds?: number; } | undefined): void; private transition; private freeze; private thaw; override update(): void; } //#endregion //#region src/gameplay/group-camera.d.ts /** * One camera, several players — the couch co-op camera. * * `FollowCamera` follows ONE target and `ChaseCamera` rides behind ONE * character, so a game with two players at the same keyboard had nothing to * frame them both: whoever the camera followed could walk the other off screen, * and the other player's half of the game went with them. * * This keeps EVERY target in frame: the camera sits at their centroid plus * `offset`, and pulls BACK along that offset until the pair that is furthest * apart fits inside `padding` of the view. It is the Overcooked / Castle * Crashers / Lovers-in-a-Dangerous-Spacetime camera. * * ```json * { "name": "Camera", "type": "Camera3D", "props": { "current": true, "fov": 50 }, * "script": { "name": "GroupCamera", * "props": { "targets": "player", "offset": [0, 12, 14], * "minZoom": 0.8, "maxZoom": 2.6, "smoothing": 0.12 } } } * ``` * * Pair it with `CharacterController3D`'s `camera: "none"` on every player, or * the controllers will fight it for the camera every frame. */ declare class GroupCamera extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; targets: string; offset: number[]; minZoom: number; maxZoom: number; padding: number; smoothing: number; lookAtGroup: boolean; /** The zoom actually in use — 1 is the authored offset. */ zoom: number; /** The point the camera is framing. */ centre: number[]; override onReady(): void; /** Everyone the camera is trying to keep on screen, right now. */ members(): Node[]; override update(dt: number): void; /** Snap to the group now — after a teleport, a scene swap, a respawn. */ snap(): void; } //#endregion //#region src/gameplay/juice.d.ts /** * Game-feel primitives ("juice"): the weapon cooldown, camera shake, screen * flash and hit-stop that action examples kept hand-rolling. */ /** * A fire-rate / ability cooldown — replaces the copy-pasted * `this.clock += dt * 1000; if (now > nextFire)` pattern: * * private gun = new Cooldown(0.2); * override update(dt: number): void { * this.gun.tick(dt); * if (this.engine.input.isPressed('fire') && this.gun.tryUse()) this.shoot(); * } */ declare class Cooldown { readonly seconds: number; private remaining; constructor(seconds: number); /** Advance time. Call once per update with the frame dt. */ tick(dt: number): void; get ready(): boolean; /** 0..1 — how far through the cooldown we are (1 = ready). UiBar-friendly. */ get progress(): number; /** Consume if ready. Returns whether the action should fire. */ tryUse(): boolean; /** Force-ready (pickups that reset your reload). */ reset(): void; } /** * Camera shake as a standalone behavior for cameras WITHOUT another script. * (FollowCamera has this built in — one script per node.) Composes with any * other position writer by applying only the DELTA of its own offset, so the * camera returns exactly to where the other writer left it. * * (camera.behavior as CameraShake).shake(8); // pixels (2D) / meters·100 feel (3D: use ~0.2) */ declare class CameraShake extends Behavior { static readonly props: Record; /** Seconds a shake takes to decay to zero. */ falloff: number; private magnitude; private t; private prev; /** * Decoration draws from its OWN stream — see `effectRng`. * * A shake drew from `engine.rng`, the stream game logic reads and * `captureState` saves, once per axis per RENDERED frame. So the player's * frame rate decided the game's random numbers, and toggling the * `reduceMotion` accessibility switch changed the rolls: a replay recorded at * 60 fps does not reproduce on a laptop that dropped to 30, and two players * with the same seed diverge because one of them was hit. The particle * siblings were moved off `engine.rng` for exactly this; both camera shakes * were left behind. */ private shakeRng; /** * Start a shake — unless the player asked for no motion. * * `settings.reduceMotion` is an accessibility switch, and camera movement the * player did not cause is the vestibular trigger it exists for. Checked HERE * rather than in `update` so a shake requested while it is on leaves nothing * queued to play the moment it is turned off. * * It deliberately does NOT touch gameplay timing (`hitStop`): freezing time * for 80 ms is feedback, not motion, and removing it would change what the * game is rather than how it moves. */ shake(magnitude: number, seconds?: number): void; override update(dt: number): void; } /** * Full-screen color flash (damage red, pickup white). DOM overlay above * everything; headless it draws nothing but still REPORTS, so a damage flash * can be checked in a run with no screen (`engine.effects.countOf('#ff0000')`). * * The engine is the first argument because it is the only juice primitive that * did not take one — which is also why nothing in the repo called it, and why a * flash was the one piece of feedback no test could see. */ declare function screenFlash(engine: Engine, color?: string, opacity?: number, seconds?: number): void; /** * Hit-stop: freeze game time for `seconds` of REAL time, then restore the * previous timeScale. Stacking calls extend the freeze instead of fighting. */ declare function hitStop(engine: Engine, seconds?: number): void; //#endregion //#region src/gameplay/path-follow.d.ts /** * Walk a list of waypoints at a constant speed, then stop — the delivery * end of `findPath`: * * const cells = findPath(grid, start, goal); * (npc.behavior as PathFollow).setPath(cells.map(([cx, cy]) => [cx * TILE, cy * TILE])); * * Waypoints are world positions in the node's own units (2D px / 3D m; use * [x, y] or [x, y, z] to match the node). Emits `waypointReached(index)` * per point and `arrived()` at the end. `loop: true` patrols the ring. */ declare class PathFollow extends Behavior { static readonly signals: readonly string[]; static readonly props: PropSchema; /** Units per second (px in 2D, meters in 3D). */ speed: number; loop: boolean; moveParent: boolean; path: string; autoStart: boolean; private declared; facePath: string; turnSpeed: number; private waypoints; private index; /** True while there is somewhere left to go. */ get moving(): boolean; setPath(waypoints: readonly (readonly number[])[]): void; override onReady(): void; /** Walk the declared `path` from its first waypoint — the cue a connection can give. */ start(): void; /** Stop in place (keeps the node where it is). */ stop(): void; override update(dt: number): void; /** `facePath` turns toward where this frame went — nothing when it did not move. */ private face; /** The node this walks: the parent under `moveParent`, else this one — both must be spatial. */ private mover; } //#endregion //#region src/gameplay/prefab-shelf.d.ts /** * A shelf of templates the GAME clones itself — towers, bolts, damage numbers. * * `Spawner` detaches its one prefab at enter, so a spawned enemy's whole * subtree is live and nobody has to think about it. A game that clones on its * own terms — a tower the player places, a bolt that tower fires — has no * spawner, so its templates sat in the tree as LIVE nodes: their behaviours * ticked, their turrets shot, and `visible: false` hid a node without switching * it off. The documented answer was to author every template `"enabled": false` * and wake the clone with `clone.behavior?.enable()`. * * That wakes ONE node, and the same docs teach that a node carries one * behaviour — so a prefab's parts live on children, and every one of those * stays asleep, silently and for good, because `enabled` is a pause switch that * never complains. Measured on a shipped tower defense: the bolt's * `Projectile` was woken and its sibling `Lifetime` was not, so every bolt that * MISSED flew forever — five still in the tree at 69 seconds, in a game that * passed its harness, its audit and every rung of the ladder. * * ```json * { "name": "Prefabs", "type": "Node2D", "script": { "name": "PrefabShelf" }, * "children": [ { "name": "Tower", "type": "Node2D", "children": [] } ] } * ``` * ```ts * const shelf = this.node.getNode('/Game/Prefabs').behavior as PrefabShelf; * const tower = shelf.make('Tower'); // detached, like `duplicateNode` * tower.position = at; // set it up BEFORE it readies * this.node.getNode('/Game/Towers').addChild(tower); * ``` * * The children never ENTER the tree, so nothing has to be asleep, nothing has * to be hidden, and `make()` hands back a clone that is awake all the way down. */ declare class PrefabShelf extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; prefabs: string; /** Templates, in the order they were authored. */ private readonly held; override onEnterTree(): void; /** A shelf attached at runtime never saw `onEnterTree`; idempotent. */ override onReady(): void; private claim; /** What this shelf holds, in authored order. */ names(): string[]; /** * The held template itself — to ASK it something, never to change it. * * `Turret.arc` needs the shell's `speed` and `gravity` to solve a firing * angle, and it needs them BEFORE it makes a clone (the barrel elevates * every frame, not only when the gun fires). Re-declaring those numbers on * the turret would be two surfaces free to disagree; reading the one that * flies is the answer. Mutating what comes back changes every future clone. */ template(name: string): Node | null; /** * A live clone of one template, DETACHED — `addChild` it yourself. * * Detached for the same reason `duplicateNode` is, and it is not a * formality: `onReady` fires on attach, and a behaviour that reads its node * there (`FloatAway` banks its starting position) would bank the wrong one. * Set the clone up, then attach it. * * The clone is VISIBLE even if the template was authored hidden (a scene * migrating off the old `visible: false` shelf keeps working), and every * behaviour in it is however it was authored: awake unless the template * itself asks to sleep, which is now only ever a state machine's own doing. */ make(name: string): Node; private childNames; } //#endregion //#region src/gameplay/sight.d.ts /** * Eyes for a watcher — a guard, a turret, a camera, a beast. * * ```json * { "name": "Guard", "type": "CharacterBody3D", "children": [ * { "name": "Skin", "type": "ModelInstance3D", "props": { "model": "$base" } }, * { "name": "Eyes", "type": "Node", * "script": { "name": "Sight", "props": { "range": 12, "coneDeg": 70, "fillSeconds": 1 } } }, * { "name": "Hunt", "type": "Node", "script": { "name": "Chase", "props": { "enabled": false } } } * ] } * ``` * ```jsonc * { "signal": "spotted", "from": "Guard/Eyes", "to": "Guard/Hunt", "handler": "enable" }, * { "signal": "lost", "from": "Guard/Eyes", "to": "Guard/Hunt", "handler": "disable" } * ``` * * On a child of the watcher's body. Every frame it looks the way the node at * `facingPath` faces (the Skin the controller turns, +Z-forward) and sees * anything in `targetGroup` inside `coneDeg` and `range` with nothing * STATIC between its eye and their chest (`los`). `suspicion` fills over * `fillSeconds` while something is seen and drains when nothing is; * `spotted(node)` fires when it reaches 1, `lost(node)` when it empties — * once each, an edge, so a connection can flip a state. `sees(node)`, * `seen()` and `target` (the nearest) answer any frame. * * Five examples wrote this by hand before it existed, and `Chase` still * chases through walls — give an enemy eyes and it stops. */ declare class Sight extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; range: number; coneDeg: number; targetGroup: string; facingPath: string; eyeHeight: number; chestHeight: number; fillSeconds: number; los: boolean; /** 0..1 — how sure it is; 1 is `spotted`. */ suspicion: number; /** The nearest node it sees right now, or null. */ target: Node | null; private seenNow; private alarmed; private lastTarget; /** Is this node in sight right now? */ sees(node: Node): boolean; /** Everything in sight right now, nearest first. */ seen(): Node[]; override update(dt: number): void; private facing; private distanceTo; } //#endregion //#region src/core/pose.d.ts /** The node's position in WORLD space — its parents' positions and rotations composed. */ declare function worldPosition(node: Node): number[]; /** * A LOCAL vector turned into world space by the node's own rotation and every * ancestor's — a barrel's forward, a muzzle's offset, a hardpoint's direction. * * `worldPosition` answers where a node IS; this answers which way it points, * and two games wrote it by hand (a tank's `forwardOf`, an artillery piece's * barrel tip) because nothing else could say it. `worldDirection(node, [0,0,1])` * is the +Z-forward convention every 3D skin in this engine follows. */ declare function worldDirection(node: Node, local: readonly number[]): number[]; /** The local `position` that puts `node` at `world` — the inverse of `worldPosition`. */ declare function localFromWorld(node: Node, world: readonly number[]): number[]; //#endregion //#region src/gameplay/turret.d.ts /** * The oldest verb in games: pick the nearest live member of `targetGroup` * within `range` and hit it every `cooldown` — a defense tower, a ship's gun, * a sentry, a trooper who fights what he meets. * * Two games in a row wrote these forty lines before it existed (an RTS's * trooper, a tower defense's tower): the engine had `Chase` for going TO a * target and `Projectile` for the bullet, and nothing that decides to shoot. * * - **Hitscan** by default: the target's `Health` (on it or a child, as * `DamageOnContact` finds it) takes `damage(n)`. * - **Or a clone**: name a `shelf` (a `PrefabShelf`) and a `prefab` on it; each * shot takes one, puts it at the turret plus `muzzle`, points its * `Projectile` at the target and adds it under `into` (the turret's parent * when empty). The clone does the hurting then — give it `DamageOnContact`. * - `facePath` turns a barrel toward the target at `turnSpeed` — and the * `muzzle` offset rides THAT node, so a shell leaves the tip of the barrel * wherever it happens to be pointing. * - **`arc`** lobs: a shell with `Projectile.gravity` aimed straight at * anything far away falls out of the sky long before it arrives, so a * lobbing gun solves its launch angle (`'low'` flat and fast, `'high'` over * whatever stands in between) and ELEVATES the barrel to match. * * Signals: `acquired(target)` when something comes in range, `fired(target)` * per shot, `lost()` when nothing live is in range any more. */ declare class Turret extends Behavior { static readonly props: PropSchema; static readonly signals: readonly string[]; targetGroup: string; requireSight: boolean; range: number; damage: number; cooldown: number; shelf: string; prefab: string; muzzle: number[]; into: string; arc: "flat" | "low" | "high"; facePath: string; turnSpeed: number; /** What it is shooting at right now, or null. */ target: Node | null; /** Shots fired so far. */ shots: number; private wait; override onReady(): void; /** `requireSight`: a static-only ray from the turret to the target reaches it. */ private canSee; override update(dt: number): void; /** Where a clone actually leaves from: the turret plus its muzzle, turned by the barrel. */ private muzzleAt; /** * The heading of this shot: straight at the target, or the arc that lands on * it. Solved from the MUZZLE, which is where the shell starts. */ private aimAt; /** The shell's own numbers, read off the template that will be cloned. */ private shellBallistics; /** One clone from the shelf, at the muzzle, aimed at the target, into the container. */ private launch; } //#endregion //#region src/gameplay/index.d.ts /** Every built-in gameplay behavior, keyed by its registration name. */ declare const GAMEPLAY_BEHAVIORS: Readonly>; /** * Register all built-in gameplay behaviors. Idempotent and hot-reload tolerant * (`replace: true`) — calling it twice, or after a user already registered a * same-named behavior, is safe; pass `replace: false` to fail on conflicts. */ declare function registerGameplayBehaviors(opts?: { replace?: boolean; }): void; //#endregion export { type BallisticPathOptions, Blast, CameraShake, Chase, ChaseCamera, Checkpoint, Collector, Cooldown, Currency, DamageOnContact, DayNight, type DayPhase, FaceTarget, FloatAway, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, type GameFlowState, GroupCamera, Health, Interactable, Lifetime, Mount, MoveTo, Oscillate, PathFollow, Patrol, Phases, Pickup, PrefabShelf, Projectile, SavePoint, ScoreKeeper, Sight, Spawner, Turret, Wander, WaveSpawner, ZombieAI, ballisticAim, ballisticPath, goToScene, hitStop, localFromWorld, maxRangeAim, phaseOf, registerGameplayBehaviors, restartScene, screenFlash, worldDirection, worldPosition };