This commit is contained in:
33333-33333 2026-05-28 00:30:09 +09:00
commit b17be0e0d2
21 changed files with 8034 additions and 2737 deletions

View file

@ -213,7 +213,7 @@ const TERRAIN_TYPES = [
coastStyle: "inland_sea",
mountainMode: "mixed",
massifnessRange: [0.34, 0.62],
seaRatioRange: [0.20, 0.33],
seaRatioRange: [0.28, 0.43],
twoSidedChance: 0.92,
mountainOffsetRange: [0.22, 0.34],
baseHeightRange: [0.46, 0.78],
@ -226,7 +226,7 @@ const TERRAIN_TYPES = [
lengthScale: 1.00,
widthScale: 1.18,
heightScale: 0.82,
coastStrength: 1.10,
coastStrength: 1.34,
plainBiasRange: [0.26, 0.50],
riverRichnessRange: [0.58, 0.96],
bigRiverChanceRange: [0.18, 0.42],
@ -580,10 +580,12 @@ function computeCoastLower(px, py, template, seed) {
let pressure = 0;
if (template.coastStyle === "inland_sea") {
// 瀬戸内型だけは中央を横切る浅い内海を許す。出現率は地形タイプ側で管理する。
const sideA = smoothstep((-axis + 0.25 + wave + bay) / 0.26);
const sideB = smoothstep((axis + 0.23 - wave + bay * 0.7) / 0.27);
const channel = smoothstep((0.060 - Math.abs(cross + wave * 0.65 + islandNoise)) / 0.090) * 0.82;
// 瀬戸内型は旧来の大きな内海+両岸海岸線に戻す。
// 海面比率はテンプレート側で高めに保ち、微細な島ノイズではなく
// 連続した水道形状で海を増やす。
const sideA = smoothstep((-axis + 0.28 + wave + bay) / 0.28);
const sideB = smoothstep((axis + 0.26 - wave + bay * 0.7) / 0.29);
const channel = smoothstep((0.082 - Math.abs(cross + wave * 0.68 + islandNoise * 0.55)) / 0.112) * 0.98;
pressure = Math.max(sideA, sideB, channel);
} else if (template.coastStyle === "parallel_spine") {
// 東北型: 左右端または上下端に海を置く。海岸線は脊梁山脈とおおよそ平行。
@ -738,6 +740,81 @@ function computeFlowAccumulation(sea, flowTo, filled, flowAccum) {
return area;
}
function buildWatershedId(sea, flowTo, flowAccum) {
const outletKey = new Int32Array(SIZE);
outletKey.fill(-1);
const ids = new Int32Array(SIZE);
ids.fill(-1);
const outletToId = new Map();
const quant = 12;
function keyForOutlet(i) {
if (i < 0) return -1;
const x = i % MAP_W;
const y = Math.floor(i / MAP_W);
const qx = Math.floor(x / quant);
const qy = Math.floor(y / quant);
return qy * 1000 + qx;
}
function resolve(start) {
if (start < 0 || sea[start]) return -1;
if (outletKey[start] >= 0) return outletKey[start];
const chain = [];
const seen = new Set();
let i = start;
let key = -1;
for (let guard = 0; guard < SIZE && i >= 0; guard++) {
if (sea[i]) { key = keyForOutlet(i); break; }
if (outletKey[i] >= 0) { key = outletKey[i]; break; }
if (seen.has(i)) { key = keyForOutlet(i); break; }
seen.add(i);
chain.push(i);
const to = flowTo[i];
if (to < 0 || to === i) { key = keyForOutlet(i); break; }
// Major channels should be a watershed's spine, not a sequence of tiny
// drainage labels. Continue to the coast/outlet even after hitting them.
i = to;
}
if (key < 0 && chain.length) key = keyForOutlet(chain[chain.length - 1]);
for (const ci of chain) outletKey[ci] = key;
return key;
}
for (let i = 0; i < SIZE; i++) {
if (sea[i]) continue;
const key = resolve(i);
if (key < 0) continue;
if (!outletToId.has(key)) outletToId.set(key, outletToId.size);
ids[i] = outletToId.get(key);
}
// Very small coastal outlet labels create noisy slivers. Merge them into the
// strongest neighbouring watershed so natural compartments remain basin-scale.
const counts = new Int32Array(outletToId.size || 1);
for (let i = 0; i < SIZE; i++) if (ids[i] >= 0) counts[ids[i]]++;
const minArea = 18;
for (let pass = 0; pass < 2; pass++) {
for (let y = 0; y < MAP_H; y++) {
for (let x = 0; x < MAP_W; x++) {
const i = indexOf(x, y);
const id = ids[i];
if (id < 0 || counts[id] >= minArea) continue;
const choices = new Map();
for (const [nx, ny] of [[x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]]) {
if (!inside(nx, ny)) continue;
const nid = ids[indexOf(nx, ny)];
if (nid >= 0 && nid !== id) choices.set(nid, (choices.get(nid) || 0) + 1 + (flowAccum[indexOf(nx, ny)] || 0));
}
let best = -1, bestScore = -1;
for (const [nid, score] of choices) if (score > bestScore) { bestScore = score; best = nid; }
if (best >= 0) { counts[id]--; counts[best]++; ids[i] = best; }
}
}
}
return ids;
}
function traceFlowPath(start, sea, flowTo, maxSteps = 900) {
const path = [];
const seen = new Set();
@ -1129,6 +1206,9 @@ export function generateTerrainAndRivers(seed) {
const lowHillNoise = clamp((fbm(x * 0.95 + 17, y * 0.95 - 23, seed + 571) - 0.38) * 2.9);
const lowHillMask = clamp(0.34 + mountainMaskMax * 0.72 + lowHillNoise * 0.50 - coastPressure * 0.12);
e += lowHillMask * 0.145;
// Do not add the previous fine speckle uplift here: it created too many
// tiny islets. Sea amount is controlled by seaRatio/coast pressure.
e -= clamp((coastPressure - 0.42) * 1.35) * 0.026;
const high = Math.max(0, e - 0.62);
e -= high * 0.42;
}
@ -1150,6 +1230,7 @@ export function generateTerrainAndRivers(seed) {
const filled = new Float32Array(SIZE);
priorityFloodFlow(elevation, sea, flowTo, filled);
computeFlowAccumulation(sea, flowTo, filled, flowAccum);
const watershedId = buildWatershedId(sea, flowTo, 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);
@ -1162,7 +1243,7 @@ export function generateTerrainAndRivers(seed) {
const natural = buildNaturalCompartments(
landMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum,
null, plain, agriculture, zeroDensity, zeroLanduse,
{ seed: seed + 17003, targetCompartmentCount: clamp(Math.round((SIZE - sea.reduce((sum, value) => sum + value, 0)) / 45), 70, 360) }
{ seed: seed + 17003, watershedId, targetCompartmentCount: clamp(Math.round((SIZE - sea.reduce((sum, value) => sum + value, 0)) / 45), 70, 360) }
);
const sharedNaturalBarrierScore = natural.naturalBarrierScore || naturalBarrierScore;
const prefectureBorder = extractMaskBorder(prefectureMask, sea);
@ -1221,6 +1302,7 @@ export function generateTerrainAndRivers(seed) {
basinField,
coastalLowland,
flowAccum,
watershedId,
erosionField,
depositionField,
arcSpineField,