@call-me-sensei/toonlab 0.2.0 → 0.3.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 (205) hide show
  1. package/AGENTS.md +127 -4
  2. package/ATTRIBUTION.md +41 -3
  3. package/README.md +234 -34
  4. package/docs/characters.md +144 -0
  5. package/docs/debug-panel.md +126 -0
  6. package/docs/docs.css +589 -0
  7. package/docs/environment.md +186 -0
  8. package/docs/getting-started.md +180 -0
  9. package/docs/index.html +14 -0
  10. package/docs/lab-architecture.md +100 -0
  11. package/docs/lighting.md +825 -0
  12. package/docs/main.jsx +739 -0
  13. package/docs/mcp.md +97 -0
  14. package/docs/post-processing.md +98 -0
  15. package/docs/settings-reference.md +1878 -0
  16. package/docs/shader-constants.md +77 -0
  17. package/docs/sky.md +182 -0
  18. package/docs/style-labs.md +309 -0
  19. package/docs/texture-lab.md +135 -0
  20. package/docs/toon-shading.md +183 -0
  21. package/docs/tsl-conventions.md +167 -0
  22. package/docs/vegetation-sky.md +275 -0
  23. package/docs/water.md +430 -0
  24. package/docs/weather.md +200 -0
  25. package/docs/world-scale.md +111 -0
  26. package/mcp/server.mjs +610 -0
  27. package/mcp/style-lab-tools.mjs +371 -0
  28. package/mcp/vite-plugin.mjs +174 -0
  29. package/mcp/workspace.mjs +397 -0
  30. package/package.json +64 -5
  31. package/src/ambientfx/INTEGRATION.md +164 -0
  32. package/src/ambientfx/ambientFxPresets.js +83 -0
  33. package/src/ambientfx/ambientFxSettings.js +368 -0
  34. package/src/ambientfx/emitters.js +169 -0
  35. package/src/ambientfx/index.js +5 -0
  36. package/src/ambientfx/particleBackbone.js +493 -0
  37. package/src/ambientfx/stylizedAmbientFx.js +450 -0
  38. package/src/assetlib/ambientcg.js +117 -0
  39. package/src/assetlib/assetRef.js +91 -0
  40. package/src/assetlib/importedEntry.js +40 -0
  41. package/src/assetlib/index.js +23 -0
  42. package/src/assetlib/kaykit.js +192 -0
  43. package/src/assetlib/kaykitStaticIndex.js +700 -0
  44. package/src/assetlib/loadImported.js +143 -0
  45. package/src/assetlib/opensource3d.js +113 -0
  46. package/src/assetlib/polyhaven.js +182 -0
  47. package/src/assetlib/polypizza.js +115 -0
  48. package/src/assetlib/smithsonian.js +214 -0
  49. package/src/assetlib/sources.js +279 -0
  50. package/src/assetlib/zip.js +58 -0
  51. package/src/biome/biomeGenerator.js +385 -0
  52. package/src/biome/biomeRuntime.js +299 -0
  53. package/src/biome/index.js +2 -0
  54. package/src/buildinggen/buildingAsset.js +44 -0
  55. package/src/buildinggen/buildingGrammar.js +311 -0
  56. package/src/buildinggen/buildingMesh.js +451 -0
  57. package/src/buildinggen/buildingPresets.js +46 -0
  58. package/src/buildinggen/buildingRecipe.js +100 -0
  59. package/src/buildinggen/buildingSettings.js +238 -0
  60. package/src/buildinggen/index.js +6 -0
  61. package/src/camera/cameraDirector.js +157 -0
  62. package/src/camera/cameraGenerator.js +367 -0
  63. package/src/camera/cameraRig.js +570 -0
  64. package/src/camera/cameraSettings.js +236 -0
  65. package/src/camera/index.js +7 -0
  66. package/src/catalog/builtinEntries.js +245 -0
  67. package/src/catalog/catalog.js +210 -0
  68. package/src/catalog/index.js +3 -0
  69. package/src/catalog/manifest.js +84 -0
  70. package/src/core/generation.js +529 -0
  71. package/src/environment/environmentRigs.js +21 -1
  72. package/src/environment/environmentSettings.js +4 -0
  73. package/src/environment/environmentSunShadowPass.js +8 -0
  74. package/src/fauna/INTEGRATION.md +174 -0
  75. package/src/fauna/boids.js +861 -0
  76. package/src/fauna/faunaBodies.js +492 -0
  77. package/src/fauna/faunaPresets.js +52 -0
  78. package/src/fauna/faunaSettings.js +525 -0
  79. package/src/fauna/index.js +5 -0
  80. package/src/fauna/stylizedFauna.js +395 -0
  81. package/src/game-feel/gameFeelGenerator.js +402 -0
  82. package/src/game-feel/gameFeelRuntime.js +549 -0
  83. package/src/game-feel/index.js +2 -0
  84. package/src/index.js +17 -6
  85. package/src/lighting/colorIntensity.js +177 -0
  86. package/src/lighting/index.js +161 -0
  87. package/src/lighting/lightDescriptors.js +249 -0
  88. package/src/lighting/lightingCapabilities.js +79 -0
  89. package/src/lighting/lightingDocuments.js +247 -0
  90. package/src/lighting/lightingFixtures.js +446 -0
  91. package/src/lighting/lightingGenerator.js +449 -0
  92. package/src/lighting/lightingPresets.js +319 -0
  93. package/src/lighting/lightingRuntime.js +723 -0
  94. package/src/lighting/lightingStyle.js +386 -0
  95. package/src/lighting/lightingSystem.js +774 -0
  96. package/src/lighting/unrealExport.js +186 -0
  97. package/src/lighting/utils.js +87 -0
  98. package/src/motion/index.js +5 -0
  99. package/src/motion/motionClip.js +441 -0
  100. package/src/motion/motionController.js +628 -0
  101. package/src/motion/motionDocuments.js +225 -0
  102. package/src/motion/motionGraph.js +307 -0
  103. package/src/motion/motionSettings.js +222 -0
  104. package/src/pathgen/index.js +7 -0
  105. package/src/pathgen/pathBridge.js +232 -0
  106. package/src/pathgen/pathPresets.js +35 -0
  107. package/src/pathgen/pathRibbon.js +410 -0
  108. package/src/pathgen/pathRouter.js +380 -0
  109. package/src/pathgen/pathSettings.js +335 -0
  110. package/src/pathgen/pathTextures.js +123 -0
  111. package/src/pathgen/stylizedPaths.js +453 -0
  112. package/src/post/index.js +1 -0
  113. package/src/post/postGenerator.js +177 -0
  114. package/src/post/postProcessing.js +41 -0
  115. package/src/propgen/generatorsWave1.js +379 -0
  116. package/src/propgen/generatorsWave2.js +462 -0
  117. package/src/propgen/index.js +5 -0
  118. package/src/propgen/propAsset.js +323 -0
  119. package/src/propgen/propParts.js +170 -0
  120. package/src/propgen/propPlacement.js +459 -0
  121. package/src/propgen/propPresets.js +82 -0
  122. package/src/propgen/propSettings.js +395 -0
  123. package/src/shaders-tsl/chunks/projected-water-caustics.js +242 -0
  124. package/src/shaders-tsl/chunks/vegetation-style.js +360 -0
  125. package/src/shaders-tsl/chunks/water-shore-state.js +31 -0
  126. package/src/shaders-tsl/chunks/water-waves.js +90 -11
  127. package/src/shaders-tsl/environment.js +15 -1
  128. package/src/shaders-tsl/flower.js +279 -30
  129. package/src/shaders-tsl/grass.js +60 -33
  130. package/src/shaders-tsl/sky.js +125 -31
  131. package/src/shaders-tsl/tree-leaf.js +61 -25
  132. package/src/shaders-tsl/water-breaker.js +7 -4
  133. package/src/shaders-tsl/water-shore-state-simulation.js +523 -0
  134. package/src/shaders-tsl/water.js +439 -49
  135. package/src/shaders-tsl/woody-surface.js +154 -0
  136. package/src/sky/sceneOverrideLayers.js +10 -0
  137. package/src/sky/skyQuality.js +26 -0
  138. package/src/sky/stylizedSky.js +753 -45
  139. package/src/soundscape/index.js +4 -0
  140. package/src/soundscape/soundscapeGenerator.js +179 -0
  141. package/src/soundscape/soundscapeRuntime.js +806 -0
  142. package/src/soundscape/soundscapeSettings.js +292 -0
  143. package/src/styles/index.js +13 -0
  144. package/src/styles/styleBundle.js +325 -0
  145. package/src/stylizedTerrain.js +32 -2
  146. package/src/stylizedWorld.js +423 -20
  147. package/src/texgen/evaluateTexture.js +675 -0
  148. package/src/texgen/index.js +60 -0
  149. package/src/texgen/noise2.js +210 -0
  150. package/src/texgen/textureAi.js +436 -0
  151. package/src/texgen/textureGenerators.js +516 -0
  152. package/src/texgen/texturePresets.js +490 -0
  153. package/src/texgen/textureSettings.js +342 -0
  154. package/src/texgen/textureThree.js +59 -0
  155. package/src/vegetation/flowerSpecies.js +15 -3
  156. package/src/vegetation/grassPalettes.js +153 -0
  157. package/src/vegetation/index.js +6 -0
  158. package/src/vegetation/stylizedBush.js +2 -0
  159. package/src/vegetation/stylizedFlower.js +82 -0
  160. package/src/vegetation/stylizedFlowers.js +48 -7
  161. package/src/vegetation/stylizedForest.js +29 -1
  162. package/src/vegetation/stylizedGrass.js +291 -56
  163. package/src/vegetation/stylizedTree.js +38 -2
  164. package/src/vegetation/stylizedTreeFoliage.js +2 -1
  165. package/src/vegetation/vegetationShaders.js +1110 -0
  166. package/src/vfxgen/INTEGRATION.md +145 -0
  167. package/src/vfxgen/core/burstBackbone.js +380 -0
  168. package/src/vfxgen/core/projectileCore.js +92 -0
  169. package/src/vfxgen/core/spriteShapes.js +98 -0
  170. package/src/vfxgen/core/trailRibbon.js +272 -0
  171. package/src/vfxgen/core/vfxRandom.js +29 -0
  172. package/src/vfxgen/effects/emitHelpers.js +37 -0
  173. package/src/vfxgen/effects/magicEffects.js +162 -0
  174. package/src/vfxgen/effects/movementEffects.js +87 -0
  175. package/src/vfxgen/effects/weaponEffects.js +118 -0
  176. package/src/vfxgen/index.js +18 -0
  177. package/src/vfxgen/moves/moveController.js +146 -0
  178. package/src/vfxgen/moves/moveLibrary.js +335 -0
  179. package/src/vfxgen/vfxPresets.js +98 -0
  180. package/src/vfxgen/vfxSettings.js +384 -0
  181. package/src/vfxgen/vfxSystem.js +449 -0
  182. package/src/vfxgen/weapons/stylizedWeapons.js +137 -0
  183. package/src/villagegen/index.js +4 -0
  184. package/src/villagegen/stylizedVillage.js +490 -0
  185. package/src/villagegen/villageArchetypes.js +160 -0
  186. package/src/villagegen/villageNames.js +40 -0
  187. package/src/villagegen/villageSites.js +105 -0
  188. package/src/water/sceneOverrideLayers.js +23 -0
  189. package/src/water/water.js +5 -0
  190. package/src/water/waterBreakerSystem.js +15 -1
  191. package/src/water/waterCurrentField.js +447 -0
  192. package/src/water/waterMaterial.js +12 -0
  193. package/src/water/waterNearshorePhase.js +320 -0
  194. package/src/water/waterScenePasses.js +83 -30
  195. package/src/water/waterSettings.js +325 -13
  196. package/src/water/waterShoreMaterial.js +322 -0
  197. package/src/water/waterShoreStateField.js +605 -0
  198. package/src/water/waterSurface.js +797 -28
  199. package/src/weather/index.js +6 -0
  200. package/src/weather/weatherPrecipitation.js +221 -0
  201. package/src/weather/weatherPresets.js +258 -0
  202. package/src/weather/weatherSettings.js +269 -0
  203. package/src/weather/weatherSystem.js +871 -0
  204. package/src/worldMinimap.js +62 -0
  205. package/src/worldPresets.js +4 -1
@@ -0,0 +1,525 @@
1
+ // Canonical settings and field metadata for the ambient fauna layer.
2
+ // Follows the library-wide convention: DEFAULT_FAUNA_SETTINGS holds the
3
+ // grouped values, FAUNA_SETTING_GROUPS + FAUNA_SETTING_FIELD_SCHEMA drive the
4
+ // debug panel and the generated settings reference, and the recipe document
5
+ // ({ schema, version, seed, species, settings }) rebuilds the identical
6
+ // population forever. Populations are passed to createFauna({ species })
7
+ // directly — they are budgets, not look settings — so they live beside the
8
+ // settings as caps/defaults rather than inside them.
9
+
10
+ /** Species ids in canonical order (also the staggered-tick walk order). */
11
+ export const FAUNA_SPECIES = Object.freeze(['birds', 'butterflies', 'dragonflies', 'fish']);
12
+
13
+ /**
14
+ * Named palette ids per species. The color data lives in faunaBodies.js
15
+ * (keyed by these ids); settings only validate the id so the settings module
16
+ * stays dependency-free like the other *Settings modules.
17
+ */
18
+ export const FAUNA_PALETTE_IDS = Object.freeze({
19
+ birds: Object.freeze(['swallow', 'egret', 'finch']),
20
+ butterflies: Object.freeze(['meadow', 'twilight']),
21
+ dragonflies: Object.freeze(['pond', 'ember']),
22
+ fish: Object.freeze(['koi', 'silver']),
23
+ });
24
+
25
+ /** Default population per species when createFauna gets no explicit budget. */
26
+ export const DEFAULT_FAUNA_POPULATIONS = Object.freeze({
27
+ birds: 40,
28
+ butterflies: 60,
29
+ dragonflies: 12,
30
+ fish: 80,
31
+ });
32
+
33
+ // Hard caps: the steering tick budget and the per-frame matrix writes scale
34
+ // linearly with population, so a runaway host request degrades to the cap
35
+ // instead of the frame rate.
36
+ export const FAUNA_POPULATION_CAPS = Object.freeze({
37
+ birds: 160,
38
+ butterflies: 240,
39
+ dragonflies: 96,
40
+ fish: 320,
41
+ });
42
+
43
+ /** Clamps `{ birds, butterflies, ... }` counts to integers within the caps. */
44
+ export function normalizeFaunaPopulations(species = {}) {
45
+ const source = species && typeof species === 'object' ? species : {};
46
+ const result = {};
47
+ for (const name of FAUNA_SPECIES) {
48
+ const requested = source[name] === undefined
49
+ ? DEFAULT_FAUNA_POPULATIONS[name]
50
+ : Number(source[name]);
51
+ const count = Number.isFinite(requested) ? Math.round(requested) : DEFAULT_FAUNA_POPULATIONS[name];
52
+ result[name] = Math.min(FAUNA_POPULATION_CAPS[name], Math.max(0, count));
53
+ }
54
+ return result;
55
+ }
56
+
57
+ export const DEFAULT_FAUNA_SETTINGS = Object.freeze({
58
+ shared: Object.freeze({
59
+ tickShare: 0.25,
60
+ farDistance: 150,
61
+ }),
62
+ birds: Object.freeze({
63
+ altitudeMin: 7,
64
+ altitudeMax: 26,
65
+ cruiseSpeed: 7,
66
+ maxSpeed: 12,
67
+ neighborRadius: 14,
68
+ separationRadius: 2.6,
69
+ cohesion: 0.9,
70
+ alignment: 0.8,
71
+ separation: 1.3,
72
+ wander: 0.45,
73
+ fleeRadius: 12,
74
+ perchChance: 0.5,
75
+ perchDuration: 11,
76
+ flapHz: 3.4,
77
+ scale: 1,
78
+ palette: 'swallow',
79
+ }),
80
+ butterflies: Object.freeze({
81
+ hoverMin: 0.5,
82
+ hoverMax: 1.7,
83
+ speed: 1.3,
84
+ wanderRadius: 6,
85
+ fleeRadius: 3.5,
86
+ flapHz: 8.5,
87
+ scale: 1,
88
+ palette: 'meadow',
89
+ }),
90
+ dragonflies: Object.freeze({
91
+ hoverHeight: 0.6,
92
+ hoverRadius: 5,
93
+ dartSpeed: 7,
94
+ dartChance: 0.5,
95
+ flapHz: 36,
96
+ scale: 1,
97
+ palette: 'pond',
98
+ }),
99
+ fish: Object.freeze({
100
+ surfaceMargin: 0.3,
101
+ bedMargin: 0.35,
102
+ minSpawnDepth: 1.1,
103
+ cruiseSpeed: 1.5,
104
+ maxSpeed: 3.2,
105
+ neighborRadius: 4,
106
+ separationRadius: 0.8,
107
+ cohesion: 0.9,
108
+ alignment: 0.85,
109
+ separation: 1.1,
110
+ wander: 0.5,
111
+ fleeRadius: 7,
112
+ swayHz: 2.8,
113
+ scale: 1,
114
+ palette: 'koi',
115
+ }),
116
+ });
117
+
118
+ export const FAUNA_SETTING_GROUPS = Object.freeze([
119
+ Object.freeze({
120
+ id: 'shared',
121
+ label: 'Shared',
122
+ description: 'Cross-species simulation budgets: the staggered steering-tick share and the distance beyond which agents degrade to scripted loops.',
123
+ }),
124
+ Object.freeze({
125
+ id: 'birds',
126
+ label: 'Birds',
127
+ description: 'Flocking boids in a roaming altitude band; perch on registered points (or terrain) and flush when the follow target approaches.',
128
+ }),
129
+ Object.freeze({
130
+ id: 'butterflies',
131
+ label: 'Butterflies',
132
+ description: 'Individual noise-wanderers anchored to flower-mask points, hovering just above the terrain.',
133
+ }),
134
+ Object.freeze({
135
+ id: 'dragonflies',
136
+ label: 'Dragonflies',
137
+ description: 'Hover-and-dart flyers anchored to the water margin, holding a fixed height above the water surface.',
138
+ }),
139
+ Object.freeze({
140
+ id: 'fish',
141
+ label: 'Fish',
142
+ description: 'Schooling boids clamped between the water surface and the bed; visible from above through the water refraction pass.',
143
+ }),
144
+ ]);
145
+
146
+ const FIELD_DEFINITIONS = Object.freeze({
147
+ shared: Object.freeze({
148
+ tickShare: {
149
+ label: 'Tick Share',
150
+ description: 'Fraction of all agents that receive a full steering tick per update; the rest integrate their last velocity. 0.25 = every agent steers at ~15 Hz on a 60 Hz host.',
151
+ range: { min: 0.05, max: 0.5, step: 0.05 },
152
+ },
153
+ farDistance: {
154
+ label: 'Far Distance',
155
+ description: 'Meters from the follow target beyond which agents stop steering entirely and fly scripted circles (fish keep their depth clamps).',
156
+ range: { min: 40, max: 400, step: 10 },
157
+ },
158
+ }),
159
+ birds: Object.freeze({
160
+ altitudeMin: {
161
+ label: 'Altitude Min',
162
+ description: 'Bottom of the preferred flight band, meters above the local terrain.',
163
+ range: { min: 1, max: 40, step: 0.5 },
164
+ },
165
+ altitudeMax: {
166
+ label: 'Altitude Max',
167
+ description: 'Top of the preferred flight band, meters above the local terrain.',
168
+ range: { min: 2, max: 80, step: 0.5 },
169
+ },
170
+ cruiseSpeed: {
171
+ label: 'Cruise Speed',
172
+ description: 'Relaxed flight speed in m/s; flocks settle around it.',
173
+ range: { min: 1, max: 20, step: 0.5 },
174
+ },
175
+ maxSpeed: {
176
+ label: 'Max Speed',
177
+ description: 'Hard speed cap in m/s, reached when fleeing.',
178
+ range: { min: 2, max: 30, step: 0.5 },
179
+ },
180
+ neighborRadius: {
181
+ label: 'Neighbor Radius',
182
+ description: 'Meters within which flockmates influence cohesion and alignment.',
183
+ range: { min: 2, max: 30, step: 0.5 },
184
+ },
185
+ separationRadius: {
186
+ label: 'Separation Radius',
187
+ description: 'Personal-space radius in meters; closer neighbors are pushed away.',
188
+ range: { min: 0.5, max: 8, step: 0.1 },
189
+ },
190
+ cohesion: {
191
+ label: 'Cohesion',
192
+ description: 'Pull toward the local flock center — the flock-tightness knob.',
193
+ range: { min: 0, max: 2, step: 0.05 },
194
+ },
195
+ alignment: {
196
+ label: 'Alignment',
197
+ description: 'Pull toward the local average heading.',
198
+ range: { min: 0, max: 2, step: 0.05 },
199
+ },
200
+ separation: {
201
+ label: 'Separation',
202
+ description: 'Push away from neighbors inside the separation radius.',
203
+ range: { min: 0, max: 3, step: 0.05 },
204
+ },
205
+ wander: {
206
+ label: 'Wander',
207
+ description: 'Per-bird sinusoidal drift so flocks meander instead of orbiting.',
208
+ range: { min: 0, max: 2, step: 0.05 },
209
+ },
210
+ fleeRadius: {
211
+ label: 'Flee Radius',
212
+ description: 'Meters from the follow target at which flying birds scatter and perched birds flush.',
213
+ range: { min: 0, max: 40, step: 0.5 },
214
+ },
215
+ perchChance: {
216
+ label: 'Perch Chance',
217
+ description: 'Appetite for landing: expected perch attempts scale with this per ~10 s of flight.',
218
+ range: { min: 0, max: 1, step: 0.05 },
219
+ },
220
+ perchDuration: {
221
+ label: 'Perch Duration',
222
+ description: 'Mean seconds a bird stays perched (each stay jitters ±40%).',
223
+ range: { min: 2, max: 40, step: 1 },
224
+ },
225
+ flapHz: {
226
+ label: 'Flap Rate',
227
+ description: 'Wingbeats per second; the GPU flap phase/speed attributes derive from it. Birds glide (near-zero amplitude) when descending.',
228
+ range: { min: 0.5, max: 8, step: 0.1 },
229
+ },
230
+ scale: {
231
+ label: 'Scale',
232
+ description: 'Uniform body scale multiplier (±12% per-bird jitter on top).',
233
+ range: { min: 0.4, max: 2.5, step: 0.05 },
234
+ },
235
+ palette: {
236
+ label: 'Palette',
237
+ description: 'Named body palette; each palette carries 2–4 vertex-colored variants.',
238
+ type: 'select',
239
+ options: FAUNA_PALETTE_IDS.birds,
240
+ optionLabels: { swallow: 'Swallow (indigo/cream/rust)', egret: 'Egret (white/slate)', finch: 'Finch (gold/brown)' },
241
+ },
242
+ }),
243
+ butterflies: Object.freeze({
244
+ hoverMin: {
245
+ label: 'Hover Min',
246
+ description: 'Bottom of the flutter band, meters above the local terrain.',
247
+ range: { min: 0.1, max: 3, step: 0.05 },
248
+ },
249
+ hoverMax: {
250
+ label: 'Hover Max',
251
+ description: 'Top of the flutter band, meters above the local terrain.',
252
+ range: { min: 0.2, max: 5, step: 0.05 },
253
+ },
254
+ speed: {
255
+ label: 'Speed',
256
+ description: 'Typical flutter speed in m/s.',
257
+ range: { min: 0.2, max: 4, step: 0.05 },
258
+ },
259
+ wanderRadius: {
260
+ label: 'Wander Radius',
261
+ description: 'Meters a butterfly may drift from its flower-mask anchor before being pulled back.',
262
+ range: { min: 2, max: 30, step: 0.5 },
263
+ },
264
+ fleeRadius: {
265
+ label: 'Flee Radius',
266
+ description: 'Meters from the follow target at which butterflies scatter upward.',
267
+ range: { min: 0, max: 15, step: 0.25 },
268
+ },
269
+ flapHz: {
270
+ label: 'Flap Rate',
271
+ description: 'Wingbeats per second for the GPU wing fold.',
272
+ range: { min: 2, max: 16, step: 0.25 },
273
+ },
274
+ scale: {
275
+ label: 'Scale',
276
+ description: 'Uniform body scale multiplier (±20% per-agent jitter on top).',
277
+ range: { min: 0.4, max: 2.5, step: 0.05 },
278
+ },
279
+ palette: {
280
+ label: 'Palette',
281
+ description: 'Named wing palette; each palette carries up to 4 vertex-colored variants.',
282
+ type: 'select',
283
+ options: FAUNA_PALETTE_IDS.butterflies,
284
+ optionLabels: { meadow: 'Meadow (monarch/morpho/cabbage/sulphur)', twilight: 'Twilight (violet/teal/moth)' },
285
+ },
286
+ }),
287
+ dragonflies: Object.freeze({
288
+ hoverHeight: {
289
+ label: 'Hover Height',
290
+ description: 'Meters above the water surface dragonflies hold.',
291
+ range: { min: 0.2, max: 3, step: 0.05 },
292
+ },
293
+ hoverRadius: {
294
+ label: 'Hover Radius',
295
+ description: 'Meters of drift allowed around the current hover anchor.',
296
+ range: { min: 1, max: 20, step: 0.5 },
297
+ },
298
+ dartSpeed: {
299
+ label: 'Dart Speed',
300
+ description: 'Straight-line speed in m/s when relocating to a new anchor.',
301
+ range: { min: 1, max: 16, step: 0.5 },
302
+ },
303
+ dartChance: {
304
+ label: 'Dart Chance',
305
+ description: 'Appetite for relocating: expected darts scale with this per ~8 s of hovering.',
306
+ range: { min: 0, max: 1, step: 0.05 },
307
+ },
308
+ flapHz: {
309
+ label: 'Flap Rate',
310
+ description: 'Wing oscillations per second; high rates read as the classic wing shimmer.',
311
+ range: { min: 10, max: 60, step: 1 },
312
+ },
313
+ scale: {
314
+ label: 'Scale',
315
+ description: 'Uniform body scale multiplier.',
316
+ range: { min: 0.4, max: 2.5, step: 0.05 },
317
+ },
318
+ palette: {
319
+ label: 'Palette',
320
+ description: 'Named body palette; each palette carries 2–3 vertex-colored variants.',
321
+ type: 'select',
322
+ options: FAUNA_PALETTE_IDS.dragonflies,
323
+ optionLabels: { pond: 'Pond (crimson/cyan/jade)', ember: 'Ember (scarlet/amber)' },
324
+ },
325
+ }),
326
+ fish: Object.freeze({
327
+ surfaceMargin: {
328
+ label: 'Surface Margin',
329
+ description: 'Minimum meters a fish stays below the water surface (never breaches).',
330
+ range: { min: 0.1, max: 2, step: 0.05 },
331
+ },
332
+ bedMargin: {
333
+ label: 'Bed Margin',
334
+ description: 'Minimum meters a fish stays above the terrain bed.',
335
+ range: { min: 0.1, max: 2, step: 0.05 },
336
+ },
337
+ minSpawnDepth: {
338
+ label: 'Min Spawn Depth',
339
+ description: 'Meters of water column required for a fish spawn point; shallower bounds simply hold fewer fish.',
340
+ range: { min: 0.3, max: 5, step: 0.1 },
341
+ },
342
+ cruiseSpeed: {
343
+ label: 'Cruise Speed',
344
+ description: 'Relaxed swim speed in m/s.',
345
+ range: { min: 0.2, max: 5, step: 0.1 },
346
+ },
347
+ maxSpeed: {
348
+ label: 'Max Speed',
349
+ description: 'Hard speed cap in m/s, reached when fleeing.',
350
+ range: { min: 0.5, max: 8, step: 0.1 },
351
+ },
352
+ neighborRadius: {
353
+ label: 'Neighbor Radius',
354
+ description: 'Meters within which schoolmates influence cohesion and alignment.',
355
+ range: { min: 1, max: 12, step: 0.25 },
356
+ },
357
+ separationRadius: {
358
+ label: 'Separation Radius',
359
+ description: 'Personal-space radius in meters.',
360
+ range: { min: 0.2, max: 4, step: 0.05 },
361
+ },
362
+ cohesion: {
363
+ label: 'Cohesion',
364
+ description: 'Pull toward the local school center — schooling tightness.',
365
+ range: { min: 0, max: 2, step: 0.05 },
366
+ },
367
+ alignment: {
368
+ label: 'Alignment',
369
+ description: 'Pull toward the local average heading.',
370
+ range: { min: 0, max: 2, step: 0.05 },
371
+ },
372
+ separation: {
373
+ label: 'Separation',
374
+ description: 'Push away from neighbors inside the separation radius.',
375
+ range: { min: 0, max: 3, step: 0.05 },
376
+ },
377
+ wander: {
378
+ label: 'Wander',
379
+ description: 'Per-fish sinusoidal drift so schools roam the basin.',
380
+ range: { min: 0, max: 2, step: 0.05 },
381
+ },
382
+ fleeRadius: {
383
+ label: 'Flee Radius',
384
+ description: 'Meters from the follow target (a swimmer, a bridge walker) at which fish scatter.',
385
+ range: { min: 0, max: 25, step: 0.5 },
386
+ },
387
+ swayHz: {
388
+ label: 'Sway Rate',
389
+ description: 'Tail-sway cycles per second for the GPU body flex.',
390
+ range: { min: 0.5, max: 8, step: 0.1 },
391
+ },
392
+ scale: {
393
+ label: 'Scale',
394
+ description: 'Uniform body scale multiplier (±25% per-fish jitter on top).',
395
+ range: { min: 0.3, max: 3, step: 0.05 },
396
+ },
397
+ palette: {
398
+ label: 'Palette',
399
+ description: 'Named body palette: koi for ponds and lakes, silver for open water.',
400
+ type: 'select',
401
+ options: FAUNA_PALETTE_IDS.fish,
402
+ optionLabels: { koi: 'Koi (kohaku/gold/asagi)', silver: 'Silver shoal' },
403
+ },
404
+ }),
405
+ });
406
+
407
+ function createFaunaFieldMetadata(group, key, field) {
408
+ const defaultValue = DEFAULT_FAUNA_SETTINGS[group.id][key];
409
+ return Object.freeze({
410
+ defaultValue: Array.isArray(defaultValue) ? [...defaultValue] : defaultValue,
411
+ description: field.description,
412
+ group: group.id,
413
+ id: `${group.id}.${key}`,
414
+ key,
415
+ label: field.label,
416
+ optionLabels: field.optionLabels ?? null,
417
+ options: field.options ?? null,
418
+ range: field.range ?? null,
419
+ serializable: true,
420
+ type: field.type ?? (typeof defaultValue === 'boolean' ? 'boolean'
421
+ : typeof defaultValue === 'number' ? 'number' : 'text'),
422
+ });
423
+ }
424
+
425
+ export const FAUNA_SETTING_FIELD_SCHEMA = Object.freeze(
426
+ Object.fromEntries(
427
+ FAUNA_SETTING_GROUPS.map((group) => [
428
+ group.id,
429
+ Object.freeze(
430
+ Object.fromEntries(
431
+ Object.entries(FIELD_DEFINITIONS[group.id] ?? {})
432
+ .map(([key, field]) => [key, createFaunaFieldMetadata(group, key, field)]),
433
+ ),
434
+ ),
435
+ ]),
436
+ ),
437
+ );
438
+
439
+ function cloneValue(value) {
440
+ if (Array.isArray(value)) return value.map(cloneValue);
441
+ if (value && typeof value === 'object') return Object.fromEntries(
442
+ Object.entries(value).map(([key, entry]) => [key, cloneValue(entry)]),
443
+ );
444
+ return value;
445
+ }
446
+
447
+ export function cloneFaunaSettings(settings = DEFAULT_FAUNA_SETTINGS) {
448
+ return cloneValue(settings);
449
+ }
450
+
451
+ function clampNumber(value, fallback, min, max) {
452
+ const number = Number(value);
453
+ if (!Number.isFinite(number)) return fallback;
454
+ return Math.min(max, Math.max(min, number));
455
+ }
456
+
457
+ /** Normalizes partial grouped overrides over the defaults; unknown keys are dropped. */
458
+ export function createFaunaSettings(overrides = {}) {
459
+ const result = cloneFaunaSettings(DEFAULT_FAUNA_SETTINGS);
460
+ for (const groupId of Object.keys(result)) {
461
+ const group = overrides?.[groupId];
462
+ if (!group || typeof group !== 'object') continue;
463
+ for (const key of Object.keys(result[groupId])) {
464
+ if (group[key] === undefined) continue;
465
+ result[groupId][key] = cloneValue(group[key]);
466
+ }
467
+ }
468
+ // Clamp using each field's declared range so panel metadata and runtime
469
+ // agree, then repair the few cross-field invariants the schema can't state.
470
+ for (const group of FAUNA_SETTING_GROUPS) {
471
+ const fields = FAUNA_SETTING_FIELD_SCHEMA[group.id];
472
+ const values = result[group.id];
473
+ for (const [key, field] of Object.entries(fields)) {
474
+ if (field.type === 'number' && field.range) {
475
+ values[key] = clampNumber(values[key], field.defaultValue, field.range.min, field.range.max);
476
+ } else if (field.type === 'select') {
477
+ if (!field.options.includes(values[key])) values[key] = field.defaultValue;
478
+ }
479
+ }
480
+ }
481
+ const birds = result.birds;
482
+ if (birds.altitudeMax < birds.altitudeMin) birds.altitudeMax = birds.altitudeMin;
483
+ if (birds.maxSpeed < birds.cruiseSpeed) birds.maxSpeed = birds.cruiseSpeed;
484
+ const butterflies = result.butterflies;
485
+ if (butterflies.hoverMax < butterflies.hoverMin) butterflies.hoverMax = butterflies.hoverMin;
486
+ const fish = result.fish;
487
+ if (fish.maxSpeed < fish.cruiseSpeed) fish.maxSpeed = fish.cruiseSpeed;
488
+ if (fish.minSpawnDepth < fish.surfaceMargin + fish.bedMargin) {
489
+ fish.minSpawnDepth = fish.surfaceMargin + fish.bedMargin;
490
+ }
491
+ return result;
492
+ }
493
+
494
+ export const FAUNA_RECIPE_SCHEMA = 'faunaRecipe';
495
+ export const FAUNA_RECIPE_VERSION = 1;
496
+
497
+ /**
498
+ * Serializable recipe: everything needed to rebuild the identical population
499
+ * given the same terrain (`heightAt` itself is the host's, not serialized).
500
+ */
501
+ export function createFaunaRecipeDocument({ seed = 1, species = {}, settings = {} } = {}) {
502
+ return {
503
+ schema: FAUNA_RECIPE_SCHEMA,
504
+ seed: Math.round(Number(seed) || 1),
505
+ settings: createFaunaSettings(settings),
506
+ species: normalizeFaunaPopulations(species),
507
+ version: FAUNA_RECIPE_VERSION,
508
+ };
509
+ }
510
+
511
+ export function validateFaunaRecipeDocument(input) {
512
+ const errors = [];
513
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
514
+ return { errors: ['Recipe document must be an object.'], ok: false };
515
+ }
516
+ if (input.schema !== FAUNA_RECIPE_SCHEMA) errors.push(`schema must be "${FAUNA_RECIPE_SCHEMA}".`);
517
+ if (!Number.isInteger(input.version) || input.version < 1 || input.version > FAUNA_RECIPE_VERSION) {
518
+ errors.push(`version must be an integer between 1 and ${FAUNA_RECIPE_VERSION}.`);
519
+ }
520
+ if (!Number.isFinite(Number(input.seed))) errors.push('seed must be a number.');
521
+ if (input.species !== undefined && (typeof input.species !== 'object' || input.species === null)) {
522
+ errors.push('species must be an object when present.');
523
+ }
524
+ return { errors, ok: errors.length === 0 };
525
+ }
@@ -0,0 +1,5 @@
1
+ export * from './faunaSettings.js';
2
+ export * from './faunaBodies.js';
3
+ export { createFaunaSimulation, FAUNA_STATE } from './boids.js';
4
+ export * from './faunaPresets.js';
5
+ export * from './stylizedFauna.js';