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.
- package/bin/_behaviors-loader.mjs +22 -0
- package/bin/_read-json.mjs +28 -0
- package/bin/incanto-assets.mjs +5 -4
- package/bin/incanto-check.mjs +58 -31
- package/bin/incanto-editor.mjs +128 -5
- package/bin/incanto-env.mjs +3 -2
- package/bin/incanto-feel.mjs +24 -13
- package/bin/incanto-frame.mjs +8 -1
- package/bin/incanto-multiplay.mjs +11 -9
- package/bin/incanto-new.mjs +105 -10
- package/bin/incanto-play.mjs +158 -16
- package/bin/incanto-playtest.mjs +66 -23
- package/bin/incanto-skills.mjs +14 -2
- package/bin/incanto-verify.mjs +121 -102
- package/bin/incanto.mjs +3 -2
- package/dist/2d.d.ts +296 -34
- package/dist/2d.js +4 -4
- package/dist/3d.d.ts +157 -9
- package/dist/3d.js +8 -8
- package/dist/{agent8-CvsfVskX.js → agent8-Cfmd3ar_.js} +61 -8
- package/dist/{audio-player-C0j2k-yB.d.ts → audio-player-L8yccdMP.d.ts} +2 -2
- package/dist/{behavior-rm8vVzu_.d.ts → behavior-Dcz0fr1S.d.ts} +755 -46
- package/dist/{create-game-CwxEDlCm.js → create-game-CH4kt78v.js} +143 -60
- package/dist/{create-game-CvJ1wSRP.js → create-game-LCHBLfA4.js} +98 -48
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +2 -3
- package/dist/diagnostics-Cu85N3tL.d.ts +12 -0
- package/dist/{editor-switch-CnIOiyNJ.d.ts → editor-switch-CFU9mCec.d.ts} +22 -13
- package/dist/editor.js +1088 -882
- package/dist/env.d.ts +1 -1
- package/dist/env.js +5 -3
- package/dist/{environment-presets-BO-7XoFV.js → environment-presets-99BzP_L-.js} +298 -45
- package/dist/{frame-report-Dlq13Gyj.js → frame-report-D-_7YF2G.js} +1 -1
- package/dist/{gameplay-D1KixWK8.js → gameplay-CNULJvwh.js} +914 -214
- package/dist/gameplay.d.ts +254 -10
- package/dist/gameplay.js +2 -2
- package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
- package/dist/index.d.ts +179 -13
- package/dist/index.js +10 -12
- package/dist/json-CfTjpvW8.js +67 -0
- package/dist/{loader-DadV2QCT.d.ts → loader-8-IHvbKD.d.ts} +2 -2
- package/dist/net.d.ts +27 -3
- package/dist/net.js +2 -2
- package/dist/{noise-CGUMx44x.js → noise-D3nPpmFg.js} +1 -1
- package/dist/{physics-2d-CF5yyjbj.js → physics-2d-FhrXUc6B.js} +136 -11
- package/dist/{physics-3d-DbYaEcZy.js → physics-3d-CFEGxBXZ.js} +104 -23
- package/dist/{teardown-CMqFVxO9.js → picking-DVo7fI13.js} +104 -13
- package/dist/react.d.ts +2 -2
- package/dist/react.js +2 -2
- package/dist/{register-DbVArsns.js → register-BYQCBySi.js} +429 -28
- package/dist/{register-mmfugpaQ.js → register-DuVdzxbq.js} +594 -831
- package/dist/{replay-BEHLDcQV.js → replay-DEvp3kyV.js} +224 -14
- package/dist/{replay-BQfkX4x7.d.ts → replay-F7IZHdFR.d.ts} +20 -3
- package/dist/{schema-B6ugCV1Q.d.ts → rng-Bb-IutXB.d.ts} +38 -21
- package/dist/{rng-DP-SR7eg.js → rng-CDOMybym.js} +22 -0
- package/dist/{loader-DZE7B07H.js → save-slots-Bvuh2p_r.js} +4619 -2318
- package/dist/{split-screen-Cg76MCV5.d.ts → split-screen-CIYf1zSf.d.ts} +36 -4
- package/dist/{split-screen-CauMwu6y.js → split-screen-eULetcg-.js} +159 -26
- package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
- package/dist/{src-CYEu-_S9.js → src-_jk0qLsS.js} +2 -17
- package/dist/test-CmN54kWv.js +4264 -0
- package/dist/test.d.ts +639 -30
- package/dist/test.js +3 -3
- package/dist/touch-BnCyPA0G.js +519 -0
- package/dist/vite.d.ts +20 -37
- package/dist/vite.js +177 -7
- package/dist/{webgl-unavailable-Z5_g8x9H.js → webgl-unavailable-C8aDbGmR.js} +44 -0
- package/editor/assets/agent8-yLIEYHbd.js +1 -0
- package/editor/assets/{debug-CMULinBW.js → debug-u31w_yhq.js} +2 -2
- package/editor/assets/index-DK9xMGpW.js +11046 -0
- package/editor/index.html +1 -1
- package/package.json +4 -14
- package/schemas/scene.schema.json +34 -3
- package/skills/incanto-3d-character.md +14 -1
- package/skills/incanto-3d-models.md +12 -0
- package/skills/incanto-assets.md +9 -1
- package/skills/incanto-audio.md +8 -2
- package/skills/incanto-behaviors-and-scripts.md +105 -7
- package/skills/incanto-building-2d-games.md +97 -10
- package/skills/incanto-building-3d-games.md +58 -4
- package/skills/incanto-editor.md +46 -7
- package/skills/incanto-environment.md +19 -1
- package/skills/incanto-game-feel.md +70 -0
- package/skills/incanto-gameplay-behaviors.md +172 -20
- package/skills/incanto-hud.md +142 -7
- package/skills/incanto-localization.md +13 -5
- package/skills/incanto-multiplayer.md +83 -3
- package/skills/incanto-node-reference.md +239 -59
- package/skills/incanto-performance.md +52 -0
- package/skills/incanto-physics-and-input.md +123 -24
- package/skills/incanto-playtesting.md +112 -2
- package/skills/incanto-save-slots.md +217 -7
- package/skills/incanto-scene-json-authoring.md +69 -12
- package/skills/incanto-verifying-your-game.md +207 -10
- package/skills/incanto-web-integration.md +28 -0
- package/skills/incanto-your-first-game.md +3 -1
- package/templates-app/beacon-isle-3d/generate-world.ts +77 -9
- package/templates-app/beacon-isle-3d/package.json +2 -2
- package/templates-app/beacon-isle-3d/src/behaviors.ts +22 -0
- package/templates-app/beacon-isle-3d/src/game.scene.json +143 -600
- package/templates-app/beacon-isle-3d/src/main.ts +11 -1
- package/templates-app/beacon-isle-3d/tsconfig.json +1 -1
- package/templates-app/beacon-isle-3d/verify.ts +3 -1
- package/templates-app/molehill-2d/.incanto/playtest/lost-seed1.json +4277 -0
- package/templates-app/molehill-2d/PROJECT/Context.md +58 -0
- package/templates-app/molehill-2d/PROJECT/Requirements.md +39 -0
- package/templates-app/molehill-2d/PROJECT/Status.md +27 -0
- package/templates-app/molehill-2d/PROJECT/Structure.md +48 -0
- package/templates-app/molehill-2d/docs/project-2d-rules.md +44 -0
- package/templates-app/molehill-2d/index.html +73 -0
- package/templates-app/molehill-2d/package.json +23 -0
- package/templates-app/molehill-2d/src/behaviors.ts +198 -0
- package/templates-app/molehill-2d/src/game.scene.json +1205 -0
- package/templates-app/molehill-2d/src/main.ts +41 -0
- package/templates-app/molehill-2d/tsconfig.json +13 -0
- package/templates-app/molehill-2d/verify.ts +247 -0
- package/templates-app/molehill-2d/vite.config.ts +12 -0
- package/templates-app/platformer-2d/package.json +2 -2
- package/templates-app/platformer-2d/src/game.scene.json +178 -986
- package/templates-app/platformer-2d/src/main.ts +11 -1
- package/templates-app/platformer-2d/tsconfig.json +1 -1
- package/templates-app/star-survivor/package.json +2 -2
- package/templates-app/star-survivor/src/game.scene.json +43 -216
- package/templates-app/star-survivor/src/main.ts +11 -1
- package/templates-app/star-survivor/tsconfig.json +1 -1
- package/templates-app/tps-3d/PROJECT/Context.md +1 -1
- package/templates-app/tps-3d/package.json +2 -2
- package/templates-app/tps-3d/src/behaviors.ts +19 -1
- package/templates-app/tps-3d/src/game.scene.json +66 -263
- package/templates-app/tps-3d/src/main.ts +13 -2
- package/templates-app/tps-3d/tsconfig.json +1 -1
- package/templates-app/village-quest-3d/.incanto/playtest/swapped-seed1.json +1735 -0
- package/templates-app/village-quest-3d/package.json +2 -2
- package/templates-app/village-quest-3d/src/behaviors.ts +42 -4
- package/templates-app/village-quest-3d/src/grove.scene.json +68 -306
- package/templates-app/village-quest-3d/src/main.ts +11 -1
- package/templates-app/village-quest-3d/src/village.scene.json +202 -863
- package/templates-app/village-quest-3d/tsconfig.json +1 -1
- package/templates-app/village-quest-3d/verify.ts +14 -1
- package/dist/duplicate-Dj4L-5AA.js +0 -22
- package/dist/json-CwwhxQgb.js +0 -36
- package/dist/registry-CF70EArN.js +0 -212
- package/dist/rolldown-runtime-D7D4PA-g.js +0 -13
- package/dist/test-D6ISmuFE.js +0 -2383
- package/dist/touch-DEAmqGdf.js +0 -225
- package/editor/assets/agent8-BDvaZMQJ.js +0 -1
- 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;
|