incanto 0.11.0 → 0.13.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 (83) hide show
  1. package/README.md +22 -0
  2. package/bin/incanto-new.mjs +87 -0
  3. package/dist/2d.d.ts +2 -2
  4. package/dist/2d.js +3 -3
  5. package/dist/3d.d.ts +246 -71
  6. package/dist/3d.js +143 -4
  7. package/dist/{behavior-Bod1AyJS.d.ts → behavior-CPibUfnH.d.ts} +4 -2
  8. package/dist/{create-game-CdWhP6ve.js → create-game-Ct86zczq.js} +5 -5
  9. package/dist/{create-game-DZ_4Nfq8.js → create-game-J6g4QInv.js} +75 -24
  10. package/dist/debug.d.ts +1 -1
  11. package/dist/{duplicate-BEvGBtb_.js → duplicate-BINwP0WG.js} +1 -1
  12. package/dist/{gameplay-Dn3yidJw.js → gameplay-sSqrUcnz.js} +75 -3
  13. package/dist/gameplay.d.ts +37 -2
  14. package/dist/gameplay.js +2 -2
  15. package/dist/index.d.ts +3 -30
  16. package/dist/index.js +4 -4
  17. package/dist/{loader-B4OEXDZ8.js → loader-BZqOKfI2.js} +3 -1
  18. package/dist/{loader-BcUDfNHn.d.ts → loader-DILt9PGC.d.ts} +1 -1
  19. package/dist/net.d.ts +1 -1
  20. package/dist/net.js +3 -3
  21. package/dist/{audio-player-Dyjg5k92.d.ts → pathfinding-RWYkNKx9.d.ts} +29 -2
  22. package/dist/{physics-2d-DweTURFt.js → physics-2d-BiIdl51r.js} +8 -4
  23. package/dist/{physics-3d-Br97Ans2.js → physics-3d-DhaAaKJo.js} +8 -4
  24. package/dist/react.d.ts +1 -1
  25. package/dist/react.js +1 -1
  26. package/dist/{register-Cwc-yCYx.js → register-BFFE1Mh1.js} +2 -2
  27. package/dist/{register-ysLR3WQS.js → register-BFw4c83z.js} +508 -123
  28. package/dist/{register-BD6zgrv_.js → register-Djwckzgx.js} +2 -2
  29. package/dist/{register-DJZXxwAn.js → register-nObreUQR.js} +1 -1
  30. package/dist/test.d.ts +2 -2
  31. package/dist/test.js +10 -10
  32. package/editor/assets/{agent8-BxNvAcLA.js → agent8-BF8Jfbkl.js} +1 -1
  33. package/editor/assets/{index-DMCQXowF.js → index-DSq0gdhE.js} +53 -41
  34. package/editor/index.html +1 -1
  35. package/package.json +4 -2
  36. package/schemas/scene.schema.json +263 -0
  37. package/skills/incanto-3d-character.md +37 -1
  38. package/skills/incanto-building-3d-games.md +83 -0
  39. package/skills/incanto-editor.md +3 -3
  40. package/skills/incanto-gameplay-behaviors.md +23 -1
  41. package/skills/incanto-node-reference.md +51 -0
  42. package/templates-app/beacon-isle-3d/PROJECT/Context.md +44 -0
  43. package/templates-app/beacon-isle-3d/PROJECT/Requirements.md +16 -0
  44. package/templates-app/beacon-isle-3d/PROJECT/Status.md +38 -0
  45. package/templates-app/beacon-isle-3d/PROJECT/Structure.md +24 -0
  46. package/templates-app/beacon-isle-3d/docs/project-3d-rules.md +38 -0
  47. package/templates-app/beacon-isle-3d/generate-world.ts +814 -0
  48. package/templates-app/beacon-isle-3d/index.html +68 -0
  49. package/templates-app/beacon-isle-3d/package.json +26 -0
  50. package/templates-app/beacon-isle-3d/src/behaviors.ts +570 -0
  51. package/templates-app/beacon-isle-3d/src/game.scene.json +2992 -0
  52. package/templates-app/beacon-isle-3d/src/main.ts +33 -0
  53. package/templates-app/beacon-isle-3d/tsconfig.json +13 -0
  54. package/templates-app/beacon-isle-3d/verify.ts +285 -0
  55. package/templates-app/beacon-isle-3d/vite.config.ts +5 -0
  56. package/templates-app/tps-3d/PROJECT/Context.md +49 -0
  57. package/templates-app/tps-3d/PROJECT/Requirements.md +38 -0
  58. package/templates-app/tps-3d/PROJECT/Status.md +41 -0
  59. package/templates-app/tps-3d/PROJECT/Structure.md +44 -0
  60. package/templates-app/tps-3d/docs/project-3d-rules.md +38 -0
  61. package/templates-app/tps-3d/index.html +244 -0
  62. package/templates-app/tps-3d/package.json +25 -0
  63. package/templates-app/tps-3d/src/behaviors.ts +405 -0
  64. package/templates-app/tps-3d/src/game.scene.json +677 -0
  65. package/templates-app/tps-3d/src/main.ts +64 -0
  66. package/templates-app/tps-3d/tsconfig.json +13 -0
  67. package/templates-app/tps-3d/verify.ts +167 -0
  68. package/templates-app/tps-3d/vite.config.ts +5 -0
  69. package/templates-app/village-quest-3d/PROJECT/Context.md +39 -0
  70. package/templates-app/village-quest-3d/PROJECT/Requirements.md +36 -0
  71. package/templates-app/village-quest-3d/PROJECT/Status.md +26 -0
  72. package/templates-app/village-quest-3d/PROJECT/Structure.md +30 -0
  73. package/templates-app/village-quest-3d/docs/project-3d-rules.md +38 -0
  74. package/templates-app/village-quest-3d/index.html +68 -0
  75. package/templates-app/village-quest-3d/package.json +25 -0
  76. package/templates-app/village-quest-3d/src/behaviors.ts +529 -0
  77. package/templates-app/village-quest-3d/src/grove.scene.json +826 -0
  78. package/templates-app/village-quest-3d/src/main.ts +35 -0
  79. package/templates-app/village-quest-3d/src/quest.ts +18 -0
  80. package/templates-app/village-quest-3d/src/village.scene.json +680 -0
  81. package/templates-app/village-quest-3d/tsconfig.json +13 -0
  82. package/templates-app/village-quest-3d/verify.ts +254 -0
  83. package/templates-app/village-quest-3d/vite.config.ts +5 -0
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Beacon Isle — the incanto 3D template: a generated island (terrain, sea,
3
+ * groves, grass, flowers), a quest with a real NPC, shades that HUNT you
4
+ * across the surface via terrain navigation, melee with a bone-mounted sword,
5
+ * golden-hour grade + bloom, touch controls on phones — regenerate the whole
6
+ * world with `bun run world`, the game logic never changes.
7
+ */
8
+ import { assetUrls, preloadUrls } from 'incanto';
9
+ import { createGame3D } from 'incanto/3d';
10
+ import { BEHAVIORS } from './behaviors';
11
+ import gameJson from './game.scene.json';
12
+
13
+ const fill = document.querySelector('#progress') as HTMLElement | null;
14
+ const text = document.querySelector('#progress-text') as HTMLElement | null;
15
+ await preloadUrls(assetUrls(gameJson.assets), (done, total) => {
16
+ const pct = total > 0 ? Math.round((done / total) * 100) : 100;
17
+ if (fill) fill.style.width = `${pct}%`;
18
+ if (text) text.textContent = `${pct}%`;
19
+ });
20
+
21
+ const canvas = document.querySelector('canvas') as HTMLCanvasElement;
22
+ const game = await createGame3D({
23
+ debug: import.meta.env.VITE_INCANTO_DEBUG === '1',
24
+ canvas,
25
+ scene: gameJson,
26
+ behaviors: BEHAVIORS,
27
+ pointer: true,
28
+ });
29
+
30
+ document.querySelector('#loading')?.remove();
31
+
32
+ // console-poking handle for playtesting
33
+ (window as unknown as { game: typeof game }).game = 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,285 @@
1
+ /**
2
+ * Beacon Isle plays headlessly: audit → full quest E2E (talk → accept → clear
3
+ * shades → relight 3 wards → report → win) → shades hunt on the terrain nav →
4
+ * death respawns with quest progress kept → record/replay bit-identical.
5
+ * Run with `bun verify.ts`.
6
+ */
7
+ import type { UiDialogue, UiText } from 'incanto';
8
+ import { Engine, loadScene, registerBehavior, replay, startRecording } from 'incanto';
9
+ import { enablePhysics3D } from 'incanto/3d';
10
+ import { auditScene, registerAllNodes, runScript } from 'incanto/test';
11
+ import { BEHAVIORS, quest, resetQuest } from './src/behaviors';
12
+ import gameJson from './src/game.scene.json';
13
+
14
+ interface Vec {
15
+ position: number[];
16
+ }
17
+
18
+ const ok = (label: string, cond: boolean): void => {
19
+ if (!cond) {
20
+ console.error(`FAIL: ${label}`);
21
+ process.exitCode = 1;
22
+ } else {
23
+ console.log(`pass: ${label}`);
24
+ }
25
+ };
26
+
27
+ // ---- AUDIT -------------------------------------------------------------------------
28
+ {
29
+ registerAllNodes();
30
+ for (const [name, ctor] of Object.entries(BEHAVIORS)) {
31
+ registerBehavior(name, ctor, { replace: true });
32
+ }
33
+ const warnings = auditScene(gameJson as unknown as Parameters<typeof auditScene>[0]);
34
+ ok(`auditScene is clean (${warnings.length} warnings)`, warnings.length === 0);
35
+ for (const w of warnings) console.error(` warn: ${w}`);
36
+ }
37
+
38
+ // ---- QUEST E2E ---------------------------------------------------------------------
39
+ {
40
+ resetQuest();
41
+ const keeper = { x: 0, z: 0 };
42
+
43
+ const result = await runScript(gameJson, {
44
+ durationMs: 90000,
45
+ seed: 11,
46
+ behaviors: BEHAVIORS,
47
+ steps: [
48
+ {
49
+ atMs: 200,
50
+ label: 'talk to the keeper, accept the quest',
51
+ do: (ctx) => {
52
+ const k = ctx.getNode('/root/Keeper') as unknown as Vec;
53
+ keeper.x = k.position[0] ?? 0;
54
+ keeper.z = k.position[2] ?? 0;
55
+ const player = ctx.getNode('/root/Player') as unknown as Vec;
56
+ player.position = [keeper.x + 1.5, (k.position[1] ?? 0) + 1.2, keeper.z + 1.5];
57
+ for (let i = 0; i < 5; i++) ctx.engine.step();
58
+ ctx.engine.input.pressAction('interact');
59
+ ctx.engine.step();
60
+ ctx.engine.input.releaseAction('interact');
61
+ const talk = ctx.getNode('/root/HUD/Dialogue') as UiDialogue;
62
+ if (!talk.active) throw new Error('dialogue did not open');
63
+ for (let i = 0; i < 200; i++) ctx.engine.step();
64
+ talk.advance();
65
+ for (let i = 0; i < 260; i++) ctx.engine.step();
66
+ talk.choose(0);
67
+ ctx.engine.step();
68
+ if (!quest.accepted) throw new Error('quest not accepted');
69
+ // the E2E teleports through shade packs — don't die mid-script
70
+ (ctx.getNode('/root/Player').behavior as unknown as { current: number }).current = 99999;
71
+ },
72
+ },
73
+ {
74
+ atMs: 5000,
75
+ label: 'shades hunt the player across the terrain nav',
76
+ do: (ctx) => {
77
+ const shade = ctx.getNode('/root/Shade1_1') as unknown as Vec;
78
+ const player = ctx.getNode('/root/Player') as unknown as Vec;
79
+ // stand 6m from the shade (inside aggro)
80
+ player.position = [
81
+ (shade.position[0] ?? 0) + 6,
82
+ (shade.position[1] ?? 0) + 0.4,
83
+ shade.position[2] ?? 0,
84
+ ];
85
+ const gap = () =>
86
+ Math.hypot(
87
+ (shade.position[0] ?? 0) - (player.position[0] ?? 0),
88
+ (shade.position[2] ?? 0) - (player.position[2] ?? 0),
89
+ );
90
+ let reached = false;
91
+ for (let i = 0; i < 700 && !reached; i++) {
92
+ player.position = [
93
+ player.position[0] ?? 0,
94
+ player.position[1] ?? 0,
95
+ player.position[2] ?? 0,
96
+ ];
97
+ ctx.engine.step();
98
+ reached = gap() < 1.6;
99
+ }
100
+ if (!reached) throw new Error(`shade never closed in (gap ${gap().toFixed(1)})`);
101
+ },
102
+ },
103
+ {
104
+ atMs: 12000,
105
+ label: 'clear the guards and relight all three wards',
106
+ do: (ctx) => {
107
+ const engine = ctx.engine;
108
+ const live = () => {
109
+ const root = engine.scene?.tree.root;
110
+ if (!root) throw new Error('no scene');
111
+ return root;
112
+ };
113
+ for (let w = 1; w <= 3; w++) {
114
+ const ward = live().getNode(`Ward${w}`) as unknown as Vec;
115
+ // kill this ward's guards (one strike each)
116
+ for (let round = 0; round < 4; round++) {
117
+ const guards = engine.scene?.tree.getNodesInGroup(`shade-ward${w}`) ?? [];
118
+ const target = guards[0] as unknown as Vec | undefined;
119
+ if (!target) break;
120
+ const player = live().getNode('Player') as unknown as Vec;
121
+ player.position = [
122
+ (target.position[0] ?? 0) + 0.9,
123
+ (target.position[1] ?? 0) + 0.3,
124
+ (target.position[2] ?? 0) + 0.9,
125
+ ];
126
+ engine.step();
127
+ engine.input.pressAction('attack');
128
+ engine.step();
129
+ engine.input.releaseAction('attack');
130
+ for (let i = 0; i < 42; i++) engine.step();
131
+ }
132
+ const left = engine.scene?.tree.getNodesInGroup(`shade-ward${w}`).length ?? -1;
133
+ if (left !== 0) throw new Error(`ward ${w}: ${left} shades survived`);
134
+ // stand at the ward and relight
135
+ const player = live().getNode('Player') as unknown as Vec;
136
+ player.position = [
137
+ (ward.position[0] ?? 0) + 1,
138
+ (ward.position[1] ?? 0) + 1,
139
+ ward.position[2] ?? 0,
140
+ ];
141
+ for (let i = 0; i < 4; i++) engine.step();
142
+ engine.input.pressAction('interact');
143
+ engine.step();
144
+ engine.input.releaseAction('interact');
145
+ for (let i = 0; i < 5; i++) engine.step();
146
+ if (!quest.lit.has(w)) throw new Error(`ward ${w} did not light`);
147
+ }
148
+ if (quest.lit.size !== 3) throw new Error('not all wards lit');
149
+ if (!quest.bossRisen) throw new Error('the Shadeheart never rose');
150
+ },
151
+ },
152
+ {
153
+ atMs: 25000,
154
+ label: 'five strikes fell the Shadeheart',
155
+ do: (ctx) => {
156
+ const engine = ctx.engine;
157
+ const root = engine.scene?.tree.root;
158
+ if (!root) throw new Error('no scene');
159
+ for (let i = 0; i < 60; i++) engine.step(); // boss wakes + starts hunting
160
+ const player = root.getNode('Player') as unknown as Vec;
161
+ for (let round = 0; round < 7 && !quest.bossDown; round++) {
162
+ const boss = engine.scene?.tree.getNodesInGroup('boss')[0] as unknown as
163
+ | Vec
164
+ | undefined;
165
+ if (!boss) break;
166
+ player.position = [
167
+ (boss.position[0] ?? 0) + 1.2,
168
+ (boss.position[1] ?? 0) + 0.3,
169
+ (boss.position[2] ?? 0) + 1.2,
170
+ ];
171
+ engine.step();
172
+ engine.input.pressAction('attack');
173
+ engine.step();
174
+ engine.input.releaseAction('attack');
175
+ for (let i = 0; i < 42; i++) engine.step();
176
+ }
177
+ if (!quest.bossDown) throw new Error('the Shadeheart survived 7 rounds');
178
+ if ((engine.scene?.tree.getNodesInGroup('boss').length ?? -1) !== 0)
179
+ throw new Error('boss node not freed');
180
+ },
181
+ },
182
+ {
183
+ atMs: 30000,
184
+ label: 'report back — quest complete + win banner',
185
+ do: (ctx) => {
186
+ const engine = ctx.engine;
187
+ const root = engine.scene?.tree.root;
188
+ if (!root) throw new Error('no scene');
189
+ const player = root.getNode('Player') as unknown as Vec;
190
+ const k = root.getNode('Keeper') as unknown as Vec;
191
+ player.position = [
192
+ (k.position[0] ?? 0) + 1.5,
193
+ (k.position[1] ?? 0) + 1.2,
194
+ (k.position[2] ?? 0) + 1.5,
195
+ ];
196
+ for (let i = 0; i < 5; i++) engine.step();
197
+ engine.input.pressAction('interact');
198
+ engine.step();
199
+ engine.input.releaseAction('interact');
200
+ const talk = root.getNode('HUD/Dialogue') as UiDialogue;
201
+ for (let i = 0; i < 400; i++) engine.step();
202
+ talk.advance();
203
+ for (let i = 0; i < 500; i++) engine.step();
204
+ talk.advance();
205
+ for (let i = 0; i < 80; i++) engine.step(); // dialogueFinished + fireworks timer
206
+ if (!quest.done) throw new Error('quest never completed');
207
+ const line = (root.getNode('HUD/Quest') as UiText).text;
208
+ if (!line.includes('safe')) throw new Error(`quest line: ${line}`);
209
+ },
210
+ },
211
+ ],
212
+ });
213
+ ok('QUEST: full loop (keeper → hunt → 3 wards → win)', result.ok);
214
+ for (const f of result.failures) console.error(` ${f.label}: ${f.message}`);
215
+ }
216
+
217
+ // ---- DEATH keeps progress ------------------------------------------------------------
218
+ {
219
+ resetQuest();
220
+ quest.accepted = true;
221
+ quest.lit.add(1); // pretend ward 1 was lit before dying
222
+ const result = await runScript(gameJson, {
223
+ durationMs: 60000,
224
+ seed: 5,
225
+ behaviors: BEHAVIORS,
226
+ steps: [
227
+ {
228
+ atMs: 300,
229
+ label: 'death → respawn keeps lit wards',
230
+ do: (ctx) => {
231
+ const engine = ctx.engine;
232
+ const health = () =>
233
+ engine.scene?.tree.root?.getNode('Player')?.behavior as unknown as { current: number };
234
+ for (let i = 0; i < 4000 && (health()?.current ?? 0) > 0; i++) {
235
+ const shades = engine.scene?.tree.getNodesInGroup('shade') ?? [];
236
+ const s = shades[0] as unknown as Vec | undefined;
237
+ if (!s) throw new Error('no shades');
238
+ const p = engine.scene?.tree.root?.getNode('Player') as unknown as Vec;
239
+ p.position = [s.position[0] ?? 0, (s.position[1] ?? 0) + 0.1, s.position[2] ?? 0];
240
+ engine.step();
241
+ }
242
+ if ((health()?.current ?? 1) > 0) throw new Error('shades never killed the player');
243
+ for (let i = 0; i < 130; i++) engine.step(); // respawn countdown
244
+ if ((health()?.current ?? 0) < 100) throw new Error('respawn did not reset HP');
245
+ if (!quest.lit.has(1)) throw new Error('respawn lost the lit ward');
246
+ const shadeCount = engine.scene?.tree.getNodesInGroup('shade').length ?? 0;
247
+ if (shadeCount !== 6) throw new Error(`respawn spawned ${shadeCount} shades`);
248
+ },
249
+ },
250
+ ],
251
+ });
252
+ ok('DEATH: respawn keeps quest progress', result.ok);
253
+ for (const f of result.failures) console.error(` ${f.label}: ${f.message}`);
254
+ }
255
+
256
+ // ---- REPLAY determinism --------------------------------------------------------------
257
+ {
258
+ resetQuest();
259
+ const boot = async (): Promise<Engine> => {
260
+ const engine = new Engine({ seed: 42, scheduler: () => () => {} });
261
+ engine.setScene(loadScene(structuredClone(gameJson) as Parameters<typeof loadScene>[0]));
262
+ await enablePhysics3D(engine);
263
+ return engine;
264
+ };
265
+ const a = await boot();
266
+ const rec = startRecording(a);
267
+ for (let frame = 0; frame <= 240; frame++) {
268
+ if (frame === 20) a.input.handleKey('KeyW', true);
269
+ if (frame === 140) a.input.handleKey('KeyA', true);
270
+ if (frame === 200) a.input.handleKey('KeyW', false);
271
+ a.tick(frame * (1000 / 60));
272
+ }
273
+ const recording = rec.stop();
274
+ const posA = (a.scene?.tree.root?.getNode('Player') as unknown as Vec).position;
275
+ resetQuest();
276
+ const b = await boot();
277
+ replay(b, recording);
278
+ const posB = (b.scene?.tree.root?.getNode('Player') as unknown as Vec).position;
279
+ ok(
280
+ `REPLAY: bit-identical after 240 replayed frames (${posA.map((v) => v.toFixed(3)).join(', ')})`,
281
+ posA.length === 3 && posA.every((v, i) => v === posB[i]),
282
+ );
283
+ }
284
+
285
+ console.log(process.exitCode ? '\nVERIFY FAILED' : '\nVERIFY OK');
@@ -0,0 +1,5 @@
1
+ import { defineConfig } from 'vite';
2
+
3
+ export default defineConfig({
4
+ base: './',
5
+ });
@@ -0,0 +1,49 @@
1
+ # Context — tps-3d (Incanto)
2
+
3
+ ## Project Overview
4
+
5
+ "Vanguard" — a 3D THIRD-PERSON shooter. The camera rides over your soldier's
6
+ shoulder (you SEE your character); the crosshair is where the hitscan rifle
7
+ fires. Move with WASD, look with the mouse (click locks the pointer), Shift to
8
+ sprint, Space to jump, click / F to fire, R to reload. Survive and clear three
9
+ escalating waves of chasing enemies to **SECURE THE AREA**; let them drain your
10
+ HP to 0 and the **MISSION FAILS**.
11
+
12
+ The entire loop is composed from `incanto/gameplay` built-ins wired in
13
+ `src/game.scene.json` — the same gameplay spine as the `fps-3d` example, but with
14
+ `CharacterController3D`'s `view: 'free'` third-person rig and a VISIBLE character.
15
+ The only custom TypeScript is `Shoot` (hitscan + face-aim) and `HudUpdater`.
16
+
17
+ ## Tech Stack
18
+
19
+ _Exact versions are in `package.json`._
20
+
21
+ - **Game engine**: `incanto` + `incanto/3d` (CharacterController3D free-view rig,
22
+ MeshInstance3D, Terrain/Tree3D, environment sky/fog/shadows, spatial audio) +
23
+ `incanto/gameplay` (Health, Chase, WaveSpawner, DamageOnContact, ScoreKeeper).
24
+ - **Build / Lang**: Vite, TypeScript. No React — a single canvas + HTML HUD.
25
+
26
+ ## Critical Memory
27
+
28
+ - READ THE SKILLS FIRST: `node_modules/incanto/skills/incanto-3d-character.md`
29
+ (the controller + camera rigs), `incanto-building-3d-games.md`,
30
+ `incanto-gameplay-behaviors.md`, `incanto-environment.md`, `incanto-audio.md`.
31
+ - **THIRD-PERSON = `view: 'free'`.** The controller OWNS the scene's current
32
+ Camera3D and orbits it behind+above the player (`camDistance` 4.2, `eyeHeight`
33
+ 1.1), looking at the pivot. Do NOT add a FollowCamera — it would fight the rig.
34
+ - **The character is a real GLB model.** The `Player/Skin` is a `ModelInstance3D`
35
+ (`base-model.glb` + mixamo idle/walk/run/jump clips, declared in scene `assets`).
36
+ The controller mounts it at 180° and yaw-rotates it toward the MOVE direction;
37
+ `Shoot.onReady` maps `movementStateChanged` → the matching clip, and
38
+ `Shoot.faceAim()` snaps the model to the AIM direction on each shot so the
39
+ soldier turns to shoot where you look. (No held-weapon bone attachment yet — the
40
+ shot reads via the crosshair + tracer + muzzle flash.)
41
+ - Aim is the CAMERA look direction: `Shoot` reads `controller.yaw/pitch`, computes
42
+ a forward vector, and hitscans the nearest enemy in the crosshair cone. The
43
+ shot originates at the player + `eyeHeight` (chest).
44
+ - ONE owner of authority per concern: enemies are spawned clones — `Health` has
45
+ `freeOnDeath: true` (scene `connections` are NOT cloned, so death cleanup must
46
+ be node-local). Friendly fire is gated by `DamageOnContact.targetGroup`.
47
+ - The HUD + crosshair + banner are plain DOM (`index.html`); `HudUpdater` writes
48
+ the numbers (guarded so headless runs are a no-op).
49
+ - `window.game` is exposed for console poking.
@@ -0,0 +1,38 @@
1
+ # Requirements — tps-3d (Incanto)
2
+
3
+ ## Coding Patterns
4
+
5
+ - Game STRUCTURE belongs in `src/game.scene.json` (nodes, props, input map,
6
+ environment, the player + enemy template, waves). Add nodes there or with
7
+ `npx incanto-editor`, NOT ad-hoc in code.
8
+ - Game LOGIC: reach for a BUILT-IN gameplay behavior first (Health, Chase,
9
+ WaveSpawner, DamageOnContact, ScoreKeeper). This game's ONLY custom behaviors
10
+ are `Shoot` (the click-to-fire hitscan + face-aim, which no built-in can be —
11
+ it reads the camera angle and the mouse) and `HudUpdater` (DOM presentation).
12
+ - THIRD-PERSON camera is declarative: `CharacterController3D` `view: 'free'` +
13
+ `camDistance`/`eyeHeight`. The controller drives the scene's `current` Camera3D
14
+ — never also attach a FollowCamera to that camera.
15
+ - The VISIBLE character is a `ModelInstance3D` named `Skin`, sibling of the
16
+ Controller (the controller rotates `../Skin`). It loads a GLB (`$characters/base`)
17
+ sized by `targetHeight` and animated by mixamo `$anims/*` clips (declared in
18
+ scene `assets`, fetched from the CDN; mixamo retargets onto the model
19
+ automatically). `Shoot.onReady` drives the clip from `movementStateChanged`.
20
+ - Enemies are a hidden `EnemyTemplate` the `WaveSpawner` clones: `Health`
21
+ (`freeOnDeath: true`) on the root, `Chase` (`moveParent: true`) on a child,
22
+ `DamageOnContact` (`targetGroup: 'player'`) on an Area3D hitbox.
23
+ - Generate every node uid with the engine's `newUid()` (or let the editor assign
24
+ them) — NEVER hand-write readable uid strings.
25
+
26
+ ## Known Issues / Constraints
27
+
28
+ - Movement-facing vs aim-facing: the controller turns the soldier toward the MOVE
29
+ direction; `Shoot.faceAim()` overrides that toward the AIM on each shot, so the
30
+ character reads as "aim while standing, run-face while moving". A full
31
+ strafe-aim rig (always face the camera) would be a custom controller tweak.
32
+ - Mouse look needs POINTER LOCK (click the canvas). A free cursor never spins the
33
+ camera (deltas only accumulate while locked or a button is held).
34
+ - HUD is DOM, not nodes: 3D scenes have no 2D `UILayer`/`Label`. The crosshair,
35
+ HUD and banner live in `index.html`; `HudUpdater` fills them (guarded for
36
+ headless). The crosshair marks the camera aim point, not the gun barrel.
37
+ - Zero-asset: arena, soldier and enemies are primitives; SFX are procedural
38
+ presets; the music is a bundled clip. Nothing is fetched at boot.
@@ -0,0 +1,41 @@
1
+ # Status — tps-3d (Incanto)
2
+
3
+ ## Implemented
4
+
5
+ - `src/game.scene.json`: third-person arena — atmosphere sky/fog/shadows, a
6
+ 48×48 walled floor with cover blocks + pillars + a ramp + a conifer `Grove`,
7
+ sun + fill light.
8
+ - **Player** (RigidBody3D, group `player`, `Health` max 100 / 0.6s i-frames):
9
+ `Controller` (CharacterController3D `view: 'free'`, camDistance 4.2, eyeHeight
10
+ 1.1, maxSpeed 4.2, `Shoot` script); a real GLB `Skin` (ModelInstance3D
11
+ `base-model.glb` + mixamo idle/walk/run/jump clips); a `Muzzle` Node3D
12
+ (shot/kill/empty SFX + flash light).
13
+ - **Spawner** (WaveSpawner, 3 waves of 4/6/8) cloning a hidden `EnemyTemplate`
14
+ (CharacterBody3D, group `enemy`, `Health` 30 freeOnDeath, red capsule + glow
15
+ eye, `Chase` moveParent AI, `DamageOnContact` hitbox, spatial growl).
16
+ - Camera3D (current), HurtSfx, `Hud` (HudUpdater). Connections: Player.died →
17
+ `/root.loseLife`, Player.damaged → HurtSfx, Spawner.allCleared → Hud.
18
+ - `src/behaviors.ts`: `Shoot` (18-round clip, hitscan within a ~9° crosshair cone
19
+ to 70 m, tracer + muzzle flash, face-aim on fire, reload) and `HudUpdater`
20
+ (HP/Kills/Wave/Ammo + health bar + SECURE/FAILED banner).
21
+ - `src/main.ts`: preloads the GLB + animation assets with a progress bar
22
+ (`preloadUrls(assetUrls(scene.assets))`), then `createGame3D` with
23
+ `pointer: true`, looping music, on-screen FIRE/JUMP buttons, `window.game`.
24
+ - `index.html`: canvas + center crosshair + HUD overlay + banner + mobile buttons.
25
+
26
+ ## Works (verified)
27
+
28
+ - `bun run check` — scene valid. `bun run verify` — 9/9 headless checks (move,
29
+ waves spawn, Chase closes in, hitscan scores, all-clear WIN, contact-damage LOSE).
30
+ - **Verified by playing in the browser (Playwright)**: over-the-shoulder camera
31
+ frames the soldier (back to camera, `skinRotationY` 180); enemies spawn + chase
32
+ + reach the player; holding fire cleared a wave (Kills 0→4) with visible tracers;
33
+ HP drained to 0 under a swarm → "MISSION FAILED" banner. HUD updates live.
34
+
35
+ ## Not Included (on purpose)
36
+
37
+ - No held-weapon bone attachment (the model has no rifle in-hand — the shot reads
38
+ via crosshair + tracer + muzzle flash); no dedicated shoot/reload/aim clips
39
+ (the CDN set is idle/walk/run/jump).
40
+ - No cover/peek mechanic, no projectile-drop weapons (hitscan only). Co-op
41
+ multiplayer lives in the sibling `examples/tps-3d-multiplay`.
@@ -0,0 +1,44 @@
1
+ # Structure — tps-3d (Incanto)
2
+
3
+ ## `index.html`
4
+
5
+ Full-window canvas + the DOM overlays a 3D scene can't host as nodes: the center
6
+ crosshair (camera aim point), the HUD (wave / HP + bar / kills / ammo), the
7
+ win-lose banner, on-screen FIRE/JUMP buttons, the loading overlay, and the
8
+ iframe-embed GAME_SIZE harness.
9
+
10
+ ## `src/game.scene.json`
11
+
12
+ THE game. Header: atmosphere `environment` (sky/fog/shadows/exposure/ambient),
13
+ physics gravity, and the input map (move/jump/sprint/fire/reload). Tree:
14
+ - `Arena` — walled floor, cover blocks, pillars, a ramp, sun + fill light, a
15
+ conifer `Grove` (Tree3D).
16
+ - `Player` (RigidBody3D, `Health`) → `Controller` (CharacterController3D
17
+ `view: 'free'` + `Shoot`), the visible `Skin` soldier (primitive parts), and a
18
+ `Muzzle` (SFX + flash).
19
+ - `Camera` (Camera3D, current — driven by the controller each frame).
20
+ - `Spawner` (WaveSpawner) → hidden `EnemyTemplate` (Health/Chase/DamageOnContact/
21
+ growl + red skin).
22
+ - `Hud` (HudUpdater). Plus top-level `connections` for death/damage/all-clear.
23
+ Open it with `npx incanto-editor`.
24
+
25
+ ## `src/behaviors.ts`
26
+
27
+ The only custom code. `Shoot` — hitscan rifle on the CharacterController3D: reads
28
+ its yaw/pitch for aim, raycasts the nearest enemy in the crosshair cone, applies
29
+ `Health.damage`, scores kills via ScoreKeeper, spawns a tracer + muzzle flash,
30
+ owns the ammo clip/reload, and `faceAim()`s the soldier to the shot direction.
31
+ `HudUpdater` — reads ScoreKeeper/Health/Shoot/wave each frame into the HTML HUD
32
+ and flips the banner (guarded so headless runs are a no-op).
33
+
34
+ ## `src/main.ts`
35
+
36
+ Boot: `createGame3D({ scene, behaviors: { Shoot, HudUpdater }, pointer: true })`,
37
+ loop the bundled music, wire the on-screen buttons, expose `window.game`.
38
+
39
+ ## `verify.ts`
40
+
41
+ Headless proof (`bun run verify`) that the loop plays end to end — move, waves,
42
+ Chase closing in, the autopilot aiming + firing the hitscan to clear all waves
43
+ (WIN), and an enemy hitbox draining the player to 0 (LOSE) — via `incanto/test`'s
44
+ `runScript` at a seeded fixed timestep.
@@ -0,0 +1,38 @@
1
+ <userRequest>
2
+ YOU MUST follow these rules EXACTLY when vibe-coding on this Incanto template:
3
+
4
+ 1. 📖 READ THE SKILLS FIRST: `node_modules/incanto/skills/` is the engine
5
+ manual, version-pinned to what is installed. Start with
6
+ `incanto-building-3d-games.md`, `incanto-3d-character.md`,
7
+ `incanto-gameplay-behaviors.md`, `incanto-environment.md` and
8
+ `incanto-audio.md` — this template composes all five.
9
+ 2. 🧱 STRUCTURE IS JSON: scenes, nodes, props, assets, input maps and
10
+ replication all live in `src/game.scene.json`. Add nodes there (or with
11
+ `npx incanto-editor`), NOT by constructing them ad-hoc in code.
12
+ 3. 🧠 LOGIC IS BEHAVIORS: gameplay code = small TypeScript Behavior classes,
13
+ registered with `registerBehavior('Name', Class)` and attached in JSON via
14
+ `"script": { "name": "Name" }`. Reach for a BUILT-IN gameplay behavior
15
+ (Health, Chase, WaveSpawner, DamageOnContact, ScoreKeeper…) before writing
16
+ your own — this game's only custom behaviors are `Shoot` and `HudUpdater`.
17
+ 4. 🔑 UIDS ARE GENERATED: every node uid comes from
18
+ `import { newUid } from 'incanto'`. NEVER invent readable uid strings.
19
+ 5. 📦 ASSETS ARE DECLARED: `assets` entries need `type` + `url`; reference
20
+ them as `"$key"`. Free extra key-values are preserved — use them to leave
21
+ notes for yourself (license, palette, source prompt).
22
+ 6. ⬆️ Y IS UP: 3D space is y-UP meters, +Z is "south", rotations are degrees
23
+ (Euler XYZ). Negative gravity falls.
24
+ 7. 🧲 COLLIDERS ARE PROPS: `"collider": { "shape": "box"|"sphere"|"capsule", … }`
25
+ on a body node — never child shape nodes. Wrong shapes hard-fail at load.
26
+ 8. 🖥️ THE HUD IS DOM, NOT NODES: 3D scenes have no 2D `UILayer`/`Label` nodes
27
+ (those are `incanto/2d`). The HUD + crosshair + banner are plain HTML in
28
+ `index.html`; `HudUpdater` writes the live numbers into them (guarded so
29
+ headless runs are a no-op).
30
+ 9. 🐛 DEBUG WHEN UNSURE: `physics.debugDraw = true` (the object
31
+ `enablePhysics3D` returns) draws every collider in the running game; or boot
32
+ with `?incanto_debug=1` for the live Explorer/Inspector/Stats overlay.
33
+ 10. 🎬 DELTA DISCIPLINE: only write props that differ from defaults — the
34
+ loader treats unknown/garbage props as hard errors, which is your friend.
35
+ 11. ✅ VERIFY LIKE A USER: `bun run check` validates the scene, `bun run verify`
36
+ proves the loop headlessly, then `bun run dev` and actually PLAY what you
37
+ changed (move, shoot, clear a wave) before declaring it done.
38
+ </userRequest>