diff --git a/mapTerrain.js b/mapTerrain.js index ef6e25f..89bbb9e 100644 --- a/mapTerrain.js +++ b/mapTerrain.js @@ -1,6 +1,5 @@ -import { INF, MAP_H, MAP_W, SIZE, clamp, createMapFields, fbm, hash2, indexOf, inside, lerp, pickEntities, rand, smoothstep, valueNoise } from "./mapUtils.js"; +import { INF, MAP_H, MAP_W, SIZE, MinHeap, clamp, createMapFields, fbm, hash2, indexOf, inside, lerp, pickEntities, rand, smoothstep, valueNoise, xyOf } from "./mapUtils.js"; import { - aStar, extractMaskBorder, extractRegionBorderSegments, generateRegionalPrefectures, @@ -8,2501 +7,753 @@ import { neighbors8, } from "./mapGeneratorHelpers.js"; -export function buildTerrainTemplate(seed) { - const deposition = 0.18 + rand(seed, 41) * 0.72; - const erosion = 0.24 + rand(seed, 42) * 0.68; - const roughness = 0.34 + rand(seed, 43) * 0.62; - const coastAxisPick = Math.floor(rand(seed, 10) * 3); - const coastAngle = coastAxisPick === 0 - ? Math.PI / 2 - : coastAxisPick === 1 - ? 0 - : (rand(seed, 11) > 0.5 ? Math.PI / 4 : -Math.PI / 4) + (rand(seed, 14) - 0.5) * 0.28; - const ridgeJaggedness = 0.20 + rand(seed, 44) * 0.70; - const spineCount = 2 + Math.floor(rand(seed, 45) * 2); - const sideAPlain = 0.035 + rand(seed, 56) * 0.115 + deposition * 0.085; - const sideBPlain = 0.035 + rand(seed, 57) * 0.115 + deposition * 0.085; - const spineAngle = coastAngle + Math.PI * (0.28 + rand(seed, 46) * 0.44); - const spineCurve = (rand(seed, 47) - 0.5) * 0.28; - const spinePosition = (rand(seed, 48) - 0.5) * 0.56; - const backboneLongShift = (rand(seed, 68) - 0.5) * 0.34; - const backboneCenterX = clamp(0.5 + Math.cos(spineAngle + Math.PI / 2) * spinePosition * 0.36 + Math.cos(spineAngle) * backboneLongShift, 0.18, 0.82); - const backboneCenterY = clamp(0.5 + Math.sin(spineAngle + Math.PI / 2) * spinePosition * 0.36 + Math.sin(spineAngle) * backboneLongShift, 0.18, 0.82); - const backboneLength = 0.50 + rand(seed, 69) * 0.28; - const backboneWidth = 0.12 + rand(seed, 83) * 0.12; - const backboneScratchCount = 34 + Math.floor(rand(seed, 84) * 30); +const ASPECT = MAP_W / MAP_H; +const SQRT2 = Math.SQRT2; +function normalizeCoord(x, y) { return { - seed, - spineCount, - spineAngle, - spineCurve, - spinePosition, - backboneCenterX, - backboneCenterY, - backboneLength, - backboneWidth, - backboneScratchCount, - spineStrength: 0.56 + rand(seed, 49) * 0.32, - spineWidth: 0.034 + rand(seed, 50) * 0.036, - // v4: 個別の丸い山塊生成を主役にしない。山地は下の folded orogeny field で一括生成する。 - secondaryMountainCount: 0, - secondaryMountainSize: 0.038 + rand(seed, 52) * 0.060, - secondaryMountainStrength: 0.40 + rand(seed, 53) * 0.25, - rangeBreakCount: 4 + Math.floor(rand(seed, 62) * 4), - rangeBreakWidth: 0.022 + rand(seed, 63) * 0.026, - rangeBreakStrength: 0.060 + rand(seed, 64) * 0.070, - plainNoiseSuppression: 0.34 + rand(seed, 65) * 0.22, - // 高標高がすぐ天井へ張り付いて平頂山化しないよう、 - // ソフトクリップ開始をやや遅らせ、肩を高めに設定する。 - peakSoftStart: 0.905 + rand(seed, 66) * 0.030, - peakSoftCap: 1.010 + rand(seed, 67) * 0.020, - orographicStrength: 0.88 + rand(seed, 70) * 0.28, - orographicCoverage: 0.72 + rand(seed, 71) * 0.18, - foldDensity: 5.2 + rand(seed, 72) * 2.2, - foldSharpness: 1.65 + rand(seed, 73) * 0.85, - fluvialAggression: 1.50 + rand(seed, 74) * 0.48, - // 粗い格子でも山肌の複雑さが出るよう、 - // 細谷の「見え」は手続きノイズ寄りにし、明示的な小流路本数は抑える。 - drainageDensity: 0.56 + rand(seed, 75) * 0.22, - gullyIncision: 0.18 + rand(seed, 76) * 0.10, - dendriticTexture: 0.72 + rand(seed, 77) * 0.24, - macroNoiseStrength: 0.018 + rand(seed, 85) * 0.014, - macroNoiseScale: 0.028 + rand(seed, 86) * 0.018, - globalNoiseStrength: 0.010 + rand(seed, 87) * 0.010, - headwaterGullyCount: 0, - alpineMicroRelief: 0.018 + rand(seed, 79) * 0.022, - basinOutletCount: 10 + Math.floor(rand(seed, 80) * 8), - meanderStrength: 0.18 + rand(seed, 81) * 0.16, - terrainNoisePatchiness: 0.42 + rand(seed, 82) * 0.28, - coastAxis: coastAxisPick === 0 ? "east-west" : coastAxisPick === 1 ? "north-south" : "diagonal", - coastAngle, - coastBias: 0.18 + rand(seed, 12) * 0.24, - coastRoughness: 0.34 + rand(seed, 54) * 0.58, - coastSides: [ - { - penetration: 0.18 + rand(seed, 58) * 0.16, - inletStrength: 0.18 + rand(seed, 59) * 0.56, - plainWidth: sideAPlain, - }, - { - penetration: 0.18 + rand(seed, 60) * 0.16, - inletStrength: 0.18 + rand(seed, 61) * 0.56, - plainWidth: sideBPlain, - }, - ], - deposition, - erosion, - roughness, - ridgeJaggedness, - ridgeBranchiness: 0.44 + rand(seed, 55) * 0.66, - detachedRangeCount: 0, - alpinePeakCount: 0, + px: (x + 0.5) / MAP_W, + py: (y + 0.5) / MAP_H, }; } -function jaggedRidgeContribution(x, y, ridge, seed) { - const dx = x - ridge.x; - const dy = y - ridge.y; - const ca = Math.cos(ridge.angle); - const sa = Math.sin(ridge.angle); - const along = dx * ca + dy * sa; - const perp = -dx * sa + dy * ca; - const nAlong = along / Math.max(0.001, ridge.length); - const lengthFade = smoothstep(1 - Math.abs(nAlong)); - if (lengthFade <= 0) return 0; - - // Bend the centerline itself with coherent long/mid waves, then apply ridge falloff. - const low = (valueNoise(along * 0.85 + ridge.seedOffset, ridge.seedOffset * 0.37, seed + 6100, 28) - 0.5) * 2; - const mid = (valueNoise(along * 1.7 - ridge.seedOffset, ridge.seedOffset * 0.23, seed + 6200, 13) - 0.5) * 2; - const sine = Math.sin(along * ridge.kinkFrequency + ridge.kinkPhase); - const curve = (ridge.curve || 0) * along * along * (along >= 0 ? 1 : -1); - const axisOffset = low * ridge.axisWobble + mid * ridge.axisWobble * 0.55 + sine * ridge.axisWobble * 0.25 + curve; - const widthNoise = 0.78 + valueNoise(along * 1.2 + ridge.seedOffset, ridge.seedOffset * 0.19, seed + 6300, 21) * ridge.widthVariation; - const localWidth = Math.max(0.006, ridge.width * widthNoise); - const jaggedPerp = perp - axisOffset; - const serration = 0.76 + valueNoise(x * 1.1 + along * 0.18, y * 1.1 + perp * 0.18, seed + ridge.seedOffset, 7) * 0.48; - return Math.exp(-(jaggedPerp * jaggedPerp) / (localWidth * localWidth)) * lengthFade * ridge.h * serration; +function distNorm(ax, ay, bx, by) { + const dx = (ax - bx) * ASPECT; + const dy = ay - by; + return Math.hypot(dx, dy); } -function spineFieldAt(x, y, template, spineIndex) { - const seed = template.seed || 0; - const spacing = spineIndex === 0 ? 0 : (spineIndex % 2 ? 0.30 : -0.30); - const angle = template.spineAngle + (spineIndex - 0.5) * 0.17 + (rand(seed, 700 + spineIndex) - 0.5) * 0.18; - const ridge = { - x: 0.5 + Math.cos(angle + Math.PI / 2) * (template.spinePosition + spacing) * 0.45, - y: 0.5 + Math.sin(angle + Math.PI / 2) * (template.spinePosition + spacing) * 0.45, - angle, - width: template.spineWidth * (0.82 + rand(seed, 710 + spineIndex) * 0.38), - length: 0.78 + rand(seed, 720 + spineIndex) * 0.28, - h: template.spineStrength * (0.24 + rand(seed, 730 + spineIndex) * 0.12), - curve: template.spineCurve, - axisWobble: template.spineWidth * (0.45 + template.ridgeJaggedness * 1.15), - kinkFrequency: 10 + rand(seed, 740 + spineIndex) * 18, - kinkPhase: rand(seed, 750 + spineIndex) * Math.PI * 2, - seedOffset: 7600 + spineIndex * 211, - widthVariation: 0.18 + template.ridgeJaggedness * 0.34, - }; - return jaggedRidgeContribution(x, y, ridge, seed); +function rotate(dx, dy, angle) { + const c = Math.cos(angle); + const s = Math.sin(angle); + return { u: dx * c + dy * s, v: -dx * s + dy * c }; } -function buildSpineRidges(seed, template) { - const spines = []; - const branches = []; - const centerX = template.backboneCenterX ?? 0.5; - const centerY = template.backboneCenterY ?? 0.5; - const baseAngle = template.spineAngle; - const axisCos = Math.cos(baseAngle); - const axisSin = Math.sin(baseAngle); - const crossCos = Math.cos(baseAngle + Math.PI / 2); - const crossSin = Math.sin(baseAngle + Math.PI / 2); - const corridorLength = template.backboneLength ?? 0.62; - const corridorWidth = template.backboneWidth ?? 0.16; - - for (let i = 0; i < template.spineCount; i++) { - const along = ((i / Math.max(1, template.spineCount - 1)) - 0.5) * corridorLength * 0.48 + (rand(seed, 705 + i) - 0.5) * corridorLength * 0.12; - const cross = (rand(seed, 706 + i) - 0.5) * corridorWidth * 0.45; - const angle = baseAngle + (rand(seed, 700 + i) - 0.5) * 0.16; - const x = clamp(centerX + axisCos * along + crossCos * cross, 0.06, 0.94); - const y = clamp(centerY + axisSin * along + crossSin * cross, 0.06, 0.94); - spines.push({ - x, y, angle, - width: template.spineWidth * (0.52 + rand(seed, 710 + i) * 0.28), - length: corridorLength * (0.58 + rand(seed, 720 + i) * 0.18), - h: template.spineStrength * (0.11 + rand(seed, 730 + i) * 0.08), - curve: template.spineCurve, - axisWobble: template.spineWidth * (0.42 + template.ridgeJaggedness * 0.95), - kinkFrequency: 10 + rand(seed, 740 + i) * 16, - kinkPhase: rand(seed, 750 + i) * Math.PI * 2, - seedOffset: 7600 + i * 211, - widthVariation: 0.18 + template.ridgeJaggedness * 0.34, - }); - } - - const scratchCount = template.backboneScratchCount ?? (36 + Math.floor(template.ridgeBranchiness * 30)); - for (let b = 0; b < scratchCount; b++) { - const centerBias = ((rand(seed, 810 + b) + rand(seed, 811 + b)) * 0.5 - 0.5) * 2; - const edgeBias = (rand(seed, 812 + b) - 0.5) * 2; - const along = centerBias * corridorLength * 0.82; - const corridorT = clamp(1 - Math.abs(centerBias)); - const lateralSpread = corridorWidth * (0.48 + corridorT * 0.72); - const cross = edgeBias * lateralSpread; - const radialT = clamp(1 - Math.abs(edgeBias)); - const density = clamp(corridorT * 0.72 + radialT * 0.28); - const x = clamp(centerX + axisCos * along + crossCos * cross, 0.04, 0.96); - const y = clamp(centerY + axisSin * along + crossSin * cross, 0.04, 0.96); - const branchAngle = baseAngle + (rand(seed, 830 + b) - 0.5) * 0.92 + edgeBias * 0.20; - branches.push({ - x, - y, - angle: branchAngle, - width: template.spineWidth * (0.16 + rand(seed, 840 + b) * 0.16 + density * 0.08), - length: 0.07 + rand(seed, 850 + b) * 0.13 + density * 0.06, - h: template.spineStrength * (0.018 + density * 0.066 + rand(seed, 860 + b) * 0.018), - curve: template.spineCurve * 0.22, - axisWobble: template.spineWidth * (0.22 + template.ridgeJaggedness * 0.58), - kinkFrequency: 15 + rand(seed, 870 + b) * 22, - kinkPhase: rand(seed, 880 + b) * Math.PI * 2, - seedOffset: 8800 + b * 37, - widthVariation: 0.16 + template.ridgeJaggedness * 0.20, - }); - } - return { spines, branches }; +function quantile(values, q) { + const arr = Array.from(values).filter(Number.isFinite).sort((a, b) => a - b); + if (!arr.length) return 0; + const p = clamp(q) * (arr.length - 1); + const i = Math.floor(p); + const f = p - i; + return lerp(arr[i], arr[Math.min(arr.length - 1, i + 1)], f); } - -function softUpperClamp(value, start = 0.8, cap = 0.96) { - if (value <= start) return value; - if (value <= cap) { - const t = clamp((value - start) / Math.max(0.001, cap - start)); - // 肩へ向かって緩やかに圧縮するが、cap 未満ではなるべく差を残す。 - return lerp(value, start + (cap - start) * (1 - Math.pow(1 - t, 1.18)), 0.16); - } - const overflow = value - cap; - // cap 超過分も少し残して、山頂が一様な平頂面にならないようにする。 - return cap + overflow * (0.28 / (1 + overflow * 4.2)); -} - -function elongatedFeatureContribution(x, y, feature, seed) { - const dx = x - feature.x; - const dy = y - feature.y; - const ca = Math.cos(feature.angle); - const sa = Math.sin(feature.angle); - const along = dx * ca + dy * sa; - const perp = -dx * sa + dy * ca; - const nAlong = along / Math.max(0.001, feature.length); - if (Math.abs(nAlong) > 1.35) return 0; - const alongFade = Math.exp(-nAlong * nAlong * 1.7); - const low = (valueNoise(along * 0.95 + feature.seedOffset, feature.seedOffset * 0.31, seed + 6400, 19) - 0.5) * 2; - const mid = (valueNoise(along * 1.75 - feature.seedOffset, feature.seedOffset * 0.21, seed + 6500, 9) - 0.5) * 2; - const axisOffset = low * feature.axisWobble + mid * feature.axisWobble * 0.45; - const localWidth = Math.max(0.008, feature.width * (0.84 + valueNoise(along * 1.15, feature.seedOffset, seed + 6600, 14) * feature.widthVariation)); - const offsetPerp = perp - axisOffset; - return Math.exp(-(offsetPerp * offsetPerp) / (localWidth * localWidth)) * alongFade * feature.h; -} - -function buildRangeBreaks(seed, template, spines) { - const rangeBreaks = []; - for (let i = 0; i < spines.length; i++) { - const spine = spines[i]; - const count = Math.max(2, template.rangeBreakCount - 1 + Math.floor(rand(seed, 890 + i) * 3)); - for (let b = 0; b < count; b++) { - const along = (rand(seed, 900 + i * 37 + b) - 0.5) * spine.length * 0.84; - const lateral = (rand(seed, 910 + i * 37 + b) - 0.5) * spine.width * 0.9; - rangeBreaks.push({ - x: spine.x + Math.cos(spine.angle) * along + Math.cos(spine.angle + Math.PI / 2) * lateral, - y: spine.y + Math.sin(spine.angle) * along + Math.sin(spine.angle + Math.PI / 2) * lateral, - angle: spine.angle + (rand(seed, 920 + i * 37 + b) > 0.5 ? Math.PI / 2 : -Math.PI / 2) + (rand(seed, 930 + i * 37 + b) - 0.5) * 0.42, - width: template.rangeBreakWidth * (0.75 + rand(seed, 940 + i * 37 + b) * 0.75), - length: 0.12 + rand(seed, 950 + i * 37 + b) * 0.14, - h: template.rangeBreakStrength * (0.78 + rand(seed, 960 + i * 37 + b) * 0.55), - axisWobble: template.rangeBreakWidth * (0.18 + rand(seed, 970 + i * 37 + b) * 0.32), - widthVariation: 0.14 + rand(seed, 980 + i * 37 + b) * 0.24, - seedOffset: 9900 + i * 311 + b * 41, - }); - } - } - return rangeBreaks; -} - - -function buildDetachedRanges(seed, template) { - const ranges = []; - const count = template.detachedRangeCount ?? 6; - for (let i = 0; i < count; i++) { - const quadrantX = i % 2 === 0 ? 0.24 : 0.76; - const quadrantY = Math.floor(i / 2) % 2 === 0 ? 0.24 : 0.76; - const free = rand(seed, 12000 + i) < 0.45; - const x = free ? 0.12 + rand(seed, 12010 + i) * 0.76 : quadrantX + (rand(seed, 12020 + i) - 0.5) * 0.28; - const y = free ? 0.12 + rand(seed, 12030 + i) * 0.76 : quadrantY + (rand(seed, 12040 + i) - 0.5) * 0.28; - const angle = template.spineAngle + (rand(seed, 12050 + i) - 0.5) * Math.PI * 0.95; - ranges.push({ - x: clamp(x, 0.08, 0.92), - y: clamp(y, 0.08, 0.92), - angle, - width: 0.020 + rand(seed, 12060 + i) * 0.030, - length: 0.18 + rand(seed, 12070 + i) * 0.28, - h: 0.075 + rand(seed, 12080 + i) * 0.095, - curve: (rand(seed, 12090 + i) - 0.5) * 0.10, - axisWobble: 0.018 + template.ridgeJaggedness * 0.030, - kinkFrequency: 14 + rand(seed, 12100 + i) * 24, - kinkPhase: rand(seed, 12110 + i) * Math.PI * 2, - seedOffset: 12120 + i * 173, - widthVariation: 0.28 + template.ridgeJaggedness * 0.36, - }); - } - return ranges; -} - -function buildAlpinePeaks(seed, template, detachedRanges) { - const peaks = []; - const count = template.alpinePeakCount ?? 8; - for (let i = 0; i < count; i++) { - const attach = detachedRanges.length && rand(seed, 12300 + i) < 0.62; - const base = attach ? detachedRanges[i % detachedRanges.length] : null; - const along = base ? (rand(seed, 12310 + i) - 0.5) * base.length * 0.90 : 0; - const perp = base ? (rand(seed, 12320 + i) - 0.5) * base.width * 4.5 : 0; - const x = base ? base.x + Math.cos(base.angle) * along + Math.cos(base.angle + Math.PI / 2) * perp : 0.10 + rand(seed, 12330 + i) * 0.80; - const y = base ? base.y + Math.sin(base.angle) * along + Math.sin(base.angle + Math.PI / 2) * perp : 0.10 + rand(seed, 12340 + i) * 0.80; - peaks.push({ - x: clamp(x, 0.06, 0.94), - y: clamp(y, 0.06, 0.94), - angle: base ? base.angle + (rand(seed, 12350 + i) - 0.5) * 0.9 : rand(seed, 12360 + i) * Math.PI * 2, - rx: 0.022 + rand(seed, 12370 + i) * 0.035, - ry: 0.012 + rand(seed, 12380 + i) * 0.024, - h: 0.070 + rand(seed, 12390 + i) * 0.100, - seedOffset: 12400 + i * 191, - }); - } - return peaks; -} - -// v5: 「全域を海底として初期化し、海底から広域隆起で山地を生やす」ための造山場。 -// 周期的な褶曲波はワッフル状の縞を作るため廃止し、広い帯状隆起・不均質な断層谷・ -// 連続した尾根核を domain-warp 付きで合成する。 -function foldedOrogenyAt(px, py, seed, template, coastLower = 0) { - const baseAngle = template.spineAngle + (rand(seed, 13001) - 0.5) * 0.28; - const warpX = (fbm(px * 2.0 + 17, py * 2.0 - 31, seed + 13010) - 0.5) * 0.20; - const warpY = (fbm(px * 2.1 - 43, py * 2.1 + 19, seed + 13020) - 0.5) * 0.20; - const x = px + warpX; - const y = py + warpY; - - let beltMass = 0; - let ridgeCores = 0; - let structuralValleys = 0; - const beltCount = 3; - - for (let k = 0; k < beltCount; k++) { - const angle = baseAngle + (k - 1) * 0.24 + (rand(seed, 13100 + k) - 0.5) * 0.26; - const ca = Math.cos(angle); - const sa = Math.sin(angle); - const along = x * ca + y * sa; - const cross = -x * sa + y * ca; - const offset = (rand(seed, 13120 + k) - 0.5) * 0.38 + (k - 1) * 0.10; - const axisWarp = (fbm(px * 1.65 + k * 11, py * 1.65 - k * 7, seed + 13200 + k) - 0.5) * (0.15 + template.ridgeJaggedness * 0.08); - const lengthWarp = (valueNoise(px * 1.3 - k * 17, py * 1.3 + k * 13, seed + 13250 + k, 2.4) - 0.5) * 0.34; - const localCross = cross - offset - axisWarp; - const localAlong = along + lengthWarp; - const width = 0.24 + rand(seed, 13300 + k) * 0.14; - const coreWidth = 0.058 + rand(seed, 13320 + k) * 0.046; - const lengthFade = smoothstep(1.38 - Math.abs(localAlong - 0.52)); - const broad = Math.exp(-(localCross * localCross) / (width * width)) * lengthFade; - const core = Math.exp(-(localCross * localCross) / (coreWidth * coreWidth)) * lengthFade; - const broken = 0.72 + valueNoise(px * 2.2 + k * 21, py * 2.2 - k * 15, seed + 13400 + k, 2.1) * 0.46; - const rugged = 0.74 + fbm(px * 4.3 + k * 19, py * 4.3 - k * 23, seed + 13480 + k) * 0.54; - beltMass += broad * (0.285 + k * 0.040) * broken; - ridgeCores += core * (0.340 + k * 0.030) * rugged; - - // 河川が後で選びやすい弱線。周期的な縞ではなく、ノイズで途切れる断層・構造谷として扱う。 - const valleyNoise = Math.max(0, valueNoise(px * 5.2 + k * 31, py * 5.2 - k * 27, seed + 13540 + k, 3.2) - 0.55); - const transverse = Math.exp(-((localAlong - (0.26 + rand(seed, 13600 + k) * 0.52)) ** 2) / 0.030); - structuralValleys += broad * (valleyNoise * 0.18 + transverse * 0.035); - } - - const broadA = fbm(px * 0.80 + 23, py * 0.80 - 61, seed + 13700); - const broadB = valueNoise(px * 1.35 - 41, py * 1.35 + 17, seed + 13710, 2.4); - const continentalPulse = clamp((broadA * 0.58 + broadB * 0.42 - 0.22) / 0.66); - const edgeDistance = Math.min(px, py, 1 - px, 1 - py); - const edgeDrowning = 1 - smoothstep(edgeDistance / 0.16); - const marineLoss = edgeDrowning * (0.22 + template.deposition * 0.10) + coastLower * 0.035; - - const mass = clamp(beltMass * 0.95 + continentalPulse * 0.40 + template.orographicCoverage * 0.12 - structuralValleys * 0.78 - marineLoss * 0.18); - const ridges = clamp(ridgeCores * 1.18 + mass * 0.20 - structuralValleys * 0.64); - const uplift = clamp((mass * 0.62 + ridges * 0.34) * template.orographicStrength); - return { uplift, ridges, valleys: clamp(structuralValleys * 5.0) }; -} - - -// v6: 谷状の細かな侵食テクスチャ。流路網のない場所も「沢に削られた山肌」に見せる。 -// 高周波ノイズをそのまま標高に足すのではなく、ノイズ場の細い等値線だけを抽出して -// 傾斜・標高・湿潤度でマスクする。 -function dendriticRavineTexture(x, y, seed) { - const warpX = (fbm(x * 0.72 + 113, y * 0.72 - 71, seed + 15010) - 0.5) * 9.5; - const warpY = (fbm(x * 0.74 - 59, y * 0.74 + 97, seed + 15020) - 0.5) * 9.5; - const wx = x + warpX; - const wy = y + warpY; - - const n1 = valueNoise(wx * 1.15 + 31, wy * 1.15 - 47, seed + 15100, 8.0); - const n2 = valueNoise(wx * 2.05 - 19, wy * 2.05 + 23, seed + 15110, 4.8); - const n3 = valueNoise(wx * 3.65 + 71, wy * 3.65 - 11, seed + 15120, 3.0); - const line1 = Math.pow(clamp(1 - Math.abs(n1 - 0.50) * 6.1), 3.4); - const line2 = Math.pow(clamp(1 - Math.abs(n2 - 0.47) * 7.6), 3.0); - const line3 = Math.pow(clamp(1 - Math.abs(n3 - 0.52) * 9.0), 2.6); - - const join = valueNoise(wx * 0.55 + 7, wy * 0.55 - 5, seed + 15140, 11.0); - return clamp(line1 * 0.48 + line1 * line2 * 0.44 + line2 * line3 * 0.26 + line3 * Math.max(0, join - 0.58) * 0.36); -} - -function addPathIncision(field, path, strength, radius = 1) { - if (!path || path.length < 2) return; - for (let k = 0; k < path.length; k++) { - const [px, py] = path[k]; - const downstream = k / Math.max(1, path.length - 1); - const local = strength * (0.72 + downstream * 0.44); - for (let dy = -radius; dy <= radius; dy++) { - for (let dx = -radius; dx <= radius; dx++) { - const nx = px + dx; - const ny = py + dy; - if (!inside(nx, ny)) continue; - const d = Math.hypot(dx, dy); - if (d > radius + 0.15) continue; - const w = d < 0.001 ? 1 : Math.max(0, 1 - d / (radius + 0.35)) * 0.52; - const i = indexOf(nx, ny); - field[i] = clamp(field[i] + local * w); - } +function forDisk(cx, cy, radius, fn) { + const r = Math.ceil(radius); + for (let dy = -r; dy <= r; dy++) { + for (let dx = -r; dx <= r; dx++) { + const x = cx + dx; + const y = cy + dy; + if (!inside(x, y)) continue; + const d = Math.hypot(dx, dy); + if (d <= radius) fn(x, y, d); } } } -// v7: line-only carving is too thin and looks like a texture overlay. This helper -// converts traced drainage paths into a distance field with a V-shaped cross-section: -// small headwater gullies stay narrow, while longer/merged channels open a slightly -// wider valley floor. The visual result is produced by terrain change, not by merely -// drawing more blue river lines. -function addValleyDistanceInfluence(incisionField, floorField, path, strength, radius = 2, floorRadius = 0.55) { - if (!path || path.length < 2) return; - for (let k = 0; k < path.length; k++) { - const [px, py] = path[k]; - const downstream = k / Math.max(1, path.length - 1); - const localRadius = Math.max(1.1, radius * (0.68 + downstream * 0.56)); - const localStrength = strength * (0.62 + downstream * 0.70); - const r = Math.ceil(localRadius + 1.2); - for (let dy = -r; dy <= r; dy++) { - for (let dx = -r; dx <= r; dx++) { - const nx = px + dx; - const ny = py + dy; - if (!inside(nx, ny)) continue; - const d = Math.hypot(dx, dy); - if (d > localRadius + 1.0) continue; - const i = indexOf(nx, ny); - const vShape = Math.pow(clamp(1 - d / (localRadius + 0.55)), 1.55); - const floor = smoothstep((floorRadius + 0.25 - d) / Math.max(0.35, floorRadius + 0.25)); - incisionField[i] = clamp(incisionField[i] + localStrength * vShape * 0.48); - floorField[i] = clamp(floorField[i] + localStrength * floor * 0.42); - } - } - } -} - -function applyAlpineMicroRelief(elevation, sea, slope, ridgeField, valleyField, coastalLowland, seaLevel, seed, terrainTemplate, surfaceTextureField = null) { - for (let y = 2; y < MAP_H - 2; y++) { - for (let x = 2; x < MAP_W - 2; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - const highland = clamp((elevation[i] - (seaLevel + 0.12)) / 0.36); - const alpineMask = clamp((elevation[i] - (seaLevel + 0.20)) / 0.28) * clamp(slope[i] * 1.15 + ridgeField[i] * 0.44 - valleyField[i] * 0.26) * (1 - coastalLowland[i] * 0.75); - const ruggedMask = clamp(highland * (0.28 + slope[i] * 0.95 + ridgeField[i] * 0.38 - valleyField[i] * 0.16)); - const patch = clamp(0.35 + (fbm(x * 0.055 + 80, y * 0.055 - 34, seed + 18018) - 0.5) * 1.45 + terrainTemplate.terrainNoisePatchiness * 0.22); - const activeMask = Math.max(alpineMask, ruggedMask * 0.56) * (0.52 + patch * 0.48); - if (activeMask <= 0.02) continue; - const warpX = x + (fbm(x * 0.18 + 24, y * 0.18 - 17, seed + 18021) - 0.5) * 5.5; - const warpY = y + (fbm(x * 0.18 - 37, y * 0.18 + 13, seed + 18022) - 0.5) * 5.5; - const coarse = (valueNoise(warpX * 0.72, warpY * 0.72, seed + 18023, 4.8) - 0.5) * 2; - const medium = (valueNoise(warpX * 1.18 - 11, warpY * 1.18 + 19, seed + 18024, 6.7) - 0.5) * 2; - const fine = (valueNoise(warpX * 1.95 + 17, warpY * 1.95 - 9, seed + 18025, 9.4) - 0.5) * 2; - const ridged = 1 - Math.abs((valueNoise(warpX * 1.36 - 7, warpY * 1.36 + 21, seed + 18026, 6.2) - 0.5) * 2); - const cellular = (fbm(warpX * 0.52 + 9, warpY * 0.52 - 4, seed + 18027) - 0.5) * 2; - const perturb = (coarse * 0.42 + medium * 0.30 + fine * 0.18 + (ridged - 0.5) * 0.88 + cellular * 0.22) * terrainTemplate.alpineMicroRelief * activeMask; - elevation[i] = clamp(elevation[i] + perturb, seaLevel + 0.006, 0.998); - if (surfaceTextureField) { - const textureSignal = clamp(Math.abs(perturb) * 44 + ridged * activeMask * 0.40 + Math.abs(cellular) * activeMask * 0.24 + patch * activeMask * 0.16); - surfaceTextureField[i] = clamp(surfaceTextureField[i] + textureSignal); - } - if (perturb < 0) valleyField[i] = clamp(valleyField[i] + (-perturb) * activeMask * 4.5); - } - } -} - - -function breakHighPlateaus(elevation, sea, slope, ridgeField, valleyField, flowAccum, coastalLowland, seaLevel, seed, terrainTemplate, visibleRavineField = null, surfaceTextureField = null) { - for (let y = 2; y < MAP_H - 2; y++) { - for (let x = 2; x < MAP_W - 2; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - const high = clamp((elevation[i] - (seaLevel + 0.18)) / 0.28); - const flat = clamp((0.26 - slope[i]) * 5.2); - const mountain = clamp(ridgeField[i] * 0.78 + high * 0.62 - valleyField[i] * 0.16) * (1 - coastalLowland[i] * 0.82); - const active = high * flat * mountain; - if (active <= 0.02) continue; - - const warpX = x + (fbm(x * 0.11 + 13, y * 0.11 - 17, seed + 18101) - 0.5) * 4.6; - const warpY = y + (fbm(x * 0.11 - 29, y * 0.11 + 7, seed + 18102) - 0.5) * 4.6; - const dend = dendriticRavineTexture(warpX * 1.05, warpY * 1.05, seed + 18103); - const broad = Math.abs((fbm(warpX * 0.16 + 4, warpY * 0.16 - 9, seed + 18104) - 0.5) * 2); - const summit = Math.max(0, valueNoise(warpX * 1.85 + 11, warpY * 1.85 - 23, seed + 18105, 4.7) - 0.57); - const drainage = Math.pow(flowAccum[i], 0.50); - const carve = active * (0.0038 + dend * 0.0062 + broad * 0.0026 + drainage * 0.0032) * (0.92 + terrainTemplate.dendriticTexture * 0.30); - const bump = active * summit * (0.0028 + terrainTemplate.roughness * 0.0022); - elevation[i] = clamp(elevation[i] - carve + bump, seaLevel + 0.006, 0.998); - valleyField[i] = clamp(valleyField[i] + carve * 18.0); - ridgeField[i] = clamp(ridgeField[i] + bump * 8.0); - if (visibleRavineField) visibleRavineField[i] = clamp(visibleRavineField[i] + carve * 8.6); - if (surfaceTextureField) surfaceTextureField[i] = clamp(surfaceTextureField[i] + active * (0.16 + dend * 0.22 + broad * 0.10)); - } - } -} - -function carveOutletChannel(elevation, sea, lake, river, valleyField, basinField, flowAccum, seaLevel, startIndex, targetIndex, seed, bonusSeed = 0) { - const start = { x: startIndex % MAP_W, y: Math.floor(startIndex / MAP_W) }; - const goal = { x: targetIndex % MAP_W, y: Math.floor(targetIndex / MAP_W) }; - const path = aStar(start, goal, (x, y, cx, cy) => { - const i = indexOf(x, y); - const ci = indexOf(cx, cy); - if (sea[i]) return 0.12; - const uphill = Math.max(0, elevation[i] - elevation[ci]); - return Math.max(0.16, 0.55 + uphill * 80 + Math.max(0, elevation[i] - seaLevel) * 0.10 - flowAccum[i] * 0.65 - valleyField[i] * 0.38 - basinField[i] * 0.12 + (hash2(x, y, seed + bonusSeed) - 0.5) * 0.05); - }); - if (path.length < 2) return []; - const startElev = elevation[startIndex]; - const targetElev = sea[targetIndex] ? seaLevel - 0.002 : Math.min(startElev - 0.010, elevation[targetIndex]); - for (let k = 0; k < path.length; k++) { - const [x, y] = path[k]; - const i = indexOf(x, y); - const t = k / Math.max(1, path.length - 1); - const base = lerp(startElev - 0.001, targetElev + 0.004, t); - const wiggle = (hash2(x, y, seed + 22000 + bonusSeed) - 0.5) * 0.0025; - const floorLimit = seaLevel + 0.020 + basinField[i] * 0.014; - elevation[i] = Math.min(elevation[i], Math.max(floorLimit, base + wiggle)); - valleyField[i] = clamp(valleyField[i] + 0.20 + (1 - t) * 0.12); - basinField[i] = Math.max(0, basinField[i] - 0.10); - river[i] = Math.max(river[i], 0.18 + flowAccum[i] * 0.42 + t * 0.10); - if (lake[i] && k < path.length - 1) lake[i] = 0; - if (!sea[i]) { +function largestComponent(mask, allowEdgePreference = false) { + const seen = new Uint8Array(SIZE); + let best = []; + let bestScore = -1; + for (let i = 0; i < SIZE; i++) { + if (!mask[i] || seen[i]) continue; + const queue = [i]; + const cells = []; + let touchesEdge = false; + seen[i] = 1; + for (let q = 0; q < queue.length; q++) { + const cur = queue[q]; + cells.push(cur); + const x = cur % MAP_W; + const y = Math.floor(cur / MAP_W); + if (x === 0 || y === 0 || x === MAP_W - 1 || y === MAP_H - 1) touchesEdge = true; for (const [nx, ny] of neighbors8(x, y)) { const ni = indexOf(nx, ny); - if (sea[ni]) continue; - const d = Math.hypot(nx - x, ny - y); - const widen = Math.max(0, 0.010 - d * 0.003); - if (widen > 0) { - elevation[ni] = Math.min(elevation[ni], Math.max(seaLevel + 0.020, elevation[i] + 0.008 + d * 0.004)); - valleyField[ni] = clamp(valleyField[ni] + widen * 12); - } + if (!mask[ni] || seen[ni]) continue; + seen[ni] = 1; + queue.push(ni); } } + const score = cells.length + (allowEdgePreference && touchesEdge ? SIZE : 0); + if (score > bestScore) { + bestScore = score; + best = cells; + } + } + const out = new Uint8Array(SIZE); + for (const i of best) out[i] = 1; + return out; +} + +function distanceField(sourceMask, maxDistance = 999) { + const dist = new Float32Array(SIZE); + dist.fill(maxDistance); + const heap = new MinHeap(); + for (let i = 0; i < SIZE; i++) { + if (!sourceMask[i]) continue; + dist[i] = 0; + heap.push({ i, f: 0 }); + } + while (heap.length) { + const cur = heap.pop(); + if (!cur || cur.f > dist[cur.i] + 1e-5) continue; + const x = cur.i % MAP_W; + const y = Math.floor(cur.i / MAP_W); + for (const [nx, ny] of neighbors8(x, y)) { + const ni = indexOf(nx, ny); + const step = (nx !== x && ny !== y) ? SQRT2 : 1; + const nd = cur.f + step; + if (nd >= dist[ni] || nd > maxDistance) continue; + dist[ni] = nd; + heap.push({ i: ni, f: nd }); + } + } + return dist; +} + +function ridgeContribution(px, py, ridge, seed) { + const dx = (px - ridge.x) * ASPECT; + const dy = py - ridge.y; + const { u, v } = rotate(dx, dy, ridge.angle); + const half = ridge.length * 0.5; + const along = Math.abs(u / Math.max(0.001, half)); + if (along >= 1.22) return 0; + const taper = smoothstep(1 - clamp((along - 0.68) / 0.54)); + const wobble = (valueNoise((u + ridge.phase) * 720, (py + ridge.phase) * 720, seed + ridge.seedOffset, 14) - 0.5) * ridge.width * ridge.wobble; + const cross = Math.abs(v + wobble); + const core = Math.exp(-Math.pow(cross / Math.max(0.0008, ridge.width), 2.0)); + const serration = 0.82 + 0.36 * valueNoise((px + ridge.phase) * 900, (py - ridge.phase) * 900, seed + ridge.seedOffset + 71, 7.5); + return ridge.height * core * taper * serration; +} + +function ellipticalMask(px, py, system) { + const dx = (px - system.x) * ASPECT; + const dy = py - system.y; + const { u, v } = rotate(dx, dy, system.angle); + const a = Math.max(0.01, system.length * 0.5); + const b = Math.max(0.01, system.width * 0.5); + const r = Math.sqrt((u / a) ** 2 + (v / b) ** 2); + return clamp(1 - smoothstep((r - 0.55) / 0.65)); +} + +function sampleInsideUnitDisk(seed, n) { + const r = Math.sqrt(rand(seed, n)); + const a = rand(seed, n + 1) * Math.PI * 2; + return { x: Math.cos(a) * r, y: Math.sin(a) * r, r }; +} + +export function buildTerrainTemplate(seed) { + const mountainModeRoll = rand(seed, 12); + const mountainMode = mountainModeRoll < 0.48 ? "range" : mountainModeRoll < 0.80 ? "mixed" : "massif"; + const mountainMassifness = mountainMode === "massif" ? 0.72 + rand(seed, 13) * 0.24 : mountainMode === "mixed" ? 0.34 + rand(seed, 14) * 0.36 : rand(seed, 15) * 0.24; + const coastAngle = rand(seed, 21) * Math.PI * 2; + const twoSidedCoast = rand(seed, 22) < 0.36; + const seaRatio = 0.14 + rand(seed, 23) * 0.16; + const mountainAngle = coastAngle + Math.PI * (0.26 + rand(seed, 24) * 0.48); + const baseHeight = 0.52 + rand(seed, 25) * 0.46; + const primaryLength = lerp(0.70 + rand(seed, 26) * 0.22, 0.38 + rand(seed, 27) * 0.20, mountainMassifness); + const primaryWidth = lerp(0.15 + rand(seed, 28) * 0.13, 0.36 + rand(seed, 29) * 0.20, mountainMassifness); + const scratchCount = Math.round(lerp(26 + rand(seed, 30) * 22, 18 + rand(seed, 31) * 18, mountainMassifness)); + // 脊梁山脈そのものを複数箇所に置く。旧版の secondary は主山脈の周囲に寄りすぎ、 + // 画面上では「単一の山塊」に見えやすかったため、独立した major system として扱う。 + const mountainSystemCount = 14 + Math.floor(rand(seed, 32) * 3); // 14〜16 + return { + seed, + seaRatio, + coastAngle, + twoSidedCoast, + coastNoise: 0.045 + rand(seed, 33) * 0.045, + mountainMode, + mountainMassifness, + mountainAngle, + mountainBaseHeight: baseHeight, + mountainDensity: 0.62 + rand(seed, 34) * 0.35, + primaryMountain: { + x: clamp(0.50 + (rand(seed, 35) - 0.5) * 0.28, 0.22, 0.78), + y: clamp(0.50 + (rand(seed, 36) - 0.5) * 0.28, 0.22, 0.78), + angle: mountainAngle, + length: primaryLength, + width: primaryWidth, + height: baseHeight, + scratchCount, + massifness: mountainMassifness, + }, + mountainSystemCount, + secondaryCount: mountainSystemCount - 1, + macroNoiseScale: 0.020 + rand(seed, 37) * 0.030, + macroNoiseStrength: 0.028 + rand(seed, 38) * 0.025, + scratchNoiseStrength: 0.018 + rand(seed, 39) * 0.022, + roughness: 0.40 + rand(seed, 40) * 0.50, + erosion: 0.34 + rand(seed, 41) * 0.48, + deposition: 0.28 + rand(seed, 42) * 0.56, + riverRichness: 0.72 + rand(seed, 43) * 0.60, + bigRiverChance: 0.34 + rand(seed, 44) * 0.34, + plainBias: 0.32 + rand(seed, 45) * 0.46, + }; +} + +function buildMountainSystems(template, seed) { + const systems = []; + const targetCount = Math.max(8, template.mountainSystemCount ?? 15); + const baseAngle = template.mountainAngle; + + // 複数の脊梁山脈システムを、画面中央ではなくマップ全域に分散配置する。 + // 5x3 / 4x4 に近い粗い格子へ jitter を入れ、さらに farthest-candidate で + // 既存システムから離れた候補を選ぶ。これにより「中央に単一山塊」化しにくくする。 + const cols = targetCount >= 14 ? 5 : 4; + const rows = Math.ceil(targetCount / cols); + const cellOrder = Array.from({ length: cols * rows }, (_, i) => i) + .map((v) => ({ v, key: rand(seed, 1000 + v * 17) })) + .sort((a, b) => a.key - b.key) + .map((o) => o.v); + + function gridCandidate(k, attempt) { + const cell = cellOrder[(k + attempt * 7) % cellOrder.length]; + const cx = cell % cols; + const cy = Math.floor(cell / cols); + const jitterX = (rand(seed, 1100 + k * 101 + attempt * 13) - 0.5) * 0.62; + const jitterY = (rand(seed, 1200 + k * 101 + attempt * 13) - 0.5) * 0.62; + const x = clamp((cx + 0.5 + jitterX) / cols, 0.055, 0.945); + const y = clamp((cy + 0.5 + jitterY) / rows, 0.055, 0.945); + const localTurn = (rand(seed, 1300 + k * 101 + attempt) - 0.5) * Math.PI * 0.92; + const diagonalBias = (cx / Math.max(1, cols - 1) - 0.5 + (cy / Math.max(1, rows - 1) - 0.5) * 0.35) * 0.16; + return { + x, + y, + angle: baseAngle + localTurn + diagonalBias, + }; + } + + function randomCandidate(k, attempt) { + return { + x: clamp(0.055 + rand(seed, 2000 + k * 137 + attempt * 31) * 0.89, 0.055, 0.945), + y: clamp(0.055 + rand(seed, 2100 + k * 137 + attempt * 31) * 0.89, 0.055, 0.945), + angle: baseAngle + (rand(seed, 2200 + k * 137 + attempt) - 0.5) * Math.PI * 1.05, + }; + } + + function candidateAt(k, attempt) { + return attempt < 5 ? gridCandidate(k, attempt) : randomCandidate(k, attempt); + } + + for (let k = 0; k < targetCount; k++) { + let best = candidateAt(k, 0); + let bestScore = -INF; + for (let attempt = 0; attempt < 18; attempt++) { + const c = candidateAt(k, attempt); + let minD = 999; + for (const s of systems) minD = Math.min(minD, distNorm(c.x, c.y, s.x, s.y)); + // 中央集中を避けるため、中心距離を少し加点する。ただし端に張り付きすぎないよう edge も見る。 + const edgeD = Math.min(c.x, c.y, 1 - c.x, 1 - c.y); + const centerD = distNorm(c.x, c.y, 0.5, 0.5); + const score = + minD * 1.25 + + centerD * 0.18 + + Math.min(edgeD, 0.16) * 0.22 + + rand(seed, 2300 + k * 101 + attempt) * 0.04; + if (score > bestScore) { bestScore = score; best = c; } + } + + const m = clamp(template.mountainMassifness + (rand(seed, 2400 + k) - 0.5) * 0.50); + const isMassif = m > 0.58; + const major = k < 4 || rand(seed, 2500 + k) > 0.68; + const length = lerp( + major ? 0.30 + rand(seed, 2600 + k) * 0.22 : 0.20 + rand(seed, 2610 + k) * 0.16, + major ? 0.22 + rand(seed, 2620 + k) * 0.14 : 0.16 + rand(seed, 2630 + k) * 0.12, + m + ); + const width = lerp( + major ? 0.055 + rand(seed, 2700 + k) * 0.060 : 0.040 + rand(seed, 2710 + k) * 0.045, + major ? 0.120 + rand(seed, 2720 + k) * 0.090 : 0.085 + rand(seed, 2730 + k) * 0.070, + m + ); + const height = template.mountainBaseHeight * ( + major + ? 0.34 + rand(seed, 2800 + k) * 0.24 + : 0.20 + rand(seed, 2810 + k) * 0.18 + ); + const scratchCount = Math.round(lerp( + major ? 10 + rand(seed, 2900 + k) * 10 : 6 + rand(seed, 2910 + k) * 7, + isMassif ? 8 + rand(seed, 2920 + k) * 9 : 6 + rand(seed, 2930 + k) * 7, + m + )); + + systems.push({ + x: best.x, + y: best.y, + angle: best.angle, + length, + width, + height, + scratchCount, + massifness: m, + role: major ? (k < 4 ? "primary" : "major") : "minor", + }); + } + + return systems; +} + +function buildScratchRidges(system, seed, systemId) { + const ridges = []; + const count = Math.max(6, Math.round(system.scratchCount)); + for (let i = 0; i < count; i++) { + const p = sampleInsideUnitDisk(seed + systemId * 10000, 2000 + i * 7); + const density = clamp(1 - p.r * 0.78); + const localAngle = system.massifness > 0.55 + ? system.angle + (rand(seed, 2100 + i + systemId * 331) - 0.5) * Math.PI * 1.45 + : system.angle + (rand(seed, 2100 + i + systemId * 331) - 0.5) * (0.36 + system.massifness * 0.80); + const along = p.x * system.length * 0.45; + const cross = p.y * system.width * 0.45; + const x = clamp(system.x + Math.cos(system.angle) * along / ASPECT + Math.cos(system.angle + Math.PI / 2) * cross / ASPECT, 0.03, 0.97); + const y = clamp(system.y + Math.sin(system.angle) * along + Math.sin(system.angle + Math.PI / 2) * cross, 0.03, 0.97); + const len = lerp(system.length * (0.18 + rand(seed, 2200 + i) * 0.20), system.width * (0.32 + rand(seed, 2200 + i) * 0.30), system.massifness); + const width = lerp(0.010 + rand(seed, 2300 + i) * 0.012, 0.018 + rand(seed, 2300 + i) * 0.020, system.massifness) * (0.80 + density * 0.60); + const height = system.height * (0.040 + density * 0.095 + rand(seed, 2400 + i) * 0.035); + ridges.push({ + x, y, + angle: localAngle, + length: len, + width, + height, + wobble: 1.2 + rand(seed, 2500 + i) * 2.0, + phase: rand(seed, 2600 + i) * 10, + seedOffset: 2700 + systemId * 997 + i * 37, + density, + systemId, + }); + } + return ridges; +} + +function computeCoastLower(px, py, template, seed) { + const axis = (px - 0.5) * Math.cos(template.coastAngle) * ASPECT + (py - 0.5) * Math.sin(template.coastAngle); + const wave = (fbm(px * 220, py * 220, seed + 300) - 0.5) * template.coastNoise; + const bay = (valueNoise(px * 500, py * 500, seed + 301, 22) - 0.5) * 0.055; + const sideA = smoothstep((-axis + 0.24 + wave + bay) / 0.26); + const sideB = template.twoSidedCoast ? smoothstep((axis + 0.20 - wave + bay * 0.7) / 0.27) : 0; + const pressure = Math.max(sideA, sideB); + return { pressure, signedAxis: axis }; +} + +function recomputeSlope(elevation, sea, slope) { + slope.fill(0); + for (let y = 1; y < MAP_H - 1; y++) { + for (let x = 1; x < MAP_W - 1; x++) { + const i = indexOf(x, y); + if (sea[i]) continue; + const gx = elevation[indexOf(x + 1, y)] - elevation[indexOf(x - 1, y)]; + const gy = elevation[indexOf(x, y + 1)] - elevation[indexOf(x, y - 1)]; + slope[i] = clamp(Math.hypot(gx, gy) * 8.2); + } + } +} + +function classifyWater(elevation, seaLevel, sea, ocean, lake) { + sea.fill(0); ocean.fill(0); lake.fill(0); + const water = new Uint8Array(SIZE); + for (let i = 0; i < SIZE; i++) water[i] = elevation[i] <= seaLevel ? 1 : 0; + const oceanMask = largestComponent(water, true); + const seen = new Uint8Array(SIZE); + for (let i = 0; i < SIZE; i++) { + if (!water[i] || seen[i]) continue; + const queue = [i]; + const cells = []; + seen[i] = 1; + for (let q = 0; q < queue.length; q++) { + const cur = queue[q]; + cells.push(cur); + const x = cur % MAP_W; + const y = Math.floor(cur / MAP_W); + for (const [nx, ny] of neighbors8(x, y)) { + const ni = indexOf(nx, ny); + if (!water[ni] || seen[ni]) continue; + seen[ni] = 1; + queue.push(ni); + } + } + const isOcean = cells.some((ci) => oceanMask[ci]); + if (isOcean || cells.length >= 22) { + for (const ci of cells) { + sea[ci] = 1; + if (isOcean) ocean[ci] = 1; + else lake[ci] = 1; + } + } else { + for (const ci of cells) elevation[ci] = seaLevel + 0.010; + } + } +} + +function priorityFloodFlow(elevation, sea, flowTo, filled) { + flowTo.fill(-1); + filled.set(elevation); + const visited = new Uint8Array(SIZE); + const heap = new MinHeap(); + let seedCount = 0; + for (let i = 0; i < SIZE; i++) { + if (sea[i]) { + visited[i] = 1; + heap.push({ i, f: filled[i] }); + seedCount++; + } + } + if (seedCount === 0) { + for (let i = 0; i < SIZE; i++) { + const x = i % MAP_W; + const y = Math.floor(i / MAP_W); + if (x === 0 || y === 0 || x === MAP_W - 1 || y === MAP_H - 1) { + visited[i] = 1; + heap.push({ i, f: filled[i] }); + } + } + } + while (heap.length) { + const cur = heap.pop(); + if (!cur) continue; + const cx = cur.i % MAP_W; + const cy = Math.floor(cur.i / MAP_W); + for (const [nx, ny] of neighbors8(cx, cy)) { + const ni = indexOf(nx, ny); + if (visited[ni]) continue; + visited[ni] = 1; + if (filled[ni] < filled[cur.i] + 0.00002) filled[ni] = filled[cur.i] + 0.00002; + heap.push({ i: ni, f: filled[ni] }); + } + } + + for (let y = 0; y < MAP_H; y++) { + for (let x = 0; x < MAP_W; x++) { + const i = indexOf(x, y); + if (sea[i]) continue; + let best = -1; + let bestScore = filled[i]; + for (const [nx, ny] of neighbors8(x, y)) { + const ni = indexOf(nx, ny); + const stepPenalty = (nx !== x && ny !== y) ? 0.000015 : 0; + const score = filled[ni] + stepPenalty + hash2(nx, ny, 9000) * 0.000002; + if (score < bestScore - 0.000001 || sea[ni]) { + bestScore = score; + best = ni; + if (sea[ni]) break; + } + } + flowTo[i] = best; + } + } +} + +function computeFlowAccumulation(sea, flowTo, filled, flowAccum) { + const area = new Float32Array(SIZE); + const order = []; + for (let i = 0; i < SIZE; i++) { + if (sea[i]) continue; + area[i] = 1; + order.push(i); + } + order.sort((a, b) => filled[b] - filled[a]); + for (const i of order) { + const to = flowTo[i]; + if (to >= 0 && !sea[to]) area[to] += area[i]; + } + let maxArea = 1; + for (let i = 0; i < SIZE; i++) if (!sea[i]) maxArea = Math.max(maxArea, area[i]); + for (let i = 0; i < SIZE; i++) flowAccum[i] = sea[i] ? 0 : clamp(Math.pow(area[i] / maxArea, 0.42)); + return area; +} + +function traceFlowPath(start, sea, flowTo, maxSteps = 900) { + const path = []; + const seen = new Set(); + let i = start; + for (let step = 0; step < maxSteps && i >= 0 && !seen.has(i); step++) { + seen.add(i); + const x = i % MAP_W; + const y = Math.floor(i / MAP_W); + path.push([x, y]); + if (sea[i]) break; + const next = flowTo[i]; + if (next < 0 || next === i) break; + i = next; } return path; } -export function generateTerrainAndRivers(seed) { - let prefectureMask; - let prefectureBorder; - - const { - elevation, - moisture, - slope, - sea, - ocean, - lake, - river, - floodplain, - plain, - agriculture, - ridgeField, - valleyField, - basinField, - coastalLowland, - flowAccum, - erosionField, - depositionField, - arcSpineField, - branchRidgeField, - depositionalLowland, - alluvialFanField, - deltaField, - naturalBarrierScore, - flowTo, - portSuitability, - crossingSuitability, - passSuitability, - } = createMapFields(); - - const terrainTemplate = buildTerrainTemplate(seed); - const coastAngle = terrainTemplate.coastAngle; - const coastX = Math.cos(coastAngle); - const coastY = Math.sin(coastAngle); - const coastThreshold = terrainTemplate.coastBias; - const coastStrength = 0.10 + (1 - terrainTemplate.deposition) * 0.12 + rand(seed, 13) * 0.09; - const { spines, branches } = buildSpineRidges(seed, terrainTemplate); - const detachedRanges = buildDetachedRanges(seed, terrainTemplate); - const alpinePeaks = buildAlpinePeaks(seed, terrainTemplate, detachedRanges); - const rangeBreaks = buildRangeBreaks(seed, terrainTemplate, spines); - - function coastPressureAt(x, y, wx = x, wy = y) { - const nx = x / (MAP_W - 1) - 0.5; - const ny = y / (MAP_H - 1) - 0.5; - const axis = nx * coastX + ny * coastY; - const waveA = (fbm(wx * 0.72 + 31, wy * 0.72 - 17, seed + 2222) - 0.5) * (0.05 + terrainTemplate.coastRoughness * terrainTemplate.coastSides[0].inletStrength * 0.18) + - (valueNoise(wx + 19, wy - 23, seed + 2233, 18) - 0.5) * (0.03 + terrainTemplate.coastSides[0].inletStrength * 0.10); - const waveB = (fbm(wx * 0.68 - 41, wy * 0.68 + 29, seed + 3222) - 0.5) * (0.05 + terrainTemplate.coastRoughness * terrainTemplate.coastSides[1].inletStrength * 0.18) + - (valueNoise(wx - 13, wy + 37, seed + 3233, 16) - 0.5) * (0.03 + terrainTemplate.coastSides[1].inletStrength * 0.10); - const sideA = smoothstep((axis + waveA - (0.50 - terrainTemplate.coastSides[0].penetration)) / Math.max(0.08, terrainTemplate.coastSides[0].plainWidth * 2.4)); - const sideB = smoothstep((-axis + waveB - (0.50 - terrainTemplate.coastSides[1].penetration)) / Math.max(0.08, terrainTemplate.coastSides[1].plainWidth * 2.4)); - return { sideA, sideB, pressure: Math.max(sideA, sideB), signedAxis: axis }; - } - - const seaLevel = 0.275; - - const mountainBlobs = Array.from({ length: terrainTemplate.secondaryMountainCount }, (_, i) => { - const spine = spines[i % spines.length]; - const nearSpine = rand(seed, 98 + i) < 0.72; - const edgeBias = rand(seed, 99 + i) < 0.28; - const along = (rand(seed, 100 + i) - 0.5) * spine.length * 0.95; - const side = rand(seed, 101 + i) > 0.5 ? 1 : -1; - const offset = (0.055 + rand(seed, 102 + i) * 0.22) * side; - let x = nearSpine ? spine.x + Math.cos(spine.angle) * along + Math.cos(spine.angle + Math.PI / 2) * offset : rand(seed, 103 + i); - let y = nearSpine ? spine.y + Math.sin(spine.angle) * along + Math.sin(spine.angle + Math.PI / 2) * offset : rand(seed, 104 + i); - if (edgeBias) { - const edgeSide = Math.floor(rand(seed, 105 + i) * 4); - if (edgeSide === 0) x = Math.min(x, 0.08 + rand(seed, 106 + i) * 0.10); - if (edgeSide === 1) x = Math.max(x, 0.92 - rand(seed, 107 + i) * 0.10); - if (edgeSide === 2) y = Math.min(y, 0.08 + rand(seed, 108 + i) * 0.10); - if (edgeSide === 3) y = Math.max(y, 0.92 - rand(seed, 109 + i) * 0.10); - } - const coastSide = (x - 0.5) * coastX + (y - 0.5) * coastY; - const mountainSide = coastSide >= 0 ? 1 : -1; - if (rand(seed, 110 + i) < 0.46 && Math.abs(coastSide) > 0.28 - coastThreshold * 0.35) { - x -= coastX * mountainSide * (0.05 + rand(seed, 111 + i) * 0.11); - y -= coastY * mountainSide * (0.05 + rand(seed, 112 + i) * 0.11); - } - const angle = nearSpine ? spine.angle + (rand(seed, 302 + i) - 0.5) * 0.75 : rand(seed, 303 + i) * Math.PI * 2; - const baseRadius = terrainTemplate.secondaryMountainSize * Math.min(MAP_W, MAP_H); - return { - x: clamp(x) * MAP_W, - y: clamp(y) * MAP_H, - angle, - rx: baseRadius * (0.95 + rand(seed, 300 + i) * 1.10), - ry: baseRadius * (0.34 + rand(seed, 301 + i) * 0.46), - h: terrainTemplate.secondaryMountainStrength * (0.11 + rand(seed, 400 + i) * 0.23), - }; - }); - - for (let y = 0; y < MAP_H; y++) { - for (let x = 0; x < MAP_W; x++) { - const nx = x / (MAP_W - 1) - 0.5; - const ny = y / (MAP_H - 1) - 0.5; - const i = indexOf(x, y); - - const warpX = (fbm(x * 0.62 + 180, y * 0.62 - 90, seed + 3101) - 0.5) * 13; - const warpY = (fbm(x * 0.62 - 70, y * 0.62 + 210, seed + 3201) - 0.5) * 13; - const wx = x + warpX; - const wy = y + warpY; - - let mountains = 0; - for (const blob of mountainBlobs) { - const dx = wx - blob.x; - const dy = wy - blob.y; - const ca = Math.cos(blob.angle); - const sa = Math.sin(blob.angle); - const along = (dx * ca + dy * sa) / Math.max(1, blob.rx); - const perp = (-dx * sa + dy * ca) / Math.max(1, blob.ry); - const d2 = along * along + perp * perp; - const rugged = 0.82 + valueNoise(wx * 0.18 + blob.x, wy * 0.18 - blob.y, seed + 12600, 8) * 0.42; - mountains += Math.exp(-d2 * 2.55) * blob.h * rugged; - } - - const px = wx / (MAP_W - 1); - const py = wy / (MAP_H - 1); - let spineRidges = 0; - for (let si = 0; si < spines.length; si++) spineRidges += jaggedRidgeContribution(px, py, spines[si], seed); - let branchRidges = 0; - for (const ridge of branches) branchRidges += jaggedRidgeContribution(px, py, ridge, seed); - let detachedRidges = 0; - for (const ridge of detachedRanges) detachedRidges += jaggedRidgeContribution(px, py, ridge, seed); - let alpineMassifs = 0; - for (const peak of alpinePeaks) { - const dx = px - peak.x; - const dy = py - peak.y; - const ca = Math.cos(peak.angle); - const sa = Math.sin(peak.angle); - const along = (dx * ca + dy * sa) / Math.max(0.002, peak.rx); - const perp = (-dx * sa + dy * ca) / Math.max(0.002, peak.ry); - const d2 = along * along + perp * perp; - const crag = 0.78 + valueNoise(px * 38 + peak.seedOffset, py * 38 - peak.seedOffset, seed + 12700, 5) * 0.52; - alpineMassifs += Math.exp(-d2 * 1.85) * peak.h * crag; - } - let rangeBreakField = 0; - for (const feature of rangeBreaks) rangeBreakField += elongatedFeatureContribution(px, py, feature, seed); - const ridges = Math.max(0, spineRidges + branchRidges + detachedRidges * 0.95 + alpineMassifs * 0.70 - rangeBreakField * 0.90); - - const coast = coastPressureAt(x, y, wx, wy); - const coastLower = coast.pressure; - const folded = foldedOrogenyAt(px, py, seed, terrainTemplate, coastLower); - const orogenicUplift = folded.uplift; - const orogenicRidges = folded.ridges; - const orogenicValleys = folded.valleys; - // v5 terrain lifecycle: - // 1) 全セルを海面下の海底として置く。 - // 2) 造山帯・広域隆起・尾根核を海底から持ち上げる。 - // 3) この後の流路計算で谷・扇状地・沖積平野を作る。 - const terrainLarge = fbm(wx * 0.30 + 40, wy * 0.30 - 60, seed + 710); - const terrainRegional = fbm(wx * 0.72 + 80, wy * 0.72 - 20, seed + 777); - const terrainLocal = fbm(wx * 1.55 + 17, wy * 1.55 - 31, seed + 1777); - const terrainFine = valueNoise(wx * 2.15 + 11, wy * 2.15 - 19, seed + 2444, 5.5); - const fineDissection = (Math.abs(terrainLocal - 0.5) * 0.050 + Math.abs(terrainFine - 0.5) * 0.024) * (0.62 + terrainTemplate.roughness * 0.58); - const edgeDistance = Math.min(x, y, MAP_W - 1 - x, MAP_H - 1 - y) / Math.min(MAP_W, MAP_H); - const deepEdge = 1 - smoothstep(edgeDistance / 0.15); - const backboneDX = px - terrainTemplate.backboneCenterX; - const backboneDY = py - terrainTemplate.backboneCenterY; - const backboneAlong = (backboneDX * Math.cos(terrainTemplate.spineAngle) + backboneDY * Math.sin(terrainTemplate.spineAngle)) / Math.max(0.001, terrainTemplate.backboneLength); - const backboneCross = (-backboneDX * Math.sin(terrainTemplate.spineAngle) + backboneDY * Math.cos(terrainTemplate.spineAngle)) / Math.max(0.001, terrainTemplate.backboneWidth); - const backboneCore = clamp(1 - Math.sqrt(backboneAlong * backboneAlong + backboneCross * backboneCross)); - const basin = 0.040 * (terrainRegional - 0.5) + 0.030 * (terrainLarge - 0.5) + (backboneCore - 0.5) * 0.010; - const protoHighland = clamp(orogenicUplift * 1.20 + orogenicRidges * 0.72 + spineRidges * 0.98 + branchRidges * 0.76 + detachedRidges * 0.66 + alpineMassifs * 0.72 + mountains * 0.24 - rangeBreakField * 1.35 - orogenicValleys * 0.42 + backboneCore * 0.10); - // 海域は画面端の一律沈降ではなく、海岸圧・低地性・非山地性から開く。 - const marineOpening = clamp( - coastLower * (0.95 + terrainTemplate.deposition * 0.35) + - Math.max(0, 0.42 - protoHighland) * 0.22 + - Math.max(0, -basin) * 0.20 - - backboneCore * 0.10 - ); - const protoLowland = clamp((1 - protoHighland) * 0.40 + marineOpening * 0.28 + Math.max(0, -basin) * 0.34 + orogenicValleys * 0.34); - const plainNoiseSuppression = protoLowland * terrainTemplate.plainNoiseSuppression; - const subduedTerrainLocal = lerp(terrainLocal, 0.5, plainNoiseSuppression * 0.70); - const subduedTerrainFine = lerp(terrainFine, 0.5, plainNoiseSuppression * 0.82); - const subduedDissection = fineDissection * (1 - plainNoiseSuppression * 0.86); - const seafloor = seaLevel - - (0.188 + (1 - terrainLarge) * 0.045 + (1 - terrainRegional) * 0.034 + coastLower * (0.070 + terrainTemplate.deposition * 0.035) + marineOpening * 0.045 + terrainTemplate.deposition * 0.016); - const platformEmergence = (0.050 + terrainTemplate.orographicCoverage * 0.032) * (1 - marineOpening * 0.62); - const broadEmergence = Math.pow(protoHighland, 1.02) * (0.305 + terrainTemplate.orographicCoverage * 0.130); - const ridgeEmergence = - orogenicUplift * 0.245 + - orogenicRidges * 0.225 + - spineRidges * 0.520 + - branchRidges * 0.430 + - detachedRidges * 0.310 + - alpineMassifs * 0.360 + - mountains * 0.080; - const shelfDepression = Math.max(0, -basin) * 0.032 + marineOpening * (0.070 + terrainTemplate.deposition * 0.030); - const rawElevation = - seafloor + - platformEmergence + - broadEmergence + - ridgeEmergence + - (terrainLarge - 0.5) * 0.055 + - (terrainRegional - 0.5) * 0.040 + - (subduedTerrainLocal - 0.5) * 0.036 + - (subduedTerrainFine - 0.5) * 0.014 + - subduedDissection + - basin - - shelfDepression - - rangeBreakField * (0.155 + terrainTemplate.erosion * 0.080) - - orogenicValleys * (0.035 + terrainTemplate.erosion * 0.045); - - elevation[i] = clamp(softUpperClamp(rawElevation, terrainTemplate.peakSoftStart, terrainTemplate.peakSoftCap)); - arcSpineField[i] = clamp(orogenicRidges * 1.05 + orogenicUplift * 0.58 + spineRidges * 1.65 + detachedRidges * 0.98 + alpineMassifs * 0.88); - branchRidgeField[i] = clamp(branchRidges * 1.70 + orogenicValleys * 0.18); - ridgeField[i] = clamp(arcSpineField[i] * 0.78 + branchRidgeField[i] * 0.42 + orogenicRidges * 0.48 + orogenicUplift * 0.25 + subduedDissection * 0.85 - rangeBreakField * 0.72 - orogenicValleys * 0.36); - basinField[i] = clamp(Math.max(0, -basin) * 2.6 + rangeBreakField * 1.25 + orogenicValleys * 1.00 + Math.max(0, seaLevel + 0.085 - elevation[i]) * (1.10 + terrainTemplate.deposition * 0.65)); - coastalLowland[i] = 0; - moisture[i] = clamp(0.44 * fbm(wx + 400, wy - 200, seed + 333) + 0.18 * valueNoise(wx, wy, seed + 343, 11) + 0.22 * (1 - Math.abs(ny * 1.7)) + 0.12 * deepEdge + 0.18 * (1 - clamp((elevation[i] - seaLevel) / 0.35)) - Math.max(0, elevation[i] - 0.62) * 0.22); - } - } - - for (let y = 0; y < MAP_H; y++) { - for (let x = 0; x < MAP_W; x++) { - const i = indexOf(x, y); - // v5: 海岸線は後から切るのではなく、海底からの隆起量が海面を超えた場所だけを陸にする。 - if (elevation[i] < seaLevel) sea[i] = 1; - if (sea[i]) elevation[i] = Math.min(elevation[i], seaLevel - 0.014 + hash2(x, y, seed + 2311) * 0.010); - } - } - - // Edge-connected water is ocean. Isolated water is only kept when it reads as - // a small mountain/valley lake or lagoon; oversized round basins become wet lowland. - const waterSeen = new Uint8Array(SIZE); - const oceanQueue = []; - for (let x = 0; x < MAP_W; x++) { - for (const y of [0, MAP_H - 1]) { - const i = indexOf(x, y); - if (sea[i] && !waterSeen[i]) { - waterSeen[i] = 1; - ocean[i] = 1; - oceanQueue.push(i); - } - } - } - for (let y = 0; y < MAP_H; y++) { - for (const x of [0, MAP_W - 1]) { - const i = indexOf(x, y); - if (sea[i] && !waterSeen[i]) { - waterSeen[i] = 1; - ocean[i] = 1; - oceanQueue.push(i); - } - } - } - for (let q = 0; q < oceanQueue.length; q++) { - const cur = oceanQueue[q]; - const [x, y] = [cur % MAP_W, Math.floor(cur / MAP_W)]; - for (const [nx, ny] of neighbors8(x, y)) { - const ni = indexOf(nx, ny); - if (!sea[ni] || waterSeen[ni]) continue; - waterSeen[ni] = 1; - ocean[ni] = 1; - oceanQueue.push(ni); - } - } - for (let i = 0; i < SIZE; i++) { - if (!sea[i] || waterSeen[i]) continue; - const queue = [i]; - const component = [i]; - waterSeen[i] = 1; - let sx = 0, sy = 0, perimeter = 0, ridgeSum = 0, valleySum = 0, coastTouch = 0; - for (let q = 0; q < queue.length; q++) { - const cur = queue[q]; - const x = cur % MAP_W; - const y = Math.floor(cur / MAP_W); - sx += x; - sy += y; - ridgeSum += ridgeField[cur]; - valleySum += valleyField[cur]; - for (const [nx, ny] of neighbors8(x, y)) { - const ni = indexOf(nx, ny); - if (!sea[ni]) { - perimeter++; - if (coastalLowland[ni] > 0.12 || coastPressureAt(nx, ny).pressure > 0.42) coastTouch++; - continue; - } - if (waterSeen[ni]) continue; - waterSeen[ni] = 1; - queue.push(ni); - component.push(ni); - } - } - const area = component.length; - const cx = sx / area; - const cy = sy / area; - let radiusSum = 0; - for (const ci of component) { - const x = ci % MAP_W; - const y = Math.floor(ci / MAP_W); - radiusSum += Math.hypot(x - cx, y - cy); - } - const meanRadius = radiusSum / Math.max(1, area); - const circularity = perimeter > 0 ? (4 * Math.PI * area) / (perimeter * perimeter) : 1; - const mountainLake = area <= 38 && ridgeSum / area > 0.28; - const valleyLake = area <= 70 && valleySum / area > 0.24 && circularity < 0.58; - const lagoon = area <= 110 && coastTouch / Math.max(1, perimeter) > 0.18 && circularity < 0.70; - const rareSpecial = area <= 145 && circularity < 0.52 && hash2(Math.round(cx), Math.round(cy), seed + 2401) > 0.88; - const keepLake = mountainLake || valleyLake || lagoon || rareSpecial; - for (const ci of component) { - if (keepLake) { - lake[ci] = 1; - continue; - } - sea[ci] = 0; - elevation[ci] = Math.max(seaLevel + 0.012, seaLevel + Math.min(0.055, meanRadius * 0.004) + hash2(ci, area, seed + 2402) * 0.012); - basinField[ci] = clamp(basinField[ci] + 0.42); - valleyField[ci] = clamp(valleyField[ci] + 0.18); - depositionalLowland[ci] = clamp(depositionalLowland[ci] + 0.28); - depositionField[ci] = clamp(depositionField[ci] + 0.035); - } - } - - // Seed ごとの過剰な平坦化・過剰な高原化を抑える救済正規化。 - // 海面を超えた陸だけを対象に、90/97 パーセンタイルを「山がちな島弧」の範囲へ寄せる。 - const emergedElevations = []; - for (let i = 0; i < SIZE; i++) if (!sea[i]) emergedElevations.push(elevation[i]); - emergedElevations.sort((a, b) => a - b); - if (emergedElevations.length > 100) { - const q = (p) => emergedElevations[Math.max(0, Math.min(emergedElevations.length - 1, Math.floor((emergedElevations.length - 1) * p)))]; - const q65 = q(0.65); - const q90 = q(0.90); - const q97 = q(0.97); - const targetQ90 = 0.680 + terrainTemplate.roughness * 0.070; - const targetQ97 = 0.840 + terrainTemplate.roughness * 0.095; - const scale97 = clamp((targetQ97 - seaLevel) / Math.max(0.045, q97 - seaLevel), 0.52, 1.50); - const shift90 = clamp(targetQ90 - q90, -0.30, 0.22); - for (let i = 0; i < SIZE; i++) { - if (sea[i]) continue; - const highShoulder = smoothstep((elevation[i] - q65) / Math.max(0.045, q97 - q65)); - const ridgeBoost = clamp(ridgeField[i] * 0.42 + arcSpineField[i] * 0.28 - valleyField[i] * 0.18 - coastalLowland[i] * 0.15); - let adjusted = seaLevel + (elevation[i] - seaLevel) * lerp(1, scale97, highShoulder * 0.92 + ridgeBoost * 0.22); - adjusted += shift90 * highShoulder * (0.48 + ridgeBoost * 0.30); - elevation[i] = clamp(softUpperClamp(adjusted, 0.895, targetQ97 + 0.070), seaLevel + 0.006, 0.992); - } - } - - // 海岸は「切断面」ではなく沈降・波食・堆積で丸める。 - // 強い尾根が海へ落ちる場所は残し、低い場所だけを浜堤・海岸低地へ寄せる。 - for (let y = 0; y < MAP_H; y++) { - for (let x = 0; x < MAP_W; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - let nearestSea = INF; - let nearestOcean = INF; - for (let dy = -8; dy <= 8; dy++) { - for (let dx = -8; dx <= 8; dx++) { - const nx = x + dx; - const ny = y + dy; - if (!inside(nx, ny) || !sea[indexOf(nx, ny)]) continue; - nearestSea = Math.min(nearestSea, Math.hypot(dx, dy)); - if (ocean[indexOf(nx, ny)]) nearestOcean = Math.min(nearestOcean, Math.hypot(dx, dy)); - } - } - if (nearestSea <= 8) { - const marineInfluence = smoothstep((8 - nearestSea) / 8); - const ridgeResistance = smoothstep((ridgeField[i] - 0.24) / 0.42); - const coastalShelf = seaLevel + 0.015 + nearestSea * (0.015 + terrainTemplate.deposition * 0.008) + Math.max(0, fbm(x * 1.1, y * 1.1, seed + 2350) - 0.5) * (0.010 + terrainTemplate.coastRoughness * 0.012); - const lowlandBlend = marineInfluence * (1 - ridgeResistance) * (0.42 + terrainTemplate.deposition * 0.30); - if (elevation[i] > coastalShelf) elevation[i] = lerp(elevation[i], coastalShelf, lowlandBlend); - if (nearestOcean <= 8) { - const plainReach = clamp(5.2 + terrainTemplate.deposition * 7.0, 5.5, 10.5); - coastalLowland[i] = clamp((1 - nearestOcean / plainReach) * (0.70 + terrainTemplate.deposition * 0.55) * (1 - ridgeField[i] * 0.62)); - } - } - } - } - - // Explicit alpine punctuation. The base ridge system defines broad relief, - // while these narrow, detached high points make several visually legible - // mountain groups instead of one round central mass. - for (let y = 1; y < MAP_H - 1; y++) { - for (let x = 1; x < MAP_W - 1; x++) { - const i = indexOf(x, y); - if (sea[i] || coastalLowland[i] > 0.42) continue; - const px = x / (MAP_W - 1); - const py = y / (MAP_H - 1); - let peakSignal = 0; - for (const peak of alpinePeaks) { - const dx = px - peak.x; - const dy = py - peak.y; - const ca = Math.cos(peak.angle); - const sa = Math.sin(peak.angle); - const along = (dx * ca + dy * sa) / Math.max(0.002, peak.rx); - const perp = (-dx * sa + dy * ca) / Math.max(0.002, peak.ry); - const d2 = along * along + perp * perp; - peakSignal += Math.exp(-d2 * 2.20) * peak.h; - } - if (peakSignal <= 0.026) continue; - const crag = Math.max(0, valueNoise(x * 2.4 + 73, y * 2.4 - 91, seed + 12880, 3.5) - 0.36); - const target = clamp(0.64 + peakSignal * 2.45 + crag * 0.092, seaLevel + 0.006, 0.992); - elevation[i] = Math.max(elevation[i], target); - ridgeField[i] = clamp(ridgeField[i] + peakSignal * 4.6 + crag * 0.28); - arcSpineField[i] = clamp(arcSpineField[i] + peakSignal * 3.2); - basinField[i] = Math.max(0, basinField[i] - peakSignal * 1.2); - depositionalLowland[i] = Math.max(0, depositionalLowland[i] - peakSignal * 1.5); - } - } - - for (let y = 1; y < MAP_H - 1; y++) { - for (let x = 1; x < MAP_W - 1; x++) { - const gx = elevation[indexOf(x + 1, y)] - elevation[indexOf(x - 1, y)]; - const gy = elevation[indexOf(x, y + 1)] - elevation[indexOf(x, y - 1)]; - slope[indexOf(x, y)] = clamp(Math.sqrt(gx * gx + gy * gy) * 10.5); - } - } - - const landOrder = []; - for (let y = 1; y < MAP_H - 1; y++) { - for (let x = 1; x < MAP_W - 1; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - let low = i; - let best = elevation[i] + 0.012 * hash2(x, y, seed + 2468); - let localMean = 0; - let localMax = elevation[i]; - let localMin = elevation[i]; - let nCount = 0; - for (const [nx, ny] of neighbors8(x, y)) { - const ni = indexOf(nx, ny); - const ev = elevation[ni]; - localMean += ev; - localMax = Math.max(localMax, ev); - localMin = Math.min(localMin, ev); - nCount++; - const directed = ev + 0.008 * hash2(nx, ny, seed + 2469); - if (directed < best || sea[ni]) { - best = directed; - low = ni; - } - } - if (low !== i) flowTo[i] = low; - localMean /= Math.max(1, nCount); - const hollow = Math.max(0, localMean - elevation[i]); - const relief = localMax - localMin; - valleyField[i] = clamp(hollow * 8.4 + Math.max(0, 0.42 - elevation[i]) * 0.32 + moisture[i] * 0.08 - ridgeField[i] * 0.18); - basinField[i] = clamp(basinField[i] + hollow * 2.4 + (relief < 0.055 && elevation[i] < 0.55 ? 0.18 : 0)); - flowAccum[i] = 0.82 + moisture[i] * 0.88 + valleyField[i] * 0.78 + Math.max(0, elevation[i] - seaLevel) * 0.14; - landOrder.push(i); - } - } - landOrder.sort((a, b) => elevation[b] - elevation[a]); - for (const i of landOrder) { - const to = flowTo[i]; - if (to >= 0 && to !== i) flowAccum[to] += flowAccum[i] * 0.91; - } - let maxFlowAccum = 0; - for (let i = 0; i < SIZE; i++) if (!sea[i]) maxFlowAccum = Math.max(maxFlowAccum, flowAccum[i]); - if (maxFlowAccum > 0) { - for (let i = 0; i < SIZE; i++) flowAccum[i] = clamp(flowAccum[i] / maxFlowAccum); - } - for (let i = 0; i < SIZE; i++) { - if (!sea[i]) valleyField[i] = clamp(valleyField[i] * 0.62 + Math.pow(flowAccum[i], 0.48) * 0.62); - } - - function recomputeDrainageFields({ reinforceValleys = false } = {}) { - flowTo.fill(-1); - flowAccum.fill(0); - const order = []; - for (let y = 1; y < MAP_H - 1; y++) { - for (let x = 1; x < MAP_W - 1; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - let low = i; - let best = elevation[i] + 0.010 * hash2(x, y, seed + 18440); - let localMean = 0; - let localMax = elevation[i]; - let localMin = elevation[i]; - let nCount = 0; - for (const [nx, ny] of neighbors8(x, y)) { - const ni = indexOf(nx, ny); - const ev = elevation[ni]; - localMean += ev; - localMax = Math.max(localMax, ev); - localMin = Math.min(localMin, ev); - nCount++; - const drainageAttraction = valleyField[ni] * 0.020 + erosionField[ni] * 0.030 + gullyIncisionField?.[ni] * 0.020; - const directed = ev + 0.007 * hash2(nx, ny, seed + 18441) - drainageAttraction; - if (directed < best || sea[ni]) { - best = directed; - low = ni; - } - } - if (low !== i) flowTo[i] = low; - localMean /= Math.max(1, nCount); - const hollow = Math.max(0, localMean - elevation[i]); - const relief = localMax - localMin; - if (reinforceValleys) { - valleyField[i] = clamp(valleyField[i] * 0.72 + hollow * 7.8 + Math.max(0, 0.055 - relief) * Math.max(0, elevation[i] - seaLevel - 0.10) * 1.2); - basinField[i] = clamp(basinField[i] + hollow * 1.8 + (relief < 0.040 && elevation[i] < 0.58 ? 0.10 : 0)); - } - flowAccum[i] = 0.72 + moisture[i] * 0.72 + valleyField[i] * 0.72 + Math.max(0, elevation[i] - seaLevel) * 0.10; - order.push(i); - } - } - order.sort((a, b) => elevation[b] - elevation[a]); - for (const i of order) { - const to = flowTo[i]; - if (to >= 0 && to !== i) flowAccum[to] += flowAccum[i] * 0.93; - } - let maxAccum = 0; - for (let i = 0; i < SIZE; i++) if (!sea[i]) maxAccum = Math.max(maxAccum, flowAccum[i]); - if (maxAccum > 0) { - for (let i = 0; i < SIZE; i++) if (!sea[i]) flowAccum[i] = clamp(flowAccum[i] / maxAccum); - } - } - - // v6/v7: 可視河川だけでなく、山地の無数の沢・ガリーを先に掘る。 - // これを描画用の川には使わず、地形侵食だけに使うことで「日本的な谷密度」を出す。 - function traceErosionGully(startX, startY, bonusSeed = 0) { - let x = startX; - let y = startY; - let lastDx = 0; - let lastDy = 0; - const path = []; - const seen = new Set(); - for (let step = 0; step < 92; step++) { - const i = indexOf(x, y); - if (seen.has(i) || sea[i]) break; - seen.add(i); - path.push([x, y]); - if (elevation[i] < seaLevel + 0.055 && path.length > 8) break; - let best = null; - let bestValue = INF; - const preferred = flowTo[i]; - for (const [nx, ny] of neighbors8(x, y)) { - const ni = indexOf(nx, ny); - if (seen.has(ni)) continue; - const dx = nx - x; - const dy = ny - y; - const drop = elevation[i] - elevation[ni]; - const uphill = Math.max(0, -drop); - if (!sea[ni] && uphill > 0.035 + flowAccum[i] * 0.10) continue; - const sameDirection = lastDx || lastDy ? (dx * lastDx + dy * lastDy) / Math.max(0.001, Math.hypot(dx, dy) * Math.hypot(lastDx, lastDy)) : 0; - const bend = Math.abs(dx * lastDy - dy * lastDx); - const preferredBonus = ni === preferred ? 0.80 : 0; - const value = - elevation[ni] * 0.96 + - uphill * 30.0 - - Math.max(0, drop) * 2.9 - - valleyField[ni] * 1.15 - - Math.pow(flowAccum[ni], 0.55) * 1.05 - - moisture[ni] * 0.22 - - preferredBonus - - Math.max(0, sameDirection) * 0.045 + - bend * 0.030 + - (hash2(nx, ny, seed + bonusSeed + step * 31) - 0.5) * 0.055; - if (value < bestValue) { - bestValue = value; - best = [nx, ny, dx, dy]; - } - } - if (!best) break; - x = best[0]; - y = best[1]; - lastDx = best[2]; - lastDy = best[3]; - } - return path; - } - - const gullyIncisionField = new Float32Array(SIZE); - const visibleRavineField = new Float32Array(SIZE); - const surfaceTextureField = new Float32Array(SIZE); - const gullyCandidates = []; - for (let y = 3; y < MAP_H - 3; y++) { - for (let x = 3; x < MAP_W - 3; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - const highland = clamp((elevation[i] - (seaLevel + 0.075)) / 0.42); - const highFlat = highland * clamp(1 - slope[i] * 2.7) * clamp((elevation[i] - (seaLevel + 0.170)) / 0.34) * (1 - coastalLowland[i] * 0.70); - const relief = clamp(slope[i] * 1.10 + ridgeField[i] * 0.34 + valleyField[i] * 0.22 + highFlat * 0.86); - const drainage = clamp(Math.pow(flowAccum[i], 0.36) * 0.74 + moisture[i] * 0.26 + dendriticRavineTexture(x, y, seed) * 0.18 + highFlat * 0.58); - const score = highland * relief * drainage + highFlat * 0.44 + hash2(x, y, seed + 15200) * 0.10; - if (score > 0.13) gullyCandidates.push({ x, y, score }); - } - } - // 明示的な細流路大量生成は粗い格子では効きにくく、過剰掘削の原因にもなる。 - // 細かな山肌表現は renderer 側の手続きノイズへ移し、ここでは実際の小河川は掘らない。 - const gullySources = []; - const erosionGullyPaths = []; - const valleyNetworkIncision = new Float32Array(SIZE); - const valleyFloorField = new Float32Array(SIZE); - for (const source of gullySources) { - const path = traceErosionGully(source.x, source.y, 15250 + source.x * 17 + source.y * 23); - if (path.length < 5) continue; - erosionGullyPaths.push(path); - const startIndex = indexOf(source.x, source.y); - const highFlat = clamp(1 - slope[startIndex] * 2.4) * clamp((elevation[startIndex] - (seaLevel + 0.16)) / 0.35); - const strength = terrainTemplate.gullyIncision * clamp(0.016 + slope[startIndex] * 0.026 + Math.pow(flowAccum[startIndex], 0.42) * 0.020 + valleyField[startIndex] * 0.010 + highFlat * 0.018); - addPathIncision(gullyIncisionField, path, strength, path.length > 22 ? 2 : 1); - const valleyRadius = path.length > 54 ? 3.2 : path.length > 28 ? 2.4 : 1.7; - addValleyDistanceInfluence(valleyNetworkIncision, valleyFloorField, path, strength * (0.55 + highFlat * 0.55), valleyRadius, path.length > 42 ? 0.80 : 0.48); - addPathIncision(visibleRavineField, path, strength * (8.4 + highFlat * 3.4), path.length > 34 ? 2 : 1); - addValleyDistanceInfluence(visibleRavineField, valleyFloorField, path, strength * (2.2 + highFlat * 1.8), Math.max(1.5, valleyRadius * 0.72), 0.30); - } - - - // v10 の明示的な支谷トレースは削除。粗い解像度では可視変化が薄く、 - // 処理だけが増えるため、支谷表現は後段の手続きノイズに集約する。 - - // v11: red-box style dissected relief should not be a local accident. - // Spread that branchy mountain texture across the whole mountainous massif, - // while still letting real drainage control the strongest valleys. - const massifDissectionField = new Float32Array(SIZE); - for (let y = 2; y < MAP_H - 2; y++) { - for (let x = 2; x < MAP_W - 2; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - const highland = clamp((elevation[i] - (seaLevel + 0.055)) / 0.46); - const mountainMask = highland * clamp(slope[i] * 1.35 + ridgeField[i] * 0.42 + arcSpineField[i] * 0.24 + branchRidgeField[i] * 0.18 - basinField[i] * 0.22) * (1 - coastalLowland[i] * 0.70); - if (mountainMask <= 0.04) continue; - const highFlat = clamp((elevation[i] - (seaLevel + 0.16)) / 0.34) * clamp(1 - slope[i] * 2.7) * mountainMask; - const warpX = x + (fbm(x * 0.075 + 17, y * 0.075 - 29, seed + 16201) - 0.5) * 7.8; - const warpY = y + (fbm(x * 0.075 - 53, y * 0.075 + 11, seed + 16202) - 0.5) * 7.8; - const ravA = dendriticRavineTexture(warpX * 0.70, warpY * 0.70, seed + 16203); - const ravB = dendriticRavineTexture(warpX * 1.02 + 37, warpY * 1.02 - 19, seed + 16204); - const coarse = Math.abs((fbm(warpX * 0.085 + 21, warpY * 0.085 - 8, seed + 16205) - 0.5) * 2); - const patch = clamp(0.64 + (fbm(x * 0.028 + 80, y * 0.028 - 41, seed + 16206) - 0.5) * 0.78); - const branchiness = clamp(ravA * 0.64 + ravB * 0.42 + coarse * 0.18); - const dissection = mountainMask * patch * (branchiness * (1.00 + highFlat * 0.72) + highFlat * 0.26); - if (dissection <= 0.03) continue; - massifDissectionField[i] = dissection; - visibleRavineField[i] = clamp(visibleRavineField[i] + dissection * (0.34 + highFlat * 0.18)); - surfaceTextureField[i] = clamp(surfaceTextureField[i] + dissection * (0.40 + highFlat * 0.32) + coarse * mountainMask * 0.08); - valleyField[i] = clamp(valleyField[i] + dissection * 0.22); - } - } - - - // Coarse grids cannot resolve endless tiny tributaries. Here the “fine - // ravines” are not just a paint/shading overlay: they are folded into the DEM - // itself as a small alternating cut/crest signal, then slope is recomputed. - // This keeps the detail persistent for rivers, labels and all later terrain uses. - const proceduralRavineField = new Float32Array(SIZE); - const proceduralReliefField = new Float32Array(SIZE); - for (let y = 2; y < MAP_H - 2; y++) { - for (let x = 2; x < MAP_W - 2; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - const highland = clamp((elevation[i] - (seaLevel + 0.07)) / 0.42); - const mountainMask = highland * clamp(slope[i] * 0.84 + ridgeField[i] * 0.48 + arcSpineField[i] * 0.22 - basinField[i] * 0.20) * (1 - coastalLowland[i] * 0.78); - if (mountainMask <= 0.025) continue; - const warpX = x + (fbm(x * 0.10 + 91, y * 0.10 - 47, seed + 16601) - 0.5) * 7.8; - const warpY = y + (fbm(x * 0.10 - 33, y * 0.10 + 28, seed + 16602) - 0.5) * 7.8; - const ravA = dendriticRavineTexture(warpX * 0.74, warpY * 0.74, seed + 16603); - const ravB = dendriticRavineTexture(warpX * 1.18 + 23, warpY * 1.18 - 41, seed + 16604); - const ridged = 1 - Math.abs((valueNoise(warpX * 0.92 + 17, warpY * 0.92 - 9, seed + 16605, 6.2) - 0.5) * 2); - const patch = clamp(0.66 + (fbm(x * 0.038 + 80, y * 0.038 - 51, seed + 16606) - 0.5) * 0.88); - const micro = clamp((ravA * 0.70 + ravB * 0.46 + ridged * 0.22) * patch * mountainMask); - if (micro <= 0.025) continue; - - const crestNoise = Math.max(0, ridged - 0.45) * mountainMask * patch; - const cut = micro * (0.024 + highland * 0.024 + terrainTemplate.roughness * 0.012 + terrainTemplate.macroNoiseStrength * 0.20); - const crest = crestNoise * (0.008 + highland * 0.010 + terrainTemplate.globalNoiseStrength * 0.18); - const relief = crest - cut; - proceduralRavineField[i] = micro; - proceduralReliefField[i] = relief; - elevation[i] = clamp(elevation[i] + relief, seaLevel + 0.006, 0.998); - visibleRavineField[i] = clamp(visibleRavineField[i] + micro * 0.66); - surfaceTextureField[i] = clamp(surfaceTextureField[i] + micro * 0.78 + Math.max(0, crestNoise) * 0.20); - valleyField[i] = clamp(valleyField[i] + micro * 0.20); - erosionField[i] = clamp(erosionField[i] + cut * 0.40); - } - } - for (let y = 1; y < MAP_H - 1; y++) { - for (let x = 1; x < MAP_W - 1; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - const upland = clamp((elevation[i] - seaLevel) / 0.56); - const macroA = (fbm(x * terrainTemplate.macroNoiseScale + 301, y * terrainTemplate.macroNoiseScale - 119, seed + 16701) - 0.5) * 2; - const macroB = (valueNoise(x * (terrainTemplate.macroNoiseScale * 1.9) - 87, y * (terrainTemplate.macroNoiseScale * 1.9) + 63, seed + 16702, 10.5) - 0.5) * 2; - const globalA = (fbm(x * 0.016 + 57, y * 0.016 - 21, seed + 16703) - 0.5) * 2; - const globalB = (valueNoise(x * 0.030 + 19, y * 0.030 - 44, seed + 16704, 14.0) - 0.5) * 2; - const macroMask = clamp(0.30 + upland * 0.78 - coastalLowland[i] * 0.44); - const macroRelief = (macroA * terrainTemplate.macroNoiseStrength + macroB * terrainTemplate.macroNoiseStrength * 0.62 + globalA * terrainTemplate.globalNoiseStrength + globalB * terrainTemplate.globalNoiseStrength * 0.58) * macroMask; - elevation[i] = clamp(elevation[i] + macroRelief, seaLevel + 0.006, 0.998); - visibleRavineField[i] = clamp(visibleRavineField[i] + Math.abs(macroRelief) * 6.4 * upland); - surfaceTextureField[i] = clamp(surfaceTextureField[i] + Math.abs(macroRelief) * 9.0 * macroMask); - valleyField[i] = clamp(valleyField[i] + Math.max(0, -macroRelief) * 2.4); - ridgeField[i] = clamp(ridgeField[i] + Math.max(0, macroRelief) * 1.9); - } - } - - for (let y = 1; y < MAP_H - 1; y++) { - for (let x = 1; x < MAP_W - 1; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - const gx = elevation[indexOf(x + 1, y)] - elevation[indexOf(x - 1, y)]; - const gy = elevation[indexOf(x, y + 1)] - elevation[indexOf(x, y - 1)]; - slope[i] = clamp(Math.sqrt(gx * gx + gy * gy) * 10.5); - } - } - - // First-order fluvial shaping: cut valley floors on steep/high-flow cells and - // deposit gently in coastal lowlands and basin floors. This gives visible - // river valleys without destroying the macro terrain structure. - const shapedElevation = new Float32Array(elevation); - for (let y = 1; y < MAP_H - 1; y++) { - for (let x = 1; x < MAP_W - 1; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - const flow = Math.pow(flowAccum[i], 0.58); - const incisionNoise = 0.82 + hash2(x, y, seed + 8120) * 0.36; - const firstOrderPower = smoothstep((flowAccum[i] - 0.010) / 0.095); - const highlandMask = clamp((elevation[i] - (seaLevel + 0.060)) / 0.44); - const ravineTexture = dendriticRavineTexture(x, y, seed); - const highFlat = highlandMask * clamp(1 - slope[i] * 2.9) * clamp((elevation[i] - (seaLevel + 0.170)) / 0.34) * (1 - coastalLowland[i] * 0.72); - const basinProtection = clamp(basinField[i] * (1 - slope[i] * 2.2) * (1 - ridgeField[i] * 0.65)); - const textureMask = clamp(highlandMask * (slope[i] * 1.40 + ridgeField[i] * 0.32 + valleyField[i] * 0.26 + highFlat * 0.82) * (0.40 + moisture[i] * 0.78)); - const syntheticRavine = proceduralRavineField[i]; - const textureCut = clamp(terrainTemplate.dendriticTexture * ravineTexture * textureMask * (0.0016 + terrainTemplate.erosion * 0.0022 + slope[i] * 0.006 + highFlat * 0.003)); - const gullyCut = clamp(gullyIncisionField[i] * (0.34 + slope[i] * 0.76 + highlandMask * 0.24 + highFlat * 0.26)); - const networkCut = clamp(valleyNetworkIncision[i] * (0.18 + highlandMask * 0.22 + slope[i] * 0.24)); - const massifDissection = massifDissectionField[i]; - const visualRavine = clamp(visibleRavineField[i] * (0.16 + highlandMask * 0.32 + slope[i] * 0.18) + massifDissection * (0.10 + highFlat * 0.08) + syntheticRavine * 0.26); - const plateauCut = clamp(highFlat * (ravineTexture * 0.002 + valleyNetworkIncision[i] * 0.06 + Math.pow(flowAccum[i], 0.52) * 0.0018 + massifDissection * 0.004 + syntheticRavine * 0.003) + visualRavine * 0.004); - const sourceProtection = clamp((0.060 - flowAccum[i]) / 0.060) * clamp((elevation[i] - (seaLevel + 0.08)) / 0.40); - const fluvialCore = terrainTemplate.fluvialAggression * firstOrderPower * flow * (0.018 + terrainTemplate.erosion * 0.028 + slope[i] * (0.050 + terrainTemplate.erosion * 0.050) + ridgeField[i] * (0.008 + terrainTemplate.erosion * 0.014)) * incisionNoise; - let steepValley = clamp((fluvialCore + textureCut + gullyCut + networkCut + plateauCut) * (1 - basinProtection * 0.70)); - let lateralCut = clamp((terrainTemplate.fluvialAggression * firstOrderPower * Math.pow(flowAccum[i], 0.76) * valleyField[i] * (0.014 + terrainTemplate.erosion * 0.024) + valleyFloorField[i] * (0.050 + highlandMask * 0.08) + gullyIncisionField[i] * 0.06) * (1 - basinProtection * 0.82)); - if (sourceProtection > 0) { - const protect = 1 - sourceProtection * 0.82; - steepValley *= protect; - lateralCut *= 1 - sourceProtection * 0.74; - } - const lowSettling = clamp(flow * (coastalLowland[i] * (0.018 + terrainTemplate.deposition * 0.040) + basinField[i] * (0.018 + terrainTemplate.deposition * 0.040) + (elevation[i] < 0.40 ? 0.006 + terrainTemplate.deposition * 0.018 : 0)) * (1 - slope[i] * 0.82) * (1 - ridgeField[i] * 0.45) + basinProtection * (0.006 + terrainTemplate.deposition * 0.012)); - erosionField[i] = steepValley + lateralCut + networkCut * 0.20; - depositionField[i] = lowSettling; - depositionalLowland[i] = clamp(lowSettling * 6.5 + basinField[i] * terrainTemplate.deposition * 0.28 + coastalLowland[i] * terrainTemplate.deposition * 0.34 + valleyFloorField[i] * 0.18); - valleyField[i] = clamp(valleyField[i] + ravineTexture * textureMask * 0.22 + gullyIncisionField[i] * 1.8 + valleyNetworkIncision[i] * 1.5 + visibleRavineField[i] * 1.30 + massifDissection * 0.80 + syntheticRavine * 0.42 + valleyFloorField[i] * 1.0 + steepValley * 1.4); - surfaceTextureField[i] = clamp(surfaceTextureField[i] + ravineTexture * textureMask * 0.30 + visibleRavineField[i] * 0.66 + massifDissection * 0.72 + syntheticRavine * 0.74 + gullyIncisionField[i] * 1.2); - const basinFloorGuard = seaLevel + 0.010 + basinProtection * 0.030 + coastalLowland[i] * 0.010; - const headwaterGuard = elevation[i] - (0.010 + slope[i] * 0.014 + highlandMask * 0.010 + Math.pow(flowAccum[i], 0.60) * 0.028); - const carved = elevation[i] - steepValley - lateralCut + lowSettling * (0.72 + basinProtection * 0.35); - shapedElevation[i] = clamp(Math.max(basinFloorGuard, sourceProtection > 0 ? headwaterGuard : seaLevel + 0.006, carved), seaLevel + 0.006, 1); - } - } - elevation.set(shapedElevation); - - // Final orographic pass: ensure true alpine/high-mountain cells remain after - // river incision and lowland smoothing. Uplift is confined to ridge cores and - // fades out in valley floors so drainage still reads correctly. - for (let y = 1; y < MAP_H - 1; y++) { - for (let x = 1; x < MAP_W - 1; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - const dissectionLock = clamp(valleyNetworkIncision[i] * 2.6 + gullyIncisionField[i] * 2.4 + erosionField[i] * 2.0); - const ridgeCore = clamp(arcSpineField[i] * 0.74 + branchRidgeField[i] * 0.58 + ridgeField[i] * 0.42 - valleyField[i] * 0.38 - flowAccum[i] * 0.28 - dissectionLock * 0.18); - const highBase = clamp((elevation[i] - 0.55) / 0.25); - const alpine = clamp(ridgeCore * 0.88 + highBase * 0.18 - coastalLowland[i] * 0.45 - depositionalLowland[i] * 0.36 - dissectionLock * 0.20); - if (alpine <= 0.08) continue; - const summitTexture = Math.max(0, valueNoise(x * 2.7 + 31, y * 2.7 - 41, seed + 9771, 3.0) - 0.38); - const uplift = Math.pow(alpine, 1.55) * (0.032 + terrainTemplate.roughness * 0.040 + summitTexture * 0.032); - const summitCap = 0.970 + Math.min(0.045, ridgeCore * 0.042) + summitTexture * 0.020; - elevation[i] = clamp(elevation[i] + uplift, seaLevel + 0.006, summitCap); - ridgeField[i] = clamp(ridgeField[i] + uplift * 1.15); - erosionField[i] = Math.max(0, erosionField[i] - uplift * 0.25); - } - } - - applyAlpineMicroRelief(elevation, sea, slope, ridgeField, valleyField, coastalLowland, seaLevel, seed, terrainTemplate, surfaceTextureField); - breakHighPlateaus(elevation, sea, slope, ridgeField, valleyField, flowAccum, coastalLowland, seaLevel, seed, terrainTemplate, visibleRavineField, surfaceTextureField); - - for (let y = 1; y < MAP_H - 1; y++) { - for (let x = 1; x < MAP_W - 1; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - const gx = elevation[indexOf(x + 1, y)] - elevation[indexOf(x - 1, y)]; - const gy = elevation[indexOf(x, y + 1)] - elevation[indexOf(x, y - 1)]; - slope[i] = clamp(Math.sqrt(gx * gx + gy * gy) * 10.5); - visibleRavineField[i] = clamp(visibleRavineField[i] + Math.pow(flowAccum[i], 0.56) * clamp(slope[i] * 1.5 + valleyField[i] * 0.28) * 0.24); - valleyField[i] = clamp(valleyField[i] + visibleRavineField[i] * 0.42 + erosionField[i] * 2.1 + depositionField[i] * 0.8 - ridgeField[i] * 0.06); - basinField[i] = clamp(basinField[i] + depositionField[i] * 1.6); - } - } - - // v7: after actual incision, recompute flow direction/accumulation so the - // visible river network follows the carved valleys rather than the pre-erosion surface. - recomputeDrainageFields({ reinforceValleys: true }); - - // v8: breach the most prominent enclosed basins so盆地 often acquire an outlet - // instead of remaining as unexplained closed depressions. - const sinkCandidates = []; - for (let y = 2; y < MAP_H - 2; y++) { - for (let x = 2; x < MAP_W - 2; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - if (flowTo[i] >= 0 && flowTo[i] !== i) continue; - const basinScore = basinField[i] * 1.15 + valleyField[i] * 0.55 + flowAccum[i] * 0.38 + (lake[i] ? 0.16 : 0) - coastalLowland[i] * 0.30; - if (basinScore > 0.24) sinkCandidates.push({ i, x, y, score: basinScore }); - } - } - sinkCandidates.sort((a, b) => b.score - a.score); - const usedOutletStarts = new Set(); - for (const candidate of sinkCandidates.slice(0, terrainTemplate.basinOutletCount)) { - const startIndex = candidate.i; - const startKey = `${candidate.x},${candidate.y}`; - if (usedOutletStarts.has(startKey)) continue; - let targetIndex = -1; - let bestScore = INF; - for (let j = 0; j < SIZE; j++) { - if (j === startIndex) continue; - if (sea[j]) { - const d = Math.hypot((j % MAP_W) - candidate.x, Math.floor(j / MAP_W) - candidate.y); - const score = d * 0.48 - 7.5; - if (score < bestScore) { bestScore = score; targetIndex = j; } - continue; - } - const elevDelta = elevation[j] - elevation[startIndex]; - if (elevDelta > 0.050) continue; - if (flowAccum[j] < Math.max(0.08, flowAccum[startIndex] + 0.010) && basinField[j] < 0.18) continue; - const dx = (j % MAP_W) - candidate.x; - const dy = Math.floor(j / MAP_W) - candidate.y; - const d = Math.hypot(dx, dy); - const score = d * 0.34 + Math.max(0, elevDelta) * 120 - flowAccum[j] * 18 - valleyField[j] * 4 - (lake[j] ? 1.5 : 0); - if (score < bestScore) { bestScore = score; targetIndex = j; } - } - if (targetIndex < 0) continue; - const outletPath = carveOutletChannel(elevation, sea, lake, river, valleyField, basinField, flowAccum, seaLevel, startIndex, targetIndex, seed, 22800 + candidate.x * 17 + candidate.y * 31); - if (outletPath.length > 2) { - for (const [px, py] of outletPath.slice(0, 6)) usedOutletStarts.add(`${px},${py}`); - } - } - recomputeDrainageFields({ reinforceValleys: true }); - - const sourceCandidates = []; - for (let y = 4; y < MAP_H - 4; y++) { - for (let x = 4; x < MAP_W - 4; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - const gullyHint = gullyIncisionField[i] * 1.8 + dendriticRavineTexture(x, y, seed) * 0.12; - const score = elevation[i] * 0.22 + moisture[i] * 0.25 + ridgeField[i] * 0.030 + arcSpineField[i] * 0.030 + branchRidgeField[i] * 0.018 + flowAccum[i] * 1.12 + valleyField[i] * 0.58 + gullyHint * 0.42 + basinField[i] * 0.12 + coastalLowland[i] * 0.07 + hash2(x, y, seed + 9000) * 0.05; - if (elevation[i] > 0.30 && elevation[i] < 0.94 && moisture[i] > 0.16 && (flowAccum[i] > 0.003 || valleyField[i] > 0.035 || gullyIncisionField[i] > 0.006 || slope[i] > 0.18) && ridgeField[i] < 0.98) sourceCandidates.push({ x, y, score }); - } - } - - const majorRiverCandidates = sourceCandidates - .filter((p) => { - const i = indexOf(p.x, p.y); - return elevation[i] > 0.24 && elevation[i] < 0.80 && moisture[i] > 0.18 && (flowAccum[i] > 0.070 || valleyField[i] > 0.18); - }) - .map((p) => { - const i = indexOf(p.x, p.y); - const inland = Math.min(p.x, p.y, MAP_W - 1 - p.x, MAP_H - 1 - p.y) / Math.min(MAP_W, MAP_H); - return { - ...p, - score: p.score + flowAccum[i] * 1.55 + valleyField[i] * 0.60 + inland * 0.55 - Math.abs(elevation[i] - 0.50) * 0.16, - }; - }); - const majorRiverDesired = rand(seed, 9120) < 0.30 ? 0 : (rand(seed, 9121) < 0.24 ? 2 : 1); - const majorSources = pickEntities(majorRiverCandidates, { - max: majorRiverDesired, - minDistance: 12, - threshold: 0.54, - seed: seed + 9122, - jitter: 0.01, - }); - - const sources = pickEntities(sourceCandidates, { - max: 48 + Math.floor(rand(seed, 910) * 30), - minDistance: 4, - threshold: 0.22 + rand(seed, 911) * 0.05, - seed, - }); - - function nearestWaterGoal(from) { - let bestSea = null; - let bestScore = INF; - for (let y = 0; y < MAP_H; y++) { - for (let x = 0; x < MAP_W; x++) { - const i = indexOf(x, y); - if (!sea[i]) continue; - const d = Math.hypot(x - from.x, y - from.y); - const score = d - coastalLowland[indexOf(Math.max(0, Math.min(MAP_W - 1, from.x)), Math.max(0, Math.min(MAP_H - 1, from.y)))] * 2; - if (score < bestScore) { - bestScore = score; - bestSea = { x, y }; - } - } - } - return bestSea; - } - - function riverRouteCost(x, y, cx, cy) { - const i = indexOf(x, y); - const ci = indexOf(cx, cy); - if (sea[i]) return 0.18; - const uphill = Math.max(0, elevation[i] - elevation[ci]); - const downhill = Math.max(0, elevation[ci] - elevation[i]); - if (!sea[i] && uphill > 0.070 && flowAccum[i] < flowAccum[ci] + 0.010) return INF; - return Math.max( - 0.18, - 1 + - uphill * 86 + - slope[i] * 0.38 + - elevation[i] * 0.42 - - downhill * 2.1 - - valleyField[i] * 1.24 - - flowAccum[i] * 1.18 - - moisture[i] * 0.22 - - coastalLowland[i] * 0.36 - ); - } - - function forceRiverToWater(path) { - if (!path.length) return path; - const [ex, ey] = path[path.length - 1]; - if (sea[indexOf(ex, ey)]) return path; - const goal = nearestWaterGoal({ x: ex, y: ey }); - if (!goal) return path; - const startElevation = elevation[indexOf(ex, ey)]; - const tail = aStar({ x: ex, y: ey }, goal, (x, y, cx, cy) => { - const i = indexOf(x, y); - const ci = indexOf(cx, cy); - if (!sea[i] && elevation[i] > Math.max(startElevation + 0.160, elevation[ci] + 0.090)) return INF; - return riverRouteCost(x, y, cx, cy); - }); - if (tail.length <= 2) return path; - return path.concat(tail.slice(1)); - } - - function confluenceAnglePenalty(nx, ny, dx, dy, lengthSoFar) { - if (lengthSoFar < 7 || river[indexOf(nx, ny)] < 0.24) return 0; - let best = 0.16; - const inLen = Math.hypot(dx, dy) || 1; - for (const [rx, ry] of neighbors8(nx, ny)) { - if (river[indexOf(rx, ry)] < 0.22) continue; - const rdx = rx - nx; - const rdy = ry - ny; - const cos = clamp((dx * rdx + dy * rdy) / Math.max(0.001, inLen * Math.hypot(rdx, rdy)), -1, 1); - const angle = Math.acos(cos); - const shallow = angle < 0.45 ? 0.28 : 0; - best = Math.min(best, Math.abs(angle - Math.PI * 0.62) * 0.045 + shallow); - } - return best; - } - - function traceRiverPath(startX, startY, bonusSeed = 0) { - let x = startX; - let y = startY; - let lastDx = 0; - let lastDy = 0; - const path = []; - const seen = new Set(); - let accum = 0; - - for (let step = 0; step < 600; step++) { - const i = indexOf(x, y); - if (seen.has(i)) break; - seen.add(i); - path.push([x, y]); - river[i] += 0.64 + path.length / 128 + flowAccum[i] * 0.92; - accum += river[i] + flowAccum[i]; - if (sea[i]) break; - - let best = null; - let bestValue = INF; - const currentElevation = elevation[i]; - const preferred = flowTo[i]; - - for (const [nx, ny] of neighbors8(x, y)) { - const ni = indexOf(nx, ny); - const dx = nx - x; - const dy = ny - y; - const drop = currentElevation - elevation[ni]; - const uphill = Math.max(0, -drop); - if (!sea[ni] && uphill > 0.040 && flowAccum[ni] < flowAccum[i] + 0.020) continue; - let surrounding = 0; - let surroundingCount = 0; - for (const [vx, vy] of neighbors8(nx, ny)) { - surrounding += elevation[indexOf(vx, vy)]; - surroundingCount++; - } - const valley = Math.max(0, surrounding / Math.max(1, surroundingCount) - elevation[ni]); - const sameDirection = lastDx || lastDy ? (dx * lastDx + dy * lastDy) / Math.max(0.001, Math.hypot(dx, dy) * Math.hypot(lastDx, lastDy)) : 0; - const lowlandMeander = clamp((1 - slope[i] * 2.4) * (0.35 + valleyField[i] * 0.60 + Math.pow(flowAccum[i], 0.35) * 0.42)); - const straightPenalty = Math.max(0, sameDirection) * (0.12 + terrainTemplate.meanderStrength * (0.42 + lowlandMeander * 0.35)); - const turnPenalty = sameDirection < -0.62 ? 0.10 : 0; - const sideSwing = dx * lastDy - dy * lastDx; - const bendMag = Math.abs(sideSwing); - const meanderWave = Math.sin((path.length + bonusSeed * 0.011) * (0.44 + terrainTemplate.meanderStrength * 0.28) + hash2(startX, startY, seed + bonusSeed) * Math.PI * 2); - const targetBend = Math.sign(meanderWave); - const meanderBias = targetBend !== 0 ? Math.max(0, sideSwing * targetBend) * (0.055 + terrainTemplate.meanderStrength * 0.110 + lowlandMeander * 0.070) : 0; - const antiStraight = bendMag * (0.024 + terrainTemplate.meanderStrength * 0.070 + lowlandMeander * 0.035); - const flowBonus = ni === preferred ? (0.36 + flowAccum[ni] * 0.18) : 0; - const junctionPenalty = confluenceAnglePenalty(nx, ny, dx, dy, path.length); - const noise = (hash2(nx, ny, seed + bonusSeed + step * 11) - 0.5) * 0.04; - const value = - elevation[ni] * 1.45 + - uphill * 88 - - Math.max(0, drop) * 2.05 - - valley * 1.05 - - valleyField[ni] * (2.00 + lowlandMeander * 0.28) - - flowAccum[ni] * (1.22 + lowlandMeander * 0.12) - - moisture[ni] * 0.18 - - coastalLowland[ni] * (0.38 + lowlandMeander * 0.22) - - (river[ni] > 0 ? 0.34 : 0) - - flowBonus + - slope[ni] * 0.04 + - straightPenalty + - turnPenalty + - junctionPenalty * 1.10 - - meanderBias - - antiStraight - - noise - - (sea[ni] ? 0.6 : 0); - - if (value < bestValue) { - bestValue = value; - best = [nx, ny, dx, dy]; - } - } - if (!best) break; - x = best[0]; - y = best[1]; - lastDx = best[2]; - lastDy = best[3]; - } - - const forced = forceRiverToWater(path); - if (forced.length > path.length) { - for (const [rx, ry] of forced.slice(path.length)) { - const ri = indexOf(rx, ry); - river[ri] += 0.50 + flowAccum[ri] * 0.68; - accum += river[ri] + flowAccum[ri]; - } - } - return { path: forced, accum }; - } - - function traceSmallStreamPath(startX, startY, bonusSeed = 0) { - let x = startX; - let y = startY; - let lastDx = 0; - let lastDy = 0; - const path = []; - const seen = new Set(); - for (let step = 0; step < 210; step++) { - const i = indexOf(x, y); - if (seen.has(i)) break; - seen.add(i); - path.push([x, y]); - river[i] += 0.026 + flowAccum[i] * 0.045; - if ((river[i] > 0.62 && path.length > 9) || sea[i]) break; - let best = null; - let bestValue = INF; - for (const [nx, ny] of neighbors8(x, y)) { - const ni = indexOf(nx, ny); - const dx = nx - x; - const dy = ny - y; - const drop = elevation[i] - elevation[ni]; - const sameDirection = lastDx || lastDy ? (dx * lastDx + dy * lastDy) / Math.max(0.001, Math.hypot(dx, dy) * Math.hypot(lastDx, lastDy)) : 0; - const swing = dx * lastDy - dy * lastDx; - const lowlandMeander = clamp((1 - slope[i] * 2.0) * (0.28 + valleyField[i] * 0.80 + Math.pow(flowAccum[i], 0.35) * 0.38)); - const meanderWave = Math.sin((step + bonusSeed * 0.009) * (0.52 + terrainTemplate.meanderStrength * 0.25) + hash2(startX, startY, seed + 23123) * Math.PI * 2); - const targetSwing = Math.sign(meanderWave); - const lateralBonus = targetSwing !== 0 ? Math.max(0, swing * targetSwing) * (0.038 + terrainTemplate.meanderStrength * 0.070 + lowlandMeander * 0.060) : 0; - const value = elevation[ni] * 1.10 + Math.max(0, -drop) * 17.5 - Math.max(0, drop) * 1.52 - valleyField[ni] * (1.42 + lowlandMeander * 0.18) - flowAccum[ni] * (0.74 + lowlandMeander * 0.06) - moisture[ni] * 0.16 + Math.max(0, sameDirection) * (0.062 + lowlandMeander * 0.03) - Math.abs(swing) * 0.016 - lateralBonus + (hash2(nx, ny, seed + bonusSeed + step * 13) - 0.5) * 0.090; - if (value < bestValue) { bestValue = value; best = [nx, ny, dx, dy]; } - } - if (!best) break; - x = best[0]; - y = best[1]; - lastDx = best[2]; - lastDy = best[3]; - } - return path; - } - - const riverPaths = []; - const majorRiverPathSet = new Set(); - const riverScores = []; - for (const source of majorSources) { - const { path, accum } = traceRiverPath(source.x, source.y, 2000 + source.x * 13 + source.y * 19); - if (path.length > 14) { - riverPaths.push(path); - majorRiverPathSet.add(path); - riverScores.push(path.length * 1.35 + accum * 0.32); - } - } - for (const source of sources) { - const { path, accum } = traceRiverPath(source.x, source.y, 0); - if (path.length > 6) { - riverPaths.push(path); - riverScores.push(path.length + accum * 0.18); - } - } - - const preliminaryMainRiverCells = new Set(riverPaths.slice().sort((a, b) => b.length - a.length).slice(0, 5).flatMap((path) => path.map(([x, y]) => `${x},${y}`))); - const tributarySources = pickEntities(sourceCandidates - .filter((p) => !preliminaryMainRiverCells.has(`${p.x},${p.y}`)) - .map((p) => ({ ...p, score: p.score + flowAccum[indexOf(p.x, p.y)] * 0.75 + valleyField[indexOf(p.x, p.y)] * 0.24 })), { - max: 42 + Math.floor(rand(seed, 915) * 28), - minDistance: 3, - threshold: 0.16, - seed: seed + 916, - jitter: 0.02, - }); - for (const source of tributarySources) { - const { path, accum } = traceRiverPath(source.x, source.y, 4000 + source.x * 7 + source.y * 11); - if (path.length > 8) { - riverPaths.push(path); - riverScores.push(path.length * 0.92 + accum * 0.17); - } - } - - const streamPaths = []; - const streamSources = pickEntities(sourceCandidates - .map((p) => ({ ...p, score: valleyField[indexOf(p.x, p.y)] * 0.46 + flowAccum[indexOf(p.x, p.y)] * 0.36 + moisture[indexOf(p.x, p.y)] * 0.18 + hash2(p.x, p.y, seed + 918) * 0.05 })) - .filter((p) => p.score > 0.095), { - max: 82 + Math.floor(rand(seed, 919) * 52), - minDistance: 1.5, - threshold: 0.062, - seed: seed + 919, - jitter: 0.015, - }); - for (const source of streamSources) { - const path = traceSmallStreamPath(source.x, source.y, 7000 + source.x * 5 + source.y * 17); - if (path.length > 4) streamPaths.push(path); - } - - if (riverPaths.length === 0 && sourceCandidates.length > 0) { - const fallback = sourceCandidates.slice().sort((a, b) => b.score - a.score)[0]; - let bestSea = null; - let bestSeaDist = INF; - for (let y = 0; y < MAP_H; y++) { - for (let x = 0; x < MAP_W; x++) { - if (!sea[indexOf(x, y)]) continue; - const d = Math.hypot(x - fallback.x, y - fallback.y); - if (d < bestSeaDist) { - bestSeaDist = d; - bestSea = { x, y }; - } - } - } - if (bestSea) { - const fallbackPath = aStar(fallback, bestSea, (x, y, cx, cy) => { - const i = indexOf(x, y); - const ci = indexOf(cx, cy); - if (sea[i]) return 0.25; - const uphill = Math.max(0, elevation[i] - elevation[ci]) * 24; - const downhill = Math.max(0, elevation[ci] - elevation[i]) * 1.8; - return Math.max(0.24, 1 + uphill + slope[i] * 0.7 + elevation[i] * 0.8 - downhill - Math.min(0.55, river[i] * 0.1)); - }); - if (fallbackPath.length > 6) { - let accum = 0; - for (const [x, y] of fallbackPath) { - const i = indexOf(x, y); - river[i] += 0.42; - accum += river[i]; - } - riverPaths.push(fallbackPath); - riverScores.push(fallbackPath.length + accum * 0.18); - } - } - } - - function sanitizeDownhillRiverPath(path, tolerance = 0.040) { - if (!path || path.length < 2) return path || []; - const out = [path[0]]; - for (let k = 1; k < path.length; k++) { - const [px, py] = out[out.length - 1]; - const [x, y] = path[k]; - const pi = indexOf(px, py); - const i = indexOf(x, y); - if (!sea[i] && elevation[i] > elevation[pi] + tolerance) break; - out.push(path[k]); - if (sea[i]) break; - } - return out.length >= 2 ? out : []; - } - function trimMountainHeadwaters(path) { - if (!path || path.length < 4) return path || []; - let start = 0; - while (start < path.length - 3) { - const [x, y] = path[start]; - const i = indexOf(x, y); - if (sea[i]) break; - if (elevation[i] <= 0.79 && (valleyField[i] >= 0.13 || flowAccum[i] >= 0.030)) break; - start++; - } - return path.slice(start); - } - for (let r = 0; r < riverPaths.length; r++) riverPaths[r] = sanitizeDownhillRiverPath(trimMountainHeadwaters(riverPaths[r]), 0.055); - for (let r = riverPaths.length - 1; r >= 0; r--) if (riverPaths[r].length < 2) riverPaths.splice(r, 1); - for (let r = 0; r < streamPaths.length; r++) streamPaths[r] = sanitizeDownhillRiverPath(trimMountainHeadwaters(streamPaths[r]), 0.040); - for (let r = streamPaths.length - 1; r >= 0; r--) if (streamPaths[r].length < 2) streamPaths.splice(r, 1); +function buildRiverNetwork(seed, template, sea, lake, elevation, slope, flowTo, flowAccum, river, erosionField) { river.fill(0); + const candidates = []; + for (let y = 2; y < MAP_H - 2; y++) { + for (let x = 2; x < MAP_W - 2; x++) { + const i = indexOf(x, y); + if (sea[i]) continue; + const f = flowAccum[i]; + const high = clamp((elevation[i] - 0.36) * 2.2); + const wet = valueNoise(x * 1.7, y * 1.7, seed + 12000, 24); + const score = f * 0.80 + high * 0.25 + wet * 0.16 - slope[i] * 0.10; + if (score > 0.24) candidates.push({ x, y, score }); + } + } + const desired = 44 + Math.floor(template.riverRichness * 28); + const sources = pickEntities(candidates, { max: desired, minDistance: 6, threshold: 0.26, seed: seed + 12100, jitter: 0.035 }); + const riverPaths = []; + for (const s of sources) { + const path = traceFlowPath(indexOf(s.x, s.y), sea, flowTo); + if (path.length >= 8 && path.some(([x, y], k) => k > 5 && (sea[indexOf(x, y)] || lake[indexOf(x, y)]))) riverPaths.push(path); + else if (path.length >= 14) riverPaths.push(path); + } + + const longPaths = riverPaths + .map((path) => { + let maxFlow = 0; + let meanFlow = 0; + for (const [x, y] of path) { + const f = flowAccum[indexOf(x, y)]; + maxFlow = Math.max(maxFlow, f); + meanFlow += f; + } + meanFlow /= Math.max(1, path.length); + return { path, score: path.length * 0.75 + maxFlow * 90 + meanFlow * 35 }; + }) + .sort((a, b) => b.score - a.score); + const mainCount = Math.min(longPaths.length, template.bigRiverChance > 0.56 ? 4 : 3); + const mainSet = new Set(longPaths.slice(0, mainCount).map((p) => p.path)); + + const riverThreshold = 0.26 - template.riverRichness * 0.035 - (template.bigRiverChance > 0.56 ? 0.030 : 0); + for (let i = 0; i < SIZE; i++) { + if (sea[i]) continue; + const f = flowAccum[i]; + if (f > riverThreshold) river[i] = clamp((f - riverThreshold) / (0.55 - riverThreshold)); + } for (const path of riverPaths) { - const major = majorRiverPathSet.has(path); + const main = mainSet.has(path); for (let k = 0; k < path.length; k++) { const [x, y] = path[k]; - const i = indexOf(x, y); - river[i] += major ? (0.72 + k / 118 + flowAccum[i] * 0.98) : (0.46 + k / 170 + flowAccum[i] * 0.72); - } - } - for (const path of streamPaths) { - for (let k = 0; k < path.length; k++) { - const [x, y] = path[k]; - const i = indexOf(x, y); - river[i] += 0.020 + flowAccum[i] * 0.032; - } - } - - const expandedRiver = new Float32Array(river); - for (let y = 1; y < MAP_H - 1; y++) { - for (let x = 1; x < MAP_W - 1; x++) { - const i = indexOf(x, y); - if (river[i] <= 0) continue; - for (const [nx, ny] of neighbors8(x, y)) { - expandedRiver[indexOf(nx, ny)] = Math.max(expandedRiver[indexOf(nx, ny)], river[i] * 0.26); - } - } - } - river.set(expandedRiver); - - // Second fluvial pass uses the actual traced river network. Main channels cut - // visible V-shaped valleys; lower reaches accumulate alluvial deposits. - const fluvialElevation = new Float32Array(elevation); - for (let y = 1; y < MAP_H - 1; y++) { - for (let x = 1; x < MAP_W - 1; x++) { - const i = indexOf(x, y); - if (sea[i] || river[i] <= 0.02) continue; - const r = clamp(river[i] / 2.6); - const smallPower = smoothstep((r - 0.025) / 0.16); - const mediumPower = smoothstep((r - 0.20) / 0.34); - const largePower = smoothstep((r - 0.45) / 0.42); - const actionPower = clamp(mediumPower * 0.20 + largePower * 0.44); - const headwaterProtect = clamp((0.54 - r) / 0.54) * clamp((elevation[i] - (seaLevel + 0.08)) / 0.42); - const highlandProtect = clamp((elevation[i] - 0.54) / 0.34) * clamp((0.58 - flowAccum[i]) / 0.58); - const localWallProtect = clamp((slope[i] - 0.22) * 1.8) * clamp((elevation[i] - 0.54) / 0.34); - const cutLimiter = 1 - clamp(headwaterProtect * 0.96 + highlandProtect * 0.76 + localWallProtect * 0.55); - const channelCutRaw = terrainTemplate.fluvialAggression * actionPower * Math.pow(r, 0.82) * (0.0045 + terrainTemplate.erosion * 0.0055 + slope[i] * (0.006 + terrainTemplate.erosion * 0.008) + ridgeField[i] * (0.0015 + terrainTemplate.erosion * 0.0025)); - const valleyWidenRaw = terrainTemplate.fluvialAggression * (mediumPower * 0.08 + largePower * 0.18) * Math.pow(r, 0.92) * (0.0015 + terrainTemplate.erosion * 0.0030 + Math.max(0, elevation[i] - seaLevel) * (0.0018 + terrainTemplate.erosion * 0.0030) + valleyField[i] * (0.0015 + terrainTemplate.erosion * 0.0030)); - const maxRiverCut = 0.0018 + mediumPower * 0.0032 + largePower * 0.0065 + Math.pow(flowAccum[i], 0.65) * 0.0045; - const channelCut = Math.min(maxRiverCut, clamp(channelCutRaw * cutLimiter)); - const valleyWiden = Math.min(maxRiverCut * 0.65, clamp(valleyWidenRaw * cutLimiter)); - const alluvium = clamp((mediumPower * 0.32 + largePower * 0.70) * Math.pow(r, 0.86) * (coastalLowland[i] * (0.010 + terrainTemplate.deposition * 0.030) + basinField[i] * (0.007 + terrainTemplate.deposition * 0.020) + (slope[i] < 0.10 ? 0.004 + terrainTemplate.deposition * 0.012 : 0)) * (1 - ridgeField[i] * 0.45)); - erosionField[i] = clamp(erosionField[i] + channelCut + valleyWiden); - depositionField[i] = clamp(depositionField[i] + alluvium); - depositionalLowland[i] = clamp(depositionalLowland[i] + alluvium * 5.5); - fluvialElevation[i] = clamp(elevation[i] - channelCut - valleyWiden + alluvium, seaLevel + 0.005, 1); - valleyField[i] = clamp(valleyField[i] + r * 0.28 + channelCut * 3.0); - basinField[i] = clamp(basinField[i] + alluvium * 3.2); - } - } - // No lateral terrain carving from traced river lines. Side-valley complexity is - // already present in the DEM through proceduralReliefField/proceduralRavineField. - - // Template-driven deposition is limited to plausible low-energy places: - // river mouths, basin floors, coastal plains, and slope breaks below ridges. - const depositionElevation = new Float32Array(fluvialElevation); - for (let y = 2; y < MAP_H - 2; y++) { - for (let x = 2; x < MAP_W - 2; x++) { const i = indexOf(x, y); if (sea[i]) continue; - let nearSea = 0; - let localRiver = river[i]; - let highSide = 0; - let lowSide = 1; - for (let dy = -4; dy <= 4; dy++) { - for (let dx = -4; dx <= 4; dx++) { - const nx = x + dx; - const ny = y + dy; - if (!inside(nx, ny)) continue; - const ni = indexOf(nx, ny); - const d = Math.hypot(dx, dy); - if (d > 4.25) continue; - if (sea[ni]) nearSea = Math.max(nearSea, 1 - d / 4.25); - localRiver = Math.max(localRiver, river[ni] / (1 + d * 0.5)); - highSide = Math.max(highSide, fluvialElevation[ni]); - lowSide = Math.min(lowSide, fluvialElevation[ni]); - } - } - const reliefDrop = clamp((highSide - lowSide - 0.075) * 4.5); - const lowlandPotential = clamp( - basinField[i] * 0.44 + - coastalLowland[i] * 0.52 + - Math.pow(flowAccum[i], 0.56) * 0.32 + - plain[i] * 0.18 + - localRiver * 0.16 - - ridgeField[i] * 0.48 - - slope[i] * 0.52 - - Math.max(0, fluvialElevation[i] - 0.55) * 1.35 - ); - const delta = clamp(nearSea * localRiver * coastalLowland[i] * (0.32 + terrainTemplate.deposition * 1.25) * (1 - ridgeField[i] * 0.55)); - const fan = clamp(reliefDrop * localRiver * valleyField[i] * (0.20 + terrainTemplate.deposition * 0.95) * (1 - coastalLowland[i] * 0.45)); - const lowland = clamp(lowlandPotential * terrainTemplate.deposition + delta * 0.72 + fan * 0.42); - if (lowland <= 0.01) continue; - deltaField[i] = clamp(deltaField[i] + delta); - alluvialFanField[i] = clamp(alluvialFanField[i] + fan); - depositionalLowland[i] = clamp(depositionalLowland[i] + lowland); - depositionField[i] = clamp(depositionField[i] + lowland * 0.050); - erosionField[i] = Math.max(0, erosionField[i] - lowland * 0.018); - const floor = seaLevel + 0.008 + basinField[i] * 0.012 + coastalLowland[i] * 0.010; - depositionElevation[i] = clamp(lerp(fluvialElevation[i], Math.max(floor, fluvialElevation[i] - 0.032), lowland * 0.55), seaLevel + 0.005, 1); - } - } - fluvialElevation.set(depositionElevation); - - // Restore rugged summit relief after strong river incision. This prevents highlands - // from becoming unnaturally flat or visually concave while keeping valleys cut. - for (let y = 1; y < MAP_H - 1; y++) { - for (let x = 1; x < MAP_W - 1; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - const high = clamp((fluvialElevation[i] - 0.62) / 0.26); - const summit = high * clamp(ridgeField[i] * 1.4 - flowAccum[i] * 0.8); - const rugged = (valueNoise(x * 2.1 + 19, y * 2.1 - 23, seed + 9661, 3.2) - 0.5) * 0.035; - const uplift = summit * (0.018 + Math.max(0, rugged)); - if (uplift > 0) { - fluvialElevation[i] = clamp(fluvialElevation[i] + uplift, seaLevel + 0.005, 0.985); - erosionField[i] = Math.max(0, erosionField[i] - uplift * 0.6); - } + const downstream = k / Math.max(1, path.length - 1); + const boost = main ? 0.54 + downstream * 0.42 : 0.30 + downstream * 0.22; + river[i] = clamp(Math.max(river[i], boost + flowAccum[i] * (main ? 0.70 : 0.42))); } } - // Guard against one-cell over-incision: a river cell next to a 0.8-0.9 ridge - // must not collapse to near sea level just because a routed channel crossed it. - const guardedFluvialElevation = new Float32Array(fluvialElevation); - for (let y = 1; y < MAP_H - 1; y++) { - for (let x = 1; x < MAP_W - 1; x++) { - const i = indexOf(x, y); - if (sea[i] || river[i] <= 0.10) continue; - let highNeighbor = fluvialElevation[i]; - let meanNeighbor = 0; - let nCount = 0; - for (const [nx, ny] of neighbors8(x, y)) { - const ni = indexOf(nx, ny); - if (sea[ni]) continue; - highNeighbor = Math.max(highNeighbor, fluvialElevation[ni]); - meanNeighbor += fluvialElevation[ni]; - nCount++; - } - meanNeighbor /= Math.max(1, nCount); - const maxAllowedDrop = 0.18 + clamp(river[i] / 2.6) * 0.06 + coastalLowland[i] * 0.08 + basinField[i] * 0.04; - const floorFromWall = highNeighbor - maxAllowedDrop; - const floorFromMean = meanNeighbor - 0.13; - if (highNeighbor > 0.64 && highNeighbor - fluvialElevation[i] > 0.26) { - guardedFluvialElevation[i] = Math.max(fluvialElevation[i], Math.min(highNeighbor - 0.08, Math.max(floorFromWall, floorFromMean))); - erosionField[i] = Math.max(0, erosionField[i] * 0.55); - } - } + const riverCells = []; + for (let i = 0; i < SIZE; i++) if (!sea[i] && river[i] > 0.12) riverCells.push(i); + for (const i of riverCells) { + const x = i % MAP_W; + const y = Math.floor(i / MAP_W); + const strength = river[i]; + const width = 1.15 + clamp((strength - 0.35) * 2.2) * 1.65; + const depth = (0.006 + strength * (0.014 + template.erosion * 0.018) + flowAccum[i] * 0.018) * (0.65 + clamp((elevation[i] - 0.28) * 1.4) * 0.55); + forDisk(x, y, width, (nx, ny, d) => { + const ni = indexOf(nx, ny); + if (sea[ni]) return; + const profile = Math.pow(Math.max(0, 1 - d / Math.max(0.1, width)), 2.15); + const sideGuard = d === 0 ? 1 : 0.38; + const cut = depth * profile * sideGuard; + const floor = 0.075; + elevation[ni] = Math.max(floor, elevation[ni] - cut); + erosionField[ni] = clamp(erosionField[ni] + cut * 9.0); + }); } - fluvialElevation.set(guardedFluvialElevation); - elevation.set(fluvialElevation); + const sortedPaths = longPaths.map((p) => p.path); + const mainRivers = sortedPaths.filter((p) => mainSet.has(p)).slice(0, mainCount); + const tributaryRivers = sortedPaths.filter((p) => !mainSet.has(p)).slice(0, 24); + const smallStreams = sortedPaths.slice(mainCount + 8, mainCount + 58); + return { riverPaths: sortedPaths.slice(0, 80), mainRivers, tributaryRivers, smallStreams }; +} - // Broad alluvial/coastal/basin plains. The plain score alone is not enough; - // the elevation surface must also be locally calm, otherwise every lowland - // still reads as rugged terrain. Smooth only low, wet depositional cells and - // leave ridges/headwaters untouched. - for (let pass = 0; pass < 4 + Math.round(terrainTemplate.deposition * 3); pass++) { - const nextElevation = new Float32Array(elevation); - for (let y = 2; y < MAP_H - 2; y++) { - for (let x = 2; x < MAP_W - 2; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - const lowland = clamp( - coastalLowland[i] * 0.72 + - basinField[i] * 0.54 + - depositionalLowland[i] * 0.52 + - deltaField[i] * 0.34 + - alluvialFanField[i] * 0.22 + - valleyField[i] * 0.18 + - Math.pow(flowAccum[i], 0.58) * 0.20 - - gullyIncisionField[i] * 3.0 - - ridgeField[i] * 0.62 - - Math.max(0, elevation[i] - 0.54) * 1.65 - - slope[i] * 0.74 - ); - if (lowland <= 0.12) continue; - let sum = 0; - let weight = 0; - let localMin = 1; - let localMax = 0; - for (let dy = -2; dy <= 2; dy++) { - for (let dx = -2; dx <= 2; dx++) { - const nx = x + dx; - const ny = y + dy; - const ni = indexOf(nx, ny); - if (sea[ni]) continue; - const d = Math.hypot(dx, dy); - if (d > 2.25) continue; - localMin = Math.min(localMin, elevation[ni]); - localMax = Math.max(localMax, elevation[ni]); - const compatible = clamp(1 - Math.abs(elevation[ni] - elevation[i]) / 0.11); - const w = compatible / (1 + d); - sum += elevation[ni] * w; - weight += w; - } - } - if (weight <= 0) continue; - const localMean = sum / weight; - const localRelief = localMax - localMin; - const flatBias = clamp(1 - localRelief / 0.10); - const terrace = Math.round(localMean * 42) / 42; - const target = lerp(localMean, terrace, 0.18 + flatBias * 0.24); - const flattenStrength = lowland * (0.32 + terrainTemplate.deposition * 0.24 + flatBias * 0.22); - nextElevation[i] = clamp(lerp(elevation[i], target, flattenStrength), seaLevel + 0.006, 1); - if (lowland > 0.55) { - depositionField[i] = clamp(depositionField[i] + lowland * (0.010 + terrainTemplate.deposition * 0.018)); - erosionField[i] = Math.max(0, erosionField[i] - lowland * 0.012); - } - } - } - elevation.set(nextElevation); - } +function deriveFields(seed, template, fields, seaLevel) { + const { + elevation, moisture, slope, sea, ocean, lake, river, floodplain, plain, agriculture, + ridgeField, valleyField, visibleRavineField, surfaceTextureField, basinField, + coastalLowland, flowAccum, erosionField, depositionField, arcSpineField, + branchRidgeField, depositionalLowland, alluvialFanField, deltaField, + naturalBarrierScore, portSuitability, crossingSuitability, passSuitability, + } = fields; + + recomputeSlope(elevation, sea, slope); + const waterDist = distanceField(sea, 80); + const riverMask = new Uint8Array(SIZE); + for (let i = 0; i < SIZE; i++) if (river[i] > 0.18) riverMask[i] = 1; + const riverDist = distanceField(riverMask, 40); for (let y = 1; y < MAP_H - 1; y++) { for (let x = 1; x < MAP_W - 1; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - const gx = elevation[indexOf(x + 1, y)] - elevation[indexOf(x - 1, y)]; - const gy = elevation[indexOf(x, y + 1)] - elevation[indexOf(x, y - 1)]; - slope[i] = clamp(Math.sqrt(gx * gx + gy * gy) * 11.2); - } - } - - // Re-trim visible river paths after fluvial reshaping changes local elevation. - for (let r = 0; r < riverPaths.length; r++) riverPaths[r] = sanitizeDownhillRiverPath(trimMountainHeadwaters(riverPaths[r]), 0.052); - for (let r = riverPaths.length - 1; r >= 0; r--) if (riverPaths[r].length < 2) riverPaths.splice(r, 1); - for (let r = 0; r < streamPaths.length; r++) streamPaths[r] = sanitizeDownhillRiverPath(trimMountainHeadwaters(streamPaths[r]), 0.022); - for (let r = streamPaths.length - 1; r >= 0; r--) if (streamPaths[r].length < 2) streamPaths.splice(r, 1); - - function pathKey(path) { - return path.map(([x, y]) => `${x},${y}`).join("|"); - } - - function buildPathCellSet(paths) { - const set = new Set(); - for (const path of paths) for (const [x, y] of path) set.add(`${x},${y}`); - return set; - } - - function riverPathStats(path) { - let maxRiver = 0; - let sumRiver = 0; - let maxFlow = 0; - let sumFlow = 0; - let populatedCorridor = 0; - for (let k = 0; k < path.length; k++) { - const [x, y] = path[k]; - const i = indexOf(x, y); - maxRiver = Math.max(maxRiver, river[i]); - sumRiver += river[i]; - maxFlow = Math.max(maxFlow, flowAccum[i]); - sumFlow += flowAccum[i]; - populatedCorridor += plain[i] * 0.18 + valleyField[i] * 0.28 + coastalLowland[i] * 0.10 + basinField[i] * 0.08; - } - const [lx, ly] = path[path.length - 1]; - const li = indexOf(lx, ly); - const outletToWater = Boolean(sea[li] || lake[li]); - const lowerReach = path.slice(Math.max(0, path.length - Math.min(path.length, 8))); - const lowerReachStrength = lowerReach.reduce((sum, [x, y]) => sum + river[indexOf(x, y)], 0) / Math.max(1, lowerReach.length); - const meanRiver = sumRiver / Math.max(1, path.length); - const meanFlow = sumFlow / Math.max(1, path.length); - const corridorMean = populatedCorridor / Math.max(1, path.length); - const score = - path.length * 0.92 + - maxRiver * 8.4 + - meanRiver * 4.4 + - maxFlow * 8.2 + - meanFlow * 2.8 + - lowerReachStrength * 3.2 + - corridorMean * 5.2 + - (outletToWater ? 5.0 : 0); - return { length: path.length, maxRiver, meanRiver, maxFlow, meanFlow, lowerReachStrength, corridorMean, outletToWater, score }; - } - - let rankedRivers = riverPaths - .map((path, i) => ({ path, score: riverScores[i] || 0, stats: riverPathStats(path), key: pathKey(path) })) - .filter((item) => item.path.length >= 5) - .sort((a, b) => (b.stats.score + b.score * 0.25) - (a.stats.score + a.score * 0.25)); - - let mainRivers = rankedRivers - .filter((item) => item.stats.length >= 8) - .slice(0, Math.min(8, rankedRivers.length)) - .map((item) => item.path); - - if (mainRivers.length === 0 && riverPaths.length > 0) mainRivers.push(riverPaths[0]); - if (mainRivers.length === 0) { - let start = null; - let startScore = -INF; - for (let y = 4; y < MAP_H - 4; y++) { - for (let x = 4; x < MAP_W - 4; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - const score = elevation[i] * 0.55 + moisture[i] * 0.35 - slope[i] * 0.15; - if (score > startScore) { - startScore = score; - start = { x, y }; - } - } - } - if (start) { - let goal = null; - let goalDist = INF; - for (let y = 0; y < MAP_H; y++) { - for (let x = 0; x < MAP_W; x++) { - if (!sea[indexOf(x, y)]) continue; - const d = Math.hypot(x - start.x, y - start.y); - if (d < goalDist) { - goalDist = d; - goal = { x, y }; - } - } - } - if (goal) { - const fallbackPath = aStar(start, goal, (x, y, cx, cy) => { - const i = indexOf(x, y); - const ci = indexOf(cx, cy); - if (sea[i]) return 0.2; - const uphillBias = Math.max(0, elevation[i] - elevation[ci]) * 22; - const downhillBias = Math.max(0, elevation[ci] - elevation[i]) * 1.7; - return Math.max(0.25, 1 + uphillBias + slope[i] * 0.65 + elevation[i] * 0.8 - downhillBias); - }); - if (fallbackPath.length > 4) { - riverPaths.push(fallbackPath); - mainRivers.push(fallbackPath); - for (const [x, y] of fallbackPath) river[indexOf(x, y)] += 0.55; - } - } - } - } - - // v4: 急峻な地形では自然流下トレースが短く切れる seed があるため、 - // 高地から海へ抜ける中〜大規模河川の骨格を数本だけ補完する。 - if (mainRivers.length < 4 && sourceCandidates.length > 0) { - const usedKeys = new Set(mainRivers.map((path) => pathKey(path))); - const starts = sourceCandidates.slice() - .sort((a, b) => (b.score + elevation[indexOf(b.x, b.y)] * 0.8 + valleyField[indexOf(b.x, b.y)] * 0.6) - (a.score + elevation[indexOf(a.x, a.y)] * 0.8 + valleyField[indexOf(a.x, a.y)] * 0.6)); - for (const start of starts) { - if (mainRivers.length >= 4) break; - const tooClose = mainRivers.some((path) => path.some(([px, py], k) => k % 8 === 0 && Math.hypot(px - start.x, py - start.y) < 10)); - if (tooClose) continue; - const goal = nearestWaterGoal(start); - if (!goal) continue; - const path = aStar(start, goal, (x, y, cx, cy) => { - const i = indexOf(x, y); - const ci = indexOf(cx, cy); - if (sea[i]) return 0.18; - const uphill = Math.max(0, elevation[i] - elevation[ci]); - const downhill = Math.max(0, elevation[ci] - elevation[i]); - return Math.max(0.22, 1 + uphill * 42 + slope[i] * 0.42 + elevation[i] * 0.32 - downhill * 2.4 - valleyField[i] * 1.65 - flowAccum[i] * 1.20 - moisture[i] * 0.18 - coastalLowland[i] * 0.38); - }); - if (path.length < 9) continue; - const key = pathKey(path); - if (usedKeys.has(key)) continue; - usedKeys.add(key); - mainRivers.push(path); - riverPaths.push(path); - riverScores.push(path.length * 1.05); - for (let k = 0; k < path.length; k++) { - const [rx, ry] = path[k]; - river[indexOf(rx, ry)] = Math.max(river[indexOf(rx, ry)], 0.62 + k / 180 + flowAccum[indexOf(rx, ry)] * 0.72); - } - } - } - - // v5: 最終的に主河川が内陸で途切れる場合は、海または湖まで河口部を補完する。 - // これは地形生成後の河川作用を明示的に効かせ、山地から海への侵食軸を保証するため。 - for (let r = 0; r < mainRivers.length; r++) { - const path = mainRivers[r]; - if (!path || path.length < 2) continue; - const connected = path.some(([x, y], k) => k > path.length * 0.45 && neighbors8(x, y).some(([nx, ny]) => sea[indexOf(nx, ny)] || lake[indexOf(nx, ny)])); - if (connected) continue; - const forced = forceRiverToWater(path); - if (forced.length > path.length) { - mainRivers[r] = forced; - riverPaths.push(forced); - riverScores.push(forced.length * 1.12); - for (let k = path.length; k < forced.length; k++) { - const [rx, ry] = forced[k]; - const ri = indexOf(rx, ry); - river[ri] = Math.max(river[ri], 0.70 + k / 180 + flowAccum[ri] * 0.70); - valleyField[ri] = clamp(valleyField[ri] + 0.18); - } - } - } - - const mainRiverCells = buildPathCellSet(mainRivers); - const mainRiverKeys = new Set(mainRivers.map((path) => pathKey(path))); - - function pathTouchesMain(path) { - for (const [x, y] of path) { - if (mainRiverCells.has(`${x},${y}`)) return true; - for (const [nx, ny] of neighbors8(x, y)) { - if (mainRiverCells.has(`${nx},${ny}`)) return true; - } - } - return false; - } - - rankedRivers = riverPaths - .map((path, i) => ({ path, score: riverScores[i] || 0, stats: riverPathStats(path), key: pathKey(path) })) - .filter((item) => item.path.length >= 5) - .sort((a, b) => (b.stats.score + b.score * 0.25) - (a.stats.score + a.score * 0.25)); - - const tributaryRivers = []; - const hiddenRiverPaths = []; - for (const item of rankedRivers) { - if (mainRiverKeys.has(item.key)) continue; - const joinsMain = pathTouchesMain(item.path); - const visibleMedium = - item.stats.score >= 18 && - item.stats.length >= 7 && - (joinsMain || item.stats.outletToWater || item.stats.maxRiver >= 0.95 || item.stats.lowerReachStrength >= 0.70); - if (visibleMedium) tributaryRivers.push(item.path); - else hiddenRiverPaths.push(item.path); - } - - for (const path of mainRivers) { - for (let k = 0; k < path.length; k++) { - const [x, y] = path[k]; - const i = indexOf(x, y); - river[i] = Math.max(river[i], 0.92 + k / 150 + flowAccum[i] * 0.96); - } - } - for (const path of tributaryRivers) { - for (let k = 0; k < path.length; k++) { - const [x, y] = path[k]; - const i = indexOf(x, y); - river[i] = Math.max(river[i], 0.58 + k / 195 + flowAccum[i] * 0.62); - } - } - - function traceFlowLinkedMinorStream(startX, startY, bonusSeed = 0) { - let x = startX; - let y = startY; - const path = []; - const seen = new Set(); - for (let step = 0; step < 120; step++) { - const i = indexOf(x, y); - if (sea[i] || seen.has(i)) break; - seen.add(i); - path.push([x, y]); - if (path.length > 7 && river[i] > 0.42) break; - let next = flowTo[i]; - if (next < 0 || next === i || sea[next]) break; - let best = next; - let bestScore = elevation[next] * 1.05 - flowAccum[next] * 0.85 - valleyField[next] * 1.20 - moisture[next] * 0.10; - const cx = x; - const cy = y; - // Micro-streams can braid into the closest descent when flowTo falls into a tiny sink. - for (const [nx, ny] of neighbors8(cx, cy)) { - const ni = indexOf(nx, ny); - if (sea[ni]) continue; - const uphill = Math.max(0, elevation[ni] - elevation[i]); - if (uphill > 0.024 && flowAccum[ni] < flowAccum[i] + 0.006) continue; - const score = elevation[ni] * 1.05 + uphill * 16 - flowAccum[ni] * 0.82 - valleyField[ni] * 1.22 - moisture[ni] * 0.10 + (hash2(nx, ny, seed + bonusSeed + step * 19) - 0.5) * 0.035; - if (score < bestScore) { - bestScore = score; - best = ni; - } - } - if (best < 0 || best === i) break; - x = best % MAP_W; - y = Math.floor(best / MAP_W); - } - return path; - } - - const minorCandidateCells = []; - for (let y = 3; y < MAP_H - 3; y += 1) { - for (let x = 3; x < MAP_W - 3; x += 1) { - const i = indexOf(x, y); - if (sea[i]) continue; - if (elevation[i] < 0.30 || elevation[i] > 0.96) continue; - const drainage = valleyField[i] * 0.52 + Math.pow(flowAccum[i], 0.48) * 0.38 + moisture[i] * 0.18 + slope[i] * 0.08 - ridgeField[i] * 0.10; - const stochastic = hash2(x, y, seed + 9340); - if (drainage > 0.085 && stochastic > 0.10) { - minorCandidateCells.push({ x, y, score: drainage + stochastic * 0.055 }); - } - } - } - const minorSources = pickEntities(minorCandidateCells, { - max: 90 + Math.floor(rand(seed, 9341) * 60), - minDistance: 2, - threshold: 0.070, - seed: seed + 9342, - jitter: 0.02, - }); - const derivedSmallStreams = []; - const occupiedMinorStarts = new Set(); - for (const source of minorSources) { - const startKey = `${source.x},${source.y}`; - if (occupiedMinorStarts.has(startKey)) continue; - const path = traceFlowLinkedMinorStream(source.x, source.y, 11000 + source.x * 13 + source.y * 17); - if (path.length >= 3) { - derivedSmallStreams.push(path); - for (const [x, y] of path.slice(0, 4)) occupiedMinorStarts.add(`${x},${y}`); - for (let k = 0; k < path.length; k++) { - const [x, y] = path[k]; - const i = indexOf(x, y); - river[i] = Math.max(river[i], 0.045 + Math.min(0.16, flowAccum[i] * 0.10) + Math.min(0.055, k / 1900)); - } - } - } - - const smallStreams = streamPaths.filter((path) => path.length >= 4) - .concat(hiddenRiverPaths.filter((path) => path.length >= 5)) - .concat(derivedSmallStreams); - - for (const path of smallStreams) { - if (!path || path.length < 3) continue; - const strength = path.length > 24 ? 0.020 : 0.013; - addPathIncision(visibleRavineField, path, strength, path.length > 18 ? 2 : 1); - } - - // v9: keep enclosed盆地 as habitable basins rather than over-incised pits. - for (let y = 1; y < MAP_H - 1; y++) { - for (let x = 1; x < MAP_W - 1; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - const basinFloor = clamp(basinField[i] * (1 - slope[i] * 3.0) * (1 - coastalLowland[i] * 0.7) * (1 - Math.min(1, river[i] * 0.7))); - if (basinFloor <= 0.16) continue; - elevation[i] = clamp(elevation[i] + basinFloor * 0.020, seaLevel + 0.008, 1); - depositionalLowland[i] = clamp(depositionalLowland[i] + basinFloor * 0.12); - plain[i] = clamp(plain[i] + basinFloor * 0.10); - } - } - // Final one-cell canyon guard. Apply it to all land cells, not only river - // cells, because a traced channel or earlier basin operation can leave a 0.3 - // cell directly beside a 0.9 ridge. The guard preserves broad basins/coasts but - // prevents single-cell cliff trenches. - for (let pass = 0; pass < 4; pass++) { - const guarded = new Float32Array(elevation); - for (let y = 1; y < MAP_H - 1; y++) { - for (let x = 1; x < MAP_W - 1; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - let highNeighbor = elevation[i]; - let meanNeighbor = 0; - let nCount = 0; - let seaTouch = 0; - for (const [nx, ny] of neighbors8(x, y)) { - const ni = indexOf(nx, ny); - if (sea[ni]) { seaTouch++; continue; } - highNeighbor = Math.max(highNeighbor, elevation[ni]); - meanNeighbor += elevation[ni]; - nCount++; - } - meanNeighbor /= Math.max(1, nCount); - if (highNeighbor > 0.64 && highNeighbor - elevation[i] > 0.22) { - const coastalAllowance = coastalLowland[i] * 0.10 + (seaTouch ? 0.08 : 0); - const basinAllowance = basinField[i] * 0.055; - const riverAllowance = clamp(river[i] / 2.8) * 0.035; - const allowedDrop = 0.19 + coastalAllowance + basinAllowance + riverAllowance; - guarded[i] = Math.max(elevation[i], Math.max(highNeighbor - allowedDrop, meanNeighbor - 0.085)); - valleyField[i] = clamp(valleyField[i] * 0.86); - erosionField[i] = Math.max(0, erosionField[i] * 0.40); - } - } - } - elevation.set(guarded); - } - - for (let y = 1; y < MAP_H - 1; y++) { - for (let x = 1; x < MAP_W - 1; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - const gx = elevation[indexOf(x + 1, y)] - elevation[indexOf(x - 1, y)]; - const gy = elevation[indexOf(x, y + 1)] - elevation[indexOf(x, y - 1)]; - slope[i] = clamp(Math.sqrt(gx * gx + gy * gy) * 10.5); - } - } - - prefectureMask = makePrefectureMask(seed, sea, elevation, slope, river); - prefectureBorder = extractMaskBorder(prefectureMask, sea); - const regionalPrefectures = generateRegionalPrefectures(seed, sea, elevation, slope, river, ridgeField, flowAccum, prefectureMask); - const prefectureRegionId = regionalPrefectures.regionId; - const regionalDebug = regionalPrefectures.debug; - const regionalPrefectureBorders = extractRegionBorderSegments(prefectureRegionId, sea); - - for (let y = 1; y < MAP_H - 1; y++) { - for (let x = 1; x < MAP_W - 1; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - const low = 1 - clamp((elevation[i] - 0.28) / 0.4); - const flat = 1 - slope[i]; - const valleyPlain = valleyField[i] * 0.44 + basinField[i] * 0.36 + coastalLowland[i] * 0.55 + depositionalLowland[i] * 0.34 + deltaField[i] * 0.28 + alluvialFanField[i] * 0.20; - plain[i] = clamp(low * 0.44 + flat * 0.58 + valleyPlain - ridgeField[i] * 0.28 - (elevation[i] > 0.62 ? 0.48 : 0)); - - let nearRiver = 0; - for (let dy = -4; dy <= 4; dy++) { - for (let dx = -4; dx <= 4; dx++) { - const nx = x + dx; - const ny = y + dy; - if (!inside(nx, ny)) continue; - nearRiver = Math.max(nearRiver, river[indexOf(nx, ny)] / (1 + Math.hypot(dx, dy))); - } - } - - const fan = clamp(Math.max(alluvialFanField[i], valleyField[i] * (1 - coastalLowland[i]) * (elevation[i] > 0.34 && elevation[i] < 0.58 ? 0.9 : 0.35)) * (1 - slope[i] * 0.55)); - floodplain[i] = clamp(nearRiver * plain[i] * 0.92 + coastalLowland[i] * nearRiver * 0.22 + deltaField[i] * 0.18); - agriculture[i] = clamp(plain[i] * 0.58 + fan * 0.30 + basinField[i] * 0.2 + depositionalLowland[i] * 0.24 + deltaField[i] * 0.18 + moisture[i] * 0.14 + clamp(nearRiver) * 0.32 - slope[i] * 0.34 - ridgeField[i] * 0.18 - floodplain[i] * 0.06); - } - } - - for (let y = 2; y < MAP_H - 2; y++) { - for (let x = 2; x < MAP_W - 2; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - let seaNear = 0; - let riverNear = 0; - let sheltered = 0; - - for (let dy = -5; dy <= 5; dy++) { - for (let dx = -5; dx <= 5; dx++) { - const nx = x + dx; - const ny = y + dy; - if (!inside(nx, ny)) continue; - const d = Math.hypot(dx, dy); - if (sea[indexOf(nx, ny)]) seaNear += 1 / (1 + d); - riverNear = Math.max(riverNear, river[indexOf(nx, ny)] / (1 + d)); - } - } - - for (let dy = -2; dy <= 2; dy++) { - for (let dx = -2; dx <= 2; dx++) { - const nx = x + dx; - const ny = y + dy; - if (inside(nx, ny) && !sea[indexOf(nx, ny)]) sheltered += 1; - } - } - - const isDelta = (riverNear > 0.22 && coastalLowland[i] > 0.18) || deltaField[i] > 0.16; - const bayShelter = sheltered * 0.012 + seaNear * 0.055 + coastalLowland[i] * 0.16; - portSuitability[i] = clamp(bayShelter + riverNear * 0.24 + (isDelta ? 0.22 : 0) + deltaField[i] * 0.18 + depositionalLowland[i] * 0.08 + plain[i] * 0.08 - slope[i] * 0.48 - ridgeField[i] * 0.16); - } - } - - for (let y = 3; y < MAP_H - 3; y++) { - for (let x = 3; x < MAP_W - 3; x++) { - const i = indexOf(x, y); - if (sea[i]) continue; - const r = river[i]; - if (r < 0.2 || r > 1.85) continue; - let bankPlain = 0; - for (const [nx, ny] of neighbors8(x, y)) bankPlain += plain[indexOf(nx, ny)]; - crossingSuitability[i] = clamp(r * 0.34 + (bankPlain / 8) * 0.54 + valleyField[i] * 0.18 - slope[i] * 0.55 - floodplain[i] * 0.06); - } - } - - for (let y = 4; y < MAP_H - 4; y++) { - for (let x = 4; x < MAP_W - 4; x++) { const i = indexOf(x, y); if (sea[i]) continue; const e = elevation[i]; - if (e < 0.43 || e > 0.82) continue; - const ewHigh = (elevation[indexOf(x - 3, y)] + elevation[indexOf(x + 3, y)]) / 2; - const nsHigh = (elevation[indexOf(x, y - 3)] + elevation[indexOf(x, y + 3)]) / 2; - const diagLow = Math.min( - elevation[indexOf(x - 3, y - 3)], - elevation[indexOf(x + 3, y + 3)], - elevation[indexOf(x - 3, y + 3)], - elevation[indexOf(x + 3, y - 3)] - ); - passSuitability[i] = clamp((Math.max(ewHigh, nsHigh) - e) * 2.2 + (e - diagLow) * 0.55 + valleyField[i] * 0.28 - ridgeField[i] * 0.18 - slope[i] * 0.2); + const mean4 = (elevation[indexOf(x - 1, y)] + elevation[indexOf(x + 1, y)] + elevation[indexOf(x, y - 1)] + elevation[indexOf(x, y + 1)]) * 0.25; + const relief = e - mean4; + const coast = clamp((24 - waterDist[i]) / 24) * clamp((0.43 - e) * 2.4) * clamp((0.42 - slope[i]) * 2.4); + coastalLowland[i] = coast; + const riverNear = clamp((8 - riverDist[i]) / 8); + const valley = clamp(flowAccum[i] * 0.82 + river[i] * 0.72 + Math.max(0, -relief) * 10.0 + riverNear * 0.38 - slope[i] * 0.22); + valleyField[i] = clamp(Math.max(valleyField[i] * 0.30, valley)); + const ridge = clamp(arcSpineField[i] * 0.76 + branchRidgeField[i] * 0.86 + Math.max(0, relief) * 9.0 + slope[i] * 0.30 + Math.max(0, e - 0.54) * 0.88 - valleyField[i] * 0.34); + ridgeField[i] = clamp(Math.max(ridgeField[i] * 0.30, ridge)); + basinField[i] = clamp((0.42 - slope[i]) * 1.55 + Math.max(0, -relief) * 5.0 + clamp((0.48 - e) * 1.35) - coast * 0.40 - river[i] * 0.32); + const low = clamp((0.58 - e) * 1.55); + const lowSlope = clamp((0.34 - slope[i]) * 2.8); + const riverGate = clamp(riverNear * 0.72 + flowAccum[i] * 0.82 + coast * 0.70 + basinField[i] * 0.20 - ridgeField[i] * 0.40); + plain[i] = clamp(low * lowSlope * (0.18 + template.plainBias * 0.42 + riverGate * 0.92)); + floodplain[i] = clamp(lowSlope * riverNear * (0.35 + flowAccum[i] * 0.82 + river[i] * 0.52)); + deltaField[i] = clamp(coast * river[i] * 1.4 + coast * flowAccum[i] * 0.72); + alluvialFanField[i] = clamp(riverNear * clamp(slope[i] * 2.5) * clamp((0.58 - e) * 1.7) * clamp(ridgeField[i] * 0.8 + arcSpineField[i] * 0.4)); + depositionalLowland[i] = clamp(floodplain[i] * 0.54 + deltaField[i] * 0.66 + alluvialFanField[i] * 0.42 + coast * 0.28 + basinField[i] * 0.18); + depositionField[i] = clamp(depositionalLowland[i] * (0.25 + template.deposition * 0.30)); + agriculture[i] = clamp(plain[i] * 0.72 + floodplain[i] * 0.42 + depositionalLowland[i] * 0.36 - slope[i] * 0.20); + visibleRavineField[i] = clamp(visibleRavineField[i] + valleyField[i] * 0.22 + erosionField[i] * 0.35); + surfaceTextureField[i] = clamp(surfaceTextureField[i] + slope[i] * 0.26 + visibleRavineField[i] * 0.38 + Math.max(0, relief) * 2.2); + naturalBarrierScore[i] = clamp(ridgeField[i] * 0.82 + slope[i] * 0.44 + river[i] * 0.42 + Math.max(0, e - 0.58) * 0.34 - plain[i] * 0.24); + portSuitability[i] = clamp(coast * (1 - slope[i]) * (0.50 + plain[i] * 0.32) - ridgeField[i] * 0.20); + crossingSuitability[i] = clamp((1 - slope[i]) * 0.38 + plain[i] * 0.34 + floodplain[i] * 0.24 - river[i] * 0.20 - ridgeField[i] * 0.28); + passSuitability[i] = clamp(slope[i] * 0.30 + valleyField[i] * 0.34 + clamp((0.75 - ridgeField[i]) * 0.8) + plain[i] * 0.18); + moisture[i] = clamp(0.30 + (1 - waterDist[i] / 65) * 0.36 + valleyField[i] * 0.22 + riverNear * 0.26 - Math.max(0, e - 0.55) * 0.36 + (fbm(x * 1.6, y * 1.6, seed + 15000) - 0.5) * 0.18); } } + for (let i = 0; i < SIZE; i++) { + if (!sea[i]) continue; + moisture[i] = 1; + slope[i] = 0; + river[i] = 0; + ridgeField[i] = 0; + valleyField[i] = 0; + plain[i] = 0; + agriculture[i] = 0; + coastalLowland[i] = 0; + naturalBarrierScore[i] = 0; + } +} - for (let y = 1; y < MAP_H - 1; y++) { - for (let x = 1; x < MAP_W - 1; x++) { +function enforceLandGradient(elevation, sea, seaLevel) { + // Keep extreme cliffs rare without flattening normal mountain relief. + for (let pass = 0; pass < 2; pass++) { + const next = new Float32Array(elevation); + for (let y = 1; y < MAP_H - 1; y++) { + for (let x = 1; x < MAP_W - 1; x++) { + const i = indexOf(x, y); + if (sea[i]) continue; + let minN = elevation[i]; + let maxN = elevation[i]; + for (const [nx, ny] of neighbors8(x, y)) { + const ni = indexOf(nx, ny); + if (sea[ni]) continue; + minN = Math.min(minN, elevation[ni]); + maxN = Math.max(maxN, elevation[ni]); + } + const range = maxN - minN; + if (range > 0.34) next[i] = lerp(elevation[i], (elevation[i] + minN + maxN) / 3, 0.22); + next[i] = Math.max(next[i], seaLevel + 0.006); + } + } + elevation.set(next); + } +} + +export function generateTerrainAndRivers(seed) { + const fields = createMapFields(); + fields.visibleRavineField = new Float32Array(SIZE); + fields.surfaceTextureField = new Float32Array(SIZE); + const { + elevation, moisture, slope, sea, ocean, lake, river, floodplain, plain, + agriculture, ridgeField, valleyField, visibleRavineField, surfaceTextureField, + basinField, coastalLowland, flowAccum, erosionField, depositionField, + arcSpineField, branchRidgeField, depositionalLowland, alluvialFanField, + deltaField, naturalBarrierScore, flowTo, portSuitability, crossingSuitability, + passSuitability, + } = fields; + + const terrainTemplate = buildTerrainTemplate(seed); + const systems = buildMountainSystems(terrainTemplate, seed); + const allRidges = systems.flatMap((system, id) => buildScratchRidges(system, seed, id)); + + for (let y = 0; y < MAP_H; y++) { + for (let x = 0; x < MAP_W; x++) { const i = indexOf(x, y); - if (sea[i]) continue; - const gx = Math.abs(elevation[indexOf(x + 1, y)] - elevation[indexOf(x - 1, y)]); - const gy = Math.abs(elevation[indexOf(x, y + 1)] - elevation[indexOf(x, y - 1)]); - const slopeBreak = clamp((gx + gy) * 3.2 + Math.max(0, slope[i] - 0.28) * 0.72); - const majorRiver = clamp(Math.max(0, river[i] - 0.34) * 1.45 + Math.max(0, flowAccum[i] - 0.42) * 0.58); - const basinRim = clamp(basinField[i] * Math.max(0, slope[i] - 0.16) * 1.25 + ridgeField[i] * basinField[i] * 0.32); - naturalBarrierScore[i] = clamp( - arcSpineField[i] * 0.80 + - branchRidgeField[i] * 0.62 + - ridgeField[i] * 0.54 + - majorRiver * 0.62 + - slopeBreak * 0.34 + - basinRim * 0.36 - - valleyField[i] * 0.30 - - depositionalLowland[i] * 0.42 - - coastalLowland[i] * 0.20 - - plain[i] * 0.18 - ); - } - } - - function countWaterComponents(mask, minArea = 1) { - const seen = new Uint8Array(SIZE); - let count = 0; - for (let i = 0; i < SIZE; i++) { - if (!mask[i] || seen[i]) continue; - const queue = [i]; - seen[i] = 1; - let area = 0; - for (let q = 0; q < queue.length; q++) { - const cur = queue[q]; - area++; - const x = cur % MAP_W; - const y = Math.floor(cur / MAP_W); - for (const [nx, ny] of neighbors8(x, y)) { - const ni = indexOf(nx, ny); - if (!mask[ni] || seen[ni]) continue; - seen[ni] = 1; - queue.push(ni); - } + const { px, py } = normalizeCoord(x, y); + const terrainLarge = (fbm(x * 0.65, y * 0.65, seed + 1) - 0.5) * 0.23; + const terrainRegional = (valueNoise(x * 0.8, y * 0.8, seed + 2, 42) - 0.5) * 0.16; + const { pressure: coastPressure } = computeCoastLower(px, py, terrainTemplate, seed); + let e = 0.42 + terrainLarge + terrainRegional - coastPressure * (0.22 + terrainTemplate.deposition * 0.040); + let mountainMaskMax = 0; + for (let s = 0; s < systems.length; s++) { + const system = systems[s]; + const mask = ellipticalMask(px, py, system); + mountainMaskMax = Math.max(mountainMaskMax, mask); + const broad = Math.pow(mask, lerp(2.0, 1.35, system.massifness)) * system.height * lerp(0.13, 0.25, system.massifness); + e += broad; + arcSpineField[i] = Math.max(arcSpineField[i], mask * (system.role === "minor" ? 0.42 : system.role === "primary" ? 0.86 : 0.72)); } - if (area >= minArea) count++; - } - return count; - } - - function countSmallLandIslands(maxArea = 8) { - const seen = new Uint8Array(SIZE); - let count = 0; - for (let i = 0; i < SIZE; i++) { - if (sea[i] || seen[i]) continue; - const queue = [i]; - seen[i] = 1; - let area = 0; - let touchesEdge = false; - for (let q = 0; q < queue.length; q++) { - const cur = queue[q]; - area++; - const x = cur % MAP_W; - const y = Math.floor(cur / MAP_W); - if (x === 0 || y === 0 || x === MAP_W - 1 || y === MAP_H - 1) touchesEdge = true; - for (const [nx, ny] of neighbors8(x, y)) { - const ni = indexOf(nx, ny); - if (sea[ni] || seen[ni]) continue; - seen[ni] = 1; - queue.push(ni); - } + for (const ridge of allRidges) { + const r = ridgeContribution(px, py, ridge, seed); + if (r <= 0) continue; + e += r; + branchRidgeField[i] = clamp(branchRidgeField[i] + r * 5.0); + arcSpineField[i] = clamp(Math.max(arcSpineField[i], r * 4.6)); } - if (!touchesEdge && area <= maxArea) count++; + const macro = (fbm(x * terrainTemplate.macroNoiseScale * 48, y * terrainTemplate.macroNoiseScale * 48, seed + 500) - 0.5) * 2; + const global = (valueNoise(x * 0.23, y * 0.23, seed + 501, 38) - 0.5) * 2; + const scratch = (fbm(x * 2.2, y * 2.2, seed + 502) - 0.5) * 2; + e += macro * terrainTemplate.macroNoiseStrength * (0.38 + mountainMaskMax * 0.80); + e += global * 0.020; + e += scratch * terrainTemplate.scratchNoiseStrength * mountainMaskMax; + elevation[i] = clamp(e, 0.025, 1.08); + visibleRavineField[i] = clamp(Math.abs(scratch) * mountainMaskMax * 0.30 + Math.max(0, -scratch) * mountainMaskMax * 0.40); + surfaceTextureField[i] = clamp(Math.abs(macro) * 0.12 + Math.abs(scratch) * mountainMaskMax * 0.46); + valleyField[i] = clamp(Math.max(0, -scratch) * mountainMaskMax * 0.18); + moisture[i] = clamp(0.45 + coastPressure * 0.28 - elevation[i] * 0.20 + (fbm(x * 1.1, y * 1.1, seed + 503) - 0.5) * 0.16); } - return count; } - const spineValues = [...arcSpineField].filter((_, i) => !sea[i]).sort((a, b) => b - a); - const strongSpineSample = Math.max(1, Math.floor(spineValues.length * 0.05)); - const primarySpineStrength = spineValues.slice(0, strongSpineSample).reduce((sum, value) => sum + value, 0) / strongSpineSample; - const riverConnectivityRate = mainRivers.length - ? mainRivers.filter((path) => path.some(([x, y], k) => k > path.length * 0.45 && neighbors8(x, y).some(([nx, ny]) => sea[indexOf(nx, ny)] || lake[indexOf(nx, ny)]))).length / mainRivers.length - : 0; - const depositionLowlandArea = [...depositionalLowland].filter((value, i) => !sea[i] && value > 0.24).length; + let seaLevel = quantile(elevation, terrainTemplate.seaRatio); + seaLevel = clamp(seaLevel, 0.20, 0.47); + classifyWater(elevation, seaLevel, sea, ocean, lake); + recomputeSlope(elevation, sea, slope); + + const filled = new Float32Array(SIZE); + priorityFloodFlow(elevation, sea, flowTo, filled); + computeFlowAccumulation(sea, flowTo, filled, flowAccum); + const { riverPaths, mainRivers, tributaryRivers, smallStreams } = buildRiverNetwork(seed, terrainTemplate, sea, lake, elevation, slope, flowTo, flowAccum, river, erosionField); + enforceLandGradient(elevation, sea, seaLevel); + deriveFields(seed, terrainTemplate, fields, seaLevel); + + const prefectureMask = makePrefectureMask(seed, sea, elevation, slope, river); + const regional = generateRegionalPrefectures(seed, sea, elevation, slope, river, ridgeField, flowAccum, prefectureMask); + const prefectureRegionId = regional.regionId; + const regionalDebug = regional.debug; + const regionalPrefectureBorders = extractRegionBorderSegments(prefectureRegionId, sea); + const prefectureBorder = extractMaskBorder(prefectureMask, sea); + + let landCount = 0; + let waterCount = 0; + let mountainCount = 0; + let plainCount = 0; + let primarySpineStrength = 0; + let spineSamples = 0; + for (let i = 0; i < SIZE; i++) { + if (sea[i]) { waterCount++; continue; } + landCount++; + if (elevation[i] > 0.60 || ridgeField[i] > 0.58) mountainCount++; + if (plain[i] > 0.36) plainCount++; + if (arcSpineField[i] > 0.55) { primarySpineStrength += arcSpineField[i]; spineSamples++; } + } + primarySpineStrength /= Math.max(1, spineSamples); const terrainDebug = { primarySpineStrength, - riverConnectivityRate, - smallIslandCount: countSmallLandIslands(8), - largeInlandLakeCount: countWaterComponents(Float32Array.from(lake, (value) => value ? 1 : 0), 120), - depositionLowlandArea, + riverConnectivityRate: mainRivers.length ? mainRivers.filter((path) => path.some(([x, y], k) => k > path.length * 0.45 && sea[indexOf(x, y)])).length / mainRivers.length : 0, + smallIslandCount: 0, + largeInlandLakeCount: lake.reduce((a, v) => a + v, 0) > 120 ? 1 : 0, + depositionLowlandArea: depositionalLowland.reduce((a, v, i) => a + (!sea[i] && v > 0.24 ? 1 : 0), 0), smallStreamCount: smallStreams.length, - erosionGullyCount: erosionGullyPaths.length, + erosionGullyCount: 0, branchRavineCount: 0, + simpleTerrainSystem: true, + seaRatio: waterCount / SIZE, + landCount, + mountainRatio: mountainCount / Math.max(1, landCount), + plainRatio: plainCount / Math.max(1, landCount), + mountainSystemCount: systems.length, }; return {