incanto 0.69.0 → 0.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/bin/_behaviors-loader.mjs +22 -0
  2. package/bin/_read-json.mjs +28 -0
  3. package/bin/incanto-assets.mjs +5 -4
  4. package/bin/incanto-check.mjs +58 -31
  5. package/bin/incanto-editor.mjs +128 -5
  6. package/bin/incanto-env.mjs +3 -2
  7. package/bin/incanto-feel.mjs +24 -13
  8. package/bin/incanto-frame.mjs +8 -1
  9. package/bin/incanto-multiplay.mjs +11 -9
  10. package/bin/incanto-new.mjs +105 -10
  11. package/bin/incanto-play.mjs +158 -16
  12. package/bin/incanto-playtest.mjs +66 -23
  13. package/bin/incanto-skills.mjs +14 -2
  14. package/bin/incanto-verify.mjs +121 -102
  15. package/bin/incanto.mjs +3 -2
  16. package/dist/2d.d.ts +296 -34
  17. package/dist/2d.js +4 -4
  18. package/dist/3d.d.ts +157 -9
  19. package/dist/3d.js +8 -8
  20. package/dist/{agent8-CvsfVskX.js → agent8-Cfmd3ar_.js} +61 -8
  21. package/dist/{audio-player-C0j2k-yB.d.ts → audio-player-L8yccdMP.d.ts} +2 -2
  22. package/dist/{behavior-rm8vVzu_.d.ts → behavior-Dcz0fr1S.d.ts} +755 -46
  23. package/dist/{create-game-CwxEDlCm.js → create-game-CH4kt78v.js} +143 -60
  24. package/dist/{create-game-CvJ1wSRP.js → create-game-LCHBLfA4.js} +98 -48
  25. package/dist/debug.d.ts +1 -1
  26. package/dist/debug.js +2 -3
  27. package/dist/diagnostics-Cu85N3tL.d.ts +12 -0
  28. package/dist/{editor-switch-CnIOiyNJ.d.ts → editor-switch-CFU9mCec.d.ts} +22 -13
  29. package/dist/editor.js +1088 -882
  30. package/dist/env.d.ts +1 -1
  31. package/dist/env.js +5 -3
  32. package/dist/{environment-presets-BO-7XoFV.js → environment-presets-99BzP_L-.js} +298 -45
  33. package/dist/{frame-report-Dlq13Gyj.js → frame-report-D-_7YF2G.js} +1 -1
  34. package/dist/{gameplay-D1KixWK8.js → gameplay-CNULJvwh.js} +914 -214
  35. package/dist/gameplay.d.ts +254 -10
  36. package/dist/gameplay.js +2 -2
  37. package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
  38. package/dist/index.d.ts +179 -13
  39. package/dist/index.js +10 -12
  40. package/dist/json-CfTjpvW8.js +67 -0
  41. package/dist/{loader-DadV2QCT.d.ts → loader-8-IHvbKD.d.ts} +2 -2
  42. package/dist/net.d.ts +27 -3
  43. package/dist/net.js +2 -2
  44. package/dist/{noise-CGUMx44x.js → noise-D3nPpmFg.js} +1 -1
  45. package/dist/{physics-2d-CF5yyjbj.js → physics-2d-FhrXUc6B.js} +136 -11
  46. package/dist/{physics-3d-DbYaEcZy.js → physics-3d-CFEGxBXZ.js} +104 -23
  47. package/dist/{teardown-CMqFVxO9.js → picking-DVo7fI13.js} +104 -13
  48. package/dist/react.d.ts +2 -2
  49. package/dist/react.js +2 -2
  50. package/dist/{register-DbVArsns.js → register-BYQCBySi.js} +429 -28
  51. package/dist/{register-mmfugpaQ.js → register-DuVdzxbq.js} +594 -831
  52. package/dist/{replay-BEHLDcQV.js → replay-DEvp3kyV.js} +224 -14
  53. package/dist/{replay-BQfkX4x7.d.ts → replay-F7IZHdFR.d.ts} +20 -3
  54. package/dist/{schema-B6ugCV1Q.d.ts → rng-Bb-IutXB.d.ts} +38 -21
  55. package/dist/{rng-DP-SR7eg.js → rng-CDOMybym.js} +22 -0
  56. package/dist/{loader-DZE7B07H.js → save-slots-Bvuh2p_r.js} +4619 -2318
  57. package/dist/{split-screen-Cg76MCV5.d.ts → split-screen-CIYf1zSf.d.ts} +36 -4
  58. package/dist/{split-screen-CauMwu6y.js → split-screen-eULetcg-.js} +159 -26
  59. package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
  60. package/dist/{src-CYEu-_S9.js → src-_jk0qLsS.js} +2 -17
  61. package/dist/test-CmN54kWv.js +4264 -0
  62. package/dist/test.d.ts +639 -30
  63. package/dist/test.js +3 -3
  64. package/dist/touch-BnCyPA0G.js +519 -0
  65. package/dist/vite.d.ts +20 -37
  66. package/dist/vite.js +177 -7
  67. package/dist/{webgl-unavailable-Z5_g8x9H.js → webgl-unavailable-C8aDbGmR.js} +44 -0
  68. package/editor/assets/agent8-yLIEYHbd.js +1 -0
  69. package/editor/assets/{debug-CMULinBW.js → debug-u31w_yhq.js} +2 -2
  70. package/editor/assets/index-DK9xMGpW.js +11046 -0
  71. package/editor/index.html +1 -1
  72. package/package.json +4 -14
  73. package/schemas/scene.schema.json +34 -3
  74. package/skills/incanto-3d-character.md +14 -1
  75. package/skills/incanto-3d-models.md +12 -0
  76. package/skills/incanto-assets.md +9 -1
  77. package/skills/incanto-audio.md +8 -2
  78. package/skills/incanto-behaviors-and-scripts.md +105 -7
  79. package/skills/incanto-building-2d-games.md +97 -10
  80. package/skills/incanto-building-3d-games.md +58 -4
  81. package/skills/incanto-editor.md +46 -7
  82. package/skills/incanto-environment.md +19 -1
  83. package/skills/incanto-game-feel.md +70 -0
  84. package/skills/incanto-gameplay-behaviors.md +172 -20
  85. package/skills/incanto-hud.md +142 -7
  86. package/skills/incanto-localization.md +13 -5
  87. package/skills/incanto-multiplayer.md +83 -3
  88. package/skills/incanto-node-reference.md +239 -59
  89. package/skills/incanto-performance.md +52 -0
  90. package/skills/incanto-physics-and-input.md +123 -24
  91. package/skills/incanto-playtesting.md +112 -2
  92. package/skills/incanto-save-slots.md +217 -7
  93. package/skills/incanto-scene-json-authoring.md +69 -12
  94. package/skills/incanto-verifying-your-game.md +207 -10
  95. package/skills/incanto-web-integration.md +28 -0
  96. package/skills/incanto-your-first-game.md +3 -1
  97. package/templates-app/beacon-isle-3d/generate-world.ts +77 -9
  98. package/templates-app/beacon-isle-3d/package.json +2 -2
  99. package/templates-app/beacon-isle-3d/src/behaviors.ts +22 -0
  100. package/templates-app/beacon-isle-3d/src/game.scene.json +143 -600
  101. package/templates-app/beacon-isle-3d/src/main.ts +11 -1
  102. package/templates-app/beacon-isle-3d/tsconfig.json +1 -1
  103. package/templates-app/beacon-isle-3d/verify.ts +3 -1
  104. package/templates-app/molehill-2d/.incanto/playtest/lost-seed1.json +4277 -0
  105. package/templates-app/molehill-2d/PROJECT/Context.md +58 -0
  106. package/templates-app/molehill-2d/PROJECT/Requirements.md +39 -0
  107. package/templates-app/molehill-2d/PROJECT/Status.md +27 -0
  108. package/templates-app/molehill-2d/PROJECT/Structure.md +48 -0
  109. package/templates-app/molehill-2d/docs/project-2d-rules.md +44 -0
  110. package/templates-app/molehill-2d/index.html +73 -0
  111. package/templates-app/molehill-2d/package.json +23 -0
  112. package/templates-app/molehill-2d/src/behaviors.ts +198 -0
  113. package/templates-app/molehill-2d/src/game.scene.json +1205 -0
  114. package/templates-app/molehill-2d/src/main.ts +41 -0
  115. package/templates-app/molehill-2d/tsconfig.json +13 -0
  116. package/templates-app/molehill-2d/verify.ts +247 -0
  117. package/templates-app/molehill-2d/vite.config.ts +12 -0
  118. package/templates-app/platformer-2d/package.json +2 -2
  119. package/templates-app/platformer-2d/src/game.scene.json +178 -986
  120. package/templates-app/platformer-2d/src/main.ts +11 -1
  121. package/templates-app/platformer-2d/tsconfig.json +1 -1
  122. package/templates-app/star-survivor/package.json +2 -2
  123. package/templates-app/star-survivor/src/game.scene.json +43 -216
  124. package/templates-app/star-survivor/src/main.ts +11 -1
  125. package/templates-app/star-survivor/tsconfig.json +1 -1
  126. package/templates-app/tps-3d/PROJECT/Context.md +1 -1
  127. package/templates-app/tps-3d/package.json +2 -2
  128. package/templates-app/tps-3d/src/behaviors.ts +19 -1
  129. package/templates-app/tps-3d/src/game.scene.json +66 -263
  130. package/templates-app/tps-3d/src/main.ts +13 -2
  131. package/templates-app/tps-3d/tsconfig.json +1 -1
  132. package/templates-app/village-quest-3d/.incanto/playtest/swapped-seed1.json +1735 -0
  133. package/templates-app/village-quest-3d/package.json +2 -2
  134. package/templates-app/village-quest-3d/src/behaviors.ts +42 -4
  135. package/templates-app/village-quest-3d/src/grove.scene.json +68 -306
  136. package/templates-app/village-quest-3d/src/main.ts +11 -1
  137. package/templates-app/village-quest-3d/src/village.scene.json +202 -863
  138. package/templates-app/village-quest-3d/tsconfig.json +1 -1
  139. package/templates-app/village-quest-3d/verify.ts +14 -1
  140. package/dist/duplicate-Dj4L-5AA.js +0 -22
  141. package/dist/json-CwwhxQgb.js +0 -36
  142. package/dist/registry-CF70EArN.js +0 -212
  143. package/dist/rolldown-runtime-D7D4PA-g.js +0 -13
  144. package/dist/test-D6ISmuFE.js +0 -2383
  145. package/dist/touch-DEAmqGdf.js +0 -225
  146. package/editor/assets/agent8-BDvaZMQJ.js +0 -1
  147. package/editor/assets/index-CZIRr5EL.js +0 -10958
@@ -0,0 +1,58 @@
1
+ # Context — molehill-2d (Incanto)
2
+
3
+ ## Project Overview
4
+
5
+ **Molehill** — a WHACK-A-MOLE you play with the mouse and nothing else. Nine
6
+ holes in a turf field; a mole pops out of one of them and ducks back a little
7
+ faster each time. Click it and you score; let it go and you have missed. Twelve
8
+ moles wins, three misses loses.
9
+
10
+ It is the clone-and-modify starter for **games you play by pointing**: match-3,
11
+ tower defense, cards, point-and-click, RTS, board games, idle. Every other
12
+ starter is a character you WALK, and that difference reaches further than it
13
+ looks — there is no `CharacterController`, no `move` action, no gravity and no
14
+ player body anywhere in the scene.
15
+
16
+ The scoring, the lives, the sounds and the game-over are BUILT-IN gameplay wired
17
+ in `game.scene.json`. The custom TypeScript is only the two things JSON cannot
18
+ say: when a mole pops, and what the HUD shows.
19
+
20
+ ## Tech Stack
21
+
22
+ _Exact versions are in `package.json`._
23
+
24
+ - **Engine**: `incanto` + `incanto/2d` (createGame2D; Node2D, ColorRect2D,
25
+ AudioPlayer, HudLayer/UiText/UiBanner/UiButton) + `incanto/gameplay`
26
+ (auto-registered `Clickable`, `ScoreKeeper`).
27
+ - **Art**: none. Every visual is a `ColorRect2D` — the field, the holes, the
28
+ moles. Swap in sprites by changing the node types and adding an `assets{}`
29
+ block; nothing in the logic knows what a mole looks like.
30
+ - **Audio**: procedural presets (`coin` on a hit, `hurt` on a miss) — zero files.
31
+ - **Input**: the pointer. `createGame2D({ pointer: { lockOnClick: false } })` —
32
+ locking would HIDE the cursor, which is exactly wrong for a game you aim.
33
+
34
+ ## The one thing to understand
35
+
36
+ `Clickable` is a behaviour on each mole. It emits `clicked(node)` and nothing
37
+ else, and the scene wires all nine of them to ONE handler:
38
+
39
+ ```jsonc
40
+ { "signal": "clicked", "from": "Field/Hole00/Mole", "to": "Field", "handler": "onMoleClicked" }
41
+ ```
42
+
43
+ The signal carries the node it happened on, so one handler serves the whole
44
+ board. A `Clickable` nobody listens to is reported by `incanto-check` — it can
45
+ never do anything, and forgetting the wire is the mistake this genre makes.
46
+
47
+ ## Verifying it
48
+
49
+ `bun run verify` drives the MOUSE, which is the only input this game has:
50
+
51
+ ```ts
52
+ { atMs: 1000, click: (ctx) => moleUp(ctx) } // click whatever is up right now
53
+ ```
54
+
55
+ `click` takes a node path, a point, or a function for the case a mouse game is
56
+ usually in — the target cannot be named when the script is written. Headless
57
+ there is no renderer and therefore no raycast, so `runScript` installs a
58
+ geometric picker; a game with a picker of its own keeps it.
@@ -0,0 +1,39 @@
1
+ # Requirements — molehill-2d
2
+
3
+ ## The game
4
+
5
+ - Nine holes in a 3×3 field. Exactly ONE mole is up at a time.
6
+ - A mole stays up for `upSeconds`, minus `quicken` for every mole so far, floored
7
+ at 0.35 s — a run that goes long still ends.
8
+ - Clicking the mole that is UP scores one and plays the `coin` preset. Clicking
9
+ anywhere else does nothing at all.
10
+ - A mole whose time runs out is a MISS: it costs a life and plays `hurt`.
11
+ - **12 whacks wins. 3 misses loses.** Both come from the built-in `ScoreKeeper`
12
+ (`scoreToWin: 12`, `lives: 3`), which emits `won` / `lost`.
13
+ - The end of the game freezes the field — `won` and `lost` are both wired to
14
+ `Field.stop`, so no mole pops after the banner.
15
+ - `PLAY AGAIN` resets the score, the lives and the field without a reload.
16
+
17
+ ## What it must NOT have
18
+
19
+ - No character body, no `CharacterController`, no `move` action, no gravity. If
20
+ you find yourself adding one, you are writing a different starter.
21
+ - No art files and no external URLs — every visual is a `ColorRect2D` and every
22
+ sound is a procedural preset. A starter you can run with no network.
23
+ - No pointer LOCK. The cursor is the thing the player aims; hiding it is the one
24
+ unrecoverable mistake this genre can make.
25
+
26
+ ## What it must prove (`bun run verify`)
27
+
28
+ | check | why |
29
+ | --- | --- |
30
+ | nine holes, and a mole up within the first second | the board exists and starts |
31
+ | whacking scores | the `Clickable` → connection → `ScoreKeeper` path is live |
32
+ | missing costs a life | the timeout half, which no click can prove |
33
+ | three misses lose | `lives` is spendable more than once |
34
+ | twelve whacks win | the goal is reachable |
35
+ | the field stops when won | the game ending reaches the level |
36
+ | a click on bare ground scores nothing | the picker is not answering "anything" |
37
+
38
+ `auditScene` must be clean, and `coverage.json` answers the gate's two questions
39
+ (`hurt: "afk"` — stand by and the moles get away; `lose: true`).
@@ -0,0 +1,27 @@
1
+ # Status — molehill-2d
2
+
3
+ ## Working
4
+
5
+ - The full loop: pop → click or miss → score or life → win or lose → play again.
6
+ - Nine `Clickable` moles wired to one handler; the built-in `ScoreKeeper` holds
7
+ the score, the lives and both end conditions.
8
+ - Procedural audio on both outcomes; a hit flashes the mole before it drops, so
9
+ a hit reads as a hit rather than as the mole vanishing.
10
+ - `bun run verify` — ten assertions, all driving the mouse.
11
+ - `coverage.json` — the gate's two questions answered without a body.
12
+
13
+ ## Deliberately not here
14
+
15
+ - **A difficulty curve beyond `quicken`.** One knob, so the ramp is legible.
16
+ - **Multiple moles at once.** One mole is the clearest possible statement of the
17
+ loop; two is a design decision the person cloning this should make.
18
+ - **Sprites.** The moles are rectangles on purpose — see Context.
19
+
20
+ ## Where to take it next
21
+
22
+ - Swap `ColorRect2D` for `Sprite2D` and give the moles a sheet: nothing in
23
+ `behaviors.ts` knows what a mole looks like.
24
+ - Golden mole worth three, bomb mole that costs a life when clicked — both are a
25
+ `tags` entry on the node and a branch in `onMoleClicked`.
26
+ - Turn the field into a GRID of clickable cells and you have a match-3, a
27
+ minesweeper or a tower-defense build menu; the input path is identical.
@@ -0,0 +1,48 @@
1
+ # Structure — molehill-2d
2
+
3
+ ```
4
+ src/game.scene.json the whole game's structure — holes, moles, HUD, wiring
5
+ src/behaviors.ts Molehill (when a mole pops) + Scoreboard (what the HUD says)
6
+ src/main.ts createGame2D, the pointer, the two behaviours
7
+ verify.ts the headless proof — drives the MOUSE
8
+ coverage.json the gate's two questions: can it hurt you, can it be lost
9
+ ```
10
+
11
+ ## The scene
12
+
13
+ ```
14
+ Game ScoreKeeper (scoreToWin 12, lives 3) — the score and the ending
15
+ ├── Sky/Turf two ColorRect2D, the backdrop
16
+ ├── Field Molehill — pops a mole, times it out
17
+ │ └── Hole00…Hole22 nine Node2D, each with
18
+ │ ├── Dirt the hole (ColorRect2D)
19
+ │ └── Mole ColorRect2D + `Clickable`, `visible: false` until it pops
20
+ ├── Whack/Miss two AudioPlayer, procedural presets
21
+ └── Hud HudLayer + Scoreboard
22
+ ├── Score / Lives UiText
23
+ ├── Banner UiBanner (the ending)
24
+ └── Again UiButton, hidden until the game is over
25
+ ```
26
+
27
+ ## The wiring, all of it in JSON
28
+
29
+ | from | signal | to | handler |
30
+ | --- | --- | --- | --- |
31
+ | every `Mole` | `clicked` | `Field` | `onMoleClicked` |
32
+ | `Field` | `whacked` | `.` (the ScoreKeeper) | `addScore` |
33
+ | `Field` | `escaped` | `.` | `loseLife` |
34
+ | `Field` | `whacked` / `escaped` | `Whack` / `Miss` | `play` |
35
+ | `.` | `scoreChanged` / `livesChanged` | `Hud` | `onScore` / `onLives` |
36
+ | `.` | `won` / `lost` | `Hud` | `onWon` / `onLost` |
37
+ | `.` | `won` / `lost` | `Field` | `stop` |
38
+ | `Hud/Again` | `pressed` | `Hud` | `onAgain` |
39
+
40
+ Nine moles, one handler: `clicked` carries the node it happened on.
41
+
42
+ ## The two behaviours
43
+
44
+ - **`Molehill`** — the only timing in the game. Emits `whacked` and `escaped`
45
+ and reaches for nothing: the scoring, the sound and the ending are wires.
46
+ `holeCount` is a read for the harness.
47
+ - **`Scoreboard`** — writes the HUD from the ScoreKeeper's signals, and owns the
48
+ `PLAY AGAIN` reset.
@@ -0,0 +1,44 @@
1
+ <userRequest>
2
+ YOU MUST follow these rules EXACTLY when vibe-coding on this Incanto template
3
+ (Molehill — a whack-a-mole you play with the MOUSE):
4
+
5
+ 1. 📖 READ THE SKILLS FIRST: `node_modules/incanto/skills/` is the engine manual,
6
+ version-pinned to what is installed. Start with `incanto-building-2d-games.md`
7
+ and `incanto-scene-json-authoring.md`, then `incanto-gameplay-behaviors.md`
8
+ (Clickable/ScoreKeeper) and `incanto-hud.md` (the HUD is scene JSON, not DOM).
9
+ 2. 🧱 STRUCTURE IS JSON: scenes, nodes, props, assets, input maps and connections
10
+ live in `src/game.scene.json`. Add content there, NOT ad-hoc in code.
11
+ 3. 🧠 LOGIC IS BEHAVIORS: prefer a BUILT-IN behavior (here: `Clickable` on each
12
+ mole reports being clicked; `ScoreKeeper` owns the score, the misses and
13
+ win/lose). Custom code is what the library leaves open: `Molehill` (which mole
14
+ is up, for how long, and what a miss costs) and `Scoreboard` (the HUD).
15
+ 4. 🔑 UIDS ARE GENERATED: omit `uid` and the loader assigns one. Never hand-invent.
16
+ 5. 📦 ASSETS ARE DECLARED: `assets` entries need `type` + `url`; reference as `"$key"`.
17
+ This template ships NO art on purpose — every mole is `ColorRect2D` blocks, so
18
+ the game is readable before any asset exists. Swap in sprites by changing only
19
+ the `assets{}` entry.
20
+ 6. 🖱 THE MOUSE IS THE INPUT: there is no character, no keyboard and no gravity.
21
+ `pointer: { lockOnClick: false }` at boot — locking would HIDE the cursor, which
22
+ is exactly wrong for a game you play by pointing. The scene declares no `input`
23
+ actions at all, and that is not an omission.
24
+ 7. ⬆️ COORDINATES: y-down pixels, (0,0) top-left, clockwise degrees.
25
+ 8. 🧲 COLLIDERS ARE PROPS when you add physics — `"collider": { "shape": … }` on a
26
+ body node, never child shape nodes. This template has no bodies: a mole is a
27
+ drawn rectangle that answers a click, which is all a board game needs.
28
+ 9. 🧩 ONE SCRIPT PER NODE: a node has exactly one `script`. `Clickable` lives on
29
+ the mole itself; anything else about that mole goes on a child node.
30
+ 10. 🎯 CLICKS ARE SIGNALS: `Clickable` emits `clicked`, `hovered` and `unhovered`
31
+ and nothing else — wire them in `connections[]`. A `Clickable` nobody listens
32
+ to is a pick every frame that can never do anything, and `incanto-check` says
33
+ so.
34
+ 11. 🖥 THE HUD IS NODES: `HudLayer` + `UiText`/`UiBanner`/`UiButton` in the scene.
35
+ Do not reach for `document` — the widgets work headlessly, so `bun run verify`
36
+ can read the score the player reads.
37
+ 12. 🎬 DELTA DISCIPLINE: only write props that differ from defaults (unknown props
38
+ hard-fail). 🧭 `this.engine` is NOT available in `onReady` during `loadScene` —
39
+ defer scene reads to the first `update`/`fixedUpdate`.
40
+ 13. ✅ VERIFY LIKE A USER: `bun run check` (scene valid) → `bun run verify`
41
+ (headless proof that a click scores, a miss costs a life, and both endings
42
+ fire) → `bun run dev`, open the browser, and actually PLAY before declaring
43
+ it done.
44
+ </userRequest>
@@ -0,0 +1,73 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><rect width=%22100%22 height=%22100%22 rx=%2220%22 fill=%22%23a9714b%22/><text x=%2250%22 y=%2270%22 font-size=%2258%22 text-anchor=%22middle%22 font-family=%22Arial%22 font-weight=%22bold%22 fill=%22%23101a2b%22>M</text></svg>" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
7
+ <title>Molehill — Incanto whack-a-mole</title>
8
+ <style>
9
+ html,
10
+ body {
11
+ margin: 0;
12
+ padding: 0;
13
+ height: 100%;
14
+ overflow: hidden;
15
+ font-family: Arial, sans-serif;
16
+ background: #101a2b;
17
+ color: #f4f4f4;
18
+ }
19
+ canvas {
20
+ display: block;
21
+ width: 100%;
22
+ height: 100%;
23
+ /* The cursor IS the controller — never hide it, and make it read as
24
+ something you aim rather than something you drag text with. */
25
+ cursor: crosshair;
26
+ }
27
+ /* The HUD is scene JSON (HudLayer), not markup: the only DOM this game
28
+ owns is the loading card, which the engine removes once it boots. */
29
+ #loading {
30
+ position: fixed;
31
+ inset: 0;
32
+ z-index: 50;
33
+ display: flex;
34
+ flex-direction: column;
35
+ gap: 14px;
36
+ align-items: center;
37
+ justify-content: center;
38
+ background: #101a2b;
39
+ }
40
+ #loading .title {
41
+ font-size: 22px;
42
+ letter-spacing: 0.14em;
43
+ color: #ffd166;
44
+ }
45
+ #loading .hint {
46
+ font-size: 13px;
47
+ color: #8b93a7;
48
+ }
49
+ #loading .spinner {
50
+ width: 34px;
51
+ height: 34px;
52
+ border: 3px solid #2a3550;
53
+ border-top-color: #ffd166;
54
+ border-radius: 50%;
55
+ animation: spin 0.9s linear infinite;
56
+ }
57
+ @keyframes spin {
58
+ to {
59
+ transform: rotate(360deg);
60
+ }
61
+ }
62
+ </style>
63
+ </head>
64
+ <body>
65
+ <div id="loading">
66
+ <div class="title">MOLEHILL</div>
67
+ <div class="spinner"></div>
68
+ <div class="hint">click the moles — twelve to win, three misses and they win</div>
69
+ </div>
70
+ <canvas id="game"></canvas>
71
+ <script type="module" src="/src/main.ts"></script>
72
+ </body>
73
+ </html>
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "molehill-2d",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "vite build",
8
+ "typecheck": "tsc --noEmit",
9
+ "check": "incanto-check src --behaviors src/behaviors.ts",
10
+ "verify": "bun verify.ts"
11
+ },
12
+ "dependencies": {
13
+ "@dimforge/rapier2d-compat": "0.19.3",
14
+ "incanto": "^0.71.0",
15
+ "three": "^0.184.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^24.0.0",
19
+ "@types/three": "^0.184.1",
20
+ "typescript": "^6.0.3",
21
+ "vite": "^8.0.16"
22
+ }
23
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Molehill — the mouse is the whole game.
3
+ *
4
+ * No character body, no keyboard, no gravity: nine holes, a mole that pops out
5
+ * of one of them, and a cursor. The engine's own `Clickable` turns each mole
6
+ * into a node that reports being clicked, the built-in `ScoreKeeper` holds the
7
+ * score and the misses, and the wiring between them is scene JSON.
8
+ *
9
+ * This is the starter for an entire family the other templates do not cover —
10
+ * match-3, tower defense, cards, point-and-click, RTS, board games, idle — all
11
+ * of which are played by pointing at things.
12
+ */
13
+
14
+ import type { UiBanner, UiButton, UiText } from 'incanto';
15
+ import { Behavior, type Node } from 'incanto';
16
+
17
+ /** Holes across the field, mirrored by `game.scene.json`. */
18
+ const HOLES = 9;
19
+ const MOLES_TO_WIN = 12;
20
+ const MISSES_ALLOWED = 3;
21
+
22
+ /** A mole node, as this game drives it. */
23
+ interface Mole extends Node {
24
+ visible: boolean;
25
+ color: string;
26
+ scale: number[];
27
+ }
28
+
29
+ const UP = '#a9714b';
30
+ const HIT = '#ffd166';
31
+
32
+ /**
33
+ * The field: pops a mole, waits, and calls it a miss if you were too slow.
34
+ *
35
+ * It emits rather than reaches — `whacked` and `escaped` are wired in the scene
36
+ * to `ScoreKeeper.addScore` / `loseLife` and to the two `AudioPlayer`s, so the
37
+ * scoring, the sound and the game-over all live in JSON.
38
+ */
39
+ export class Molehill extends Behavior {
40
+ static readonly props = {
41
+ /** Seconds a mole stays up at the start. */
42
+ upSeconds: { default: 1.4 },
43
+ /** Seconds between one mole going away and the next arriving. */
44
+ gapSeconds: { default: 0.55 },
45
+ /** Every mole is this much quicker than the one before (0 = no ramp). */
46
+ quicken: { default: 0.04 },
47
+ };
48
+ static readonly signals: readonly string[] = ['whacked', 'escaped'];
49
+
50
+ upSeconds = 1.4;
51
+ gapSeconds = 0.55;
52
+ quicken = 0.04;
53
+
54
+ /** The mole that is up right now, or null between moles. */
55
+ private up: Mole | null = null;
56
+ private timer = 0;
57
+ private popped = 0;
58
+ private running = true;
59
+ /** The mole showing its hit colour, and how long it has left to show it. */
60
+ private flashing: Mole | null = null;
61
+ private flash = 0;
62
+
63
+ override onReady(): void {
64
+ this.timer = this.gapSeconds;
65
+ for (const mole of this.moles()) mole.visible = false;
66
+ }
67
+
68
+ /** Freeze the field — wired to the ScoreKeeper's `won` and `lost`. */
69
+ stop(): void {
70
+ this.running = false;
71
+ this.hide();
72
+ }
73
+
74
+ /** Start over, from the button on the HUD. */
75
+ restart(): void {
76
+ this.running = true;
77
+ this.popped = 0;
78
+ this.timer = this.gapSeconds;
79
+ this.hide();
80
+ }
81
+
82
+ override update(dt: number): void {
83
+ if (this.flashing) {
84
+ this.flash -= dt;
85
+ if (this.flash <= 0) {
86
+ this.flashing.color = UP;
87
+ this.flashing.visible = false;
88
+ this.flashing = null;
89
+ }
90
+ }
91
+ if (!this.running) return;
92
+ this.timer -= dt;
93
+ if (this.timer > 0) return;
94
+ if (this.up) {
95
+ // Its time ran out with nobody clicking it — that is the miss.
96
+ this.hide();
97
+ this.timer = this.gapSeconds;
98
+ this.emit('escaped');
99
+ return;
100
+ }
101
+ this.pop();
102
+ }
103
+
104
+ onMoleClicked(mole: Node): void {
105
+ // Only the mole that is UP counts: a click on one already going down is a
106
+ // click on nothing, which is what the player sees.
107
+ if (!this.running || mole !== (this.up as unknown as Node)) return;
108
+ // It flashes before it goes, so a hit reads as a hit rather than as the
109
+ // mole simply vanishing.
110
+ const hit = this.up;
111
+ if (!hit) return;
112
+ hit.color = HIT;
113
+ this.flashing = hit;
114
+ this.flash = 0.09;
115
+ this.up = null;
116
+ this.timer = this.gapSeconds;
117
+ this.emit('whacked', 1);
118
+ }
119
+
120
+ private pop(): void {
121
+ const moles = this.moles();
122
+ if (moles.length === 0) return;
123
+ const next = moles[Math.floor(this.engine.rng.next() * moles.length)];
124
+ if (!next) return;
125
+ next.visible = true;
126
+ next.color = UP;
127
+ this.up = next;
128
+ this.popped += 1;
129
+ // Quicker every time, so a run that goes long still ends.
130
+ this.timer = Math.max(0.35, this.upSeconds - this.popped * this.quicken);
131
+ }
132
+
133
+ private hide(): void {
134
+ if (this.up) this.up.visible = false;
135
+ if (this.flashing) {
136
+ this.flashing.color = UP;
137
+ this.flashing.visible = false;
138
+ this.flashing = null;
139
+ }
140
+ this.up = null;
141
+ }
142
+
143
+ private moles(): Mole[] {
144
+ const out: Mole[] = [];
145
+ for (const hole of this.node.children) {
146
+ const mole = hole.getNodeOrNull('Mole');
147
+ if (mole) out.push(mole as unknown as Mole);
148
+ }
149
+ return out;
150
+ }
151
+
152
+ /** How many holes this field found — read by the harness. */
153
+ get holeCount(): number {
154
+ return this.node.children.length;
155
+ }
156
+ }
157
+
158
+ /** The HUD, driven entirely by the ScoreKeeper's signals. */
159
+ export class Scoreboard extends Behavior {
160
+ private text(name: string): UiText {
161
+ return this.node.getNode(name) as unknown as UiText;
162
+ }
163
+
164
+ onScore(score: number): void {
165
+ this.text('Score').text = `Moles ${score} / ${MOLES_TO_WIN}`;
166
+ }
167
+
168
+ onLives(lives: number): void {
169
+ this.text('Lives').text = `Misses left ${lives}`;
170
+ }
171
+
172
+ onWon(): void {
173
+ this.finish(`ALL ${MOLES_TO_WIN} WHACKED`, '#ffd166');
174
+ }
175
+
176
+ onLost(): void {
177
+ this.finish('THEY GOT AWAY', '#ef4444');
178
+ }
179
+
180
+ onAgain(): void {
181
+ const game = this.node.getNode('/root');
182
+ (game.behavior as unknown as { setScore(n: number): void; lives: number }).setScore(0);
183
+ (game.behavior as unknown as { lives: number }).lives = MISSES_ALLOWED;
184
+ (this.node.getNode('/root/Field').behavior as unknown as { restart(): void }).restart();
185
+ (this.node.getNode('Banner') as unknown as UiBanner).clear();
186
+ (this.node.getNode('Again') as unknown as UiButton).visible = false;
187
+ this.onScore(0);
188
+ this.onLives(MISSES_ALLOWED);
189
+ }
190
+
191
+ private finish(text: string, color: string): void {
192
+ (this.node.getNode('Banner') as unknown as UiBanner).show(text, { color, seconds: 0 });
193
+ (this.node.getNode('Again') as unknown as UiButton).visible = true;
194
+ }
195
+ }
196
+
197
+ /** Holes the field expects, for a harness that wants to check the board. */
198
+ export const FIELD_SIZE = HOLES;