<userRequest>
YOU MUST follow these rules EXACTLY when vibe-coding on this Incanto template
(Hollow Horde — a top-down survivors-like):

1. 📖 READ THE SKILLS FIRST: `node_modules/incanto/skills/` is the engine manual,
   version-pinned to what is installed. Start with `incanto-building-2d-games.md`
   and `incanto-scene-json-authoring.md`, then `incanto-gameplay-behaviors.md`
   (Spawner/Chase/Health/DamageOnContact/ScoreKeeper) and
   `incanto-physics-and-input.md` (2D bodies, the input map, units).
2. 🧱 STRUCTURE IS JSON: scenes, nodes, props, assets, input maps and connections
   live in `src/game.scene.json`. Add content there, NOT ad-hoc in code.
3. 🧠 LOGIC IS BEHAVIORS: prefer a BUILT-IN behavior (here: two `Spawner`s clone
   the enemy templates; each enemy `Chase`s the player; a `DamageOnContact` hitbox
   drains the player's `Health`; `ScoreKeeper` owns lives/win-lose). Custom code is
   the survivor FEEL the library leaves open: `Survivor` (move + AUTO-ATTACK +
   XP/level-up/upgrades + the DOM HUD), `Director` (ring-spawn around the player +
   escalation), `EnemyDrop` (XP shard + kill on death), `EnemyFace` (sprite facing).
4. 🔑 UIDS ARE GENERATED: omit `uid` and the loader assigns one. Never hand-invent.
5. 📦 ASSETS ARE DECLARED: `assets` entries need `type` + `url`; reference as `"$key"`.
   The knight/goblin/ghost sheets are BUILT-INS imported in `App.tsx` and injected
   into the asset url placeholders before boot. ⚠️ For `incanto/assets/*` imports to
   resolve in `bun run dev`, `vite.config.ts` needs an `'incanto/assets'` ALIAS to
   `../../packages/engine/assets` (see this template's config).
6. ⬆️ COORDINATES: y-down pixels, (0,0) top-left, clockwise degrees. This is TOP-DOWN
   so `physics.gravity` is `[0, 0]` and `CharacterController2D` would use
   `mode: 'topDown'` (the custom `Survivor` integrates its own move instead).
7. 🧲 COLLIDERS ARE PROPS: `"collider": { "shape": "rect"|"circle"|"capsule", … }`
   on a body node — never child shape nodes. Wrong shapes hard-fail at load.
8. 🧩 ONE SCRIPT PER NODE: a node has exactly one `script`. So the player's `Health`
   lives on a child `Vitals` node (the player node's script is `Survivor`);
   `DamageOnContact` still finds it by searching the contacted body's DESCENDANTS.
   Likewise each enemy's `EnemyDrop`/`EnemyFace` sit on child nodes that read the
   parent (whose script is `Health`/that `Chase` moves).
9. 🌀 SPAWN A HORDE WITHOUT A CLONE API: the engine's clone path (`SpawnSource`) is
   internal, so spawn with the built-in `Spawner` (it clones a hidden `visible:false`
   template child + emits `spawned(clone)`), and let a custom `Director` LISTEN to
   `spawned` and reposition each clone onto a ring around the player. Keep spawners at
   the ORIGIN so a clone's local position equals world (clones attach under the spawner).
10. ⏸ PAUSE WITH `engine.stop()` / `engine.start()`: the level-up overlay freezes the
    whole world via `this.engine.stop()` and resumes on pick with `start()`. Guard DOM
    use with a `hasDom` check so the headless `verify` (which drives `engine.step()`,
    not the loop) auto-applies an upgrade instead.
11. 🧮 MANY ENTITIES = AABB IN ONE OWNER: bolts + XP shards are spawned `ColorRect2D`s
    MANAGED by `Survivor` (moved each frame, AABB-tested vs the `enemy`/`xp` groups) —
    scene `connections` are NOT cloned, so per-clone wiring won't work; group + AABB does.
12. 🎬 DELTA DISCIPLINE: only write props that differ from defaults (unknown props
    hard-fail). 🧭 `this.engine` is NOT available in `onReady` during `loadScene` —
    defer scene reads to the first `fixedUpdate`.
13. ✅ VERIFY LIKE A USER: `bun run check` (scene valid) → `bun run verify` (headless
    move/auto-attack/XP-drop/level/win/lose proof) → `bun run dev`, open the browser,
    and actually PLAY (kite the horde, watch the auto-attack, vacuum XP, take a
    level-up) before declaring it done.
</userRequest>
