incanto 0.57.0 → 0.58.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 (63) hide show
  1. package/README.md +6 -4
  2. package/bin/incanto-check.mjs +27 -0
  3. package/bin/incanto-new.mjs +29 -7
  4. package/bin/incanto-playtest.mjs +28 -2
  5. package/bin/incanto-verify.mjs +87 -15
  6. package/bin/incanto.mjs +106 -0
  7. package/dist/2d.js +2 -2
  8. package/dist/3d.js +2 -2
  9. package/dist/{create-game-DpbUrMOQ.js → create-game-BZwWJIns.js} +1 -1
  10. package/dist/{create-game-C5jQYPah.js → create-game-D10bU5_J.js} +56 -8
  11. package/dist/index.js +1 -1
  12. package/dist/{physics-2d-BLcvEFDR.js → physics-2d-EqA-jddf.js} +2 -1
  13. package/dist/{physics-3d-QBrfIT2Y.js → physics-3d-Dnz4fsXX.js} +2 -1
  14. package/dist/quiet-rapier-BAJ4K94N.js +46 -0
  15. package/dist/react.js +1 -1
  16. package/dist/{src-CGjmPw65.js → src-C1J09Op6.js} +1 -1
  17. package/dist/{test-it1VekWs.js → test-ZBga8kQ9.js} +36 -10
  18. package/dist/test.d.ts +1 -1
  19. package/dist/test.js +1 -1
  20. package/dist/vite.js +2 -2
  21. package/editor/assets/{agent8-CGT7r3Mb.js → agent8-BQQjE9UQ.js} +1 -1
  22. package/editor/assets/{debug-BxWSIHG3.js → debug-C9UCsXBS.js} +1 -1
  23. package/editor/assets/{index-CV1m-aX5.js → index-CBgfM3WD.js} +51 -51
  24. package/editor/index.html +1 -1
  25. package/package.json +2 -1
  26. package/skills/incanto-building-2d-games.md +13 -0
  27. package/skills/incanto-building-3d-games.md +3 -3
  28. package/skills/incanto-playtesting.md +19 -5
  29. package/skills/incanto-verifying-your-game.md +11 -4
  30. package/templates-app/beacon-isle-3d/package.json +1 -1
  31. package/templates-app/beacon-isle-3d/src/game.scene.json +7 -6
  32. package/templates-app/platformer-2d/PROJECT/Context.md +70 -0
  33. package/templates-app/platformer-2d/PROJECT/Requirements.md +63 -0
  34. package/templates-app/platformer-2d/PROJECT/Status.md +60 -0
  35. package/templates-app/platformer-2d/PROJECT/Structure.md +77 -0
  36. package/templates-app/platformer-2d/docs/project-2d-rules.md +61 -0
  37. package/templates-app/platformer-2d/index.html +99 -0
  38. package/templates-app/platformer-2d/package.json +23 -0
  39. package/templates-app/platformer-2d/src/behaviors.ts +541 -0
  40. package/templates-app/platformer-2d/src/game.scene.json +2061 -0
  41. package/templates-app/platformer-2d/src/main.ts +68 -0
  42. package/templates-app/platformer-2d/tsconfig.json +13 -0
  43. package/templates-app/platformer-2d/verify.ts +275 -0
  44. package/templates-app/platformer-2d/vite.config.ts +12 -0
  45. package/templates-app/star-survivor/PROJECT/Context.md +55 -0
  46. package/templates-app/star-survivor/PROJECT/Requirements.md +47 -0
  47. package/templates-app/star-survivor/PROJECT/Status.md +44 -0
  48. package/templates-app/star-survivor/PROJECT/Structure.md +63 -0
  49. package/templates-app/star-survivor/docs/project-2d-rules.md +53 -0
  50. package/templates-app/star-survivor/index.html +232 -0
  51. package/templates-app/star-survivor/package.json +23 -0
  52. package/templates-app/star-survivor/src/behaviors.ts +624 -0
  53. package/templates-app/star-survivor/src/game.scene.json +464 -0
  54. package/templates-app/star-survivor/src/main.ts +49 -0
  55. package/templates-app/star-survivor/tsconfig.json +13 -0
  56. package/templates-app/star-survivor/verify.ts +193 -0
  57. package/templates-app/star-survivor/vite.config.ts +12 -0
  58. package/templates-app/tps-3d/package.json +1 -1
  59. package/templates-app/tps-3d/src/game.scene.json +6 -3
  60. package/templates-app/tps-3d/verify.ts +17 -1
  61. package/templates-app/village-quest-3d/package.json +1 -1
  62. package/templates-app/village-quest-3d/src/grove.scene.json +14 -13
  63. package/templates-app/village-quest-3d/src/village.scene.json +5 -5
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Castle Run — a polished 2D adventure PLATFORMER on Incanto. Run a medieval
3
+ * knight through a castle: A/D (or arrows) to run, Space/W to jump — tap again in
4
+ * the air to DOUBLE-JUMP, and the controller forgives you with coyote-time +
5
+ * jump-buffering. STOMP goblins from above (and chain-bounce), ride the moving
6
+ * platform and the lift, grab coins + gems, dodge the spikes and the pit, touch
7
+ * the Checkpoint flag so a fall sends you back there, and reach the gold flag to
8
+ * clear the castle. Three hearts per life, three lives.
9
+ *
10
+ * The level is JSON: StaticBody2D platforms, Patrol/Oscillate moving platforms +
11
+ * goblins, Pickup coins/gems, ScoreKeeper score/lives/win-lose, AudioPlayer SFX —
12
+ * all wired in `game.scene.json`. The custom TypeScript is the game FEEL the
13
+ * built-ins leave open: PlayerController (coyote/buffer/double-jump/variable
14
+ * height/stomp/knockback/checkpoint-respawn/platform-carry), GoblinSkin,
15
+ * FollowCam (follow + screen-shake), ParallaxLayer, HudUpdater.
16
+ */
17
+ import { createGame2D } from 'incanto/2d';
18
+ import goblinUrl from 'incanto/assets/characters/goblin.png';
19
+ // Built-in sprite art ships in the package — import the URLs and inject them into
20
+ // the scene's asset placeholders before boot (the engine clones the scene).
21
+ import knightUrl from 'incanto/assets/characters/medieval-knight.png';
22
+ import coinUrl from 'incanto/assets/items/coin.png';
23
+ import gemUrl from 'incanto/assets/items/gem.png';
24
+ import { FollowCam, GoblinSkin, HudUpdater, ParallaxLayer, PlayerController } from './behaviors';
25
+ import sceneJson from './game.scene.json';
26
+
27
+ // OPTIONAL looping music (large tracks aren't bundled). Point at any URL to loop.
28
+ const MUSIC_URL = '';
29
+
30
+ const behaviors = { PlayerController, GoblinSkin, FollowCam, ParallaxLayer, HudUpdater };
31
+
32
+ const canvas = document.querySelector('canvas') as HTMLCanvasElement;
33
+
34
+ const scene = structuredClone(sceneJson) as typeof sceneJson & {
35
+ assets: {
36
+ knight: { url: string };
37
+ goblin: { url: string };
38
+ coin: { url: string };
39
+ gem: { url: string };
40
+ };
41
+ };
42
+ scene.assets.knight.url = knightUrl;
43
+ scene.assets.goblin.url = goblinUrl;
44
+ scene.assets.coin.url = coinUrl;
45
+ scene.assets.gem.url = gemUrl;
46
+
47
+ const game = await createGame2D({
48
+ debug: import.meta.env.VITE_INCANTO_DEBUG === '1',
49
+ canvas,
50
+ scene,
51
+ behaviors,
52
+ });
53
+
54
+ document.querySelector('#loading')?.remove();
55
+
56
+ if (MUSIC_URL) {
57
+ game.engine.music.play(MUSIC_URL, { loop: true, fadeIn: 1.5 });
58
+ game.engine.audio.music = 0.3;
59
+ }
60
+
61
+ // On-screen JUMP button (mobile + desktop parity); the joystick is auto-shown by
62
+ // createGame2D on coarse pointers for the 'move' action.
63
+ const jumpBtn = document.querySelector('#btn-jump');
64
+ jumpBtn?.addEventListener('pointerdown', () => game.engine.input.handleKey('Space', true));
65
+ jumpBtn?.addEventListener('pointerup', () => game.engine.input.handleKey('Space', false));
66
+
67
+ // Console playground: game.scene.root.getNode('Player').position = [x, y]
68
+ Object.assign(window, { game });
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "skipLibCheck": true,
9
+ "resolveJsonModule": true,
10
+ "types": ["vite/client", "node"]
11
+ },
12
+ "include": ["src", "verify.ts"]
13
+ }
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Headless proof that Castle Run actually PLAYS — the browserless half of the
3
+ * agent loop (author → VERIFY → fix). Drives the REAL `game.scene.json` through
4
+ * `incanto/test`'s `runScript` at a fixed, seeded timestep and asserts the whole
5
+ * composition end to end. The platformer FEEL lives in the custom PlayerController
6
+ * (coyote/buffer/double-jump/stomp/knockback/checkpoint-respawn), so the checks
7
+ * exercise it directly rather than the built-in controller:
8
+ *
9
+ * RUN + JUMP — holding `move` right walks the knight; `jump` while grounded
10
+ * leaves the floor (y rises) and gravity returns it to solid ground.
11
+ * COIN — overlapping a coin Area fires Pickup.collected → ScoreKeeper
12
+ * addScore; the score climbs.
13
+ * STOMP — falling onto a goblin from above frees it and adds score.
14
+ * HIT — touching a goblin from the SIDE costs the player a heart.
15
+ * CHECKPOINT — touching the flag then dying respawns the player AT the flag.
16
+ * WIN — touching the Goal sets the score to scoreToWin → `won`.
17
+ * LOSE — falling into the pit with the last life → `died`-equivalent →
18
+ * ScoreKeeper.loseLife → `lost`.
19
+ *
20
+ * Run with `bun verify.ts` (or `bun run verify`).
21
+ */
22
+ import { registerBehavior } from 'incanto';
23
+ import { auditScene, registerAllNodes, runScript } from 'incanto/test';
24
+ import {
25
+ FollowCam,
26
+ GoblinSkin,
27
+ HudUpdater,
28
+ ParallaxLayer,
29
+ PlayerController,
30
+ } from './src/behaviors';
31
+ import sceneJson from './src/game.scene.json';
32
+
33
+ const behaviors = { PlayerController, GoblinSkin, FollowCam, ParallaxLayer, HudUpdater };
34
+
35
+ const ok = (label: string, cond: boolean): void => {
36
+ if (!cond) {
37
+ console.error(`FAIL: ${label}`);
38
+ process.exitCode = 1;
39
+ } else {
40
+ console.log(`pass: ${label}`);
41
+ }
42
+ };
43
+
44
+ // ---- AUDIT ---------------------------------------------------------------------------
45
+ // The check that fails on a WARNING, not just an error. Three of the five
46
+ // shipped templates had no audit block, which is how every one of them came to
47
+ // ship a scene whose saveable state had no uid to key it under: the save came
48
+ // back empty, the load reported no problem, and nothing here asked.
49
+ {
50
+ registerAllNodes();
51
+ for (const [name, ctor] of Object.entries(behaviors)) {
52
+ registerBehavior(name, ctor as never, { replace: true });
53
+ }
54
+ const warnings = auditScene(sceneJson as unknown as Parameters<typeof auditScene>[0]);
55
+ ok(`auditScene is clean (${warnings.length} warnings)`, warnings.length === 0);
56
+ for (const w of warnings) console.error(` warn: ${w}`);
57
+ }
58
+
59
+ interface Body {
60
+ position: number[];
61
+ velocity: number[];
62
+ }
63
+ interface Player {
64
+ hearts: number;
65
+ }
66
+
67
+ // ---- RUN + JUMP --------------------------------------------------------------
68
+ {
69
+ let startX = 0;
70
+ let startY = 0;
71
+ let leftGround = false;
72
+ let landedY = 0;
73
+
74
+ await runScript(sceneJson, {
75
+ durationMs: 2200,
76
+ seed: 1,
77
+ behaviors,
78
+ steps: [
79
+ {
80
+ atMs: 200,
81
+ label: 'capture spawn pose',
82
+ do: (ctx) => {
83
+ const p = ctx.getNode('Player') as unknown as Body;
84
+ startX = p.position[0] ?? 0;
85
+ startY = p.position[1] ?? 0;
86
+ },
87
+ },
88
+ { atMs: 250, vector: ['move', 1, 0] },
89
+ { atMs: 600, press: 'jump' },
90
+ { atMs: 660, release: 'jump' },
91
+ {
92
+ atMs: 820,
93
+ label: 'jump lifted the knight off the ground',
94
+ do: (ctx) => {
95
+ const p = ctx.getNode('Player') as unknown as Body;
96
+ if ((p.position[1] ?? 0) < startY - 18) leftGround = true;
97
+ },
98
+ },
99
+ { atMs: 900, vector: ['move', 0, 0] },
100
+ {
101
+ atMs: 2000,
102
+ label: 'capture settled pose',
103
+ do: (ctx) => {
104
+ landedY = (ctx.getNode('Player') as unknown as Body).position[1] ?? 0;
105
+ },
106
+ },
107
+ {
108
+ atMs: 2100,
109
+ label: 'knight ran right',
110
+ assert: (ctx) =>
111
+ ((ctx.getNode('Player') as unknown as Body).position[0] ?? 0) > startX + 50,
112
+ },
113
+ ],
114
+ });
115
+
116
+ ok('knight ran right from spawn', true);
117
+ ok('jump lifted the knight off the ground', leftGround);
118
+ ok('knight settled back onto solid ground', landedY > startY - 60 && landedY < startY + 60);
119
+ }
120
+
121
+ // ---- COIN + STOMP + HIT ------------------------------------------------------
122
+ {
123
+ const scores: number[] = [];
124
+ let enemiesBefore = 0;
125
+ let enemiesAfterStomp = 0;
126
+ let heartsAfterHit = 3;
127
+
128
+ const result = await runScript(sceneJson, {
129
+ durationMs: 4000,
130
+ seed: 2,
131
+ behaviors,
132
+ steps: [
133
+ {
134
+ atMs: 100,
135
+ do: (ctx) => {
136
+ ctx.scene.root.on('scoreChanged', (s) => scores.push(s as number));
137
+ enemiesBefore = ctx.scene.tree.getNodesInGroup('enemy').length;
138
+ },
139
+ },
140
+ {
141
+ atMs: 300,
142
+ label: 'collect a coin: snap the knight onto Coin1',
143
+ do: (ctx) => {
144
+ const player = ctx.getNode('Player') as unknown as Body;
145
+ const coin = ctx.getNode('Coins/Coin1') as unknown as Body;
146
+ player.position = [...coin.position];
147
+ for (let i = 0; i < 3; i++) ctx.engine.step();
148
+ },
149
+ },
150
+ {
151
+ atMs: 900,
152
+ label: 'stomp: drop onto Gob1 from above',
153
+ do: (ctx) => {
154
+ const player = ctx.getNode('Player') as unknown as Body;
155
+ const gob = ctx.getNode('Gob1') as unknown as Body;
156
+ player.position = [gob.position[0] ?? 0, (gob.position[1] ?? 0) - 56];
157
+ player.velocity = [0, 220];
158
+ for (let i = 0; i < 8; i++) ctx.engine.step();
159
+ enemiesAfterStomp = ctx.scene.tree.getNodesInGroup('enemy').length;
160
+ },
161
+ },
162
+ {
163
+ atMs: 1600,
164
+ label: 'side hit: overlap Gob2 at the same height → lose a heart',
165
+ do: (ctx) => {
166
+ const player = ctx.getNode('Player') as unknown as Body;
167
+ const gob = ctx.getNode('Gob2') as unknown as Body;
168
+ player.position = [gob.position[0] ?? 0, gob.position[1] ?? 0];
169
+ player.velocity = [0, 0];
170
+ for (let i = 0; i < 3; i++) ctx.engine.step();
171
+ heartsAfterHit = (ctx.getNode('Player').behavior as unknown as Player).hearts;
172
+ },
173
+ },
174
+ ],
175
+ });
176
+
177
+ ok(
178
+ 'a coin raised the score (Pickup → addScore)',
179
+ scores.some((s) => s >= 10),
180
+ );
181
+ ok('stomping a goblin removed it', enemiesBefore > 0 && enemiesAfterStomp === enemiesBefore - 1);
182
+ ok(
183
+ 'a stomp also scored',
184
+ scores.some((s) => s >= 50),
185
+ );
186
+ ok('a side hit cost a heart', heartsAfterHit < 3);
187
+ ok('runScript reported no failures (coin/stomp/hit path)', result.ok);
188
+ }
189
+
190
+ // ---- CHECKPOINT + RESPAWN + WIN ----------------------------------------------
191
+ {
192
+ let won = 0;
193
+ let respawnedAtCheckpoint = false;
194
+
195
+ const result = await runScript(sceneJson, {
196
+ durationMs: 4000,
197
+ seed: 3,
198
+ behaviors,
199
+ steps: [
200
+ { atMs: 100, do: (ctx) => ctx.scene.root.on('won', () => won++) },
201
+ {
202
+ atMs: 300,
203
+ label: 'touch the checkpoint flag',
204
+ do: (ctx) => {
205
+ const player = ctx.getNode('Player') as unknown as Body;
206
+ const cp = ctx.getNode('Checkpoint') as unknown as Body;
207
+ player.position = [...cp.position];
208
+ for (let i = 0; i < 3; i++) ctx.engine.step();
209
+ },
210
+ },
211
+ {
212
+ atMs: 900,
213
+ label: 'die in the pit (lives remain) → respawn at the checkpoint',
214
+ do: (ctx) => {
215
+ const player = ctx.getNode('Player') as unknown as Body;
216
+ const cp = ctx.getNode('Checkpoint') as unknown as Body;
217
+ player.position = [1400, 880]; // into the death plane
218
+ for (let i = 0; i < 4; i++) ctx.engine.step();
219
+ const p = (ctx.getNode('Player') as unknown as Body).position;
220
+ respawnedAtCheckpoint =
221
+ Math.abs((p[0] ?? 0) - (cp.position[0] ?? 0)) < 40 &&
222
+ Math.abs((p[1] ?? 0) - (cp.position[1] ?? 0)) < 60;
223
+ },
224
+ },
225
+ {
226
+ atMs: 1600,
227
+ label: 'reach the goal flag → WIN',
228
+ do: (ctx) => {
229
+ const player = ctx.getNode('Player') as unknown as Body;
230
+ const goal = ctx.getNode('Goal') as unknown as Body;
231
+ player.position = [...goal.position];
232
+ for (let i = 0; i < 4 && won === 0; i++) ctx.engine.step();
233
+ },
234
+ },
235
+ ],
236
+ });
237
+
238
+ ok('dying respawned the knight at the checkpoint', respawnedAtCheckpoint);
239
+ ok('reaching the goal emitted `won` once', won === 1);
240
+ ok('runScript reported no failures (checkpoint/win path)', result.ok);
241
+ }
242
+
243
+ // ---- LOSE (pit with the last life) -------------------------------------------
244
+ {
245
+ let lost = 0;
246
+
247
+ const result = await runScript(sceneJson, {
248
+ durationMs: 2500,
249
+ seed: 4,
250
+ behaviors,
251
+ steps: [
252
+ {
253
+ atMs: 100,
254
+ do: (ctx) => {
255
+ (ctx.scene.root.behavior as unknown as { lives: number }).lives = 1;
256
+ ctx.scene.root.on('lost', () => lost++);
257
+ },
258
+ },
259
+ {
260
+ atMs: 400,
261
+ label: 'fall into the pit with the last life',
262
+ do: (ctx) => {
263
+ const player = ctx.getNode('Player') as unknown as Body;
264
+ player.position = [1400, 880];
265
+ for (let i = 0; i < 6 && lost === 0; i++) ctx.engine.step();
266
+ },
267
+ },
268
+ ],
269
+ });
270
+
271
+ ok('the last life lost the game (`lost`)', lost === 1);
272
+ ok('runScript reported no failures (lose path)', result.ok);
273
+ }
274
+
275
+ console.log(process.exitCode ? '\nVERIFY FAILED' : '\nVERIFY OK — Castle Run plays end to end');
@@ -0,0 +1,12 @@
1
+ import { incantoLibrary, incantoScenes } from 'incanto/vite';
2
+ import { defineConfig } from 'vite';
3
+
4
+ export default defineConfig({
5
+ base: './',
6
+ // Dev-server only, and both are why the editor is useful here:
7
+ // - incantoScenes(): validates every *.scene.json the moment you save it,
8
+ // AND serves this project's scenes, so the editor (☰ debug ▸ edit this
9
+ // scene ▸ scenes) can open, create and save any scene in the project.
10
+ // - incantoLibrary(): the agent8 asset catalog behind the 📚 buttons.
11
+ plugins: [incantoScenes(), incantoLibrary()],
12
+ });
@@ -0,0 +1,55 @@
1
+ # Context — star-survivor (Incanto)
2
+
3
+ ## Project Overview
4
+
5
+ **Hollow Horde** — a top-down SURVIVORS-LIKE (Vampire Survivors genre), the
6
+ clone-and-modify starter for the survivor/roguelite arena. Move a knight with
7
+ WASD/arrows; he AUTO-ATTACKS the nearest enemy. Goblins and faster ghosts pour in
8
+ from every side and grow tougher over time; cut them down, vacuum up the XP shards
9
+ they drop, and on each LEVEL-UP pick one of three upgrades (damage / fire-rate /
10
+ extra bolt / pierce / move-speed / magnet / max-HP). Survive 90 seconds to WIN; let
11
+ the horde drain your HP and you fall.
12
+
13
+ The horde is BUILT-IN gameplay wired in `game.scene.json`; the SURVIVOR feel is the
14
+ custom TypeScript.
15
+
16
+ ## Tech Stack
17
+
18
+ _Exact versions are in `package.json`._
19
+
20
+ - **Engine**: `incanto` + `incanto/2d` (createGame2D; CharacterBody2D/Area2D,
21
+ AnimatedSprite2D, ColorRect2D, Camera2D, AudioPlayer) + `incanto/gameplay`
22
+ (auto-registered Spawner, Chase, Health, DamageOnContact, ScoreKeeper, …).
23
+ - **Art**: built-in animated sheets `medieval-knight` (player), `goblin` + `ghost`
24
+ (enemies), bundler-imported in `main.ts` and injected into the scene asset urls.
25
+ Arena/bolts/XP shards are `ColorRect2D`.
26
+ - **Audio**: procedural SFX presets (shoot/coin/hurt/win/lose). HUD is DOM.
27
+ - **Build/Lang**: Vite, TypeScript. **Headless verify**: `incanto/test` (`runScript`).
28
+
29
+ ## Critical Memory
30
+
31
+ - READ THE SKILLS FIRST: `node_modules/incanto/skills/` —
32
+ `incanto-gameplay-behaviors.md`, `incanto-building-2d-games.md`,
33
+ `incanto-physics-and-input.md`.
34
+ - TOP-DOWN: `physics.gravity` is `[0,0]`. The custom `Survivor` integrates its own
35
+ velocity (`moveAndSlide`); enemies move via built-in `Chase` (moveParent).
36
+ - ONE SCRIPT PER NODE: the player node's script is `Survivor`, so its `Health` lives
37
+ on a child `Vitals` node — `DamageOnContact` finds it by searching the body's
38
+ descendants. Each enemy's `EnemyDrop`/`EnemyFace` are child scripts reading the
39
+ parent (whose script is `Health`).
40
+ - HORDE: the engine clone path (`SpawnSource`) is internal, so two built-in
41
+ `Spawner`s clone hidden goblin/ghost templates and emit `spawned(clone)`; the
42
+ custom `Director` repositions each clone onto a RING around the player and ramps
43
+ spawn rate + enemy HP/speed over time. Spawners sit at the ORIGIN (clone local pos
44
+ = world).
45
+ - LEVEL-UP PAUSE: `Survivor` freezes the world with `this.engine.stop()` while the
46
+ DOM upgrade overlay is shown, and `start()`s on pick. Headless (`verify`) drives
47
+ `engine.step()` (not the loop) and auto-applies an upgrade (guarded by `hasDom`).
48
+ - MANY ENTITIES: bolts + XP shards are spawned `ColorRect2D`s MANAGED by `Survivor`
49
+ (moved + AABB-tested vs the `enemy`/`xp` groups each frame) — cloned nodes can't use
50
+ scene `connections`, so group + AABB is the pattern. WIN = `Survivor` sets the score
51
+ to `scoreToWin` at the survive-timer → `ScoreKeeper` `won`; LOSE = player `Health`
52
+ `died` → `loseLife` → `lost`.
53
+ - `incanto/assets/*` dev imports need an `'incanto/assets'` alias in `vite.config.ts`.
54
+ - `this.engine` is unavailable in `onReady` during `loadScene`; defer scene reads.
55
+ - Node uids omitted (loader-generated). `window.game` exposes the Game handle.
@@ -0,0 +1,47 @@
1
+ # Requirements — star-survivor (Incanto)
2
+
3
+ ## Coding Patterns
4
+
5
+ - Game STRUCTURE belongs in `src/game.scene.json`. Prefer a BUILT-IN behavior over
6
+ custom logic — check `incanto-gameplay-behaviors.md` /
7
+ `incanto-physics-and-input.md` BEFORE writing a Behavior class.
8
+ - TOP-DOWN setup: `physics.gravity [0,0]`. The horde is built-in: two `Spawner`s
9
+ (infinite `total 0`) clone hidden goblin/ghost templates; each enemy `Chase`s
10
+ `/root/Player` (`moveParent`); a `DamageOnContact` hitbox (`targetGroup: player`)
11
+ drains the player's `Health`. `ScoreKeeper(lives:1)` owns win/lose.
12
+ - Custom logic (`src/behaviors.ts`), the survivor FEEL only:
13
+ - `Survivor` — move + facing/anim, AUTO-ATTACK (managed bolts, AABB vs `enemy`),
14
+ XP pickup + magnet (AABB vs `xp`), level-up (pause + 3 upgrade cards), survive
15
+ timer → win, DOM HUD.
16
+ - `Director` — ring-scatter each spawned clone around the player + escalate.
17
+ - `EnemyDrop` (death → XP shard + kill), `EnemyFace` (sprite facing).
18
+ - ONE SCRIPT PER NODE: keep `Health` on a child `Vitals` (player) — DamageOnContact
19
+ searches descendants; keep `EnemyDrop`/`EnemyFace` on child nodes reading the
20
+ parent. Don't try to stack two scripts on one node.
21
+ - CLONES CAN'T USE CONNECTIONS: spawned enemies/bolts/XP shards carry their behaviors
22
+ but NOT scene `connections`. Resolve interactions by GROUP + AABB in one owner
23
+ (`Survivor`), or by a behavior ON the clone (`EnemyDrop` listening to the parent).
24
+ - SPAWN A HORDE without the internal clone API: built-in `Spawner` + a `Director`
25
+ that listens to `spawned(clone)` and repositions onto a ring (spawners at origin so
26
+ clone local = world). Escalate by mutating spawner `interval` + clone Health/Chase.
27
+ - PAUSE for the level-up overlay with `this.engine.stop()` / `start()`; guard DOM use
28
+ with `hasDom` so headless `verify` auto-applies an upgrade.
29
+ - VITE: `vite.config.ts` MUST alias `incanto/assets` → `../../packages/engine/assets`
30
+ (most-specific alias first) or the built-in sprite imports 500 in `bun run dev`.
31
+ - `this.engine` is unavailable in `onReady` during `loadScene` — defer scene reads.
32
+ - Omit node `uid`s (loader-generated). VERIFY headlessly (`bun run verify`) before
33
+ claiming it works.
34
+
35
+ ## Known Issues / Constraints
36
+
37
+ - No collision LAYERS in v0; enemies overlap each other + the player freely (typical
38
+ for survivors). Damage is the enemy `Hit` Area → player `Health`; the player has
39
+ i-frames (`invulnerableFor`).
40
+ - `Health` dies once (no revive), which is fine here — death = game over (`lost`), no
41
+ respawn. Enemies are `freeOnDeath`.
42
+ - The clone path is internal, so spawning leans on built-in `Spawner` + `Director`
43
+ scatter; bolts/XP are managed `ColorRect2D`s (no per-projectile behavior).
44
+ - Sprites: `medieval-knight`/`goblin` (192px frames), `ghost` (112×128). Swap a sheet
45
+ by changing only its asset entry + the import in `main.ts`.
46
+ - Difficulty ramps with elapsed time (spawn rate + enemy HP/speed); tune in `Director`
47
+ and the `Survivor` constants. Music is OPTIONAL (`MUSIC_URL` empty; SFX need no files).
@@ -0,0 +1,44 @@
1
+ # Status — star-survivor (Incanto)
2
+
3
+ ## Implemented
4
+
5
+ - **Hollow Horde** — a top-down survivors-like. Full-window canvas + DOM HUD (HP/XP
6
+ bars, level, kills, survive-timer), a LEVEL-UP overlay with 3 upgrade cards, and a
7
+ win/lose banner.
8
+ - `game.scene.json`: top-down (`gravity [0,0]`), `move` input, knight/goblin/ghost
9
+ spritesheets, `ScoreKeeper(lives 1)`, a dark arena, the `Player`
10
+ (`CharacterBody2D` + `Survivor`, `Health` on child `Vitals`), two built-in
11
+ `Spawner`s (goblin/ghost, infinite) with hidden templates (`Health` freeOnDeath +
12
+ `Chase` + `EnemyFace` + `DamageOnContact` + `EnemyDrop`), a `Director`, a
13
+ `FollowCamera`, and SFX players.
14
+ - Custom `src/behaviors.ts`: `Survivor` (move + auto-attack managed bolts + XP magnet
15
+ + level-up/upgrades + pause via `engine.stop/start` + DOM HUD), `Director`
16
+ (ring-scatter spawns + time escalation), `EnemyDrop`, `EnemyFace`.
17
+ - `main.ts` injects the three sheet urls; `vite.config.ts` aliases `incanto/assets`.
18
+
19
+ ## Works (verified)
20
+
21
+ - `bun run check` → 1/1 scene valid. `bun run typecheck` / `bun run lint` clean.
22
+ - `bun run verify` (headless `runScript`): 7/7 — move walks the player; the
23
+ auto-attack kills a spawned goblin which drops an XP shard + tallies a kill;
24
+ gaining XP raises the level; the survive-timer emits `won`; HP→0 emits `lost`.
25
+ - Played in a real browser (Playwright): the horde streams in from all sides as real
26
+ animated **goblin + ghost** sprites; the knight auto-attacks the nearest enemy;
27
+ kills drop cyan XP shards that the magnet vacuums up; LEVEL-UP freezes the world and
28
+ shows 3 random upgrade cards (e.g. +Damage / +Fire rate / +Pierce / +Magnet / +Max
29
+ HP), and picking one applies it + resumes; difficulty escalates. 0 console errors.
30
+ - HIT FEEDBACK: every bolt that connects pops an additive impact spark, shoves the
31
+ enemy back (knockback), and PUNCHES its sprite (scale-up + opacity dip via
32
+ EnemyFace on the `damaged` signal); a kill adds a colour POOF burst + SFX. The
33
+ ARENA is an INFINITE grid (`GridFloor`): a camera-following backdrop + grid lines
34
+ snapped to world multiples of `cell`, so it tiles forever with no visible border.
35
+
36
+ ## Not Included (on purpose — and next polish passes)
37
+
38
+ - Bolts/XP are simple `ColorRect2D`s (swap for sprites/FX if desired); the grid floor
39
+ is procedural (no tile art).
40
+ - No weapon VARIETY beyond the straight bolt (the upgrade pool tunes one weapon), no
41
+ boss, no meta-progression, no bundled music (SFX presets only).
42
+ - Spawning leans on the built-in `Spawner` + `Director` scatter (the engine's clone
43
+ path is internal). Add enemy types by copying a template + spawner into the
44
+ `Director.spawners` list.
@@ -0,0 +1,63 @@
1
+ # Structure — star-survivor (Incanto)
2
+
3
+ ## `index.html`
4
+
5
+ Full-window page: loading overlay, one `#game` canvas, the DOM HUD (`#hp-track`/
6
+ `#hp-fill`/`#hp-text`, `#xp-track`/`#xp-fill`, `#level-text`, `#kills-text`,
7
+ `#timer-text`, `#hint`), the LEVEL-UP overlay (`#levelup` → `#levelup-level` +
8
+ `#levelup-cards`, populated by `Survivor`), a center `#banner`, and the iframe
9
+ GAME_SIZE script. `Survivor` writes all of these (guarded for headless).
10
+
11
+ ## `src/game.scene.json`
12
+
13
+ The arena + horde, mostly BUILT-INS:
14
+
15
+ - header — `background`, `viewport` (`design [960,540]`, `fit: expand`),
16
+ `physics.gravity [0,0]` (top-down), `move` input (WASD+arrows), and the
17
+ `knight`/`goblin`/`ghost` spritesheet `assets` (urls injected in `main.ts`).
18
+ - `Game` root — `ScoreKeeper` (`scoreToWin 1`, `lives 1`).
19
+ - `Arena`/`ArenaPad`/`ArenaRing*` — dark `ColorRect2D` floor + a centre marker.
20
+ - `Player` — `CharacterBody2D` (group `player`) + `Survivor`; children `Skin`
21
+ (`AnimatedSprite2D` knight) + `Vitals` (`Health`, the body's HP via a child).
22
+ - `GoblinSpawner` / `GhostSpawner` — built-in `Spawner` (interval, max, `total 0`
23
+ = infinite) at the ORIGIN, each with a hidden `visible:false` template child:
24
+ - `GoblinTemplate` / `GhostTemplate` — `CharacterBody2D` (group `enemy`) +
25
+ `Health(freeOnDeath)`; children `Skin` (`AnimatedSprite2D`), `AI` (`Chase`
26
+ target `/root/Player`, `moveParent`), `Face` (`EnemyFace`), `Hit` (`Area2D` +
27
+ `DamageOnContact` targetGroup `player`), `Drop` (`EnemyDrop`).
28
+ - `Director` — custom: listens to both spawners' `spawned`, ring-scatters each
29
+ clone around the player, ramps interval down + enemy HP/speed up.
30
+ - `Camera` — `Camera2D` + `FollowCamera` (target player).
31
+ - `SfxShoot/Xp/Hurt/Win/Lose` — `AudioPlayer` presets.
32
+ - `connections` — `Player/Vitals died → /root loseLife`; `damaged → SfxHurt`;
33
+ root `won/lost → SfxWin/SfxLose`. (Auto-attack/XP/level/win-trigger are in code.)
34
+
35
+ ## `src/behaviors.ts`
36
+
37
+ - `Survivor` — the player brain: top-down move + facing/anim, AUTO-ATTACK (managed
38
+ bolt list, AABB vs `enemy`), XP pickup + magnet (AABB vs `xp`), level-up (pause +
39
+ 3 random upgrade cards + `engine.stop/start`), survive-timer win, and the DOM HUD.
40
+ - `Director` — ring-spawn around the player + time escalation (see above).
41
+ - `EnemyDrop` — on the parent enemy's `died`, drop an XP shard (`ColorRect2D` in
42
+ group `xp` with an `_xp` value) + `Survivor.addKill()`.
43
+ - `EnemyFace` — flip the enemy sprite toward travel + play `move`.
44
+
45
+ ## `src/main.ts`
46
+
47
+ Inject the knight/goblin/ghost urls, `createGame2D({ canvas, scene, behaviors })`,
48
+ optional music, remove the loader, expose `window.game`.
49
+
50
+ ## `vite.config.ts`
51
+
52
+ Aliases `incanto`, `incanto/2d`, AND `incanto/assets` (the last is REQUIRED for the
53
+ built-in sprite imports to resolve in `bun run dev`).
54
+
55
+ ## `verify.ts`
56
+
57
+ Headless `runScript` proof: move, auto-attack kills an enemy → XP shard drop + kill
58
+ tally, XP → level-up, survive-timer → `won`, HP→0 → `lost`. Run with `bun run verify`.
59
+
60
+ ## `docs/project-2d-rules.md`
61
+
62
+ The vibe-coding rules for this survivor template (one-script-per-node, horde-without-
63
+ clone-API, engine pause, AABB-in-one-owner, the `incanto/assets` vite alias, …).
@@ -0,0 +1,53 @@
1
+ <userRequest>
2
+ YOU MUST follow these rules EXACTLY when vibe-coding on this Incanto template
3
+ (Hollow Horde — a top-down survivors-like):
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
+ (Spawner/Chase/Health/DamageOnContact/ScoreKeeper) and
9
+ `incanto-physics-and-input.md` (2D bodies, the input map, units).
10
+ 2. 🧱 STRUCTURE IS JSON: scenes, nodes, props, assets, input maps and connections
11
+ live in `src/game.scene.json`. Add content there, NOT ad-hoc in code.
12
+ 3. 🧠 LOGIC IS BEHAVIORS: prefer a BUILT-IN behavior (here: two `Spawner`s clone
13
+ the enemy templates; each enemy `Chase`s the player; a `DamageOnContact` hitbox
14
+ drains the player's `Health`; `ScoreKeeper` owns lives/win-lose). Custom code is
15
+ the survivor FEEL the library leaves open: `Survivor` (move + AUTO-ATTACK +
16
+ XP/level-up/upgrades + the DOM HUD), `Director` (ring-spawn around the player +
17
+ escalation), `EnemyDrop` (XP shard + kill on death), `EnemyFace` (sprite facing).
18
+ 4. 🔑 UIDS ARE GENERATED: omit `uid` and the loader assigns one. Never hand-invent.
19
+ 5. 📦 ASSETS ARE DECLARED: `assets` entries need `type` + `url`; reference as `"$key"`.
20
+ The knight/goblin/ghost sheets are BUILT-INS imported in `main.ts` and injected
21
+ into the asset url placeholders before boot. ⚠️ For `incanto/assets/*` imports to
22
+ resolve in `bun run dev`, `vite.config.ts` needs an `'incanto/assets'` ALIAS to
23
+ `../../packages/engine/assets` (see this template's config).
24
+ 6. ⬆️ COORDINATES: y-down pixels, (0,0) top-left, clockwise degrees. This is TOP-DOWN
25
+ so `physics.gravity` is `[0, 0]` and `CharacterController2D` would use
26
+ `mode: 'topDown'` (the custom `Survivor` integrates its own move instead).
27
+ 7. 🧲 COLLIDERS ARE PROPS: `"collider": { "shape": "rect"|"circle"|"capsule", … }`
28
+ on a body node — never child shape nodes. Wrong shapes hard-fail at load.
29
+ 8. 🧩 ONE SCRIPT PER NODE: a node has exactly one `script`. So the player's `Health`
30
+ lives on a child `Vitals` node (the player node's script is `Survivor`);
31
+ `DamageOnContact` still finds it by searching the contacted body's DESCENDANTS.
32
+ Likewise each enemy's `EnemyDrop`/`EnemyFace` sit on child nodes that read the
33
+ parent (whose script is `Health`/that `Chase` moves).
34
+ 9. 🌀 SPAWN A HORDE WITHOUT A CLONE API: the engine's clone path (`SpawnSource`) is
35
+ internal, so spawn with the built-in `Spawner` (it clones a hidden `visible:false`
36
+ template child + emits `spawned(clone)`), and let a custom `Director` LISTEN to
37
+ `spawned` and reposition each clone onto a ring around the player. Keep spawners at
38
+ the ORIGIN so a clone's local position equals world (clones attach under the spawner).
39
+ 10. ⏸ PAUSE WITH `engine.stop()` / `engine.start()`: the level-up overlay freezes the
40
+ whole world via `this.engine.stop()` and resumes on pick with `start()`. Guard DOM
41
+ use with a `hasDom` check so the headless `verify` (which drives `engine.step()`,
42
+ not the loop) auto-applies an upgrade instead.
43
+ 11. 🧮 MANY ENTITIES = AABB IN ONE OWNER: bolts + XP shards are spawned `ColorRect2D`s
44
+ MANAGED by `Survivor` (moved each frame, AABB-tested vs the `enemy`/`xp` groups) —
45
+ scene `connections` are NOT cloned, so per-clone wiring won't work; group + AABB does.
46
+ 12. 🎬 DELTA DISCIPLINE: only write props that differ from defaults (unknown props
47
+ hard-fail). 🧭 `this.engine` is NOT available in `onReady` during `loadScene` —
48
+ defer scene reads to the first `fixedUpdate`.
49
+ 13. ✅ VERIFY LIKE A USER: `bun run check` (scene valid) → `bun run verify` (headless
50
+ move/auto-attack/XP-drop/level/win/lose proof) → `bun run dev`, open the browser,
51
+ and actually PLAY (kite the horde, watch the auto-attack, vacuum XP, take a
52
+ level-up) before declaring it done.
53
+ </userRequest>