@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,269 @@
1
+ // Canonical, serializable settings for the weather coordinator. Weather is
2
+ // deliberately not a shader: it drives the sky, light rig, fog, precipitation,
3
+ // water, vegetation, fauna, and ambient effects through their public setters.
4
+
5
+ export const WEATHER_PRECIPITATION_TYPES = Object.freeze([
6
+ 'none',
7
+ 'rain',
8
+ 'snow',
9
+ 'sleet',
10
+ 'hail',
11
+ 'dust',
12
+ ]);
13
+
14
+ export const DEFAULT_WEATHER_SETTINGS = Object.freeze({
15
+ atmosphere: Object.freeze({
16
+ ambientIntensity: 1,
17
+ cloudCoverage: 0.18,
18
+ cloudShadowCoverage: 0.35,
19
+ cloudShadowScale: 0.012,
20
+ cloudShadowStrength: 0.15,
21
+ cloudSpeed: 1,
22
+ fogColor: null,
23
+ fogRangeScale: 1,
24
+ skyDarkening: 0,
25
+ skyDesaturation: 0,
26
+ skyTint: Object.freeze([1, 1, 1]),
27
+ sunIntensity: 1,
28
+ sunTint: Object.freeze([1, 1, 1]),
29
+ }),
30
+ wind: Object.freeze({
31
+ direction: Object.freeze([1, 0.3]),
32
+ gustFrequency: 0.35,
33
+ gustSpeed: 1,
34
+ speed: 1,
35
+ strength: 0.16,
36
+ }),
37
+ precipitation: Object.freeze({
38
+ areaSize: 42,
39
+ color: Object.freeze([0.8, 0.88, 0.95]),
40
+ fallHeight: 24,
41
+ intensity: 0,
42
+ maxParticles: 8000,
43
+ opacity: 0.5,
44
+ size: 0.12,
45
+ speed: 16,
46
+ streakLength: 0.42,
47
+ type: 'none',
48
+ }),
49
+ lightning: Object.freeze({
50
+ color: Object.freeze([0.72, 0.82, 1]),
51
+ duration: 0.22,
52
+ enabled: false,
53
+ intensity: 4,
54
+ strikesPerMinute: 0,
55
+ }),
56
+ surface: Object.freeze({
57
+ ice: 0,
58
+ snowCover: 0,
59
+ waterRippleRate: 0,
60
+ waterWaveBoost: 0,
61
+ wetness: 0,
62
+ }),
63
+ });
64
+
65
+ export const WEATHER_SETTING_GROUPS = Object.freeze([
66
+ Object.freeze({ id: 'atmosphere', label: 'Atmosphere', description: 'Clouds, sunlight, ambient light, sky tone, and fog.' }),
67
+ Object.freeze({ id: 'wind', label: 'Wind', description: 'One wind field shared by precipitation, grass, flowers, trees, and ambient effects.' }),
68
+ Object.freeze({ id: 'precipitation', label: 'Precipitation', description: 'GPU-looping rain, snow, sleet, hail, or airborne dust in a camera-following volume.' }),
69
+ Object.freeze({ id: 'lightning', label: 'Lightning & Thunder', description: 'Seeded lightning flashes and thunder events for host audio.' }),
70
+ Object.freeze({ id: 'surface', label: 'Surface Response', description: 'Normalized outputs for water agitation, ripples, wetness, snow cover, and ice.' }),
71
+ ]);
72
+
73
+ const FIELDS = Object.freeze({
74
+ atmosphere: Object.freeze({
75
+ ambientIntensity: { label: 'Ambient Intensity', description: 'Multiplier for ambient and hemisphere lights.', range: { min: 0, max: 2, step: 0.01 } },
76
+ cloudCoverage: { label: 'Cloud Coverage', description: 'Fraction of the procedural sky occupied by clouds.', range: { min: 0, max: 1, step: 0.01 } },
77
+ cloudShadowCoverage: { label: 'Shadow Coverage', description: 'Coverage threshold for the shared cloud-shadow field.', range: { min: 0, max: 1, step: 0.01 } },
78
+ cloudShadowScale: { label: 'Shadow Scale', description: 'World-space noise scale of the cloud-shadow field.', range: { min: 0.001, max: 0.08, step: 0.001 } },
79
+ cloudShadowStrength: { label: 'Cloud Shadow', description: 'Strength shared by terrain, water, vegetation, and fauna.', range: { min: 0, max: 1, step: 0.01 } },
80
+ cloudSpeed: { label: 'Cloud Speed', description: 'Cloud animation speed multiplier.', range: { min: 0, max: 5, step: 0.01 } },
81
+ fogColor: { label: 'Fog Color', description: 'Weather fog color. Null preserves the scene baseline.', type: 'nullableColor' },
82
+ fogRangeScale: { label: 'Fog Density', description: 'Scales the scene fog range; larger values bring fog closer.', range: { min: 0.25, max: 8, step: 0.01 } },
83
+ skyDarkening: { label: 'Sky Darkening', description: 'Darkens the sky without changing its hue.', range: { min: 0, max: 0.9, step: 0.01 } },
84
+ skyDesaturation: { label: 'Sky Desaturation', description: 'Pulls sky and cloud colors toward gray.', range: { min: 0, max: 1, step: 0.01 } },
85
+ skyTint: { label: 'Sky Tint', description: 'RGB multiplier applied after the scene sky baseline.', type: 'color' },
86
+ sunIntensity: { label: 'Sun Intensity', description: 'Multiplier over the current time-of-day sun intensity.', range: { min: 0, max: 1.5, step: 0.01 } },
87
+ sunTint: { label: 'Sun Tint', description: 'RGB multiplier over the current sun color.', type: 'color' },
88
+ }),
89
+ wind: Object.freeze({
90
+ direction: { label: 'Direction', description: 'Horizontal XZ direction the wind blows toward.', type: 'vector2' },
91
+ gustFrequency: { label: 'Gust Frequency', description: 'Spatial frequency of vegetation gust bands.', range: { min: 0, max: 3, step: 0.01 } },
92
+ gustSpeed: { label: 'Gust Speed', description: 'Travel speed of vegetation gust bands.', range: { min: 0, max: 6, step: 0.01 } },
93
+ speed: { label: 'Wind Speed', description: 'Animation and precipitation advection speed.', range: { min: 0, max: 8, step: 0.01 } },
94
+ strength: { label: 'Wind Strength', description: 'Displacement strength shared by weather consumers.', range: { min: 0, max: 2, step: 0.01 } },
95
+ }),
96
+ precipitation: Object.freeze({
97
+ type: { label: 'Type', description: 'Rendered precipitation family.', type: 'select', options: WEATHER_PRECIPITATION_TYPES },
98
+ intensity: { label: 'Intensity', description: 'Draw-count and opacity multiplier.', range: { min: 0, max: 1, step: 0.01 } },
99
+ color: { label: 'Color', description: 'Precipitation color.', type: 'color' },
100
+ opacity: { label: 'Opacity', description: 'Maximum particle opacity.', range: { min: 0, max: 1, step: 0.01 } },
101
+ areaSize: { label: 'Follow Area', description: 'Width and depth of the precipitation volume in meters.', range: { min: 10, max: 140, step: 1 } },
102
+ fallHeight: { label: 'Fall Height', description: 'Height of the precipitation volume in meters.', range: { min: 4, max: 80, step: 1 } },
103
+ speed: { label: 'Fall Speed', description: 'Vertical fall speed in meters per second.', range: { min: 0.2, max: 40, step: 0.1 } },
104
+ size: { label: 'Particle Size', description: 'Snow, hail, sleet, and dust particle size.', range: { min: 0.02, max: 0.8, step: 0.01 } },
105
+ streakLength: { label: 'Streak Length', description: 'Rain and sleet streak length.', range: { min: 0.05, max: 1.5, step: 0.01 } },
106
+ maxParticles: { label: 'Particle Budget', description: 'Maximum instances allocated at construction.', range: { min: 500, max: 20000, step: 500 }, serializable: true },
107
+ }),
108
+ lightning: Object.freeze({
109
+ enabled: { label: 'Enabled', description: 'Allows seeded lightning strikes.', type: 'boolean' },
110
+ strikesPerMinute: { label: 'Strikes / Minute', description: 'Average strike rate with deterministic timing jitter.', range: { min: 0, max: 30, step: 0.1 } },
111
+ intensity: { label: 'Flash Intensity', description: 'Peak intensity of the camera-enclosing lightning flash.', range: { min: 0, max: 20, step: 0.1 } },
112
+ duration: { label: 'Flash Duration', description: 'Visible flash envelope in seconds.', range: { min: 0.05, max: 1, step: 0.01 } },
113
+ color: { label: 'Flash Color', description: 'Lightning flash color.', type: 'color' },
114
+ }),
115
+ surface: Object.freeze({
116
+ waterWaveBoost: { label: 'Water Wave Boost', description: 'Added to the water system’s baseline wave-intensity dial.', range: { min: 0, max: 0.75, step: 0.01 } },
117
+ waterRippleRate: { label: 'Rain Ripple Rate', description: 'Average water ripple impulses per second.', range: { min: 0, max: 20, step: 0.1 } },
118
+ wetness: { label: 'Wetness', description: 'Normalized host-facing wet-surface output.', range: { min: 0, max: 1, step: 0.01 } },
119
+ snowCover: { label: 'Snow Cover', description: 'Normalized host-facing snow accumulation target.', range: { min: 0, max: 1, step: 0.01 } },
120
+ ice: { label: 'Ice', description: 'Normalized host-facing ice accumulation target.', range: { min: 0, max: 1, step: 0.01 } },
121
+ }),
122
+ });
123
+
124
+ export const WEATHER_SETTING_FIELD_SCHEMA = Object.freeze(Object.fromEntries(
125
+ WEATHER_SETTING_GROUPS.map((group) => {
126
+ const fields = Object.fromEntries(
127
+ Object.entries(FIELDS[group.id]).map(([key, field]) => [key, Object.freeze({
128
+ defaultValue: DEFAULT_WEATHER_SETTINGS[group.id][key],
129
+ description: field.description,
130
+ group: group.id,
131
+ id: `${group.id}.${key}`,
132
+ key,
133
+ label: field.label,
134
+ options: field.options ?? null,
135
+ range: field.range ?? null,
136
+ serializable: field.serializable ?? true,
137
+ type: field.type ?? 'number',
138
+ })]),
139
+ );
140
+ return [group.id, Object.freeze(fields)];
141
+ }),
142
+ ));
143
+
144
+ const clamp = (value, min, max) => Math.min(Math.max(Number(value), min), max);
145
+ const finite = (value, fallback) => Number.isFinite(Number(value)) ? Number(value) : fallback;
146
+
147
+ function vector(value, fallback, size) {
148
+ if (!Array.isArray(value) || value.length < size) return [...fallback];
149
+ const next = value.slice(0, size).map(Number);
150
+ return next.every(Number.isFinite) ? next : [...fallback];
151
+ }
152
+
153
+ function groupSource(options, id) {
154
+ return options?.[id] && typeof options[id] === 'object' && !Array.isArray(options[id])
155
+ ? options[id]
156
+ : {};
157
+ }
158
+
159
+ /** Normalizes partial grouped settings into a complete, mutable plain object. */
160
+ export function createWeatherSettings(options = {}) {
161
+ const atmosphere = groupSource(options, 'atmosphere');
162
+ const wind = groupSource(options, 'wind');
163
+ const precipitation = groupSource(options, 'precipitation');
164
+ const lightning = groupSource(options, 'lightning');
165
+ const surface = groupSource(options, 'surface');
166
+ const da = DEFAULT_WEATHER_SETTINGS.atmosphere;
167
+ const dw = DEFAULT_WEATHER_SETTINGS.wind;
168
+ const dp = DEFAULT_WEATHER_SETTINGS.precipitation;
169
+ const dl = DEFAULT_WEATHER_SETTINGS.lightning;
170
+ const ds = DEFAULT_WEATHER_SETTINGS.surface;
171
+ const fogColor = atmosphere.fogColor === null || atmosphere.fogColor === undefined
172
+ ? null
173
+ : vector(atmosphere.fogColor, [0.72, 0.83, 0.94], 3);
174
+ const precipitationType = WEATHER_PRECIPITATION_TYPES.includes(precipitation.type)
175
+ ? precipitation.type
176
+ : dp.type;
177
+
178
+ return {
179
+ atmosphere: {
180
+ ambientIntensity: clamp(finite(atmosphere.ambientIntensity, da.ambientIntensity), 0, 2),
181
+ cloudCoverage: clamp(finite(atmosphere.cloudCoverage, da.cloudCoverage), 0, 1),
182
+ cloudShadowCoverage: clamp(finite(atmosphere.cloudShadowCoverage, da.cloudShadowCoverage), 0, 1),
183
+ cloudShadowScale: clamp(finite(atmosphere.cloudShadowScale, da.cloudShadowScale), 0.0001, 0.2),
184
+ cloudShadowStrength: clamp(finite(atmosphere.cloudShadowStrength, da.cloudShadowStrength), 0, 1),
185
+ cloudSpeed: clamp(finite(atmosphere.cloudSpeed, da.cloudSpeed), 0, 10),
186
+ fogColor,
187
+ fogRangeScale: clamp(finite(atmosphere.fogRangeScale, da.fogRangeScale), 0.1, 20),
188
+ skyDarkening: clamp(finite(atmosphere.skyDarkening, da.skyDarkening), 0, 0.95),
189
+ skyDesaturation: clamp(finite(atmosphere.skyDesaturation, da.skyDesaturation), 0, 1),
190
+ skyTint: vector(atmosphere.skyTint, da.skyTint, 3),
191
+ sunIntensity: clamp(finite(atmosphere.sunIntensity, da.sunIntensity), 0, 2),
192
+ sunTint: vector(atmosphere.sunTint, da.sunTint, 3),
193
+ },
194
+ wind: {
195
+ direction: vector(wind.direction, dw.direction, 2),
196
+ gustFrequency: clamp(finite(wind.gustFrequency, dw.gustFrequency), 0, 5),
197
+ gustSpeed: clamp(finite(wind.gustSpeed, dw.gustSpeed), 0, 10),
198
+ speed: clamp(finite(wind.speed, dw.speed), 0, 12),
199
+ strength: clamp(finite(wind.strength, dw.strength), 0, 3),
200
+ },
201
+ precipitation: {
202
+ areaSize: clamp(finite(precipitation.areaSize, dp.areaSize), 4, 300),
203
+ color: vector(precipitation.color, dp.color, 3),
204
+ fallHeight: clamp(finite(precipitation.fallHeight, dp.fallHeight), 2, 160),
205
+ intensity: clamp(finite(precipitation.intensity, dp.intensity), 0, 1),
206
+ maxParticles: Math.round(clamp(finite(precipitation.maxParticles, dp.maxParticles), 100, 40000)),
207
+ opacity: clamp(finite(precipitation.opacity, dp.opacity), 0, 1),
208
+ size: clamp(finite(precipitation.size, dp.size), 0.005, 2),
209
+ speed: clamp(finite(precipitation.speed, dp.speed), 0.05, 80),
210
+ streakLength: clamp(finite(precipitation.streakLength, dp.streakLength), 0.01, 4),
211
+ type: precipitationType,
212
+ },
213
+ lightning: {
214
+ color: vector(lightning.color, dl.color, 3),
215
+ duration: clamp(finite(lightning.duration, dl.duration), 0.02, 2),
216
+ enabled: lightning.enabled === undefined ? dl.enabled : Boolean(lightning.enabled),
217
+ intensity: clamp(finite(lightning.intensity, dl.intensity), 0, 40),
218
+ strikesPerMinute: clamp(finite(lightning.strikesPerMinute, dl.strikesPerMinute), 0, 60),
219
+ },
220
+ surface: {
221
+ ice: clamp(finite(surface.ice, ds.ice), 0, 1),
222
+ snowCover: clamp(finite(surface.snowCover, ds.snowCover), 0, 1),
223
+ waterRippleRate: clamp(finite(surface.waterRippleRate, ds.waterRippleRate), 0, 60),
224
+ waterWaveBoost: clamp(finite(surface.waterWaveBoost, ds.waterWaveBoost), 0, 1),
225
+ wetness: clamp(finite(surface.wetness, ds.wetness), 0, 1),
226
+ },
227
+ };
228
+ }
229
+
230
+ /** Deep-merges grouped overrides, then normalizes the result. */
231
+ export function mergeWeatherSettings(base, overrides = {}) {
232
+ const source = createWeatherSettings(base);
233
+ const merged = {};
234
+ for (const group of WEATHER_SETTING_GROUPS) {
235
+ merged[group.id] = {
236
+ ...source[group.id],
237
+ ...groupSource(overrides, group.id),
238
+ };
239
+ }
240
+ return createWeatherSettings(merged);
241
+ }
242
+
243
+ function interpolateValue(a, b, t) {
244
+ if (Array.isArray(a) && Array.isArray(b)) {
245
+ return a.map((value, index) => value + ((b[index] ?? value) - value) * t);
246
+ }
247
+ if (typeof a === 'number' && typeof b === 'number') return a + (b - a) * t;
248
+ return t < 0.5 ? a : b;
249
+ }
250
+
251
+ /** Interpolates weather settings for smooth preset transitions. */
252
+ export function interpolateWeatherSettings(from, to, amount) {
253
+ const a = createWeatherSettings(from);
254
+ const b = createWeatherSettings(to);
255
+ const t = clamp(finite(amount, 0), 0, 1);
256
+ const output = {};
257
+ for (const group of WEATHER_SETTING_GROUPS) {
258
+ output[group.id] = {};
259
+ for (const key of Object.keys(a[group.id])) {
260
+ output[group.id][key] = interpolateValue(a[group.id][key], b[group.id][key], t);
261
+ }
262
+ }
263
+ // Null fog colors are baseline-preserving, so they cannot be numerically
264
+ // interpolated. Switch only after the transition midpoint.
265
+ if (a.atmosphere.fogColor === null || b.atmosphere.fogColor === null) {
266
+ output.atmosphere.fogColor = t < 0.5 ? a.atmosphere.fogColor : b.atmosphere.fogColor;
267
+ }
268
+ return createWeatherSettings(output);
269
+ }