@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,380 @@
1
+ // Cost-field routing for path networks. Pure math — no THREE imports — so
2
+ // the router is trivially testable and runs anywhere (workers, Node).
3
+ //
4
+ // The whole contract is `heightAt(x, z)` + `waterLevel`: slope is expensive,
5
+ // water is very expensive (but crossable, which is exactly where bridges
6
+ // appear), and cells an earlier route already uses are discounted so later
7
+ // routes fork off the existing network instead of cutting parallel tracks.
8
+
9
+ function mulberry32(seed) {
10
+ let state = (Math.trunc(seed) || 1) >>> 0;
11
+ return () => {
12
+ state = (state + 0x6d2b79f5) >>> 0;
13
+ let t = state;
14
+ t = Math.imul(t ^ (t >>> 15), t | 1);
15
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
16
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
17
+ };
18
+ }
19
+
20
+ // Binary min-heap keyed on `score` — A*'s open set. Plain arrays beat
21
+ // fancier structures at the few-thousand-node sizes a routing grid hits.
22
+ class MinHeap {
23
+ constructor() { this.items = []; }
24
+ get size() { return this.items.length; }
25
+ push(node, score) {
26
+ const items = this.items;
27
+ items.push({ node, score });
28
+ let index = items.length - 1;
29
+ while (index > 0) {
30
+ const parent = (index - 1) >> 1;
31
+ if (items[parent].score <= items[index].score) break;
32
+ [items[parent], items[index]] = [items[index], items[parent]];
33
+ index = parent;
34
+ }
35
+ }
36
+ pop() {
37
+ const items = this.items;
38
+ const top = items[0];
39
+ const last = items.pop();
40
+ if (items.length > 0) {
41
+ items[0] = last;
42
+ let index = 0;
43
+ for (;;) {
44
+ const left = index * 2 + 1;
45
+ const right = left + 1;
46
+ let smallest = index;
47
+ if (left < items.length && items[left].score < items[smallest].score) smallest = left;
48
+ if (right < items.length && items[right].score < items[smallest].score) smallest = right;
49
+ if (smallest === index) break;
50
+ [items[smallest], items[index]] = [items[index], items[smallest]];
51
+ index = smallest;
52
+ }
53
+ }
54
+ return top?.node;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Builds the routing grid: per-cell height, wetness, and a `used` layer the
60
+ * router marks as routes land (feeding the reuse discount).
61
+ */
62
+ export function createRoutingGrid({
63
+ heightAt,
64
+ waterLevel = 0,
65
+ size = { x: 1000, z: 1000 },
66
+ gridStep = 8,
67
+ shoreMargin = 0.6,
68
+ } = {}) {
69
+ const width = Number(size?.width ?? size?.x ?? size) || 1000;
70
+ const depth = Number(size?.depth ?? size?.z ?? size) || 1000;
71
+ const step = Math.max(Number(gridStep) || 8, 2);
72
+ const cols = Math.max(Math.round(width / step) + 1, 4);
73
+ const rows = Math.max(Math.round(depth / step) + 1, 4);
74
+ const originX = -width / 2;
75
+ const originZ = -depth / 2;
76
+ const sample = typeof heightAt === 'function' ? heightAt : () => 0;
77
+
78
+ const heights = new Float32Array(cols * rows);
79
+ const wet = new Uint8Array(cols * rows);
80
+ const used = new Uint8Array(cols * rows);
81
+ const threshold = waterLevel + shoreMargin;
82
+ for (let iz = 0; iz < rows; iz += 1) {
83
+ for (let ix = 0; ix < cols; ix += 1) {
84
+ const index = iz * cols + ix;
85
+ const y = Number(sample(originX + ix * step, originZ + iz * step)) || 0;
86
+ heights[index] = y;
87
+ wet[index] = y <= threshold ? 1 : 0;
88
+ }
89
+ }
90
+
91
+ return {
92
+ cols,
93
+ rows,
94
+ step,
95
+ originX,
96
+ originZ,
97
+ heights,
98
+ wet,
99
+ used,
100
+ waterLevel,
101
+ indexOf: (ix, iz) => iz * cols + ix,
102
+ worldOf: (ix, iz) => ({ x: originX + ix * step, z: originZ + iz * step }),
103
+ cellOf: (x, z) => ({
104
+ ix: Math.min(cols - 1, Math.max(0, Math.round((x - originX) / step))),
105
+ iz: Math.min(rows - 1, Math.max(0, Math.round((z - originZ) / step))),
106
+ }),
107
+ };
108
+ }
109
+
110
+ const NEIGHBORS = [
111
+ [1, 0, 1], [-1, 0, 1], [0, 1, 1], [0, -1, 1],
112
+ [1, 1, Math.SQRT2], [1, -1, Math.SQRT2], [-1, 1, Math.SQRT2], [-1, -1, Math.SQRT2],
113
+ ];
114
+
115
+ /**
116
+ * A* over the routing grid. Returns grid-cell waypoints (world coordinates)
117
+ * or null when no route exists. Slope enters quadratically — a 2x steeper
118
+ * climb hurts 4x — which is what bends routes into contour-hugging arcs and
119
+ * switchbacks instead of straight assaults.
120
+ */
121
+ export function routeBetween(grid, from, to, {
122
+ slopeCost = 26,
123
+ waterCost = 14,
124
+ reuseBonus = 0.45,
125
+ } = {}) {
126
+ const start = grid.cellOf(from.x ?? from[0], from.z ?? from[1]);
127
+ const goal = grid.cellOf(to.x ?? to[0], to.z ?? to[1]);
128
+ const startIndex = grid.indexOf(start.ix, start.iz);
129
+ const goalIndex = grid.indexOf(goal.ix, goal.iz);
130
+ if (startIndex === goalIndex) return null;
131
+
132
+ const { cols, rows, step, heights, wet, used } = grid;
133
+ const gScore = new Float64Array(cols * rows).fill(Infinity);
134
+ const cameFrom = new Int32Array(cols * rows).fill(-1);
135
+ const closed = new Uint8Array(cols * rows);
136
+ gScore[startIndex] = 0;
137
+
138
+ const heuristic = (ix, iz) => {
139
+ const dx = Math.abs(ix - goal.ix);
140
+ const dz = Math.abs(iz - goal.iz);
141
+ // Octile distance in meters — admissible because flat dry reused ground
142
+ // can never cost less than distance * (1 - reuseBonus).
143
+ return (Math.max(dx, dz) + (Math.SQRT2 - 1) * Math.min(dx, dz)) * step * (1 - reuseBonus);
144
+ };
145
+
146
+ const open = new MinHeap();
147
+ open.push(startIndex, heuristic(start.ix, start.iz));
148
+
149
+ const reuse = Math.min(Math.max(reuseBonus, 0), 0.9);
150
+ let found = false;
151
+ while (open.size > 0) {
152
+ const current = open.pop();
153
+ if (current === goalIndex) { found = true; break; }
154
+ if (closed[current]) continue;
155
+ closed[current] = 1;
156
+ const cx = current % cols;
157
+ const cz = (current / cols) | 0;
158
+ for (const [dx, dz, factor] of NEIGHBORS) {
159
+ const nx = cx + dx;
160
+ const nz = cz + dz;
161
+ if (nx < 0 || nz < 0 || nx >= cols || nz >= rows) continue;
162
+ const next = nz * cols + nx;
163
+ if (closed[next]) continue;
164
+ const run = factor * step;
165
+ const rise = Math.abs(heights[next] - heights[current]);
166
+ const slope = rise / run;
167
+ let cost = run * (1 + slopeCost * slope * slope);
168
+ if (wet[next]) cost *= Math.max(waterCost, 1);
169
+ if (used[next]) cost *= (1 - reuse);
170
+ const tentative = gScore[current] + cost;
171
+ if (tentative >= gScore[next]) continue;
172
+ gScore[next] = tentative;
173
+ cameFrom[next] = current;
174
+ open.push(next, tentative + heuristic(nx, nz));
175
+ }
176
+ }
177
+ if (!found) return null;
178
+
179
+ const cells = [];
180
+ for (let index = goalIndex; index !== -1; index = cameFrom[index]) {
181
+ cells.push(index);
182
+ if (index === startIndex) break;
183
+ }
184
+ cells.reverse();
185
+ return cells.map((index) => {
186
+ const ix = index % cols;
187
+ const iz = (index / cols) | 0;
188
+ const { x, z } = grid.worldOf(ix, iz);
189
+ return { x, z, wet: wet[index] === 1 };
190
+ });
191
+ }
192
+
193
+ /** Marks a routed polyline into the grid's `used` layer (reuse discount). */
194
+ export function markRouteUsed(grid, waypoints, radiusCells = 1) {
195
+ for (const point of waypoints) {
196
+ const { ix, iz } = grid.cellOf(point.x, point.z);
197
+ for (let dz = -radiusCells; dz <= radiusCells; dz += 1) {
198
+ for (let dx = -radiusCells; dx <= radiusCells; dx += 1) {
199
+ const nx = ix + dx;
200
+ const nz = iz + dz;
201
+ if (nx < 0 || nz < 0 || nx >= grid.cols || nz >= grid.rows) continue;
202
+ grid.used[grid.indexOf(nx, nz)] = 1;
203
+ }
204
+ }
205
+ }
206
+ }
207
+
208
+ /** Drops collinear middle points, then Chaikin-smooths the polyline. */
209
+ export function smoothWaypoints(waypoints, { iterations = 2 } = {}) {
210
+ if (!Array.isArray(waypoints) || waypoints.length < 3) return waypoints ?? [];
211
+ let points = waypoints.filter((point, index) => {
212
+ if (index === 0 || index === waypoints.length - 1) return true;
213
+ const prev = waypoints[index - 1];
214
+ const next = waypoints[index + 1];
215
+ const cross = (point.x - prev.x) * (next.z - prev.z) - (point.z - prev.z) * (next.x - prev.x);
216
+ return Math.abs(cross) > 1e-6;
217
+ });
218
+ for (let pass = 0; pass < iterations; pass += 1) {
219
+ const smoothed = [points[0]];
220
+ for (let index = 0; index < points.length - 1; index += 1) {
221
+ const a = points[index];
222
+ const b = points[index + 1];
223
+ smoothed.push(
224
+ { x: a.x * 0.75 + b.x * 0.25, z: a.z * 0.75 + b.z * 0.25 },
225
+ { x: a.x * 0.25 + b.x * 0.75, z: a.z * 0.25 + b.z * 0.75 },
226
+ );
227
+ }
228
+ smoothed.push(points[points.length - 1]);
229
+ points = smoothed;
230
+ }
231
+ return points;
232
+ }
233
+
234
+ /**
235
+ * Low-frequency perpendicular wander over a smoothed polyline — the
236
+ * hand-drawn read. Routed corridors are already safe, and the amplitude is
237
+ * around a meter, so the wander never leaves them.
238
+ */
239
+ export function addCenterlineWander(points, { seed = 1, amplitude = 1.4, wavelength = 26 } = {}) {
240
+ if (!Array.isArray(points) || points.length < 3 || amplitude <= 0) return points ?? [];
241
+ const random = mulberry32(seed * 69069 + 5);
242
+ const phase = random() * 100;
243
+ let s = 0;
244
+ const result = [points[0]];
245
+ for (let index = 1; index < points.length - 1; index += 1) {
246
+ const prev = points[index - 1];
247
+ const point = points[index];
248
+ const next = points[index + 1];
249
+ s += Math.hypot(point.x - prev.x, point.z - prev.z);
250
+ const tx = next.x - prev.x;
251
+ const tz = next.z - prev.z;
252
+ const invLen = 1 / Math.max(Math.hypot(tx, tz), 1e-6);
253
+ // two octaves of sine wander with seeded phases — cheap, smooth, tileable
254
+ const w = Math.sin((s / wavelength + phase) * Math.PI * 2) * 0.7
255
+ + Math.sin((s / (wavelength * 0.37) + phase * 1.7) * Math.PI * 2) * 0.3;
256
+ result.push({
257
+ x: point.x + (-tz * invLen) * w * amplitude,
258
+ z: point.z + (tx * invLen) * w * amplitude,
259
+ });
260
+ }
261
+ result.push(points[points.length - 1]);
262
+ return result;
263
+ }
264
+
265
+ /**
266
+ * Auto mode: probes `count` walkable, mutually distant points of interest.
267
+ * Deterministic per seed. POIs prefer locally flat, dry, open ground with a
268
+ * mild affinity for shorelines — the same instinct a settlement has (and
269
+ * shoreline destinations are what make routes earn their bridges).
270
+ */
271
+ export function pickPointsOfInterest({
272
+ heightAt,
273
+ waterLevel = 0,
274
+ size = { x: 1000, z: 1000 },
275
+ count = 4,
276
+ seed = 1,
277
+ shoreMargin = 0.6,
278
+ margin = 0.12,
279
+ } = {}) {
280
+ const width = Number(size?.width ?? size?.x ?? size) || 1000;
281
+ const depth = Number(size?.depth ?? size?.z ?? size) || 1000;
282
+ const random = mulberry32(seed * 2654435761 + 97);
283
+ const sample = typeof heightAt === 'function' ? heightAt : () => 0;
284
+ const innerX = width * (0.5 - margin);
285
+ const innerZ = depth * (0.5 - margin);
286
+
287
+ const flatness = (x, z) => {
288
+ const step = 4;
289
+ const center = sample(x, z);
290
+ let deviation = 0;
291
+ for (const [dx, dz] of [[step, 0], [-step, 0], [0, step], [0, -step]]) {
292
+ deviation += Math.abs(sample(x + dx, z + dz) - center);
293
+ }
294
+ return deviation / (4 * step);
295
+ };
296
+
297
+ const nearShore = (x, z) => {
298
+ for (let probe = 0; probe < 8; probe += 1) {
299
+ const angle = (probe / 8) * Math.PI * 2;
300
+ if (sample(x + Math.cos(angle) * 26, z + Math.sin(angle) * 26) <= waterLevel) return true;
301
+ }
302
+ return false;
303
+ };
304
+
305
+ const candidates = [];
306
+ const attempts = Math.max(count * 60, 240);
307
+ for (let attempt = 0; attempt < attempts && candidates.length < count * 24; attempt += 1) {
308
+ const x = (random() * 2 - 1) * innerX;
309
+ const z = (random() * 2 - 1) * innerZ;
310
+ const y = sample(x, z);
311
+ if (y <= waterLevel + shoreMargin + 0.4) continue;
312
+ const slope = flatness(x, z);
313
+ if (slope > 0.45) continue;
314
+ const score = (1 / (0.08 + slope)) * (nearShore(x, z) ? 1.5 : 1);
315
+ candidates.push({ score, x, y, z });
316
+ }
317
+ if (candidates.length === 0) return [];
318
+ candidates.sort((a, b) => b.score - a.score);
319
+
320
+ // Greedy max-min-distance pick from the best half — spread beats score.
321
+ const pool = candidates.slice(0, Math.max(count * 8, 16));
322
+ const picked = [pool[0]];
323
+ while (picked.length < count && pool.length > picked.length) {
324
+ let best = null;
325
+ let bestDistance = -1;
326
+ for (const candidate of pool) {
327
+ if (picked.includes(candidate)) continue;
328
+ let nearest = Infinity;
329
+ for (const point of picked) {
330
+ nearest = Math.min(nearest, Math.hypot(candidate.x - point.x, candidate.z - point.z));
331
+ }
332
+ if (nearest > bestDistance) {
333
+ bestDistance = nearest;
334
+ best = candidate;
335
+ }
336
+ }
337
+ if (!best) break;
338
+ picked.push(best);
339
+ }
340
+ return picked.map(({ x, y, z }) => ({ x, y, z }));
341
+ }
342
+
343
+ /**
344
+ * Connects points into route pairs: a minimum spanning tree (every POI
345
+ * reachable, no redundant parallel roads) plus, per `loopChance`, one ring
346
+ * edge so the network isn't always a tree.
347
+ */
348
+ export function connectPointsOfInterest(points, { seed = 1, loopChance = 0.35 } = {}) {
349
+ if (!Array.isArray(points) || points.length < 2) return [];
350
+ const random = mulberry32(seed * 1103515245 + 12345);
351
+ const edges = [];
352
+ const inTree = new Set([0]);
353
+ while (inTree.size < points.length) {
354
+ let best = null;
355
+ for (const from of inTree) {
356
+ for (let to = 0; to < points.length; to += 1) {
357
+ if (inTree.has(to)) continue;
358
+ const distance = Math.hypot(points[from].x - points[to].x, points[from].z - points[to].z);
359
+ if (!best || distance < best.distance) best = { distance, from, to };
360
+ }
361
+ }
362
+ if (!best) break;
363
+ edges.push([best.from, best.to]);
364
+ inTree.add(best.to);
365
+ }
366
+ if (points.length > 2 && random() < loopChance) {
367
+ // Close the longest open pair that isn't already an edge.
368
+ const has = new Set(edges.map(([a, b]) => `${Math.min(a, b)}-${Math.max(a, b)}`));
369
+ let far = null;
370
+ for (let a = 0; a < points.length; a += 1) {
371
+ for (let b = a + 1; b < points.length; b += 1) {
372
+ if (has.has(`${a}-${b}`)) continue;
373
+ const distance = Math.hypot(points[a].x - points[b].x, points[a].z - points[b].z);
374
+ if (!far || distance > far.distance) far = { a, b, distance };
375
+ }
376
+ }
377
+ if (far) edges.push([far.a, far.b]);
378
+ }
379
+ return edges;
380
+ }