@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.
- package/AGENTS.md +127 -4
- package/ATTRIBUTION.md +41 -3
- package/README.md +234 -34
- package/docs/characters.md +144 -0
- package/docs/debug-panel.md +126 -0
- package/docs/docs.css +589 -0
- package/docs/environment.md +186 -0
- package/docs/getting-started.md +180 -0
- package/docs/index.html +14 -0
- package/docs/lab-architecture.md +100 -0
- package/docs/lighting.md +825 -0
- package/docs/main.jsx +739 -0
- package/docs/mcp.md +97 -0
- package/docs/post-processing.md +98 -0
- package/docs/settings-reference.md +1878 -0
- package/docs/shader-constants.md +77 -0
- package/docs/sky.md +182 -0
- package/docs/style-labs.md +309 -0
- package/docs/texture-lab.md +135 -0
- package/docs/toon-shading.md +183 -0
- package/docs/tsl-conventions.md +167 -0
- package/docs/vegetation-sky.md +275 -0
- package/docs/water.md +430 -0
- package/docs/weather.md +200 -0
- package/docs/world-scale.md +111 -0
- package/mcp/server.mjs +610 -0
- package/mcp/style-lab-tools.mjs +371 -0
- package/mcp/vite-plugin.mjs +174 -0
- package/mcp/workspace.mjs +397 -0
- package/package.json +64 -5
- package/src/ambientfx/INTEGRATION.md +164 -0
- package/src/ambientfx/ambientFxPresets.js +83 -0
- package/src/ambientfx/ambientFxSettings.js +368 -0
- package/src/ambientfx/emitters.js +169 -0
- package/src/ambientfx/index.js +5 -0
- package/src/ambientfx/particleBackbone.js +493 -0
- package/src/ambientfx/stylizedAmbientFx.js +450 -0
- package/src/assetlib/ambientcg.js +117 -0
- package/src/assetlib/assetRef.js +91 -0
- package/src/assetlib/importedEntry.js +40 -0
- package/src/assetlib/index.js +23 -0
- package/src/assetlib/kaykit.js +192 -0
- package/src/assetlib/kaykitStaticIndex.js +700 -0
- package/src/assetlib/loadImported.js +143 -0
- package/src/assetlib/opensource3d.js +113 -0
- package/src/assetlib/polyhaven.js +182 -0
- package/src/assetlib/polypizza.js +115 -0
- package/src/assetlib/smithsonian.js +214 -0
- package/src/assetlib/sources.js +279 -0
- package/src/assetlib/zip.js +58 -0
- package/src/biome/biomeGenerator.js +385 -0
- package/src/biome/biomeRuntime.js +299 -0
- package/src/biome/index.js +2 -0
- package/src/buildinggen/buildingAsset.js +44 -0
- package/src/buildinggen/buildingGrammar.js +311 -0
- package/src/buildinggen/buildingMesh.js +451 -0
- package/src/buildinggen/buildingPresets.js +46 -0
- package/src/buildinggen/buildingRecipe.js +100 -0
- package/src/buildinggen/buildingSettings.js +238 -0
- package/src/buildinggen/index.js +6 -0
- package/src/camera/cameraDirector.js +157 -0
- package/src/camera/cameraGenerator.js +367 -0
- package/src/camera/cameraRig.js +570 -0
- package/src/camera/cameraSettings.js +236 -0
- package/src/camera/index.js +7 -0
- package/src/catalog/builtinEntries.js +245 -0
- package/src/catalog/catalog.js +210 -0
- package/src/catalog/index.js +3 -0
- package/src/catalog/manifest.js +84 -0
- package/src/core/generation.js +529 -0
- package/src/environment/environmentRigs.js +21 -1
- package/src/environment/environmentSettings.js +4 -0
- package/src/environment/environmentSunShadowPass.js +8 -0
- package/src/fauna/INTEGRATION.md +174 -0
- package/src/fauna/boids.js +861 -0
- package/src/fauna/faunaBodies.js +492 -0
- package/src/fauna/faunaPresets.js +52 -0
- package/src/fauna/faunaSettings.js +525 -0
- package/src/fauna/index.js +5 -0
- package/src/fauna/stylizedFauna.js +395 -0
- package/src/game-feel/gameFeelGenerator.js +402 -0
- package/src/game-feel/gameFeelRuntime.js +549 -0
- package/src/game-feel/index.js +2 -0
- package/src/index.js +17 -6
- package/src/lighting/colorIntensity.js +177 -0
- package/src/lighting/index.js +161 -0
- package/src/lighting/lightDescriptors.js +249 -0
- package/src/lighting/lightingCapabilities.js +79 -0
- package/src/lighting/lightingDocuments.js +247 -0
- package/src/lighting/lightingFixtures.js +446 -0
- package/src/lighting/lightingGenerator.js +449 -0
- package/src/lighting/lightingPresets.js +319 -0
- package/src/lighting/lightingRuntime.js +723 -0
- package/src/lighting/lightingStyle.js +386 -0
- package/src/lighting/lightingSystem.js +774 -0
- package/src/lighting/unrealExport.js +186 -0
- package/src/lighting/utils.js +87 -0
- package/src/motion/index.js +5 -0
- package/src/motion/motionClip.js +441 -0
- package/src/motion/motionController.js +628 -0
- package/src/motion/motionDocuments.js +225 -0
- package/src/motion/motionGraph.js +307 -0
- package/src/motion/motionSettings.js +222 -0
- package/src/pathgen/index.js +7 -0
- package/src/pathgen/pathBridge.js +232 -0
- package/src/pathgen/pathPresets.js +35 -0
- package/src/pathgen/pathRibbon.js +410 -0
- package/src/pathgen/pathRouter.js +380 -0
- package/src/pathgen/pathSettings.js +335 -0
- package/src/pathgen/pathTextures.js +123 -0
- package/src/pathgen/stylizedPaths.js +453 -0
- package/src/post/index.js +1 -0
- package/src/post/postGenerator.js +177 -0
- package/src/post/postProcessing.js +41 -0
- package/src/propgen/generatorsWave1.js +379 -0
- package/src/propgen/generatorsWave2.js +462 -0
- package/src/propgen/index.js +5 -0
- package/src/propgen/propAsset.js +323 -0
- package/src/propgen/propParts.js +170 -0
- package/src/propgen/propPlacement.js +459 -0
- package/src/propgen/propPresets.js +82 -0
- package/src/propgen/propSettings.js +395 -0
- package/src/shaders-tsl/chunks/projected-water-caustics.js +242 -0
- package/src/shaders-tsl/chunks/vegetation-style.js +360 -0
- package/src/shaders-tsl/chunks/water-shore-state.js +31 -0
- package/src/shaders-tsl/chunks/water-waves.js +90 -11
- package/src/shaders-tsl/environment.js +15 -1
- package/src/shaders-tsl/flower.js +279 -30
- package/src/shaders-tsl/grass.js +60 -33
- package/src/shaders-tsl/sky.js +125 -31
- package/src/shaders-tsl/tree-leaf.js +61 -25
- package/src/shaders-tsl/water-breaker.js +7 -4
- package/src/shaders-tsl/water-shore-state-simulation.js +523 -0
- package/src/shaders-tsl/water.js +439 -49
- package/src/shaders-tsl/woody-surface.js +154 -0
- package/src/sky/sceneOverrideLayers.js +10 -0
- package/src/sky/skyQuality.js +26 -0
- package/src/sky/stylizedSky.js +753 -45
- package/src/soundscape/index.js +4 -0
- package/src/soundscape/soundscapeGenerator.js +179 -0
- package/src/soundscape/soundscapeRuntime.js +806 -0
- package/src/soundscape/soundscapeSettings.js +292 -0
- package/src/styles/index.js +13 -0
- package/src/styles/styleBundle.js +325 -0
- package/src/stylizedTerrain.js +32 -2
- package/src/stylizedWorld.js +423 -20
- package/src/texgen/evaluateTexture.js +675 -0
- package/src/texgen/index.js +60 -0
- package/src/texgen/noise2.js +210 -0
- package/src/texgen/textureAi.js +436 -0
- package/src/texgen/textureGenerators.js +516 -0
- package/src/texgen/texturePresets.js +490 -0
- package/src/texgen/textureSettings.js +342 -0
- package/src/texgen/textureThree.js +59 -0
- package/src/vegetation/flowerSpecies.js +15 -3
- package/src/vegetation/grassPalettes.js +153 -0
- package/src/vegetation/index.js +6 -0
- package/src/vegetation/stylizedBush.js +2 -0
- package/src/vegetation/stylizedFlower.js +82 -0
- package/src/vegetation/stylizedFlowers.js +48 -7
- package/src/vegetation/stylizedForest.js +29 -1
- package/src/vegetation/stylizedGrass.js +291 -56
- package/src/vegetation/stylizedTree.js +38 -2
- package/src/vegetation/stylizedTreeFoliage.js +2 -1
- package/src/vegetation/vegetationShaders.js +1110 -0
- package/src/vfxgen/INTEGRATION.md +145 -0
- package/src/vfxgen/core/burstBackbone.js +380 -0
- package/src/vfxgen/core/projectileCore.js +92 -0
- package/src/vfxgen/core/spriteShapes.js +98 -0
- package/src/vfxgen/core/trailRibbon.js +272 -0
- package/src/vfxgen/core/vfxRandom.js +29 -0
- package/src/vfxgen/effects/emitHelpers.js +37 -0
- package/src/vfxgen/effects/magicEffects.js +162 -0
- package/src/vfxgen/effects/movementEffects.js +87 -0
- package/src/vfxgen/effects/weaponEffects.js +118 -0
- package/src/vfxgen/index.js +18 -0
- package/src/vfxgen/moves/moveController.js +146 -0
- package/src/vfxgen/moves/moveLibrary.js +335 -0
- package/src/vfxgen/vfxPresets.js +98 -0
- package/src/vfxgen/vfxSettings.js +384 -0
- package/src/vfxgen/vfxSystem.js +449 -0
- package/src/vfxgen/weapons/stylizedWeapons.js +137 -0
- package/src/villagegen/index.js +4 -0
- package/src/villagegen/stylizedVillage.js +490 -0
- package/src/villagegen/villageArchetypes.js +160 -0
- package/src/villagegen/villageNames.js +40 -0
- package/src/villagegen/villageSites.js +105 -0
- package/src/water/sceneOverrideLayers.js +23 -0
- package/src/water/water.js +5 -0
- package/src/water/waterBreakerSystem.js +15 -1
- package/src/water/waterCurrentField.js +447 -0
- package/src/water/waterMaterial.js +12 -0
- package/src/water/waterNearshorePhase.js +320 -0
- package/src/water/waterScenePasses.js +83 -30
- package/src/water/waterSettings.js +325 -13
- package/src/water/waterShoreMaterial.js +322 -0
- package/src/water/waterShoreStateField.js +605 -0
- package/src/water/waterSurface.js +797 -28
- package/src/weather/index.js +6 -0
- package/src/weather/weatherPrecipitation.js +221 -0
- package/src/weather/weatherPresets.js +258 -0
- package/src/weather/weatherSettings.js +269 -0
- package/src/weather/weatherSystem.js +871 -0
- package/src/worldMinimap.js +62 -0
- 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
|
+
}
|