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
package/editor/index.html CHANGED
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Incanto Scene Editor</title>
7
7
  <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><rect width='16' height='16' rx='3' fill='%236ee7dc'/><text x='8' y='12' text-anchor='middle' font-size='11' font-family='monospace' fill='%230e1018'>i</text></svg>" />
8
- <script type="module" crossorigin src="./assets/index-DMCQXowF.js"></script>
8
+ <script type="module" crossorigin src="./assets/index-DSq0gdhE.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="./assets/GameServer-C56iOUgF.js">
10
10
  <link rel="stylesheet" crossorigin href="./assets/index-D8QvwvOm.css">
11
11
  </head>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incanto",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Vibe-coding-first web game engine SDK — JSON-driven scenes on three.js",
5
5
  "keywords": [
6
6
  "game-engine",
@@ -27,6 +27,7 @@
27
27
  "skills",
28
28
  "bin",
29
29
  "editor",
30
+ "templates-app",
30
31
  "assets",
31
32
  "LICENSE",
32
33
  "THIRD-PARTY-NOTICES.md"
@@ -92,6 +93,7 @@
92
93
  "incanto-check": "bin/incanto-check.mjs",
93
94
  "incanto-assets": "bin/incanto-assets.mjs",
94
95
  "incanto-env": "bin/incanto-env.mjs",
95
- "incanto-play": "bin/incanto-play.mjs"
96
+ "incanto-play": "bin/incanto-play.mjs",
97
+ "incanto-new": "bin/incanto-new.mjs"
96
98
  }
97
99
  }
@@ -193,6 +193,9 @@
193
193
  {
194
194
  "$ref": "#/$defs/BoneAttachment3D"
195
195
  },
196
+ {
197
+ "$ref": "#/$defs/BoneLookAt3D"
198
+ },
196
199
  {
197
200
  "$ref": "#/$defs/Camera2D"
198
201
  },
@@ -226,6 +229,9 @@
226
229
  {
227
230
  "$ref": "#/$defs/HudLayer"
228
231
  },
232
+ {
233
+ "$ref": "#/$defs/InstancedMesh3D"
234
+ },
229
235
  {
230
236
  "$ref": "#/$defs/Joint2D"
231
237
  },
@@ -1196,6 +1202,126 @@
1196
1202
  },
1197
1203
  "required": ["name", "type"]
1198
1204
  },
1205
+ "BoneLookAt3D": {
1206
+ "type": "object",
1207
+ "properties": {
1208
+ "name": {
1209
+ "type": "string"
1210
+ },
1211
+ "uid": {
1212
+ "type": "string"
1213
+ },
1214
+ "type": {
1215
+ "const": "BoneLookAt3D"
1216
+ },
1217
+ "groups": {
1218
+ "type": "array",
1219
+ "items": {
1220
+ "type": "string"
1221
+ }
1222
+ },
1223
+ "tags": {
1224
+ "type": "object"
1225
+ },
1226
+ "props": {
1227
+ "type": "object",
1228
+ "properties": {
1229
+ "position": {
1230
+ "type": "array",
1231
+ "items": {
1232
+ "type": "number"
1233
+ },
1234
+ "minItems": 3,
1235
+ "maxItems": 3,
1236
+ "default": [0, 0, 0]
1237
+ },
1238
+ "rotation": {
1239
+ "type": "array",
1240
+ "items": {
1241
+ "type": "number"
1242
+ },
1243
+ "minItems": 3,
1244
+ "maxItems": 3,
1245
+ "default": [0, 0, 0]
1246
+ },
1247
+ "static": {
1248
+ "type": "boolean",
1249
+ "default": false
1250
+ },
1251
+ "scale": {
1252
+ "type": "array",
1253
+ "items": {
1254
+ "type": "number"
1255
+ },
1256
+ "minItems": 3,
1257
+ "maxItems": 3,
1258
+ "default": [1, 1, 1]
1259
+ },
1260
+ "visible": {
1261
+ "type": "boolean",
1262
+ "default": true
1263
+ },
1264
+ "renderOrder": {
1265
+ "type": "number",
1266
+ "default": 0
1267
+ },
1268
+ "orderGroup": {
1269
+ "type": "string",
1270
+ "enum": ["background", "terrain", "default", "characters", "effects", "overlay"],
1271
+ "default": "default"
1272
+ },
1273
+ "target": {
1274
+ "type": "string",
1275
+ "default": ""
1276
+ },
1277
+ "bone": {
1278
+ "type": "string",
1279
+ "default": "Head"
1280
+ },
1281
+ "lookAt": {
1282
+ "type": "string",
1283
+ "default": ""
1284
+ },
1285
+ "maxAngleDeg": {
1286
+ "type": "number",
1287
+ "default": 75
1288
+ },
1289
+ "weight": {
1290
+ "type": "number",
1291
+ "default": 0.85
1292
+ },
1293
+ "forwardAxis": {
1294
+ "type": "string",
1295
+ "enum": ["+x", "-x", "+y", "-y", "+z", "-z"],
1296
+ "default": "+z"
1297
+ }
1298
+ },
1299
+ "additionalProperties": false
1300
+ },
1301
+ "script": {
1302
+ "type": "object",
1303
+ "properties": {
1304
+ "name": {
1305
+ "type": "string"
1306
+ },
1307
+ "props": {
1308
+ "type": "object"
1309
+ }
1310
+ },
1311
+ "required": ["name"]
1312
+ },
1313
+ "network": {
1314
+ "type": "object"
1315
+ },
1316
+ "children": {
1317
+ "type": "array",
1318
+ "items": {
1319
+ "$ref": "#/$defs/node"
1320
+ }
1321
+ }
1322
+ },
1323
+ "required": ["name", "type"]
1324
+ },
1199
1325
  "Camera2D": {
1200
1326
  "type": "object",
1201
1327
  "properties": {
@@ -2526,6 +2652,131 @@
2526
2652
  },
2527
2653
  "required": ["name", "type"]
2528
2654
  },
2655
+ "InstancedMesh3D": {
2656
+ "type": "object",
2657
+ "properties": {
2658
+ "name": {
2659
+ "type": "string"
2660
+ },
2661
+ "uid": {
2662
+ "type": "string"
2663
+ },
2664
+ "type": {
2665
+ "const": "InstancedMesh3D"
2666
+ },
2667
+ "groups": {
2668
+ "type": "array",
2669
+ "items": {
2670
+ "type": "string"
2671
+ }
2672
+ },
2673
+ "tags": {
2674
+ "type": "object"
2675
+ },
2676
+ "props": {
2677
+ "type": "object",
2678
+ "properties": {
2679
+ "position": {
2680
+ "type": "array",
2681
+ "items": {
2682
+ "type": "number"
2683
+ },
2684
+ "minItems": 3,
2685
+ "maxItems": 3,
2686
+ "default": [0, 0, 0]
2687
+ },
2688
+ "rotation": {
2689
+ "type": "array",
2690
+ "items": {
2691
+ "type": "number"
2692
+ },
2693
+ "minItems": 3,
2694
+ "maxItems": 3,
2695
+ "default": [0, 0, 0]
2696
+ },
2697
+ "static": {
2698
+ "type": "boolean",
2699
+ "default": false
2700
+ },
2701
+ "scale": {
2702
+ "type": "array",
2703
+ "items": {
2704
+ "type": "number"
2705
+ },
2706
+ "minItems": 3,
2707
+ "maxItems": 3,
2708
+ "default": [1, 1, 1]
2709
+ },
2710
+ "visible": {
2711
+ "type": "boolean",
2712
+ "default": true
2713
+ },
2714
+ "renderOrder": {
2715
+ "type": "number",
2716
+ "default": 0
2717
+ },
2718
+ "orderGroup": {
2719
+ "type": "string",
2720
+ "enum": ["background", "terrain", "default", "characters", "effects", "overlay"],
2721
+ "default": "default"
2722
+ },
2723
+ "mesh": {
2724
+ "type": "string",
2725
+ "enum": ["box", "sphere", "capsule", "plane", "cylinder", "gem"],
2726
+ "default": "box"
2727
+ },
2728
+ "size": {
2729
+ "type": "array",
2730
+ "items": {
2731
+ "type": "number"
2732
+ },
2733
+ "minItems": 3,
2734
+ "maxItems": 3,
2735
+ "default": [1, 1, 1]
2736
+ },
2737
+ "material": {
2738
+ "type": "object",
2739
+ "default": {}
2740
+ },
2741
+ "transforms": {
2742
+ "type": "array",
2743
+ "default": []
2744
+ },
2745
+ "castShadow": {
2746
+ "type": "boolean",
2747
+ "default": false
2748
+ },
2749
+ "receiveShadow": {
2750
+ "type": "boolean",
2751
+ "default": false
2752
+ }
2753
+ },
2754
+ "additionalProperties": false
2755
+ },
2756
+ "script": {
2757
+ "type": "object",
2758
+ "properties": {
2759
+ "name": {
2760
+ "type": "string"
2761
+ },
2762
+ "props": {
2763
+ "type": "object"
2764
+ }
2765
+ },
2766
+ "required": ["name"]
2767
+ },
2768
+ "network": {
2769
+ "type": "object"
2770
+ },
2771
+ "children": {
2772
+ "type": "array",
2773
+ "items": {
2774
+ "$ref": "#/$defs/node"
2775
+ }
2776
+ }
2777
+ },
2778
+ "required": ["name", "type"]
2779
+ },
2529
2780
  "Joint2D": {
2530
2781
  "type": "object",
2531
2782
  "properties": {
@@ -3228,6 +3479,18 @@
3228
3479
  "type": "boolean",
3229
3480
  "default": true
3230
3481
  },
3482
+ "animationUpper": {
3483
+ "type": "string",
3484
+ "default": ""
3485
+ },
3486
+ "animationUpperLoop": {
3487
+ "type": "boolean",
3488
+ "default": false
3489
+ },
3490
+ "upperBodyRoot": {
3491
+ "type": "string",
3492
+ "default": "Spine"
3493
+ },
3231
3494
  "receiveShadow": {
3232
3495
  "type": "boolean",
3233
3496
  "default": false
@@ -102,6 +102,25 @@ shambling clip + glowing eyes for an enemy that reads as a different creature.
102
102
  "animation": "$walk", "castShadow": true } }
103
103
  ```
104
104
 
105
+ ## Two-layer animation: attack WHILE running (`animationUpper`)
106
+
107
+ A single `animation` clip owns the whole body — setting an attack clip freezes
108
+ the legs. The UPPER LAYER fixes that:
109
+
110
+ ```ts
111
+ skin.animationUpper = '$anims/attack'; // spine-up plays the attack…
112
+ // …while `animation` (idle/run via the controller) keeps driving the legs.
113
+ // One-shots AUTO-CLEAR when the clip ends (and emit animationFinished) —
114
+ // set it per attack and forget it.
115
+ ```
116
+
117
+ - `upperBodyRoot` (default `'Spine'`, Mixamo spellings matched) picks the bone
118
+ subtree the layer owns; the base clip is automatically masked to the rest,
119
+ so there's no half-blended overlap and no stride reset (time stays aligned).
120
+ - `animationUpperLoop: true` for sustained upper loops (carry, aim, wave).
121
+ - Works with the controller's `animations` map untouched — locomotion logic
122
+ never learns the attack exists.
123
+
105
124
  ## Animations without code
106
125
 
107
126
  Map movement states straight in JSON — no behavior needed:
@@ -146,7 +165,24 @@ A sword in the hand, a hat on the head, sparks on a wingtip — parent them to a
146
165
  - Purely VISUAL: it follows the rendered skeleton, so headless the node stays
147
166
  at its prop transform — keep hit checks range-based from the body, never
148
167
  bone-based.
149
- - `model.boneNames()` lists every bone at runtime when you need to hunt one.
168
+ - `model.boneNames()` lists every bone at runtime when you need to hunt one
169
+ (the editor's inspector offers the same list as a dropdown on the `bone` prop).
170
+
171
+ ## Heads that WATCH things (`BoneLookAt3D`)
172
+
173
+ NPCs feel alive when they track you. One node, zero code:
174
+
175
+ ```jsonc
176
+ { "name": "HeadTrack", "type": "BoneLookAt3D",
177
+ "props": { "target": "../Body", "bone": "Head", "lookAt": "%Player" } }
178
+ ```
179
+
180
+ - Runs on top of the playing animation each frame; blends in/out smoothly and
181
+ DISENGAGES when the target leaves the `maxAngleDeg` comfort cone (75° —
182
+ no owl necks).
183
+ - `weight` (0.85) is how far the head commits; `forwardAxis` defaults to the
184
+ mixamo head convention (+z).
185
+ - Also good for chests (`bone: "Spine2"`, small weight) and turrets.
150
186
 
151
187
  ## Facing a direction in 3D — the +Z-FORWARD rule (read before turning ANY skin)
152
188
 
@@ -17,6 +17,19 @@ GLB/glTF/VRM files load via `ModelInstance3D` — see `incanto-3d-models.md` for
17
17
  asset declaration, `targetHeight` sizing, and the `bunx incanto-model` inspector that
18
18
  reports a file's real bounding box and animation names.
19
19
 
20
+ ## Fastest start: scaffold a whole game
21
+
22
+ ```bash
23
+ bunx incanto-new my-game # Beacon Isle — 3D flagship template
24
+ bunx incanto-new my-game --template tps-3d # third-person shooter starter
25
+ bunx incanto-new --list
26
+ ```
27
+
28
+ Templates are COMPLETE games (world generation, quest, enemies, verify
29
+ harness) meant to be reshaped — regenerate the Beacon Isle world with
30
+ `bun run world`, keep the game logic. Prefer starting from one over wiring
31
+ from scratch.
32
+
20
33
  ## Boot sequence (the only TypeScript you need to start)
21
34
 
22
35
  ```ts
@@ -65,6 +78,28 @@ All 3D nodes extend `Node3D` and therefore have the transform props:
65
78
  | `material` | `{}` | `{color: '#hex', metalness: 0..1, roughness: 0..1, opacity: 0..1, clearcoat: 0..1, clearcoatRoughness: 0..1, envMapIntensity: >=0, wireframe, flatShading, depthTest, depthWrite, emissive, emissiveIntensity, map, normalMap, repeat: [u,v]}` — the object IS the material state: omitted keys reset to defaults (`#ffffff`, 0, 1, 1, false, false, depthTest/Write true). `opacity < 1` turns on transparency; `flatShading` gives faceted per-face glints (the gem sparkle). `depthTest: false` (+ a high `renderOrder`) makes a flat `plane`/`cylinder` an ALWAYS-ON-TOP ground decal — AoE telegraphs, selection/spawn rings — that never z-fights with or is hidden by bumpy terrain (`depthWrite: false` keeps it from occluding later effects). `map`/`normalMap` are texture URLs (lazy-loaded, headless-safe; map samples sRGB, normalMap linear), tiled `repeat` times across each face — box/plane UVs span 0..1 per face, so for worldspace density use `repeat: [length/tile, height/tile]` (e.g. a 6×2.5 m brick wall at one tile per 2 m → `[3, 1.25]`). `color` tints the map (near-white keeps the texture's own color). CAR PAINT: `clearcoat: 1` adds a glossy lacquer layer with its own reflection over a metallic base (`clearcoatRoughness` ~0.05-0.1 = mirror lacquer) — THE premium vehicle-paint look; `envMapIntensity` scales how strongly the surface mirrors the scene's sky/HDRI (glass windows want ~2, matte surfaces <1; needs a `sky`/`hdri` environment to reflect — at night with no sky there is nothing to mirror). NB: under ACES a very bright `emissiveIntensity` blows out to white — keep it modest (~1–2) + a saturated color for a vivid GLOWING look. Unknown keys, malformed `repeat`, or `repeat` without a map hard-fail at load |
66
79
  | `castShadow` / `receiveShadow` | `false` | |
67
80
 
81
+ ### `InstancedMesh3D` (hundreds of props, one draw call)
82
+
83
+ Scattered rocks, fence posts, crates, gravestones — identical meshes should
84
+ never be one node each. One instanced node draws them all at once:
85
+
86
+ ```jsonc
87
+ { "name": "Rocks", "type": "InstancedMesh3D",
88
+ "props": { "mesh": "gem", "size": [0.7, 0.5, 0.7],
89
+ "material": { "color": "#8a8f98", "roughness": 0.95, "flatShading": true },
90
+ "transforms": [
91
+ [12.2, 0.1, -8.4, 40, 1.3],
92
+ [-6.0, 0.2, 14.1, 210, 0.8],
93
+ [3.5, 0.0, 22.7, 118, 1.0]
94
+ ] } }
95
+ ```
96
+
97
+ - Each row: `[x, y, z, yawDeg?, scale?]` — dense enough to GENERATE hundreds
98
+ (pair with `terrain.heightAt` for y).
99
+ - Same `mesh`/`size`/`material` surface as MeshInstance3D. Replace the whole
100
+ `transforms` array to update. No per-instance colliders — add a few
101
+ StaticBody3D boxes only where gameplay needs blocking.
102
+
68
103
  ### `LoftMesh3D` (smooth swept hulls — vehicle bodies, canopies)
69
104
 
70
105
  A SMOOTH curved surface swept through cross-sections — what box/capsule assemblies can
@@ -283,6 +318,23 @@ demo into a commercial-looking outdoor scene:
283
318
  dreamier), `strength` how strongly the blurred glow is added back (default
284
319
  0.85; ~1.2 = neon night). One extra half-res blur pass per frame — cheap.
285
320
  Caustics/clouds composites take precedence (pipelines don't stack yet).
321
+ - `post`: the color-grade tail — `{ "vignette"?, "saturation"?, "contrast"? }`.
322
+ `vignette` 0..1 darkens the corners (0.3 = cinematic, 0.6 = heavy mood);
323
+ `saturation` 0..4 (0 = grayscale, 1 = neutral, 1.2 = vivid); `contrast`
324
+ 0.2..3 around mid-gray (1 = neutral). Applied after tone mapping on the same
325
+ composite pass as bloom — declaring `post` WITHOUT `bloom` costs just one
326
+ offscreen pass. The fastest way to give a scene a LOOK: dusk = vignette 0.35
327
+ + saturation 0.85, candy = saturation 1.25 + contrast 1.08. Caustics/clouds
328
+ composites take precedence (pipelines don't stack yet).
329
+ - **Runtime changes**: the renderer re-reads `environment` every frame — call
330
+ `setEnvironment3D(engine, { sky: { elevationDeg: 4 }, exposure: 0.8 })`
331
+ (from `incanto/3d`) to change the look LIVE. One-level deep merge, `null`
332
+ deletes a key, and a bad patch throws in YOUR call stack, never mid-render.
333
+ For a full day/night loop, attach the built-in `DayNight` behavior instead:
334
+ `{ "script": { "name": "DayNight", "props": { "daySeconds": 240 } } }` —
335
+ sun arc + ambient/exposure ramps + a `dayPhaseChanged(dawn|day|dusk|night)`
336
+ signal to hook spawners/music to. Its `hour` field is writable (jump to a
337
+ time, or `paused: true` and animate it yourself).
286
338
  - `shadows`: `true` | `false` | `{ "mapSize": 1024|2048, "radius": n }`.
287
339
  Truthy makes the main DirectionalLight cast (PCF soft, terrain + trees +
288
340
  meshes cast/receive, mesh grass RECEIVES only). One static ortho box (±75 m
@@ -388,6 +440,37 @@ from a `seed`, with `heightAt(x, z)` for spawning/placement and a
388
440
  `StaticBody3D`). Full themes table, custom layer format and the water/beach
389
441
  pairing live in the **incanto-environment** skill.
390
442
 
443
+ ### Navigation on terrain (`buildTerrainNav` + findPath)
444
+
445
+ Enemies that chase across an open world need paths that respect the surface —
446
+ `buildTerrainNav` samples the heightfield into a walkable grid for the core
447
+ A* (`findPath`), and lifts waypoints back ONTO the surface:
448
+
449
+ ```ts
450
+ import { buildTerrainNav } from 'incanto/3d';
451
+ import { findPath } from 'incanto';
452
+
453
+ const nav = buildTerrainNav(terrain, {
454
+ cellSize: 2, // meters per cell (character-scale corridors)
455
+ maxSlopeDeg: 40, // steeper cells are unwalkable
456
+ minHeight: 0.5, // below sea level = unwalkable (pair with your Water3D y)
457
+ });
458
+ const cells = findPath(nav.grid, nav.toCell(ex, ez), nav.toCell(px, pz));
459
+ if (cells) follow.setPath(cells.map(([cx, cy]) => nav.toWorld(cx, cy)));
460
+ // nav.toWorld's y IS the terrain height — PathFollow rides the surface.
461
+ ```
462
+
463
+ - Build it ONCE per scene (onReady) — sampling is O(cells); re-pathing with
464
+ `findPath` per second is cheap (binary-heap A*).
465
+ - Trees/rocks/buildings aren't in the heightfield: stamp them out with
466
+ `nav.setSolid(...nav.toCell(x, z))` after building.
467
+ - Walkability is judged inside each cell (corner samples), so a flat trail
468
+ beside a cliff stays walkable — only genuinely steep cells go solid.
469
+ - **See what the enemies see**: `root.addChild(createNavDebugNode(nav))`
470
+ paints every walkable cell as a translucent quad on the surface (one
471
+ instanced draw call) — add while tuning `cellSize`/`maxSlopeDeg`, drop when
472
+ the routes look right.
473
+
391
474
  ## Performance levers for big scenes
392
475
 
393
476
  - **`static: true`** (any Node3D): the renderer syncs that subtree ONCE and
@@ -119,9 +119,9 @@ active brush. The palette chips offer every char already used in `cells`, the
119
119
  single char (digits map straight to atlas tile indices, other chars need a
120
120
  `legend` entry). The grid grows right/down automatically when you paint past
121
121
  the edge — to grow left/up, move the node instead (cell (0,0) hangs on the
122
- node origin). Merged solid colliders re-derive live as you paint. Each painted
123
- cell is one undo step; toggle paint off (or select another node) to get normal
124
- click-select back.
122
+ node origin). Merged solid colliders re-derive live as you paint. A whole
123
+ drag stroke is ONE undo step; toggle paint off (or select another node) to
124
+ get normal click-select back.
125
125
 
126
126
  ## Generate environments
127
127
 
@@ -749,7 +749,29 @@ Multi-scene games: `flow.goToScene(nextSceneJson, { fadeSeconds: 0.4 })`
749
749
  fades to black, swaps, fades back (headless = instant). Title → level →
750
750
  next level is three JSON files and this one call.
751
751
 
752
- ## Boot loading overlay
752
+ ## DayNight (live 24-hour cycle)
753
+
754
+ Drives the scene `environment` (needs an atmosphere sky): sun elevation/
755
+ azimuth arc, ambient + exposure ramps with a moonlight floor.
756
+
757
+ ```jsonc
758
+ { "script": { "name": "DayNight", "props": { "daySeconds": 240, "startHour": 9 } } }
759
+ ```
760
+
761
+ | Prop | Default | Notes |
762
+ |---|---|---|
763
+ | `daySeconds` | 240 | real seconds per full day |
764
+ | `startHour` | 10 | 0-24 clock at scene start |
765
+ | `maxSunElevationDeg` | 55 | noon height |
766
+ | `nightDarkness` | 0.75 | 0 = night looks like day, 1 = black |
767
+ | `paused` | false | freeze the clock; write `hour` yourself |
768
+
769
+ Signals: `dayPhaseChanged('dawn'|'day'|'dusk'|'night')` on transitions — hook
770
+ spawners, music, or monster aggression to it. The `hour` field (0-24) is
771
+ writable: jump to a time of day in one assignment. For one-off look changes
772
+ use `setEnvironment3D(engine, patch)` from `incanto/3d` directly.
773
+
774
+ ## Boot loading overlay## Boot loading overlay
753
775
 
754
776
  ```ts
755
777
  import { preloadSceneAssets } from 'incanto';
@@ -144,6 +144,24 @@ Signals: `finished`
144
144
  | `target` | `""` | string |
145
145
  | `bone` | `""` | string |
146
146
 
147
+ ## `BoneLookAt3D` — `incanto/3d`
148
+
149
+ | Prop | Default | Kind |
150
+ |---|---|---|
151
+ | `position` | `[0,0,0]` | array |
152
+ | `rotation` | `[0,0,0]` | array |
153
+ | `static` | `false` | boolean |
154
+ | `scale` | `[1,1,1]` | array |
155
+ | `visible` | `true` | boolean |
156
+ | `renderOrder` | `0` | number |
157
+ | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
158
+ | `target` | `""` | string |
159
+ | `bone` | `"Head"` | string |
160
+ | `lookAt` | `""` | string |
161
+ | `maxAngleDeg` | `75` | number |
162
+ | `weight` | `0.85` | number |
163
+ | `forwardAxis` | `"+z"` | one of: `+x` `-x` `+y` `-y` `+z` `-z` |
164
+
147
165
  ## `Camera2D` — `incanto/2d`
148
166
 
149
167
  | Prop | Default | Kind |
@@ -354,6 +372,24 @@ Signals: `movementStateChanged(state)`
354
372
  | `zIndex` | `100` | number |
355
373
  | `visible` | `true` | boolean |
356
374
 
375
+ ## `InstancedMesh3D` — `incanto/3d`
376
+
377
+ | Prop | Default | Kind |
378
+ |---|---|---|
379
+ | `position` | `[0,0,0]` | array |
380
+ | `rotation` | `[0,0,0]` | array |
381
+ | `static` | `false` | boolean |
382
+ | `scale` | `[1,1,1]` | array |
383
+ | `visible` | `true` | boolean |
384
+ | `renderOrder` | `0` | number |
385
+ | `orderGroup` | `"default"` | one of: `background` `terrain` `default` `characters` `effects` `overlay` |
386
+ | `mesh` | `"box"` | one of: `box` `sphere` `capsule` `plane` `cylinder` `gem` |
387
+ | `size` | `[1,1,1]` | array |
388
+ | `material` | `{}` | object |
389
+ | `transforms` | `[]` | array |
390
+ | `castShadow` | `false` | boolean |
391
+ | `receiveShadow` | `false` | boolean |
392
+
357
393
  ## `Joint2D` — `incanto/2d`
358
394
 
359
395
  | Prop | Default | Kind |
@@ -460,6 +496,9 @@ Signals: `movementStateChanged(state)`
460
496
  | `targetHeight` | `0` | number |
461
497
  | `castShadow` | `false` | boolean |
462
498
  | `animationLoop` | `true` | boolean |
499
+ | `animationUpper` | `""` | string |
500
+ | `animationUpperLoop` | `false` | boolean |
501
+ | `upperBodyRoot` | `"Spine"` | string |
463
502
  | `receiveShadow` | `false` | boolean |
464
503
  | `tint` | `""` | string |
465
504
  | `metalness` | `-1` | number |
@@ -961,6 +1000,18 @@ Signals: `totalChanged(total)`
961
1000
 
962
1001
  Signals: `dealtDamage(amount, target)`
963
1002
 
1003
+ ### `DayNight`
1004
+
1005
+ | Prop | Default | Kind |
1006
+ |---|---|---|
1007
+ | `daySeconds` | `240` | number |
1008
+ | `startHour` | `10` | number |
1009
+ | `maxSunElevationDeg` | `55` | number |
1010
+ | `nightDarkness` | `0.75` | number |
1011
+ | `paused` | `false` | boolean |
1012
+
1013
+ Signals: `dayPhaseChanged`
1014
+
964
1015
  ### `FollowCamera`
965
1016
 
966
1017
  | Prop | Default | Kind |
@@ -0,0 +1,44 @@
1
+ # Beacon Isle — Context
2
+
3
+ **The incanto 3D template.** A complete island action-adventure vertical
4
+ slice designed to be COPIED as the starting point for 3D vibe-coding: a
5
+ generated world, a real quest loop, enemies with terrain-aware AI, readable
6
+ combat, win AND lose states, mobile controls, and a headless verify harness —
7
+ all in ~2 files of game code.
8
+
9
+ ## What it plays like
10
+
11
+ You wash up on a golden-hour island. The lighthouse keeper (walk up, **E**)
12
+ asks you to relight three ancient wards. Shades — floating wraiths with
13
+ burning eyes — patrol each ward and HUNT you across the island surface when
14
+ you come close. Cut them down (**click/F**, a real melee swing with the sword
15
+ riding your hand bone), stand at the dark crystal and press **E**: it blazes
16
+ back to life. Three lit wards → report back → fireworks over the lighthouse.
17
+ The shades bite; at 0 HP the isle takes you and you respawn — lit wards stay lit.
18
+
19
+ ## The template story (why it's built this way)
20
+
21
+ 1. **The world is GENERATED, the game is AUTHORED.** `bun run world` re-emits
22
+ `src/game.scene.json` from `generate-world.ts` (deterministic seeds):
23
+ island terrain, sea, tree groves, grass, flowers, and QUEST SITES picked on
24
+ provably walkable ground (the same `buildTerrainNav` grid the enemies use).
25
+ Change the seed → new island, game logic untouched.
26
+ 2. **Game code is two behaviors** (`IsleDirector`, `SwordStrike`); everything
27
+ else — movement/camera/animations, NPC proximity, dialogue, HUD, touch
28
+ controls, contact damage, physics — is engine surface declared in JSON.
29
+ 3. **Verified headlessly**: `bun run verify` plays the whole quest, proves the
30
+ shades hunt, proves death keeps progress, and replays recorded input
31
+ bit-identically.
32
+
33
+ ## Feature map (engine 0.12)
34
+
35
+ | Feature | Where |
36
+ |---|---|
37
+ | `generateTerrain` island + Water3D sea | generate-world.ts |
38
+ | `buildTerrainNav` + `findPath` | ward-site picking AND shade patrol/hunt AI |
39
+ | `BoneAttachment3D` | sword in the right hand |
40
+ | `environment.post` + bloom | golden-hour grade, glowing ward crystals |
41
+ | input-map `"touch"` keys | joystick + buttons on phones, zero code |
42
+ | `UiDialogue`/`UiBar`/`UiBanner` | keeper quest, HP, win/lose |
43
+ | `startVoice('wind')` | speed-reactive ambience |
44
+ | `startRecording`/`replay` | determinism regression in verify.ts |
@@ -0,0 +1,16 @@
1
+ # Beacon Isle — Requirements
2
+
3
+ 1. Quest arc: keeper dialogue (choice) → 3 wards guarded by shades → clear +
4
+ E to relight each → report back → win banner + fireworks.
5
+ 2. Shades: patrol rings AROUND their ward on the island surface; hunt the
6
+ player within 9 m (re-path through terrain, never through sea/cliffs);
7
+ bites drain 20 HP per 1.2 s of contact.
8
+ 3. Losing: 0 HP → banner → fresh respawn; lit wards persist.
9
+ 4. A dark ward with living guards refuses to light (hint line).
10
+ 5. One strike fells a shade (dissolve poof); the sword rides the hand bone
11
+ and plays the melee clip.
12
+ 6. Works on phones: joystick + jump/interact/attack touch buttons from the
13
+ input map alone.
14
+ 7. `bun run verify` green: audit 0 warnings, quest E2E, hunt proof,
15
+ death/respawn proof, bit-identical replay.
16
+ 8. `bun run world` regenerates the island deterministically.