# Requirements — platformer-2d (Incanto)

## Coding Patterns

- Game STRUCTURE belongs in `src/game.scene.json` (nodes, props, input map,
  assets, scripts, connections, viewport, physics). Prefer a BUILT-IN gameplay
  behavior over hand-writing logic — check
  `node_modules/incanto/skills/incanto-gameplay-behaviors.md` and
  `incanto-physics-and-input.md` BEFORE writing a Behavior class.
- MOVEMENT is the custom `PlayerController` on the player `CharacterBody2D` (the
  built-in `CharacterController2D` is intentionally stiff; a great platformer
  needs coyote-time/jump-buffer/double-jump/variable height/stomp). It integrates
  `velocity` + `moveAndSlide()` + `isOnFloor()` itself. Tune feel with the
  constants at the top of `behaviors.ts` (RUN_SPEED, JUMP_V, COYOTE, BUFFER,
  STOMP_BOUNCE, …), not by editing the loop.
- Custom logic in TypeScript (`src/behaviors.ts`):
  - `PlayerController` — run/gravity/coyote/buffer/double-jump/variable height,
    stomp + knockback, hearts+lives, checkpoint respawn, moving-platform carry,
    and the idle/run/jump sprite + facing + i-frame blink.
  - `GoblinSkin` — face the patrol heading + play the goblin walk clip (it lives
    on the goblin's `AI` child, so it reads the PARENT's position and `../Skin`).
  - `FollowCam` — follow the knight (look-ahead + smoothing + world clamp) AND
    screen-shake on demand (one script per node).
  - `ParallaxLayer` — scroll a castle backdrop slower than the camera.
  - `HudUpdater` — paint score/hearts/lives into the HUD + win/lose banner.
- DAMAGE + INTERACTIONS are GROUP-DRIVEN, not per-node wiring: tag a node with a
  group and `PlayerController` handles it by AABB each frame — `enemy`, `hazard`,
  `pit`, `checkpoint`, `goal`, `platform`. Adding an enemy/hazard = add a node in
  the right group. (Pickups are the exception: coins/gems use the built-in
  `Pickup` + `connections` `collected → ScoreKeeper.addScore` + `→ AudioPlayer.play`.)
- Moving platforms: `Patrol` (horizontal ferry) / `Oscillate` (vertical lift) on
  a `StaticBody2D` in the `platform` group — the `Feet` sensor + carry logic ride
  them. Bobbing pickups: `Oscillate` on the sprite child.
- Keep `App.tsx` thin: inject the four built-in asset urls (knight/goblin/coin/
  gem), boot `createGame2D` with the scene + behaviors, optional music, the
  on-screen JUMP button, remove the loader, expose `window.game`. There is NO
  respawn-via-scene-reload (respawn is in-level in `PlayerController`).
- Omit node `uid`s (the loader generates them).
- VERIFY headlessly first: `bun run verify` drives the real scene through
  `incanto/test`'s `runScript` and asserts run+jump, coin scoring, STOMP, side-
  hit hearts, checkpoint respawn, the win path, and the lose path.

## Known Issues / Constraints

- No collision LAYERS in v0 — gameplay collisions are resolved by `PlayerController`
  AABB against groups, so PLACE content in the right group; physics solids
  (`StaticBody2D`) are only for standing/colliding.
- `Health` dies once and exposes no revive, so the PLAYER deliberately does NOT
  use it — hearts/lives live in `PlayerController` + `ScoreKeeper`, and respawn is
  an in-level teleport to the last checkpoint (no scene reload, no lost progress
  jump-cut). Goblins are removed with `queueFree()` on stomp (no `Health` needed).
- Score is flavour: coins 10, gems 50; `scoreToWin` 100000 so ONLY the goal flag
  wins (collecting everything can't reach the threshold).
- Built on `incanto/2d` physics (auto-enabled — the scene has bodies). Player =
  `CharacterBody2D` (kinematic KCC), platforms/ground = `StaticBody2D`, pickups/
  hazards/checkpoint/goal/feet-sensor = `Area2D`.
- Sprites: `medieval-knight` (idle 0–5, move 6–11, attack 12–17; jump reuses a
  move frame) and `goblin` (idle 0–6, move 7–12). Swap a sheet by changing only
  the asset entry + the import in `App.tsx`.
- The parallax "castle" is styled `ColorRect2D` bands + towers (no tilemap node
  in v0). Background art is the obvious next polish pass; the hero sprites carry
  the look. Music is OPTIONAL (`MUSIC_URL` empty by default; SFX presets need no
  files).
