incanto 0.12.0 → 0.14.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 +219 -82
  6. package/dist/3d.js +74 -4
  7. package/dist/{behavior-Bod1AyJS.d.ts → behavior-CPibUfnH.d.ts} +4 -2
  8. package/dist/{create-game-C_xf4gm9.js → create-game-Ct86zczq.js} +5 -5
  9. package/dist/{create-game-B2fewJUy.js → create-game-OSntcIiM.js} +5 -5
  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 -3
  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/{pathfinding-BSiXjd6W.d.ts → pathfinding-RWYkNKx9.d.ts} +1 -1
  22. package/dist/{physics-2d-B6D864oZ.js → physics-2d-BiIdl51r.js} +2 -2
  23. package/dist/{physics-3d-RLNaxRyw.js → physics-3d-D_kUdOjr.js} +2 -2
  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-C9-16x_t.js} +562 -157
  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-D_4QY8Aq.js → agent8-BDC4LhjC.js} +1 -1
  33. package/editor/assets/{index-s0DZuroe.js → index-7qOYg7t8.js} +74 -74
  34. package/editor/index.html +1 -1
  35. package/package.json +4 -2
  36. package/schemas/scene.schema.json +266 -6
  37. package/skills/incanto-3d-character.md +35 -0
  38. package/skills/incanto-building-3d-games.md +48 -0
  39. package/skills/incanto-environment.md +15 -16
  40. package/skills/incanto-gameplay-behaviors.md +23 -1
  41. package/skills/incanto-node-reference.md +54 -3
  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 +812 -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 +2969 -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-s0DZuroe.js"></script>
8
+ <script type="module" crossorigin src="./assets/index-7qOYg7t8.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.12.0",
3
+ "version": "0.14.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": {
@@ -2245,8 +2371,7 @@
2245
2371
  "default": 0.5
2246
2372
  },
2247
2373
  "drape": {
2248
- "type": "boolean",
2249
- "default": false
2374
+ "default": null
2250
2375
  },
2251
2376
  "terrain": {
2252
2377
  "type": "string",
@@ -2433,8 +2558,7 @@
2433
2558
  "default": 1
2434
2559
  },
2435
2560
  "drape": {
2436
- "type": "boolean",
2437
- "default": false
2561
+ "default": null
2438
2562
  },
2439
2563
  "terrain": {
2440
2564
  "type": "string",
@@ -2526,6 +2650,131 @@
2526
2650
  },
2527
2651
  "required": ["name", "type"]
2528
2652
  },
2653
+ "InstancedMesh3D": {
2654
+ "type": "object",
2655
+ "properties": {
2656
+ "name": {
2657
+ "type": "string"
2658
+ },
2659
+ "uid": {
2660
+ "type": "string"
2661
+ },
2662
+ "type": {
2663
+ "const": "InstancedMesh3D"
2664
+ },
2665
+ "groups": {
2666
+ "type": "array",
2667
+ "items": {
2668
+ "type": "string"
2669
+ }
2670
+ },
2671
+ "tags": {
2672
+ "type": "object"
2673
+ },
2674
+ "props": {
2675
+ "type": "object",
2676
+ "properties": {
2677
+ "position": {
2678
+ "type": "array",
2679
+ "items": {
2680
+ "type": "number"
2681
+ },
2682
+ "minItems": 3,
2683
+ "maxItems": 3,
2684
+ "default": [0, 0, 0]
2685
+ },
2686
+ "rotation": {
2687
+ "type": "array",
2688
+ "items": {
2689
+ "type": "number"
2690
+ },
2691
+ "minItems": 3,
2692
+ "maxItems": 3,
2693
+ "default": [0, 0, 0]
2694
+ },
2695
+ "static": {
2696
+ "type": "boolean",
2697
+ "default": false
2698
+ },
2699
+ "scale": {
2700
+ "type": "array",
2701
+ "items": {
2702
+ "type": "number"
2703
+ },
2704
+ "minItems": 3,
2705
+ "maxItems": 3,
2706
+ "default": [1, 1, 1]
2707
+ },
2708
+ "visible": {
2709
+ "type": "boolean",
2710
+ "default": true
2711
+ },
2712
+ "renderOrder": {
2713
+ "type": "number",
2714
+ "default": 0
2715
+ },
2716
+ "orderGroup": {
2717
+ "type": "string",
2718
+ "enum": ["background", "terrain", "default", "characters", "effects", "overlay"],
2719
+ "default": "default"
2720
+ },
2721
+ "mesh": {
2722
+ "type": "string",
2723
+ "enum": ["box", "sphere", "capsule", "plane", "cylinder", "gem"],
2724
+ "default": "box"
2725
+ },
2726
+ "size": {
2727
+ "type": "array",
2728
+ "items": {
2729
+ "type": "number"
2730
+ },
2731
+ "minItems": 3,
2732
+ "maxItems": 3,
2733
+ "default": [1, 1, 1]
2734
+ },
2735
+ "material": {
2736
+ "type": "object",
2737
+ "default": {}
2738
+ },
2739
+ "transforms": {
2740
+ "type": "array",
2741
+ "default": []
2742
+ },
2743
+ "castShadow": {
2744
+ "type": "boolean",
2745
+ "default": false
2746
+ },
2747
+ "receiveShadow": {
2748
+ "type": "boolean",
2749
+ "default": false
2750
+ }
2751
+ },
2752
+ "additionalProperties": false
2753
+ },
2754
+ "script": {
2755
+ "type": "object",
2756
+ "properties": {
2757
+ "name": {
2758
+ "type": "string"
2759
+ },
2760
+ "props": {
2761
+ "type": "object"
2762
+ }
2763
+ },
2764
+ "required": ["name"]
2765
+ },
2766
+ "network": {
2767
+ "type": "object"
2768
+ },
2769
+ "children": {
2770
+ "type": "array",
2771
+ "items": {
2772
+ "$ref": "#/$defs/node"
2773
+ }
2774
+ }
2775
+ },
2776
+ "required": ["name", "type"]
2777
+ },
2529
2778
  "Joint2D": {
2530
2779
  "type": "object",
2531
2780
  "properties": {
@@ -3228,6 +3477,18 @@
3228
3477
  "type": "boolean",
3229
3478
  "default": true
3230
3479
  },
3480
+ "animationUpper": {
3481
+ "type": "string",
3482
+ "default": ""
3483
+ },
3484
+ "animationUpperLoop": {
3485
+ "type": "boolean",
3486
+ "default": false
3487
+ },
3488
+ "upperBodyRoot": {
3489
+ "type": "string",
3490
+ "default": "Spine"
3491
+ },
3231
3492
  "receiveShadow": {
3232
3493
  "type": "boolean",
3233
3494
  "default": false
@@ -5393,8 +5654,7 @@
5393
5654
  "default": true
5394
5655
  },
5395
5656
  "drape": {
5396
- "type": "boolean",
5397
- "default": false
5657
+ "default": null
5398
5658
  },
5399
5659
  "terrain": {
5400
5660
  "type": "string",
@@ -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:
@@ -149,6 +168,22 @@ A sword in the hand, a hat on the head, sparks on a wingtip — parent them to a
149
168
  - `model.boneNames()` lists every bone at runtime when you need to hunt one
150
169
  (the editor's inspector offers the same list as a dropdown on the `bone` prop).
151
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.
186
+
152
187
  ## Facing a direction in 3D — the +Z-FORWARD rule (read before turning ANY skin)
153
188
 
154
189
  This trips people up REPEATEDLY, so here is the one rule. agent8's `base-model` (and
@@ -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
@@ -291,6 +326,15 @@ demo into a commercial-looking outdoor scene:
291
326
  offscreen pass. The fastest way to give a scene a LOOK: dusk = vignette 0.35
292
327
  + saturation 0.85, candy = saturation 1.25 + contrast 1.08. Caustics/clouds
293
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).
294
338
  - `shadows`: `true` | `false` | `{ "mapSize": 1024|2048, "radius": n }`.
295
339
  Truthy makes the main DirectionalLight cast (PCF soft, terrain + trees +
296
340
  meshes cast/receive, mesh grass RECEIVES only). One static ortho box (±75 m
@@ -422,6 +466,10 @@ if (cells) follow.setPath(cells.map(([cx, cy]) => nav.toWorld(cx, cy)));
422
466
  `nav.setSolid(...nav.toCell(x, z))` after building.
423
467
  - Walkability is judged inside each cell (corner samples), so a flat trail
424
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.
425
473
 
426
474
  ## Performance levers for big scenes
427
475
 
@@ -358,14 +358,13 @@ tops radially away with a smooth falloff as a character wades through.
358
358
  | `coverage` | `0.85` | mesh: carpet fill in (0, 1] — below 1 noise carves dirt patches (load-time check) |
359
359
  | `flowers` | `0` | DEPRECATED — confetti heads; use a `Flowers3D` node (load-time check 0–0.2) |
360
360
  | `seed` | `1` | placement seed — identical field across runs/machines |
361
- | `drape` | `false` | **drape each blade onto a Terrain3D's surface** so a field follows rolling ground instead of floating on a flat plane — one big field covers a whole terrain |
362
- | `terrain` | `""` | drape target node path; empty = **auto-find the first Terrain3D** in the tree (the zero-config default when `drape` is on) |
361
+ | `drape` | `null` | **AUTO by default**: whenever the scene has a Terrain3D, every blade roots on the ground under it (samples `heightAt`) — omit this prop and rolling terrain just works. `true` forces it (keeps retrying until a terrain appears — streamed worlds), `false` opts out (flat carpet at the node's Y) |
362
+ | `terrain` | `""` | drape target node path; empty = **auto-find the first Terrain3D** in the tree. A non-empty path that is NOT a Terrain3D fails at load — no silent floating |
363
363
 
364
- > **Rolling terrain?** Set `drape: true` and the field's blades root on the
365
- > ground per-instance (it samples the Terrain3D's `heightAt`). Without draping a
366
- > Foliage3D/Flowers3D is a FLAT carpet at the node's Y, so on hills it
367
- > floats/sinks — either drape it, or place small patches at flat spots (the
368
- > generators still do the latter for backward-compatible output).
364
+ > **Rolling terrain just works.** Since 0.14 draping is on by default: with a
365
+ > Terrain3D in the scene the blades root on the ground per-instance, no wiring.
366
+ > Only set `drape: false` if you WANT a flat carpet at the node's Y (e.g. grass
367
+ > on a flat MeshInstance3D platform above the terrain).
369
368
 
370
369
  ## Flowers3D
371
370
 
@@ -415,8 +414,8 @@ billboard confetti. Deterministic from the `seed` PROP.
415
414
  | `height` | `0.45` | plant height, meters (±25% per-plant jitter) |
416
415
  | `clustering` | `0.6` | 0 uniform … 1 tight Voronoi patches (load-time check) |
417
416
  | `sway` | `0.5` | head-bob wind strength; 0 disables |
418
- | `drape` | `false` | drape each plant onto a Terrain3D's surface (rolling terrain) — see the Foliage3D drape note above |
419
- | `terrain` | `""` | drape target node path; empty = auto-find the first Terrain3D |
417
+ | `drape` | `null` | AUTO by default — plants root on the scene's Terrain3D when one exists; `true` forces, `false` opts out — see the Foliage3D drape note above |
418
+ | `terrain` | `""` | drape target node path; empty = auto-find the first Terrain3D (a wrong path fails at load) |
420
419
 
421
420
  ## Tree3D
422
421
  Procedural trees grown by the ported ez-tree generator (MIT, Dan Greenheck;
@@ -484,8 +483,8 @@ Every instance in a grove also gets a deterministic per-instance color jitter (
484
483
  | `leafFadeStart` | `0` | **leaf LOD** — leaf cards begin collapsing toward their branch at this camera distance (m). `0`/`0` disables (full leaves at any range) |
485
484
  | `leafFadeEnd` | `0` | leaf LOD — cards fully collapsed (zero overdraw) by here; tune ≈ the scene `fog.far` so the thinning hides in fog (load-time check: `> leafFadeStart`) |
486
485
  | `leafShadows` | `true` | `false` keeps trunk/branch shadows but drops the expensive alpha-cutout LEAF shadow pass — big FPS in dense forests |
487
- | `drape` | `false` | **drape a scattered grove onto a Terrain3D**: each instance roots at the ground height under it instead of the node's single Y, so a `count > 1` patch on rolling/carved terrain never floats or buries |
488
- | `terrain` | `""` | drape target node path; empty = auto-find the first Terrain3D (the zero-config default when `drape` is on) |
486
+ | `drape` | `null` | AUTO by default: each scattered instance roots at the ground height under it (instead of the node's single Y) whenever the scene has a Terrain3D — a `count > 1` grove on rolling/carved terrain never floats or buries. `true` forces, `false` opts out |
487
+ | `terrain` | `""` | drape target node path; empty = auto-find the first Terrain3D (a wrong path fails at load) |
489
488
 
490
489
  > **Tree LOD / forest FPS.** Leaf overdraw + the leaf shadow pass dominate a
491
490
  > dense-forest frame (~90% in profiling). For big groves set `leafFadeStart`/
@@ -493,11 +492,11 @@ Every instance in a grove also gets a deterministic per-instance color jitter (
493
492
  > leaf cards (branch silhouette stays), and `leafShadows: false` to drop the leaf
494
493
  > shadow pass. Both default off → existing scenes are unchanged.
495
494
 
496
- > **Floating trees?** A scattered grove (`count > 1`) places every instance at
497
- > the node's single Y, so on rolling or basin-carved terrain the far instances
498
- > float or sink. Set `drape: true` (auto-finds the Terrain3D) so each instance
499
- > roots on the ground under it. The drape is a pure heightAt lookup — no rng
500
- > draw, so the seed/grove layout is byte-identical, only the Y shifts.
495
+ > **Floating trees?** Since 0.14 groves drape automatically whenever the scene
496
+ > has a Terrain3D — each instance roots on the ground under it. The drape is a
497
+ > pure heightAt lookup — no rng draw, so the seed/grove layout is
498
+ > byte-identical, only the Y shifts. `drape: false` restores the flat
499
+ > single-Y placement.
501
500
 
502
501
  ## Terrain3D
503
502
  Procedural heightfield terrain with biome texture splatting (in `incanto/3d`,
@@ -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';