incanto 0.1.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 (138) hide show
  1. package/LICENSE +30 -0
  2. package/README.md +36 -0
  3. package/THIRD-PARTY-NOTICES.md +88 -0
  4. package/assets/audio/attacked.mp3 +0 -0
  5. package/assets/audio/explosion.mp3 +0 -0
  6. package/assets/audio/gold_loot.mp3 +0 -0
  7. package/assets/audio/heal.mp3 +0 -0
  8. package/assets/audio/hit_metal_bang.mp3 +0 -0
  9. package/assets/audio/ice_spear.mp3 +0 -0
  10. package/assets/audio/monster_died.mp3 +0 -0
  11. package/assets/audio/slash.mp3 +0 -0
  12. package/assets/audio/smite.mp3 +0 -0
  13. package/assets/audio/spells_cast.mp3 +0 -0
  14. package/assets/audio/ui_click.wav +0 -0
  15. package/assets/audio/walk.mp3 +0 -0
  16. package/assets/catalog.json +390 -0
  17. package/assets/characters/2dbasic.json +41 -0
  18. package/assets/characters/2dbasic.png +0 -0
  19. package/assets/characters/ghost.json +46 -0
  20. package/assets/characters/ghost.png +0 -0
  21. package/assets/characters/goblin.json +40 -0
  22. package/assets/characters/goblin.png +0 -0
  23. package/assets/characters/medieval-knight.json +41 -0
  24. package/assets/characters/medieval-knight.png +0 -0
  25. package/assets/effects/swoosh.png +0 -0
  26. package/assets/items/box.png +0 -0
  27. package/assets/items/buff_potion.png +0 -0
  28. package/assets/items/coin.png +0 -0
  29. package/assets/items/gem.png +0 -0
  30. package/assets/items/gold.png +0 -0
  31. package/assets/items/hp_potion.png +0 -0
  32. package/assets/items/locked_item_box.png +0 -0
  33. package/assets/items/map.png +0 -0
  34. package/assets/items/resurrection_potion.png +0 -0
  35. package/assets/items/super_box.png +0 -0
  36. package/assets/items/trap.png +0 -0
  37. package/assets/tiles/floor00.jpg +0 -0
  38. package/assets/tiles/minecraft-tiles.png +0 -0
  39. package/assets/tiles/wall00.jpg +0 -0
  40. package/assets/vegetation/ash_color.png +0 -0
  41. package/assets/vegetation/aspen_color.png +0 -0
  42. package/assets/vegetation/bark/birch_color_1k.jpg +0 -0
  43. package/assets/vegetation/bark/birch_normal_1k.jpg +0 -0
  44. package/assets/vegetation/bark/birch_roughness_1k.jpg +0 -0
  45. package/assets/vegetation/bark/oak_color_1k.jpg +0 -0
  46. package/assets/vegetation/bark/oak_normal_1k.jpg +0 -0
  47. package/assets/vegetation/bark/oak_roughness_1k.jpg +0 -0
  48. package/assets/vegetation/bark/pine_color_1k.jpg +0 -0
  49. package/assets/vegetation/bark/pine_normal_1k.jpg +0 -0
  50. package/assets/vegetation/bark/pine_roughness_1k.jpg +0 -0
  51. package/assets/vegetation/ground/dirt_color.jpg +0 -0
  52. package/assets/vegetation/ground/dirt_normal.jpg +0 -0
  53. package/assets/vegetation/ground/grass.jpg +0 -0
  54. package/assets/vegetation/oak_color.png +0 -0
  55. package/assets/vegetation/pine_color.png +0 -0
  56. package/bin/incanto-assets.mjs +107 -0
  57. package/bin/incanto-check.mjs +107 -0
  58. package/bin/incanto-editor.mjs +343 -0
  59. package/bin/incanto-env.mjs +144 -0
  60. package/bin/incanto-model.mjs +296 -0
  61. package/bin/incanto-play.mjs +219 -0
  62. package/bin/incanto-skills.mjs +71 -0
  63. package/dist/2d.d.ts +642 -0
  64. package/dist/2d.js +44 -0
  65. package/dist/3d.d.ts +1860 -0
  66. package/dist/3d.js +5 -0
  67. package/dist/agent8-DzU2fFyH.js +129 -0
  68. package/dist/audio-player-DqUR3XFs.d.ts +110 -0
  69. package/dist/behavior-BAQq7HGM.d.ts +851 -0
  70. package/dist/create-game-BdjpTHrW.js +1725 -0
  71. package/dist/create-game-CZHROKcT.js +527 -0
  72. package/dist/debug-draw-CZmOYjL2.js +13 -0
  73. package/dist/debug.d.ts +66 -0
  74. package/dist/debug.js +658 -0
  75. package/dist/duplicate-DP2WPYom.js +22 -0
  76. package/dist/env.d.ts +430 -0
  77. package/dist/env.js +3152 -0
  78. package/dist/errors-BMFaY68Q.d.ts +33 -0
  79. package/dist/errors-BpWbnbb_.js +13 -0
  80. package/dist/gameplay-Ccruc3Wd.js +1501 -0
  81. package/dist/gameplay.d.ts +543 -0
  82. package/dist/gameplay.js +2 -0
  83. package/dist/heightmap-CroQPEER.js +185 -0
  84. package/dist/index.d.ts +305 -0
  85. package/dist/index.js +62 -0
  86. package/dist/json-BLk7H2Qa.js +30 -0
  87. package/dist/loader-CGs_G-r0.js +919 -0
  88. package/dist/loader-Mo0KghCv.d.ts +41 -0
  89. package/dist/net.d.ts +427 -0
  90. package/dist/net.js +772 -0
  91. package/dist/noise-CGUMx44x.js +82 -0
  92. package/dist/particle-sim-CbN4YUuH.d.ts +63 -0
  93. package/dist/particle-sim-DYuSUxvK.js +1319 -0
  94. package/dist/physics-2d-KuMWPTf6.js +288 -0
  95. package/dist/physics-3d-Dl67vOLT.js +434 -0
  96. package/dist/react.d.ts +65 -0
  97. package/dist/react.js +209 -0
  98. package/dist/register-BuUV1_KB.js +561 -0
  99. package/dist/register-CNlYAS1_.js +10634 -0
  100. package/dist/register-DPEV9_9t.js +851 -0
  101. package/dist/register-Dasmnurl.js +374 -0
  102. package/dist/registry-BVJ2HbCn.js +132 -0
  103. package/dist/rng-DP-SR7eg.js +38 -0
  104. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  105. package/dist/schema-CcoWb32N.d.ts +104 -0
  106. package/dist/test.d.ts +158 -0
  107. package/dist/test.js +275 -0
  108. package/dist/touch-031PxtCR.js +208 -0
  109. package/dist/vite.d.ts +26 -0
  110. package/dist/vite.js +57 -0
  111. package/editor/assets/GameServer-C56iOUgF.js +1 -0
  112. package/editor/assets/agent8-Bp7QFI7v.js +1 -0
  113. package/editor/assets/index-DF3tMeKJ.css +1 -0
  114. package/editor/assets/index-Dl2pjA8e.js +7365 -0
  115. package/editor/assets/rapier-CEuLKeCu.js +1 -0
  116. package/editor/assets/rapier-DE6a0vmv.js +1 -0
  117. package/editor/index.html +169 -0
  118. package/package.json +97 -0
  119. package/schemas/scene.schema.json +4254 -0
  120. package/skills/README.md +9 -0
  121. package/skills/incanto-3d-character.md +229 -0
  122. package/skills/incanto-3d-models.md +151 -0
  123. package/skills/incanto-assets.md +118 -0
  124. package/skills/incanto-audio.md +309 -0
  125. package/skills/incanto-behaviors-and-scripts.md +169 -0
  126. package/skills/incanto-building-2d-games.md +242 -0
  127. package/skills/incanto-building-3d-games.md +245 -0
  128. package/skills/incanto-editor.md +163 -0
  129. package/skills/incanto-environment.md +743 -0
  130. package/skills/incanto-gameplay-behaviors.md +707 -0
  131. package/skills/incanto-multiplayer.md +264 -0
  132. package/skills/incanto-node-reference.md +797 -0
  133. package/skills/incanto-physics-and-input.md +164 -0
  134. package/skills/incanto-scene-json-authoring.md +325 -0
  135. package/skills/incanto-verifying-your-game.md +191 -0
  136. package/skills/incanto-web-integration.md +96 -0
  137. package/templates/agent8-server.js +84 -0
  138. package/templates/agent8-server.ts +138 -0
@@ -0,0 +1,743 @@
1
+ ---
2
+ name: incanto-environment
3
+ description: Generating game environments in Incanto — the deterministic `incanto-env` CLI and `incanto/env` library. EXACTLY three themed 3D generators (arena with boxes/ruins/garden, Terrain3D heightfield worlds with island/alpine/plains/desert/meadow/forest/savanna/snow/wetland/volcanic themes, maze with stone/hedge/canyon) plus 2D mazes, roguelike dungeons and platform courses; insertIntoScene, the GENERATORS catalog + runGenerator; and the environment nodes — Terrain3D (biome texture splatting, heightAt, heightfield collider), shader-water Water3D (FBM waves, anisotropic wind ripple, sparse sun glints, crystal shallows, refraction + depth absorption, reflections, shoreline foam, splash signals + ripples), shader-grass Foliage3D (real curved blade meshes, clumps, rolling wind, distance LOD, character bend), Flowers3D flower plants (lush/sparse/none density dial, daisy/cosmos/bellflower varieties, Voronoi patches, head-bob sway) and ez-tree Tree3D groves (branchy trunks, textured wind-swayed leaves, budget-checked tiers). Use when a game needs a level, stage, arena, terrain, heightfield, island, meadow, grass field, flowers, flower bed, forest, trees, mountain, desert, savanna, snow, tundra, swamp, wetland, marsh, volcano, lava field, maze, dungeon, platform course, scattered props, clouds, water, or a splash effect.
4
+ ---
5
+
6
+ # Environment generation — levels from a seed
7
+
8
+ > Shipped inside the `incanto` npm package — this document always matches the
9
+ > installed engine version. Sibling skills live in `node_modules/incanto/skills/`.
10
+
11
+ Generators are PURE and DETERMINISTIC: all randomness flows through a seeded
12
+ `Rng`, so the same `--seed` always emits byte-identical JSON. A generated
13
+ level is reproducible from its seed alone — keep the seed in your scene
14
+ notes/PROJECT docs, and change it (not the output) to reroll a layout.
15
+
16
+ ## CLI
17
+
18
+ ```bash
19
+ bunx incanto-env --list # every generator + params
20
+ bunx incanto-env arena --seed 42 --theme ruins # print the NodeJson
21
+ bunx incanto-env terrain --seed 42 --theme island --json
22
+ bunx incanto-env terrain --seed 42 --theme meadow --size 120
23
+ bunx incanto-env maze --seed 7 --theme hedge --width 10 --depth 10
24
+
25
+ # Insert straight into a scene file (validates with incanto-check afterwards):
26
+ bunx incanto-env terrain --seed 42 --theme forest --into src/game.scene.json --at Root/Level
27
+ ```
28
+
29
+ - `--seed` is REQUIRED — determinism is the contract.
30
+ - Subcommands and `--<param>` flags come from the `GENERATORS` metadata
31
+ (camelCase params become kebab-case flags: `wallHeight` → `--wall-height`;
32
+ boolean params are bare flags: `--water`). `--list` prints the full table.
33
+ - Without `--into` the pretty-printed NodeJson goes to stdout (`--json`
34
+ silences the stderr summary for piping).
35
+ - `--into <file>` reads the scene, appends the generated node under `--at`
36
+ (default: the scene root) and writes the file back pretty-printed.
37
+ - `--at` is the '/'-joined chain of node NAMES from the root, root included:
38
+ `Root/Level` targets the root's child `Level`. A missing path is a hard
39
+ `NODE_NOT_FOUND` listing the children that DO exist.
40
+ - `scatter` is library-only — its item templates don't fit CLI flags.
41
+ - The scene editor (`bunx incanto-editor`) drives the same catalog visually:
42
+ the ✦ Generate dialog builds its param form from `GENERATORS` and inserts
43
+ under the selected node as one undoable step (see the incanto-editor skill).
44
+
45
+ ## The catalog: 3 themed 3D generators + 3 2D generators
46
+
47
+ The 3D set is EXACTLY `arena`, `terrain`, `maze` — the `theme` param does
48
+ the heavy lifting (one knob, big payoff). Every generator: same skeleton,
49
+ themed palette/dressing, deterministic child names, sun/fill lighting.
50
+
51
+ | generator | theme | what you get |
52
+ |---|---|---|
53
+ | `terrain` | `island` (default) | Terrain3D island splat (beach→grass→cliff→snow), edge-wrapped rim, a `Sea` Water3D at a COMPUTED drownable level, conifers on the grass band, a `Clouds` group |
54
+ | `terrain` | `alpine` | rugged alpine splat (meadow valleys, stone, snow from 0.55), conifer treeline at mid heights, boulders in the snow band |
55
+ | `terrain` | `plains` | rolling plains splat (grass + dirt patches), scattered broadleaf trees + boulders |
56
+ | `terrain` | `desert` | dune splat (sand, gravel pans, stone mesas), dead trees + boulders |
57
+ | `terrain` | `meadow` (잔디밭) | LOW grassland splat (ez-tree ground set, noise dirt patches), dense `Foliage3D` TUFT carpets + sparse `Flowers3D` beds at flat spots, broadleaf groves, boulders, a HIGH HOT sun (sunlit-meadow stage) |
58
+ | `terrain` | `forest` | LAYERED woodland on a litter splat (ez-tree dark soil + noise moss patches): ~480 trees in 40 region-mixed groves (low-freq noise picks pine- vs broadleaf-dominant patches) + high-tier ELDERS, aspen/birch accent clumps and sapling fringes; bush undergrowth + FERN tuft beds near trunks; fallen logs (tipped dead trunks) near elders, stumps, mossy boulders; a meadow-treated central clearing (bright tuft carpets + sparse `Flowers3D`) |
59
+ | `terrain` | `savanna` | dry GOLDEN plains on a dry-grass splat (grass + sand pans + dirt patches): sparse wide-LOW-canopy acacias (broadleaf, gold-green tint, 3.5–5 m), cured gold tuft carpets, dry-scrub bush clumps, scattered boulders; warm hazy low sun (exposure 1.05), no water |
60
+ | `terrain` | `snow` | snow-dominant TUNDRA splat (snow at every height, rock on steep) — distinct from alpine's banded peaks: sparse cool-DARK conifers (snow-dusted tint), snow-dusted boulders, a LOW pale crisp sun (exposure 0.9), light fog, no water |
61
+ | `terrain` | `wetland` | SWAMP on a mossy-grass + dark-mud splat near a HIGH water table: a STRUCTURAL broad `Swamp` Water3D (always shipped — `quality: 'simple'`, murky green, not the blue fancy sea), lush `Foliage3D` REED beds at the waterline, mossy ground tufts, dead snags + bush undergrowth, mossy boulders; soft overcast green-grey haze |
62
+ | `terrain` | `volcanic` | dark basalt/ash splat (stone + gravel ash beds): bare DEAD snags, dark basalt boulders, 3 `Particles3D` smoke + ember vent pairs; ember-warm low sun + DENSE dark fog (the smoky read rides the fog + ember light — the atmosphere sky stays blue up high, see LEARNINGS), no water |
63
+ | `arena` | `boxes` (default) | the classic FPS stage: floor, 4 walls, colorful random crate obstacles |
64
+ | `arena` | `ruins` | stone palette, obstacles become broken colonnade rows (standing columns + toppled stumps) |
65
+ | `arena` | `garden` | hedge-green walls/blocks, `Foliage3D` grass patches, lush `Flowers3D` beds, a calm `Water3D` pool at the center |
66
+ | `maze` | `stone` (default) | brick-textured walls (`wall/blocks` color + normal maps) with coping caps, stone floor, west entrance → east exit |
67
+ | `maze` | `hedge` | foliage-look walls (grass map) with trimmed `Foliage3D` grass strips along the wall tops, lawn floor, corridor grass patches |
68
+ | `maze` | `canyon` | warm sandstone walls, sand floor, boulders perched on the wall rim |
69
+
70
+ **`terrain`** (3D heightfield world): emits the canonical physics recipe —
71
+ a `Ground` `StaticBody3D` with `collider: {shape: 'heightfield'}` and a
72
+ `Surface` `Terrain3D` child — plus the theme dressing above, ALL placed by
73
+ probing the exact heightmap the node rebuilds at runtime (`heightAt` for
74
+ tree/rock/grass ground contact, slope + height-band rejection so trees skip
75
+ sand/snow/steep cells). Options: `theme`, `size` (200 m square), `maxHeight`
76
+ (0 = theme default: island 4.5, alpine 8, plains 4, desert 5, meadow 1.2,
77
+ forest 2.5, savanna 3, snow 2.2, wetland 1.4, volcanic 4), `water` (adds a
78
+ low valley `Lake` to non-island themes; the island ALWAYS ships its sea and
79
+ the wetland ALWAYS ships its broad swamp water — see the sea-level math
80
+ below). Islands
81
+ auto-fit `maxHeight` DOWN when a seed realizes rim hills no drownable sea
82
+ could cover. Live (conifer/broadleaf) trees get a per-grove SPECIES color
83
+ drawn from the seed: conifers deep blue-green (pine/spruce/fir) with red-brown
84
+ bark, broadleaf mid-warm-green oak/maple, lighter yellow-green aspen/birch
85
+ (grey bark), plus a few autumnal gold/amber groves — grove-to-grove drift on
86
+ top of Tree3D's per-instance jitter, so a forest reads varied, not one flat
87
+ green. Same seed → byte-identical colors (deterministic); dead/bush keep their
88
+ node defaults.
89
+
90
+ **Themed atmosphere — `terrainEnvironment(theme, size?)`**: generators emit
91
+ node trees, so the matching scene `environment` header ships as a separate
92
+ one-liner. It composes the whole rendering stage per theme — physical `sky`
93
+ (island: bright maritime; forest: low WARM sun raking dappled pools under a
94
+ close green-grey haze, lifted ambient so shadowed floor stays readable; desert:
95
+ warm dusty; alpine: crisp far fog + cooler sun + 0.92 exposure so the snow
96
+ keeps texture; meadow/plains: pleasant mid-morning; savanna: warm hazy late
97
+ afternoon over the gold grass; snow: low pale crisp cold-blue light + 0.9
98
+ exposure; wetland: soft overcast green-grey humid haze + lifted ambient;
99
+ volcanic: ember-warm low sun + DENSE dark fog + 0.86 exposure — the smoky
100
+ mood rides the fog + ember light, NOT the sky dome, which stays Preetham-blue
101
+ up high), size-scaled `fog`, `shadows: true`, a LOW ambient (0.18 — the sky's
102
+ image-based ambience is the fill; the generator's `Sun` light runs at ~1.7
103
+ against it so shadows stay visible under ACES; deep-haze themes lift it:
104
+ forest/wetland 0.24–0.26, volcanic 0.22) and a per-theme `iblIntensity` (the
105
+ `?scene=tree` stand's lit-FORM dial: cuts the physical-sky IBL below full so
106
+ the hot sun carves each crown's lit-vs-shadow side instead of a flat-bright
107
+ wash — volcanic 0.5, forest 0.55, plains 0.58, savanna/meadow 0.6–0.62,
108
+ wetland 0.66, alpine/island/snow 0.72, desert 0.75; island stays mild so the
109
+ deep cut never dulls its sea). Spread it into your scene:
110
+
111
+ ```ts
112
+ import { runGenerator, terrainEnvironment } from 'incanto/env';
113
+ const scene = {
114
+ …,
115
+ environment: { background: '#9bc4e2', ...terrainEnvironment('island') },
116
+ root: { …, children: [camera, runGenerator('terrain', { seed: 7, theme: 'island' })] },
117
+ };
118
+ ```
119
+
120
+ **`arena`** (3D FPS stage): a `StaticBody3D` floor, 4 perimeter walls and N
121
+ obstacles — every body a box collider with a `MeshInstance3D` `Skin` child
122
+ (the canonical visible+collidable composition) — plus `Sun`/`FillLight`/
123
+ `Lamp` lights. Children are deterministically named (`Wall1`…`Wall4`,
124
+ `Obstacle1`…`ObstacleN`). Options: `theme`, `width`, `depth` (meters,
125
+ default 30), `wallHeight` (3), `obstacles` (8).
126
+
127
+ **`maze`** (3D): a recursive-backtracker maze — entrance on the west edge,
128
+ exit on the east, every cell reachable — as one-tile-thick `StaticBody3D`
129
+ wall boxes (merged into runs) over a floor slab, with lights. Every theme
130
+ textures its walls and floor (worldspace `repeat` tiling on the
131
+ `MeshInstance3D` skins) and adds structure: junction `Pillar`s where runs
132
+ meet, `Gate1`–`Gate4` posts framing the entrance/exit plus a tinted
133
+ `EntrancePath`/`ExitPath` floor tile; 'stone' caps every run with coping
134
+ trim, 'hedge' grows `HedgeTop` grass strips along the longest rims, 'canyon'
135
+ keeps its rim boulders. All dressing is decorative `MeshInstance3D` — the
136
+ COLLISION layout (walls only) is identical across themes, and the carved
137
+ LAYOUT is identical across themes for the same seed. Options: `theme`,
138
+ `width`/`depth` (corridor cells, default 8), `cellSize` (2 m), `wallHeight`
139
+ (2.5 m). The boolean grid is also available in the library as
140
+ `carveMaze(rng, cols, rows)` for pathing/item placement.
141
+
142
+ **Maze mood — `mazeEnvironment(theme, span?)`**: the terrainEnvironment
143
+ contract for mazes. Maze scenes should read MOODY at corridor level, not
144
+ showroom-bright: every theme gets a LOW hazy sun, exposure 0.85–0.95, dim
145
+ ambient (≤0.15) and a close fog window (scaled by `span` — pass
146
+ `(2·width+1)·cellSize` if you changed the size, default 34). 'stone' is the
147
+ coolest and foggiest (dungeon courtyard), 'hedge' an overcast garden,
148
+ 'canyon' a warm dusk raking the sandstone. The generator's `Sun`/`FillLight`
149
+ are theme-graded to match — pair both halves:
150
+
151
+ ```ts
152
+ import { mazeEnvironment, runGenerator } from 'incanto/env';
153
+ const scene = {
154
+ …,
155
+ environment: { background: '#9bc4e2', ...mazeEnvironment('stone') },
156
+ root: { …, children: [camera, runGenerator('maze', { seed: 7, theme: 'stone' })] },
157
+ };
158
+ ```
159
+
160
+ **`maze2d`** (2D): the SAME algorithm as `ColorRect2D` floor +
161
+ `StaticBody2D` wall tiles with `ColorRect2D` skins (px, y-down, centered).
162
+ Options: `cols` (10), `rows` (8), `cellPx` (64).
163
+
164
+ **`dungeon2d`** (2D roguelike): rectangular rooms joined by 1-tile
165
+ L-corridors on a 32px tile grid — `ColorRect2D` floor rects (`Room1`…,
166
+ `Corridor1H`/`1V`…) ringed by merged `StaticBody2D` wall segments with
167
+ skins. Options: `rooms` (5), `size` (px; square number on the CLI,
168
+ `[width, height]` via the library; default [960, 720]).
169
+
170
+ **`platforms2d`** (2D course): `StaticBody2D` rect colliders with
171
+ `ColorRect2D` skins, left to right. Spacing is caller-tunable — keep `gapX`
172
+ and `stepY` inside your character's jump arc (px, y-DOWN: negative `stepY`
173
+ steps UP).
174
+
175
+ ### Migrating from the removed generators
176
+
177
+ `meadow`, `forest`, `island`, `rocks` and `clouds` left the catalog — they
178
+ are terrain themes now (`runGenerator('meadow', …)` is a hard error that
179
+ says so):
180
+
181
+ | before | now |
182
+ |---|---|
183
+ | `incanto-env meadow --seed 42` | `incanto-env terrain --seed 42 --theme meadow` |
184
+ | `incanto-env forest --seed 42` | `incanto-env terrain --seed 42 --theme forest` |
185
+ | `incanto-env island --seed 42` | `incanto-env terrain --seed 42 --theme island` (smooth Terrain3D island, computed sea level, heightfield collider — no longer voxel) |
186
+ | `incanto-env rocks` / `clouds` | themes scatter their own; the library still exports `generateRocks`/`generateClouds` (deprecated) for custom compositions |
187
+ | `incanto-env terrain` (voxel) | library-only `generateVoxelTerrain` — see the voxel worlds note |
188
+
189
+ The old functions stay exported from `incanto/env` (deprecated, see jsdoc)
190
+ so library code keeps compiling.
191
+
192
+ ### Voxel worlds
193
+
194
+ The old voxel terrain lives on as **`generateVoxelTerrain`** (library-only,
195
+ NOT in the catalog, not deprecated): a `VoxelGrid3D` whose `voxels` prop
196
+ carries the heightfield as `[x, y, z, tile]` tuples (value noise; grass
197
+ top, dirt fill, bedrock base). Reach for it when BLOCKS are the point —
198
+ digging, building, minecraft-style games on `VoxelGrid3D` (colliders are
199
+ the game's job: chunk trimeshes near the player, see the minecraft
200
+ template). Options: `seed`, `size` (32 blocks), `height` (8), `water`. The
201
+ voxel `generateIsland` (radial dome falloff, sandy shore) also remains for
202
+ the same use case.
203
+
204
+ ## Library
205
+
206
+ ```ts
207
+ import {
208
+ generateArena, generateTerrain, generateMaze, generateMaze2D,
209
+ generateDungeon2D, generatePlatforms2D, generateScatter,
210
+ generateVoxelTerrain, GENERATORS, runGenerator, carveMaze, insertIntoScene,
211
+ } from 'incanto/env';
212
+
213
+ const world = generateTerrain({ seed: 42, theme: 'forest', size: 160 });
214
+
215
+ // Every generator is also invokable uniformly by name — what the CLI and
216
+ // editor use. Unknown names are a hard IncantoError listing the valid ones:
217
+ const island = runGenerator('terrain', { seed: 7, theme: 'island' });
218
+
219
+ // GENERATORS is the machine-readable catalog (description, dimension, typed
220
+ // params with defaults/min/max/options) — render UIs and help from it. The
221
+ // theme params carry options arrays that drive the editor dropdowns:
222
+ console.log(GENERATORS.terrain.params.theme.options);
223
+ // ['island','alpine','plains','desert','meadow','forest','savanna','snow','wetland','volcanic']
224
+
225
+ // Scatter takes weighted node JSON templates — YOU say what to strew:
226
+ const props = generateScatter({
227
+ seed: 42,
228
+ count: 30,
229
+ area: [36, 20],
230
+ items: [
231
+ { type: 'MeshInstance3D', props: { mesh: 'cylinder', size: [0.3, 2, 0.3],
232
+ material: { color: '#6b4a2b' } }, weight: 3 }, // trees (3x as likely)
233
+ { type: 'MeshInstance3D', props: { mesh: 'sphere', size: [0.5, 0.5, 0.5],
234
+ material: { color: '#777777' } } }, // rocks
235
+ ],
236
+ });
237
+ // Each instance gets a random position in `area`, y-rotation, and ±20% scale
238
+ // jitter; the template's position[1] survives as the ground offset.
239
+
240
+ // insertIntoScene is PURE — it returns a NEW scene, inputs untouched:
241
+ let scene = JSON.parse(readFileSync('src/game.scene.json', 'utf-8'));
242
+ scene = insertIntoScene(scene, meadow, 'Root/Level');
243
+ scene = insertIntoScene(scene, props); // default: under the root
244
+ writeFileSync('src/game.scene.json', JSON.stringify(scene, null, 2));
245
+ ```
246
+
247
+ Tune `generatePlatforms2D` spacing for reachability:
248
+
249
+ ```ts
250
+ generatePlatforms2D({
251
+ seed: 3,
252
+ count: 14,
253
+ width: [80, 160], // platform width range, px
254
+ gapX: [40, 120], // edge-to-edge horizontal gap, px
255
+ stepY: [-80, 40], // vertical step, px y-down — negative climbs
256
+ start: [0, 300], // first platform center
257
+ });
258
+ ```
259
+
260
+ ## Foliage3D
261
+
262
+ An instanced vegetation field (in `incanto/3d`, registered by
263
+ `registerNodes3D`): grass, flowers or reeds scattered over an XZ `area`.
264
+ Placement is deterministic from the `seed` PROP (never the engine rng), so
265
+ the same JSON grows the identical field on every machine.
266
+
267
+ Four looks, picked by `style`:
268
+
269
+ | style | what renders | when to pick it |
270
+ |---|---|---|
271
+ | `"tufts"` | ONE InstancedMesh of 3-crossed-quad CARDS (12 verts each) carrying a baked multi-blade alpha-cutout texture, per-instance clump-coherent green tints + dry patches, ported sin·cos sway — kind `grass` only | the dense 잔디밭 — the ez-tree demo's actual grass mechanism; overlapping textured fans read as a full meadow at a fraction of the instances. Layer a sparse `mesh` field on top for close-up blade silhouettes |
272
+ | `"mesh"` (default) | ONE InstancedMesh where EVERY instance is a real tapered blade (7-vertex strip) curved along a vertex-shader quadratic bezier — kind `grass` only | real per-blade geometry — best up close; reads thin at field scale |
273
+ | `"blades"` | TWO crossed quads per slot; the fragment shader draws 8 procedural SDF blades per quad — kind `grass` only; bends to the player (`interaction`) and receives scene `fog` (distant fields melt into the haze like the mesh grass / trees / water) | pinning the pre-mesh ported look |
274
+ | `"simple"` | legacy crossed quads, per-instance colorA→colorB lerp, whole-field shear sway | `flowers`/`reeds` (their only look); ultra-cheap set dressing |
275
+
276
+ What the **tufts** style does (the recon finding behind it: the famous
277
+ ez-tree meadow is NOT per-blade geometry — its `grass.glb` is three crossed
278
+ quads with one grayscale tall-grass cutout texture, instanced 5 000× with
279
+ random green instance colors):
280
+
281
+ - the card texture is **baked at runtime** (~80 tapered blade silhouettes
282
+ fanning from a clumped base, dark roots → bright tips) — no asset fetch,
283
+ headless-safe (no DOM → no texture, geometry still deterministic);
284
+ - count is `⌊area·density/6⌋` (cards are BIG); cards stand ~2.2× `height`
285
+ and ride the same regional height / dry-patch / coverage noise as mesh;
286
+ - per-tuft luminance varies ~2× (upstream-style) — that brightness
287
+ diversity is what carves depth into the canopy;
288
+ - tufts skip the character bend (a card fan has no blade tops to push);
289
+ generators emit `style: "tufts"` for their carpets — meadow density ≈ 30.
290
+
291
+ What the **mesh** style adds (all deterministic from `seed`):
292
+
293
+ - **Real curved blades** — each blade bends along a quadratic bezier whose
294
+ tip is displaced by resting lean + wind + character bend; the root never
295
+ moves. Blades are fully opaque: no alpha sorting, no transparency cost.
296
+ - **8× instances** — one blade per instance needs more instances to match
297
+ the SDF coverage, so mesh plants `⌊area·density·8⌋` (still capped at
298
+ `maxInstances`; past the cap the LOD keeps the NEAR field dense).
299
+ - **Clumps** — a Voronoi-ish cell id from the placement position gives each
300
+ tuft a SHARED hue shift and lean direction, so the field grows patchy
301
+ like a real meadow instead of uniformly green.
302
+ - **Regional height field** — low-frequency noise scales blade height
303
+ 0.55–1.7× in ~6 m patches (tall meadow tufts beside short worn spots),
304
+ and tall blades lean/curve harder — they DROOP like real long grass.
305
+ - **Dry-hue patches** — a second noise channel lerps blades toward a dry
306
+ yellow-green ramp, mixing cured-grass hues through the green.
307
+ - **Coverage holes** — `coverage` < 1 (default 0.85) drops blades in
308
+ noise-carved patches so dirt breaks the carpet like a real field;
309
+ `coverage: 1` restores the wall-to-wall carpet (and the exact
310
+ `⌊area·density·8⌋` instance count).
311
+ - **Flowers** — DEPRECATED: `flowers` (0–0.2, default 0) renders that
312
+ fraction of instances as cheap 5-vertex heads riding the canopy. They
313
+ read as confetti — use a **`Flowers3D` node** (real plants, below)
314
+ instead; the generators already do. The prop keeps working for existing
315
+ scenes.
316
+ - **Base-to-tip ramp** — blades root in `groundColor` (soil, with DEEP
317
+ root occlusion — the inside of the volume reads near-black), pass through
318
+ `colorA` and tip out at `colorB`. **Pair `groundColor` with the ground
319
+ under the field** (the `MeshInstance3D` floor color or the Terrain3D
320
+ grass band) so roots melt into the terrain instead of floating on it.
321
+ - **Sun sheen** — fake-cylinder normals across each blade feed a cheap
322
+ Blinn-Phong vs the fixed `sunDirection`; tips glint as they sway. Match
323
+ it to the scene's key light direction.
324
+ - **Rolling wind** — 2-octave directional gusts (large rolling wave + slow
325
+ swell) plus per-blade flutter: waves visibly travel across the field.
326
+ - **Distance LOD** — blades shrink to zero between `fadeStart`..`fadeEnd`
327
+ meters from the camera, and HALF the blades (instance-hash) bow out by
328
+ the midpoint, halving far-field work. Far blades also widen slightly so
329
+ the carpet stays closed instead of aliasing away.
330
+
331
+ mesh + blades share the character bend: with `interaction: true` the
332
+ nearest ≤4 moving bodies (`CharacterBody3D` / `RigidBody3D`) push blade
333
+ tops radially away with a smooth falloff as a character wades through.
334
+
335
+ ```json
336
+ { "name": "Grass", "type": "Foliage3D",
337
+ "props": { "area": [30, 30], "density": 10, "seed": 7,
338
+ "groundColor": "#2e4a26" } }
339
+ ```
340
+
341
+ | prop | default | meaning |
342
+ |---|---|---|
343
+ | `kind` | `"grass"` | `grass` \| `flowers` \| `reeds` — blade silhouette (load-time check) |
344
+ | `style` | `"mesh"` | `mesh` (curved real blades) \| `tufts` (ez-tree textured cards — the dense 잔디밭) \| `blades` (SDF quads) \| `simple` (legacy quads) |
345
+ | `tuftStyle` | `"grass"` | tufts only: `grass` (dense blade-fan bake) \| `fern` (fewer, wider, arching fronds with leaflet notches — forest undergrowth) |
346
+ | `area` | `[20, 20]` | [x, z] extent in meters, centered on the node |
347
+ | `density` | `12` | instances per m² (mesh plants 8× — total capped at `maxInstances`) |
348
+ | `maxInstances` | `50000` | instance cap (hard ceiling 200000 — split bigger fields) |
349
+ | `colorA` | `"#4d7232"` | bottom/body color (mesh/blades) / lerp start (simple) — warm sunlit meadow green |
350
+ | `colorB` | `"#9aab55"` | tip color (mesh/blades) / lerp end (simple) |
351
+ | `groundColor` | `"#1f3015"` | mesh: soil tint at the blade ROOT — match the ground/terrain color |
352
+ | `sunDirection` | `[0.5, 0.8, 0.3]` | mesh: fixed sun for the specular sheen — match the key light (load-time check: non-zero [x,y,z]) |
353
+ | `fadeStart` | `60` | mesh LOD: camera distance where blades start shrinking, meters |
354
+ | `fadeEnd` | `90` | mesh LOD: gone by here (load-time check: > `fadeStart`) |
355
+ | `height` | `0.25` | base blade height, meters (mesh scales it 0.55–1.7× regionally) |
356
+ | `sway` | `0.6` | wind strength; 0 disables wind entirely |
357
+ | `interaction` | `true` | mesh/blades styles: moving bodies bend nearby grass |
358
+ | `coverage` | `0.85` | mesh: carpet fill in (0, 1] — below 1 noise carves dirt patches (load-time check) |
359
+ | `flowers` | `0` | DEPRECATED — confetti heads; use a `Flowers3D` node (load-time check 0–0.2) |
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) |
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).
369
+
370
+ ## Flowers3D
371
+
372
+ A first-class flower FIELD (in `incanto/3d`, registered by
373
+ `registerNodes3D`): instanced procedural flower PLANTS — a curved stem,
374
+ 2-3 leaf blades low on the stem, a 5-8 petal head cupped around a
375
+ contrasting center disc, 1-3 blooms per plant at staggered heights — not
376
+ billboard confetti. Deterministic from the `seed` PROP.
377
+
378
+ - **`density` is the vibe dial** — `'lush'` (풍성하게, 2.2 plants/m²),
379
+ `'sparse'` (듬성듬성, 0.35 — the default), `'none'` (없게, renders
380
+ nothing), or any number in plants/m². Load-time checks: valid
381
+ preset/number AND `area × density ≤ 10 000` plants (~1.3M tris budget).
382
+ - **Varieties** — `varieties` picks a unique subset of
383
+ `daisy` (8 flat petals, 3 blooms), `cosmos` (8 cupped petals, 2 tall
384
+ blooms), `bellflower` (5 petals in a deep nodding bell, 3 blooms);
385
+ `[]` (default) = all three. Each variety is ONE merged template geometry,
386
+ instanced per plant as a green-structure mesh + a petal mesh —
387
+ **≤3 varieties → ≤6 draw calls**.
388
+ - **Patches, not confetti** — placement gathers plants into jittered-Voronoi
389
+ patches (`clustering` 0 = uniform … 1 = tight patches, default 0.6), and
390
+ each patch blooms ONE variety in ONE palette color, like real meadows.
391
+ - **`palette`** — head colors as `'#rrggbb'` strings; `[]` (default) = the
392
+ reference white/yellow/violet trio. The center disc keeps its contrasting
393
+ per-variety color.
394
+ - **`height`** — plant height in meters (default 0.45, ±25% per-plant
395
+ jitter; petals scale along). Pick it ABOVE the surrounding grass canopy
396
+ (e.g. 0.6–0.7 over `height: 0.35` Foliage3D) so heads ride the grass.
397
+ - **`sway`** — gentle vertex-shader head bob, phase-hashed per plant; roots
398
+ never move. 0 holds still.
399
+ - Like Foliage3D the field is planar — on rolling terrain place small
400
+ patches at flat spots (the meadow/garden generators do this for you).
401
+
402
+ ```json
403
+ { "name": "Flowers", "type": "Flowers3D",
404
+ "props": { "area": [26, 26], "density": "sparse", "seed": 7,
405
+ "height": 0.7 } }
406
+ ```
407
+
408
+ | prop | default | meaning |
409
+ |---|---|---|
410
+ | `density` | `"sparse"` | `lush` \| `sparse` \| `none` \| number (plants/m²) — budget-checked at load |
411
+ | `area` | `[20, 20]` | [x, z] extent in meters, centered on the node |
412
+ | `seed` | `1` | placement seed — identical field across runs/machines |
413
+ | `varieties` | `[]` | unique subset of `daisy`/`cosmos`/`bellflower`; `[]` = all three (load-time check) |
414
+ | `palette` | `[]` | head colors, `'#rrggbb'` each; `[]` = white/yellow/violet (load-time check) |
415
+ | `height` | `0.45` | plant height, meters (±25% per-plant jitter) |
416
+ | `clustering` | `0.6` | 0 uniform … 1 tight Voronoi patches (load-time check) |
417
+ | `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 |
420
+
421
+ ## Tree3D
422
+ Procedural trees grown by the ported ez-tree generator (MIT, Dan Greenheck;
423
+ in `incanto/3d`, registered by `registerNodes3D`) — recursive gnarled
424
+ branches plus alpha-cutout leaf billboards sampling the packaged leaf
425
+ textures. One node is a whole grove in ≤6 draw calls: up to 3 seed VARIANTS
426
+ (differently-grown trees), each one branches-InstancedMesh + one
427
+ leaves-InstancedMesh, `count` instances scattered over `area` and dealt
428
+ round-robin among the variants. Construction is deterministic from the
429
+ `seed` PROP: identical grove on every machine. Per-instance
430
+ hue/scale/rotation jitter keeps a patch from reading as copies; `height`
431
+ jitters ±20% per tree; leaves ride a ported simplex-wind vertex sway.
432
+
433
+ **Crown shading (automatic):** leaf billboards do NOT light like flat
434
+ quads — the generator bakes crown-VOLUME normals (deciduous crowns shade as
435
+ an ellipsoid, conifers as a cone) plus per-leaf occlusion vertex colors
436
+ (interior leaves darken toward 0.45) and per-cluster hue/value jitter, so
437
+ canopies read as lit/shaded three-dimensional masses with a dark heart, not
438
+ paper cut-outs. The leaf cutout threshold relaxes with camera distance
439
+ (1.0× → 0.25× over 40–240 m) and edges ride MSAA alpha-to-coverage, so far
440
+ groves keep solid crowns instead of mip-ghosting away (~300 m). Leaf albedo
441
+ is calibrated for the atmosphere-sky + ACES stage; `canopyColor` still
442
+ tints on top.
443
+
444
+ **Bark** (medium/high tiers): trunks and branches sample the packaged
445
+ ez-tree BARK sets — color + normal + roughness (1k, ambientcg CC0 packaged
446
+ by ez-tree) — tiled over the generator's ring UVs with the upstream
447
+ per-preset repeat, so trunks show ridged bark up close instead of smooth
448
+ plastic. The mapping follows upstream: oak/ash → oak bark, aspen → birch,
449
+ pine → pine; `dead` trees stay barkless weathered gray. Default texture
450
+ source is the agent8 CDN (the only sanctioned external host, like leaves and
451
+ the terrain splat); the same JPGs also ship in `incanto/assets/vegetation/bark/`
452
+ for offline serving.
453
+
454
+ **Tiers are a quality/cost dial** (ported preset → tris per tree):
455
+
456
+ | tier | what grows | tris/tree |
457
+ |---|---|---|
458
+ | `simple` | the original primitive low-poly trees (cones/blobs) — cheapest, unchanged | ~0.1–0.5k |
459
+ | `medium` (default) | light `*-forest` presets — use for FORESTS (many trees) | ≤3k |
460
+ | `high` | full ez-tree presets (oak/ash rotation, airy pine whorls) — hero trees | 7–20k |
461
+
462
+ **Perf budget (load-time check):** `count × tris-per-tree ≤ 1 500 000` —
463
+ the error states the math and the max count for the tier. 500 medium trees
464
+ fit; `high` caps at ~75 trees per node (worst variant ≈ 20k). Geometry
465
+ grows lazily once (1–30 ms per variant), then scatter is matrix-only.
466
+ ```json
467
+ { "name": "Pines", "type": "Tree3D",
468
+ "props": { "type": "conifer", "tier": "medium",
469
+ "count": 24, "area": [40, 40], "seed": 11 } }
470
+ ```
471
+ | prop | default | meaning |
472
+ |---|---|---|
473
+ | `tier` | `"medium"` | `simple` \| `medium` \| `high` — see the dial above (load-time check) |
474
+ | `type` | `"conifer"` | `conifer` (pine whorls) \| `broadleaf` (oak/ash greens — autumn-gold aspen left out of the mix on purpose) \| `dead` (bare branches, no leaves) \| `bush` (ported bush_1 shrub — undergrowth; pair with height 2–4) |
475
+ | `seed` | `1` | construction + scatter seed |
476
+ | `height` | `6` | tree height, meters (±20% per-instance jitter) |
477
+ | `count` | `1` | instances; > 1 scatters a forest patch (max 500, budget-checked) |
478
+ | `area` | `[10, 10]` | [x, z] scatter extent when count > 1 |
479
+ | `trunkColor` | `"#7a5a3a"` | bark tint — the DEFAULT hands color to the upstream preset tint (the bark maps carry it); a custom value keeps its hue, max-channel-normalized so it tints the map instead of darkening it. Headless / `dead`: flat untextured tint |
480
+ | `canopyColor` | `"#4a7c3f"` | leaf tint — hue kept, value normalized over the leaf textures |
481
+
482
+ Every instance in a grove also gets a deterministic per-instance color jitter (±~20° hue, ±0.11 lightness off the base canopy/trunk color) so neighbouring trees read varied, not a flat wall of one green — the `terrain` generator stacks per-grove SPECIES colors on top (see above).
483
+ | `leafTexture` | `""` | leaf texture URL override; `""` = packaged per-type ez-tree texture (agent8 CDN — the only sanctioned external host; loads zero-setup). The same PNGs also ship in `incanto/assets/vegetation/` (`incanto-assets list`, kind `foliage`) — copy one next to your game and point here to serve offline |
484
+ | `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
+ | `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
+ | `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) |
489
+
490
+ > **Tree LOD / forest FPS.** Leaf overdraw + the leaf shadow pass dominate a
491
+ > dense-forest frame (~90% in profiling). For big groves set `leafFadeStart`/
492
+ > `leafFadeEnd` (e.g. `340`/`620` under fog far ~620) so far crowns shed their
493
+ > leaf cards (branch silhouette stays), and `leafShadows: false` to drop the leaf
494
+ > shadow pass. Both default off → existing scenes are unchanged.
495
+
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.
501
+
502
+ ## Terrain3D
503
+ Procedural heightfield terrain with biome texture splatting (in `incanto/3d`,
504
+ registered by `registerNodes3D`), ported from the agent8 starter terrain:
505
+ seeded simplex octaves displace a plane grid ONCE on the CPU, and a patched
506
+ MeshStandardMaterial blends up to 4 biome textures by height and slope
507
+ (per-vertex weights in vertex colors, world-UV sampling with fbm anti-tiling
508
+ jitter — mip-correct: every sample passes the un-jittered UV's gradients via
509
+ `textureGrad` — normal maps via a screen-space TBN). Same `seed` → identical
510
+ terrain on every machine. Within 25 m of the camera every active layer is
511
+ re-sampled at 10× repeat and added as zero-mean grain (colors AND normals,
512
+ plus a crevice micro-AO), fading out by 60 m — the ground resolves into crisp
513
+ grain up close instead of bilinear mush without shifting tone; textures load
514
+ with anisotropy 8 so grazing angles stay sharp into the distance. All
515
+ automatic, no props.
516
+ { "name": "Island", "type": "StaticBody3D",
517
+ "props": { "collider": { "shape": "heightfield" } },
518
+ "children": [
519
+ { "name": "Terrain", "type": "Terrain3D",
520
+ "props": { "size": [200, 200], "maxHeight": 6, "seed": 1, "theme": "island" } }
521
+ ] }
522
+ That snippet is the canonical PHYSICS recipe: the `heightfield` collider has
523
+ no params of its own — it pulls the height grid from its Terrain3D CHILD, so
524
+ visuals and collision can never drift apart. It is static-only (load-time
525
+ check) and hard-fails at physics start if the Terrain3D child is missing.
526
+ Skip the StaticBody3D wrapper when the terrain is scenery-only.
527
+ **Themes** preset the splat layers (and the island edge wrap) — load-time
528
+ check lists them on a typo:
529
+ | theme | layers (height × slope placement) | edge wrap |
530
+ | `island` | sand beach 0–0.12 · grass 0.1–0.7 · stone on slopes >45° · snow 0.8–1 | ON — borders curl down a quarter-circle to −50 m (sea floor) |
531
+ | `alpine` | grass valleys 0–0.45 · stone mid+high · gravel on slopes >36° · snow from 0.55 | off |
532
+ | `plains` | grass everywhere · dirt patches 0.35–0.65 · gravel on slopes >45° | off |
533
+ | `desert` | sand everywhere · gravel pans 0–0.15 · stone mesas on slopes >45° | off |
534
+ | `grassland` | the ez-tree demo GROUND set (mossy grass + dense crisp dirt, 30 m tiling): grass at EVERY height · dirt on slopes >45° AND in noise-carved flat patches (the demo's exact 100 m simplex / 0.7 patchiness shaping). With a custom `textureBase` it falls back to the `<base>/grass.png` + `dirt.png` contract | off |
535
+ | `forest` | grassland INVERTED — the LITTER floor: the ez-tree dark organic soil at every height/slope, moss-grass ONLY in noise-carved patches (55 m simplex, ~1/4 coverage). Custom `textureBase`: `<base>/dirt.png` + `grass.png` | off |
536
+ | `savanna` | dry GOLDEN plains: grass at every height · sand pans 0–0.22 · dirt on slopes >45° AND in noise-carved patches (the warm sun does the golden tinting) | off |
537
+ | `snow` | snow-dominant TUNDRA: snow at every height · stone (rock) on slopes >45° only — flat white field, distinct from alpine's banded peaks | off |
538
+ | `wetland` | SWAMP floor: mossy grass at every height · dark dirt (mud) on slopes >45° AND in frequent noise patches (the soft overcast light reads it humid) | off |
539
+ | `volcanic` | dark BASALT/ash: stone (basalt) at every height/slope · gravel (ash beds) pooling in the lows 0–0.28 | off |
540
+ | `custom` | YOUR `layers` (1–4 entries, load-time checked) | off |
541
+ Custom layers look like
542
+ `{ "texture": "url.png", "normalMap": "url_normal.png", "heightRange": [0, 0.3], "slopeRange": [0, 0.8], "repeat": 2, "roughness": 0.9 }`
543
+ — `heightRange` is normalized 0..1 over the realized height span, `slopeRange`
544
+ is radians of `atan(|∇h|)`. Default textures stream from the live agent8 CDN
545
+ (`textureBase` prop) — override it to self-host.
546
+ | `size` | `[200, 200]` | [width, depth] meters, centered on the node |
547
+ | `maxHeight` | `28` | height SCALE — realized heights land at ~3.5–8× this (the ported pipeline sums positive noise octaves); read real numbers off `heightAt` |
548
+ | `seed` | `1` | integer seed — deterministic terrain |
549
+ | `resolution` | `128` | grid segments per side (2–128, load-time check) |
550
+ | `theme` | `"island"` | see table — `custom` requires `layers` |
551
+ | `roughness` | `0.5` | detail-octave persistence (higher = more rugged) |
552
+ | `detail` | `4` | detail octave count |
553
+ | `flatThreshold` | `0.95` | snap-flatten heights within ±0.6 m of `maxHeight·flatThreshold` |
554
+ | `islandEdge` | `null` | `null` = theme decides; force the rim wrap on/off with a boolean |
555
+ | `layers` | `[]` | custom splat layers (theme `custom` only) |
556
+ | `textureBase` | agent8 CDN | base URL: `<base>/<name>.png` + `<base>/<name>_normal.png` |
557
+ | `basins` | `[]` | lake/pond bowls — `[{ x, z, radius, depth }]` (centered meters) carved into the surface (smooth, 0-slope rim); `heightAt`, the collider AND draped grass all see them |
558
+
559
+ **Carving a lake.** A flat Water3D plane on rolling/dome terrain reads as a wet
560
+ patch, not a lake. Carve a `basins` bowl so water fills a real depression with a
561
+ shoreline, then drop a `Water3D` at the floor + a little:
562
+ ```ts
563
+ // Terrain3D prop: a 9 m-radius, 3 m-deep bowl at the interior point (20, -15)
564
+ "basins": [{ "x": 20, "z": -15, "radius": 9, "depth": 3 }]
565
+ // then place water just above the carved floor (heightAt sees the bowl):
566
+ const floor = terrain.heightAt(20, -15); // ≈ surroundingGround - 3
567
+ pond.position = [20, floor + 1.2, -15]; // ~60% of the bank submerged
568
+ ```
569
+ Keep `radius`/`depth` gentle (depth/radius ≈ 0.3–0.5) so the bank stays walkable
570
+ and the shore splat reads. Interior ponds/lakes are cheapest as `Water3D
571
+ quality:'simple'` — the lake shader (fresnel sky tint + ripple normals + sun
572
+ glint + soft edge fade) with ZERO scene re-renders, vs fancy's ~2 full-scene
573
+ re-renders a frame. Give simple ponds a vivid water `color` (a muddy/dark hue
574
+ reads grey, since the surface IS mostly that color plus a sky sheen) and a
575
+ `sunDirection` matching the key light so the glint lands; its soft alpha edge
576
+ melts the rectangular plane into the shore (no hard rim poking out of the bowl).
577
+ **`heightAt(x, z)`** answers the surface height (world y) at WORLD x/z —
578
+ bilinear over the displaced grid, works headless and before any render.
579
+ Spawn points, prop placement, AI ground checks:
580
+ ```ts
581
+ const terrain = scene.root.getNode('Island/Terrain') as Terrain3D;
582
+ crate.position = [x, terrain.heightAt(x, z) + 0.5, z];
583
+ ```
584
+
585
+ Splat bands follow the surface the eye sees: the island rim's wrap shoulder
586
+ descends through the bands (snow → grass → sand) and every shoreline ends in
587
+ the bottom layer, so beaches happen automatically at the waterline.
588
+
589
+ **Island sea level is a TWO-sided constraint** — probe heights with
590
+ `heightAt` (or `buildHeightmap`) instead of guessing:
591
+
592
+ - the edge wrap only drops the rim 20 m before the vertical skirt to −50 m,
593
+ so the waterline must sit ABOVE `(highest border height − 20 m)` or bare
594
+ cliff walls with stretched stripes poke out of the sea;
595
+ - it should also sit inside the sand band (bottom 12% of the realized span)
596
+ so the visible shore reads as beach.
597
+
598
+ Both fit comfortably at `maxHeight ≈ 4–5` with `size: [200, 200]` (e.g.
599
+ seed 1 → surface 10.4–32 m, cliffs top out at 12.0, sea at 12.8 works).
600
+ Tall islands (`maxHeight ≥ 8` at that size) have rim heights no drownable
601
+ sea level can cover — scale height with care, the wrap radius does not grow.
602
+
603
+ `runGenerator('terrain', { seed, theme: 'island' })` does ALL of this math
604
+ for you: it probes the border with `buildHeightmap`, places the sea above
605
+ every rim cliff top and inside the sand band, and auto-fits `maxHeight`
606
+ down for seeds whose rim is too tall. Hand-author only when you need a
607
+ non-default composition.
608
+
609
+ ## Water3D
610
+
611
+ A commercial-quality water surface (in `incanto/3d`, registered by
612
+ `registerNodes3D`). The default `quality: 'fancy'` is a real water shader:
613
+ 9-iteration simplex-FBM wave displacement with analytic normals, THREE
614
+ animated ANISOTROPIC detail-normal layers — wind-stretched (~2.8× along the
615
+ wind), ridge-sharpened fine ripple in crossing directions, each layer
616
+ distance-faded at its own range so the far field goes glassy instead of
617
+ aliasing — sun glints as SPARSE micro-facet sparks (extreme shininess +
618
+ noise-gated + weighted toward the sun's azimuth path and grazing views;
619
+ the rest of the surface stays broad calm color fields), per-channel
620
+ Beer's-law depth `absorption` (red dies first — turquoise shallows fade
621
+ WIDE into deep teal-blue, like a real sea), CRYSTAL shallows (inside the
622
+ first ~1.5 m of water the refracted bottom dominates, ripple-distorted —
623
+ you see the sand through the surface), screen-space `refraction`
624
+ (submerged geometry shimmers SOFTLY through the surface — the offset reads
625
+ the smooth gloss normal, fades to zero at object waterlines, mip-blurs the
626
+ grab pass and blends instead of snapping at above-water silhouettes, so
627
+ contact lines stay clean with no comb-pattern streaks), always-on wispy
628
+ noise-broken shoreline `foam` (the foam band's depth range scales with the
629
+ water's extent: ponds get a contact kiss, open seas get whole shallow
630
+ channels), a trough/surface/peak color ramp (teal sea-blue defaults),
631
+ fresnel-mixed CubeCamera reflections re-rendered every `reflectionInterval`
632
+ ms and an edge-fade ring. Fresnel/reflection read a SMOOTH long-wavelength
633
+ normal (never the ridged ripple — that's what kept turning HDR sky
634
+ reflections into white sequins), HDR-capped per sample. `quality: 'simple'`
635
+ is the cheap LAKE shader — fresnel sky-tint reflection (a procedural
636
+ horizon→zenith gradient, NO cube map), animated procedural ripple normals
637
+ (`detailStrength`), a sun glint+sheen (`sunDirection`/`sunColor`/
638
+ `sunIntensity`), a depth-free view-angle body tint (clearer looking down,
639
+ deeper/more-reflective grazing) and a soft alpha edge fade that melts the
640
+ plane's rim into the shore. It does ZERO scene re-renders (no CubeCamera, no
641
+ depth/refraction pre-pass — the three things that drop fancy to ~30 fps at a
642
+ pond), so it's the low-end/perf path AND the deterministic headless path. A
643
+ lake reads as ONE hue derived from `color` (deep = darkened, surface =
644
+ lifted, sky = mostly real sky-blue), so give it a vivid color — a dark/muddy
645
+ hue reads grey.
646
+
647
+ ```json
648
+ { "name": "Water", "type": "Water3D",
649
+ "props": { "size": [40, 40], "position": [0, 3.2, 0] } }
650
+ ```
651
+
652
+ | prop | default | meaning |
653
+ |---|---|---|
654
+ | `size` | `[40, 40]` | [width, depth] meters — must be positive (load-time check) |
655
+ | `color` | `"#2a6fbe"` | THE lake hue for `simple` (deep/surface/sky derived from it — use a vivid color); if customized it also tints the fancy ramp |
656
+ | `opacity` | `0.8` | water-body density: lower = clearer (scales how much the refracted scene shows through) |
657
+ | `waveHeight` | `0.04` | wave intensity, meters-ish (0 = mirror-flat; 0.04 = calm sea; 0.08 = the older choppier look) |
658
+ | `waveSpeed` | `1` | animation speed multiplier |
659
+ | `quality` | `"fancy"` | `fancy` full shader water (CubeCamera + scene pre-pass) \| `simple` cheap lake shader, no scene re-renders (load-time check) |
660
+ | `colors` | `{}` | fancy ramp overrides: `{"trough", "surface", "peak"}` hex strings |
661
+ | `reflection` | `true` | live CubeCamera reflections (fancy only; needs a renderer) |
662
+ | `reflectionInterval` | `1000` | ms between CubeCamera reflection re-renders |
663
+ | `foam` | `true` | noise-broken shoreline foam band + crest foam near shores |
664
+ | `interaction` | `true` | character interaction: bodies (CharacterBody3D/RigidBody3D) inside the XZ footprint whose FEET dip below the surface count as in-water — so a WADING character (origin above water, feet below, sized from its collider) splashes, not only a fully-submerged one. Entry/exit emit `entered`/`exited` + a splash ripple; striding through water trails a WAKE (a ripple every ~0.45 m); floating bodies bob gently. Ripples drive both shaders' `uRipples[8]` (fancy) and CPU-displace the mesh (`simple`) |
665
+ | `splash` | `true` | built-in splash WHITEWATER (needs `interaction`): the shaders paint expanding **surface foam** where bodies interact — a **"풍덩"** froth bloom when a body plunges in (bigger the faster it falls) and a **foam trail ("물살")** behind a body wading/running through. Real frothy whitewater on the surface, not flying droplets; works on both `fancy` and `simple` quality. `false` keeps the ripples/waves but paints no foam. No wiring needed; for an extra custom burst add your own `Particles3D` on the `entered` signal |
666
+ | `sunDirection` | `[0.5, 0.8, 0.3]` | TOWARD the sun, for the specular glint — match the scene's key light (non-zero, load-time check) |
667
+ | `sunColor` | `"#fff5d6"` | glint/sheen tint |
668
+ | `sunIntensity` | `1` | glint/sheen strength (≥ 0; 0 = off) |
669
+ | `detailStrength` | `0.3` | animated detail-normal strength (≥ 0; 0 = vertex normals only) |
670
+ | `absorption` | `0.15` | Beer's-law constant per meter of water depth (≥ 0; lower = clearer/wider turquoise band; red absorbs ~3× faster than blue under the hood) |
671
+ | `refraction` | `true` | screen-space refraction of the submerged scene |
672
+ | `underwater` | `true` | submerged-camera look: ONLY when the camera EYE is below this surface (inside its XZ footprint) — wading with the camera above the water never tints the view — the scene switches to short-range underwater fog + tint, the sky is hidden, and animated **caustics** (depth-aware, projected onto the submerged floor/props) play. `false` disables it; an object `{ color?, visibility?, caustics? }` overrides the murk hue / view distance (m, default 22) / caustics. `caustics` is `true` (default) \| `false` \| `{ color?, intensity?, scale?, speed? }`. The murk color defaults to a darkened shade of `color`. Works for both qualities (the caustics composite runs only while the camera is genuinely underwater) |
673
+
674
+ Performance notes (fancy): beyond drawing the surface, each rendered frame
675
+ pays ONE half-resolution scene pre-pass (clamped at 1024px) that feeds the
676
+ depth texture (absorption + foam + soft intersections) AND the refraction
677
+ color grab; the CubeCamera reflection stays on its `reflectionInterval`
678
+ throttle. Measured on the generated island (sea 1600×1600 m, ~206k tris):
679
+ 60 fps, frame times indistinguishable from the pre-v2 water. Headless runs
680
+ never invoke any of it. Multiple fancy waters each pay their own pre-pass —
681
+ prefer one large surface over many small ones.
682
+
683
+ Two placement rules keep the surface artifact-free:
684
+
685
+ - **Waves dip below the waterline.** Troughs reach ≈6.5× `waveHeight` meters
686
+ below `position.y` (the default 0.04 ≈ 0.26 m). Float the waterline at
687
+ least that far above any ground/terrain it overlaps — otherwise the ground
688
+ pokes through wave troughs as dark bites in the surface.
689
+ - **Reflections mirror DISTANT content only.** The cube map renders from the
690
+ water's center with everything inside the footprint near-plane-culled (a
691
+ nearby mesh would smear across the whole surface as a giant false-color
692
+ patch). Shoreline, terrain, and sky reflect; a boat floating ON the water
693
+ intentionally does not — its submerged hull shows through `refraction`
694
+ instead.
695
+
696
+ ### Character interaction — `entered(body)` / `exited(body)`
697
+
698
+ With `interaction: true` the water scans the tree each frame for
699
+ `CharacterBody3D`/`RigidBody3D` inside its XZ footprint, compares their world
700
+ y against the waterline at their xz (wave offset included), and:
701
+
702
+ - emits `entered(body)` / `exited(body)` signals (the argument is the node),
703
+ - pushes a ripple impulse into a fixed 8-slot ring buffer that the shader
704
+ turns into expanding ring waves (faster falls splash harder; floating
705
+ bodies feed gentle bob rings on an interval),
706
+ - and (with `splash: true`, the default) paints expanding **surface FOAM** —
707
+ a **"풍덩"** froth bloom on entry whose size tracks the plunge speed, plus a
708
+ **foam trail ("물살")** behind a body wading/running through. This is real
709
+ whitewater on the water surface (driven by the same ripple buffer, in both
710
+ shaders), not flying droplets. Automatic; zero wiring. Set `splash: false`
711
+ to keep only the ripples/waves.
712
+
713
+ The surface has no bottom — anything below the waterline inside the
714
+ footprint counts as in-water. Keep using an `Area3D` for volumetric gameplay
715
+ (swim zones, drowning); the signals are for SPLASH moments. The built-in
716
+ `splash` particles already cover the common case — reach for a hand-wired
717
+ emitter only when you want a specific custom burst:
718
+
719
+ ```json
720
+ {
721
+ "root": { "name": "World", "type": "Node3D", "children": [
722
+ { "name": "Lake", "type": "Water3D",
723
+ "props": { "size": [60, 60], "position": [0, 2.8, 0] } },
724
+ { "name": "Splash", "type": "Particles3D",
725
+ "props": { "position": [0, 3, 0], "preset": "explosion", "emitting": false,
726
+ "rate": 0, "burst": 40, "colorStart": "#dff3ff", "colorEnd": "#9bd8ff" } }
727
+ ] },
728
+ "connections": [
729
+ { "signal": "entered", "from": "Lake", "to": "Splash", "handler": "replay" }
730
+ ]
731
+ }
732
+ ```
733
+
734
+ (`replay` re-arms the one-shot burst; a small behavior can also move the
735
+ emitter to the body first — the signal hands you the node:
736
+ `onEntered(body) { splash.position = body.position; splash.replay(); }`.)
737
+
738
+ ## Determinism, verified
739
+
740
+ `--seed` makes every run reproducible — keep the seed in your scene comments
741
+ or PROJECT docs. Regenerating with the same seed after an engine upgrade is
742
+ the cheap way to diff what changed; `bunx incanto-check` validates the scene
743
+ after every insert.